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'); } }