Files
ladill-care/tests/Feature/CareQueueWorkflowTest.php
T
isaaccladandCursor 015a4cc7fe
Deploy Ladill Care / deploy (push) Successful in 1m45s
Wire Care lists into Ladill Queue workflows instead of reception panels.
When Queue integration is on, role pages issue tickets into their own
department queues and drive Call next → Serve → Complete on the native
lists. Integration off leaves existing UI unchanged.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-17 16:27:48 +00:00

362 lines
14 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\Prescription;
use App\Models\User;
use App\Models\Visit;
use App\Services\Care\AppointmentService;
use App\Services\Care\CareQueueBridge;
use App\Services\Care\CareQueueContexts;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use Tests\TestCase;
class CareQueueWorkflowTest extends TestCase
{
use RefreshDatabase;
protected User $owner;
protected Organization $organization;
protected Branch $branch;
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' => 'care-queue-workflow-owner',
'name' => 'Owner',
'email' => 'queue-workflow@example.com',
]);
$this->organization = Organization::create([
'owner_ref' => $this->owner->public_id,
'name' => 'Workflow Clinic',
'slug' => 'workflow-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,
]);
}
protected function fakeQueueApi(): void
{
$consultationQueue = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa';
$pharmacyQueue = 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb';
$consultationCounter = 'cccccccc-cccc-cccc-cccc-cccccccccccc';
$pharmacyCounter = 'dddddddd-dddd-dddd-dddd-dddddddddddd';
$seq = ['C' => 0, 'P' => 0];
Http::fake(function (\Illuminate\Http\Client\Request $request) use (
$consultationQueue,
$pharmacyQueue,
$consultationCounter,
$pharmacyCounter,
&$seq,
) {
$url = $request->url();
$method = $request->method();
if (str_contains($url, '/integrations/status')) {
return Http::response([
'data' => [
'provisioned' => true,
'integrations' => ['care' => true],
],
], 200);
}
if ($method === 'POST' && str_contains($url, '/branches')) {
return Http::response(['data' => ['id' => 1, 'name' => 'Main']], 201);
}
if ($method === 'POST' && str_contains($url, '/queues') && ! str_contains($url, 'call-next')) {
$payload = $request->data();
$key = (string) ($payload['external_key'] ?? '');
$isPharmacy = str_contains($key, 'pharmacy');
return Http::response([
'data' => [
'uuid' => $isPharmacy ? $pharmacyQueue : $consultationQueue,
'name' => $payload['name'] ?? 'Queue',
'prefix' => $payload['prefix'] ?? 'X',
],
], 201);
}
if ($method === 'POST' && str_contains($url, '/counters')) {
$payload = $request->data();
$key = (string) ($payload['external_key'] ?? '');
$isPharmacy = str_contains($key, 'pharmacy');
return Http::response([
'data' => [
'uuid' => $isPharmacy ? $pharmacyCounter : $consultationCounter,
'name' => $payload['name'] ?? 'Counter',
],
], 201);
}
if ($method === 'POST' && preg_match('#/tickets$#', parse_url($url, PHP_URL_PATH) ?? '')) {
$payload = $request->data();
$queueId = (string) ($payload['service_queue_id'] ?? '');
$prefix = $queueId === $pharmacyQueue ? 'P' : 'C';
$seq[$prefix]++;
return Http::response([
'data' => [
'uuid' => (string) Str::uuid(),
'ticket_number' => $prefix.sprintf('%03d', $seq[$prefix]),
'status' => 'waiting',
'customer_name' => $payload['customer_name'] ?? null,
],
], 201);
}
if ($method === 'POST' && str_contains($url, '/call-next')) {
$path = parse_url($url, PHP_URL_PATH) ?? '';
$isPharmacy = str_contains($path, $pharmacyQueue);
return Http::response([
'data' => [
'uuid' => (string) Str::uuid(),
'ticket_number' => $isPharmacy ? 'P001' : 'C001',
'status' => 'called',
'customer_name' => 'Called Patient',
],
], 200);
}
if ($method === 'POST' && str_contains($url, '/tickets/') && str_contains($url, '/action')) {
$payload = $request->data();
return Http::response([
'data' => [
'uuid' => basename(dirname(parse_url($url, PHP_URL_PATH) ?? '/x/x')),
'ticket_number' => 'C001',
'status' => $payload['action'] === 'complete' ? 'completed' : ($payload['action'] === 'start' ? 'serving' : 'called'),
],
], 200);
}
return Http::response(['message' => 'unexpected '.$method.' '.$url], 500);
});
}
public function test_integration_off_does_not_issue_tickets_on_check_in(): void
{
$settings = $this->organization->settings;
$settings['queue_integration_enabled'] = false;
$this->organization->update(['settings' => $settings]);
Http::fake();
$patient = Patient::create([
'uuid' => (string) Str::uuid(),
'owner_ref' => $this->owner->public_id,
'organization_id' => $this->organization->id,
'branch_id' => $this->branch->id,
'patient_number' => 'P-1',
'first_name' => 'Ada',
'last_name' => 'Lovelace',
]);
$appointment = Appointment::create([
'uuid' => (string) Str::uuid(),
'owner_ref' => $this->owner->public_id,
'organization_id' => $this->organization->id,
'branch_id' => $this->branch->id,
'patient_id' => $patient->id,
'type' => Appointment::TYPE_SCHEDULED,
'status' => Appointment::STATUS_SCHEDULED,
'scheduled_at' => now(),
]);
app(AppointmentService::class)->checkIn($appointment, $this->owner->public_id, $this->owner->public_id);
$appointment->refresh();
$this->assertNull($appointment->queue_ticket_uuid);
$this->assertNull($appointment->queue_ticket_number);
Http::assertNothingSent();
}
public function test_integration_on_issues_consultation_ticket_on_check_in(): void
{
$this->fakeQueueApi();
$patient = Patient::create([
'uuid' => (string) Str::uuid(),
'owner_ref' => $this->owner->public_id,
'organization_id' => $this->organization->id,
'branch_id' => $this->branch->id,
'patient_number' => 'P-2',
'first_name' => 'Grace',
'last_name' => 'Hopper',
]);
$appointment = Appointment::create([
'uuid' => (string) Str::uuid(),
'owner_ref' => $this->owner->public_id,
'organization_id' => $this->organization->id,
'branch_id' => $this->branch->id,
'patient_id' => $patient->id,
'type' => Appointment::TYPE_SCHEDULED,
'status' => Appointment::STATUS_SCHEDULED,
'scheduled_at' => now(),
]);
app(AppointmentService::class)->checkIn($appointment, $this->owner->public_id, $this->owner->public_id);
$appointment->refresh();
$this->assertNotNull($appointment->queue_ticket_uuid);
$this->assertSame('C001', $appointment->queue_ticket_number);
$this->assertSame('waiting', $appointment->queue_ticket_status);
}
public function test_pharmacy_call_next_uses_pharmacy_queue_not_reception(): void
{
$this->fakeQueueApi();
$this->organization->update([
'settings' => array_merge($this->organization->settings ?? [], [
'department_queue_provisioning' => [
CareQueueContexts::PHARMACY => [
'queues' => [[
'branch_id' => $this->branch->id,
'branch_name' => 'Main',
'name' => 'Pharmacy',
'prefix' => 'P',
'active' => true,
'synced' => true,
'queue_uuid' => 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb',
'counter_uuid' => 'dddddddd-dddd-dddd-dddd-dddddddddddd',
'queue_external_key' => CareQueueContexts::queueExternalKey(CareQueueContexts::PHARMACY, $this->branch->id),
'counter_external_key' => CareQueueContexts::counterExternalKey(CareQueueContexts::PHARMACY, $this->branch->id),
]],
],
],
]),
]);
Member::query()->where('user_ref', $this->owner->public_id)->update(['role' => 'pharmacist']);
$this->actingAs($this->owner)
->post(route('care.prescriptions.call-next'), ['branch_id' => $this->branch->id])
->assertRedirect();
Http::assertSent(function (\Illuminate\Http\Client\Request $request) {
return $request->method() === 'POST'
&& str_contains($request->url(), '/queues/bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb/call-next');
});
}
public function test_complete_advances_ticket_status_on_appointment(): void
{
$this->fakeQueueApi();
$patient = Patient::create([
'uuid' => (string) Str::uuid(),
'owner_ref' => $this->owner->public_id,
'organization_id' => $this->organization->id,
'branch_id' => $this->branch->id,
'patient_number' => 'P-3',
'first_name' => 'Alan',
'last_name' => 'Turing',
]);
$appointment = Appointment::create([
'uuid' => (string) Str::uuid(),
'owner_ref' => $this->owner->public_id,
'organization_id' => $this->organization->id,
'branch_id' => $this->branch->id,
'patient_id' => $patient->id,
'type' => Appointment::TYPE_WALK_IN,
'status' => Appointment::STATUS_WAITING,
'queue_ticket_uuid' => 'eeeeeeee-eeee-eeee-eeee-eeeeeeeeeeee',
'queue_ticket_number' => 'C009',
'queue_ticket_status' => 'serving',
'scheduled_at' => now(),
]);
app(CareQueueBridge::class)->complete($this->organization, $appointment);
$appointment->refresh();
$this->assertSame('completed', $appointment->queue_ticket_status);
}
public function test_patient_queue_shows_call_next_not_service_counter_panel(): void
{
$this->fakeQueueApi();
$this->organization->update([
'settings' => array_merge($this->organization->settings ?? [], [
'department_queue_provisioning' => [
CareQueueContexts::CONSULTATION => [
'queues' => [[
'branch_id' => $this->branch->id,
'branch_name' => 'Main',
'name' => 'Consultation',
'prefix' => 'C',
'active' => true,
'synced' => true,
'queue_uuid' => 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
'counter_uuid' => 'cccccccc-cccc-cccc-cccc-cccccccccccc',
]],
],
],
]),
]);
$this->actingAs($this->owner)
->get(route('care.queue.index'))
->assertOk()
->assertDontSee('Service counter')
->assertSee('Call next')
->assertSee('tickets are for this department only', false);
}
public function test_integration_off_hides_queue_ops_on_pharmacy_page(): void
{
$settings = $this->organization->settings;
$settings['queue_integration_enabled'] = false;
$this->organization->update(['settings' => $settings]);
Member::query()->where('user_ref', $this->owner->public_id)->update(['role' => 'pharmacist']);
$this->actingAs($this->owner)
->get(route('care.prescriptions.queue'))
->assertOk()
->assertDontSee('Call next')
->assertDontSee('Service counter');
}
}