Deploy Ladill Queue / deploy (push) Successful in 2m17s
Counters gain destination/staff metadata; tickets can be pre-assigned to a service point with assigned_only queues for healthcare while shared_pool preserves generic bank/government behavior. Display and announcements expose ticket, staff, and destination clearly for Care and public boards. Co-authored-by: Cursor <cursoragent@cursor.com>
246 lines
9.0 KiB
PHP
246 lines
9.0 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\Branch;
|
|
use App\Models\Counter;
|
|
use App\Models\DisplayScreen;
|
|
use App\Models\Organization;
|
|
use App\Models\ServiceQueue;
|
|
use App\Models\User;
|
|
use App\Services\Qms\DisplayService;
|
|
use App\Services\Qms\OrganizationResolver;
|
|
use App\Services\Qms\QueueEngine;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Tests\TestCase;
|
|
|
|
class ServicePointRoutingTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
/**
|
|
* @return array{0: User, 1: Organization, 2: Branch, 3: ServiceQueue, 4: Counter, 5: Counter}
|
|
*/
|
|
protected function setUpAssignedConsultation(): array
|
|
{
|
|
$user = User::create([
|
|
'public_id' => 'sp-owner',
|
|
'name' => 'Owner',
|
|
'email' => 'sp@example.com',
|
|
'password' => bcrypt('password'),
|
|
]);
|
|
$org = app(OrganizationResolver::class)->completeOnboarding($user, [
|
|
'organization_name' => 'Clinic',
|
|
'industry' => 'healthcare',
|
|
'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' => 'Consultation',
|
|
'prefix' => 'C',
|
|
'strategy' => 'fifo',
|
|
'is_active' => true,
|
|
'settings' => ['routing_mode' => 'assigned_only'],
|
|
]);
|
|
$room4 = Counter::create([
|
|
'owner_ref' => $user->public_id,
|
|
'organization_id' => $org->id,
|
|
'branch_id' => $branch->id,
|
|
'name' => 'Dr. Mensah',
|
|
'destination' => 'Consultation Room 4',
|
|
'staff_ref' => 'doc-mensah',
|
|
'staff_display_name' => 'Dr. Mensah',
|
|
'status' => 'available',
|
|
'is_active' => true,
|
|
]);
|
|
$room5 = Counter::create([
|
|
'owner_ref' => $user->public_id,
|
|
'organization_id' => $org->id,
|
|
'branch_id' => $branch->id,
|
|
'name' => 'Dr. Okai',
|
|
'destination' => 'Consultation Room 5',
|
|
'staff_ref' => 'doc-okai',
|
|
'staff_display_name' => 'Dr. Okai',
|
|
'status' => 'available',
|
|
'is_active' => true,
|
|
]);
|
|
$room4->serviceQueues()->sync([$queue->id]);
|
|
$room5->serviceQueues()->sync([$queue->id]);
|
|
|
|
return [$user, $org, $branch, $queue, $room4, $room5];
|
|
}
|
|
|
|
public function test_call_next_cannot_steal_ticket_assigned_to_another_service_point(): void
|
|
{
|
|
[$user, , , $queue, $room4, $room5] = $this->setUpAssignedConsultation();
|
|
$engine = app(QueueEngine::class);
|
|
|
|
$ticket = $engine->issueTicket($queue, $user->public_id, [
|
|
'customer_name' => 'Ada',
|
|
'assigned_counter_uuid' => $room4->uuid,
|
|
]);
|
|
|
|
$this->assertSame($room4->id, $ticket->assigned_counter_id);
|
|
|
|
$stolen = $engine->callNext($queue, $room5, $user->public_id);
|
|
$this->assertNull($stolen);
|
|
|
|
$called = $engine->callNext($queue, $room4, $user->public_id);
|
|
$this->assertNotNull($called);
|
|
$this->assertSame($ticket->id, $called->id);
|
|
$this->assertSame('called', $called->status);
|
|
$this->assertSame($room4->id, $called->counter_id);
|
|
}
|
|
|
|
public function test_assigned_only_queue_requires_service_point_on_issue(): void
|
|
{
|
|
[$user, , , $queue] = $this->setUpAssignedConsultation();
|
|
|
|
$this->expectException(\RuntimeException::class);
|
|
$this->expectExceptionMessage('assigned service point');
|
|
|
|
app(QueueEngine::class)->issueTicket($queue, $user->public_id, [
|
|
'customer_name' => 'No Point',
|
|
]);
|
|
}
|
|
|
|
public function test_display_shows_latest_per_service_point_with_destination_and_staff(): void
|
|
{
|
|
[$user, , $branch, $queue, $room4, $room5] = $this->setUpAssignedConsultation();
|
|
$engine = app(QueueEngine::class);
|
|
|
|
$t1 = $engine->issueTicket($queue, $user->public_id, [
|
|
'assigned_counter_uuid' => $room4->uuid,
|
|
'customer_name' => 'Patient A',
|
|
]);
|
|
$t2 = $engine->issueTicket($queue, $user->public_id, [
|
|
'assigned_counter_uuid' => $room5->uuid,
|
|
'customer_name' => 'Patient B',
|
|
]);
|
|
|
|
$engine->callTicket($t1, $room4, $user->public_id);
|
|
$engine->callTicket($t2, $room5, $user->public_id);
|
|
|
|
$screen = DisplayScreen::create([
|
|
'owner_ref' => $user->public_id,
|
|
'organization_id' => $queue->organization_id,
|
|
'branch_id' => $branch->id,
|
|
'name' => 'Lobby',
|
|
'layout' => 'standard',
|
|
'service_queue_ids' => [$queue->id],
|
|
'is_active' => true,
|
|
]);
|
|
|
|
$payload = app(DisplayService::class)->payload($screen);
|
|
|
|
$this->assertCount(2, $payload['now_serving']);
|
|
$destinations = collect($payload['now_serving'])->pluck('destination')->all();
|
|
$this->assertContains('Consultation Room 4', $destinations);
|
|
$this->assertContains('Consultation Room 5', $destinations);
|
|
$staff = collect($payload['now_serving'])->pluck('staff_display_name')->all();
|
|
$this->assertContains('Dr. Mensah', $staff);
|
|
$this->assertContains('Dr. Okai', $staff);
|
|
|
|
$mensah = collect($payload['now_serving'])->firstWhere('staff_display_name', 'Dr. Mensah');
|
|
$this->assertStringContainsString('Dr. Mensah', (string) ($mensah['announcement_text'] ?? ''));
|
|
$this->assertStringContainsString('Consultation Room 4', (string) ($mensah['announcement_text'] ?? ''));
|
|
}
|
|
|
|
public function test_shared_pool_still_allows_unassigned_call_next(): void
|
|
{
|
|
$user = User::create([
|
|
'public_id' => 'bank-owner',
|
|
'name' => 'Bank',
|
|
'email' => 'bank@example.com',
|
|
'password' => bcrypt('password'),
|
|
]);
|
|
$org = app(OrganizationResolver::class)->completeOnboarding($user, [
|
|
'organization_name' => 'Bank',
|
|
'industry' => 'banking',
|
|
'appointment_mode' => 'walk_in_only',
|
|
'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' => 'Teller',
|
|
'prefix' => 'T',
|
|
'strategy' => 'fifo',
|
|
'is_active' => true,
|
|
'settings' => ['routing_mode' => 'shared_pool'],
|
|
]);
|
|
$window = Counter::create([
|
|
'owner_ref' => $user->public_id,
|
|
'organization_id' => $org->id,
|
|
'branch_id' => $branch->id,
|
|
'name' => 'Window 1',
|
|
'destination' => 'Window 1',
|
|
'status' => 'available',
|
|
'is_active' => true,
|
|
]);
|
|
$window->serviceQueues()->sync([$queue->id]);
|
|
|
|
$engine = app(QueueEngine::class);
|
|
$ticket = $engine->issueTicket($queue, $user->public_id, ['customer_name' => 'Customer']);
|
|
$this->assertNull($ticket->assigned_counter_id);
|
|
|
|
$called = $engine->callNext($queue, $window, $user->public_id);
|
|
$this->assertNotNull($called);
|
|
$this->assertSame($ticket->id, $called->id);
|
|
}
|
|
|
|
public function test_care_api_can_issue_assigned_ticket(): void
|
|
{
|
|
config(['qms.service_api_keys.care' => 'test-care-key']);
|
|
$owner = 'care-sp-owner';
|
|
$headers = [
|
|
'Authorization' => 'Bearer test-care-key',
|
|
'Accept' => 'application/json',
|
|
];
|
|
|
|
$this->postJson('/api/v1/integrations/provision', [
|
|
'owner' => $owner,
|
|
'organization_name' => 'Care Clinic',
|
|
'branch_name' => 'Main',
|
|
], $headers)->assertSuccessful();
|
|
|
|
$queueUuid = $this->postJson('/api/v1/queues', [
|
|
'owner' => $owner,
|
|
'name' => 'Consultation',
|
|
'prefix' => 'C',
|
|
'branch_name' => 'Main',
|
|
'external_key' => 'care:dept:consultation:queue:1',
|
|
'routing_mode' => 'assigned_only',
|
|
], $headers)->assertCreated()->json('data.uuid');
|
|
|
|
$counterUuid = $this->postJson('/api/v1/counters', [
|
|
'owner' => $owner,
|
|
'name' => 'Dr. Mensah',
|
|
'destination' => 'Room 4',
|
|
'staff_display_name' => 'Dr. Mensah',
|
|
'branch_name' => 'Main',
|
|
'queue_uuids' => [$queueUuid],
|
|
'external_key' => 'care:point:consultation:practitioner:9',
|
|
], $headers)->assertCreated()->json('data.uuid');
|
|
|
|
$this->postJson('/api/v1/tickets', [
|
|
'owner' => $owner,
|
|
'service_queue_id' => $queueUuid,
|
|
'customer_name' => 'Ada',
|
|
'assigned_counter_id' => $counterUuid,
|
|
], $headers)
|
|
->assertCreated()
|
|
->assertJsonPath('data.assigned_counter.uuid', $counterUuid)
|
|
->assertJsonPath('data.destination', 'Room 4')
|
|
->assertJsonPath('data.staff_display_name', 'Dr. Mensah');
|
|
}
|
|
}
|