user->attachPermission( Permission::where('name', 'read-admin-panel')->firstOrFail() ); } public function testAdminCanCreateUpdateAndSwitchSigningSubjects(): void { $this->grantWorkspaceAccess(); $first = $this->loginAs()->postJson( route('business.courier-agreement-signatures.subjects.store'), ['name' => '甲公司', 'unified_social_credit_code' => '91320100MA1TEST001'], ); $first->assertOk() ->assertJsonPath('subject.name', '甲公司') ->assertJsonPath('subject.is_default', true); $firstSubject = CourierSigningSubject::query()->where('name', '甲公司')->firstOrFail(); $second = $this->postJson( route('business.courier-agreement-signatures.subjects.store'), ['name' => '乙公司'], ); $second->assertOk()->assertJsonPath('subject.is_default', false); $secondSubject = CourierSigningSubject::query()->where('name', '乙公司')->firstOrFail(); $this->postJson( route('business.courier-agreement-signatures.subjects.default', $secondSubject), )->assertOk()->assertJsonPath('subject.is_default', true); $this->assertFalse($firstSubject->fresh()->is_default); $this->assertTrue($secondSubject->fresh()->is_default); $this->postJson( route('business.courier-agreement-signatures.subjects.update', $secondSubject), ['name' => '乙公司(新)', 'unified_social_credit_code' => '91320100MA1TEST002'], )->assertOk()->assertJsonPath('subject.name', '乙公司(新)'); $this->assertDatabaseHas('business_courier_signing_subjects', [ 'id' => $secondSubject->id, 'name' => '乙公司(新)', 'is_default' => 1, ]); } public function testDuplicateSubjectNameIsRejected(): void { $this->grantWorkspaceAccess(); $this->loginAs()->postJson( route('business.courier-agreement-signatures.subjects.store'), ['name' => '甲公司'], )->assertOk(); $this->withExceptionHandling()->postJson( route('business.courier-agreement-signatures.subjects.store'), ['name' => '甲公司'], )->assertStatus(422) ->assertJsonValidationErrors(['name']); } public function testDeletingAnOrdinarySubjectPreservesTheDefaultAndHistoricalAgreement(): void { $this->grantWorkspaceAccess(); $default = $this->subject('默认公司', true); $subject = $this->subject('历史签署公司'); $task = CourierAgreementSignature::create([ 'company_id' => $this->company->id, 'business_no' => 'AGR-SUBJECT-DELETE', 'idempotency_key' => 'subject-delete-snapshot', 'document_type' => 'account-proof', 'template_code' => 'TPL-ACCOUNT-CERT-V2', 'template_version' => '2', 'source_file_name' => '收款账户证明文件.doc', 'status' => CourierAgreementSignature::STATUS_ARCHIVED, 'courier_name' => '测试快递', 'company_full_name' => $subject->name, 'organization_name' => '测试快递有限公司', 'payload' => ['signing_subject' => $subject->name], 'environment' => 'sandbox', 'source_pdf_path' => 'subject-delete/source.pdf', 'source_pdf_sha256' => str_repeat('a', 64), 'created_by' => $this->user->id, 'updated_by' => $this->user->id, ]); $this->loginAs()->deleteJson($this->deleteUrl($subject), ['delete_confirmed' => true]) ->assertOk()->assertJsonCount(1, 'subjects') ->assertJsonPath('subjects.0.id', $default->id) ->assertJsonPath('subjects.0.is_default', true); $this->assertSoftDeleted($subject); $this->assertSame($this->user->id, $subject->fresh()->updated_by); $this->assertSame($subject->name, $task->fresh()->company_full_name); $this->assertSame($subject->name, $task->fresh()->payload['signing_subject']); $this->assertFalse($task->fresh()->trashed()); $this->withExceptionHandling()->deleteJson($this->deleteUrl($subject), ['delete_confirmed' => true])->assertNotFound(); } public function testDeletingTheDefaultSelectsOneRemainingSubjectInTheSameCompany(): void { $this->grantWorkspaceAccess(); $default = $this->subject('默认公司', true); $next = $this->subject('剩余公司一'); $other = $this->subject('剩余公司二'); $foreign = $this->subject('其他租户公司', true, $this->company->id + 100); $this->loginAs()->deleteJson($this->deleteUrl($default), ['delete_confirmed' => true]) ->assertOk()->assertJsonCount(2, 'subjects') ->assertJsonPath('subjects.0.id', $next->id) ->assertJsonPath('subjects.0.is_default', true); $this->assertSoftDeleted($default); $this->assertTrue($next->fresh()->is_default); $this->assertFalse($other->fresh()->is_default); $this->assertTrue($foreign->fresh()->is_default); $this->assertSame(1, CourierSigningSubject::query()->where('is_default', true)->count()); } public function testLastSubjectCanBeDeletedAndANewSubjectBecomesDefault(): void { $this->grantWorkspaceAccess(); $subject = $this->subject('唯一公司', true); $this->loginAs()->postJson($this->deleteUrl($subject), ['_method' => 'DELETE', 'delete_confirmed' => '1']) ->assertOk()->assertJsonCount(0, 'subjects')->assertJsonPath('subject', null); $this->assertSoftDeleted($subject); $this->postJson(route('business.courier-agreement-signatures.subjects.store'), ['name' => '唯一公司']) ->assertOk()->assertJsonPath('subject.is_default', true); } public function testSubjectDeletionRequiresConfirmation(): void { $this->grantWorkspaceAccess(); $subject = $this->subject('保留公司', true); $this->withExceptionHandling()->loginAs()->deleteJson($this->deleteUrl($subject)) ->assertStatus(422)->assertJsonValidationErrors(['delete_confirmed']); $this->assertFalse($subject->fresh()->trashed()); } public function testSubjectDeletionEnforcesCompanyAndManagementPermission(): void { $this->grantWorkspaceAccess(); $subject = $this->subject('本租户公司', true); $foreign = $this->subject('其他租户公司', true, $this->company->id + 100); $this->withExceptionHandling()->loginAs()->deleteJson($this->deleteUrl($foreign), ['delete_confirmed' => true]) ->assertNotFound(); $this->assertFalse($foreign->fresh()->trashed()); $this->user->roles()->detach(); $this->user->unsetRelation('roles'); $this->deleteJson($this->deleteUrl($subject), ['delete_confirmed' => true])->assertForbidden(); $this->assertFalse($subject->fresh()->trashed()); } private function subject(string $name, bool $default = false, ?int $companyId = null): CourierSigningSubject { return CourierSigningSubject::create([ 'company_id' => $companyId ?? $this->company->id, 'name' => $name, 'is_default' => $default, 'created_by' => $this->user->id, 'updated_by' => $this->user->id, ]); } private function deleteUrl(CourierSigningSubject $subject): string { return route('business.courier-agreement-signatures.subjects.destroy', $subject); } }