withoutVite(); Http::fake(); Mail::fake(); config(['portal.organization_country_enabled' => true, 'portal.profile_access_enabled' => true]); $this->actor = User::factory()->create(); $this->organization = (string) Str::uuid(); DB::table('organizations')->insert(['id' => $this->organization, 'name' => 'SYNTHETIC country master', 'organization_type' => 'private', 'source_system' => 'synthetic-command-fixture', 'external_id' => 'SYNTHETIC-stable-source']); DB::table('memberships')->insert(['user_id' => $this->actor->id, 'organization_id' => $this->organization, 'role' => 'officer']); $this->actingAs($this->actor); } private function command(): array { return ['operation_key' => (string) Str::uuid(), 'version' => 1, 'country' => 'FR', 'source_reference' => 'SYNTHETIC PRIVATE declaration reference']; } private function url(?string $id = null): string { return '/api/v1/investment/organizations/'.($id ?? $this->organization).'/country'; } public function test_replay_keeps_country_provenance_version_and_audit_identical(): void { $command = $this->command(); $first = $this->putJson($this->url(), $command)->assertOk()->assertJsonPath('version', 2)->json(); $before = DB::table('organizations')->where('id', $this->organization)->first(); $audits = DB::table('audit_events')->get()->toArray(); $this->putJson($this->url(), [...$command, 'operation_key' => strtoupper($command['operation_key'])])->assertExactJson($first); $this->assertEquals($before, DB::table('organizations')->where('id', $this->organization)->first()); $this->assertEquals($audits, DB::table('audit_events')->get()->toArray()); $this->assertSame(2, DB::table('audit_events')->count()); $receipt = json_decode(DB::table('audit_events')->where('source_system', 'investment-workspace-command')->value('after_data'), true); $this->assertStringNotContainsString('SYNTHETIC PRIVATE', json_encode($receipt)); $this->assertSame($command['source_reference'], json_decode(Crypt::decryptString($receipt['command_encrypted']), true)['payload']['source_reference']); } public function test_original_receipt_survives_a_later_change_without_reverting_current_country(): void { $command = $this->command(); $this->putJson($this->url(), $command)->assertOk(); $this->putJson($this->url(), [...$command, 'operation_key' => (string) Str::uuid(), 'version' => 2, 'country' => 'ZA'])->assertJsonPath('version', 3); $before = DB::table('organizations')->where('id', $this->organization)->first(); $this->putJson($this->url(), $command)->assertExactJson(['version' => 2, 'country' => 'FR']); $this->assertEquals($before, DB::table('organizations')->where('id', $this->organization)->first()); $this->assertSame('ZA', $before->incorporation_country_code); } public function test_changed_reference_country_target_or_operation_cannot_reuse_the_same_key(): void { $command = $this->command(); $this->putJson($this->url(), $command)->assertOk(); $this->putJson($this->url(), [...$command, 'source_reference' => 'SYNTHETIC altered reference'])->assertConflict(); $this->putJson($this->url(), [...$command, 'country' => 'ZA'])->assertConflict(); $id = (string) Str::uuid(); DB::table('organizations')->insert(['id' => $id, 'name' => 'SYNTHETIC second master', 'organization_type' => 'private']); DB::table('memberships')->insert(['user_id' => $this->actor->id, 'organization_id' => $id, 'role' => 'officer']); $this->putJson($this->url($id), $command)->assertConflict(); $this->putJson('/api/v1/investment/profile', ['version' => 0, 'operation_key' => $command['operation_key'], 'payload' => ['residence_country_code' => 'PT']])->assertConflict(); $this->assertSame(0, DB::table('investment_accounts')->count()); $this->assertSame(2, DB::table('audit_events')->count()); } public function test_revoked_representation_auditor_and_deleted_organization_cannot_read_a_replay_confirmation(): void { $command = $this->command(); $this->putJson($this->url(), $command)->assertOk(); $before = DB::table('organizations')->where('id', $this->organization)->first(); DB::table('memberships')->where('user_id', $this->actor->id)->update(['deleted_at' => now()]); $this->putJson($this->url(), $command)->assertForbidden(); DB::table('memberships')->where('user_id', $this->actor->id)->update(['deleted_at' => null]); $approver = User::factory()->create(); app(PortalProfileAdministration::class)->insertGrant((string) Str::uuid(), $approver, ['user_id' => $this->actor->id, 'role' => 'auditor', 'opportunity_types' => [], 'source' => 'synthetic-country-command-auditor']); $this->actor->refresh(); $this->actingAs($this->actor)->withSession(['portal.access_revision' => $this->actor->portal_access_revision]); $this->putJson($this->url(), $command)->assertForbidden(); DB::table('portal_role_assignments')->where('user_id', $this->actor->id)->update(['revoked_at' => now()]); DB::table('organizations')->where('id', $this->organization)->update(['deleted_at' => now()]); $this->putJson($this->url(), $command)->assertForbidden(); $after = DB::table('organizations')->where('id', $this->organization)->first(); $this->assertSame($before->incorporation_country_provenance, $after->incorporation_country_provenance); $this->assertSame(2, $after->version_no); } public function test_invalid_country_reference_and_key_do_not_increment_version_and_legacy_requests_still_work(): void { $command = $this->command(); foreach ([['country' => 'ZZ'], ['source_reference' => 'x'], ['operation_key' => 'bad']] as $invalid) { $this->putJson($this->url(), [...$command, ...$invalid])->assertUnprocessable()->assertJsonValidationErrors(array_keys($invalid)); } $this->assertSame(1, DB::table('organizations')->where('id', $this->organization)->value('version_no')); $this->assertSame(0, DB::table('audit_events')->count()); unset($command['operation_key']); $this->putJson($this->url(), $command)->assertOk()->assertJsonPath('version', 2); $this->assertSame(1, DB::table('audit_events')->count()); } public function test_current_membership_role_changes_project_the_same_authority_as_the_write_endpoint(): void { $this->getJson('/api/v1/investment/profile')->assertOk()->assertJsonCount(1, 'organizations')->assertJsonPath('organizations.0.canEdit', true); $this->putJson($this->url(), $this->command())->assertOk(); DB::table('memberships')->where('user_id', $this->actor->id)->update(['role' => 'viewer']); $this->getJson('/api/v1/investment/profile')->assertOk()->assertJsonPath('organizations.0.canEdit', false); $this->putJson($this->url(), [...$this->command(), 'version' => 2])->assertForbidden(); } }