fix: 协议签署业务操作授权与按钮权限一致 #342

Merged
limeng merged 1 commits from fix/limeng-agreement-operation-permission into main 2026-10-15 17:38:18 +08:00
8 changed files with 307 additions and 13 deletions

View File

@ -13,7 +13,7 @@ use App\Support\CourierAgreementSignatureService;
use App\Support\CourierQuoteConfirmation;
use App\Support\CourierQuoteSignatureService;
use App\Support\SupplierCooperationTemplates;
use App\Utilities\SystemAccess;
use App\Utilities\AgreementSigningPermissions;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
@ -224,7 +224,7 @@ class CourierAgreementSignatures extends Controller
*/
public function storeSubject(Request $request): JsonResponse
{
$this->ensureCanManage();
$this->ensureCanAdminister();
if ($response = $this->subjectsTableUnavailable()) {
return $response;
@ -251,7 +251,7 @@ class CourierAgreementSignatures extends Controller
public function updateSubject(Request $request, CourierSigningSubject $subject): JsonResponse
{
$this->ensureCanManage();
$this->ensureCanAdminister();
$this->ensureSubjectBelongsToCurrentCompany($subject);
if ($response = $this->subjectsTableUnavailable()) {
@ -272,7 +272,7 @@ class CourierAgreementSignatures extends Controller
public function setDefaultSubject(CourierSigningSubject $subject): JsonResponse
{
$this->ensureCanManage();
$this->ensureCanAdminister();
$this->ensureSubjectBelongsToCurrentCompany($subject);
if ($response = $this->subjectsTableUnavailable()) {
@ -299,7 +299,7 @@ class CourierAgreementSignatures extends Controller
public function destroySubject(Request $request, CourierSigningSubject $subject): JsonResponse
{
$this->ensureCanManage();
$this->ensureCanAdminister();
$this->ensureSubjectBelongsToCurrentCompany($subject);
if ($response = $this->subjectsTableUnavailable()) {
@ -371,7 +371,7 @@ class CourierAgreementSignatures extends Controller
private function deleteTask(Request $request, CourierAgreementSignature|CourierQuoteSignature $task): JsonResponse|RedirectResponse
{
$this->ensureCanManage();
$this->ensureCanAdminister();
abort_unless($task->company_id === (int) company_id(), 404);
$request->validate(['delete_confirmed' => ['required', 'accepted']]);
@ -608,12 +608,17 @@ class CourierAgreementSignatures extends Controller
private function ensureCanManage(): void
{
abort_unless(
in_array(SystemAccess::roleName(user()), [SystemAccess::SUPER_ADMIN, SystemAccess::ADMIN], true),
AgreementSigningPermissions::canOperate(),
403,
'当前账号没有协议签署管理权限。',
'当前账号没有协议签署操作权限,请联系管理员授权。',
);
}
private function ensureCanAdminister(): void
{
abort_unless(AgreementSigningPermissions::canAdminister(), 403, '当前账号没有协议签署管理权限。');
}
private function subjectData(Request $request): array
{
$data = $request->validate([

View File

@ -5,6 +5,7 @@ namespace App\Http\Controllers\SystemSettings;
use App\Models\Auth\Role;
use App\Models\Auth\User;
use App\Models\Auth\UserAccessGroup;
use App\Utilities\AgreementSigningPermissions;
use App\Utilities\SystemAccess;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
@ -66,6 +67,9 @@ class Users extends Controller
$created->roles()->attach($role->id);
$created->companies()->attach(company_id());
if (array_key_exists('agreement_signing_allowed', $data)) {
AgreementSigningPermissions::setGranted($created, (bool) $data['agreement_signing_allowed']);
}
});
flash('用户已新增。')->success();
@ -122,6 +126,9 @@ class Users extends Controller
$managedUser->update($attributes);
$managedUser->syncRoles([$role->id]);
if (array_key_exists('agreement_signing_allowed', $data)) {
AgreementSigningPermissions::setGranted($managedUser, (bool) $data['agreement_signing_allowed']);
}
});
$managedUser->flushCache();
@ -182,6 +189,7 @@ class Users extends Controller
'role' => ['required', Rule::in($assignable)],
'password' => [$managedUser ? 'nullable' : 'required', 'string', 'min:8', 'confirmed'],
'enabled' => ['nullable', 'boolean'],
'agreement_signing_allowed' => ['sometimes', 'boolean'],
]);
}

View File

@ -0,0 +1,58 @@
<?php
namespace App\Utilities;
use App\Models\Auth\User;
class AgreementSigningPermissions
{
public const SETTING_KEY = 'system.agreement_signing_user_ids';
public static function grantedUserIds(): array
{
$ids = setting(self::SETTING_KEY, []);
if (is_string($ids)) {
$ids = json_decode($ids, true) ?: [];
}
return array_values(array_unique(array_map('intval', is_array($ids) ? $ids : [])));
}
public static function setGranted(User $target, bool $granted): void
{
$ids = array_values(array_diff(self::grantedUserIds(), [$target->id]));
if ($granted) {
$ids[] = (int) $target->id;
}
setting()->set(self::SETTING_KEY, json_encode($ids));
setting()->save();
}
public static function canOperate(?User $actor = null): bool
{
$actor ??= user();
if (! $actor || ! $actor->enabled) {
return false;
}
$role = SystemAccess::roleName($actor);
$granted = in_array($role, [SystemAccess::SUPER_ADMIN, SystemAccess::ADMIN], true)
|| ($role === SystemAccess::USER && in_array((int) $actor->id, self::grantedUserIds(), true));
if (! $granted || ! $actor->companies()->where('companies.id', company_id())->exists()) {
return false;
}
$section = collect(SidebarPermissions::navigationFor($actor))->firstWhere('slug', 'courier-payables');
return collect($section['pages'] ?? [])
->flatMap(fn (array $page): array => [$page, ...($page['children'] ?? [])])
->contains('slug', 'agreement-signing');
}
public static function canAdminister(?User $actor = null): bool
{
$actor ??= user();
return $actor && SystemAccess::canOpenSettings($actor) && self::canOperate($actor);
}
}

View File

@ -13,6 +13,8 @@
@php($agreementCompanyName = $agreementDefaultSubject?->name ?: company()?->name ?: '当前公司')
@php($agreementSubjectPayload = $agreementSubjects->map(fn ($subject) => ['id' => $subject->id, 'name' => $subject->name, 'unified_social_credit_code' => $subject->unified_social_credit_code, 'is_default' => (bool) $subject->is_default])->values())
@php($esignConfigured = \App\Services\Esign\EsignClient::configured())
@php($agreementCanOperate = \App\Utilities\AgreementSigningPermissions::canOperate())
@php($agreementCanAdminister = \App\Utilities\AgreementSigningPermissions::canAdminister())
@php($agreementSearch = trim((string) request('agreement_search', '')))
@php($agreementStatus = trim((string) request('agreement_status', '')))
@php($agreementType = trim((string) request('agreement_type', '')))
@ -1344,6 +1346,10 @@
</span>
</div>
@unless ($agreementCanOperate)
<p class="agreement-help" role="status" data-signing-permission-hint>当前账号仅可查看协议签署,尚未获签署操作授权,请联系管理员。</p>
@endunless
<section class="agreement-panel" id="agreement-panel-tasks" role="tabpanel" aria-labelledby="agreement-tab-tasks" data-agreement-panel="tasks">
@php($scopeQuery = request()->except(['agreement_scope', 'agreement_status', 'agreement_task_page', 'agreement_quote_page']))
<nav class="agreement-scope-bar" aria-label="任务归档视图">
@ -1444,6 +1450,7 @@
<td>{{ $agreementCompanyName }}</td>
<td>{{ optional($quoteSignature->updated_at)->format('Y-m-d H:i') }}</td>
<td class="agreement-table-actions">
@if ($agreementCanOperate)
@if ($quoteSignature->esign_sign_flow_id && ! in_array($quoteSignature->status, ['archived', 'rejected', 'expired', 'revoked'], true))
<form method="POST" action="{{ route('business.courier-agreement-signatures.quotes.synchronize', $quoteSignature) }}">
@csrf
@ -1459,12 +1466,17 @@
@if ($quoteSignature->certificate_path)
<a class="agreement-icon-button" href="{{ route('business.courier-agreement-signatures.quotes.download', [$quoteSignature, 'certificate']) }}" title="下载签署完成证书"><span class="material-icons-outlined" aria-hidden="true">workspace_premium</span></a>
@endif
@if ($agreementCanAdminister)
<form method="POST" action="{{ route('business.courier-agreement-signatures.quotes.destroy', $quoteSignature) }}" data-delete-task-form data-task-number="{{ $quoteVersion?->version_code ?? ('报价版本 #' . $quoteSignature->quote_version_id) }}" data-task-description="{{ $quoteVersion?->carrier_name ?? $quoteSignature->organization_name }} · 报价确认单 · {{ $quoteSignature->statusLabel() }}">
@csrf
@method('DELETE')
<input type="hidden" name="delete_confirmed" value="1">
<button class="agreement-button agreement-button--compact agreement-button--danger" type="button" data-delete-task-open title="删除签署任务"><span class="material-icons-outlined" aria-hidden="true">delete_outline</span>删除</button>
</form>
@endif
@else
<span class="agreement-help">仅可查看</span>
@endif
</td>
</tr>
@endforeach
@ -1489,6 +1501,7 @@
<td>{{ $task->initiatorName() }}</td>
<td>{{ optional($task->updated_at)->format('Y-m-d H:i') }}</td>
<td class="agreement-table-actions">
@if ($agreementCanOperate)
@if ($task->esign_sign_flow_id && ! in_array($task->status, ['archived', 'rejected', 'expired', 'revoked'], true))
<form method="POST" action="{{ route('business.courier-agreement-signatures.synchronize', $task) }}">
@csrf
@ -1519,12 +1532,17 @@
<a class="agreement-button agreement-button--compact" href="{{ route('business.courier-agreement-signatures.download', [$task, 'credential-' . $credentialKey . '-signed']) }}">{{ $credential['name'] }}已签署</a>
@endif
@endforeach
@if ($agreementCanAdminister)
<form method="POST" action="{{ route('business.courier-agreement-signatures.destroy', $task) }}" data-delete-task-form data-task-number="{{ $task->business_no }}" data-task-description="{{ $task->courier_name }} · {{ $task->documentLabel() }} · {{ $task->statusLabel() }}">
@csrf
@method('DELETE')
<input type="hidden" name="delete_confirmed" value="1">
<button class="agreement-button agreement-button--compact agreement-button--danger" type="button" data-delete-task-open title="删除签署任务"><span class="material-icons-outlined" aria-hidden="true">delete_outline</span>删除</button>
</form>
@endif
@else
<span class="agreement-help">仅可查看</span>
@endif
</td>
</tr>
@endforeach
@ -1642,7 +1660,7 @@
<div class="agreement-field">
<div class="agreement-field-label-row">
<label for="agreement-signing-subject">我方签署主体 <span class="agreement-required" aria-hidden="true">*</span></label>
<button class="agreement-inline-button" type="button" data-subject-manage-open>
<button class="agreement-inline-button" type="button" data-subject-manage-open @disabled(! $agreementCanAdminister) title="{{ $agreementCanAdminister ? '管理公司主体' : '公司主体维护仅限管理员' }}">
<span class="material-icons-outlined" aria-hidden="true">business</span>
<span>管理主体</span>
</button>
@ -1960,7 +1978,7 @@
<footer class="agreement-request-actions">
<button class="agreement-button" type="button" data-request-cancel><span class="material-icons-outlined" aria-hidden="true">close</span><span>取消</span></button>
<button class="agreement-button" type="button" data-preview-refresh><span class="material-icons-outlined" aria-hidden="true">refresh</span><span>刷新预览</span></button>
<button class="agreement-button agreement-button--primary" type="submit" data-sign-request-send {{ $esignConfigured ? '' : 'disabled' }} title="{{ $esignConfigured ? '推送至E签宝' : 'E签宝沙箱应用尚未配置' }}"><span class="material-icons-outlined" aria-hidden="true">send</span><span>推送至E签宝</span></button>
<button class="agreement-button agreement-button--primary" type="submit" data-sign-request-send @disabled(! $agreementCanOperate || ! $esignConfigured) title="{{ ! $agreementCanOperate ? '尚未获签署操作授权,请联系管理员' : ($esignConfigured ? '推送至E签宝' : 'E签宝尚未配置') }}"><span class="material-icons-outlined" aria-hidden="true">send</span><span>推送至E签宝</span></button>
</footer>
</form>
@ -2342,6 +2360,7 @@
var currentPartnerType = 'courier';
var sendButton = root.querySelector('[data-sign-request-send]');
var esignAvailable = @json($esignConfigured);
var canOperate = @json($agreementCanOperate);
var courierOptions = courierSelect ? Array.prototype.filter.call(courierSelect.options, function (option) {
return option.value !== '' && !option.disabled;
}).map(function (option) {
@ -3343,7 +3362,7 @@
async function loadQuotePreview() {
var option = quoteVersionOption();
var requestId = ++quotePreviewRequest;
if (!option || !option.dataset.previewUrl) {
if (!canOperate || !option || !option.dataset.previewUrl) {
renderQuoteMatrix([], '请选择报价版本后查看报价明细');
return;
}
@ -3400,8 +3419,8 @@
var templateType = documentOption.value;
var supplierContract = currentPartnerType === 'supplier' && templateType === 'cooperation-contract';
var supplierUnavailable = currentPartnerType === 'supplier' && !supplierOptions.length;
sendButton.disabled = supplierUnavailable || !esignAvailable;
sendButton.title = supplierUnavailable ? '供应商资料暂不可用' : (esignAvailable ? '推送至E签宝' : 'E签宝尚未配置');
sendButton.disabled = !canOperate || supplierUnavailable || !esignAvailable;
sendButton.title = !canOperate ? '尚未获签署操作授权,请联系管理员' : (supplierUnavailable ? '供应商资料暂不可用' : (esignAvailable ? '推送至E签宝' : 'E签宝尚未配置'));
sendButton.querySelector('span:last-child').textContent = '推送至E签宝';
var courier = fieldValue('[name="courier_name"]', currentPartnerType === 'supplier' ? '供应商' : '快递公司');
var companyName = fieldValue('[name="company_full_name"]', '待填写公司全称');
@ -3933,6 +3952,11 @@
form.addEventListener('submit', function (event) {
refreshPreview();
if (sendButton.disabled) {
event.preventDefault();
announce(sendButton.title);
return;
}
if (!form.checkValidity()) {
event.preventDefault();
var error = root.querySelector('[data-request-error]');

View File

@ -53,6 +53,15 @@
</label>
</div>
<div class="mt-5 border-t border-gray-100 pt-5">
<input type="hidden" name="agreement_signing_allowed" value="0">
<label class="inline-flex items-center gap-2 text-sm font-semibold text-gray-700">
<input type="checkbox" name="agreement_signing_allowed" value="1" @checked((bool) old('agreement_signing_allowed', $managedUser && in_array((int) $managedUser->id, \App\Utilities\AgreementSigningPermissions::grantedUserIds(), true))) class="rounded border-gray-300 settings-accent settings-field-focus">
协议签署操作权限
</label>
<p class="mt-1 text-xs text-gray-600">允许普通业务账号推送至 E 签宝、同步签署状态、归档及下载文件;还需有“协议签署”页面权限。管理员保留原有操作权限,公司主体维护与任务删除仍仅限管理员。</p>
</div>
<div class="mt-7 flex justify-end gap-3 border-t border-gray-100 pt-5">
<a href="{{ route('system-settings.users.index') }}" class="rounded-lg px-5 py-2.5 text-sm text-gray-600 hover:bg-gray-100">取消</a>
<button type="submit" class="rounded-lg settings-primary">保存</button>

View File

@ -0,0 +1,128 @@
<?php
namespace Tests\Feature\Business;
use App\Models\Auth\Permission;
use App\Models\Auth\Role;
use App\Models\Business\CourierAgreementSignature;
use App\Utilities\AgreementSigningPermissions;
use App\Utilities\SystemAccess;
use DOMDocument;
use DOMXPath;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
use Tests\Feature\FeatureTestCase;
class AgreementSigningPermissionsTest extends FeatureTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->withExceptionHandling();
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
Http::preventStrayRequests();
Storage::fake('uploads');
config(['services.esign.environment' => 'sandbox', 'services.esign.base_url' => 'https://smlopenapi.esign.test',
'services.esign.app_id' => 'sandbox-app-id', 'services.esign.app_secret' => 'sandbox-app-secret']);
}
public function test_operation_requires_an_enabled_company_member_with_page_access_and_explicit_grant(): void
{
$this->regularUser();
$this->assertFalse(AgreementSigningPermissions::canOperate($this->user));
AgreementSigningPermissions::setGranted($this->user, true);
$this->assertTrue(AgreementSigningPermissions::canOperate($this->user));
$this->assertFalse(AgreementSigningPermissions::canAdminister($this->user));
$this->user->enabled = false;
$this->assertFalse(AgreementSigningPermissions::canOperate($this->user));
$this->user->enabled = true;
$this->user->companies()->detach();
$this->assertFalse(AgreementSigningPermissions::canOperate($this->user));
}
public function test_page_permission_is_required_even_with_an_operation_grant(): void
{
$this->regularUser();
AgreementSigningPermissions::setGranted($this->user, true);
$subject = 'user_' . $this->user->id;
foreach ([[], ['courier-payables/agreement-signing'], ['courier-payables']] as $pages) {
setting()->set('system.sidebar_permissions', ['configured' => [$subject], 'subjects' => [$subject => $pages]]);
$this->assertSame($pages !== [], AgreementSigningPermissions::canOperate($this->user));
}
}
public function test_administrators_keep_signing_and_management_access(): void
{
foreach (['admin', 'super_admin'] as $role) {
$this->user->syncRoles([Role::where('name', $role)->firstOrFail()->id]);
$this->assertTrue(AgreementSigningPermissions::canOperate($this->user));
$this->assertTrue(AgreementSigningPermissions::canAdminister($this->user));
}
}
public function test_button_and_task_actions_follow_the_same_grant_as_the_interface(): void
{
$this->regularUser();
$this->task();
foreach ([false, true, false] as $granted) {
AgreementSigningPermissions::setGranted($this->user, $granted);
$html = $this->loginAs()->get($this->pageUrl())->assertOk()->getContent();
$document = new DOMDocument();
@$document->loadHTML('<?xml encoding="UTF-8">' . $html);
$xpath = new DOMXPath($document);
$button = $xpath->query('//button[@data-sign-request-send]')->item(0);
$this->assertNotNull($button);
$this->assertSame(! $granted, $button->hasAttribute('disabled'));
$this->assertSame($granted ? 1 : 0, $xpath->query('//td[contains(@class,"agreement-table-actions")]//a[@title="下载签署原文"]')->length);
$this->assertSame(0, $xpath->query('//form[@data-delete-task-form]')->length);
$this->assertSame(1, $xpath->query('//button[@data-subject-manage-open and @disabled]')->length);
$this->assertSame($granted ? 0 : 1, $xpath->query('//*[@data-signing-permission-hint]')->length);
}
}
public function test_authorized_users_can_download_and_sync_but_cannot_delete_or_manage_company_subjects(): void
{
$this->regularUser();
AgreementSigningPermissions::setGranted($this->user, true);
$task = $this->task();
Storage::disk('uploads')->put($task->source_pdf_path, '%PDF-test');
$this->loginAs()->get(route('business.courier-agreement-signatures.download', [$task, 'source']))->assertOk();
$this->post(route('business.courier-agreement-signatures.synchronize', $task))->assertRedirect();
$this->deleteJson(route('business.courier-agreement-signatures.destroy', $task), ['delete_confirmed' => true])->assertForbidden();
$this->postJson(route('business.courier-agreement-signatures.subjects.store'), ['name' => '禁止新增主体'])->assertForbidden();
$this->assertFalse($task->fresh()->trashed());
Http::assertNothingSent();
}
public function test_unauthorized_users_cannot_use_task_interfaces_or_grant_themselves_access(): void
{
$this->regularUser();
$task = $this->task();
$this->loginAs()->get(route('business.courier-agreement-signatures.download', [$task, 'source']))->assertForbidden();
$this->post(route('business.courier-agreement-signatures.synchronize', $task))->assertForbidden();
$this->patchJson(route('system-settings.users.update', $this->user->id), ['agreement_signing_allowed' => true])->assertForbidden();
$this->get(route('system-settings.users.index'))->assertForbidden();
$this->assertSame([], AgreementSigningPermissions::grantedUserIds());
Http::assertNothingSent();
}
private function regularUser(): void
{
$this->user->syncRoles([Role::where('name', SystemAccess::USER)->firstOrFail()->id]);
}
private function pageUrl(): string
{
return route('business.pages.show', ['section' => 'courier-payables', 'page' => 'agreement-signing', 'agreement_view' => 'create']);
}
private function task(): CourierAgreementSignature
{
return CourierAgreementSignature::create(['company_id' => $this->company->id, 'business_no' => 'SIGN-PERMISSION-TEST',
'idempotency_key' => 'permission-test', 'document_type' => 'account-proof', 'template_code' => 'TPL-ACCOUNT-CERT-V2',
'template_version' => '2', 'source_file_name' => 'test.doc', 'provider' => 'esign', 'environment' => 'sandbox',
'status' => 'prepared', 'courier_name' => '测试快递', 'organization_name' => '测试主体', 'company_full_name' => '测试公司',
'operator_account' => '13800138000', 'notice_types' => '', 'source_pdf_path' => 'permission-test.pdf', 'source_pdf_sha256' => str_repeat('a', 64),
'payload' => [], 'created_by' => $this->user->id, 'updated_by' => $this->user->id]);
}
}

View File

@ -4,12 +4,14 @@ namespace Tests\Feature\Business;
use App\Jobs\Business\ArchiveCourierAgreementSignature;
use App\Models\Auth\Permission;
use App\Models\Auth\Role;
use App\Models\Business\CourierAgreementSignature;
use App\Models\Business\CourierQuoteChange;
use App\Models\Business\CourierQuoteSignature;
use App\Models\Business\CourierQuoteVersion;
use App\Support\CourierAgreementPdf;
use App\Support\SupplierCooperationTemplates;
use App\Utilities\AgreementSigningPermissions;
use DOMDocument;
use DOMXPath;
use Illuminate\Http\Client\Request as ClientRequest;
@ -76,6 +78,43 @@ class CourierAgreementSignatureWorkflowTest extends FeatureTestCase
$this->assertSame(1, CourierAgreementSignature::query()->count());
}
public function test_authorized_business_user_can_push_without_becoming_an_administrator(): void
{
$this->user->syncRoles([Role::where('name', 'user')->firstOrFail()->id]);
AgreementSigningPermissions::setGranted($this->user, true);
$this->fakeEsign();
$this->loginAs()->post(route('business.courier-agreement-signatures.store'), $this->accountProofPayload('business-authorized'))
->assertRedirect();
$signature = CourierAgreementSignature::query()->firstOrFail();
$this->assertSame('flow-agreement-001', $signature->esign_sign_flow_id);
$this->assertSame($this->user->id, $signature->created_by);
$this->assertSame(['user'], $this->user->roles()->pluck('name')->all());
$this->assertFalse(AgreementSigningPermissions::canAdminister($this->user));
}
public function test_revoked_signing_or_page_permission_blocks_push_before_any_esign_call(): void
{
$this->withExceptionHandling();
Http::preventStrayRequests();
$this->user->syncRoles([Role::where('name', 'user')->firstOrFail()->id]);
$this->loginAs();
$url = route('business.courier-agreement-signatures.store');
$payload = $this->accountProofPayload('business-not-authorized');
$this->post($url, $payload)->assertForbidden();
AgreementSigningPermissions::setGranted($this->user, true);
AgreementSigningPermissions::setGranted($this->user, false);
$this->post($url, $payload)->assertForbidden();
AgreementSigningPermissions::setGranted($this->user, true);
$subject = 'user_' . $this->user->id;
setting()->set('system.sidebar_permissions', ['configured' => [$subject], 'subjects' => [$subject => []]]);
$this->post($url, $payload)->assertForbidden();
$this->assertSame(0, CourierAgreementSignature::query()->count());
Http::assertNothingSent();
}
public function test_supplier_requests_require_a_selected_supplier_before_sending(): void
{
Http::preventStrayRequests();

View File

@ -6,6 +6,7 @@ use App\Models\Auth\DingTalkIdentity;
use App\Models\Auth\Permission;
use App\Models\Auth\Role;
use App\Models\Auth\User;
use App\Utilities\AgreementSigningPermissions;
use App\Utilities\BusinessNavigation;
use App\Utilities\SidebarPermissions;
use App\Utilities\SystemAccess;
@ -13,6 +14,28 @@ use Tests\Feature\FeatureTestCase;
class UserPermissionsTest extends FeatureTestCase
{
public function test_administrator_can_grant_and_revoke_signing_without_changing_business_user_role(): void
{
$admin = $this->account('admin');
$target = $this->account('user');
$this->loginAs($admin)->get(route('system-settings.users.edit', $target->id))->assertOk()
->assertSee('name="agreement_signing_allowed"', false);
foreach ([true, false] as $allowed) {
$this->patch(route('system-settings.users.update', $target->id),
$this->payload($target, 'user') + ['agreement_signing_allowed' => (int) $allowed])->assertRedirect();
$this->assertSame($allowed, AgreementSigningPermissions::canOperate($target->fresh()));
$this->assertSame(['user'], $target->roles()->pluck('name')->all());
}
}
public function test_unrelated_account_edit_preserves_existing_signing_grant(): void
{
$target = $this->account('user');
AgreementSigningPermissions::setGranted($target, true);
$this->loginAs()->patch(route('system-settings.users.update', $target->id), $this->payload($target, 'user'))->assertRedirect();
$this->assertTrue(AgreementSigningPermissions::canOperate($target->fresh()));
}
public function test_admin_sees_all_company_accounts_but_cannot_edit_administrators(): void
{
$admin = $this->account('admin');