true]); } private function reviewer(): User { $user = User::factory()->create(); $user->forceFill(['two_factor_secret' => Crypt::encryptString('SYNTHETIC'), 'two_factor_confirmed_at' => now()])->save(); config(['portal.managers' => [$user->email], 'existing-business.reviewers' => [$user->email]]); return $user; } /** @return array */ private function draft(): array { return ['modality' => 'participation_sale', 'classification' => 'restricted', 'reason' => 'Synthetic initial draft', 'payload' => ['title' => 'Private synthetic business', 'description' => 'Synthetic reserved description for the internal review only.', 'source_reference' => 'Synthetic source reference', 'valid_until' => today()->addMonth()->toDateString()]]; } public function test_private_intake_review_information_and_withdrawal_preserve_isolation(): void { $owner = User::factory()->create(); $reviewer = $this->reviewer(); $this->actingAs($owner); $id = $this->postJson($this->base, $this->draft())->assertCreated()->json('id'); $url = $this->base.'/'.$id; $this->getJson($url)->assertOk()->assertHeader('Cache-Control', 'no-store, private'); $this->actingAs($reviewer)->getJson($url)->assertForbidden(); $this->actingAs($owner)->postJson($url.'/transitions', ['version_no' => 1, 'action' => 'submitted', 'note' => 'Synthetic review requested'])->assertUnprocessable(); $this->postJson($url.'/transitions', ['version_no' => 1, 'action' => 'submitted', 'note' => 'Synthetic review requested', 'review_consent' => true])->assertOk()->assertJsonPath('version_no', 2); $this->putJson($url, [...$this->draft(), 'version_no' => 2])->assertForbidden(); $this->postJson($url.'/transitions', ['version_no' => 2, 'action' => 'in_review', 'note' => 'Attempted self review'])->assertForbidden(); $this->actingAs($reviewer)->getJson('/api/v1/existing-business/reviews')->assertOk()->assertJsonPath('records.total', 1); $this->postJson($url.'/transitions', ['version_no' => 1, 'action' => 'in_review', 'note' => 'Synthetic review started'])->assertConflict(); $this->postJson($url.'/transitions', ['version_no' => 2, 'action' => 'in_review', 'note' => 'Synthetic review started'])->assertOk()->assertJsonPath('version_no', 3); $this->postJson($url.'/transitions', ['version_no' => 3, 'action' => 'needs_information', 'note' => 'Please clarify synthetic source'])->assertOk(); $this->actingAs($owner)->getJson($url)->assertOk()->assertJsonPath('history.0.note', 'Please clarify synthetic source'); $this->postJson($url, [...$this->draft(), '_method' => 'PUT', 'version_no' => 4])->assertOk()->assertJsonPath('version_no', 5); $this->postJson($url.'/transitions', ['version_no' => 5, 'action' => 'withdrawn', 'note' => 'Synthetic withdrawal requested'])->assertOk(); $this->actingAs($reviewer)->getJson($url)->assertForbidden(); $this->getJson('/api/v1/existing-business/reviews')->assertJsonPath('records.total', 0); $this->assertDatabaseCount('existing_business_reviews', 4); $this->assertDatabaseCount('existing_business_revisions', 6); $this->assertStringNotContainsString('Please clarify', DB::table('existing_business_reviews')->where('action', 'needs_information')->value('note')); } public function test_granular_verification_requires_independent_review_and_invalidates_after_changes(): void { $owner = User::factory()->create(); $reviewer = $this->reviewer(); $this->actingAs($owner); $id = $this->postJson($this->base, $this->draft())->assertCreated()->json('id'); $url = $this->base.'/'.$id; $finding = ['version_no' => 3, 'check' => 'identity', 'outcome' => 'confirmed', 'evidence_reference' => 'Synthetic identity evidence', 'note' => 'Synthetic independent review', 'valid_until' => today()->addDay()->toDateString()]; $this->postJson($url.'/transitions', ['version_no' => 1, 'action' => 'submitted', 'note' => 'Synthetic review requested', 'review_consent' => true])->assertOk(); $this->postJson($url.'/verifications', $finding)->assertForbidden(); $this->actingAs($reviewer)->postJson($url.'/verifications', [...$finding, 'version_no' => 2])->assertConflict(); $this->postJson($url.'/transitions', ['version_no' => 2, 'action' => 'in_review', 'note' => 'Synthetic review started'])->assertOk(); $this->postJson($url.'/verifications', [...$finding, 'valid_until' => null])->assertUnprocessable(); $this->postJson($url.'/verifications', [...$finding, 'object_id' => '00000000-0000-4000-8000-000000000001'])->assertNotFound(); $this->postJson($url.'/verifications', $finding)->assertOk()->assertJsonPath('version_no', 4); $this->postJson($url.'/verifications', $finding)->assertConflict(); $this->getJson($url)->assertOk()->assertJsonPath('verifications.0.effective_outcome', 'confirmed'); $raw = DB::table('existing_business_revisions')->where('business_id', $id)->where('action', 'verification')->value('snapshot'); $this->assertStringNotContainsString('Synthetic identity evidence', $raw); $this->travel(2)->days(); $this->getJson($url)->assertOk()->assertJsonPath('verifications.0.effective_outcome', 'expired'); $this->travelBack(); $this->postJson($url.'/transitions', ['version_no' => 4, 'action' => 'needs_information', 'note' => 'Synthetic clarification requested'])->assertOk(); $this->actingAs($owner)->putJson($url, [...$this->draft(), 'version_no' => 5])->assertOk(); $this->getJson($url)->assertJsonPath('verifications.0.effective_outcome', 'stale'); $this->actingAs(User::factory()->create())->getJson($url)->assertForbidden(); } public function test_flag_mfa_and_current_designation_are_required(): void { $this->getJson($this->base)->assertUnauthorized(); $reviewer = $this->reviewer(); $this->actingAs($reviewer)->getJson('/api/v1/existing-business/reviews')->assertOk(); config(['existing-business.reviewers' => []]); $this->getJson('/api/v1/existing-business/reviews')->assertForbidden(); config(['existing-business.reviewers' => [$reviewer->email]]); $reviewer->forceFill(['two_factor_confirmed_at' => null])->save(); $this->getJson('/api/v1/existing-business/reviews')->assertForbidden(); config(['existing-business.enabled' => false]); $this->getJson($this->base)->assertNotFound(); } public function test_arbitrary_states_and_incomplete_or_expired_submission_are_rejected(): void { $this->actingAs(User::factory()->create()); $draft = $this->draft(); $draft['payload']['valid_until'] = today()->subDay()->toDateString(); $id = $this->postJson($this->base, $draft)->assertCreated()->json('id'); foreach (['published', 'approved', 'submitted'] as $action) { $this->postJson($this->base.'/'.$id.'/transitions', ['version_no' => 1, 'action' => $action, 'note' => 'Synthetic invalid transition', 'review_consent' => true])->assertUnprocessable(); } $this->assertDatabaseCount('existing_business_reviews', 0); } public function test_reserved_objects_are_scoped_versioned_and_never_verified_by_declaration(): void { $this->actingAs(User::factory()->create()); $first = $this->postJson($this->base, $this->draft())->assertCreated()->json('id'); $second = $this->postJson($this->base, $this->draft())->assertCreated()->json('id'); $object = ['kind' => 'license', 'version_no' => 1, 'reason' => 'Synthetic license declaration', 'payload' => ['label' => 'Private synthetic license', 'source_reference' => 'Synthetic source only', 'declared_holder' => 'Synthetic holder', 'valid_from' => '2026-01-01', 'valid_until' => '2027-01-01', 'amount' => '1200.25', 'currency' => 'MZN', 'value_kind' => 'asking']]; $id = $this->postJson($this->base.'/'.$first.'/objects', $object)->assertCreated()->assertJsonPath('verification_status', 'pending')->assertJsonPath('version_no', 2)->json('id'); $this->postJson($this->base.'/'.$first.'/objects', $object)->assertConflict(); $this->putJson($this->base.'/'.$second.'/objects/'.$id, $object)->assertNotFound(); $this->putJson($this->base.'/'.$first.'/objects/'.$id, [...$object, 'version_no' => 2, 'kind' => 'asset'])->assertUnprocessable(); $object['payload']['currency'] = null; $this->putJson($this->base.'/'.$first.'/objects/'.$id, [...$object, 'version_no' => 2])->assertUnprocessable(); $this->getJson($this->base.'/'.$first)->assertOk()->assertJsonPath('objects.0.payload.amount', '1200.25'); $this->assertStringNotContainsString('Synthetic holder', DB::table('existing_business_objects')->where('id', $id)->value('payload')); $snapshot = Crypt::decryptString(DB::table('existing_business_revisions')->where('business_id', $first)->where('version_no', 2)->value('snapshot')); $this->assertStringContainsString('Synthetic holder', $snapshot); $this->actingAs(User::factory()->create())->getJson($this->base.'/'.$first)->assertForbidden(); } }