Files
Investidor-V2/tests/Feature/OrganizationCountryReuseCommandsTest.php

149 lines
9.1 KiB
PHP

<?php
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Mail;
use Illuminate\Support\Str;
use Tests\TestCase;
final class OrganizationCountryReuseCommandsTest extends TestCase
{
use RefreshDatabase;
private User $actor;
private string $organization;
private string $company;
private string $submission;
protected function setUp(): void
{
parent::setUp();
$this->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());
}
}