withoutVite(); config(['portal.profile_access_enabled' => true, 'portal.opportunity_specialized_drafts_enabled' => true, 'portal.managers' => [], 'investor-path.enabled' => true, 'suitecrm.enabled' => false, 'investmoz_ai.enabled' => false]); $this->actor = F::operator(); $this->login($this->actor); } private function login(User $u): void { $this->actingAs($u)->withSession(['portal.mfa_actor' => $u->id, 'portal.mfa_at' => time(), 'portal.access_revision' => (int) $u->portal_access_revision]); } private function create(string $pilot = 'Cahora Bassa'): Opportunity { $response = $this->postJson('/minhas-oportunidades/drafts', F::input($pilot)); if ($response->status() !== 201) { fwrite(STDERR, substr($response->getContent(), 0, 400)."\n"); }$id = $response->assertCreated()->json('id'); return Opportunity::findOrFail($id); } private function publication(Opportunity $o, array $patch = []): array { $a = app(OpportunityManagement::class)->values($o)['proactive']; return [...['etag' => app(OpportunityManagement::class)->etag($o), 'action' => 'disclose', 'title' => $o->title, 'summary' => 'SYNTHETIC QA ideia em preparação; não é oportunidade com participação disponível.', 'description' => 'Conteúdo público expressamente redigido.', 'limitations' => 'Conclusões locais por confirmar.', 'source_reference' => 'SYNTHETIC QA exemplo público', 'edition' => 'QA1', 'source_date' => '2026-10-27', 'sector_id' => $o->sector_id, 'territory_ids' => [$o->primary_territory_id]], ...F::publication($a), ...$patch]; } private function publish(Opportunity $o): array { $p = $this->postJson('/minhas-oportunidades/'.$o->id.'/divulgacao/previa', $this->publication($o))->assertOk()->json(); $input = ['token' => $p['token'], 'operation_key' => (string) Str::uuid(), 'confirm' => true, 'reason' => 'SYNTHETIC QA explicit public projection reviewed without private information']; $this->postJson('/minhas-oportunidades/'.$o->id.'/divulgacao/confirmar', $input)->assertOk(); return $input; } public function test_legacy_records_without_analysis_add_no_publication_lookup(): void { $o = new Opportunity; $o->forceFill(['workflow_context' => []]); DB::enableQueryLog(); DB::flushQueryLog(); $this->assertNull(app(ProactiveInvestment::class)->publicData($o)); $this->assertSame([], DB::getQueryLog()); DB::disableQueryLog(); } public function test_create_recover_edit_with_etag_and_historical_foundation_without_financial_or_promoter_requirements(): void { $o = $this->create(); $a = app(OpportunityManagement::class)->values($o)['proactive']; $tag = app(OpportunityManagement::class)->etag($o); $this->assertNull($o->estimated_investment_amount); $this->assertNull($o->promoter_name); $this->assertNotNull($o->primary_territory_id); $this->get('/minhas-oportunidades/'.$o->id.'/edit')->assertOk()->assertInertia(fn ($page) => $page->where('values.proactive.need.state', 'hypothesis')->has('proactiveHistory', 1)); $a['need']['state'] = 'confirmed'; $a['need']['confirmation'] = 'SYNTHETIC QA confirmation of fixture conditions only, no real market validation.'; $this->putJson('/minhas-oportunidades/'.$o->id.'/draft', ['etag' => $tag, 'proactive' => $a, 'reason' => 'SYNTHETIC QA updated documented foundation for this fixture only'])->assertOk(); $this->assertSame('confirmed', app(OpportunityManagement::class)->values($o->fresh())['proactive']['need']['state']); $history = app(ProactiveInvestment::class)->history($o); $this->assertCount(2, $history); $this->assertSame('hypothesis', $history[1]['state']); $this->putJson('/minhas-oportunidades/'.$o->id.'/draft', ['etag' => $tag, 'proactive' => $a, 'reason' => 'SYNTHETIC QA stale version cannot overwrite current analysis'])->assertStatus(409); } public function test_hypothesis_cannot_be_confirmed_without_documented_conditions_and_missing_is_not_zero(): void { $o = $this->create(); $a = app(OpportunityManagement::class)->values($o)['proactive']; $a['need']['state'] = 'confirmed'; $a['need']['observed_demand'] = ''; $this->putJson('/minhas-oportunidades/'.$o->id.'/draft', ['etag' => app(OpportunityManagement::class)->etag($o), 'proactive' => $a, 'reason' => 'SYNTHETIC QA incomplete confirmation must preserve prior hypothesis'])->assertUnprocessable(); $this->assertSame('hypothesis', app(OpportunityManagement::class)->values($o->fresh())['proactive']['need']['state']); $a['need']['state'] = 'hypothesis'; $a['idea']['resources'] = ''; $this->putJson('/minhas-oportunidades/'.$o->id.'/draft', ['etag' => app(OpportunityManagement::class)->etag($o), 'proactive' => $a, 'reason' => 'SYNTHETIC QA unknown resource remains blank rather than zero'])->assertOk(); $this->assertNull(app(OpportunityManagement::class)->values($o->fresh())['proactive']['idea']['resources']); } public function test_methodology_alone_cannot_confirm_local_need(): void { $o = $this->create(); $a = app(OpportunityManagement::class)->values($o)['proactive']; $a['need']['state'] = 'confirmed'; $a['evidence'][0]['purpose'] = 'methodology'; $this->putJson('/minhas-oportunidades/'.$o->id.'/draft', ['etag' => app(OpportunityManagement::class)->etag($o), 'proactive' => $a, 'reason' => 'SYNTHETIC QA international methodology is not evidence of local demand'])->assertUnprocessable(); $this->assertSame('hypothesis', app(OpportunityManagement::class)->values($o->fresh())['proactive']['need']['state']); } public function test_creation_replay_and_duplicate_evidence_and_relations_do_not_duplicate_analysis(): void { $input = F::input(); $id = $this->postJson('/minhas-oportunidades/drafts', $input)->assertCreated()->json('id'); $this->postJson('/minhas-oportunidades/drafts', $input)->assertCreated()->assertJsonPath('id', $id); $o = Opportunity::findOrFail($id); $a = $input['proactive']; $a['evidence'][] = [...$a['evidence'][0], 'id' => (string) Str::uuid()]; $this->putJson('/minhas-oportunidades/'.$id.'/draft', ['etag' => app(OpportunityManagement::class)->etag($o), 'proactive' => $a, 'reason' => 'SYNTHETIC QA repeated evidence must be reconciled, not counted twice'])->assertUnprocessable(); $this->assertSame(1, Opportunity::where('management_creation_key', $input['creation_key'])->count()); } public function test_document_from_another_scope_cannot_be_attached_or_reclassified_as_public(): void { $o = $this->create(); $a = app(OpportunityManagement::class)->values($o)['proactive']; $a['evidence'][0]['document_id'] = (string) Str::uuid(); $this->putJson('/minhas-oportunidades/'.$o->id.'/draft', ['etag' => app(OpportunityManagement::class)->etag($o), 'proactive' => $a, 'reason' => 'SYNTHETIC QA document association must use existing access rules'])->assertUnprocessable(); } public function test_existing_permissions_reject_unassigned_and_unrelated_operators(): void { $o = $this->create(); $this->login(User::factory()->create()); $this->postJson('/minhas-oportunidades/drafts', F::input())->assertForbidden(); $this->get('/minhas-oportunidades/'.$o->id.'/edit')->assertForbidden(); $this->login(F::operator()); $this->get('/minhas-oportunidades/'.$o->id.'/edit')->assertForbidden(); $this->login($this->actor); $input = F::input(); unset($input['proactive']); $this->postJson('/minhas-oportunidades/drafts', $input)->assertForbidden(); $this->assertNull($o->organization_id); } public function test_internal_attached_document_cannot_be_changed_to_public_through_analysis(): void { $o = $this->create(); $doc = (string) Str::uuid(); $hash = hash('sha256', 'SYNTHETIC QA private source'); DB::table('documents')->insert(['id' => $doc, 'file_name' => 'SYNTHETIC-QA-INTERNAL-LIST.txt', 'storage_key' => 'synthetic-qa/'.$doc, 'media_type' => 'text/plain', 'sha256' => $hash, 'size_bytes' => strlen('SYNTHETIC QA private source'), 'uploaded_by' => $this->actor->id, 'status' => 'active', 'data_classification' => 'internal', 'malware_scan_status' => 'clean', 'malware_scanned_sha256' => $hash]); DB::table('opportunity_documents')->insert(['id' => (string) Str::uuid(), 'opportunity_id' => $o->id, 'document_id' => $doc, 'version' => 1, 'created_by' => $this->actor->id, 'payload' => json_encode(['title' => 'SYNTHETIC QA internal list', 'visibility' => 'internal'])]); $a = app(OpportunityManagement::class)->values($o)['proactive']; $a['evidence'][0]['document_id'] = $doc; $a['evidence'][0]['access'] = 'public'; $this->putJson('/minhas-oportunidades/'.$o->id.'/draft', ['etag' => app(OpportunityManagement::class)->etag($o), 'proactive' => $a, 'reason' => 'SYNTHETIC QA original document restrictions cannot be overridden here'])->assertUnprocessable(); $this->assertSame('internal', DB::table('documents')->where('id', $doc)->value('data_classification')); } public function test_selected_public_relationship_and_assumptions_travel_without_private_reference_or_duplicate(): void { $o = $this->create(); $a = app(OpportunityManagement::class)->values($o)['proactive']; $target = app(OpportunityClassification::class)->targets('project')->first(['id', 'name']); $this->assertNotNull($target); $r = ['id' => (string) Str::uuid(), 'kind' => 'project', 'target_id' => $target->id, 'relation' => 'hypothesis', 'reference' => 'PRIVATE-QA-RELATION-REFERENCE-NOT-FOR-PUBLICATION', 'evidence_ids' => [$a['evidence'][0]['id']]]; $a['relations'] = [$r]; $this->putJson('/minhas-oportunidades/'.$o->id.'/draft', ['etag' => app(OpportunityManagement::class)->etag($o), 'proactive' => $a, 'reason' => 'SYNTHETIC QA reference is a hypothesis, not access or contract'])->assertOk(); $o->refresh(); $bad = $a; $bad['relations'][] = [...$r, 'id' => (string) Str::uuid()]; $this->putJson('/minhas-oportunidades/'.$o->id.'/draft', ['etag' => app(OpportunityManagement::class)->etag($o), 'proactive' => $bad, 'reason' => 'SYNTHETIC QA same reference cannot appear twice in the same analysis'])->assertUnprocessable(); $d = $this->publication($o); $d['proactive_public'] = [...$d['proactive_public'], 'relation_ids' => [$r['id']], 'resources' => 'SYNTHETIC QA public resource, no guaranteed capacity.', 'assumptions' => 'No contract, land or power guaranteed.', 'conditions' => 'Confirm actual local evidence.']; $p = $this->postJson('/minhas-oportunidades/'.$o->id.'/divulgacao/previa', $d)->assertOk()->json(); $this->assertSame($target->name, $p['projection']['proactive']['relations'][0]['label']); $this->assertStringNotContainsString('PRIVATE-QA-RELATION', json_encode($p['projection'])); $this->postJson('/minhas-oportunidades/'.$o->id.'/divulgacao/confirmar', ['token' => $p['token'], 'operation_key' => (string) Str::uuid(), 'confirm' => true, 'reason' => 'SYNTHETIC QA explicitly reviewed public relationship and assumptions'])->assertOk(); $ref = app(InvestorContext::class)->reference('opportunity:'.$o->id); $this->assertSame('hypothesis', $ref['analysis']['proactive']['relations'][0]['relationship']); $this->assertSame('No contract, land or power guaranteed.', $ref['analysis']['proactive']['assumptions']); } public function test_publication_needs_explicit_review_and_rejects_internal_source_and_literal_leakage(): void { $o = $this->create(); $data = $this->publication($o); unset($data['proactive_reviewed']); $this->postJson('/minhas-oportunidades/'.$o->id.'/divulgacao/previa', $data)->assertUnprocessable(); $data = $this->publication($o); $a = app(OpportunityManagement::class)->values($o)['proactive']; $data['proactive_public']['evidence_ids'] = [$a['evidence'][1]['id']]; $this->postJson('/minhas-oportunidades/'.$o->id.'/divulgacao/previa', $data)->assertUnprocessable(); $data = $this->publication($o, ['description' => $a['evidence'][1]['statement']]); $this->postJson('/minhas-oportunidades/'.$o->id.'/divulgacao/previa', $data)->assertUnprocessable(); $data = $this->publication($o); $data['proactive_public']['state'] = 'confirmed'; $this->postJson('/minhas-oportunidades/'.$o->id.'/divulgacao/previa', $data)->assertUnprocessable(); } public function test_public_snapshot_separate_from_private_edits_and_anonymous_access_no_coordinates_required(): void { $o = $this->create(); $private = $o->management_draft; $confirm = $this->publish($o); $this->postJson('/minhas-oportunidades/'.$o->id.'/divulgacao/confirmar', $confirm)->assertOk()->assertJsonPath('replayed', true); $o->refresh(); $this->assertSame($private, $o->management_draft); $this->assertFalse(app(ProactiveInvestment::class)->publicData($o)['economic_validation']); $a = app(OpportunityManagement::class)->values($o)['proactive']; $a['idea']['solution'] = 'PRIVATE-QA-NEW-UNPUBLISHED-SOLUTION'; $this->putJson('/minhas-oportunidades/'.$o->id.'/draft', ['etag' => app(OpportunityManagement::class)->etag($o), 'proactive' => $a, 'reason' => 'SYNTHETIC QA private update must not change published projection'])->assertOk(); $this->app['auth']->forgetGuards(); $this->get('/oportunidades/'.$o->slug)->assertOk()->assertDontSee('PRIVATE-QA-'); $this->get('/minhas-oportunidades/'.$o->id.'/edit')->assertRedirect('/login'); $this->assertNotNull(app(ProactiveInvestment::class)->publicData($o->fresh())); } public function test_three_synthetic_pilots_reuse_existing_territories_and_catalogue_groups(): void { foreach (['Cahora Bassa', 'Montepuez', 'Gurué'] as $pilot) { $o = $this->create($pilot); $this->publish($o); $this->assertNotNull($o->fresh()->primary_territory_id); $this->assertTrue(app(InvestmentCatalogue::class)->query(['perspective' => 'ideas'])->whereKey($o->id)->exists()); $this->assertFalse(app(InvestmentCatalogue::class)->query(['perspective' => 'needs'])->whereKey($o->id)->exists()); } $this->get('/oportunidades?perspective=ideas')->assertOk(); } public function test_existing_facts_and_needs_do_not_require_irrelevant_idea_fields(): void { foreach (['exists', 'needs'] as $kind) { $input = F::input(); $input['proactive']['kind'] = $kind; $id = $this->postJson('/minhas-oportunidades/drafts', $input)->assertCreated()->json('id'); $o = Opportunity::findOrFail($id); $d = $this->publication($o); unset($d['proactive_public']['proposal']); if ($kind === 'exists') { unset($d['proactive_public']['need']); } $p = $this->postJson('/minhas-oportunidades/'.$id.'/divulgacao/previa', $d)->assertOk()->json(); $this->assertSame($kind, $p['projection']['proactive']['group']); $this->assertSame('', $p['projection']['proactive']['proposal']); $this->postJson('/minhas-oportunidades/'.$id.'/divulgacao/confirmar', ['token' => $p['token'], 'operation_key' => (string) Str::uuid(), 'confirm' => true, 'reason' => 'SYNTHETIC QA only applicable questions for this content group'])->assertOk(); $this->assertTrue(app(InvestmentCatalogue::class)->query(['perspective' => $kind])->whereKey($id)->exists()); } } public function test_private_analysis_complements_keep_preparation_valid_without_inventing_public_offers(): void { $private = $this->create('Gurué'); $public = $this->create(); $input = $this->publication($public, ['summary' => 'SYNTHETIC QA: silos e armazenagem como componente documentada.']); $plan = $this->postJson('/minhas-oportunidades/'.$public->id.'/divulgacao/previa', $input)->assertOk()->json(); $this->postJson('/minhas-oportunidades/'.$public->id.'/divulgacao/confirmar', ['token' => $plan['token'], 'operation_key' => (string) Str::uuid(), 'confirm' => true, 'reason' => 'SYNTHETIC QA public component in transaction only'])->assertOk(); $result = $this->postJson('/complementos-investimento', ['contexts' => [['origin' => 'opportunity:'.$private->id], ['origin' => 'opportunity:'.$public->id]]])->assertOk()->json(); $this->assertSame([], $result['issues']); $this->assertNotEmpty($result['items']); foreach ($result['items'] as $item) { $this->assertSame('opportunity:'.$public->id, $item['origin']['key']); } $this->assertStringNotContainsString('PRIVATE-QA-', json_encode($result)); $this->assertFalse(Opportunity::published()->whereKey($private->id)->exists()); $this->assertSame('draft', $private->fresh()->status); } public function test_private_analysis_complements_still_hide_unauthorised_sources(): void { $private = $this->create('Gurué'); $this->login(User::factory()->create()); $this->postJson('/complementos-investimento', ['origin' => 'opportunity:'.$private->id])->assertNotFound(); $result = $this->postJson('/complementos-investimento', ['contexts' => [['origin' => 'opportunity:'.$private->id]]])->assertOk()->json(); $this->assertSame([], $result['items']); $this->assertCount(1, $result['issues']); $this->assertStringNotContainsString($private->title, json_encode($result)); $this->assertStringNotContainsString('PRIVATE-QA-', json_encode($result)); } public function test_authenticated_multi_idea_preparation_keeps_each_origin_and_deliberate_edits_in_existing_dossier(): void { $first = $this->create(); $second = $this->create('Gurué'); $this->publish($first); $this->publish($second); $investor = User::factory()->create(); $this->login($investor); $key = (string) Str::uuid(); $create = ['title' => 'SYNTHETIC QA deliberately chosen project title', 'client_key' => $key, 'reuse_consent' => true]; $id = $this->postJson('/investimentos', $create)->assertCreated()->json('id'); $this->postJson('/investimentos', $create)->assertCreated()->assertJsonPath('id', $id); $before = $this->getJson('/investimentos/'.$id.'/contexto')->assertOk()->json(); $input = ['version' => $before['case']['version'], 'selection' => [], 'opportunity_choices' => array_map(fn ($o) => ['source' => 'opportunity:'.$o->id, 'territory' => 'territory:'.$o->primary_territory_id], [$first, $second])]; $preview = $this->postJson('/investimentos/'.$id.'/contexto/previa', $input)->assertOk()->json(); $this->assertCount(2, $preview['opportunity_choices']); $this->assertSame('SYNTHETIC QA deliberately chosen project title', $preview['fields']['project_title']); foreach ($preview['opportunity_choices'] as $c) { $this->assertSame('ideas', $c['opportunity']['analysis']['proactive']['group']); $this->assertStringNotContainsString('PRIVATE-QA-', json_encode($c)); } $confirm = [...$input, 'preview_hash' => $preview['preview_hash'], 'operation_key' => (string) Str::uuid(), 'confirm' => true]; $this->postJson('/investimentos/'.$id.'/contexto/confirmar', $confirm)->assertOk(); $this->postJson('/investimentos/'.$id.'/contexto/confirmar', $confirm)->assertOk()->assertJsonPath('replayed', true); $saved = $this->getJson('/investimentos/'.$id.'/contexto')->assertOk()->json(); $this->assertCount(2, $saved['context']['opportunity_choices']); $consumers = []; foreach ([$first, $second] as $record) { $consumer = $this->getJson('/investimentos/'.$id.'/contexto?opportunity=opportunity:'.$record->id)->assertOk()->json(); $snapshot = json_decode($consumer['prefill']['simulator']['context_applicability_snapshot'], true); $this->assertCount(1, $snapshot['opportunity_choices']); $this->assertSame('opportunity:'.$record->id, $snapshot['opportunity_choices'][0]['opportunity']['key']); $this->assertSame('ideas', $snapshot['opportunity_choices'][0]['opportunity']['analysis']['proactive']['group']); $this->assertArrayNotHasKey('tax_benefit', $consumer['prefill']['simulator']); $consumers[] = $consumer; }$saved['consumer_contexts'] = $consumers; $evidenceDirectory = base_path('outputs/proactive-investment-20261027'); if (! is_dir($evidenceDirectory)) { mkdir($evidenceDirectory, 0777, true); } file_put_contents($evidenceDirectory.'/recovered-context.json', json_encode($saved, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR)); $this->get('/investimentos/'.$id)->assertOk(); $this->login($this->actor); $this->getJson('/investimentos/'.$id.'/contexto')->assertForbidden(); } }