withoutVite(); Http::fake(); $this->owner = User::factory()->create(); $this->manager = User::factory()->create(); config(['portal.managers' => [$this->manager->email], 'investmoz_ai.enabled' => false, 'suitecrm.enabled' => false]); $edition = $this->actingAs($this->manager)->putJson('/management/balcao/servicos/project-change', ['base_version' => 0, 'definition' => config('virtual-desk.services.project-change'), 'reason' => 'SYNTHETIC lifecycle QA', 'confirm' => true])->assertCreated()->json('id'); $this->caseId = $this->actingAs($this->owner)->postJson('/balcao-virtual/dossiers', ['title' => 'SYNTHETIC closed dossier', 'client_key' => (string) Str::uuid(), 'reuse_consent' => true])->assertCreated()->json('id'); $this->requestId = $this->postJson('/balcao-virtual/dossiers/'.$this->caseId.'/pedidos', ['service_version_id' => $edition, 'client_key' => (string) Str::uuid()])->assertCreated()->json('id'); } private function submit(): void { $this->putJson('/balcao-virtual/pedidos/'.$this->requestId, ['version' => 1, 'payload' => ['title' => 'SYNTHETIC project', 'description' => 'SYNTHETIC private preparation', 'original_reference' => 'SYNTHETIC-REF', 'change_reason' => 'SYNTHETIC change'], 'submit' => true, 'consent' => true])->assertOk(); } private function closeFixture(): array { DB::table('desk_cases')->where('id', $this->caseId)->update(['closed_at' => now()]); return ['request' => (array) DB::table('desk_requests')->where('id', $this->requestId)->first(), 'case' => (array) DB::table('desk_cases')->where('id', $this->caseId)->first(), 'events' => DB::table('desk_events')->count(), 'notifications' => DB::table('desk_notifications')->count(), 'messages' => DB::table('desk_messages')->count()]; } private function assertUnchanged(array $before): void { $this->assertSame($before['request'], (array) DB::table('desk_requests')->where('id', $this->requestId)->first()); $this->assertSame($before['case'], (array) DB::table('desk_cases')->where('id', $this->caseId)->first()); foreach (['events' => 'desk_events', 'notifications' => 'desk_notifications', 'messages' => 'desk_messages'] as $key => $table) { $this->assertSame($before[$key], DB::table($table)->count()); } } public function test_closed_dossier_rejects_request_transition_without_changing_its_history(): void { $this->submit(); $before = $this->closeFixture(); $this->actingAs($this->manager)->postJson('/balcao-virtual/pedidos/'.$this->requestId.'/estado', ['version' => 2, 'status' => 'reviewing', 'reason' => 'SYNTHETIC review attempt'])->assertConflict(); $this->assertUnchanged($before); $this->getJson('/balcao-virtual/pedidos/'.$this->requestId)->assertOk()->assertJsonPath('operationsEditable', false)->assertJsonCount(0, 'transitions'); } public function test_closed_dossier_rejects_stage_changes_but_keeps_workflow_readable(): void { $before = $this->closeFixture(); $this->postJson('/balcao-virtual/dossiers/'.$this->caseId.'/etapas', ['version' => 1, 'stage' => 'E0', 'status' => 'preparing', 'reason' => 'SYNTHETIC preparation attempt'])->assertConflict(); $this->assertUnchanged($before); $this->getJson('/balcao-virtual/dossiers/'.$this->caseId)->assertOk()->assertJsonPath('canWrite', false)->assertJsonStructure(['case' => ['workflow', 'closed_at']]); } public function test_closed_dossier_is_excluded_from_triage_and_rejects_assignment(): void { $this->submit(); $before = $this->closeFixture(); $this->actingAsWithMfa($this->manager)->postJson('/balcao-virtual/pedidos/'.$this->requestId.'/responsavel', ['version' => 2, 'analyst_id' => $this->manager->id, 'reason' => 'SYNTHETIC assignment attempt'])->assertConflict(); $this->assertUnchanged($before); $this->getJson('/management/balcao/atribuicoes')->assertOk()->assertJsonCount(0, 'requests'); } public function test_closed_dossier_refuses_messages_even_if_its_request_is_still_open(): void { $this->submit(); $before = $this->closeFixture(); foreach ([$this->owner, $this->manager] as $actor) { $this->actingAs($actor)->postJson('/balcao-virtual/pedidos/'.$this->requestId.'/mensagens', ['body' => 'SYNTHETIC message attempt', 'client_key' => (string) Str::uuid()])->assertConflict(); } $this->assertUnchanged($before); } public function test_closed_dossier_refuses_task_updates_and_guidance_remains_readable_without_write_actions(): void { $view = $this->getJson('/balcao-virtual/dossiers/'.$this->caseId.'/orientacao')->assertOk()->json(); $this->postJson('/balcao-virtual/dossiers/'.$this->caseId.'/recomendacoes', ['code' => 'support_preparation', 'context_hash' => $view['fingerprint'], 'decision' => 'accepted', 'reason' => 'SYNTHETIC preparation intent'])->assertOk(); $task = $this->postJson('/balcao-virtual/dossiers/'.$this->caseId.'/tarefas', ['code' => 'support_preparation', 'context_hash' => $view['fingerprint'], 'consent' => true, 'due_at' => now()->addDays(2)->toIso8601String()])->assertCreated()->json('id'); $before = $this->closeFixture(); foreach ([$this->owner, $this->manager] as $actor) { $this->actingAs($actor)->postJson('/balcao-virtual/dossiers/'.$this->caseId.'/tarefas/'.$task, ['version' => 1, 'status' => 'done', 'reason' => 'SYNTHETIC task update attempt'])->assertConflict(); } $this->assertDatabaseHas('tasks', ['id' => $task, 'status' => 'open', 'version_no' => 1]); $this->assertUnchanged($before); $this->getJson('/balcao-virtual/dossiers/'.$this->caseId.'/orientacao')->assertOk()->assertJsonPath('can_write', false)->assertJsonPath('can_update_tasks', false)->assertJsonCount(1, 'tasks'); } public function test_open_dossier_keeps_existing_transition_assignment_message_and_task_permissions(): void { $this->submit(); $this->postJson('/balcao-virtual/pedidos/'.$this->requestId.'/estado', ['version' => 2, 'status' => 'reviewing', 'reason' => 'SYNTHETIC self-review'])->assertForbidden(); $this->actingAsWithMfa($this->manager)->postJson('/balcao-virtual/pedidos/'.$this->requestId.'/responsavel', ['version' => 2, 'analyst_id' => $this->manager->id, 'reason' => 'SYNTHETIC permitted assignment'])->assertOk(); $this->postJson('/balcao-virtual/pedidos/'.$this->requestId.'/estado', ['version' => 3, 'status' => 'reviewing', 'reason' => 'SYNTHETIC permitted review'])->assertOk(); $this->actingAs($this->owner)->postJson('/balcao-virtual/pedidos/'.$this->requestId.'/mensagens', ['body' => 'SYNTHETIC permitted message', 'client_key' => (string) Str::uuid()])->assertOk(); $this->getJson('/balcao-virtual/dossiers/'.$this->caseId.'/orientacao')->assertOk()->assertJsonPath('can_write', true)->assertJsonPath('can_update_tasks', true); $this->actingAs(User::factory()->create())->getJson('/balcao-virtual/pedidos/'.$this->requestId)->assertForbidden(); } public function test_draft_cancellation_preserves_payload_documents_and_case_and_cannot_cancel_a_submitted_request(): void { $before = DB::table('desk_requests')->where('id', $this->requestId)->first(); $this->postJson('/balcao-virtual/pedidos/'.$this->requestId.'/estado', ['version' => 1, 'status' => 'cancelled', 'reason' => 'SYNTHETIC deliberate cancellation'])->assertOk()->assertJsonPath('status', 'cancelled'); $after = DB::table('desk_requests')->where('id', $this->requestId)->first(); $this->assertSame($before->payload, $after->payload); $this->assertSame($before->submitted_snapshot, $after->submitted_snapshot); $this->assertDatabaseHas('desk_cases', ['id' => $this->caseId, 'closed_at' => null]); $this->assertNotNull($after->closed_at); $this->getJson('/balcao-virtual/pedidos/'.$this->requestId)->assertOk()->assertJsonPath('operationsEditable', false); $edition = DB::table('desk_requests')->where('id', $this->requestId)->value('service_version_id'); $this->requestId = $this->postJson('/balcao-virtual/dossiers/'.$this->caseId.'/pedidos', ['service_version_id' => $edition, 'client_key' => (string) Str::uuid()])->assertCreated()->json('id'); $this->submit(); $this->postJson('/balcao-virtual/pedidos/'.$this->requestId.'/estado', ['version' => 2, 'status' => 'cancelled', 'reason' => 'SYNTHETIC submitted cancellation attempt'])->assertForbidden(); $this->assertDatabaseHas('desk_requests', ['id' => $this->requestId, 'status' => 'submitted', 'version' => 2]); } public function test_dual_linked_crm_task_observes_dossier_closure_without_changing_crm_authority(): void { $lead = Lead::create(['title' => 'SYNTHETIC dual-linked lead', 'status' => 'disqualified', 'owner_user_id' => $this->manager->id]); $task = CrmTask::create(['lead_id' => $lead->id, 'desk_case_id' => $this->caseId, 'guidance_key' => 'synthetic_dual_link', 'title' => 'SYNTHETIC existing linked task', 'status' => 'open', 'assignee_user_id' => $this->manager->id, 'accepted_by' => $this->owner->id, 'accepted_at' => now(), 'external_sync_status' => 'pending_authorization', 'data_classification' => 'confidential']); $url = '/management/crm/leads/'.$lead->id.'/tasks/'.$task->id; $this->actingAsWithMfa($this->manager)->patch($url, ['version' => $task->fresh()->getAttribute('version_no'), 'status' => 'in_progress'])->assertRedirect(); $this->assertDatabaseHas('tasks', ['id' => $task->id, 'status' => 'in_progress']); $this->closeFixture(); $this->patchJson($url, ['version' => $task->fresh()->getAttribute('version_no'), 'status' => 'done'])->assertConflict(); $this->assertDatabaseHas('tasks', ['id' => $task->id, 'status' => 'in_progress']); $this->get('/management/crm/leads/'.$lead->id)->assertOk()->assertInertia(fn ($page) => $page->where('lead.tasks.0.can_update', false)); } public function test_crm_without_linked_tasks_does_not_require_an_installed_desk(): void { $lead = Lead::create(['title' => 'SYNTHETIC standalone CRM', 'status' => 'disqualified', 'owner_user_id' => $this->manager->id]); $this->actingAsWithMfa($this->manager); DB::statement('ALTER TABLE desk_cases RENAME TO desk_cases_qa_unavailable'); try { $this->get('/management/crm/leads/'.$lead->id)->assertOk()->assertInertia(fn ($page) => $page->where('lead.title', 'SYNTHETIC standalone CRM')); } finally { DB::statement('ALTER TABLE desk_cases_qa_unavailable RENAME TO desk_cases'); } } }