Files
ladill-care/app/Services/Care/CareQueueBridge.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

387 lines
12 KiB
PHP

<?php
namespace App\Services\Care;
use App\Models\Appointment;
use App\Models\Bill;
use App\Models\InvestigationRequest;
use App\Models\Organization;
use App\Models\Patient;
use App\Models\Prescription;
use App\Services\Queue\QueueClient;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Http\Client\RequestException;
use Illuminate\Support\Facades\Log;
/**
* Bridges Care workflow entities to Ladill Queue tickets for the matching department queue.
*/
class CareQueueBridge
{
public function __construct(
protected QueueClient $queue,
protected CareQueueProvisioner $provisioner,
protected PlanService $plans,
protected SpecialtyModuleService $specialties,
) {}
public function isEnabled(Organization $organization): bool
{
return $this->plans->canUseQueueIntegration($organization)
&& (bool) data_get($organization->settings, 'queue_integration_enabled')
&& $this->queue->configured();
}
/**
* Resolve which queue context an appointment should use (specialty if applicable).
*/
public function contextForAppointment(Organization $organization, Appointment $appointment): string
{
$departmentId = $appointment->department_id;
if (! $departmentId) {
return CareQueueContexts::CONSULTATION;
}
foreach ($this->specialties->enabledKeys($organization) as $key) {
$deptIds = data_get($organization->settings, "specialty_module_provisioning.{$key}.department_ids", []);
if (is_array($deptIds) && in_array($departmentId, array_map('intval', $deptIds), true)) {
return $key;
}
}
return CareQueueContexts::CONSULTATION;
}
public function issueForAppointment(Organization $organization, Appointment $appointment): ?Appointment
{
if (! $this->isEnabled($organization) || filled($appointment->queue_ticket_uuid)) {
return $appointment;
}
$context = $this->contextForAppointment($organization, $appointment);
$ticket = $this->issue(
$organization,
$context,
(int) $appointment->branch_id,
$appointment->patient,
[
'care_entity' => 'appointment',
'care_entity_uuid' => $appointment->uuid,
'care_entity_id' => $appointment->id,
],
$appointment->type === Appointment::TYPE_WALK_IN ? 'walk_in' : 'appointment',
'appointment',
);
if (! $ticket) {
return $appointment;
}
$this->applyTicket($appointment, $ticket);
return $appointment->fresh();
}
public function issueForPrescription(Organization $organization, Prescription $prescription): ?Prescription
{
if (! $this->isEnabled($organization) || filled($prescription->queue_ticket_uuid)) {
return $prescription;
}
$prescription->loadMissing(['patient', 'visit']);
$branchId = (int) ($prescription->visit?->branch_id ?? 0);
if ($branchId < 1) {
return $prescription;
}
$ticket = $this->issue(
$organization,
CareQueueContexts::PHARMACY,
$branchId,
$prescription->patient,
[
'care_entity' => 'prescription',
'care_entity_uuid' => $prescription->uuid,
'care_entity_id' => $prescription->id,
],
);
if (! $ticket) {
return $prescription;
}
$this->applyTicket($prescription, $ticket);
return $prescription->fresh();
}
public function issueForInvestigation(Organization $organization, InvestigationRequest $request): ?InvestigationRequest
{
if (! $this->isEnabled($organization) || filled($request->queue_ticket_uuid)) {
return $request;
}
$request->loadMissing('patient');
$ticket = $this->issue(
$organization,
CareQueueContexts::LABORATORY,
(int) $request->branch_id,
$request->patient,
[
'care_entity' => 'investigation_request',
'care_entity_uuid' => $request->uuid,
'care_entity_id' => $request->id,
],
);
if (! $ticket) {
return $request;
}
$this->applyTicket($request, $ticket);
return $request->fresh();
}
public function issueForBill(Organization $organization, Bill $bill): ?Bill
{
if (! $this->isEnabled($organization) || filled($bill->queue_ticket_uuid)) {
return $bill;
}
if (in_array($bill->status, [Bill::STATUS_PAID, Bill::STATUS_VOID], true)) {
return $bill;
}
$bill->loadMissing('patient');
$ticket = $this->issue(
$organization,
CareQueueContexts::BILLING,
(int) $bill->branch_id,
$bill->patient,
[
'care_entity' => 'bill',
'care_entity_uuid' => $bill->uuid,
'care_entity_id' => $bill->id,
],
);
if (! $ticket) {
return $bill;
}
$this->applyTicket($bill, $ticket);
return $bill->fresh();
}
/**
* Call the next waiting ticket for a Care page context + branch.
*
* @return array{ticket: ?array<string, mixed>, entity: ?Model, resources: ?array<string, mixed>}
*/
public function callNext(Organization $organization, string $context, int $branchId): array
{
$resources = $this->provisioner->ensure($organization, $context, $branchId);
if (! $resources || empty($resources['queue_uuid']) || empty($resources['counter_uuid'])) {
return ['ticket' => null, 'entity' => null, 'resources' => $resources];
}
$owner = (string) $organization->owner_ref;
try {
$ticket = $this->queue->callNext(
$owner,
(string) $resources['queue_uuid'],
(string) $resources['counter_uuid'],
);
} catch (RequestException $e) {
Log::warning('care.queue_call_next_failed', [
'context' => $context,
'branch_id' => $branchId,
'message' => $e->getMessage(),
]);
return ['ticket' => null, 'entity' => null, 'resources' => $resources];
}
if (! $ticket) {
return ['ticket' => null, 'entity' => null, 'resources' => $resources];
}
$entity = $this->findEntityByTicket($organization, $context, (string) ($ticket['uuid'] ?? ''));
if ($entity) {
$this->applyTicket($entity, $ticket);
}
return ['ticket' => $ticket, 'entity' => $entity?->fresh(), 'resources' => $resources];
}
/**
* Mark ticket as serving (Queue "start").
*
* @return array<string, mixed>|null
*/
public function serve(Organization $organization, Model $entity): ?array
{
return $this->ticketAction($organization, $entity, 'start');
}
/**
* Complete the Queue ticket linked to a Care entity.
*
* @return array<string, mixed>|null
*/
public function complete(Organization $organization, Model $entity): ?array
{
return $this->ticketAction($organization, $entity, 'complete');
}
/**
* Recall the Queue ticket linked to a Care entity.
*
* @return array<string, mixed>|null
*/
public function recall(Organization $organization, Model $entity): ?array
{
return $this->ticketAction($organization, $entity, 'recall');
}
/**
* @return array{enabled: bool, context: string, queue_uuid: ?string, counter_uuid: ?string}
*/
public function listMeta(Organization $organization, string $context, int $branchId): array
{
if (! $this->isEnabled($organization)) {
return [
'enabled' => false,
'context' => $context,
'queue_uuid' => null,
'counter_uuid' => null,
];
}
$resources = $this->provisioner->ensure($organization->fresh(), $context, $branchId);
return [
'enabled' => true,
'context' => $context,
'queue_uuid' => $resources['queue_uuid'] ?? null,
'counter_uuid' => $resources['counter_uuid'] ?? null,
];
}
/**
* @param array<string, mixed> $metadata
* @return array<string, mixed>|null
*/
protected function issue(
Organization $organization,
string $context,
int $branchId,
?Patient $patient,
array $metadata = [],
string $priority = 'walk_in',
string $source = 'api',
): ?array {
$resources = $this->provisioner->ensure($organization, $context, $branchId);
if (! $resources || empty($resources['queue_uuid'])) {
Log::info('care.queue_issue_skipped_no_queue', [
'context' => $context,
'branch_id' => $branchId,
]);
return null;
}
$owner = (string) $organization->owner_ref;
try {
return $this->queue->issueTicket($owner, [
'service_queue_id' => $resources['queue_uuid'],
'customer_name' => $patient?->fullName(),
'customer_phone' => $patient?->phone,
'priority' => $priority,
'source' => $source,
'metadata' => $metadata,
]);
} catch (\Throwable $e) {
Log::warning('care.queue_issue_failed', [
'context' => $context,
'branch_id' => $branchId,
'message' => $e->getMessage(),
]);
return null;
}
}
/**
* @param array<string, mixed> $ticket
*/
protected function applyTicket(Model $entity, array $ticket): void
{
$entity->forceFill([
'queue_ticket_uuid' => $ticket['uuid'] ?? $entity->getAttribute('queue_ticket_uuid'),
'queue_ticket_number' => $ticket['ticket_number'] ?? $entity->getAttribute('queue_ticket_number'),
'queue_ticket_status' => $ticket['status'] ?? $entity->getAttribute('queue_ticket_status'),
])->save();
}
/**
* @return array<string, mixed>|null
*/
protected function ticketAction(Organization $organization, Model $entity, string $action): ?array
{
$uuid = (string) $entity->getAttribute('queue_ticket_uuid');
if ($uuid === '' || ! $this->isEnabled($organization)) {
return null;
}
try {
$ticket = $this->queue->ticketAction((string) $organization->owner_ref, $uuid, [
'action' => $action,
]);
$this->applyTicket($entity, $ticket);
return $ticket;
} catch (\Throwable $e) {
Log::warning('care.queue_ticket_action_failed', [
'action' => $action,
'ticket_uuid' => $uuid,
'message' => $e->getMessage(),
]);
return null;
}
}
protected function findEntityByTicket(Organization $organization, string $context, string $ticketUuid): ?Model
{
if ($ticketUuid === '') {
return null;
}
$owner = (string) $organization->owner_ref;
return match (true) {
$context === CareQueueContexts::PHARMACY => Prescription::owned($owner)
->where('organization_id', $organization->id)
->where('queue_ticket_uuid', $ticketUuid)
->first(),
$context === CareQueueContexts::LABORATORY => InvestigationRequest::owned($owner)
->where('organization_id', $organization->id)
->where('queue_ticket_uuid', $ticketUuid)
->first(),
$context === CareQueueContexts::BILLING => Bill::owned($owner)
->where('organization_id', $organization->id)
->where('queue_ticket_uuid', $ticketUuid)
->first(),
default => Appointment::owned($owner)
->where('organization_id', $organization->id)
->where('queue_ticket_uuid', $ticketUuid)
->first(),
};
}
}