withoutVite(); $this->withoutMiddleware(ThrottleRequests::class); $db = DB::selectOne('select current_database() as name,inet_server_port() as port'); $this->assertSame('simulator_fiscal_delta_20261021_test', $db->name); $this->assertSame(55441, (int) $db->port); config(['investor-path.enabled' => true, 'investor-path.private_preparation' => true, 'suitecrm.enabled' => false, 'investmoz_ai.enabled' => false]); $this->actor = User::factory()->create(); $this->actingAs($this->actor); $this->id = $this->postJson('/investimentos', ['title' => 'SYNTHETIC QA — âmbito específico', 'client_key' => (string) Str::uuid(), 'reuse_consent' => true])->assertCreated()->json('id'); $corridors = app(InvestmentCorridorContext::class); $records = []; foreach (['beira', 'limpopo'] as $corridor) { $entry = collect(app(CorridorAgribusinessOpportunities::class)->entries())->firstWhere('corridor', config('investment_corridors.'.$corridor.'.name')); $records[] = Opportunity::where('management_creation_key', $entry['key'])->firstOrFail(); } $sofala = collect($corridors->provinces('beira'))->firstWhere('source_code', '07')->id; $gaza = $corridors->provinces('limpopo')[0]->id; $manica = collect($corridors->provinces('beira'))->firstWhere('name', 'Manica')->id; $template = app(InvestmentZones::class)->visible()->whereIn('category', ['industrial_park', 'zee', 'zfi'])->firstOrFail(); foreach ([$sofala, $gaza, $manica] as $territory) { $p = $template->replicate(); $p->code = 'SYNTHETIC-QA-'.Str::uuid(); $p->name = 'SYNTHETIC QA — parque '.$territory; $p->primary_territory_id = $territory; $p->save(); $this->parks[] = $p; } $this->choices = [['source' => 'opportunity:'.$records[0]->id, 'corridor_key' => 'beira', 'territory' => 'territory:'.$sofala, 'park' => 'park:'.$this->parks[0]->id, 'activities' => ['production']], ['source' => 'opportunity:'.$records[1]->id, 'corridor_key' => 'limpopo', 'territory' => 'territory:'.$gaza, 'park' => 'park:'.$this->parks[1]->id, 'activities' => ['trade']]]; $this->confirm(); } private function confirm(): array { $v = $this->getJson('/investimentos/'.$this->id.'/contexto')->assertOk()->json(); $body = ['version' => $v['case']['version'], 'selection' => [], 'opportunity_choices' => $this->choices]; $plan = $this->postJson('/investimentos/'.$this->id.'/contexto/previa', $body)->assertOk()->json(); $this->postJson('/investimentos/'.$this->id.'/contexto/confirmar', [...$body, 'preview_hash' => $plan['preview_hash'], 'operation_key' => (string) Str::uuid(), 'confirm' => true])->assertOk(); return $this->getJson('/investimentos/'.$this->id.'/contexto')->assertOk()->json(); } private function treatment(string $key): array { return $this->getJson('/investimentos/'.$this->id.'?opportunity='.urlencode($key))->assertOk()->json(); } public function test_opportunity_decisions_steps_and_simulator_are_isolated_from_common_dossier(): void { $keys = array_column($this->choices, 'source'); foreach ($keys as $index => $key) { $v = $this->treatment($key); $this->postJson('/investimentos/'.$this->id.'/aplicabilidade', ['version' => $v['case']['version'], 'context_opportunity' => $key, 'land_needed' => $index ? 'yes' : 'no', 'external_funding' => 'unknown', 'viability' => 'proceed', 'reason' => 'QA decisão específica por oportunidade'])->assertOk(); } $a = $this->treatment($keys[0]); $b = $this->treatment($keys[1]); $this->assertSame('no', $a['facts']['land_needed']); $this->assertSame('yes', $b['facts']['land_needed']); $this->assertNull($a['tools']['land']); $this->assertNotNull($b['tools']['land']); $this->assertSame('not_needed_for_preparation', collect($a['steps'])->firstWhere('code', 'S10')['applicability']); $this->postJson('/investimentos/'.$this->id.'/etapas/S10', ['version' => $b['case']['version'], 'context_opportunity' => $keys[1], 'state' => 'preparing', 'action' => 'prepare', 'reason' => 'Preparação apenas da segunda oportunidade'])->assertOk(); $this->assertSame('preparing', collect($this->treatment($keys[1])['steps'])->firstWhere('code', 'S10')['saved_state']); $this->assertSame('not_started', collect($this->treatment($keys[0])['steps'])->firstWhere('code', 'S10')['saved_state']); $v = $this->getJson('/investimentos/'.$this->id.'/contexto?opportunity='.urlencode($keys[1]))->assertOk()->json(); $sim = $v['prefill']['simulator']; $snap = json_decode($sim['context_applicability_snapshot'], true); $this->assertSame($keys[1], $snap['consumer_opportunity']); $this->assertSame('yes', $snap['land_preparation']['need']); $this->assertSame($this->parks[1]->primary_territory_id, $sim['province_id']); $this->assertStringContainsString('opportunity=', $b['tools']['simulator']); $this->assertArrayNotHasKey('province_id', $this->getJson('/investimentos/'.$this->id.'/contexto')->json('prefill.simulator')); $payload = app(VirtualDesk::class)->decode(DB::table('desk_cases')->where('id', $this->id)->value('payload')); $this->assertArrayNotHasKey('facts', $payload['investor_path']); } public function test_land_shortcut_recovers_associated_request_even_when_other_request_is_newer(): void { $ids = []; foreach (array_reverse($this->choices) as $choice) { $ids[$choice['source']] = $this->postJson('/investidor/terra-duat', ['payload' => ['investor_path_case_id' => $this->id, 'context_opportunity' => $choice['source'], 'project_reference' => 'QA scope']])->assertCreated()->json('id'); } foreach ($ids as $key => $id) { $this->assertStringEndsWith($id, $this->treatment($key)['tools']['land']); } $unscoped = $this->getJson('/investimentos/'.$this->id)->assertOk()->json(); foreach ($ids as $id) { $this->assertStringNotContainsString($id, $unscoped['tools']['land']); } $this->getJson('/investimentos/'.$this->id.'?opportunity=opportunity:99999999')->assertUnprocessable(); } public function test_simulator_saves_recovers_selected_scope_and_rejects_foreign_choice(): void { $key = $this->choices[1]['source']; $sim = $this->getJson('/investimentos/'.$this->id.'/contexto?opportunity='.urlencode($key))->json('prefill.simulator'); $data = ['context' => $this->id, 'personal_snapshot' => [], 'economic_snapshot' => ['purpose' => 'simulate_only', 'project_title' => 'QA âmbito simulado', 'context_opportunity' => $key, 'context_applicability_snapshot' => $sim['context_applicability_snapshot'], 'financial_tools_snapshot' => json_encode(['version' => 1, 'data' => []])]]; $this->postJson('/api/v1/simulation-drafts', $data, ['Idempotency-Key' => (string) Str::uuid()])->assertCreated(); $this->getJson('/api/v1/simulation-drafts/current?context='.$this->id)->assertOk()->assertJsonPath('data.economic_snapshot.context_opportunity', $key); $this->actingAs($this->actor->fresh()); $saved = $this->getJson('/api/v1/simulation-drafts/current?context='.$this->id)->assertOk()->json('data.economic_snapshot'); $this->assertSame($key, json_decode($saved['context_applicability_snapshot'], true)['consumer_opportunity']); $current = $this->getJson('/api/v1/simulation-drafts/current?context='.$this->id)->json('data'); $case = $this->treatment($key); $this->postJson('/investimentos/'.$this->id.'/resultados', ['version' => $case['case']['version'], 'draft_id' => $current['id'], 'draft_version' => $current['version_no'], 'context_revision' => $case['investment_context']['revision'], 'context_reviewed' => true])->assertOk()->assertJsonPath('result.context_opportunity', $key); $other = $this->postJson('/investimentos', ['title' => 'QA identidade alheia ao dossier', 'client_key' => (string) Str::uuid(), 'reuse_consent' => true])->assertCreated()->json('id'); $this->postJson('/api/v1/simulation-drafts', [...$data, 'context' => $other], ['Idempotency-Key' => (string) Str::uuid()])->assertUnprocessable(); } public function test_authorised_common_contacts_are_reused_without_inventing_company_or_tax_identity(): void { $this->putJson('/investidor/profile', ['version' => 0, 'payload' => ['phone' => '+258840001234', 'country' => 'Portugal', 'sector' => 'Sector de preferência']])->assertOk(); $id = $this->postJson('/investimentos', ['title' => 'QA contactos autorizados', 'client_key' => (string) Str::uuid(), 'reuse_consent' => true])->assertCreated()->json('id'); $v = $this->getJson('/investimentos/'.$id.'/contexto')->assertOk()->json('prefill'); foreach (['land', 'simulator'] as $tool) { $this->assertSame('+258840001234', $v[$tool]['phone']); $this->assertSame($this->actor->email, $v[$tool]['email']); $this->assertArrayNotHasKey('tax_residency_country', $v[$tool]); $this->assertArrayNotHasKey('sector', $v[$tool]); } $this->assertSame($this->actor->name, $v['land']['full_name']); } public function test_idea_sector_and_territory_entries_keep_explicit_source_through_recovery(): void { $id = $this->postJson('/investimentos', ['title' => 'QA ideia livre', 'client_key' => (string) Str::uuid(), 'reuse_consent' => true])->assertCreated()->json('id'); $this->get('/investimentos/iniciar')->assertOk()->assertInertia(fn ($p) => $p->where('source', null)); $sector = DB::table('sectors')->where('is_active', true)->first(); foreach (['sector:'.$sector->id, 'territory:'.$this->parks[1]->primary_territory_id] as $source) { $this->get('/investimentos/'.$id.'/contexto/editar?source='.urlencode($source))->assertOk()->assertInertia(fn ($p) => $p->where('source.key', $source)); $slot = str_starts_with($source, 'sector:') ? 'sector' : 'territory'; $v = $this->getJson('/investimentos/'.$id.'/contexto')->json(); $body = ['version' => $v['case']['version'], 'selection' => [$slot => $source]]; $preview = $this->postJson('/investimentos/'.$id.'/contexto/previa', $body)->assertOk()->json(); $this->postJson('/investimentos/'.$id.'/contexto/confirmar', [...$body, 'preview_hash' => $preview['preview_hash'], 'operation_key' => (string) Str::uuid(), 'confirm' => true])->assertOk(); $this->actingAs($this->actor->fresh()); $this->getJson('/investimentos/'.$id.'/contexto')->assertOk()->assertJsonPath('context.selection.'.$slot.'.key', $source); } $view = $this->getJson('/investimentos/'.$id.'/contexto')->json('prefill'); $this->assertSame($this->parks[1]->primary_territory_id, $view['simulator']['province_id']); $this->assertSame($this->parks[1]->primary_territory_id, $view['land']['parcels'][0]['province_id']); $this->assertSame($id, $this->getJson('/investimentos/'.$id)->json('case.id')); } public function test_sector_entry_survives_authentication_redirect_with_same_source(): void { $sector = DB::table('sectors')->where('is_active', true)->first(); $url = '/investimentos/iniciar?source='.urlencode('sector:'.$sector->id); Auth::logout(); $this->get($url)->assertRedirect('/login')->assertSessionHas('url.intended', url($url)); $this->post('/login', ['email' => $this->actor->email, 'password' => 'password'])->assertRedirect(url($url)); $this->get($url)->assertOk()->assertInertia(fn ($p) => $p->where('source.key', 'sector:'.$sector->id)); } public function test_removing_first_keeps_second_identity_common_edits_and_historical_requests(): void { $before = $this->confirm(); $second = $before['context']['opportunity_choices'][1]; $key = $this->choices[1]['source']; $land = $this->postJson('/investidor/terra-duat', ['payload' => ['investor_path_case_id' => $this->id, 'context_opportunity' => $this->choices[0]['source'], 'project_reference' => 'QA histórico']])->assertCreated()->json('id'); $this->choices = [$this->choices[1]]; $v = $this->getJson('/investimentos/'.$this->id.'/contexto')->json(); $body = ['version' => $v['case']['version'], 'selection' => [], 'opportunity_choices' => $this->choices, 'declarations' => ['project_title' => 'Título deliberado comum']]; $preview = $this->postJson('/investimentos/'.$this->id.'/contexto/previa', $body)->assertOk()->json(); $this->postJson('/investimentos/'.$this->id.'/contexto/confirmar', [...$body, 'preview_hash' => $preview['preview_hash'], 'operation_key' => (string) Str::uuid(), 'confirm' => true])->assertOk(); $after = $this->getJson('/investimentos/'.$this->id.'/contexto')->assertOk()->json(); $this->assertSame($second, $after['context']['opportunity_choices'][0]); $this->assertSame('Título deliberado comum', $after['context']['fields']['project_title']); $this->getJson('/investidor/terra-duat/'.$land)->assertOk(); $this->assertStringNotContainsString($land, $this->treatment($key)['tools']['land']); } }