> organization_id => [department_id => specialty_key] */ protected array $specialtyDepartmentMaps = []; public function __construct( protected CareQueueEngine $engine, protected CareQueueProvisioner $provisioner, protected PlanService $plans, protected SpecialtyModuleService $specialties, protected WorkflowQueueGate $workflowGate, ) {} public function isEnabled(Organization $organization): bool { return $this->plans->canUseQueueIntegration($organization) && (bool) data_get($organization->settings, 'queue_integration_enabled'); } public function contextForAppointment(Organization $organization, Appointment $appointment): string { $departmentId = $appointment->department_id; if (! $departmentId) { return CareQueueContexts::CONSULTATION; } return $this->contextForDepartment($organization, (int) $departmentId); } public function contextForDepartment(Organization $organization, int $departmentId): string { if ($departmentId <= 0) { return CareQueueContexts::CONSULTATION; } return $this->specialtyDepartmentMap($organization)[$departmentId] ?? CareQueueContexts::CONSULTATION; } /** * Department id → specialty module key for this org (request-scoped cache). * * @return array */ public function specialtyDepartmentMap(Organization $organization): array { $orgId = (int) $organization->id; if (isset($this->specialtyDepartmentMaps[$orgId])) { return $this->specialtyDepartmentMaps[$orgId]; } $map = []; foreach ($this->specialties->enabledKeys($organization) as $key) { $deptIds = data_get($organization->settings, "specialty_module_provisioning.{$key}.department_ids", []); if (! is_array($deptIds)) { continue; } foreach ($deptIds as $deptId) { $map[(int) $deptId] = $key; } } return $this->specialtyDepartmentMaps[$orgId] = $map; } public function contextForPractitioner(Organization $organization, ?Practitioner $practitioner): string { if (! $practitioner?->department_id) { return CareQueueContexts::CONSULTATION; } return $this->contextForDepartment($organization, (int) $practitioner->department_id); } public function issueForAppointment(Organization $organization, Appointment $appointment): ?Appointment { if (! $this->isEnabled($organization) || filled($appointment->queue_ticket_uuid)) { return $appointment; } $appointment->loadMissing('visit'); $context = $this->contextForAppointment($organization, $appointment); if (! $this->workflowGate->canRelease($organization, $appointment->visit, $context)) { return $appointment; } $point = $this->resolveAppointmentPoint($organization, $appointment); if (CareQueueContexts::usesAssignedRouting($context) && empty($point['counter_uuid'] ?? null)) { $appointment->forceFill([ 'queue_routing_status' => CareQueueContexts::ROUTING_UNRESOLVED, ])->save(); Log::info('care.queue_issue_unresolved_assignment', [ 'appointment_id' => $appointment->id, 'practitioner_id' => $appointment->practitioner_id, 'context' => $context, ]); return $appointment->fresh(); } $ticket = $this->issue( $organization, $context, (int) $appointment->branch_id, $appointment->patient, [ 'care_entity' => 'appointment', 'care_entity_uuid' => $appointment->uuid, 'care_entity_id' => $appointment->id, 'practitioner_id' => $appointment->practitioner_id, ], $appointment->type === Appointment::TYPE_WALK_IN ? 'walk_in' : 'appointment', 'appointment', $point, ); if (! $ticket) { return $appointment; } $this->applyTicket($appointment, $ticket, $point); 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']); if (! $this->workflowGate->canRelease($organization, $prescription->visit, CareQueueContexts::PHARMACY)) { return $prescription; } $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, ], 'walk_in', 'api', null, ); if (! $ticket) { return $prescription; } $this->applyTicket($prescription, $ticket, null); 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', 'visit', 'investigationType']); $context = $this->contextForInvestigation($request); if (! $this->workflowGate->canRelease($organization, $request->visit, $context)) { return $request; } $ticket = $this->issue( $organization, $context, (int) $request->branch_id, $request->patient, [ 'care_entity' => 'investigation_request', 'care_entity_uuid' => $request->uuid, 'care_entity_id' => $request->id, ], 'walk_in', 'api', null, ); if (! $ticket) { return $request; } $this->applyTicket($request, $ticket, null); return $request->fresh(); } public function contextForInvestigation(InvestigationRequest $request): string { $request->loadMissing('investigationType'); return in_array($request->investigationType?->category, ['xray', 'ultrasound', 'ct', 'mri'], true) ? CareQueueContexts::IMAGING : CareQueueContexts::LABORATORY; } 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', 'visit']); if (! $this->workflowGate->canRelease($organization, $bill->visit, CareQueueContexts::BILLING)) { return $bill; } $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, ], 'walk_in', 'api', null, ); if (! $ticket) { return $bill; } $this->applyTicket($bill, $ticket, null); return $bill->fresh(); } /** * After completing a stage, issue the next department ticket when applicable. */ public function advanceAfterComplete(Organization $organization, Model $entity, string $fromContext): void { if (! $this->isEnabled($organization)) { return; } // Lab/imaging completion → return-to-doctor consultation ticket when practitioner known. if (in_array($fromContext, [CareQueueContexts::LABORATORY, CareQueueContexts::IMAGING], true) && $entity instanceof InvestigationRequest) { $entity->loadMissing(['patient', 'practitioner', 'visit']); $appointment = Appointment::owned($organization->owner_ref) ->where('organization_id', $organization->id) ->where('patient_id', $entity->patient_id) ->where('branch_id', $entity->branch_id) ->whereIn('status', [ Appointment::STATUS_IN_CONSULTATION, Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN, ]) ->when($entity->practitioner_id, fn ($q) => $q->where('practitioner_id', $entity->practitioner_id)) ->latest('id') ->first(); if ($appointment && blank($appointment->queue_ticket_uuid)) { $this->issueForAppointment($organization, $appointment); } } } /** * @return int Number of tickets successfully issued */ public function syncMissingTickets(Organization $organization, string $context, int $branchId, int $limit = 50): int { if (! $this->isEnabled($organization) || $branchId < 1 || $limit < 1) { return 0; } $this->provisioner->ensure($organization, $context, $branchId); return match (true) { $context === CareQueueContexts::PHARMACY => $this->syncMissingPrescriptions($organization, $branchId, $limit), in_array($context, [CareQueueContexts::LABORATORY, CareQueueContexts::IMAGING], true) => $this->syncMissingInvestigations($organization, $context, $branchId, $limit), $context === CareQueueContexts::BILLING => $this->syncMissingBills($organization, $branchId, $limit), default => $this->syncMissingAppointments($organization, $context, $branchId, $limit), }; } /** * Issue Queue tickets for every waiting appointment at a branch (consultation + specialties). */ public function syncMissingAppointmentTickets(Organization $organization, int $branchId, int $limit = 100): int { if (! $this->isEnabled($organization) || $branchId < 1 || $limit < 1) { return 0; } $owner = (string) $organization->owner_ref; $appointments = Appointment::owned($owner) ->where('organization_id', $organization->id) ->where('branch_id', $branchId) ->whereIn('status', [Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN]) ->where(function ($q) { $q->whereNull('queue_ticket_uuid')->orWhere('queue_ticket_uuid', ''); }) ->with(['patient', 'organization', 'practitioner', 'visit']) ->orderBy('queue_position') ->orderBy('waiting_at') ->orderBy('checked_in_at') ->orderBy('id') ->limit($limit) ->get(); $issued = 0; foreach ($appointments as $appointment) { $updated = $this->issueForAppointment($organization, $appointment->fresh([ 'patient', 'organization', 'practitioner', 'visit', ])); if ($updated && filled($updated->queue_ticket_uuid)) { $issued++; } } return $issued; } /** * @return array{ticket: ?array, entity: ?Model, resources: ?array} */ public function callNext( Organization $organization, string $context, int $branchId, ?Member $member = null, ?int $practitionerId = null, ): array { $resources = $this->provisioner->ensure($organization, $context, $branchId); if (! $resources || empty($resources['queue_uuid'])) { return ['ticket' => null, 'entity' => null, 'resources' => $resources]; } $point = $this->resolveCallNextPoint($organization, $context, $branchId, $member, $practitionerId, $resources); if (! $point || empty($point['counter_uuid'])) { return ['ticket' => null, 'entity' => null, 'resources' => $resources]; } // GP / specialty: advance the next Care waiter (board order), even when their // ticket sits on another desk or was never issued (sync-on-GET was removed). if ($this->isAppointmentQueueContext($context)) { $fromCare = $this->callNextAppointment( $organization, $context, $branchId, $practitionerId, $resources, $point, ); if ($fromCare['ticket']) { return $fromCare; } } $callResources = [ 'queue_uuid' => $resources['queue_uuid'], 'counter_uuid' => $point['counter_uuid'], ]; $ticket = $this->attemptCallNext($organization, $callResources); if (! $ticket) { $this->syncMissingTickets($organization, $context, $branchId, 25); $ticket = $this->attemptCallNext($organization, $callResources); } // Admins / reception without a desk: try every service point on this queue so // Call next matches the specialty waiting list instead of one empty desk. if (! $ticket && ! $practitionerId && ( CareQueueContexts::isSpecialty($context) || $context === CareQueueContexts::CONSULTATION )) { $ticket = $this->attemptCallNextAcrossPoints($organization, $resources, $point); } if (! $ticket) { return ['ticket' => null, 'entity' => null, 'resources' => $resources]; } $entity = $this->findEntityByTicket($organization, $context, (string) ($ticket['uuid'] ?? '')); if ($entity) { $matchedPoint = $point; $ticketPointUuid = (string) data_get($ticket, 'assigned_counter.uuid', data_get($ticket, 'counter.uuid', '')); if ($ticketPointUuid !== '') { $matchedPoint = collect($resources['points'] ?? [])->first( fn ($p) => ($p['counter_uuid'] ?? null) === $ticketPointUuid ) ?? $point; } $this->applyTicket($entity, $ticket, $matchedPoint); } return ['ticket' => $ticket, 'entity' => $entity?->fresh(), 'resources' => $resources]; } protected function isAppointmentQueueContext(string $context): bool { return $context === CareQueueContexts::CONSULTATION || CareQueueContexts::isSpecialty($context); } /** * Call the next waiting appointment for this context — matching board order. * * @param array{queue_uuid?: ?string, counter_uuid?: ?string, points?: list>} $resources * @param array $point * @return array{ticket: ?array, entity: ?Model, resources: array} */ protected function callNextAppointment( Organization $organization, string $context, int $branchId, ?int $practitionerId, array $resources, array $point, ): array { $empty = ['ticket' => null, 'entity' => null, 'resources' => $resources]; $candidates = $this->callableAppointments($organization, $context, $branchId, $practitionerId); if ($candidates === []) { $this->syncMissingTickets($organization, $context, $branchId, 25); $candidates = $this->callableAppointments($organization, $context, $branchId, $practitionerId); } foreach ($candidates as $appointment) { $called = $this->callSpecificAppointment($organization, $context, $branchId, $appointment, $resources, $point); if ($called['ticket']) { return $called; } } return $empty; } /** * @param array{queue_uuid?: ?string, counter_uuid?: ?string, points?: list>} $resources * @param array $point * @return array{ticket: ?array, entity: ?Model, resources: array} */ protected function callSpecificAppointment( Organization $organization, string $context, int $branchId, Appointment $appointment, array $resources, array $point, ): array { $empty = ['ticket' => null, 'entity' => null, 'resources' => $resources]; if (blank($appointment->queue_ticket_uuid)) { $appointment = $this->issueForAppointment( $organization, $appointment->fresh(['patient', 'organization', 'practitioner', 'visit']) ?? $appointment, ) ?? $appointment; } if (blank($appointment->queue_ticket_uuid)) { // Last resort: issue directly onto the calling desk. $ticket = $this->issue( $organization, $context, $branchId, $appointment->patient, [ 'care_entity' => 'appointment', 'care_entity_uuid' => $appointment->uuid, 'care_entity_id' => $appointment->id, 'practitioner_id' => $appointment->practitioner_id, ], $appointment->type === Appointment::TYPE_WALK_IN ? 'walk_in' : 'appointment', 'appointment', $point, ); if ($ticket) { $this->applyTicket($appointment, $ticket, $point); $appointment = $appointment->fresh() ?? $appointment; } } if (blank($appointment->queue_ticket_uuid)) { return $empty; } $ticket = $this->attemptCallWaitingTicket( $organization, (string) $appointment->queue_ticket_uuid, (string) $point['counter_uuid'], ); // Stale uuid / wrong queue / non-waiting row — clear and re-issue onto this desk. if (! $ticket) { $appointment->forceFill([ 'queue_ticket_uuid' => null, 'queue_ticket_number' => null, 'queue_ticket_status' => null, 'queue_service_point_uuid' => null, ])->save(); $ticket = $this->issue( $organization, $context, $branchId, $appointment->patient, [ 'care_entity' => 'appointment', 'care_entity_uuid' => $appointment->uuid, 'care_entity_id' => $appointment->id, 'practitioner_id' => $appointment->practitioner_id, ], $appointment->type === Appointment::TYPE_WALK_IN ? 'walk_in' : 'appointment', 'appointment', $point, ); if (! $ticket) { return $empty; } $this->applyTicket($appointment, $ticket, $point); $appointment = $appointment->fresh() ?? $appointment; $ticket = $this->attemptCallWaitingTicket( $organization, (string) $appointment->queue_ticket_uuid, (string) $point['counter_uuid'], ); } if (! $ticket) { return $empty; } $this->applyTicket($appointment, $ticket, $point); return [ 'ticket' => $ticket, 'entity' => $appointment->fresh(), 'resources' => $resources, ]; } /** * @return list */ protected function callableAppointments( Organization $organization, string $context, int $branchId, ?int $practitionerId, ): array { $owner = (string) $organization->owner_ref; $query = Appointment::owned($owner) ->where('organization_id', $organization->id) ->where('branch_id', $branchId) ->whereIn('status', [Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN]) ->where(function ($q) { $q->whereNull('queue_ticket_status') ->orWhere('queue_ticket_status', '') ->orWhereNotIn('queue_ticket_status', ['called', 'serving']); }) ->with(['patient', 'organization', 'practitioner', 'visit']) ->orderBy('queue_position') ->orderBy('waiting_at') ->orderBy('checked_in_at') ->orderBy('id') ->limit(50); if ($practitionerId) { $query->where('practitioner_id', $practitionerId); } $callable = []; foreach ($query->get() as $appointment) { if ($this->contextForAppointment($organization, $appointment) !== $context) { continue; } // Already ticketed waiters stay callable; gate only blocks new issuance. if (blank($appointment->queue_ticket_uuid) && ! $this->workflowGate->canRelease($organization, $appointment->visit, $context)) { continue; } $callable[] = $appointment; } return $callable; } /** * @return array|null */ protected function attemptCallWaitingTicket( Organization $organization, string $ticketUuid, string $counterUuid, ): ?array { try { return $this->engine->callWaitingTicket($organization, $ticketUuid, $counterUuid); } catch (\Throwable $e) { Log::warning('care.queue_call_waiting_ticket_failed', [ 'ticket_uuid' => $ticketUuid, 'message' => $e->getMessage(), ]); return null; } } /** * @param array{queue_uuid?: ?string, counter_uuid?: ?string, points?: list>} $resources * @param array $preferredPoint * @return array|null */ protected function attemptCallNextAcrossPoints( Organization $organization, array $resources, array $preferredPoint, ): ?array { $queueUuid = (string) ($resources['queue_uuid'] ?? ''); if ($queueUuid === '') { return null; } $preferredUuid = (string) ($preferredPoint['counter_uuid'] ?? ''); $points = collect($resources['points'] ?? []) ->filter(fn ($p) => ! empty($p['counter_uuid'])) ->sortBy(fn ($p) => ($p['counter_uuid'] ?? '') === $preferredUuid ? 0 : 1) ->values(); foreach ($points as $candidate) { $uuid = (string) $candidate['counter_uuid']; if ($uuid === $preferredUuid) { continue; } $ticket = $this->attemptCallNext($organization, [ 'queue_uuid' => $queueUuid, 'counter_uuid' => $uuid, ]); if ($ticket) { return $ticket; } } return null; } /** * @param array{queue_uuid?: ?string, counter_uuid?: ?string} $resources * @return array|null */ protected function attemptCallNext(Organization $organization, array $resources): ?array { try { return $this->engine->callNext( $organization, (string) $resources['queue_uuid'], (string) $resources['counter_uuid'], ); } catch (\Throwable $e) { Log::warning('care.queue_call_next_failed', [ 'message' => $e->getMessage(), ]); return null; } } /** * @return array|null */ public function serve(Organization $organization, Model $entity): ?array { return $this->ticketAction($organization, $entity, 'start'); } /** * @return array|null */ public function complete(Organization $organization, Model $entity, ?string $context = null): ?array { $ticket = $this->ticketAction($organization, $entity, 'complete'); if ($ticket && $context) { $this->advanceAfterComplete($organization, $entity, $context); } return $ticket; } /** * @return array|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, points: list>, routing_mode: ?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, 'points' => [], 'routing_mode' => null, ]; } $resources = $this->provisioner->ensure($organization, $context, $branchId); return [ 'enabled' => true, 'context' => $context, 'queue_uuid' => $resources['queue_uuid'] ?? null, 'counter_uuid' => $resources['counter_uuid'] ?? null, 'points' => $resources['points'] ?? [], 'routing_mode' => $resources['routing_mode'] ?? null, ]; } /** * @return array|null */ protected function resolveAppointmentPoint(Organization $organization, Appointment $appointment): ?array { $branchId = (int) $appointment->branch_id; $context = $this->contextForAppointment($organization, $appointment); $this->provisioner->ensure($organization, $context, $branchId); $organization = $organization->fresh() ?? $organization; if ($appointment->practitioner_id) { $point = $this->provisioner->pointForPractitioner( $organization, $branchId, (int) $appointment->practitioner_id, $context, ); if ($point && ! empty($point['counter_uuid'])) { return $point; } // Refresh points for this context only, then retry the assigned desk. $this->provisioner->ensure($organization, $context, $branchId); $organization = $organization->fresh() ?? $organization; $point = $this->provisioner->pointForPractitioner( $organization, $branchId, (int) $appointment->practitioner_id, $context, ); if ($point && ! empty($point['counter_uuid'])) { return $point; } } // Prefer a desk ticket over leaving waiters without a Queue number. return $this->provisioner->defaultPoint($organization, $context, $branchId); } /** * @param array $resources * @return array|null */ protected function resolveCallNextPoint( Organization $organization, string $context, int $branchId, ?Member $member, ?int $practitionerId, array $resources, ): ?array { if ($context === CareQueueContexts::CONSULTATION || CareQueueContexts::isSpecialty($context)) { if ($practitionerId) { return $this->provisioner->pointForPractitioner($organization, $branchId, $practitionerId, $context) ?? collect($resources['points'] ?? [])->first( fn ($p) => ($p['kind'] ?? '') === 'practitioner' && (int) ($p['ref_id'] ?? 0) === $practitionerId ); } if ($member && $member->role === 'doctor') { $prac = Practitioner::owned($organization->owner_ref) ->where('organization_id', $organization->id) ->where('branch_id', $branchId) ->where(function ($q) use ($member) { $q->where('member_id', $member->id) ->orWhere('user_ref', $member->user_ref); }) ->first(); if ($prac) { return $this->provisioner->pointForPractitioner($organization, $branchId, $prac->id, $context); } } } if ($member) { $byMember = $this->provisioner->pointForMember($organization, $context, $branchId, $member); if ($byMember) { return $byMember; } } return $this->provisioner->defaultPoint($organization, $context, $branchId); } /** * @param array $metadata * @param array|null $point * @return array|null */ protected function issue( Organization $organization, string $context, int $branchId, ?Patient $patient, array $metadata = [], string $priority = 'walk_in', string $source = 'api', ?array $point = null, ): ?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; } if (CareQueueContexts::usesAssignedRouting($context) && empty($point['counter_uuid'])) { Log::info('care.queue_issue_skipped_no_point', [ 'context' => $context, 'branch_id' => $branchId, ]); return null; } $payload = [ 'service_queue_id' => $resources['queue_uuid'], 'customer_name' => $patient?->fullName(), 'customer_phone' => $patient?->phone, 'priority' => $priority, 'source' => $source, 'metadata' => $metadata, ]; if (! empty($point['counter_uuid'])) { $payload['assigned_counter_id'] = $point['counter_uuid']; } try { return $this->engine->issueTicket($organization, $payload); } catch (\Throwable $e) { Log::warning('care.queue_issue_failed', [ 'context' => $context, 'branch_id' => $branchId, 'message' => $e->getMessage(), ]); return null; } } /** * @param array $ticket * @param array|null $point */ protected function applyTicket(Model $entity, array $ticket, ?array $point = null): void { $assigned = $ticket['assigned_counter'] ?? null; $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'), 'queue_service_point_uuid' => $ticket['assigned_counter']['uuid'] ?? $ticket['counter']['uuid'] ?? $point['counter_uuid'] ?? $entity->getAttribute('queue_service_point_uuid'), 'queue_destination' => $ticket['destination'] ?? $point['destination'] ?? $entity->getAttribute('queue_destination'), 'queue_staff_display_name' => $ticket['staff_display_name'] ?? $point['staff_display_name'] ?? (is_array($assigned) ? ($assigned['staff_display_name'] ?? null) : null) ?? $entity->getAttribute('queue_staff_display_name'), 'queue_routing_status' => CareQueueContexts::ROUTING_ROUTED, ])->save(); } /** * @return array|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->engine->ticketAction($organization, $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; $entity = match (true) { $context === CareQueueContexts::PHARMACY => Prescription::owned($owner) ->where('organization_id', $organization->id) ->where('queue_ticket_uuid', $ticketUuid) ->first(), in_array($context, [CareQueueContexts::LABORATORY, CareQueueContexts::IMAGING], true) => 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(), }; if ($entity) { return $entity; } // Recover when the Care row lost its uuid mirror but the ticket still points at it. $ticket = CareQueueTicket::query() ->where('organization_id', $organization->id) ->where('uuid', $ticketUuid) ->first(); if (! $ticket || ! $ticket->care_entity_id) { return null; } return match ($ticket->care_entity) { 'prescription' => Prescription::owned($owner) ->where('organization_id', $organization->id) ->whereKey($ticket->care_entity_id) ->first(), 'investigation_request' => InvestigationRequest::owned($owner) ->where('organization_id', $organization->id) ->whereKey($ticket->care_entity_id) ->first(), 'bill' => Bill::owned($owner) ->where('organization_id', $organization->id) ->whereKey($ticket->care_entity_id) ->first(), 'appointment' => Appointment::owned($owner) ->where('organization_id', $organization->id) ->whereKey($ticket->care_entity_id) ->first(), default => null, }; } protected function syncMissingAppointments(Organization $organization, string $context, int $branchId, int $limit): int { $owner = (string) $organization->owner_ref; $appointments = Appointment::owned($owner) ->where('organization_id', $organization->id) ->where('branch_id', $branchId) ->whereIn('status', [Appointment::STATUS_WAITING, Appointment::STATUS_CHECKED_IN]) ->where(function ($q) { $q->whereNull('queue_ticket_uuid')->orWhere('queue_ticket_uuid', ''); }) ->with(['patient', 'organization', 'practitioner']) ->orderBy('queue_position') ->orderBy('waiting_at') ->orderBy('checked_in_at') ->orderBy('id') ->limit($limit) ->get(); $issued = 0; foreach ($appointments as $appointment) { if ($this->contextForAppointment($organization, $appointment) !== $context) { continue; } $updated = $this->issueForAppointment($organization, $appointment->fresh(['patient', 'organization', 'practitioner'])); if ($updated && filled($updated->queue_ticket_uuid)) { $issued++; } } return $issued; } protected function syncMissingPrescriptions(Organization $organization, int $branchId, int $limit): int { $owner = (string) $organization->owner_ref; $prescriptions = Prescription::owned($owner) ->where('organization_id', $organization->id) ->where('status', Prescription::STATUS_ACTIVE) ->where(function ($q) { $q->whereNull('queue_ticket_uuid')->orWhere('queue_ticket_uuid', ''); }) ->whereHas('visit', fn ($q) => $q->where('branch_id', $branchId)) ->with(['patient', 'visit']) ->orderBy('created_at') ->orderBy('id') ->limit($limit) ->get(); $issued = 0; foreach ($prescriptions as $prescription) { $updated = $this->issueForPrescription($organization, $prescription); if ($updated && filled($updated->queue_ticket_uuid)) { $issued++; } } return $issued; } protected function syncMissingInvestigations(Organization $organization, string $context, int $branchId, int $limit): int { $owner = (string) $organization->owner_ref; $requests = InvestigationRequest::owned($owner) ->where('organization_id', $organization->id) ->where('branch_id', $branchId) ->whereIn('status', [ InvestigationRequest::STATUS_PENDING, InvestigationRequest::STATUS_SAMPLE_COLLECTED, InvestigationRequest::STATUS_IN_PROGRESS, ]) ->where(function ($q) { $q->whereNull('queue_ticket_uuid')->orWhere('queue_ticket_uuid', ''); }) ->with(['patient', 'visit', 'investigationType']) ->orderBy('id') ->limit($limit) ->get(); $issued = 0; foreach ($requests as $request) { if ($this->contextForInvestigation($request) !== $context) { continue; } $updated = $this->issueForInvestigation($organization, $request); if ($updated && filled($updated->queue_ticket_uuid)) { $issued++; } } return $issued; } protected function syncMissingBills(Organization $organization, int $branchId, int $limit): int { $owner = (string) $organization->owner_ref; $bills = Bill::owned($owner) ->where('organization_id', $organization->id) ->where('branch_id', $branchId) ->whereIn('status', [Bill::STATUS_OPEN, Bill::STATUS_PARTIAL, Bill::STATUS_DRAFT]) ->where(function ($q) { $q->whereNull('queue_ticket_uuid')->orWhere('queue_ticket_uuid', ''); }) ->with('patient') ->orderBy('id') ->limit($limit) ->get(); $issued = 0; foreach ($bills as $bill) { $updated = $this->issueForBill($organization, $bill); if ($updated && filled($updated->queue_ticket_uuid)) { $issued++; } } return $issued; } }