assertSame('investor_intent_20261115_test', $db->name); $this->assertSame(55441, (int) $db->port); $this->withoutVite(); config(['investor-path.enabled' => true, 'suitecrm.enabled' => false, 'investmoz_ai.enabled' => false]); } public function test_internal_business_observations_remain_closed_to_an_ordinary_investor(): void { $this->expectException(HttpException::class); app(ProactiveSupply::class)->authorize(User::factory()->create()); } public function test_demography_source_recovers_its_hierarchy_without_replacing_a_saved_location(): void { $owner = User::factory()->create(); $parent = null; $ids = []; foreach (['country', 'province', 'district'] as $type) { $id = (string) Str::uuid(); DB::table('geo_locations')->insert(['id' => $id, 'parent_id' => $parent, 'name' => 'SYNTHETIC '.$type, 'code' => $id, 'location_type' => $type, 'data_classification' => 'public', 'source_system' => 'cartografia-workbook', 'external_id' => '964a187ad32ea553b9e112cd159caa90b900050c42b7329594a75bcd798a96ff:QA:'.$id]); $ids[$type] = $id; $parent = $id; } $id = $this->actingAs($owner)->postJson('/investimentos', ['title' => 'SYNTHETIC demography source', 'client_key' => (string) Str::uuid(), 'reuse_consent' => true])->assertCreated()->json('id'); $url = '/investimentos/'.$id.'/contexto/editar?'.http_build_query(['source' => 'territory:'.$ids['district']]); $this->get($url)->assertOk()->assertInertia(fn ($page) => $page->where('entryTerritory.id', $ids['district'])->where('territorySelection.0.id', $ids['province'])->where('territorySelection.1.id', $ids['district'])); $version = $this->getJson('/investimentos/'.$id.'/contexto')->assertOk()->json('case.version'); $body = ['version' => $version, 'selection' => ['territory' => 'territory:'.$ids['province']], 'opportunity_choices' => [], 'declarations' => []]; $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(); $this->get($url)->assertOk()->assertInertia(fn ($page) => $page->where('initial.context.selection.territory.id', $ids['province'])->where('territorySelection.0.id', $ids['province'])->has('territorySelection', 1)); $this->actingAs(User::factory()->create())->get($url)->assertForbidden(); } public function test_an_own_idea_preserves_sector_locality_and_budget_interval_without_any_advert_or_corridor(): void { $owner = User::factory()->create(); $parent = null; $path = []; foreach (['country' => 'QA Mozambique', 'province' => 'Maputo Provincia', 'district' => 'Boane', 'admin_post' => 'Matola Rio', 'locality' => 'Matola Rio-Sede'] as $type => $name) { $id = (string) Str::uuid(); DB::table('geo_locations')->insert(['id' => $id, 'parent_id' => $parent, 'name' => 'SYNTHETIC '.$name, 'code' => $id, 'location_type' => $type, 'data_classification' => 'public', 'source_system' => 'cartografia-workbook', 'external_id' => '964a187ad32ea553b9e112cd159caa90b900050c42b7329594a75bcd798a96ff:QA:'.$id]); $path[$type.'_id'] = $id; $parent = $id; } $reviewer = User::factory()->create(); $zone = InvestmentZone::query()->create(['id' => (string) Str::uuid(), 'code' => 'SINT-QA-'.Str::random(8), 'name' => 'SYNTHETIC documented site', 'category' => 'industrial_park', 'physical_category' => 'industrial_park', 'primary_territory_id' => $parent, 'publication_status' => 'published', 'validation_status' => 'validated', 'editor_id' => $owner->id, 'reviewer_id' => $reviewer->id, 'published_at' => now(), 'source_evidence' => [['source_type' => 'promoter', 'url' => 'https://example.invalid/synthetic-qa', 'supports' => 'Synthetic QA only, no operational or legal assertion.']]]); $zones = $this->actingAs($owner)->getJson('/investimentos/opcoes-corredor?include_corridors=0&territory='.$parent); $this->assertSame(200, $zones->status(), $zones->getContent()); $zones->assertJsonPath('parks.0.id', $zone->id)->assertJsonCount(1, 'parks'); $this->getJson('/investimentos/opcoes-corredor?include_corridors=0')->assertOk()->assertJsonPath('parks', []); config(['investor-path.aggregate_supply_qa_enabled' => true]); $source = hash('sha256', 'Synthetic private aggregate source'); DB::table('proactive_supply_sources')->insert(['sha256' => $source, 'name' => 'PRIVATE-SOURCE-NOT-FOR-OUTPUT.xlsx', 'period' => '2020', 'limitations' => 'Synthetic only', 'receipt' => '{}', 'imported_at' => now()]); $base = ['source_hash' => $source, 'sheet' => 'PRIVATE-SHEET', 'entity_key' => hash('sha256', 'entity-a'), 'name' => 'PRIVATE-COMPANY-NAME', 'province' => 'Synthetic', 'district' => 'Synthetic', 'cae' => '01110', 'activity' => 'PRIVATE-CONTACT-+258000000000', 'sector' => 'PRIVATE-RAW-SECTOR', 'source_date' => '2020', 'fingerprint' => hash('sha256', 'fingerprint'), 'duplicate_of' => null, 'identity_conflict' => false, 'territory_id' => $path['district_id'], 'territory_resolution' => 'exact_normalized_province_district']; foreach ([1 => [], 2 => ['duplicate_of' => hash('sha256', 'row-1')], 3 => ['identity_conflict' => true, 'entity_key' => hash('sha256', 'uncertain')], 4 => ['entity_key' => hash('sha256', 'unclassified'), 'cae' => 'PRIVATE-UNCLASSIFIED-TEXT']] as $number => $changes) { DB::table('proactive_supply_observations')->insert([...$base, 'id' => hash('sha256', 'row-'.$number), 'row_number' => $number, ...$changes]); } foreach (['Agronegocio' => 'agro-qa', 'Criar Hotel' => 'hotel-qa'] as $title => $slug) { $sector = Sector::create(['name' => 'SYNTHETIC '.$slug, 'slug' => $slug, 'is_active' => true]); $id = $this->actingAs($owner)->postJson('/investimentos', ['title' => $title, 'client_key' => (string) Str::uuid(), 'reuse_consent' => true])->assertCreated()->json('id'); $view = $this->getJson('/investimentos/'.$id.'/contexto')->assertOk()->json(); $entry = [...array_diff_key($path, ['country_id' => true]), 'sector_id' => (string) $sector->id, 'currency' => 'MZN', 'investment_budget_min' => '1000000', 'investment_budget_max' => '5000000']; $this->get('/investimentos/iniciar?'.http_build_query($entry))->assertOk()->assertInertia(fn ($page) => $page->where('entryQuery.investment_budget_max', '5000000')->where('entryQuery.locality_id', $parent)); $this->get('/investimentos/'.$id.'/contexto/editar?'.http_build_query($entry))->assertOk()->assertInertia(fn ($page) => $page->where('entryTerritory.id', $parent)->where('entryIdea.sector_id', (string) $sector->id)); $body = ['version' => $view['case']['version'], 'selection' => ['sector' => 'sector:'.$sector->id, 'territory' => 'territory:'.$parent], 'opportunity_choices' => [], 'declarations' => ['project_title' => $title, 'products_services' => 'SYNTHETIC explicit activity '.$slug, '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(); $saved = $this->getJson('/investimentos/'.$id.'/contexto')->assertOk()->assertJsonPath('prefill.simulator.locality_id', $parent) ->assertJsonPath('prefill.simulator.investment_budget_min', '1000000')->assertJsonPath('prefill.simulator.investment_budget_max', '5000000') ->assertJsonPath('prefill.simulator.project_title', $title)->json(); $this->assertSame('SYNTHETIC explicit activity '.$slug, $saved['prefill']['simulator']['products_services']); $this->assertEmpty($saved['context']['opportunity_choices'] ?? []); $this->assertEmpty($saved['context']['selection']['opportunity'] ?? null); $this->assertEmpty($saved['prefill']['simulator']['total_investment'] ?? null); $command = ['version' => $saved['case']['version'], 'operation_key' => (string) Str::uuid()]; $url = '/investimentos/'.$id.'/contexto/oferta-agregada'; $aggregate = $this->postJson($url, $command)->assertOk()->assertJsonPath('analysis.entities', 2)->assertJsonPath('analysis.repeated', 1)->assertJsonPath('analysis.uncertain', 1) ->assertJsonPath('analysis.broader_context', true)->assertJsonPath('analysis.scope.id', $path['district_id']); $this->assertStringNotContainsString('PRIVATE-', $aggregate->getContent()); $this->assertStringNotContainsString($source, $aggregate->getContent()); $this->postJson($url, $command)->assertOk()->assertJsonPath('replayed', true); $this->getJson('/investimentos/'.$id)->assertOk()->assertJsonCount(1, 'supply_aggregates')->assertJsonPath('supply_aggregates.0.entities', 2); config(['investor-path.aggregate_supply_qa_enabled' => false]); $this->postJson($url, $command)->assertNotFound(); $this->getJson('/investimentos/'.$id)->assertOk()->assertJsonPath('supply_aggregates', [])->assertJsonPath('supply_aggregate_enabled', false); config(['investor-path.aggregate_supply_qa_enabled' => true]); $this->actingAs(User::factory()->create())->postJson($url, $command)->assertForbidden(); $this->actingAs($owner); // UI keeps display labels in selection_labels, outside canonical fields. $economic = array_filter($saved['prefill']['simulator'], fn ($key) => ! str_ends_with($key, '_label') && $key !== 'email', ARRAY_FILTER_USE_KEY); $economic['purpose'] = 'own_investment'; // Explicit synthetic scenario; the budget interval remains separate. $economic['total_investment'] = '2000000'; $economic['secured_amount'] = '1000000'; $economic['annual_revenue'] = '300000'; $economic['planning_loan'] = 'no'; $economic['planning_assumptions_snapshot'] = json_encode(['version' => 1, 'data' => ['model' => 'other', 'capacity' => '100', 'price' => '250', 'occupancy' => '', 'frequency' => '12', 'source' => 'Synthetic explicit assumptions', 'costs' => []]], JSON_THROW_ON_ERROR); $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 declared revenue', 'amount' => '300000']], 'expenses' => [['label' => 'Synthetic declared costs', 'amount' => '200000']]]], JSON_THROW_ON_ERROR); $allowed = explode(',', substr((new StoreSimulationFormDraftRequest)->rules()['economic_snapshot'][1], 6)); $this->assertSame([], array_values(array_diff(array_keys($economic), $allowed))); $simulation = $this->postJson('/api/v1/simulation-drafts', ['context' => $id, 'personal_snapshot' => [], 'economic_snapshot' => $economic], ['Idempotency-Key' => (string) Str::uuid()])->assertCreated()->json('data.id'); $draft = $this->getJson('/api/v1/simulation-drafts/'.$simulation)->assertOk()->assertJsonPath('data.economic_snapshot.investment_budget_min', '1000000')->assertJsonPath('data.economic_snapshot.investment_budget_max', '5000000')->assertJsonPath('data.economic_snapshot.financial_tools_snapshot', $economic['financial_tools_snapshot'])->json('data'); $dossier = $this->getJson('/investimentos/'.$id)->assertOk()->json(); $return = ['version' => $dossier['case']['version'], 'draft_id' => $simulation, 'draft_version' => $draft['version_no'], 'context_revision' => $dossier['investment_context']['revision'], 'context_reviewed' => true]; $result = $this->postJson('/investimentos/'.$id.'/resultados', $return)->assertOk()->assertJsonPath('result.financial_snapshot', $economic['financial_tools_snapshot'])->assertJsonPath('result.preparation_snapshot.planning_loan', 'no')->assertJsonPath('result.preparation_snapshot.annual_revenue', '300000')->assertJsonPath('result.preparation_snapshot.planning_assumptions_snapshot', $economic['planning_assumptions_snapshot'])->json('result.id'); $this->postJson('/investimentos/'.$id.'/resultados', $return)->assertOk()->assertJsonPath('result.id', $result); $this->getJson('/investimentos/'.$id)->assertOk()->assertJsonCount(1, 'results')->assertJsonPath('results.0.id', $result)->assertJsonPath('results.0.currency', 'MZN'); $this->actingAs(User::factory()->create())->getJson('/api/v1/simulation-drafts/'.$simulation)->assertNotFound(); $this->actingAs($owner); $this->getJson('/investimentos/'.$id.'/contexto')->assertOk()->assertJsonPath('prefill.simulator.investment_budget_max', '5000000'); $this->actingAs(User::factory()->create())->getJson('/investimentos/'.$id.'/contexto')->assertForbidden(); } } public function test_an_incomplete_currency_can_be_recovered_privately_but_cannot_be_confirmed(): void { $this->actingAs(User::factory()->create()); $id = $this->postJson('/investimentos', ['title' => 'Synthetic draft', 'client_key' => (string) Str::uuid(), 'reuse_consent' => true])->assertCreated()->json('id'); $view = $this->getJson('/investimentos/'.$id.'/contexto')->assertOk()->json(); $body = ['version' => $view['case']['version'], 'selection' => [], 'declarations' => ['currency' => 'MZ']]; $this->postJson('/investimentos/'.$id.'/contexto/rascunho', [...$body, 'draft_sequence' => 1])->assertOk(); $this->assertSame('MZ', app(InvestorContext::class)->draft(auth()->user(), $id)['declarations']['currency']); $this->postJson('/investimentos/'.$id.'/contexto/previa', $body)->assertUnprocessable()->assertJsonValidationErrors('declarations.currency'); $body['declarations'] = ['currency' => 'MZN', 'investment_budget_min' => '5000000', 'investment_budget_max' => '1000000']; $this->postJson('/investimentos/'.$id.'/contexto/previa', $body)->assertUnprocessable()->assertJsonValidationErrors('declarations.investment_budget_max'); } public function test_district_demography_is_never_presented_as_locality_population(): void { $owner = User::factory()->create(); $parent = null; $district = null; foreach (['country', 'province', 'district', 'admin_post', 'locality'] as $type) { $id = (string) Str::uuid(); DB::table('geo_locations')->insert(['id' => $id, 'parent_id' => $parent, 'name' => 'SYNTHETIC '.$type, 'code' => $id, 'location_type' => $type, 'data_classification' => 'public', 'source_system' => 'cartografia-workbook', 'external_id' => '964a187ad32ea553b9e112cd159caa90b900050c42b7329594a75bcd798a96ff:QA:'.$id]); if ($type === 'district') { $district = $id; } $parent = $id; } $batch = (string) Str::uuid(); DB::table('demographic_imports')->insert(['id' => $batch, 'sha256' => hash('sha256', $batch), 'file_name' => 'SYNTHETIC.xlsx', 'source' => 'Synthetic only', 'publication_version' => 'QA/1', 'status' => 'approved', 'report' => '{}', 'preview_sha256' => str_repeat('0', 64), 'prepared_by' => $owner->id, 'created_at' => now(), 'updated_at' => now()]); DB::table('demographic_observations')->insert(['import_id' => $batch, 'geo_location_id' => $district, 'year' => 2026, 'indicator' => 'population', 'sex' => 'all', 'residence' => 'all', 'age_group' => 'all', 'unit' => 'pessoas', 'data_kind' => 'projected', 'original_value' => '12345', 'numeric_value' => '12345', 'sheet' => '2026', 'cell' => 'B4', 'validation_status' => 'valid']); $url = '/api/v1/investment/demography?'.http_build_query(['territories' => [$parent], 'start_year' => 2026, 'investment_context' => 1]); $this->getJson($url)->assertOk()->assertJsonPath('broader_context', true)->assertJsonPath('requested_territory.id', $parent) ->assertJsonPath('territories.0.id', $district)->assertJsonPath('territories.0.level', 'district')->assertJsonPath('territories.0.series.0.values.0.original_value', '12345'); DB::table('geo_locations')->where('id', $parent)->update(['data_classification' => 'internal']); $this->getJson($url)->assertOk()->assertJsonPath('requested_territory', null)->assertJsonPath('territories', []); } public function test_guided_unknown_answers_and_their_labels_save_and_recover_in_the_existing_simulator_contract(): void { $this->actingAs(User::factory()->create()); $economic = ['purpose' => 'simulate_only', 'journey_type' => 'investor']; $economic['preparation_step'] = 'operating'; $economic['fiscal_tools_selection'] = ['tax:sales', 'iva:acquisitions']; $economic['initial_budget_snapshot'] = json_encode(['version' => 1, 'rows' => [['label' => 'Synthetic equipment', 'amount' => '1000000']]], JSON_THROW_ON_ERROR); $economic['planning_assumptions_snapshot'] = json_encode(['version' => 1, 'data' => ['model' => 'hotel', 'capacity' => '10', 'price' => '5000', 'occupancy' => '50', 'frequency' => '300', 'source' => 'Synthetic only', 'costs' => []]], JSON_THROW_ON_ERROR); foreach (['customer_validation_status', 'location_assessment_status', 'cost_estimate_status', 'receivables_timing_status', 'funding_status'] as $field) { $economic[$field] = 'unknown'; $economic['selection_labels'][$field] = 'Ainda não sei'; } $id = $this->postJson('/api/v1/simulation-drafts', ['personal_snapshot' => [], 'economic_snapshot' => $economic], ['Idempotency-Key' => (string) Str::uuid()])->assertCreated()->json('data.id'); $this->getJson('/api/v1/simulation-drafts/'.$id)->assertOk()->assertJsonPath('data.economic_snapshot.selection_labels.funding_status', 'Ainda não sei')->assertJsonPath('data.economic_snapshot.preparation_step', 'operating')->assertJsonPath('data.economic_snapshot.planning_assumptions_snapshot', $economic['planning_assumptions_snapshot'])->assertJsonPath('data.economic_snapshot.fiscal_tools_selection', $economic['fiscal_tools_selection'])->assertJsonPath('data.economic_snapshot.initial_budget_snapshot', $economic['initial_budget_snapshot']); $economic['selection_labels']['unsupported_field'] = 'Unsupported'; $this->postJson('/api/v1/simulation-drafts', ['personal_snapshot' => [], 'economic_snapshot' => $economic], ['Idempotency-Key' => (string) Str::uuid()])->assertUnprocessable()->assertJsonValidationErrors('economic_snapshot.selection_labels.unsupported_field'); } }