Issue Queue tickets for every waiting appointment when integration is on.
Deploy Ladill Care / deploy (push) Successful in 51s

Patient queue syncs consultation and specialty waiters; specialty contexts now provision desks so check-in/onboarding no longer leaves half the list on local positions only.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-18 06:12:32 +00:00
co-authored by Cursor
parent 4e910a1db7
commit e94708d46c
6 changed files with 312 additions and 66 deletions
+73 -36
View File
@@ -66,15 +66,16 @@ class CareQueueBridge
return $appointment;
}
$point = $this->resolveConsultationPoint($organization, $appointment);
$point = $this->resolveAppointmentPoint($organization, $appointment);
if (CareQueueContexts::usesAssignedRouting($context) && ! $point) {
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();
@@ -280,6 +281,44 @@ class CareQueueBridge
};
}
/**
* 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<string, mixed>, entity: ?Model, resources: ?array<string, mixed>}
*/
@@ -405,43 +444,41 @@ class CareQueueBridge
/**
* @return array<string, mixed>|null
*/
protected function resolveConsultationPoint(Organization $organization, Appointment $appointment): ?array
protected function resolveAppointmentPoint(Organization $organization, Appointment $appointment): ?array
{
$branchId = (int) $appointment->branch_id;
$context = $this->contextForAppointment($organization, $appointment);
// Assigned-only consultation requires a doctor — leave Unassigned until one is chosen.
if (! $appointment->practitioner_id) {
return null;
$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;
}
}
$point = $this->provisioner->pointForPractitioner(
$organization,
$branchId,
(int) $appointment->practitioner_id,
);
if ($point) {
return $point;
}
// Practitioner assigned but point not provisioned yet — refresh this branch only.
$this->provisioner->ensure($organization, CareQueueContexts::CONSULTATION, $branchId);
$point = $this->provisioner->pointForPractitioner(
$organization->fresh(),
$branchId,
(int) $appointment->practitioner_id,
);
if ($point) {
return $point;
}
// Prefer a desk ticket over leaving waiters Unassigned while counters catch up
// after demo seed / deferred settings provision.
return $this->provisioner->defaultPoint(
$organization->fresh(),
CareQueueContexts::CONSULTATION,
$branchId,
);
// Prefer a desk ticket over leaving waiters without a Queue number.
return $this->provisioner->defaultPoint($organization, $context, $branchId);
}
/**
@@ -458,7 +495,7 @@ class CareQueueBridge
): ?array {
if ($context === CareQueueContexts::CONSULTATION || CareQueueContexts::isSpecialty($context)) {
if ($practitionerId) {
return $this->provisioner->pointForPractitioner($organization, $branchId, $practitionerId)
return $this->provisioner->pointForPractitioner($organization, $branchId, $practitionerId, $context)
?? collect($resources['points'] ?? [])->first(
fn ($p) => ($p['kind'] ?? '') === 'practitioner' && (int) ($p['ref_id'] ?? 0) === $practitionerId
);
@@ -473,7 +510,7 @@ class CareQueueBridge
})
->first();
if ($prac) {
return $this->provisioner->pointForPractitioner($organization, $branchId, $prac->id);
return $this->provisioner->pointForPractitioner($organization, $branchId, $prac->id, $context);
}
}
}