Deploy Ladill Care / deploy (push) Successful in 1m28s
Issue, call-next, serve, and complete tickets locally so Care Pro no longer needs QUEUE_API_* when CARE_QUEUE_DRIVER=native (default). Remote Ladill Queue remains available for cutover; PHPUnit keeps the remote driver for existing fakes. Co-authored-by: Cursor <cursoragent@cursor.com>
824 lines
29 KiB
PHP
824 lines
29 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Care;
|
|
|
|
use App\Models\Appointment;
|
|
use App\Models\Bill;
|
|
use App\Models\InvestigationRequest;
|
|
use App\Models\Member;
|
|
use App\Models\Organization;
|
|
use App\Models\Patient;
|
|
use App\Models\Practitioner;
|
|
use App\Models\Prescription;
|
|
use App\Services\Care\Workflow\WorkflowQueueGate;
|
|
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 service-queue tickets (native Care Queue
|
|
* Engine by default; optional legacy Ladill Queue HTTP when driver=remote).
|
|
*/
|
|
class CareQueueBridge
|
|
{
|
|
public function __construct(
|
|
protected QueueClient $queue,
|
|
protected CareQueueEngine $engine,
|
|
protected CareQueueProvisioner $provisioner,
|
|
protected PlanService $plans,
|
|
protected SpecialtyModuleService $specialties,
|
|
protected WorkflowQueueGate $workflowGate,
|
|
) {}
|
|
|
|
public function usesNative(): bool
|
|
{
|
|
return config('care.queue.driver', 'native') === 'native';
|
|
}
|
|
|
|
public function isEnabled(Organization $organization): bool
|
|
{
|
|
$enabled = $this->plans->canUseQueueIntegration($organization)
|
|
&& (bool) data_get($organization->settings, 'queue_integration_enabled');
|
|
|
|
if (! $enabled) {
|
|
return false;
|
|
}
|
|
|
|
return $this->usesNative() || $this->queue->configured();
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
$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<string, mixed>, entity: ?Model, resources: ?array<string, mixed>}
|
|
*/
|
|
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];
|
|
}
|
|
|
|
$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);
|
|
}
|
|
|
|
if (! $ticket) {
|
|
return ['ticket' => null, 'entity' => null, 'resources' => $resources];
|
|
}
|
|
|
|
$entity = $this->findEntityByTicket($organization, $context, (string) ($ticket['uuid'] ?? ''));
|
|
if ($entity) {
|
|
$this->applyTicket($entity, $ticket, $point);
|
|
}
|
|
|
|
return ['ticket' => $ticket, 'entity' => $entity?->fresh(), 'resources' => $resources];
|
|
}
|
|
|
|
/**
|
|
* @param array{queue_uuid?: ?string, counter_uuid?: ?string} $resources
|
|
* @return array<string, mixed>|null
|
|
*/
|
|
protected function attemptCallNext(Organization $organization, array $resources): ?array
|
|
{
|
|
try {
|
|
if ($this->usesNative()) {
|
|
return $this->engine->callNext(
|
|
$organization,
|
|
(string) $resources['queue_uuid'],
|
|
(string) $resources['counter_uuid'],
|
|
);
|
|
}
|
|
|
|
return $this->queue->callNext(
|
|
(string) $organization->owner_ref,
|
|
(string) $resources['queue_uuid'],
|
|
(string) $resources['counter_uuid'],
|
|
);
|
|
} catch (RequestException $e) {
|
|
Log::warning('care.queue_call_next_failed', [
|
|
'message' => $e->getMessage(),
|
|
]);
|
|
|
|
return null;
|
|
} catch (\Throwable $e) {
|
|
Log::warning('care.queue_call_next_failed', [
|
|
'message' => $e->getMessage(),
|
|
]);
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>|null
|
|
*/
|
|
public function serve(Organization $organization, Model $entity): ?array
|
|
{
|
|
return $this->ticketAction($organization, $entity, 'start');
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>|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<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, points: list<array<string, mixed>>, 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->fresh(), $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<string, mixed>|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<string, mixed> $resources
|
|
* @return array<string, mixed>|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<string, mixed> $metadata
|
|
* @param array<string, mixed>|null $point
|
|
* @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 $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;
|
|
}
|
|
|
|
$owner = (string) $organization->owner_ref;
|
|
$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 {
|
|
if ($this->usesNative()) {
|
|
return $this->engine->issueTicket($organization, $payload);
|
|
}
|
|
|
|
return $this->queue->issueTicket($owner, $payload);
|
|
} catch (\Throwable $e) {
|
|
Log::warning('care.queue_issue_failed', [
|
|
'context' => $context,
|
|
'branch_id' => $branchId,
|
|
'message' => $e->getMessage(),
|
|
]);
|
|
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $ticket
|
|
* @param array<string, mixed>|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<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 {
|
|
if ($this->usesNative()) {
|
|
$ticket = $this->engine->ticketAction($organization, $uuid, [
|
|
'action' => $action,
|
|
]);
|
|
} else {
|
|
$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(),
|
|
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(),
|
|
};
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|