, * assigned_counter_id?: ?string * } $payload * @return array */ public function issueTicket(Organization $organization, array $payload): array { $queueUuid = (string) ($payload['service_queue_id'] ?? ''); if ($queueUuid === '') { throw new InvalidArgumentException('service_queue_id is required.'); } return DB::transaction(function () use ($organization, $payload, $queueUuid) { /** @var CareServiceQueue|null $queue */ $queue = CareServiceQueue::query() ->where('organization_id', $organization->id) ->where('uuid', $queueUuid) ->lockForUpdate() ->first(); if (! $queue || ! $queue->is_active) { throw new InvalidArgumentException('Service queue not found.'); } $point = null; $counterUuid = (string) ($payload['assigned_counter_id'] ?? ''); if ($counterUuid !== '') { $point = CareServicePoint::query() ->where('organization_id', $organization->id) ->where('service_queue_id', $queue->id) ->where('uuid', $counterUuid) ->where('is_active', true) ->first(); if (! $point) { throw new InvalidArgumentException('Service point not found.'); } } $seq = (int) $queue->next_number; $queue->forceFill(['next_number' => $seq + 1])->save(); $ticketNumber = $queue->prefix.str_pad((string) $seq, 3, '0', STR_PAD_LEFT); $metadata = is_array($payload['metadata'] ?? null) ? $payload['metadata'] : []; $ticket = CareQueueTicket::create([ 'owner_ref' => $organization->owner_ref, 'organization_id' => $organization->id, 'service_queue_id' => $queue->id, 'service_point_id' => $point?->id, 'ticket_number' => $ticketNumber, 'status' => CareQueueTicket::STATUS_WAITING, 'priority' => (string) ($payload['priority'] ?? 'walk_in'), 'source' => (string) ($payload['source'] ?? 'api'), 'customer_name' => $payload['customer_name'] ?? null, 'customer_phone' => $payload['customer_phone'] ?? null, 'care_entity' => $metadata['care_entity'] ?? null, 'care_entity_uuid' => $metadata['care_entity_uuid'] ?? null, 'care_entity_id' => isset($metadata['care_entity_id']) ? (int) $metadata['care_entity_id'] : null, 'visit_id' => isset($metadata['visit_id']) ? (int) $metadata['visit_id'] : null, 'metadata' => $metadata, ]); $ticket->setRelation('servicePoint', $point); $ticket->setRelation('serviceQueue', $queue); return $this->toApiArray($ticket); }); } /** * @return array|null */ public function callNext(Organization $organization, string $queueUuid, string $counterUuid): ?array { return DB::transaction(function () use ($organization, $queueUuid, $counterUuid) { /** @var CareServiceQueue|null $queue */ $queue = CareServiceQueue::query() ->where('organization_id', $organization->id) ->where('uuid', $queueUuid) ->lockForUpdate() ->first(); if (! $queue || ! $queue->is_active) { return null; } /** @var CareServicePoint|null $point */ $point = CareServicePoint::query() ->where('organization_id', $organization->id) ->where('service_queue_id', $queue->id) ->where('uuid', $counterUuid) ->where('is_active', true) ->first(); if (! $point) { return null; } $waiting = CareQueueTicket::query() ->where('service_queue_id', $queue->id) ->where('status', CareQueueTicket::STATUS_WAITING) ->when( $queue->routing_mode === 'assigned_only', fn ($q) => $q->where('service_point_id', $point->id), fn ($q) => $q->where(function ($inner) use ($point) { $inner->whereNull('service_point_id') ->orWhere('service_point_id', $point->id); }), ) ->orderByRaw("CASE priority WHEN 'appointment' THEN 0 WHEN 'walk_in' THEN 1 ELSE 2 END") ->orderBy('id') ->lockForUpdate() ->first(); if (! $waiting) { return null; } $waiting->forceFill([ 'service_point_id' => $point->id, 'status' => CareQueueTicket::STATUS_CALLED, 'called_at' => now(), ])->save(); $waiting->setRelation('servicePoint', $point); $waiting->setRelation('serviceQueue', $queue); try { app(CareVoiceAnnouncementService::class)->announceCalled($waiting, $point); } catch (Throwable $e) { Log::warning('Care voice announcement failed on call-next', [ 'ticket_id' => $waiting->id, 'error' => $e->getMessage(), ]); } return $this->toApiArray($waiting); }); } /** * @param array{action: string} $payload * @return array */ public function ticketAction(Organization $organization, string $ticketUuid, array $payload): array { $action = (string) ($payload['action'] ?? ''); return DB::transaction(function () use ($organization, $ticketUuid, $action) { /** @var CareQueueTicket|null $ticket */ $ticket = CareQueueTicket::query() ->where('organization_id', $organization->id) ->where('uuid', $ticketUuid) ->lockForUpdate() ->first(); if (! $ticket) { throw new InvalidArgumentException('Ticket not found.'); } $ticket->loadMissing('servicePoint', 'serviceQueue'); match ($action) { 'start' => $this->markServing($ticket), 'complete' => $this->markCompleted($ticket), 'recall' => $this->markCalled($ticket), 'skip' => $this->markSkipped($ticket), 'cancel' => $this->markCancelled($ticket), default => throw new InvalidArgumentException("Unknown ticket action [{$action}]."), }; return $this->toApiArray($ticket->fresh(['servicePoint', 'serviceQueue'])); }); } /** * @return array */ public function toApiArray(CareQueueTicket $ticket): array { $point = $ticket->servicePoint; return [ 'uuid' => $ticket->uuid, 'ticket_number' => $ticket->ticket_number, 'status' => $ticket->status, 'priority' => $ticket->priority, 'source' => $ticket->source, 'customer_name' => $ticket->customer_name, 'customer_phone' => $ticket->customer_phone, 'metadata' => $ticket->metadata ?? [], 'assigned_counter' => $point ? [ 'uuid' => $point->uuid, 'name' => $point->name, 'staff_display_name' => $point->staff_display_name, 'destination' => $point->destination, ] : null, 'counter' => $point ? [ 'uuid' => $point->uuid, 'name' => $point->name, ] : null, 'destination' => $point?->destination, 'staff_display_name' => $point?->staff_display_name, 'called_at' => $ticket->called_at?->toIso8601String(), 'serving_at' => $ticket->serving_at?->toIso8601String(), 'completed_at' => $ticket->completed_at?->toIso8601String(), ]; } protected function markServing(CareQueueTicket $ticket): void { $ticket->forceFill([ 'status' => CareQueueTicket::STATUS_SERVING, 'serving_at' => $ticket->serving_at ?? now(), 'called_at' => $ticket->called_at ?? now(), ])->save(); } protected function markCompleted(CareQueueTicket $ticket): void { $ticket->forceFill([ 'status' => CareQueueTicket::STATUS_COMPLETED, 'completed_at' => now(), 'serving_at' => $ticket->serving_at ?? now(), ])->save(); } protected function markCalled(CareQueueTicket $ticket): void { $ticket->forceFill([ 'status' => CareQueueTicket::STATUS_CALLED, 'called_at' => now(), ])->save(); $point = $ticket->servicePoint; if (! $point && $ticket->service_point_id) { $ticket->loadMissing('servicePoint'); $point = $ticket->servicePoint; } if ($point) { try { app(CareVoiceAnnouncementService::class)->announceCalled($ticket, $point); } catch (Throwable $e) { Log::warning('Care voice announcement failed on recall', [ 'ticket_id' => $ticket->id, 'error' => $e->getMessage(), ]); } } } protected function markSkipped(CareQueueTicket $ticket): void { $ticket->forceFill([ 'status' => CareQueueTicket::STATUS_SKIPPED, 'completed_at' => now(), ])->save(); } protected function markCancelled(CareQueueTicket $ticket): void { $ticket->forceFill([ 'status' => CareQueueTicket::STATUS_CANCELLED, 'completed_at' => now(), ])->save(); } }