withoutVite(); $this->owner = User::factory()->create(); $this->manager = User::factory()->create(); config(['investor-path.enabled' => true, 'portal.managers' => [$this->manager->email], 'suitecrm.enabled' => false, 'investmoz_ai.enabled' => false]); $identity = DB::selectOne('select current_database() as name, inet_server_port() as port'); $this->assertStringEndsWith('_test', $identity->name); $this->assertSame(55441, (int) $identity->port); Http::fake(); } private function context(string $title = 'Ideia sintética QA'): string { return $this->actingAs($this->owner)->postJson('/investimentos', ['title' => $title, 'client_key' => (string) Str::uuid(), 'reuse_consent' => true])->assertCreated()->json('id'); } private function pathView(string $id): array { return $this->actingAs($this->owner)->getJson('/investimentos/'.$id)->assertOk()->json(); } private function snapshot(string $title): array { return ['personal_snapshot' => [], 'economic_snapshot' => ['purpose' => 'simulate_only', 'project_title' => $title, 'project_summary' => 'Descrição guardada do investimento '.$title, 'financial_tools_snapshot' => json_encode(['version' => 1, 'data' => ['budgetPeriod' => 'Mensal', 'loan' => ['principal' => '100000', 'rate' => '0', 'months' => '24', 'rateType' => 'nominal', 'periodMonths' => '1', 'gracePeriods' => '0', 'upfrontFee' => '0', 'recurringFee' => '0'], 'breakEven' => ['fixed' => '1000', 'price' => '100', 'variable' => '50'], 'funding' => ['total' => '100000', 'secured' => '30000'], 'incomes' => [['label' => 'Receitas QA', 'amount' => '100']], 'expenses' => [['label' => 'Custos QA', 'amount' => '50']]]])]]; } public function test_existing_empty_desk_draft_can_explicitly_reuse_current_contextual_summary_without_overwriting_data(): void { $service = $this->actingAs($this->manager)->postJson('/management/balcao/servicos', ['code' => 'project-preparation', 'confirm' => true])->assertCreated()->json('id'); $id = $this->context('Dossier anterior à ferramenta'); $support = $this->postJson('/balcao-virtual/dossiers/'.$id.'/pedidos', ['service_version_id' => $service, 'client_key' => (string) Str::uuid()])->assertCreated()->json('id'); $data = [...$this->snapshot('Resumo posterior'), 'context' => $id]; $draft = $this->postJson('/api/v1/simulation-drafts', $data, ['Idempotency-Key' => (string) Str::uuid()])->assertCreated()->json('data.id'); $initial = $this->getJson('/balcao-virtual/pedidos/'.$support)->assertOk()->assertJsonPath('request.payload.description', '')->json(); $preview = $initial['toolPrefill'] ?? null; $this->assertNotNull($preview, 'The existing empty draft needs an explicit contextual reuse action.'); $this->assertSame($draft, $preview['draft_id']); $this->assertSame($data['economic_snapshot']['project_summary'], $preview['description']); $this->actingAs($this->manager)->getJson('/balcao-virtual/pedidos/'.$support)->assertJsonPath('toolPrefill', null); $this->actingAs($this->owner); $payload = [...$initial['request']['payload'], 'description' => $preview['description'], 'country' => 'PT', 'declared_origin_country_code' => 'ZA']; $source = ['draft_id' => $preview['draft_id'], 'draft_version' => $preview['draft_version'], 'snapshot_sha256' => $preview['snapshot_sha256']]; $this->putJson('/balcao-virtual/pedidos/'.$support, ['version' => $initial['request']['version'], 'payload' => $payload, 'tool_prefill' => [...$source, 'draft_id' => (string) Str::uuid()]])->assertUnprocessable(); $this->putJson('/balcao-virtual/pedidos/'.$support, ['version' => $initial['request']['version'], 'payload' => $payload, 'tool_prefill' => [...$source, 'draft_version' => $source['draft_version'] + 1]])->assertUnprocessable(); $this->getJson('/balcao-virtual/pedidos/'.$support)->assertJsonPath('request.version', $initial['request']['version'])->assertJsonPath('request.payload.description', ''); $this->assertDatabaseCount('desk_revisions', 0); $this->putJson('/balcao-virtual/pedidos/'.$support, ['version' => $initial['request']['version'], 'payload' => $payload, 'tool_prefill' => $source])->assertOk()->assertJsonPath('status', 'draft'); $recovered = $this->getJson('/balcao-virtual/pedidos/'.$support)->assertJsonPath('request.payload.description', $preview['description'])->assertJsonPath('request.payload.country', 'PT')->assertJsonPath('request.payload.declared_origin_country_code', 'ZA')->assertJsonPath('toolPrefill', null)->json(); $this->assertSame('Dossier anterior à ferramenta', $recovered['request']['payload']['title']); $this->assertDatabaseCount('desk_requests', 1); $this->assertDatabaseCount('desk_revisions', 1); $this->assertDatabaseHas('desk_events', ['request_id' => $support, 'action' => 'request_prefill_reused']); $this->putJson('/balcao-virtual/pedidos/'.$support, ['version' => $recovered['request']['version'], 'payload' => [...$payload, 'description' => 'Tentativa de substituir texto existente'], 'tool_prefill' => $source])->assertUnprocessable(); $this->actingAs(User::factory()->create())->putJson('/balcao-virtual/pedidos/'.$support, ['version' => $recovered['request']['version'], 'payload' => $payload, 'tool_prefill' => $source])->assertForbidden(); } public function test_two_independent_investments_recover_tool_evidence_support_and_internal_tasks_without_external_calls(): void { $service = $this->actingAs($this->manager)->postJson('/management/balcao/servicos', ['code' => 'project-preparation', 'confirm' => true])->assertCreated()->json('id'); $ids = [$this->context('A sem terra nem financiamento externo'), $this->context('B com terra e financiamento externo')]; $draftIds = []; foreach ($ids as $index => $id) { $view = $this->pathView($id); $this->assertCount(19, $view['steps']); $this->assertCount(5, $view['phases']); $this->postJson('/investimentos/'.$id.'/aplicabilidade', ['version' => $view['case']['version'], 'land_needed' => $index ? 'yes' : 'no', 'external_funding' => $index ? 'yes' : 'no', 'viability' => 'proceed', 'reason' => 'Cenário sintético declarado pelo promotor'])->assertOk(); $data = [...$this->snapshot($index ? 'B' : 'A'), 'context' => $id]; $key = (string) Str::uuid(); $created = $this->postJson('/api/v1/simulation-drafts', $data, ['Idempotency-Key' => $key])->assertCreated(); $draft = $created->json('data.id'); $draftIds[] = $draft; $this->postJson('/api/v1/simulation-drafts', $data, ['Idempotency-Key' => $key])->assertOk()->assertJsonPath('data.replayed', true); $this->postJson('/api/v1/simulation-drafts', $data, ['Idempotency-Key' => (string) Str::uuid()])->assertConflict(); $this->getJson('/api/v1/simulation-drafts/current?context='.$id)->assertJsonPath('data.id', $draft)->assertJsonPath('data.economic_snapshot.project_title', $index ? 'B' : 'A'); $returned = $this->postJson('/investimentos/'.$id.'/resultados', ['version' => $this->pathView($id)['case']['version'], 'draft_id' => $draft, 'draft_version' => 1])->assertOk()->json('result'); $this->postJson('/investimentos/'.$id.'/resultados', ['version' => 1, 'draft_id' => $draft, 'draft_version' => 1])->assertOk()->assertJsonPath('result.id', $returned['id']); $support = $this->postJson('/balcao-virtual/dossiers/'.$id.'/pedidos', ['service_version_id' => $service, 'client_key' => (string) Str::uuid()])->assertCreated()->json('id'); $this->getJson('/balcao-virtual/pedidos/'.$support)->assertJsonPath('request.status', 'draft')->assertJsonPath('request.payload.description', $data['economic_snapshot']['project_summary']); $v = $this->pathView($id); $this->assertSame($index ? 'applicable' : 'not_needed_for_preparation', collect($v['steps'])->firstWhere('code', 'S10')['applicability']); $this->postJson('/investimentos/'.$id.'/etapas/S10', ['version' => $v['case']['version'], 'state' => 'waiting_external', 'action' => 'prepare', 'reason' => 'Aguarda localização e informação do terreno'])->assertStatus($index ? 200 : 422); $task = $this->postJson('/investimentos/'.$id.'/tarefas', ['version' => $this->pathView($id)['case']['version'], 'code' => 'S02', 'title' => 'Análise de mercado independente', 'due_at' => now()->addWeek()->toIso8601String(), 'consent' => true])->assertCreated()->json('id'); $this->actingAs($this->manager)->postJson('/balcao-virtual/dossiers/'.$id.'/tarefas/'.$task, ['version' => 1, 'status' => 'in_progress', 'assignee_user_id' => $this->manager->id, 'reason' => 'Acompanhamento interno atribuído'])->assertOk(); $this->postJson('/balcao-virtual/dossiers/'.$id.'/tarefas/'.$task, ['version' => 2, 'status' => 'done', 'reason' => 'Evidência de mercado preparada enquanto a terra aguarda'])->assertOk(); $this->getJson('/investimentos/'.$id)->assertJsonPath('results', [])->assertJsonPath('tasks.0.status', 'done')->assertJsonPath('canWrite', false)->assertJsonPath('canReview', true); $this->getJson('/balcao-virtual/dossiers/'.$id)->assertJsonMissingPath('case.payload.investor_path'); // A fresh HTTP session and a newly resolved account recover only this context's data. $this->flushSession(); $this->actingAs(User::findOrFail($this->owner->id))->getJson('/investimentos/'.$id)->assertJsonPath('results.0.id', $returned['id'])->assertJsonPath('results.0.draft_id', $draft)->assertJsonPath('tasks.0.status', 'done'); $this->get('/investimentos/'.$id)->assertInertia(fn (Assert $page) => $page->component('investor-path/show')->has('steps', 19)->has('phases', 5)->where('case.id', $id)); } $this->assertNotSame($draftIds[0], $draftIds[1]); $this->actingAs($this->owner)->getJson('/api/v1/simulation-drafts/'.$draftIds[0].'?context='.$ids[1])->assertForbidden(); $this->putJson('/api/v1/simulation-drafts/'.$draftIds[0], [...$this->snapshot('Outra ideia'), 'context' => $ids[1]], ['If-Match' => '"sfd-'.$draftIds[0].'-v1"'])->assertForbidden(); $this->postJson('/investimentos/'.$ids[1].'/resultados', ['version' => $this->pathView($ids[1])['case']['version'], 'draft_id' => $draftIds[0], 'draft_version' => 1])->assertForbidden(); $this->actingAs($this->manager)->getJson('/api/v1/simulation-drafts/'.$draftIds[0])->assertNotFound(); $this->actingAs($this->owner)->getJson('/api/v1/simulation-drafts/current')->assertJsonPath('data', null); $this->assertDatabaseCount('simulation_form_drafts', 2); $this->assertDatabaseCount('journey_steps', 38); $this->assertDatabaseCount('desk_requests', 2); $this->assertDatabaseCount('tasks', 2); Http::assertNothingSent(); } public function test_permissions_stale_versions_and_real_dependencies_do_not_block_independent_preparation(): void { $id = $this->context(); $v = $this->pathView($id); $this->assertSame('undetermined', collect($v['steps'])->firstWhere('code', 'S10')['applicability']); $payload = ['version' => $v['case']['version'], 'state' => 'preparing', 'action' => 'prepare', 'reason' => 'Preparação independente']; $this->postJson('/investimentos/'.$id.'/etapas/S18', $payload)->assertOk(); $this->postJson('/investimentos/'.$id.'/etapas/S02', $payload)->assertConflict(); $payload['version'] = $this->pathView($id)['case']['version']; $this->postJson('/investimentos/'.$id.'/etapas/S18', [...$payload, 'action' => 'operate'])->assertUnprocessable(); $this->postJson('/investimentos/'.$id.'/etapas/S18', [...$payload, 'state' => 'completed'])->assertUnprocessable(); $this->postJson('/investimentos/'.$id.'/etapas/S02', [...$payload, 'state' => 'approved', 'action' => 'record_external'])->assertForbidden(); $this->actingAs($this->manager)->postJson('/investimentos/'.$id.'/etapas/S02', [...$payload, 'state' => 'approved', 'action' => 'record_external'])->assertUnprocessable(); $stranger = User::factory()->create(); $this->actingAs($stranger)->getJson('/investimentos/'.$id)->assertForbidden(); $this->postJson('/api/v1/simulation-drafts', [...$this->snapshot('Invasão'), 'context' => $id], ['Idempotency-Key' => (string) Str::uuid()])->assertForbidden(); $this->postJson('/investimentos/'.$id.'/etapas/S02', $payload)->assertForbidden(); $this->assertDatabaseCount('simulation_form_drafts', 0); $this->assertDatabaseCount('journey_steps', 19); $this->actingAs($this->owner)->get('/dashboard')->assertInertia(fn (Assert $page) => $page->where('summary', null)->where('investorPath.contexts.0.id', $id)); $this->actingAs($this->manager)->get('/dashboard')->assertInertia(fn (Assert $page) => $page->has('summary')->missing('investorPath')); } public function test_applicability_changes_preserve_old_branch_evidence_and_legacy_workflow(): void { $id = $this->context(); $workflow = DB::table('desk_cases')->where('id', $id)->value('workflow'); $facts = ['land_needed' => 'yes', 'external_funding' => 'unknown', 'viability' => 'reformulate', 'reason' => 'Preparação provisória']; $this->postJson('/investimentos/'.$id.'/aplicabilidade', ['version' => $this->pathView($id)['case']['version'], ...$facts])->assertOk(); $this->postJson('/investimentos/'.$id.'/etapas/S10', ['version' => $this->pathView($id)['case']['version'], 'state' => 'preparing', 'action' => 'prepare', 'reason' => 'Evidência anterior do terreno'])->assertOk(); $this->postJson('/investimentos/'.$id.'/aplicabilidade', ['version' => $this->pathView($id)['case']['version'], ...$facts, 'land_needed' => 'no'])->assertOk(); $step = collect($this->pathView($id)['steps'])->firstWhere('code', 'S10'); $this->assertSame('dispensed', $step['state']); $this->assertSame('preparing', $step['saved_state']); $this->assertSame('Evidência anterior do terreno', $step['detail']['reason']); $this->postJson('/investimentos/'.$id.'/aplicabilidade', ['version' => $this->pathView($id)['case']['version'], ...$facts])->assertOk(); $this->assertSame('preparing', collect($this->pathView($id)['steps'])->firstWhere('code', 'S10')['state']); $this->assertSame($workflow, DB::table('desk_cases')->where('id', $id)->value('workflow')); config(['investor-path.enabled' => false]); $this->getJson('/investimentos')->assertNotFound(); $this->getJson('/investimentos/'.$id)->assertNotFound(); $this->getJson('/balcao-virtual/dossiers/'.$id)->assertOk(); } }