Files
ladill-care/tests/Feature/CareQueueBridgeTest.php
T
isaaccladandCursor e94708d46c
Deploy Ladill Care / deploy (push) Successful in 51s
Issue Queue tickets for every waiting appointment when integration is on.
Patient queue syncs consultation and specialty waiters; specialty contexts now provision desks so check-in/onboarding no longer leaves half the list on local positions only.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-18 06:12:32 +00:00

472 lines
18 KiB
PHP

<?php
namespace Tests\Feature;
use App\Http\Middleware\EnsurePlatformSession;
use App\Models\Appointment;
use App\Models\Branch;
use App\Models\Member;
use App\Models\Organization;
use App\Models\Patient;
use App\Models\User;
use App\Services\Care\AppointmentService;
use App\Services\Care\CareQueueContexts;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http;
use Tests\TestCase;
class CareQueueBridgeTest extends TestCase
{
use RefreshDatabase;
protected User $owner;
protected Organization $organization;
protected Branch $branch;
protected Patient $patient;
protected function setUp(): void
{
parent::setUp();
$this->withoutMiddleware(EnsurePlatformSession::class);
config([
'care.queue.api_url' => 'https://queue.test/api/v1',
'care.queue.api_key' => 'care-test-key',
]);
$this->owner = User::create([
'public_id' => 'bridge-owner',
'name' => 'Owner',
'email' => 'bridge@example.com',
]);
$this->organization = Organization::create([
'owner_ref' => $this->owner->public_id,
'name' => 'Bridge Clinic',
'slug' => 'bridge-clinic',
'settings' => [
'onboarded' => true,
'facility_type' => 'clinic',
'plan' => 'pro',
'plan_expires_at' => now()->addMonth()->toIso8601String(),
'queue_integration_enabled' => true,
],
]);
Member::create([
'owner_ref' => $this->owner->public_id,
'organization_id' => $this->organization->id,
'user_ref' => $this->owner->public_id,
'role' => 'hospital_admin',
]);
$this->branch = Branch::create([
'owner_ref' => $this->owner->public_id,
'organization_id' => $this->organization->id,
'name' => 'Main',
'is_active' => true,
]);
$settings = $this->organization->settings;
$settings['department_queue_provisioning'] = [
CareQueueContexts::CONSULTATION => [
'queues' => [[
'branch_id' => $this->branch->id,
'branch_name' => 'Main',
'name' => 'Consultation',
'prefix' => 'C',
'active' => true,
'synced' => true,
'routing_mode' => 'assigned_only',
'queue_uuid' => 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
'counter_uuid' => 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb',
'queue_external_key' => CareQueueContexts::queueExternalKey(CareQueueContexts::CONSULTATION, $this->branch->id),
'counter_external_key' => CareQueueContexts::counterExternalKey(CareQueueContexts::CONSULTATION, $this->branch->id),
'points' => [[
'kind' => 'practitioner',
'ref_id' => 0, // filled after practitioner create in tests that need it
'destination' => 'Consultation desk',
'counter_uuid' => 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb',
'synced' => true,
]],
]],
],
];
$this->organization->update(['settings' => $settings]);
$this->patient = Patient::create([
'owner_ref' => $this->owner->public_id,
'organization_id' => $this->organization->id,
'branch_id' => $this->branch->id,
'patient_number' => 'P-100',
'first_name' => 'Ada',
'last_name' => 'Lovelace',
'gender' => 'female',
'date_of_birth' => '1990-01-01',
]);
}
public function test_check_in_issues_consultation_ticket_when_integration_on(): void
{
$practitioner = \App\Models\Practitioner::create([
'owner_ref' => $this->owner->public_id,
'organization_id' => $this->organization->id,
'branch_id' => $this->branch->id,
'name' => 'Dr. Bridge',
'room' => 'Room 1',
'is_active' => true,
]);
$settings = $this->organization->settings;
$settings['department_queue_provisioning'][CareQueueContexts::CONSULTATION]['queues'][0]['points'] = [[
'kind' => 'practitioner',
'ref_id' => $practitioner->id,
'destination' => 'Room 1',
'staff_display_name' => 'Dr. Bridge',
'counter_uuid' => 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb',
'synced' => true,
]];
$this->organization->update(['settings' => $settings]);
Http::fake(function (\Illuminate\Http\Client\Request $request) {
if (str_contains($request->url(), '/tickets') && $request->method() === 'POST') {
$this->assertSame('bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', $request['assigned_counter_id'] ?? null);
return Http::response([
'data' => [
'uuid' => 'cccccccc-cccc-cccc-cccc-cccccccccccc',
'ticket_number' => 'C001',
'status' => 'waiting',
'assigned_counter' => ['uuid' => 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'],
'destination' => 'Room 1',
'staff_display_name' => 'Dr. Bridge',
],
], 201);
}
return Http::response(['message' => 'unexpected '.$request->url()], 500);
});
$appointment = Appointment::create([
'owner_ref' => $this->owner->public_id,
'organization_id' => $this->organization->id,
'branch_id' => $this->branch->id,
'patient_id' => $this->patient->id,
'practitioner_id' => $practitioner->id,
'type' => Appointment::TYPE_SCHEDULED,
'status' => Appointment::STATUS_SCHEDULED,
'scheduled_at' => now()->addHour(),
]);
$result = app(AppointmentService::class)->checkIn($appointment, $this->owner->public_id, $this->owner->public_id);
$this->assertSame('C001', $result->queue_ticket_number);
$this->assertSame('waiting', $result->queue_ticket_status);
$this->assertSame('cccccccc-cccc-cccc-cccc-cccccccccccc', $result->queue_ticket_uuid);
Http::assertSent(fn ($request) => str_contains($request->url(), '/tickets')
&& ($request['service_queue_id'] ?? null) === 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'
&& ($request['customer_name'] ?? null) === 'Ada Lovelace');
}
public function test_check_in_skips_ticket_when_integration_off(): void
{
Http::fake();
$settings = $this->organization->settings;
$settings['queue_integration_enabled'] = false;
$this->organization->update(['settings' => $settings]);
$appointment = Appointment::create([
'owner_ref' => $this->owner->public_id,
'organization_id' => $this->organization->id,
'branch_id' => $this->branch->id,
'patient_id' => $this->patient->id,
'type' => Appointment::TYPE_SCHEDULED,
'status' => Appointment::STATUS_SCHEDULED,
'scheduled_at' => now()->addHour(),
]);
$result = app(AppointmentService::class)->checkIn(
$appointment->fresh(['organization', 'patient']),
$this->owner->public_id,
$this->owner->public_id,
);
$this->assertNull($result->queue_ticket_uuid);
Http::assertNothingSent();
}
public function test_waiting_list_shows_ticket_number_when_integration_on(): void
{
Appointment::create([
'owner_ref' => $this->owner->public_id,
'organization_id' => $this->organization->id,
'branch_id' => $this->branch->id,
'patient_id' => $this->patient->id,
'type' => Appointment::TYPE_WALK_IN,
'status' => Appointment::STATUS_WAITING,
'scheduled_at' => now(),
'waiting_at' => now(),
'queue_position' => 1,
'queue_ticket_uuid' => 'dddddddd-dddd-dddd-dddd-dddddddddddd',
'queue_ticket_number' => 'C042',
'queue_ticket_status' => 'waiting',
]);
$this->actingAs($this->owner)
->get(route('care.queue.index'))
->assertOk()
->assertSee('C042')
->assertSee('Call next')
->assertSee('Serve');
}
public function test_call_next_backfills_missing_ticket_then_calls(): void
{
$practitioner = \App\Models\Practitioner::create([
'owner_ref' => $this->owner->public_id,
'organization_id' => $this->organization->id,
'branch_id' => $this->branch->id,
'name' => 'Dr. Backfill',
'room' => 'Room 3',
'is_active' => true,
]);
$settings = $this->organization->settings;
$settings['department_queue_provisioning'][CareQueueContexts::CONSULTATION]['queues'][0]['points'] = [[
'kind' => 'practitioner',
'ref_id' => $practitioner->id,
'destination' => 'Room 3',
'counter_uuid' => 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb',
'synced' => true,
]];
$settings['department_queue_provisioning'][CareQueueContexts::CONSULTATION]['queues'][0]['counter_uuid'] = 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb';
$this->organization->update(['settings' => $settings]);
$ticketSeq = 0;
Http::fake(function (\Illuminate\Http\Client\Request $request) use (&$ticketSeq) {
if (str_contains($request->url(), '/call-next') && $request->method() === 'POST') {
if ($ticketSeq < 1) {
return Http::response(['data' => null], 200);
}
return Http::response([
'data' => [
'uuid' => 'ffffffff-ffff-ffff-ffff-ffffffffffff',
'ticket_number' => 'C100',
'customer_name' => 'Ada Lovelace',
'status' => 'called',
],
], 200);
}
if (str_contains($request->url(), '/tickets') && $request->method() === 'POST') {
$ticketSeq++;
return Http::response([
'data' => [
'uuid' => 'ffffffff-ffff-ffff-ffff-ffffffffffff',
'ticket_number' => 'C100',
'status' => 'waiting',
'assigned_counter' => ['uuid' => 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'],
],
], 201);
}
return Http::response(['message' => 'unexpected '.$request->url()], 500);
});
Appointment::create([
'owner_ref' => $this->owner->public_id,
'organization_id' => $this->organization->id,
'branch_id' => $this->branch->id,
'patient_id' => $this->patient->id,
'practitioner_id' => $practitioner->id,
'type' => Appointment::TYPE_WALK_IN,
'status' => Appointment::STATUS_WAITING,
'scheduled_at' => now(),
'waiting_at' => now(),
'queue_position' => 1,
]);
$this->actingAs($this->owner)
->from(route('care.queue.index'))
->post(route('care.queue.call-next'), [
'branch_id' => $this->branch->id,
'practitioner_id' => $practitioner->id,
])
->assertRedirect(route('care.queue.index'))
->assertSessionHas('success');
$this->assertNotNull(Appointment::query()->where('patient_id', $this->patient->id)->value('queue_ticket_uuid'));
Http::assertSent(fn ($r) => str_contains($r->url(), '/tickets') && $r->method() === 'POST');
Http::assertSent(fn ($r) => str_contains($r->url(), '/call-next'));
}
public function test_call_next_empty_queue_flashes_info_not_error(): void
{
Http::fake([
'*/queues/*/call-next*' => Http::response(['data' => null], 200),
]);
$this->actingAs($this->owner)
->from(route('care.queue.index'))
->post(route('care.queue.call-next'), ['branch_id' => $this->branch->id])
->assertRedirect(route('care.queue.index'))
->assertSessionHas('info', 'No patients waiting at your consultation service point.')
->assertSessionMissing('error');
}
public function test_recall_reannounces_called_ticket_from_patient_queue(): void
{
$appointment = Appointment::create([
'owner_ref' => $this->owner->public_id,
'organization_id' => $this->organization->id,
'branch_id' => $this->branch->id,
'patient_id' => $this->patient->id,
'type' => Appointment::TYPE_WALK_IN,
'status' => Appointment::STATUS_WAITING,
'scheduled_at' => now(),
'waiting_at' => now(),
'queue_position' => 1,
'queue_ticket_uuid' => 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee',
'queue_ticket_number' => 'C055',
'queue_ticket_status' => 'called',
]);
Http::fake([
'*/tickets/*/action*' => Http::response([
'data' => [
'uuid' => 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee',
'ticket_number' => 'C055',
'status' => 'called',
'customer_name' => 'Ada Lovelace',
],
], 200),
]);
$this->actingAs($this->owner)
->from(route('care.queue.index'))
->post(route('care.queue.recall', $appointment))
->assertRedirect(route('care.queue.index'))
->assertSessionHas('success');
Http::assertSent(function ($request) {
return str_contains($request->url(), '/tickets/eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee/action')
&& ($request['action'] ?? null) === 'recall';
});
$this->assertSame('called', $appointment->fresh()->queue_ticket_status);
$this->actingAs($this->owner)
->get(route('care.queue.index', ['branch_id' => $this->branch->id]))
->assertOk()
->assertSee('Call again')
->assertSee('C055');
}
public function test_patient_queue_sync_issues_specialty_tickets(): void
{
$department = \App\Models\Department::create([
'owner_ref' => $this->owner->public_id,
'branch_id' => $this->branch->id,
'name' => 'Dentistry',
'type' => 'dental',
'is_active' => true,
]);
$practitioner = \App\Models\Practitioner::create([
'owner_ref' => $this->owner->public_id,
'organization_id' => $this->organization->id,
'branch_id' => $this->branch->id,
'department_id' => $department->id,
'name' => 'Dentistry Clinic',
'room' => 'Dental Bay 1',
'is_active' => true,
]);
$settings = $this->organization->settings;
$settings['specialty_modules'] = ['dentistry' => true];
$settings['specialty_module_provisioning'] = [
'dentistry' => [
'active' => true,
'department_ids' => [$department->id],
'queues' => [[
'module' => 'dentistry',
'branch_id' => $this->branch->id,
'branch_name' => 'Main',
'name' => 'Dentistry',
'prefix' => 'DEN',
'active' => true,
'synced' => true,
'routing_mode' => 'assigned_only',
'queue_uuid' => 'dddddddd-dddd-dddd-dddd-dddddddddddd',
'counter_uuid' => 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee',
'queue_external_key' => CareQueueContexts::queueExternalKey('dentistry', $this->branch->id),
'counter_external_key' => CareQueueContexts::counterExternalKey('dentistry', $this->branch->id),
'points' => [[
'kind' => 'practitioner',
'ref_id' => $practitioner->id,
'destination' => 'Dental Bay 1',
'staff_display_name' => 'Dentistry Clinic',
'counter_uuid' => 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee',
'synced' => true,
]],
]],
],
];
$this->organization->update(['settings' => $settings]);
Appointment::create([
'owner_ref' => $this->owner->public_id,
'organization_id' => $this->organization->id,
'branch_id' => $this->branch->id,
'patient_id' => $this->patient->id,
'practitioner_id' => $practitioner->id,
'department_id' => $department->id,
'type' => Appointment::TYPE_WALK_IN,
'status' => Appointment::STATUS_WAITING,
'scheduled_at' => now(),
'waiting_at' => now(),
'queue_position' => 1,
'reason' => 'Toothache',
]);
Http::fake(function (\Illuminate\Http\Client\Request $request) {
if (str_contains($request->url(), '/tickets') && $request->method() === 'POST') {
$this->assertSame('dddddddd-dddd-dddd-dddd-dddddddddddd', $request['service_queue_id'] ?? null);
$this->assertSame('eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee', $request['assigned_counter_id'] ?? null);
return Http::response([
'data' => [
'uuid' => 'ffffffff-ffff-ffff-ffff-ffffffffffff',
'ticket_number' => 'DEN001',
'status' => 'waiting',
'assigned_counter' => ['uuid' => 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee'],
'destination' => 'Dental Bay 1',
'staff_display_name' => 'Dentistry Clinic',
],
], 201);
}
return Http::response(['data' => []], 200);
});
$this->actingAs($this->owner)
->get(route('care.queue.index', ['branch_id' => $this->branch->id]))
->assertOk()
->assertSee('DEN001')
->assertSee('Toothache');
$this->assertDatabaseHas('care_appointments', [
'patient_id' => $this->patient->id,
'queue_ticket_number' => 'DEN001',
]);
}
}