prefix . 'twp_queued_calls'; // Get current position in queue $max_position = $wpdb->get_var($wpdb->prepare( "SELECT MAX(position) FROM $table_name WHERE queue_id = %d AND status = 'waiting'", $queue_id )); $position = $max_position ? $max_position + 1 : 1; $result = $wpdb->insert( $table_name, array( 'queue_id' => $queue_id, 'call_sid' => sanitize_text_field($call_data['call_sid']), 'from_number' => sanitize_text_field($call_data['from_number']), 'to_number' => sanitize_text_field($call_data['to_number']), 'position' => $position, 'status' => 'waiting' ), array('%d', '%s', '%s', '%s', '%d', '%s') ); if ($result !== false) { // Notify agents via SMS when a new call enters the queue self::notify_agents_for_queue($queue_id, $call_data['from_number']); return $position; } return false; } /** * Remove call from queue */ public static function remove_from_queue($call_sid) { global $wpdb; $table_name = $wpdb->prefix . 'twp_queued_calls'; // Get call info before removing $call = $wpdb->get_row($wpdb->prepare( "SELECT * FROM $table_name WHERE call_sid = %s", $call_sid )); if ($call) { // Update status $wpdb->update( $table_name, array( 'status' => 'completed', 'ended_at' => current_time('mysql') ), array('call_sid' => $call_sid), array('%s', '%s'), array('%s') ); // Reorder queue positions self::reorder_queue($call->queue_id); return true; } return false; } /** * Get next call in queue */ public static function get_next_call($queue_id) { global $wpdb; $table_name = $wpdb->prefix . 'twp_queued_calls'; return $wpdb->get_row($wpdb->prepare( "SELECT * FROM $table_name WHERE queue_id = %d AND status = 'waiting' ORDER BY position ASC LIMIT 1", $queue_id )); } /** * Answer queued call */ public static function answer_call($call_sid, $agent_number) { global $wpdb; $table_name = $wpdb->prefix . 'twp_queued_calls'; // Update call status $wpdb->update( $table_name, array( 'status' => 'answered', 'answered_at' => current_time('mysql') ), array('call_sid' => $call_sid), array('%s', '%s'), array('%s') ); // Connect call to agent $twilio = new TWP_Twilio_API(); $twilio->forward_call($call_sid, $agent_number); return true; } /** * Process waiting calls */ public function process_waiting_calls() { global $wpdb; $table_name = $wpdb->prefix . 'twp_queued_calls'; $queue_table = $wpdb->prefix . 'twp_call_queues'; error_log('TWP Queue Process: Starting queue processing'); // Get all active queues $queues = $wpdb->get_results("SELECT * FROM $queue_table"); foreach ($queues as $queue) { error_log('TWP Queue Process: Processing queue ' . $queue->queue_name . ' (ID: ' . $queue->id . ')'); // First, try to assign agents to waiting calls $this->assign_agents_to_waiting_calls($queue); // Check for timed out calls $timeout_time = date('Y-m-d H:i:s', strtotime('-' . $queue->timeout_seconds . ' seconds')); $timed_out_calls = $wpdb->get_results($wpdb->prepare( "SELECT * FROM $table_name WHERE queue_id = %d AND status = 'waiting' AND joined_at <= %s", $queue->id, $timeout_time )); foreach ($timed_out_calls as $call) { error_log('TWP Queue Process: Handling timeout for call ' . $call->call_sid); // Handle timeout $this->handle_timeout($call, $queue); } // Update caller positions and play position messages $this->update_queue_positions($queue->id); } error_log('TWP Queue Process: Finished queue processing'); } /** * Handle call timeout */ private function handle_timeout($call, $queue) { global $wpdb; $table_name = $wpdb->prefix . 'twp_queued_calls'; // Update status $wpdb->update( $table_name, array( 'status' => 'timeout', 'ended_at' => current_time('mysql') ), array('id' => $call->id), array('%s', '%s'), array('%d') ); // Offer callback instead of hanging up $callback_twiml = TWP_Callback_Manager::create_callback_twiml($queue->id, $call->from_number); $twilio = new TWP_Twilio_API(); $twilio->update_call($call->call_sid, array( 'twiml' => $callback_twiml )); // Reorder queue self::reorder_queue($queue->id); } /** * Assign agents to waiting calls */ private function assign_agents_to_waiting_calls($queue) { global $wpdb; $table_name = $wpdb->prefix . 'twp_queued_calls'; // Get waiting calls in order $waiting_calls = $wpdb->get_results($wpdb->prepare( "SELECT * FROM $table_name WHERE queue_id = %d AND status = 'waiting' ORDER BY position ASC", $queue->id )); if (empty($waiting_calls)) { return; } error_log('TWP Queue Process: Found ' . count($waiting_calls) . ' waiting calls in queue ' . $queue->queue_name); // Get available agents for this queue $available_agents = $this->get_available_agents_for_queue($queue); if (empty($available_agents)) { error_log('TWP Queue Process: No available agents for queue ' . $queue->queue_name); return; } error_log('TWP Queue Process: Found ' . count($available_agents) . ' available agents'); // Assign agents to calls (one agent per call) $assignments = 0; foreach ($waiting_calls as $call) { if ($assignments >= count($available_agents)) { break; // No more agents available } $agent = $available_agents[$assignments]; error_log('TWP Queue Process: Attempting to assign call ' . $call->call_sid . ' to agent ' . $agent['phone']); // Try to bridge the call to the agent if ($this->bridge_call_to_agent($call, $agent, $queue)) { $assignments++; error_log('TWP Queue Process: Successfully initiated bridge for call ' . $call->call_sid); } else { error_log('TWP Queue Process: Failed to bridge call ' . $call->call_sid . ' to agent'); } } error_log('TWP Queue Process: Made ' . $assignments . ' call assignments'); } /** * Get available agents for a queue */ private function get_available_agents_for_queue($queue) { // If queue has assigned agent groups, get agents from those groups if (!empty($queue->agent_groups)) { $group_ids = explode(',', $queue->agent_groups); $agents = array(); foreach ($group_ids as $group_id) { $group_agents = TWP_Agent_Manager::get_available_agents(intval($group_id)); if ($group_agents) { $agents = array_merge($agents, $group_agents); } } return $agents; } // Fallback to all available agents return TWP_Agent_Manager::get_available_agents(); } /** * Bridge call to agent */ private function bridge_call_to_agent($call, $agent, $queue) { $twilio = new TWP_Twilio_API(); try { // Create a new call to the agent $agent_call_data = array( 'to' => $agent['phone'], 'from' => $queue->caller_id ?: $call->to_number, // Use queue caller ID or original number 'url' => home_url('/wp-json/twilio-webhook/v1/agent-connect?' . http_build_query(array( 'customer_call_sid' => $call->call_sid, 'customer_number' => $call->from_number, 'queue_id' => $queue->id, 'agent_phone' => $agent['phone'], 'queued_call_id' => $call->id ))), 'method' => 'POST', 'timeout' => 20, 'statusCallback' => home_url('/wp-json/twilio-webhook/v1/agent-call-status'), 'statusCallbackEvent' => array('answered', 'completed', 'busy', 'no-answer'), 'statusCallbackMethod' => 'POST' ); error_log('TWP Queue Bridge: Creating agent call with data: ' . json_encode($agent_call_data)); $agent_call_response = $twilio->create_call($agent_call_data); if ($agent_call_response['success']) { // Update call status to indicate agent is being contacted global $wpdb; $table_name = $wpdb->prefix . 'twp_queued_calls'; $updated = $wpdb->update( $table_name, array( 'status' => 'connecting', 'agent_phone' => $agent['phone'], 'agent_call_sid' => $agent_call_response['data']['sid'] ), array('call_sid' => $call->call_sid), array('%s', '%s', '%s'), array('%s') ); if ($updated) { error_log('TWP Queue Bridge: Updated call status to connecting'); return true; } else { error_log('TWP Queue Bridge: Failed to update call status'); } } else { error_log('TWP Queue Bridge: Failed to create agent call: ' . ($agent_call_response['error'] ?? 'Unknown error')); } } catch (Exception $e) { error_log('TWP Queue Bridge: Exception bridging call: ' . $e->getMessage()); } return false; } /** * Update queue positions */ private function update_queue_positions($queue_id) { global $wpdb; $table_name = $wpdb->prefix . 'twp_queued_calls'; $waiting_calls = $wpdb->get_results($wpdb->prepare( "SELECT * FROM $table_name WHERE queue_id = %d AND status = 'waiting' ORDER BY position ASC", $queue_id )); foreach ($waiting_calls as $index => $call) { $position = $index + 1; // Update position if changed if ($call->position != $position) { $wpdb->update( $table_name, array('position' => $position), array('id' => $call->id), array('%d'), array('%d') ); } // Announce position every 30 seconds $last_announcement = get_transient('twp_queue_announce_' . $call->call_sid); if (!$last_announcement) { $this->announce_position($call, $position); set_transient('twp_queue_announce_' . $call->call_sid, true, 30); } } } /** * Announce queue position */ private function announce_position($call, $position) { $twilio = new TWP_Twilio_API(); $elevenlabs = new TWP_ElevenLabs_API(); $message = "You are currently number $position in the queue. Please hold and an agent will be with you shortly."; // Generate TTS audio $audio_result = $elevenlabs->text_to_speech($message); if ($audio_result['success']) { // Create TwiML with audio $twiml = new SimpleXMLElement(''); $play = $twiml->addChild('Play', $audio_result['file_url']); $play->addAttribute('loop', '0'); // Add wait music $queue = self::get_queue($call->queue_id); if ($queue && $queue->wait_music_url) { $play_music = $twiml->addChild('Play', $queue->wait_music_url); $play_music->addAttribute('loop', '0'); } $twilio->update_call($call->call_sid, array( 'twiml' => $twiml->asXML() )); } } /** * Reorder queue positions */ private static function reorder_queue($queue_id) { global $wpdb; $table_name = $wpdb->prefix . 'twp_queued_calls'; $waiting_calls = $wpdb->get_results($wpdb->prepare( "SELECT id FROM $table_name WHERE queue_id = %d AND status = 'waiting' ORDER BY position ASC", $queue_id )); foreach ($waiting_calls as $index => $call) { $wpdb->update( $table_name, array('position' => $index + 1), array('id' => $call->id), array('%d'), array('%d') ); } } /** * Create queue */ public static function create_queue($data) { global $wpdb; $table_name = $wpdb->prefix . 'twp_call_queues'; $insert_data = array( 'queue_name' => sanitize_text_field($data['queue_name']), 'notification_number' => !empty($data['notification_number']) ? sanitize_text_field($data['notification_number']) : '', 'agent_group_id' => !empty($data['agent_group_id']) ? intval($data['agent_group_id']) : null, 'max_size' => intval($data['max_size']), 'wait_music_url' => esc_url_raw($data['wait_music_url']), 'tts_message' => sanitize_textarea_field($data['tts_message']), 'timeout_seconds' => intval($data['timeout_seconds']) ); $insert_format = array('%s', '%s'); if ($insert_data['agent_group_id'] === null) { $insert_format[] = null; } else { $insert_format[] = '%d'; } $insert_format = array_merge($insert_format, array('%d', '%s', '%s', '%d')); return $wpdb->insert($table_name, $insert_data, $insert_format); } /** * Update queue */ public static function update_queue($queue_id, $data) { global $wpdb; $table_name = $wpdb->prefix . 'twp_call_queues'; $update_data = array( 'queue_name' => sanitize_text_field($data['queue_name']), 'notification_number' => !empty($data['notification_number']) ? sanitize_text_field($data['notification_number']) : '', 'agent_group_id' => !empty($data['agent_group_id']) ? intval($data['agent_group_id']) : null, 'max_size' => intval($data['max_size']), 'wait_music_url' => esc_url_raw($data['wait_music_url']), 'tts_message' => sanitize_textarea_field($data['tts_message']), 'timeout_seconds' => intval($data['timeout_seconds']) ); $update_format = array('%s', '%s'); if ($update_data['agent_group_id'] === null) { $update_format[] = null; } else { $update_format[] = '%d'; } $update_format = array_merge($update_format, array('%d', '%s', '%s', '%d')); return $wpdb->update( $table_name, $update_data, array('id' => intval($queue_id)), $update_format, array('%d') ); } /** * Get queue */ public static function get_queue($queue_id) { global $wpdb; $table_name = $wpdb->prefix . 'twp_call_queues'; return $wpdb->get_row($wpdb->prepare( "SELECT * FROM $table_name WHERE id = %d", $queue_id )); } /** * Get all queues */ public static function get_all_queues() { global $wpdb; $table_name = $wpdb->prefix . 'twp_call_queues'; return $wpdb->get_results("SELECT * FROM $table_name ORDER BY queue_name ASC"); } /** * Delete queue */ public static function delete_queue($queue_id) { global $wpdb; $queue_table = $wpdb->prefix . 'twp_call_queues'; $calls_table = $wpdb->prefix . 'twp_queued_calls'; // First delete all queued calls for this queue $wpdb->delete($calls_table, array('queue_id' => $queue_id), array('%d')); // Then delete the queue itself return $wpdb->delete($queue_table, array('id' => $queue_id), array('%d')); } /** * Get queue status */ public static function get_queue_status() { global $wpdb; $queue_table = $wpdb->prefix . 'twp_call_queues'; $calls_table = $wpdb->prefix . 'twp_queued_calls'; $queues = $wpdb->get_results("SELECT * FROM $queue_table"); $status = array(); foreach ($queues as $queue) { $waiting_count = $wpdb->get_var($wpdb->prepare( "SELECT COUNT(*) FROM $calls_table WHERE queue_id = %d AND status = 'waiting'", $queue->id )); $status[] = array( 'queue_id' => $queue->id, 'queue_name' => $queue->queue_name, 'waiting_calls' => $waiting_count, 'max_size' => $queue->max_size, 'available_slots' => $queue->max_size - $waiting_count ); } return $status; } /** * Notify agents via SMS when a call enters the queue */ private static function notify_agents_for_queue($queue_id, $caller_number) { global $wpdb; // Get queue information including assigned agent group and phone number $queue_table = $wpdb->prefix . 'twp_call_queues'; $queue = $wpdb->get_row($wpdb->prepare( "SELECT * FROM $queue_table WHERE id = %d", $queue_id )); if (!$queue || !$queue->agent_group_id) { error_log("TWP: No agent group assigned to queue {$queue_id}, skipping SMS notifications"); return; } // Send Discord/Slack notification for incoming call require_once dirname(__FILE__) . '/class-twp-notifications.php'; TWP_Notifications::send_call_notification('incoming_call', array( 'type' => 'incoming_call', 'caller' => $caller_number, 'queue' => $queue->queue_name, 'queue_id' => $queue_id )); // Get members of the assigned agent group require_once dirname(__FILE__) . '/class-twp-agent-groups.php'; $members = TWP_Agent_Groups::get_group_members($queue->agent_group_id); if (empty($members)) { error_log("TWP: No members found in agent group {$queue->agent_group_id} for queue {$queue_id}"); return; } $twilio = new TWP_Twilio_API(); // Use the queue's notification number as the from number, or fall back to default $from_number = !empty($queue->notification_number) ? $queue->notification_number : TWP_Twilio_API::get_sms_from_number(); if (empty($from_number)) { error_log("TWP: No SMS from number available for queue notifications"); return; } $message = "Call waiting in queue '{$queue->queue_name}' from {$caller_number}. Text '1' to this number to receive the next available call."; foreach ($members as $member) { $agent_phone = get_user_meta($member->user_id, 'twp_phone_number', true); if (!empty($agent_phone)) { // Send SMS notification using the queue's phone number $twilio->send_sms($agent_phone, $message, $from_number); // Log the notification error_log("TWP: Queue SMS notification sent to agent {$member->user_id} at {$agent_phone} from {$from_number} for queue {$queue_id}"); } } } }