withoutVite(); $identity = DB::selectOne('select current_database() as name, inet_server_port() as port'); $this->assertStringEndsWith('_test', $identity->name); $this->assertSame(55441, (int) $identity->port); $this->investor = User::factory()->create(); $this->manager = User::factory()->create(); config(['portal.managers' => [$this->manager->email], 'investmoz_ai.enabled' => false, 'suitecrm.enabled' => false]); Http::fake(); } private function dossier(array $extra = []): string { return $this->actingAs($this->investor)->postJson('/balcao-virtual/dossiers', ['title' => 'Ideia fictícia QA', 'client_key' => (string) Str::uuid(), 'reuse_consent' => true, ...$extra])->assertCreated()->json('id'); } private function guidance(string $id): array { return $this->actingAs($this->investor)->getJson('/balcao-virtual/dossiers/'.$id.'/orientacao')->assertOk()->json(); } private function service(): string { return $this->actingAs($this->manager)->postJson('/management/balcao/servicos', ['code' => 'project-preparation', 'confirm' => true])->assertCreated()->json('id'); } public function test_desk_declared_origin_is_recovered_and_invalid_country_preserves_draft(): void { $case = $this->dossier(); $service = $this->service(); $id = $this->actingAs($this->investor)->postJson('/balcao-virtual/dossiers/'.$case.'/pedidos', ['service_version_id' => $service, 'client_key' => (string) Str::uuid()])->assertCreated()->json('id'); $url = '/balcao-virtual/pedidos/'.$id; $this->getJson($url)->assertOk()->assertJsonMissingPath('request.payload.declared_origin_country_code'); $this->putJson($url, ['version' => 1, 'payload' => ['title' => 'Preparação QA', 'declared_origin_country_code' => 'PT']])->assertOk(); $this->actingAs(User::factory()->create())->getJson($url)->assertForbidden(); $this->actingAs($this->investor)->getJson($url)->assertOk()->assertJsonPath('request.payload.declared_origin_country_code', 'PT')->assertJsonPath('request.status', 'draft'); $this->putJson($url, ['version' => 2, 'payload' => ['title' => 'Preparação QA', 'declared_origin_country_code' => 'ZZ']])->assertUnprocessable()->assertJsonValidationErrors('payload.declared_origin_country_code'); $this->getJson($url)->assertJsonPath('request.payload.declared_origin_country_code', 'PT')->assertJsonPath('request.version', 2); $this->assertDatabaseCount('interest_requests', 0); Http::assertNothingSent(); } public function test_panel_creation_redirect_confirms_persistence_and_never_submits_a_request(): void { $payload = ['title' => 'Dossier de teste QA do painel', 'client_key' => (string) Str::uuid(), 'reuse_consent' => true]; $response = $this->actingAs($this->investor)->post('/balcao-virtual/dossiers', $payload); $case = DB::table('desk_cases')->where('client_key', $payload['client_key'])->first(); $this->assertNotNull($case); $response->assertRedirect('/balcao-virtual/dossiers/'.$case->id)->assertSessionHas('success'); $this->post('/balcao-virtual/dossiers', $payload)->assertRedirect('/balcao-virtual/dossiers/'.$case->id); $this->assertDatabaseCount('desk_cases', 1); $this->assertDatabaseCount('desk_requests', 0); $this->getJson('/balcao-virtual/painel')->assertJsonPath('cases.0.id', $case->id); $this->getJson('/balcao-virtual/dossiers/'.$case->id)->assertJsonPath('case.title', $payload['title']); $this->post('/balcao-virtual/dossiers', [...$payload, 'reuse_consent' => false, 'client_key' => (string) Str::uuid()])->assertSessionHasErrors('reuse_consent'); $this->assertDatabaseCount('desk_cases', 1); } public function test_message_kind_is_recorded_at_send_time_and_legacy_is_not_reinterpreted(): void { $id = $this->dossier(); $service = $this->service(); $requestId = $this->actingAs($this->investor)->postJson('/balcao-virtual/dossiers/'.$id.'/pedidos', ['service_version_id' => $service, 'client_key' => (string) Str::uuid()])->json('id'); $this->actingAs($this->manager)->postJson('/balcao-virtual/pedidos/'.$requestId.'/mensagens', ['body' => 'Resposta original do responsável.', 'client_key' => (string) Str::uuid()])->assertOk(); config(['portal.managers' => []]); $this->actingAs($this->investor)->getJson('/balcao-virtual/pedidos/'.$requestId)->assertOk()->assertJsonPath('messages.0.kind', 'responsible_response')->assertJsonPath('messages.0.name', $this->manager->name); $historicalId = (string) Str::uuid(); DB::table('desk_messages')->insert(['id' => $historicalId, 'request_id' => $requestId, 'actor_id' => $this->investor->id, 'client_key' => (string) Str::uuid(), 'body' => Crypt::encryptString('Mensagem histórica sem tipo original.'), 'created_at' => now()->addSecond()]); $messages = $this->getJson('/balcao-virtual/pedidos/'.$requestId)->assertOk()->json('messages'); $this->assertSame('historical_unclassified', collect($messages)->firstWhere('id', $historicalId)['kind']); $this->assertDatabaseCount('desk_messages', 2); Http::assertNothingSent(); } public function test_link_monitor_expires_checks_and_records_human_evidence_without_external_calls(): void { $id = $this->dossier(); $link = (string) Str::uuid(); DB::table('desk_institutional_links')->insert(['id' => $link, 'stage' => 'E10', 'entity' => 'Instituição sintética QA', 'purpose' => 'Preparação', 'url' => 'https://example.invalid/', 'contact' => null, 'source_reference' => 'Fixture QA', 'status' => 'active', 'verified_at' => now()->subDays(8)]); $this->get('/balcao-virtual/encaminhar/'.$link.'?case_id='.$id)->assertRedirect('/balcao-virtual/dossiers/'.$id)->assertSessionHas('warning'); $path = '/management/balcao/ligacoes/'.$link.'/conferencia'; $data = ['version' => 1, 'available' => false, 'evidence' => 'Consulta manual QA identificou indisponibilidade.', 'contact' => 'Contacto sintético QA; fonte fixture']; $this->postJson($path, $data)->assertForbidden(); $this->actingAs($this->manager)->postJson($path, $data)->assertOk(); $this->postJson($path, $data)->assertConflict(); $this->assertDatabaseHas('desk_institutional_links', ['id' => $link, 'status' => 'unavailable', 'verified_by' => $this->manager->id]); $this->assertDatabaseHas('audit_events', ['action' => 'institutional_link_checked', 'actor_user_id' => $this->manager->id]); $this->actingAs($this->investor)->getJson('/balcao-virtual/dossiers/'.$id)->assertOk()->assertJsonPath('links.0.monitor.available', false); $this->getJson('/balcao-virtual/dossiers/'.$id.'/orientacao')->assertOk(); Http::assertNothingSent(); } public function test_complete_idea_request_response_and_internal_crm_task_reuses_existing_identity(): void { $existing = Investor::create(['legal_name' => 'Investidor QA', 'investor_type' => 'individual', 'status' => 'prospect']); DB::table('investor_accounts')->insert(['user_id' => $this->investor->id, 'investor_id' => $existing->id]); $project = (string) Str::uuid(); DB::table('projects')->insert(['id' => $project, 'code' => 'GUIDANCE-QA', 'name' => 'Projecto fictício', 'status' => 'draft']); $dossier = (string) Str::uuid(); DB::table('investment_dossiers')->insert(['id' => $dossier, 'project_id' => $project, 'title' => 'Projecto QA', 'content' => '{}', 'source' => 'fixture']); $journey = InvestorJourney::create(['user_id' => $this->investor->id, 'investor_id' => $existing->id, 'dossier_id' => $dossier, 'status' => 'started']); $id = $this->dossier(['journey_id' => $journey->id]); $this->assertDatabaseCount('investors', 1); $this->assertDatabaseHas('desk_cases', ['id' => $id, 'investor_id' => $existing->id, 'project_id' => $project, 'journey_id' => $journey->id]); $view = $this->guidance($id); $this->assertCount(7, $view['phases']); $this->assertDatabaseCount('tasks', 0); $this->assertDatabaseCount('desk_requests', 0); $service = $this->service(); $requestKey = (string) Str::uuid(); $requestId = $this->actingAs($this->investor)->postJson('/balcao-virtual/dossiers/'.$id.'/pedidos', ['service_version_id' => $service, 'client_key' => $requestKey])->assertCreated()->json('id'); $this->postJson('/balcao-virtual/dossiers/'.$id.'/pedidos', ['service_version_id' => $service, 'client_key' => $requestKey])->assertJsonPath('id', $requestId); $code = 'support_preparation'; $this->postJson('/balcao-virtual/dossiers/'.$id.'/recomendacoes', ['code' => $code, 'context_hash' => $view['fingerprint'], 'decision' => 'accepted', 'reason' => 'Pretendo preparar apoio.'])->assertOk(); $this->assertDatabaseCount('tasks', 0); $taskInput = ['code' => $code, 'context_hash' => $view['fingerprint'], 'consent' => true, 'due_at' => now()->addDays(2)->toIso8601String(), 'request_id' => $requestId]; $taskId = $this->postJson('/balcao-virtual/dossiers/'.$id.'/tarefas', $taskInput)->assertCreated()->json('id'); $this->postJson('/balcao-virtual/dossiers/'.$id.'/tarefas', $taskInput)->assertJsonPath('id', $taskId); $this->assertDatabaseCount('tasks', 1); $this->assertDatabaseHas('tasks', ['id' => $taskId, 'desk_case_id' => $id, 'desk_request_id' => $requestId, 'assignee_user_id' => $this->investor->id, 'status' => 'open', 'external_sync_status' => 'pending_authorization']); $payload = ['title' => 'Projecto QA', 'description' => 'Preciso de orientação sobre preparação.', 'regime' => 'unknown', 'investment_amount' => '1000.00', 'currency' => 'MZN', 'own_finance' => '600.00', 'other_finance' => '400.00']; $this->putJson('/balcao-virtual/pedidos/'.$requestId, ['version' => 1, 'payload' => $payload, 'submit' => false])->assertOk()->assertJsonPath('status', 'draft'); $this->getJson('/balcao-virtual/pedidos/'.$requestId)->assertJsonPath('request.payload.description', $payload['description']); $submit = ['version' => 2, 'payload' => $payload, 'submit' => true, 'consent' => true]; $this->putJson('/balcao-virtual/pedidos/'.$requestId, $submit)->assertOk()->assertJsonPath('status', 'submitted'); $this->putJson('/balcao-virtual/pedidos/'.$requestId, $submit)->assertOk(); $this->get('/balcao-virtual/pedidos/'.$requestId.'/recibo')->assertOk(); $this->postJson('/balcao-virtual/pedidos/'.$requestId.'/estado', ['version' => 3, 'status' => 'reviewing', 'reason' => 'Autodecisão indevida.'])->assertForbidden(); $this->actingAs($this->manager)->postJson('/balcao-virtual/pedidos/'.$requestId.'/responsavel', ['version' => 3, 'analyst_id' => $this->manager->id, 'reason' => 'Atribuição real QA.'])->assertOk(); $this->postJson('/balcao-virtual/pedidos/'.$requestId.'/estado', ['version' => 4, 'status' => 'reviewing', 'reason' => 'Análise interna iniciada.'])->assertOk(); $this->postJson('/balcao-virtual/pedidos/'.$requestId.'/mensagens', ['body' => 'Resposta humana QA: indique as evidências de mercado.', 'client_key' => (string) Str::uuid()])->assertOk(); $this->actingAs($this->investor)->getJson('/balcao-virtual/pedidos/'.$requestId)->assertJsonPath('messages.0.kind', 'responsible_response'); $this->postJson('/balcao-virtual/dossiers/'.$id.'/tarefas/'.$taskId, ['version' => 1, 'status' => 'done', 'reason' => 'Pedido submetido e primeira resposta consultada.'])->assertOk(); $this->assertDatabaseHas('tasks', ['id' => $taskId, 'status' => 'done']); $this->assertDatabaseHas('desk_events', ['case_id' => $id, 'action' => 'guidance_task_updated']); Http::assertNothingSent(); } public function test_incomplete_context_corrections_stale_choices_and_no_external_services(): void { $id = $this->dossier(); $view = $this->guidance($id); $this->assertNotEmpty($view['phases'][1]['questions']); $this->assertSame('not_configured', $view['external_ai']['status']); $this->assertSame([], $view['consulted']); $this->postJson('/balcao-virtual/dossiers/'.$id.'/recomendacoes', ['code' => 'market_preparation', 'context_hash' => $view['fingerprint'], 'decision' => 'rejected', 'reason' => 'Hipótese ainda sem evidência.'])->assertOk(); $this->postJson('/balcao-virtual/dossiers/'.$id.'/pressupostos', ['version' => 1, 'assumptions' => ['activity' => 'Transformação', 'expected_start_date' => '2027-01-01', 'ready_date' => '2027-02-01']])->assertOk(); $this->postJson('/balcao-virtual/dossiers/'.$id.'/pressupostos', ['version' => 1, 'assumptions' => ['activity' => 'Alteração obsoleta']])->assertConflict(); $new = $this->guidance($id); $this->assertTrue($new['phases'][1]['recommendations'][0]['review']['stale']); $this->assertSame('declared_contradiction', $new['phases'][5]['risks'][0]['classification']); $this->postJson('/balcao-virtual/dossiers/'.$id.'/tarefas', ['code' => 'market_preparation', 'context_hash' => $view['fingerprint'], 'consent' => true, 'due_at' => now()->addDay()->toIso8601String()])->assertConflict(); $this->postJson('/balcao-virtual/dossiers/'.$id.'/fontes', ['query' => 'Licenciamento'])->assertOk()->assertJsonPath('status', 'unavailable')->assertJsonPath('consulted', []); $this->assertDatabaseCount('tasks', 0); $this->get('/balcao-virtual/dossiers/'.$id)->assertOk(); Http::assertNothingSent(); } public function test_private_linked_draft_is_not_shared_with_manager_and_is_recovered_for_owner(): void { $draft = SimulationFormDraft::create(['id' => (string) Str::uuid(), 'owner_user_id' => $this->investor->id, 'version_no' => 1, 'create_key_hash' => hash('sha256', 'qa-key'), 'create_payload_hash' => hash('sha256', 'qa-data'), 'snapshot_sha256' => hash('sha256', 'qa-data'), 'personal_snapshot' => SimulationFormDraft::encryptSnapshot(['name' => 'PRIVATE PERSONAL']), 'economic_snapshot' => SimulationFormDraft::encryptSnapshot(['project_summary' => 'Ideia guardada', 'market_assumption' => 'Hipótese, sem procura provada', 'business_plan_snapshot' => '{"version":999}'])]); $id = $this->dossier(['simulation_id' => $draft->id]); $owner = $this->guidance($id); $this->assertSame('Ideia guardada', $owner['facts']['project_summary']); $this->assertArrayNotHasKey('market_evidence', $owner['facts']); $this->assertStringNotContainsString('PRIVATE PERSONAL', json_encode($owner)); $this->assertSame(1, $owner['consulted'][0]['version']); $draftUrl = '/simulador-fiscal?draft='.$draft->id; $this->assertSame($draftUrl, $owner['actions']['simulator']); $this->assertSame($draftUrl, $owner['actions']['plan']); $finance = collect($owner['phases'])->firstWhere('id', 'finance'); $this->assertSame($draftUrl, $finance['recommendations'][0]['href']); $otherDraft = $draft->replicate(); $otherDraft->id = (string) Str::uuid(); $otherDraft->economic_snapshot = SimulationFormDraft::encryptSnapshot(['project_summary' => 'Outro projecto, não reutilizar']); $otherDraft->save(); $this->getJson('/api/v1/simulation-drafts/'.$draft->id)->assertOk()->assertJsonPath('data.id', $draft->id)->assertJsonPath('data.economic_snapshot.project_summary', 'Ideia guardada'); $manager = $this->actingAs($this->manager)->getJson('/balcao-virtual/dossiers/'.$id.'/orientacao')->assertOk()->json(); $this->assertNull($manager['financial_draft']); $this->assertSame([], $manager['consulted']); $this->assertSame('/simulador-fiscal', $manager['actions']['simulator']); $this->getJson('/api/v1/simulation-drafts/'.$draft->id)->assertNotFound(); $this->actingAs($this->investor)->postJson('/balcao-virtual/dossiers', ['title' => 'Reutilização', 'simulation_id' => $draft->id, 'client_key' => (string) Str::uuid(), 'reuse_consent' => true])->assertJsonPath('id', $id); $this->assertDatabaseCount('desk_cases', 1); $this->actingAs(User::factory()->create())->getJson('/balcao-virtual/dossiers/'.$id.'/orientacao')->assertForbidden(); $this->postJson('/balcao-virtual/dossiers', ['title' => 'Tentativa', 'simulation_id' => $draft->id, 'client_key' => (string) Str::uuid(), 'reuse_consent' => true])->assertNotFound(); } public function test_request_constraints_and_draft_permissions_preserve_existing_flow(): void { $id = $this->dossier(); $service = $this->service(); $requestId = $this->actingAs($this->investor)->postJson('/balcao-virtual/dossiers/'.$id.'/pedidos', ['service_version_id' => $service, 'client_key' => (string) Str::uuid()])->assertCreated()->json('id'); $data = ['version' => 1, 'payload' => ['title' => 'QA', 'description' => 'Contradição declarada', 'investment_amount' => '1000.00', 'currency' => 'MZN', 'own_finance' => '1.00', 'other_finance' => '1.00'], 'submit' => true, 'consent' => true]; $this->putJson('/balcao-virtual/pedidos/'.$requestId, $data)->assertUnprocessable(); $this->assertDatabaseHas('desk_requests', ['id' => $requestId, 'status' => 'draft', 'version' => 1]); $this->actingAs($this->manager)->putJson('/balcao-virtual/pedidos/'.$requestId, $data)->assertForbidden(); $this->actingAs(User::factory()->create())->getJson('/balcao-virtual/pedidos/'.$requestId)->assertForbidden(); } public function test_clarification_resubmission_and_signed_internal_outcome_preserve_snapshots(): void { $id = $this->dossier(); $service = $this->service(); $requestId = $this->actingAs($this->investor)->postJson('/balcao-virtual/dossiers/'.$id.'/pedidos', ['service_version_id' => $service, 'client_key' => (string) Str::uuid()])->json('id'); $payload = ['title' => 'QA', 'description' => 'Descrição inicial', 'regime' => 'unknown', 'investment_amount' => '1000.00', 'currency' => 'MZN']; $this->putJson('/balcao-virtual/pedidos/'.$requestId, ['version' => 1, 'payload' => $payload, 'submit' => true, 'consent' => true])->assertOk(); $this->actingAs($this->manager)->postJson('/balcao-virtual/pedidos/'.$requestId.'/estado', ['version' => 2, 'status' => 'returned', 'reason' => 'Esclareça o mercado previsto.', 'editable_fields' => ['description']])->assertOk(); $this->actingAs($this->investor)->getJson('/balcao-virtual/pedidos/'.$requestId)->assertJsonPath('request.submitted_snapshot.payload.description', 'Descrição inicial'); $changed = [...$payload, 'title' => 'Alteração indevida', 'description' => 'Mercado declarado, evidência pendente.']; $this->putJson('/balcao-virtual/pedidos/'.$requestId, ['version' => 3, 'payload' => $changed, 'submit' => true, 'consent' => true, 'reason' => 'Resposta ao esclarecimento.'])->assertUnprocessable(); $payload['description'] = 'Mercado declarado, evidência pendente.'; $this->putJson('/balcao-virtual/pedidos/'.$requestId, ['version' => 3, 'payload' => $payload, 'submit' => true, 'consent' => true, 'reason' => 'Resposta ao esclarecimento.'])->assertOk()->assertJsonPath('status', 'submitted'); $this->actingAs($this->manager)->postJson('/balcao-virtual/pedidos/'.$requestId.'/estado', ['version' => 4, 'status' => 'reviewing', 'reason' => 'Resposta analisada internamente.'])->assertOk(); $this->postJson('/balcao-virtual/pedidos/'.$requestId.'/estado', ['version' => 5, 'status' => 'opinion', 'reason' => 'Parecer interno, sem licença oficial.'])->assertOk(); $this->postJson('/balcao-virtual/pedidos/'.$requestId.'/estado', ['version' => 6, 'status' => 'accepted', 'reason' => 'Dossier aceite para acompanhamento interno.'])->assertOk(); $this->actingAs($this->investor)->getJson('/balcao-virtual/pedidos/'.$requestId)->assertJsonPath('request.status', 'accepted')->assertJsonCount(2, 'revisions'); Http::assertNothingSent(); } public function test_manual_document_payment_and_communication_evidence_without_external_confirmation(): void { Storage::fake('local'); $id = $this->dossier(); $service = $this->service(); $requestId = $this->actingAs($this->investor)->postJson('/balcao-virtual/dossiers/'.$id.'/pedidos', ['service_version_id' => $service, 'client_key' => (string) Str::uuid()])->json('id'); $doc = (string) Str::uuid(); $link = (string) Str::uuid(); $bytes = 'Comprovativo fictício QA, sem valor oficial.'; Storage::disk('local')->put('qa-proof.txt', $bytes); $hash = hash('sha256', $bytes); DB::table('documents')->insert(['id' => $doc, 'source_system' => 'qa', 'file_name' => 'qa-proof.txt', 'media_type' => 'text/plain', 'storage_key' => 'qa-proof.txt', 'sha256' => $hash, 'size_bytes' => strlen($bytes), 'uploaded_by' => $this->investor->id, 'status' => 'active', 'malware_scan_status' => 'clean', 'malware_scanned_sha256' => $hash, 'data_classification' => 'confidential']); DB::table('desk_documents')->insert(['id' => $link, 'case_id' => $id, 'document_id' => $doc, 'category' => 'proof', 'source_reference' => 'Fixture QA', 'linked_by' => $this->investor->id]); $review = ['version' => 1, 'outcome' => 'internal_verified', 'what_checked' => 'Legibilidade e referência declarada.', 'evidence' => 'Documento QA e declaração do investidor.']; $this->postJson('/balcao-virtual/dossiers/'.$id.'/documentos/'.$link.'/conferencia', $review)->assertForbidden(); $this->actingAs($this->manager)->postJson('/balcao-virtual/dossiers/'.$id.'/documentos/'.$link.'/conferencia', $review)->assertOk(); $event = DB::table('desk_events')->where('action', 'document_reviewed')->first(); $this->assertSame($this->manager->id, (int) $event->actor_id); $this->assertSame($hash, app(VirtualDesk::class)->decode($event->payload)['sha256']); $this->assertDatabaseHas('documents', ['id' => $doc, 'status' => 'active']); $payment = ['reference' => 'QA-MANUAL-1', 'amount' => '25.50', 'currency' => 'MZN', 'source_reference' => 'Valor declarado em comprovativo fictício, não taxa automática.', 'document_id' => $doc, 'consent' => true]; $this->actingAs($this->investor)->postJson('/balcao-virtual/pedidos/'.$requestId.'/pagamentos', $payment)->assertOk(); $this->postJson('/balcao-virtual/pedidos/'.$requestId.'/pagamentos', $payment)->assertOk(); $this->assertDatabaseCount('desk_payments', 1); $record = DB::table('desk_payments')->first(); $this->assertSame('pending', $record->status); $check = ['version' => 1, 'status' => 'internal_verified', 'reason' => 'Conferida referência e valor no comprovativo.', 'evidence' => 'Comprovativo QA, não consulta bancária.']; $this->postJson('/balcao-virtual/pedidos/'.$requestId.'/pagamentos/'.$record->id.'/conferencia', $check)->assertForbidden(); $this->actingAs($this->manager)->postJson('/balcao-virtual/pedidos/'.$requestId.'/pagamentos/'.$record->id.'/conferencia', $check)->assertOk(); $this->assertDatabaseHas('desk_payments', ['id' => $record->id, 'status' => 'internal_verified', 'verified_by' => $this->manager->id]); $communication = ['channel' => 'whatsapp', 'direction' => 'received', 'occurred_at' => now()->subMinute()->toIso8601String(), 'summary' => 'Comunicação fictícia registada manualmente.', 'evidence' => 'Referência declarada pelo responsável QA.', 'client_key' => (string) Str::uuid()]; $this->postJson('/balcao-virtual/pedidos/'.$requestId.'/comunicacoes', $communication)->assertOk(); $this->postJson('/balcao-virtual/pedidos/'.$requestId.'/comunicacoes', $communication)->assertOk(); $this->assertSame(1, DB::table('desk_events')->where('action', 'manual_communication_recorded')->count()); Storage::disk('local')->put('qa-proof.txt', 'Ficheiro alterado'); $this->postJson('/balcao-virtual/dossiers/'.$id.'/documentos/'.$link.'/conferencia', [...$review, 'version' => 2])->assertUnprocessable(); Http::assertNothingSent(); } public function test_parallel_preparation_unavailable_link_and_ordered_contestable_recommendations(): void { $id = $this->dossier(); $this->postJson('/balcao-virtual/dossiers/'.$id.'/etapas', ['version' => 1, 'stage' => 'E10', 'status' => 'preparing', 'reason' => 'Preparar em paralelo com o registo.'])->assertOk(); $this->actingAs($this->manager)->postJson('/balcao-virtual/dossiers/'.$id.'/etapas', ['version' => 2, 'stage' => 'E10', 'status' => 'completed', 'reason' => 'Conclusão sem suporte.', 'evidence' => 'Afirmação sem documento.'])->assertUnprocessable(); $view = $this->guidance($id); foreach (['accepted', 'rejected', 'corrected'] as $decision) { $this->postJson('/balcao-virtual/dossiers/'.$id.'/recomendacoes', ['code' => 'support_preparation', 'context_hash' => $view['fingerprint'], 'decision' => $decision, 'reason' => 'Revisão explícita QA.'])->assertOk(); } $updated = $this->guidance($id); $this->assertSame('corrected', $updated['phases'][6]['recommendations'][0]['review']['decision']); $link = (string) Str::uuid(); DB::table('desk_institutional_links')->insert(['id' => $link, 'stage' => 'E10', 'entity' => 'Entidade QA', 'purpose' => 'Preparação', 'url' => 'https://example.invalid/', 'contact' => 'Contacto fictício QA', 'status' => 'pending', 'source_reference' => 'Fixture não confirmada']); $this->get('/balcao-virtual/encaminhar/'.$link.'?case_id='.$id)->assertRedirect('/balcao-virtual/dossiers/'.$id)->assertSessionHas('warning'); $this->getJson('/balcao-virtual/dossiers/'.$id)->assertOk(); $this->getJson('/balcao-virtual/dossiers/'.$id.'/orientacao')->assertOk(); Http::assertNothingSent(); } public function test_organization_viewers_and_removed_members_keep_module_permissions(): void { $viewer = User::factory()->create(); $officer = User::factory()->create(); $organization = (string) Str::uuid(); DB::table('organizations')->insert(['id' => $organization, 'name' => 'Organização fictícia QA', 'organization_type' => 'private']); foreach ([[$this->investor, 'manager'], [$viewer, 'viewer'], [$officer, 'officer']] as [$user, $role]) { DB::table('memberships')->insert(['user_id' => $user->id, 'organization_id' => $organization, 'role' => $role]); } $id = $this->dossier(['organization_id' => $organization]); $service = $this->service(); $requestId = $this->actingAs($this->investor)->postJson('/balcao-virtual/dossiers/'.$id.'/pedidos', ['service_version_id' => $service, 'client_key' => (string) Str::uuid()])->json('id'); $this->actingAs($viewer)->getJson('/balcao-virtual/dossiers/'.$id.'/orientacao')->assertOk()->assertJsonPath('can_write', false); $this->postJson('/balcao-virtual/dossiers/'.$id.'/pressupostos', ['version' => 1, 'assumptions' => ['activity' => 'Alteração indevida']])->assertForbidden(); $this->postJson('/balcao-virtual/pedidos/'.$requestId.'/mensagens', ['body' => 'Tentativa de escrita viewer.', 'client_key' => (string) Str::uuid()])->assertForbidden(); $this->postJson('/balcao-virtual/pedidos/'.$requestId.'/comunicacoes', ['channel' => 'phone', 'direction' => 'received', 'occurred_at' => now()->subMinute()->toIso8601String(), 'summary' => 'Tentativa de escrita viewer.', 'evidence' => 'Sem permissão.', 'client_key' => (string) Str::uuid()])->assertForbidden(); $this->actingAs($officer)->postJson('/balcao-virtual/dossiers/'.$id.'/pressupostos', ['version' => 1, 'assumptions' => ['activity' => 'Declaração autorizada da organização']])->assertOk(); $this->postJson('/balcao-virtual/pedidos/'.$requestId.'/mensagens', ['body' => 'Mensagem original do membro investidor.', 'client_key' => (string) Str::uuid()])->assertOk(); DB::table('memberships')->where('organization_id', $organization)->where('user_id', $officer->id)->update(['deleted_at' => now()]); $this->actingAs($this->manager)->getJson('/balcao-virtual/pedidos/'.$requestId)->assertOk()->assertJsonPath('messages.0.kind', 'investor_message')->assertJsonPath('messages.0.name', $officer->name); DB::table('memberships')->where('organization_id', $organization)->where('user_id', $this->investor->id)->update(['deleted_at' => now()]); $this->actingAs($this->investor)->getJson('/balcao-virtual/dossiers/'.$id)->assertForbidden(); $this->getJson('/balcao-virtual/pedidos/'.$requestId)->assertForbidden(); $this->getJson('/balcao-virtual/contextos')->assertJsonCount(0, 'cases'); } public function test_pending_installation_preserves_legacy_desk_and_requires_authentication(): void { $this->mock(VirtualDesk::class)->shouldReceive('installed')->andReturn(false); $this->actingAs($this->investor)->get('/balcao-virtual')->assertOk(); $this->getJson('/balcao-virtual/painel')->assertStatus(503); $this->getJson('/management/balcao/fontes')->assertForbidden(); $this->actingAs($this->manager)->getJson('/management/balcao/fontes')->assertOk()->assertJsonStructure(['total', 'attachmentCount', 'forms']); Http::assertNothingSent(); } public function test_installation_preview_shows_target_and_refuses_proofs_for_a_different_database(): void { $this->mock(VirtualDesk::class)->shouldReceive('installed')->andReturn(false); $this->actingAs($this->investor)->getJson('/management/balcao-install')->assertForbidden(); $view = $this->actingAs($this->manager)->getJson('/management/balcao-install')->assertOk() ->assertJsonPath('target.database', DB::connection()->getDatabaseName())->assertJsonPath('backupVerified', false) ->assertJsonStructure(['pendingMigrations', 'databaseReady', 'scanner', 'rollback', 'compatibilityVerified'])->json(); $before = DB::table('migrations')->count(); $this->postJson('/management/balcao-install', ['hash' => $view['hash'], 'confirm' => true])->assertStatus(422); $this->assertSame($before, DB::table('migrations')->count()); } public function test_existing_support_request_and_crm_investor_are_associated_without_copying(): void { $canonical = Investor::create(['legal_name' => 'Investidor já acompanhado QA', 'investor_type' => 'individual', 'status' => 'active']); $original = InterestRequest::create(['reference' => 'QA-EXISTING-SUPPORT', 'full_name' => 'Investidor QA', 'email' => 'private@example.invalid', 'investor_type' => 'individual', 'country' => 'Moçambique', 'sector_interest' => 'Energia QA', 'message' => 'Pedido original preservado.', 'privacy_consent' => true, 'investor_id' => $canonical->id, 'assigned_to' => $this->manager->id, 'next_action' => 'Conferir documentação no registo existente.', 'metadata' => ['portal_user_id' => $this->investor->id]]); $id = $this->dossier(['interest_request_id' => $original->id]); $this->assertDatabaseCount('interest_requests', 1); $this->assertDatabaseCount('desk_requests', 0); $this->assertDatabaseCount('investors', 1); $this->assertDatabaseHas('desk_cases', ['id' => $id, 'interest_request_id' => $original->id, 'investor_id' => $canonical->id]); $this->getJson('/balcao-virtual/dossiers/'.$id)->assertJsonPath('legacyRequest.reference', 'QA-EXISTING-SUPPORT')->assertJsonPath('legacyRequest.responsible_name', $this->manager->name)->assertJsonMissingPath('legacyRequest.email'); $this->postJson('/balcao-virtual/dossiers', ['title' => 'Reutilizar', 'interest_request_id' => $original->id, 'reuse_consent' => true, 'client_key' => (string) Str::uuid()])->assertJsonPath('id', $id); $this->assertDatabaseCount('desk_cases', 1); $this->actingAs(User::factory()->create())->postJson('/balcao-virtual/dossiers', ['title' => 'Sem autorização', 'interest_request_id' => $original->id, 'reuse_consent' => true, 'client_key' => (string) Str::uuid()])->assertNotFound(); } public function test_guidance_evidence_tracks_scope_latest_review_hash_expiration_and_permissions(): void { $id = $this->dossier(); $initial = $this->guidance($id); $this->assertSame('absent', $initial['phases'][2]['evidence'][0]['state']); $doc = (string) Str::uuid(); $link = (string) Str::uuid(); $bytes = 'Evidencia ficticia V2 QA'; Storage::disk('local')->put('v2-location-proof.txt', $bytes); $hash = hash('sha256', $bytes); DB::table('documents')->insert(['id' => $doc, 'source_system' => 'qa', 'file_name' => 'v2-location-proof.txt', 'media_type' => 'text/plain', 'storage_key' => 'v2-location-proof.txt', 'sha256' => $hash, 'version_no' => 1, 'size_bytes' => strlen($bytes), 'uploaded_by' => $this->investor->id, 'status' => 'active', 'malware_scan_status' => 'clean', 'malware_scanned_sha256' => $hash, 'data_classification' => 'confidential']); DB::table('desk_documents')->insert(['id' => $link, 'case_id' => $id, 'document_id' => $doc, 'category' => 'location_evidence', 'source_reference' => 'Fixture QA', 'linked_by' => $this->investor->id]); $attached = $this->guidance($id); $this->assertSame('attached_unchecked', $attached['phases'][2]['evidence'][0]['state']); $this->assertNotSame($initial['fingerprint'], $attached['fingerprint']); $review = ['version' => 1, 'outcome' => 'internal_verified', 'what_checked' => 'Acesso descrito no documento ficticio.', 'evidence' => 'Texto e hash da fixture QA.', 'supports' => ['location_evidence']]; $url = '/balcao-virtual/dossiers/'.$id.'/documentos/'.$link.'/conferencia'; $this->postJson($url, $review)->assertForbidden(); $this->actingAs($this->manager)->postJson($url, $review)->assertOk(); $verified = $this->guidance($id); $e = $verified['phases'][2]['evidence'][0]; $this->assertSame('internally_verified', $e['state']); $this->assertSame($this->manager->name, $e['documents'][0]['review']['author']); $this->assertNotEmpty($e['documents'][0]['review']['date']); $this->assertSame($review['what_checked'], $e['documents'][0]['review']['what_checked']); $this->assertSame([], $verified['phases'][2]['risks']); $this->assertSame('absent', $verified['phases'][3]['evidence'][0]['state']); $this->actingAs(User::factory()->create())->getJson('/balcao-virtual/dossiers/'.$id.'/orientacao')->assertForbidden(); DB::table('desk_documents')->where('id', $link)->update(['valid_until' => now()->subDay()->toDateString()]); $this->assertSame('expired', $this->guidance($id)['phases'][2]['evidence'][0]['state']); DB::table('desk_documents')->where('id', $link)->update(['valid_until' => null]); DB::table('documents')->where('id', $doc)->update(['version_no' => 2]); $this->assertSame('superseded', $this->guidance($id)['phases'][2]['evidence'][0]['state']); DB::table('documents')->where('id', $doc)->update(['version_no' => 1]); Storage::disk('local')->put('v2-location-proof.txt', 'Bytes substituidos QA'); $this->assertSame('superseded', $this->guidance($id)['phases'][2]['evidence'][0]['state']); Storage::disk('local')->put('v2-location-proof.txt', $bytes); $this->actingAs($this->manager)->postJson($url, [...$review, 'version' => 2, 'outcome' => 'contradiction_identified'])->assertOk(); $this->assertSame('contradiction', $this->guidance($id)['phases'][2]['evidence'][0]['state']); // A new conference with no scope must not resurrect an older verification. $this->actingAs($this->manager)->postJson($url, [...$review, 'version' => 3, 'supports' => []])->assertOk(); $this->assertSame('attached_unchecked', $this->guidance($id)['phases'][2]['evidence'][0]['state']); Http::assertNothingSent(); } }