true]); } public function test_auditor_with_officer_membership_cannot_change_the_master_country(): void { config(['portal.profile_access_enabled' => true]); $auditor = User::factory()->create(); $approver = User::factory()->create(); $org = (string) Str::uuid(); DB::table('organizations')->insert(['id' => $org, 'name' => 'Synthetic QA auditor scope', 'source_system' => 'unit-qa', 'organization_type' => 'private']); DB::table('memberships')->insert(['user_id' => $auditor->id, 'organization_id' => $org, 'role' => 'officer']); app(PortalProfileAdministration::class)->insertGrant((string) Str::uuid(), $approver, ['user_id' => $auditor->id, 'role' => 'auditor', 'opportunity_types' => [], 'responsibilities' => []]); $auditor->refresh(); $this->actingAs($auditor)->withSession(['portal.access_revision' => (int) $auditor->portal_access_revision]); $this->getJson('/api/v1/investment/profile')->assertOk()->assertJsonPath('organizations.0.canEdit', false); $this->putJson('/api/v1/investment/organizations/'.$org.'/country', ['version' => 1, 'country' => 'FR', 'source_reference' => 'Synthetic declaration blocked by auditor policy'])->assertForbidden(); $this->assertNull(DB::table('organizations')->where('id', $org)->value('incorporation_country_code')); $this->assertSame(1, DB::table('organizations')->where('id', $org)->value('version_no')); } public function test_master_country_is_explicit_authorized_versioned_and_never_rewrites_previous_submissions(): void { $this->withoutVite(); $owner = User::factory()->create(); $viewer = User::factory()->create(); $other = User::factory()->create(); $org = (string) Str::uuid(); DB::table('organizations')->insert(['id' => $org, 'name' => 'Synthetic QA master', 'source_system' => 'unit-qa', 'external_id' => 'stable-master', 'organization_type' => 'private']); foreach ([[$owner, 'officer'], [$viewer, 'viewer']] as [$actor,$role]) { DB::table('memberships')->insert(['user_id' => $actor->id, 'organization_id' => $org, 'role' => $role]); } $this->actingAs($owner)->putJson('/api/v1/investment/profile', ['version' => 0, 'payload' => ['country' => 'Historical value', 'residence_country_code' => 'PT']])->assertOk(); $company = $this->postJson('/api/v1/investment/companies', ['version' => 0, 'payload' => ['name' => 'Private declared entity', 'role' => 'QA representative', 'incorporation_country_code' => 'MZ']])->assertOk()->json('id'); $old = $this->postJson('/api/v1/investment/submissions', ['company_id' => $company])->assertCreated()->json('id'); $before = InvestmentSubmission::findOrFail($old)->getRawOriginal('payload'); $url = '/api/v1/investment/organizations/'.$org.'/country'; $this->putJson($url, ['version' => 1, 'country' => 'ZZ', 'source_reference' => 'Synthetic source'])->assertUnprocessable(); $this->putJson($url, ['version' => 1, 'country' => 'FR', 'source_reference' => 'Synthetic declaration; no real registration'])->assertOk()->assertJsonPath('version', 2); $this->putJson($url, ['version' => 1, 'country' => 'ZA', 'source_reference' => 'Stale synthetic declaration'])->assertConflict(); $reuse = '/api/v1/investment/companies/'.$company.'/organization-country'; $this->postJson($reuse, ['version' => 1, 'organization_id' => $org, 'organization_version' => 2])->assertOk()->assertJsonPath('country', 'FR'); $new = $this->postJson('/api/v1/investment/submissions', ['company_id' => $company])->assertCreated()->json('id'); $source = InvestmentSubmission::findOrFail($new)->payload['organization_country_snapshot']; $this->assertSame($org, $source['organization_id']); $this->assertSame(2, $source['organization_version']); $this->putJson('/api/v1/investment/submissions/'.$new, ['version' => 1, 'payload' => ['incorporation_country_code' => 'FR', 'organization_country_snapshot' => array_reverse($source, true)]])->assertOk(); $forged = $source; $forged['country'] = 'ZA'; $this->putJson('/api/v1/investment/submissions/'.$new, ['version' => 2, 'payload' => ['incorporation_country_code' => 'FR', 'organization_country_snapshot' => $forged]])->assertUnprocessable(); $this->putJson($url, ['version' => 2, 'country' => 'ZA', 'source_reference' => 'Synthetic second declaration'])->assertOk(); $this->assertSame($source, InvestmentSubmission::findOrFail($new)->payload['organization_country_snapshot']); $this->assertSame($before, InvestmentSubmission::findOrFail($old)->getRawOriginal('payload')); $this->assertSame('MZ', InvestmentSubmission::findOrFail($old)->payload['incorporation_country_code']); $this->assertSame('PT', InvestmentSubmission::findOrFail($old)->payload['residence_country_code']); $this->assertSame(1, DB::table('organizations')->count()); $row = DB::table('organizations')->where('id', $org)->first(); $this->assertSame('unit-qa', $row->source_system); $this->assertSame('stable-master', $row->external_id); $audit = DB::table('audit_events')->where('action', 'incorporation_country_saved')->orderByRaw("(after_data->>'version')::integer")->get(); $this->assertCount(2, $audit); $snapshot = json_decode(Crypt::decryptString(json_decode($audit[1]->before_data, true)['snapshot']), true); $this->assertSame('FR', $snapshot['incorporation_country_code']); $this->actingAs($viewer)->getJson('/api/v1/investment/profile')->assertOk()->assertJsonPath('organizations.0.canEdit', false); $this->putJson($url, ['version' => 3, 'country' => 'MZ', 'source_reference' => 'Not authorized'])->assertForbidden(); $this->actingAs($other)->getJson('/api/v1/investment/profile')->assertOk()->assertJsonCount(0, 'organizations'); $this->putJson($url, ['version' => 3, 'country' => 'MZ', 'source_reference' => 'Wrong organization'])->assertForbidden(); DB::table('memberships')->where('user_id', $owner->id)->update(['deleted_at' => now()]); $this->actingAs($owner)->postJson($reuse, ['version' => 2, 'organization_id' => $org, 'organization_version' => 3])->assertForbidden(); $this->assertSame(3, DB::table('organizations')->where('id', $org)->value('version_no')); } }