165 lines
11 KiB
PHP
165 lines
11 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\CrmTask;
|
|
use App\Models\Lead;
|
|
use App\Models\User;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Support\Str;
|
|
use Tests\Support\AuthenticatesQaActors;
|
|
use Tests\TestCase;
|
|
|
|
final class DeskClosedLifecycleTest extends TestCase
|
|
{
|
|
use AuthenticatesQaActors;
|
|
use RefreshDatabase;
|
|
|
|
private User $owner;
|
|
|
|
private User $manager;
|
|
|
|
private string $caseId;
|
|
|
|
private string $requestId;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
parent::setUp();
|
|
$this->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');
|
|
}
|
|
}
|
|
}
|