Fix queue handoff so tickets keep their number across services.
Deploy Ladill Queue / deploy (push) Successful in 40s

Handoff now frees the counter, ends the service session, and requeues
the same ticket for the next department — critical for hospital flows
like doctor → lab.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
isaacclad
2026-07-14 21:22:01 +00:00
co-authored by Cursor
parent d938acf909
commit d2f5ff11d2
12 changed files with 333 additions and 74 deletions
+79 -25
View File
@@ -230,35 +230,89 @@ class QueueEngine
?string $reason = null,
?string $actorRef = null,
): Ticket {
$fromQueueId = $ticket->service_queue_id;
return DB::transaction(function () use ($ticket, $toQueue, $counter, $reason, $actorRef) {
$ticket = Ticket::query()->lockForUpdate()->findOrFail($ticket->id);
$toQueue = ServiceQueue::query()->lockForUpdate()->findOrFail($toQueue->id);
$ticket->update([
'service_queue_id' => $toQueue->id,
'branch_id' => $toQueue->branch_id,
'status' => 'waiting',
'counter_id' => null,
'called_at' => null,
'serving_started_at' => null,
]);
if ($toQueue->id === $ticket->service_queue_id) {
throw new \RuntimeException('Ticket is already in that queue.');
}
DB::table('queue_ticket_transfers')->insert([
'owner_ref' => $ticket->owner_ref,
'ticket_id' => $ticket->id,
'from_service_queue_id' => $fromQueueId,
'to_service_queue_id' => $toQueue->id,
'from_counter_id' => $counter?->id,
'reason' => $reason,
'transferred_by' => $actorRef,
'transferred_at' => now(),
]);
if ($ticket->organization_id !== $toQueue->organization_id || $ticket->owner_ref !== $toQueue->owner_ref) {
throw new \RuntimeException('Cannot hand off to a queue outside this organization.');
}
$this->recordEvent($ticket, 'transferred', $actorRef, $counter?->id, [
'to_queue_id' => $toQueue->id,
'reason' => $reason,
]);
$this->audit($ticket, 'ticket.transferred', $actorRef, ['to_queue' => $toQueue->name]);
if (! $toQueue->is_active || $toQueue->is_paused) {
throw new \RuntimeException('Target queue is not accepting tickets.');
}
return $ticket->fresh(['serviceQueue']);
if ($toQueue->max_capacity) {
$waiting = Ticket::query()
->where('service_queue_id', $toQueue->id)
->whereIn('status', ['waiting', 'called', 'serving', 'on_hold'])
->count();
if ($waiting >= $toQueue->max_capacity) {
throw new \RuntimeException('Target queue has reached maximum capacity.');
}
}
$fromQueueId = $ticket->service_queue_id;
$fromCounterId = $counter?->id ?? $ticket->counter_id;
$ticketNumber = $ticket->ticket_number;
$waitingCount = Ticket::query()
->where('service_queue_id', $toQueue->id)
->where('status', 'waiting')
->count();
$estimatedWait = $waitingCount * (int) $toQueue->avg_service_seconds;
// Same ticket row and number — only the queue assignment changes.
$ticket->update([
'service_queue_id' => $toQueue->id,
'branch_id' => $toQueue->branch_id,
'status' => 'waiting',
'position' => $waitingCount + 1,
'estimated_wait_seconds' => $estimatedWait,
'counter_id' => null,
'called_at' => null,
'serving_started_at' => null,
'completed_at' => null,
]);
if ($fromCounterId) {
Counter::where('id', $fromCounterId)->update(['status' => 'available']);
}
app(ServiceSessionTracker::class)->endForTicket($ticket);
DB::table('queue_ticket_transfers')->insert([
'owner_ref' => $ticket->owner_ref,
'ticket_id' => $ticket->id,
'from_service_queue_id' => $fromQueueId,
'to_service_queue_id' => $toQueue->id,
'from_counter_id' => $fromCounterId,
'reason' => $reason,
'transferred_by' => $actorRef,
'transferred_at' => now(),
]);
$this->recordEvent($ticket, 'transferred', $actorRef, $fromCounterId, [
'from_queue_id' => $fromQueueId,
'to_queue_id' => $toQueue->id,
'ticket_number' => $ticketNumber,
'reason' => $reason,
]);
$this->audit($ticket, 'ticket.transferred', $actorRef, [
'to_queue' => $toQueue->name,
'ticket_number' => $ticketNumber,
]);
$ticket = $ticket->fresh(['serviceQueue', 'counter']);
app(QueueNotificationService::class)->ticketTransferred($ticket);
$this->notifyWaitingPositions($toQueue);
return $ticket;
});
}
public function pauseQueue(ServiceQueue $queue, ?string $actorRef = null): ServiceQueue