where('status', 'waiting')->exists()) { return 0; } $queue = trim((string) CompanySettings::get('order_callback_queue', '')); if ($queue === '') { $this->error('Set an order callback queue in Dialer Settings before processing requests.'); return 1; } $client = new ClientImpl([ 'host' => config('pbx.ami.host'), 'scheme' => config('pbx.ami.scheme'), 'port' => config('pbx.ami.port'), 'username' => config('pbx.ami.username'), 'secret' => config('pbx.ami.secret'), 'connect_timeout' => config('pbx.ami.connect_timeout'), 'read_timeout' => max((int) config('pbx.ami.read_timeout', 60) * 1000, 1000), ]); $opened = false; $queueMemberEvents = []; $client->registerEventListener(function (EventMessage $event) use (&$queueMemberEvents) { if ($event instanceof QueueMemberEvent) { $queueMemberEvents[] = $event; } }); try { $client->open(); $opened = true; $queueStatus = $client->send(new QueueStatusAction($queue)); if (!$queueStatus->isSuccess() || !$queueStatus->isComplete()) { throw new RuntimeException('AMI did not return a complete queue status'); } $events = array_merge($queueStatus->getEvents(), $queueMemberEvents); $availableAgents = self::countAvailableAgents($events, $queue); if ($availableAgents === 0) { $this->info('No idle agents in queue ' . $queue . '; callback requests remain waiting.'); return 0; } $callbacks = DB::transaction(function () use ($availableAgents) { $callbacks = DB::table('order_callback_requests') ->where('status', 'waiting') ->orderBy('id') ->lockForUpdate() ->limit($availableAgents) ->get(); foreach ($callbacks as $callback) { DB::table('order_callback_requests') ->where('id', $callback->id) ->update([ 'status' => 'attempting', 'updated_at' => now(), ]); } return $callbacks; }, 3); foreach ($callbacks as $index => $callback) { try { $originate = new OriginateAction('local/' . $callback->caller_number . '@from-internal'); $originate->setContext('ext-did'); $originate->setExtension($queue); $originate->setPriority(1); $originate->setCallerId(config('app.didNum')); $originate->setAsync(true); $originate->setActionID(uniqid('order-callback-', true)); $response = $client->send($originate); if (!$response->isSuccess()) { throw new RuntimeException('AMI rejected callback for request ' . $callback->id . ': ' . $response->getMessage()); } DB::table('order_callback_requests') ->where('id', $callback->id) ->update([ 'status' => 'attempted', 'callback_at' => now(), 'callback_result' => 'accepted', 'updated_at' => now(), ]); \Log::info('Order callback originate accepted', [ 'callback_id' => $callback->id, 'queue' => $queue, ]); $this->info('Dialed callback request ' . $callback->id . ' into queue ' . $queue . '.'); } catch (Throwable $e) { DB::table('order_callback_requests') ->where('id', $callback->id) ->update([ 'status' => 'waiting', 'callback_result' => 'failed', 'updated_at' => now(), ]); foreach ($callbacks->slice($index + 1) as $pendingCallback) { DB::table('order_callback_requests') ->where('id', $pendingCallback->id) ->where('status', 'attempting') ->update([ 'status' => 'waiting', 'updated_at' => now(), ]); } throw $e; } } return 0; } catch (Throwable $e) { \Log::error('Order callback processing failed', ['exception' => $e]); $this->error($e->getMessage()); return 1; } finally { if ($opened) { $client->close(); } } } public static function countAvailableAgents(array $events, string $queue): int { $available = 0; foreach ($events as $event) { if ( $event instanceof QueueMemberEvent && (string) $event->getQueue() === $queue && !$event->getPaused() && (int) $event->getStatus() === 1 ) { $available++; } } return $available; } }