withoutVite(); Http::fake(); Mail::fake(); config(['portal.organization_country_enabled' => true, 'portal.profile_access_enabled' => true]); $this->actor = User::factory()->create(); $this->actingAs($this->actor); $this->organization = (string) Str::uuid(); DB::table('organizations')->insert(['id' => $this->organization, 'name' => 'SYNTHETIC reuse source', 'organization_type' => 'private', 'source_system' => 'synthetic-reuse-fixture', 'external_id' => 'SYNTHETIC-stable', 'version_no' => 3, 'incorporation_country_code' => 'FR', 'incorporation_country_provenance' => json_encode(['kind' => 'representative_declaration', 'reference' => 'SYNTHETIC source reference'])]); DB::table('memberships')->insert(['user_id' => $this->actor->id, 'organization_id' => $this->organization, 'role' => 'viewer']); $this->putJson('/api/v1/investment/profile', ['version' => 0, 'payload' => ['residence_country_code' => 'PT']])->assertOk(); $this->company = $this->postJson('/api/v1/investment/companies', ['version' => 0, 'payload' => ['name' => 'SYNTHETIC private reuse target', 'role' => 'SYNTHETIC representative', 'incorporation_country_code' => 'MZ']])->assertOk()->json('id'); $this->submission = $this->postJson('/api/v1/investment/submissions', ['company_id' => $this->company])->assertCreated()->json('id'); } private function command(): array { return ['version' => 1, 'organization_id' => $this->organization, 'organization_version' => 3, 'operation_key' => (string) Str::uuid()]; } private function url(?string $id = null): string { return '/api/v1/investment/companies/'.($id ?? $this->company).'/organization-country'; } private function target(): \stdClass { return DB::table('investment_company_declarations')->where('id', $this->company)->first(); } public function test_read_authority_can_reuse_once_and_replay_keeps_the_original_snapshot_and_encrypted_receipt(): void { $command = $this->command(); $submission = DB::table('investment_submissions')->where('id', $this->submission)->first(); $first = $this->postJson($this->url(), $command)->assertOk()->assertHeader('Cache-Control', 'no-store, private')->json(); $target = $this->target(); $auditCount = DB::table('audit_events')->count(); $this->postJson($this->url(strtoupper($this->company)), [...$command, 'organization_id' => strtoupper($this->organization), 'operation_key' => strtoupper($command['operation_key'])])->assertExactJson($first); $this->assertEquals($target, $this->target()); $this->assertSame($auditCount, DB::table('audit_events')->count()); $payload = json_decode(Crypt::decryptString($target->payload), true); $this->assertSame('FR', $payload['incorporation_country_code']); $this->assertSame(3, $payload['organization_country_snapshot']['organization_version']); $receipt = json_decode(DB::table('audit_events')->where('source_system', 'investment-workspace-command')->value('after_data'), true); $this->assertArrayNotHasKey('result', $receipt); $this->assertArrayNotHasKey('country', $receipt); $this->assertSame(['version' => 2, 'country' => 'FR'], json_decode(Crypt::decryptString($receipt['result_encrypted']), true)); $this->assertEquals($submission, DB::table('investment_submissions')->where('id', $this->submission)->first()); $this->getJson('/api/v1/investment/profile')->assertJsonPath('account.payload.residence_country_code', 'PT')->assertJsonPath('organizations.0.canEdit', false); $this->assertSame(1, DB::table('organizations')->count()); $this->assertSame(1, DB::table('memberships')->count()); } public function test_replay_reads_its_original_country_after_later_source_and_target_changes_even_if_source_country_is_now_missing(): void { $first = $this->command(); $this->postJson($this->url(), $first)->assertOk(); DB::table('organizations')->where('id', $this->organization)->update(['version_no' => 4, 'incorporation_country_code' => 'ZA']); $this->postJson($this->url(), [...$first, 'operation_key' => (string) Str::uuid(), 'version' => 2, 'organization_version' => 4])->assertJsonPath('version', 3)->assertJsonPath('country', 'ZA'); $target = $this->target(); DB::table('organizations')->where('id', $this->organization)->update(['version_no' => 5, 'incorporation_country_code' => null]); $this->postJson($this->url(), $first)->assertExactJson(['version' => 2, 'country' => 'FR']); $this->assertEquals($target, $this->target()); $this->postJson($this->url(), [...$first, 'operation_key' => (string) Str::uuid(), 'version' => 3, 'organization_version' => 5])->assertUnprocessable(); $this->assertEquals($target, $this->target()); $this->assertSame(2, DB::table('audit_events')->where('action', 'organization_country_reused')->count()); } public function test_stale_versions_malformed_keys_and_changed_intentions_do_not_write(): void { $command = $this->command(); $before = $this->target(); $this->postJson($this->url(), [...$command, 'organization_version' => 2])->assertConflict(); $this->postJson($this->url(), [...$command, 'version' => 2])->assertConflict(); $this->postJson($this->url(), [...$command, 'operation_key' => 'bad'])->assertUnprocessable()->assertJsonValidationErrors('operation_key'); $this->assertEquals($before, $this->target()); $this->postJson($this->url(), $command)->assertOk(); $after = $this->target(); $this->postJson($this->url(), [...$command, 'organization_version' => 4])->assertConflict(); $this->putJson('/api/v1/investment/profile', ['version' => 1, 'operation_key' => $command['operation_key'], 'payload' => ['residence_country_code' => 'ZA']])->assertConflict(); $this->assertEquals($after, $this->target()); } public function test_replay_requires_current_source_access_and_current_ownership_and_can_resume_after_representation_is_restored(): void { $command = $this->command(); $this->postJson($this->url(), $command)->assertOk(); $target = $this->target(); DB::table('memberships')->where('user_id', $this->actor->id)->update(['deleted_at' => now()]); $this->postJson($this->url(), $command)->assertForbidden(); DB::table('memberships')->where('user_id', $this->actor->id)->update(['deleted_at' => null]); $this->postJson($this->url(), $command)->assertExactJson(['version' => 2, 'country' => 'FR']); DB::table('organizations')->where('id', $this->organization)->update(['deleted_at' => now()]); $this->postJson($this->url(), $command)->assertForbidden(); $this->actingAs(User::factory()->create())->postJson($this->url(), $command)->assertNotFound(); $this->assertEquals($target, $this->target()); } public function test_legacy_reuse_without_a_key_is_supported_and_its_country_never_updates_old_submissions(): void { $command = $this->command(); unset($command['operation_key']); $before = DB::table('investment_submissions')->where('id', $this->submission)->first(); $this->postJson($this->url(), $command)->assertOk()->assertJsonPath('country', 'FR'); $new = $this->postJson('/api/v1/investment/submissions', ['company_id' => $this->company])->assertCreated()->json('id'); $this->getJson('/api/v1/investment/submissions/'.$new)->assertJsonPath('submission.payload.incorporation_country_code', 'FR')->assertJsonPath('submission.payload.organization_country_snapshot.organization_version', 3); $this->assertEquals($before, DB::table('investment_submissions')->where('id', $this->submission)->first()); $this->assertSame(0, DB::table('audit_events')->where('source_system', 'investment-workspace-command')->count()); } public function test_a_receipt_failure_rolls_back_the_company_and_business_audit_together(): void { $before = $this->target(); $auditCount = DB::table('audit_events')->count(); DB::statement("ALTER TABLE audit_events ADD CONSTRAINT qa_reject_country_reuse_receipt CHECK (source_system <> 'investment-workspace-command')"); $response = $this->postJson($this->url(), $this->command())->assertStatus(503); $this->assertStringNotContainsString('SQLSTATE', $response->getContent()); $this->assertEquals($before, $this->target()); $this->assertSame($auditCount, DB::table('audit_events')->count()); } }