Deploy Ladill Care / deploy (push) Successful in 1m40s
Provision one consultation point per doctor (room + identity) and role-based points for pharmacy, lab, billing, and triage. Fixed-doctor appointments and walk-ins issue only to the assigned point; Call next respects that assignment and flags unresolved patients rather than random routing. Co-authored-by: Cursor <cursoragent@cursor.com>
398 lines
12 KiB
PHP
398 lines
12 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Queue;
|
|
|
|
use Illuminate\Http\Client\RequestException;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
class QueueClient
|
|
{
|
|
private function base(): string
|
|
{
|
|
return rtrim((string) config('care.queue.api_url'), '/');
|
|
}
|
|
|
|
private function token(): string
|
|
{
|
|
return (string) (config('care.queue.api_key') ?? '');
|
|
}
|
|
|
|
private function http(string $owner)
|
|
{
|
|
return Http::withToken($this->token())
|
|
->acceptJson()
|
|
->timeout(12)
|
|
->withQueryParameters(['owner' => $owner]);
|
|
}
|
|
|
|
public function configured(): bool
|
|
{
|
|
return $this->token() !== '' && $this->base() !== '';
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function status(string $owner): array
|
|
{
|
|
$response = $this->http($owner)->get($this->base().'/integrations/status');
|
|
$response->throw();
|
|
|
|
return (array) ($response->json('data') ?? []);
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function enable(string $owner, string $organizationName, ?string $branchName = null): array
|
|
{
|
|
$this->http($owner)->post($this->base().'/integrations/provision', array_filter([
|
|
'organization_name' => $organizationName,
|
|
'branch_name' => $branchName,
|
|
'industry' => 'healthcare',
|
|
]))->throw();
|
|
|
|
return $this->syncIntegration($owner, true);
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function syncIntegration(string $owner, bool $enabled = true): array
|
|
{
|
|
$response = $this->http($owner)->patch($this->base().'/integrations', [
|
|
'care_enabled' => $enabled,
|
|
]);
|
|
$response->throw();
|
|
|
|
return (array) ($response->json('data') ?? []);
|
|
}
|
|
|
|
public function disable(string $owner): void
|
|
{
|
|
// Local Care setting only; Queue must be disabled separately in Queue settings.
|
|
}
|
|
|
|
public function isLinked(string $owner): bool
|
|
{
|
|
if (! $this->configured()) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
$status = $this->status($owner);
|
|
|
|
return (bool) ($status['provisioned'] ?? false)
|
|
&& (bool) data_get($status, 'integrations.care', false);
|
|
} catch (RequestException) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return list<array<string, mixed>>
|
|
*/
|
|
public function counters(string $owner): array
|
|
{
|
|
$response = $this->http($owner)->get($this->base().'/counters');
|
|
$response->throw();
|
|
|
|
return (array) ($response->json('data') ?? []);
|
|
}
|
|
|
|
/**
|
|
* @return list<array<string, mixed>>
|
|
*/
|
|
public function branches(string $owner): array
|
|
{
|
|
$response = $this->http($owner)->get($this->base().'/branches');
|
|
$response->throw();
|
|
|
|
return (array) ($response->json('data') ?? []);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $payload
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function ensureDepartment(string $owner, array $payload): array
|
|
{
|
|
$response = $this->http($owner)->post($this->base().'/departments', $payload);
|
|
$response->throw();
|
|
|
|
return (array) ($response->json('data') ?? []);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $payload
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function ensureBranch(string $owner, array $payload): array
|
|
{
|
|
$response = $this->http($owner)->post($this->base().'/branches', $payload);
|
|
$response->throw();
|
|
|
|
return (array) ($response->json('data') ?? []);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $payload
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function ensureQueue(string $owner, array $payload): array
|
|
{
|
|
$response = $this->http($owner)->post($this->base().'/queues', $payload);
|
|
$response->throw();
|
|
|
|
return (array) ($response->json('data') ?? []);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $payload
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function ensureCounter(string $owner, array $payload): array
|
|
{
|
|
$response = $this->http($owner)->post($this->base().'/counters', $payload);
|
|
$response->throw();
|
|
|
|
return (array) ($response->json('data') ?? []);
|
|
}
|
|
|
|
/**
|
|
* Soft-deactivate a Queue counter without destroying history.
|
|
*
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function updateCounter(string $owner, string $counterUuid, array $payload): array
|
|
{
|
|
$response = $this->http($owner)->patch($this->base().'/counters/'.$counterUuid, $payload);
|
|
$response->throw();
|
|
|
|
return (array) ($response->json('data') ?? []);
|
|
}
|
|
|
|
/**
|
|
* Soft-deactivate a Queue service queue without destroying history.
|
|
*
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function updateQueue(string $owner, string $queueUuid, array $payload): array
|
|
{
|
|
$response = $this->http($owner)->patch($this->base().'/queues/'.$queueUuid, $payload);
|
|
$response->throw();
|
|
|
|
return (array) ($response->json('data') ?? []);
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function console(string $owner, string $counterUuid): array
|
|
{
|
|
$response = $this->http($owner)->get($this->base().'/counters/'.$counterUuid.'/console');
|
|
$response->throw();
|
|
|
|
return (array) ($response->json('data') ?? []);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $payload
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function consoleAction(string $owner, string $counterUuid, array $payload): array
|
|
{
|
|
$response = $this->http($owner)->post($this->base().'/counters/'.$counterUuid.'/console', $payload);
|
|
$response->throw();
|
|
|
|
return (array) ($response->json('data') ?? []);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $payload
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function issueTicket(string $owner, array $payload): array
|
|
{
|
|
$response = $this->http($owner)->post($this->base().'/tickets', $payload);
|
|
$response->throw();
|
|
|
|
return (array) ($response->json('data') ?? []);
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>|null
|
|
*/
|
|
public function callNext(string $owner, string $queueUuid, string $counterUuid): ?array
|
|
{
|
|
$response = $this->http($owner)->post($this->base().'/queues/'.$queueUuid.'/call-next', [
|
|
'counter_id' => $counterUuid,
|
|
]);
|
|
$response->throw();
|
|
|
|
$data = $response->json('data');
|
|
|
|
return is_array($data) ? $data : null;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $payload
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function ticketAction(string $owner, string $ticketUuid, array $payload): array
|
|
{
|
|
$response = $this->http($owner)->post($this->base().'/tickets/'.$ticketUuid.'/action', $payload);
|
|
$response->throw();
|
|
|
|
return (array) ($response->json('data') ?? []);
|
|
}
|
|
|
|
/**
|
|
* Create or link real Queue queues/counters for Care specialty stubs.
|
|
* Idempotent via external_key (and Queue-side name+branch fallback).
|
|
*
|
|
* @param list<array<string, mixed>> $stubs
|
|
* @return list<array<string, mixed>>
|
|
*/
|
|
public function syncSpecialtyQueueStubs(string $owner, array $stubs): array
|
|
{
|
|
if (! $this->configured() || $stubs === []) {
|
|
return $stubs;
|
|
}
|
|
|
|
try {
|
|
if (! $this->isLinked($owner)) {
|
|
return $stubs;
|
|
}
|
|
} catch (\Throwable) {
|
|
return $stubs;
|
|
}
|
|
|
|
foreach ($stubs as $i => $stub) {
|
|
if (! ($stub['active'] ?? true)) {
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
$stubs[$i] = $this->ensureSpecialtyStub($owner, $stub);
|
|
} catch (\Throwable $e) {
|
|
Log::warning('queue.specialty_stub_sync_failed', [
|
|
'owner' => $owner,
|
|
'stub' => $stub['name'] ?? null,
|
|
'branch' => $stub['branch_name'] ?? null,
|
|
'message' => $e->getMessage(),
|
|
]);
|
|
$stubs[$i]['synced'] = false;
|
|
}
|
|
}
|
|
|
|
return $stubs;
|
|
}
|
|
|
|
/**
|
|
* Soft-deactivate Queue resources linked on Care stubs (leaves history intact).
|
|
*
|
|
* @param list<array<string, mixed>> $stubs
|
|
* @return list<array<string, mixed>>
|
|
*/
|
|
public function deactivateSpecialtyQueueStubs(string $owner, array $stubs): array
|
|
{
|
|
if (! $this->configured() || $stubs === []) {
|
|
return $stubs;
|
|
}
|
|
|
|
foreach ($stubs as $i => $stub) {
|
|
try {
|
|
if (! empty($stub['counter_uuid'])) {
|
|
$this->updateCounter($owner, (string) $stub['counter_uuid'], ['is_active' => false]);
|
|
}
|
|
if (! empty($stub['queue_uuid'])) {
|
|
$this->updateQueue($owner, (string) $stub['queue_uuid'], ['is_active' => false]);
|
|
}
|
|
} catch (\Throwable $e) {
|
|
Log::warning('queue.specialty_stub_deactivate_failed', [
|
|
'owner' => $owner,
|
|
'counter_uuid' => $stub['counter_uuid'] ?? null,
|
|
'queue_uuid' => $stub['queue_uuid'] ?? null,
|
|
'message' => $e->getMessage(),
|
|
]);
|
|
}
|
|
|
|
$stubs[$i]['active'] = false;
|
|
}
|
|
|
|
return $stubs;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $stub
|
|
* @return array<string, mixed>
|
|
*/
|
|
protected function ensureSpecialtyStub(string $owner, array $stub): array
|
|
{
|
|
$branchName = trim((string) ($stub['branch_name'] ?? ''));
|
|
$name = trim((string) ($stub['name'] ?? ''));
|
|
$prefix = trim((string) ($stub['prefix'] ?? 'A'));
|
|
$module = (string) ($stub['module'] ?? 'specialty');
|
|
$careBranchId = (string) ($stub['branch_id'] ?? '0');
|
|
|
|
if ($branchName === '' || $name === '') {
|
|
$stub['synced'] = false;
|
|
|
|
return $stub;
|
|
}
|
|
|
|
$queueKey = (string) ($stub['queue_external_key'] ?? "care:specialty:{$module}:queue:{$careBranchId}");
|
|
$counterKey = (string) ($stub['counter_external_key'] ?? "care:specialty:{$module}:counter:{$careBranchId}");
|
|
|
|
try {
|
|
$this->ensureBranch($owner, ['name' => $branchName]);
|
|
} catch (RequestException $e) {
|
|
// Free Queue plans may already be at branch cap — continue if a branch can be resolved.
|
|
if ($e->response?->status() !== 422) {
|
|
throw $e;
|
|
}
|
|
$branches = collect($this->branches($owner));
|
|
$existing = $branches->first(
|
|
fn ($b) => strcasecmp((string) ($b['name'] ?? ''), $branchName) === 0
|
|
);
|
|
if (! $existing && $branches->count() === 1) {
|
|
// Single-location Queue orgs often use a different default name (e.g. "Main Branch").
|
|
$existing = $branches->first();
|
|
$branchName = (string) ($existing['name'] ?? $branchName);
|
|
}
|
|
if (! $existing) {
|
|
throw $e;
|
|
}
|
|
}
|
|
|
|
$queue = $this->ensureQueue($owner, [
|
|
'name' => $name,
|
|
'prefix' => $prefix !== '' ? $prefix : 'A',
|
|
'branch_name' => $branchName,
|
|
'strategy' => 'fifo',
|
|
'external_key' => $queueKey,
|
|
'routing_mode' => (string) ($stub['routing_mode'] ?? 'shared_pool'),
|
|
'is_active' => true,
|
|
]);
|
|
|
|
$counter = $this->ensureCounter($owner, [
|
|
'name' => $name.' counter',
|
|
'destination' => $name,
|
|
'branch_name' => $branchName,
|
|
'queue_uuids' => array_values(array_filter([(string) ($queue['uuid'] ?? '')])),
|
|
'external_key' => $counterKey,
|
|
'is_active' => true,
|
|
]);
|
|
|
|
$stub['queue_external_key'] = $queueKey;
|
|
$stub['counter_external_key'] = $counterKey;
|
|
$stub['queue_uuid'] = $queue['uuid'] ?? null;
|
|
$stub['counter_uuid'] = $counter['uuid'] ?? null;
|
|
$stub['synced'] = ! empty($stub['queue_uuid']) && ! empty($stub['counter_uuid']);
|
|
|
|
return $stub;
|
|
}
|
|
}
|