withoutVite(); config(['portal.profile_access_enabled' => true]); $owner = User::factory()->create(); app(PortalProfileAdministration::class)->insertGrant((string) Str::uuid(), User::factory()->create(), [ 'user_id' => $owner->id, 'role' => 'promoter', 'organization_id' => null, 'opportunity_types' => ['business_sale'], 'source' => 'synthetic-acceptance-audit', ]); $payload = ['creation_key' => (string) Str::uuid(), 'opportunity_category' => 'business_sale', 'title' => 'Synthetic acquisition acceptance', 'description' => 'Existing company description to reuse once in investment preparation.', 'private_offer' => ['price_mode' => 'amount', 'amount' => '120000.00', 'currency' => 'MZN', 'business' => ['revenue' => '60000.00', 'cash_flow' => '20000.00', 'currency' => 'MZN']], 'reason' => 'Synthetic final acceptance integration audit only.', ...$location]; $record = $this->actingAs($owner)->postJson('/minhas-oportunidades/ofertas/rascunhos', $payload)->assertCreated()->json('record'); return [$owner, $record, $payload]; } public function test_saved_offer_reuses_its_description_in_the_existing_investment_context(): void { [$owner, $record, $payload] = $this->prepare(); $reference = app(InvestorContext::class)->reference('opportunity:'.$record['id']); $this->assertSame('private', $reference['visibility']); $this->assertSame($payload['title'], $reference['fields']['project_title']); $this->assertSame($payload['description'], $reference['fields']['project_summary'] ?? null, 'Write once/read always: the existing description must not disappear at the investment-context boundary.'); $this->assertSame('/minhas-oportunidades/ofertas/'.$record['id'], $reference['href']); $this->assertArrayNotHasKey('private_offer', $reference); $this->assertArrayNotHasKey('revenue', $reference['fields']); $this->assertArrayNotHasKey('currency', $reference['fields']); } public function test_native_context_options_do_not_disclose_another_owners_private_offer(): void { [$owner, $record, $payload] = $this->prepare(); $this->getJson('/minhas-oportunidades/ofertas/'.$record['id'])->assertOk() ->assertJsonPath('record.values.private_offer.business.revenue', '60000.00') ->assertJsonPath('record.values.private_offer.business.cash_flow', '20000.00'); $this->assertFalse(Opportunity::published()->whereKey($record['id'])->exists()); $this->actingAs(User::factory()->create()); $this->assertNotContains('opportunity:'.$record['id'], array_column(app(InvestorContext::class)->options(), 'key')); $this->getJson('/minhas-oportunidades/ofertas/'.$record['id'])->assertForbidden(); } public function test_private_description_is_not_truncated_and_does_not_replace_an_existing_summary(): void { [$owner, $record, $payload] = $this->prepare(); $description = str_repeat('Synthetic complete description. ', 400); $this->postJson('/minhas-oportunidades/ofertas/'.$record['id'].'/guardar', [ 'etag' => $record['etag'], 'description' => $description, 'reason' => $payload['reason'], ])->assertOk(); $this->assertSame(trim($description), app(InvestorContext::class)->reference('opportunity:'.$record['id'])['fields']['project_summary']); DB::table('opportunities')->where('id', $record['id'])->update(['summary' => 'Deliberate existing summary']); $this->assertSame('Deliberate existing summary', app(InvestorContext::class)->reference('opportunity:'.$record['id'])['fields']['project_summary']); $this->assertSame(trim($description), Opportunity::findOrFail($record['id'])->description); } public function test_approved_offer_continues_in_same_dossier_location_analysis_saved_result_and_report_input(): void { Mail::fake(); config(['editorial_workflow.enabled' => true, 'investor-path.enabled' => true, 'suitecrm.enabled' => false, 'investmoz_ai.enabled' => false]); $country = (string) Str::uuid(); $province = (string) Str::uuid(); foreach ([[$country, 'country', null], [$province, 'province', $country]] as [$id, $type, $parent]) { DB::table('geo_locations')->insert(['id' => $id, 'code' => 'SYNTHETIC-'.$id, 'name' => 'Synthetic '.$type, 'location_type' => $type, 'parent_id' => $parent, 'data_classification' => 'public', // Same isolated reference contract as OpportunitySearchFixture; never an operational import. 'source_system' => 'cartografia-workbook', 'external_id' => '964a187ad32ea553b9e112cd159caa90b900050c42b7329594a75bcd798a96ff:'.$id]); } [$owner, $record, $payload] = $this->prepare(['province_id' => $province]); $contact = (string) Str::uuid(); DB::table('contacts')->insert(['id' => $contact, 'full_name' => 'Synthetic private contact', 'source_system' => 'portal-user-contact', 'external_id' => 'user:'.$owner->id]); $url = '/minhas-oportunidades/ofertas/'.$record['id']; $this->postJson($url.'/guardar', ['etag' => $record['etag'], 'private_offer' => [...$payload['private_offer'], 'contact_id' => $contact], 'reason' => $payload['reason']])->assertOk(); $this->postJson($url.'/submeter', ['etag' => app(OpportunityManagement::class)->etag(Opportunity::findOrFail($record['id'])), 'confirm' => true])->assertOk()->assertJsonPath('record.state', 'submitted'); $reviewer = User::factory()->withTwoFactor()->create(); app(PortalProfileAdministration::class)->insertGrant((string) Str::uuid(), User::factory()->create(), [ 'user_id' => $reviewer->id, 'role' => 'validator', 'responsibilities' => ['validation_coordinator'], 'opportunity_types' => ['business_sale'], 'source' => 'synthetic-acceptance-audit', ]); $this->actingAs($reviewer)->withSession(['portal.mfa_actor' => $reviewer->id, 'portal.mfa_at' => time(), 'portal.access_revision' => (int) $reviewer->fresh()->portal_access_revision]); foreach (['start_review', 'approve'] as $action) { $this->postJson($url.'/revisao', ['etag' => app(OpportunityManagement::class)->etag(Opportunity::findOrFail($record['id'])), 'action' => $action, 'reason' => 'Synthetic independent review for investment continuity.'])->assertOk(); } $offer = Opportunity::findOrFail($record['id']); $public = ['title' => 'Synthetic public acquisition', 'summary' => 'Approved synthetic public summary only.', 'category' => 'business_sale', 'province_id' => $province, 'district_id' => null, 'price_mode' => 'negotiable', 'amount' => null, 'currency' => null]; $hash = OfferPublicCatalogue::projectionHash($public); $context = $offer->workflow_context; $context['offer_publication'] = ['contract' => OfferPublicCatalogue::CONTRACT, 'qa_fixture' => true, 'revision' => 1, 'payload' => $public, 'approval' => ['state' => 'approved', 'hash' => $hash, 'actor_id' => $reviewer->id, 'at' => now()->subMinutes(2)->toIso8601String()], 'publication' => ['state' => 'published', 'hash' => $hash, 'actor_id' => User::factory()->create()->id, 'at' => now()->subMinute()->toIso8601String()]]; $offer->forceFill(['workflow_context' => $context])->save(); $this->getJson('/api/v1/offers')->assertOk()->assertJsonPath('results.total', 0); $this->app->instance(OfferPublicCatalogue::class, new OfferPublicCatalogue(true)); $response = $this->getJson('/api/v1/offers/'.$offer->id)->assertOk()->assertJsonPath('data.summary', $public['summary']); $this->assertStringNotContainsString($payload['description'], $response->getContent()); $this->assertStringNotContainsString('60000.00', $response->getContent()); $this->assertNull($offer->fresh()->published_at); $this->actingAs($owner); $case = $this->postJson('/investimentos', ['title' => $payload['title'], 'client_key' => (string) Str::uuid(), 'reuse_consent' => true])->assertCreated()->json('id'); $view = $this->getJson('/investimentos/'.$case.'/contexto')->assertOk()->json(); $body = ['version' => $view['case']['version'], 'selection' => ['opportunity' => 'opportunity:'.$offer->id], 'declarations' => ['currency' => 'MZN']]; $plan = $this->postJson('/investimentos/'.$case.'/contexto/previa', $body)->assertOk()->json(); $this->assertSame($payload['description'], $plan['fields']['project_summary']); $this->postJson('/investimentos/'.$case.'/contexto/confirmar', [...$body, 'preview_hash' => $plan['preview_hash'], 'operation_key' => (string) Str::uuid(), 'confirm' => true])->assertOk(); $view = $this->getJson('/investimentos/'.$case.'/contexto')->assertOk()->json(); $prefill = $view['prefill']['simulator']; $this->assertSame($province, $prefill['province_id']); $this->assertSame($payload['description'], $prefill['project_summary']); $this->assertStringNotContainsString('60000.00', json_encode($view, JSON_THROW_ON_ERROR)); $financial = ['version' => 1, 'data' => ['budgetPeriod' => 'Mensal', 'loan' => ['principal' => '100000', 'rate' => '0', 'months' => '24', 'rateType' => 'nominal', 'periodMonths' => '1', 'gracePeriods' => '0', 'upfrontFee' => '0', 'recurringFee' => '0'], 'breakEven' => ['fixed' => '3000', 'price' => '100', 'variable' => '50'], 'funding' => ['total' => '100000', 'secured' => '30000'], 'incomes' => [['label' => 'Synthetic scenario revenue', 'amount' => '10000']], 'expenses' => [['label' => 'Declared rent at synthetic province', 'amount' => '2000'], ['label' => 'Declared transport at synthetic province', 'amount' => '1000']]]]; $draft = $this->postJson('/api/v1/simulation-drafts', ['context' => $case, 'personal_snapshot' => [], 'economic_snapshot' => [...array_intersect_key($prefill, array_flip(['purpose', 'journey_type', 'project_title', 'project_summary', 'currency', 'province_id', 'context_applicability_snapshot'])), 'financial_tools_snapshot' => json_encode($financial, JSON_THROW_ON_ERROR)]], ['Idempotency-Key' => (string) Str::uuid()])->assertCreated()->json('data.id'); $saved = $this->getJson('/api/v1/simulation-drafts/'.$draft.'?context='.$case)->assertOk()->json('data.economic_snapshot'); $this->assertSame($payload['description'], $saved['project_summary']); $path = $this->getJson('/investimentos/'.$case)->assertOk()->json(); $this->postJson('/investimentos/'.$case.'/resultados', ['version' => $path['case']['version'], 'draft_id' => $draft, 'draft_version' => 1, 'context_revision' => $path['investment_context']['revision'], 'context_reviewed' => true])->assertOk(); auth()->logout(); $this->actingAs(User::findOrFail($owner->id)); $path = $this->getJson('/investimentos/'.$case)->assertOk()->json(); $this->assertSame($draft, $path['results'][0]['draft_id']); $this->assertSame($payload['description'], $path['investment_context']['fields']['project_summary']); $this->assertSame(1, Opportunity::count()); $this->assertSame(1, DB::table('desk_cases')->count()); $this->assertStringNotContainsString('60000.00', json_encode($path, JSON_THROW_ON_ERROR)); $output = base_path('outputs/offers-investment-continuity-20261114'); if (! is_dir($output)) { mkdir($output, 0777, true); } file_put_contents($output.'/integrated-report-input.json', json_encode(['path' => $path, 'saved' => $saved], JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)); $body = ['version' => $path['case']['version'], 'selection' => ['opportunity' => 'opportunity:'.$offer->id], 'declarations' => ['project_summary' => 'Deliberate investment-specific summary']]; $plan = $this->postJson('/investimentos/'.$case.'/contexto/previa', $body)->assertOk()->json(); $this->assertSame('Deliberate investment-specific summary', $plan['fields']['project_summary']); $this->postJson('/investimentos/'.$case.'/contexto/confirmar', [...$body, 'preview_hash' => $plan['preview_hash'], 'operation_key' => (string) Str::uuid(), 'confirm' => true])->assertOk(); $view = $this->getJson('/investimentos/'.$case.'/contexto')->assertOk()->json(); $this->postJson('/investimentos/'.$case.'/contexto/previa', ['version' => $view['case']['version'], 'selection' => ['opportunity' => 'opportunity:'.$offer->id]])->assertOk()->assertJsonPath('fields.project_summary', 'Deliberate investment-specific summary'); $this->assertSame($payload['description'], $offer->fresh()->description); $this->actingAs(User::factory()->create())->getJson('/investimentos/'.$case)->assertForbidden(); $this->getJson('/api/v1/simulation-drafts/'.$draft.'?context='.$case)->assertNotFound(); } }