assertSame('sector_context_20261115_test', $identity->name); $this->assertSame(55441, (int) $identity->port); $this->withoutVite(); Storage::fake('local'); config(['portal.legacy_content_qa_enabled' => true, 'investor-path.enabled' => true]); } private function source(string $sector, int $legacy = 999999): array { $s = Sector::firstOrCreate(['slug' => $sector], ['name' => 'SYNTHETIC '.$sector, 'is_active' => true]); $slug = match ($sector) { 'energia-especifica' => 'energia', default => $sector }; $external = 'post-'.$legacy.'-synthetic-sector-'.Str::random(10); $url = 'https://example.invalid/'.$slug; config(['legacy_content.articles.'.$slug => ['external_id' => $external, 'source_url' => $url, 'group' => $sector === 'mineracao' || $sector === 'energia-especifica' ? 'energia-mineracao' : $sector, 'sector_slug' => $sector, 'publishable' => true]]); $id = (string) Str::uuid(); $quote = 'SYNTHETIC: hotelaria, turismo cultural, conservacao; produção agrícola, processamento, logística, água, saneamento, construção, geração e distribuição de electricidade. Não representa uma oferta aberta.'; $bytes = json_encode(['format' => 'investmoz-legacy-article-v1', 'source_url' => $url, 'content_html' => '
'.$quote.'
', 'title' => 'SYNTHETIC context', 'author' => 'SYNTHETIC', 'legacy_id' => $legacy, 'legacy_category' => 'SYNTHETIC', 'published_at' => '2026-01-01', 'modified_at' => '2026-01-02'], JSON_THROW_ON_ERROR); Storage::disk('local')->put('synthetic/'.$id, $bytes); DB::table('documents')->insert(['id' => $id, 'source_system' => 'synthetic-test', 'external_id' => $external, 'file_name' => 'synthetic.json', 'media_type' => 'application/json', 'storage_key' => 'synthetic/'.$id, 'sha256' => hash('sha256', $bytes), 'malware_scanned_sha256' => hash('sha256', $bytes), 'malware_scan_status' => 'clean', 'size_bytes' => strlen($bytes), 'status' => 'active', 'rights_status' => 'authorized', 'data_classification' => 'public']); $resource = DB::table('portal_resources')->insertGetId(['document_id' => $id, 'title' => 'SYNTHETIC context', 'category' => 'education', 'topic' => $sector, 'published' => true, 'rights_status' => 'authorized', 'source_url' => $url, 'source_reference' => 'SYNTHETIC', 'metadata_version' => 1]); return [app(LegacyContent::class)->article($slug, false), $resource, $slug]; } public function test_five_sectors_save_the_exact_passage_in_the_same_dossier_without_an_advert_and_isolate_accounts(): void { $owner = User::factory()->create(); $stranger = User::factory()->create(); $country = (string) Str::uuid(); DB::table('geo_locations')->insert(['id' => $country, 'name' => 'SYNTHETIC country', 'code' => $country, 'location_type' => 'country', 'source_system' => 'cartografia-workbook', 'data_classification' => 'public', 'external_id' => '964a187ad32ea553b9e112cd159caa90b900050c42b7329594a75bcd798a96ff:QA:'.$country]); $territory = (string) Str::uuid(); DB::table('geo_locations')->insert(['id' => $territory, 'parent_id' => $country, 'name' => 'SYNTHETIC outside zones', 'code' => $territory, 'location_type' => 'province', 'source_system' => 'cartografia-workbook', 'data_classification' => 'public', 'external_id' => '964a187ad32ea553b9e112cd159caa90b900050c42b7329594a75bcd798a96ff:QA:'.$territory]); config(['investor-path.aggregate_supply_qa_enabled' => true]); foreach (['turismo', 'agronegocio', 'infraestruturas', 'mineracao', 'energia-especifica'] as $sector) { [$article, $resource, $slug] = $this->source($sector); $idea = $article['sector_analysis']['ideas'][0]; $input = ['evidence' => $idea['evidence']['key'], 'source_sha256' => $article['sha256'], 'source_version' => $article['version'], 'idea' => $idea['title']]; $this->actingAs($owner)->get('/investimentos/a-partir-de/'.$slug.'?'.http_build_query($input))->assertOk(); $before = DB::table('opportunities')->count(); $id = $this->postJson('/investimentos', ['title' => 'SYNTHETIC '.$sector, 'client_key' => (string) Str::uuid(), 'reuse_consent' => true, 'content_resource_id' => $resource, ...$input])->assertCreated()->json('id'); $view = $this->getJson('/investimentos/'.$id)->assertOk()->json(); $this->assertSame($id, $view['case']['id']); $this->assertSame($idea['evidence']['quote'], $view['content_sources'][0]['analyses'][0]['quote']); $this->assertSame($article['sha256'], $view['content_sources'][0]['analyses'][0]['source_sha256']); $this->assertFalse($view['content_sources'][0]['analyses'][0]['open_participation']); $this->assertSame($before, DB::table('opportunities')->count()); $context = $this->getJson('/investimentos/'.$id.'/contexto')->assertOk()->json(); $body = ['version' => $context['case']['version'], 'selection' => ['sector' => 'sector:'.$article['sector']['id'], 'territory' => 'territory:'.$territory], 'opportunity_choices' => [], 'declarations' => ['project_title' => 'SYNTHETIC '.$sector, 'products_services' => $idea['title'], 'currency' => 'MZN', 'investment_budget_min' => '1000000', 'investment_budget_max' => '5000000']]; $plan = $this->postJson('/investimentos/'.$id.'/contexto/previa', $body)->assertOk()->json(); $this->postJson('/investimentos/'.$id.'/contexto/confirmar', [...$body, 'preview_hash' => $plan['preview_hash'], 'operation_key' => (string) Str::uuid(), 'confirm' => true])->assertOk(); $context = $this->getJson('/investimentos/'.$id.'/contexto')->assertOk()->json(); $this->postJson('/investimentos/'.$id.'/contexto/oferta-agregada', ['version' => $context['case']['version'], 'operation_key' => (string) Str::uuid()])->assertOk()->assertJsonPath('analysis.coverage_unknown', true); $context = $this->getJson('/investimentos/'.$id.'/contexto')->assertOk()->json(); $economic = array_filter($context['prefill']['simulator'], fn ($k) => ! str_ends_with($k, '_label') && $k !== 'email', ARRAY_FILTER_USE_KEY); $economic = [...$economic, 'purpose' => 'own_investment', 'total_investment' => '2000000', 'secured_amount' => '1000000', 'annual_revenue' => '300000', 'planning_loan' => 'no']; $economic['financial_tools_snapshot'] = json_encode(['version' => 1, 'data' => ['budgetPeriod' => 'Mensal', 'loan' => ['principal' => '', 'rate' => '', 'months' => '', 'rateType' => 'nominal', 'periodMonths' => '1', 'gracePeriods' => '', 'upfrontFee' => '', 'recurringFee' => ''], 'breakEven' => ['fixed' => '100000', 'price' => '5000', 'variable' => '2000'], 'funding' => ['total' => '2000000', 'secured' => '1000000'], 'incomes' => [['label' => 'SYNTHETIC revenue', 'amount' => '300000']], 'expenses' => [['label' => 'SYNTHETIC costs', 'amount' => '200000']]]], JSON_THROW_ON_ERROR); $draft = $this->postJson('/api/v1/simulation-drafts', ['context' => $id, 'personal_snapshot' => [], 'economic_snapshot' => $economic], ['Idempotency-Key' => (string) Str::uuid()])->assertCreated()->json('data'); $view = $this->getJson('/investimentos/'.$id)->assertOk()->json(); $this->postJson('/investimentos/'.$id.'/resultados', ['version' => $view['case']['version'], 'draft_id' => $draft['id'], 'draft_version' => $draft['version_no'], 'context_revision' => $view['investment_context']['revision'], 'context_reviewed' => true])->assertOk(); $this->getJson('/investimentos/'.$id)->assertOk()->assertJsonCount(1, 'results')->assertJsonPath('results.0.preparation_snapshot.annual_revenue', '300000') ->assertJsonPath('content_sources.0.analyses.0.quote', $idea['evidence']['quote']); $this->actingAs($stranger)->getJson('/investimentos/'.$id)->assertForbidden(); $this->actingAs($owner); } } public function test_all_eleven_regions_keep_provincial_scope_and_never_assign_districts(): void { foreach (SectorContentAnalysis::REGIONS as $legacy => $name) { [$a] = $this->source('turismo', $legacy); $analysis = $a['sector_analysis']; $this->assertSame($name, $analysis['region']); $this->assertSame('province', $analysis['ideas'][0]['location_precision']); $this->assertArrayNotHasKey('district_id', $analysis['ideas'][0]); } } public function test_district_consultation_reuses_only_the_matching_provincial_reading(): void { [$article] = $this->source('turismo', 172237); $country = (string) Str::uuid(); $province = (string) Str::uuid(); $district = (string) Str::uuid(); foreach ([[$country, null, 'SYNTHETIC country', 'country'], [$province, $country, 'Maputo Província', 'province'], [$district, $province, 'SYNTHETIC district', 'district']] as [$id, $parent, $name, $type]) { DB::table('geo_locations')->insert(['id' => $id, 'parent_id' => $parent, 'name' => $name, 'code' => $id, 'location_type' => $type, 'source_system' => 'cartografia-workbook', 'data_classification' => 'public', 'external_id' => '964a187ad32ea553b9e112cd159caa90b900050c42b7329594a75bcd798a96ff:QA:'.$id]); } $readings = app(SectorContentAnalysis::class)->forTerritories([$district]); $this->assertCount(1, $readings); $this->assertSame($article['href'], $readings[0]['href']); $this->assertSame('Maputo Província', $readings[0]['analysis']['region']); $this->assertSame('province', $readings[0]['analysis']['ideas'][0]['location_precision']); $this->assertSame([], app(SectorContentAnalysis::class)->forTerritories([(string) Str::uuid()])); } public function test_changed_source_or_fabricated_passage_cannot_be_attached(): void { [$a, $resource] = $this->source('turismo'); $owner = User::factory()->create(); $id = $this->actingAs($owner)->postJson('/investimentos', ['title' => 'SYNTHETIC guard', 'client_key' => (string) Str::uuid(), 'reuse_consent' => true])->assertCreated()->json('id'); $version = $this->getJson('/investimentos/'.$id)->json('case.version'); $idea = $a['sector_analysis']['ideas'][0]; $input = ['version' => $version, 'resource_id' => $resource, 'evidence' => $idea['evidence']['key'], 'idea' => $idea['title'], 'source_version' => $a['version'], 'source_sha256' => str_repeat('0', 64)]; $this->postJson('/investimentos/'.$id.'/conteudos', $input)->assertConflict(); $this->postJson('/investimentos/'.$id.'/conteudos', [...$input, 'source_sha256' => $a['sha256'], 'idea' => 'Fabricated availability'])->assertUnprocessable(); $this->assertSame([], $this->getJson('/investimentos/'.$id)->json('content_sources')); } public function test_saved_passage_survives_editorial_changes_and_withdrawal_with_a_review_notice(): void { [$a, $resource] = $this->source('turismo'); $owner = User::factory()->create(); $idea = $a['sector_analysis']['ideas'][0]; $id = $this->actingAs($owner)->postJson('/investimentos', ['title' => 'SYNTHETIC deliberate title', 'client_key' => (string) Str::uuid(), 'reuse_consent' => true, 'content_resource_id' => $resource, 'evidence' => $idea['evidence']['key'], 'idea' => $idea['title'], 'source_sha256' => $a['sha256'], 'source_version' => $a['version']])->assertCreated()->json('id'); $this->getJson('/investimentos/'.$id)->assertOk()->assertJsonPath('content_sources.0.analysis_review_required', false); $selection = app(SectorContentAnalysis::class)->selection($a, ['evidence' => $idea['evidence']['key'], 'idea' => $idea['title'], 'source_sha256' => $a['sha256'], 'source_version' => $a['version']]); $this->assertStringNotContainsString('project_title=', app(SectorContentAnalysis::class)->continuation($id, $selection)); $version = $this->getJson('/investimentos/'.$id)->json('case.version'); $this->postJson('/investimentos/'.$id.'/conteudos', ['version' => $version, 'resource_id' => $resource, 'evidence' => $idea['evidence']['key'], 'idea' => $idea['title'], 'source_sha256' => $a['sha256'], 'source_version' => $a['version']])->assertOk(); $this->getJson('/investimentos/'.$id)->assertOk()->assertJsonCount(1, 'content_sources.0.analyses')->assertJsonPath('case.title', 'SYNTHETIC deliberate title'); DB::table('portal_resources')->where('id', $resource)->update(['metadata_version' => 2, 'editorial_content' => json_encode(['body' => 'SYNTHETIC amended source; the saved conclusion must not change.'], JSON_THROW_ON_ERROR)]); $this->getJson('/investimentos/'.$id)->assertOk()->assertJsonPath('case.title', 'SYNTHETIC deliberate title') ->assertJsonPath('content_sources.0.analysis_review_required', true)->assertJsonPath('content_sources.0.analyses.0.quote', $idea['evidence']['quote']); DB::table('portal_resources')->where('id', $resource)->update(['published' => false]); $this->getJson('/investimentos/'.$id)->assertOk()->assertJsonPath('content_sources.0.analysis_review_required', true) ->assertJsonPath('content_sources.0.analyses.0.source_version', 1)->assertJsonPath('content_sources.0.analyses.0.quote', $idea['evidence']['quote']); } }