Files
ladill-care/tests/Feature/CareQueueBridgeTest.php
T
isaaccladandCursor a00a8249ad
Deploy Ladill Care / deploy (push) Successful in 40s
Improve specialty workspace actions and Call next handoff.
Move timeline into a workspace tab, surface Complete consultation and Call again in Actions, and make Call next end the current encounter then open the next called patient.

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

501 lines
18 KiB
PHP

<?php
namespace Tests\Feature;
use App\Http\Middleware\EnsurePlatformSession;
use App\Models\Appointment;
use App\Models\Branch;
use App\Models\CareQueueTicket;
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);
$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' => 'receptionist',
]);
$this->branch = Branch::create([
'owner_ref' => $this->owner->public_id,
'organization_id' => $this->organization->id,
'name' => 'Main',
'is_active' => true,
]);
$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
{
Http::fake();
$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,
]);
$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->assertNotNull($result->queue_ticket_uuid);
$this->assertSame('waiting', $result->queue_ticket_status);
$this->assertStringStartsWith('C', (string) $result->queue_ticket_number);
$this->assertSame('Room 1', $result->queue_destination);
$this->assertSame('Dr. Bridge', $result->queue_staff_display_name);
$this->assertDatabaseHas('care_queue_tickets', [
'uuid' => $result->queue_ticket_uuid,
'ticket_number' => $result->queue_ticket_number,
'status' => CareQueueTicket::STATUS_WAITING,
'care_entity' => 'appointment',
'care_entity_id' => $result->id,
]);
Http::assertNothingSent();
}
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
{
Http::fake();
$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,
]);
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,
]);
$response = $this->actingAs($this->owner)
->from(route('care.queue.index'))
->post(route('care.queue.call-next'), [
'branch_id' => $this->branch->id,
'practitioner_id' => $practitioner->id,
])
->assertSessionHas('success');
$appointment = Appointment::query()->where('patient_id', $this->patient->id)->first();
$this->assertNotNull($appointment?->queue_ticket_uuid);
$this->assertSame('called', $appointment->queue_ticket_status);
$response->assertRedirect(route('care.appointments.show', $appointment));
$this->assertDatabaseHas('care_queue_tickets', [
'uuid' => $appointment->queue_ticket_uuid,
'status' => CareQueueTicket::STATUS_CALLED,
]);
Http::assertNothingSent();
}
public function test_call_next_empty_queue_flashes_info_not_error(): void
{
Http::fake();
$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');
Http::assertNothingSent();
}
public function test_recall_reannounces_called_ticket_from_patient_queue(): void
{
Http::fake();
$practitioner = \App\Models\Practitioner::create([
'owner_ref' => $this->owner->public_id,
'organization_id' => $this->organization->id,
'branch_id' => $this->branch->id,
'name' => 'Dr. Recall',
'room' => 'Room 2',
'is_active' => true,
]);
$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_WALK_IN,
'status' => Appointment::STATUS_SCHEDULED,
'scheduled_at' => now(),
]);
$checkedIn = app(AppointmentService::class)->checkIn(
$appointment->fresh(['organization', 'patient', 'practitioner']),
$this->owner->public_id,
$this->owner->public_id,
);
$bridge = app(\App\Services\Care\CareQueueBridge::class);
$called = $bridge->callNext(
$this->organization->fresh(),
CareQueueContexts::CONSULTATION,
(int) $this->branch->id,
null,
$practitioner->id,
);
$this->assertNotNull($called['ticket']);
$appointment = $checkedIn->fresh();
$this->assertSame('called', $appointment->queue_ticket_status);
$doctorUser = User::create([
'public_id' => 'bridge-recall-doc',
'name' => 'Recall Doctor',
'email' => 'bridge-recall@example.com',
]);
Member::create([
'owner_ref' => $this->owner->public_id,
'organization_id' => $this->organization->id,
'user_ref' => $doctorUser->public_id,
'role' => 'doctor',
'branch_id' => $this->branch->id,
]);
\App\Models\Practitioner::create([
'owner_ref' => $this->owner->public_id,
'organization_id' => $this->organization->id,
'branch_id' => $this->branch->id,
'member_id' => Member::query()->where('user_ref', $doctorUser->public_id)->value('id'),
'user_ref' => $doctorUser->public_id,
'name' => 'Recall Doctor',
'room' => 'Room 2',
'is_active' => true,
]);
$this->actingAs($doctorUser)
->from(route('care.queue.index'))
->post(route('care.queue.recall', $appointment))
->assertRedirect(route('care.queue.index'))
->assertSessionHas('success');
$this->assertSame('called', $appointment->fresh()->queue_ticket_status);
$this->actingAs($this->owner)
->get(route('care.queue.index', ['branch_id' => $this->branch->id]))
->assertOk()
->assertSee($appointment->queue_ticket_number);
Http::assertNothingSent();
}
public function test_patient_queue_sync_issues_specialty_tickets(): void
{
Http::fake();
$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' => false,
]],
],
];
$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',
]);
$this->actingAs($this->owner)
->get(route('care.queue.index', ['branch_id' => $this->branch->id]))
->assertOk()
->assertSee('Toothache');
$appointment = Appointment::query()->where('patient_id', $this->patient->id)->first();
$this->assertNotNull($appointment?->queue_ticket_uuid);
$this->assertStringStartsWith('DEN', (string) $appointment->queue_ticket_number);
$this->assertDatabaseHas('care_service_queues', [
'organization_id' => $this->organization->id,
'branch_id' => $this->branch->id,
'context' => 'dentistry',
]);
$this->assertDatabaseHas('care_queue_tickets', [
'uuid' => $appointment->queue_ticket_uuid,
'ticket_number' => $appointment->queue_ticket_number,
'status' => CareQueueTicket::STATUS_WAITING,
]);
Http::assertNothingSent();
}
public function test_call_next_ends_current_session_and_opens_next_patient(): void
{
Http::fake();
$practitioner = \App\Models\Practitioner::create([
'owner_ref' => $this->owner->public_id,
'organization_id' => $this->organization->id,
'branch_id' => $this->branch->id,
'name' => 'Dr. Advance',
'room' => 'Room 4',
'is_active' => true,
]);
$currentPatient = Patient::create([
'owner_ref' => $this->owner->public_id,
'organization_id' => $this->organization->id,
'branch_id' => $this->branch->id,
'patient_number' => 'P-CUR',
'first_name' => 'Current',
'last_name' => 'Patient',
'gender' => 'female',
'date_of_birth' => '1991-01-01',
]);
$currentVisit = \App\Models\Visit::create([
'owner_ref' => $this->owner->public_id,
'organization_id' => $this->organization->id,
'branch_id' => $this->branch->id,
'patient_id' => $currentPatient->id,
'status' => \App\Models\Visit::STATUS_IN_PROGRESS,
'checked_in_at' => now(),
]);
$current = Appointment::create([
'owner_ref' => $this->owner->public_id,
'organization_id' => $this->organization->id,
'branch_id' => $this->branch->id,
'patient_id' => $currentPatient->id,
'practitioner_id' => $practitioner->id,
'visit_id' => $currentVisit->id,
'type' => Appointment::TYPE_WALK_IN,
'status' => Appointment::STATUS_IN_CONSULTATION,
'scheduled_at' => now()->subHour(),
'waiting_at' => now()->subHour(),
'checked_in_at' => now()->subHour(),
'started_at' => now()->subMinutes(30),
'queue_ticket_status' => 'serving',
]);
\App\Models\Consultation::create([
'owner_ref' => $this->owner->public_id,
'visit_id' => $currentVisit->id,
'appointment_id' => $current->id,
'practitioner_id' => $practitioner->id,
'patient_id' => $currentPatient->id,
'status' => \App\Models\Consultation::STATUS_DRAFT,
'started_at' => now()->subMinutes(30),
]);
$nextVisit = \App\Models\Visit::create([
'owner_ref' => $this->owner->public_id,
'organization_id' => $this->organization->id,
'branch_id' => $this->branch->id,
'patient_id' => $this->patient->id,
'status' => \App\Models\Visit::STATUS_OPEN,
'checked_in_at' => now(),
]);
$next = 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,
'visit_id' => $nextVisit->id,
'type' => Appointment::TYPE_WALK_IN,
'status' => Appointment::STATUS_WAITING,
'scheduled_at' => now(),
'waiting_at' => now(),
'checked_in_at' => now(),
'queue_position' => 1,
]);
app(\App\Services\Care\CareQueueBridge::class)->issueForAppointment(
$this->organization->fresh(),
$next->fresh(['patient', 'practitioner', 'organization']),
);
$response = $this->actingAs($this->owner)
->post(route('care.queue.call-next'), [
'branch_id' => $this->branch->id,
'practitioner_id' => $practitioner->id,
'appointment_id' => $current->id,
])
->assertSessionHas('success');
$this->assertSame(Appointment::STATUS_COMPLETED, $current->fresh()->status);
$this->assertSame(\App\Models\Consultation::STATUS_COMPLETED, $current->consultation->fresh()->status);
$next = $next->fresh();
$this->assertSame('called', $next->queue_ticket_status);
$response->assertRedirect(route('care.appointments.show', $next));
$this->actingAs($this->owner)
->get(route('care.appointments.show', $next))
->assertOk()
->assertSee('Call again');
Http::assertNothingSent();
}
}