Files
ladill-queue/tests/Feature/QmsQueueTest.php
T
isaaccladandCursor d2f5ff11d2
Deploy Ladill Queue / deploy (push) Successful in 40s
Fix queue handoff so tickets keep their number across services.
Handoff now frees the counter, ends the service session, and requeues
the same ticket for the next department — critical for hospital flows
like doctor → lab.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-14 21:22:01 +00:00

251 lines
8.7 KiB
PHP

<?php
namespace Tests\Feature;
use App\Models\Branch;
use App\Models\Organization;
use App\Models\ServiceQueue;
use App\Models\User;
use App\Services\Qms\OrganizationResolver;
use App\Services\Qms\QueueEngine;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class QmsQueueTest extends TestCase
{
use RefreshDatabase;
protected function setUpOrg(): array
{
$user = User::create([
'public_id' => 'test-owner-uuid',
'name' => 'Test User',
'email' => 'test@example.com',
'password' => bcrypt('password'),
]);
$resolver = app(OrganizationResolver::class);
$org = $resolver->completeOnboarding($user, [
'organization_name' => 'Test Org',
'industry' => 'retail',
'appointment_mode' => 'hybrid',
'branch_name' => 'Main',
'timezone' => 'UTC',
]);
$branch = Branch::first();
$queue = ServiceQueue::create([
'owner_ref' => $user->public_id,
'organization_id' => $org->id,
'branch_id' => $branch->id,
'name' => 'Reception',
'prefix' => 'A',
'strategy' => 'fifo',
'is_active' => true,
]);
return [$user, $org, $branch, $queue];
}
public function test_issue_ticket_increments_sequence(): void
{
[$user, , , $queue] = $this->setUpOrg();
$engine = app(QueueEngine::class);
$ticket = $engine->issueTicket($queue, $user->public_id, ['source' => 'api']);
$this->assertSame('A001', $ticket->ticket_number);
$this->assertSame('waiting', $ticket->status);
$ticket2 = $engine->issueTicket($queue->fresh(), $user->public_id);
$this->assertSame('A002', $ticket2->ticket_number);
}
public function test_health_endpoint(): void
{
$this->getJson('/api/health')->assertOk()->assertJson(['app' => 'queue']);
}
public function test_appointment_check_in_issues_ticket(): void
{
[$user, $org, $branch, $queue] = $this->setUpOrg();
$appointment = \App\Models\QueueAppointment::create([
'owner_ref' => $user->public_id,
'organization_id' => $org->id,
'branch_id' => $branch->id,
'service_queue_id' => $queue->id,
'customer_name' => 'Jane Doe',
'status' => 'scheduled',
'scheduled_at' => now()->addHour(),
'reference' => 'APT-TEST',
]);
$ticket = app(\App\Services\Qms\AppointmentService::class)->checkIn($appointment, $user->public_id);
$this->assertSame('appointment', $ticket->priority);
$this->assertSame('checked_in', $appointment->fresh()->status);
$this->assertSame($ticket->id, $appointment->fresh()->ticket_id);
}
public function test_workflow_metadata_attached_on_issue(): void
{
[$user, $org, $branch, $queue] = $this->setUpOrg();
$workflow = \App\Models\Workflow::create([
'owner_ref' => $user->public_id,
'organization_id' => $org->id,
'name' => 'Test flow',
'is_active' => true,
]);
\App\Models\WorkflowStep::create([
'workflow_id' => $workflow->id,
'service_queue_id' => $queue->id,
'name' => 'Step 1',
'sort_order' => 1,
]);
$ticket = app(QueueEngine::class)->issueTicket($queue, $user->public_id);
$this->assertSame($workflow->id, data_get($ticket->metadata, 'workflow_id'));
}
public function test_routing_rule_redirects_ticket(): void
{
[$user, $org, $branch, $queue] = $this->setUpOrg();
$target = ServiceQueue::create([
'owner_ref' => $user->public_id,
'organization_id' => $org->id,
'branch_id' => $branch->id,
'name' => 'VIP',
'prefix' => 'V',
'strategy' => 'fifo',
'is_active' => true,
]);
\App\Models\QueueRule::create([
'owner_ref' => $user->public_id,
'service_queue_id' => $queue->id,
'rule_type' => 'routing',
'conditions' => ['source' => 'api'],
'actions' => ['target_queue_id' => $target->id],
'sort_order' => 1,
'is_active' => true,
]);
$ticket = app(QueueEngine::class)->issueTicket($queue, $user->public_id, ['source' => 'api']);
$this->assertSame($target->id, $ticket->service_queue_id);
}
public function test_service_session_tracked(): void
{
[$user, , , $queue] = $this->setUpOrg();
$engine = app(QueueEngine::class);
$ticket = $engine->issueTicket($queue, $user->public_id);
$counter = \App\Models\Counter::create([
'owner_ref' => $user->public_id,
'organization_id' => $queue->organization_id,
'branch_id' => $queue->branch_id,
'name' => 'C1',
'status' => 'available',
'is_active' => true,
]);
$engine->callTicket($ticket, $counter, $user->public_id);
$engine->startServing($ticket->fresh(), $user->public_id);
$engine->completeTicket($ticket->fresh(), $user->public_id);
$this->assertDatabaseHas('queue_service_sessions', [
'ticket_id' => $ticket->id,
]);
$session = \App\Models\ServiceSession::where('ticket_id', $ticket->id)->first();
$this->assertNotNull($session->ended_at);
}
public function test_handoff_keeps_ticket_number_and_frees_counter(): void
{
[$user, $org, $branch, $queue] = $this->setUpOrg();
$lab = ServiceQueue::create([
'owner_ref' => $user->public_id,
'organization_id' => $org->id,
'branch_id' => $branch->id,
'name' => 'Lab',
'prefix' => 'L',
'strategy' => 'fifo',
'is_active' => true,
]);
$engine = app(QueueEngine::class);
$ticket = $engine->issueTicket($queue, $user->public_id, [
'source' => 'reception',
'customer_name' => 'Patient One',
]);
$this->assertSame('A001', $ticket->ticket_number);
$counter = \App\Models\Counter::create([
'owner_ref' => $user->public_id,
'organization_id' => $org->id,
'branch_id' => $branch->id,
'name' => 'Doctor 1',
'status' => 'available',
'is_active' => true,
]);
$engine->callTicket($ticket, $counter, $user->public_id);
$engine->startServing($ticket->fresh(), $user->public_id);
$handedOff = $engine->transfer(
$ticket->fresh(),
$lab,
$counter,
'Lab tests needed',
$user->public_id,
);
$this->assertSame('A001', $handedOff->ticket_number);
$this->assertSame($lab->id, $handedOff->service_queue_id);
$this->assertSame('waiting', $handedOff->status);
$this->assertNull($handedOff->counter_id);
$this->assertNull($handedOff->completed_at);
$this->assertSame('available', $counter->fresh()->status);
$this->assertDatabaseHas('queue_ticket_transfers', [
'ticket_id' => $ticket->id,
'from_service_queue_id' => $queue->id,
'to_service_queue_id' => $lab->id,
'reason' => 'Lab tests needed',
]);
$session = \App\Models\ServiceSession::where('ticket_id', $ticket->id)->first();
$this->assertNotNull($session);
$this->assertNotNull($session->ended_at);
}
public function test_reports_sqlite_compatible(): void
{
[$user, $org, , $queue] = $this->setUpOrg();
app(QueueEngine::class)->issueTicket($queue, $user->public_id, ['source' => 'api']);
$reports = app(\App\Services\Qms\ReportService::class);
$from = now()->subDay();
$to = now()->addDay();
$data = $reports->ticketsReport($user->public_id, $org->id, $from, $to);
$this->assertArrayHasKey('total_issued', $data);
$this->assertGreaterThanOrEqual(1, $data['total_issued']);
}
public function test_device_heartbeat(): void
{
[$user, $org, $branch] = $this->setUpOrg();
$device = \App\Models\Device::create([
'owner_ref' => $user->public_id,
'organization_id' => $org->id,
'branch_id' => $branch->id,
'name' => 'Kiosk 1',
'type' => 'kiosk',
'device_token' => 'test-device-token-123',
]);
$this->postJson('/api/devices/heartbeat', [], [
'Authorization' => 'Bearer test-device-token-123',
])->assertOk()->assertJsonPath('status', 'ok');
$this->assertSame('online', $device->fresh()->status);
}
}