2025-08-06 15:25:47 -07:00
|
|
|
<?php
|
|
|
|
/**
|
|
|
|
* Callback management class for queue callbacks and outbound calling
|
|
|
|
*/
|
|
|
|
class TWP_Callback_Manager {
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Request a callback from queue
|
|
|
|
*/
|
|
|
|
public static function request_callback($phone_number, $queue_id = null, $call_sid = null) {
|
|
|
|
global $wpdb;
|
|
|
|
$table_name = $wpdb->prefix . 'twp_callbacks';
|
|
|
|
|
|
|
|
$result = $wpdb->insert(
|
|
|
|
$table_name,
|
|
|
|
array(
|
|
|
|
'phone_number' => sanitize_text_field($phone_number),
|
|
|
|
'queue_id' => $queue_id ? intval($queue_id) : null,
|
|
|
|
'original_call_sid' => $call_sid,
|
|
|
|
'status' => 'pending'
|
|
|
|
),
|
|
|
|
array('%s', '%d', '%s', '%s')
|
|
|
|
);
|
|
|
|
|
|
|
|
if ($result !== false) {
|
|
|
|
// Send confirmation SMS if configured
|
|
|
|
$sms_number = get_option('twp_sms_notification_number');
|
|
|
|
if ($sms_number) {
|
|
|
|
$message = "Callback requested for " . $phone_number . ". We'll call you back shortly.";
|
|
|
|
self::send_sms($phone_number, $message);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $wpdb->insert_id;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Process pending callbacks
|
|
|
|
*/
|
|
|
|
public static function process_callbacks() {
|
|
|
|
global $wpdb;
|
|
|
|
$table_name = $wpdb->prefix . 'twp_callbacks';
|
|
|
|
|
|
|
|
// Get pending callbacks older than 2 minutes (to avoid immediate callback)
|
|
|
|
$callbacks = $wpdb->get_results("
|
|
|
|
SELECT * FROM $table_name
|
|
|
|
WHERE status = 'pending'
|
|
|
|
AND requested_at <= DATE_SUB(NOW(), INTERVAL 2 MINUTE)
|
|
|
|
AND attempts < 3
|
|
|
|
ORDER BY requested_at ASC
|
|
|
|
LIMIT 10
|
|
|
|
");
|
|
|
|
|
|
|
|
foreach ($callbacks as $callback) {
|
|
|
|
self::initiate_callback($callback);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Initiate a callback
|
|
|
|
*/
|
|
|
|
private static function initiate_callback($callback) {
|
|
|
|
global $wpdb;
|
|
|
|
$table_name = $wpdb->prefix . 'twp_callbacks';
|
|
|
|
|
|
|
|
// Find an available agent
|
|
|
|
$available_agent = TWP_Agent_Manager::get_available_agents();
|
|
|
|
|
|
|
|
if (empty($available_agent)) {
|
|
|
|
// No agents available, try again later
|
|
|
|
$wpdb->update(
|
|
|
|
$table_name,
|
|
|
|
array('last_attempt' => current_time('mysql')),
|
|
|
|
array('id' => $callback->id),
|
|
|
|
array('%s'),
|
|
|
|
array('%d')
|
|
|
|
);
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
$agent = $available_agent[0]; // Get first available agent
|
|
|
|
|
|
|
|
// Create a conference call
|
|
|
|
$twilio = new TWP_Twilio_API();
|
|
|
|
|
|
|
|
// First call the agent
|
|
|
|
$agent_call_result = $twilio->make_call(
|
|
|
|
$agent->phone_number,
|
|
|
|
home_url('/wp-json/twilio-webhook/v1/callback-agent'),
|
|
|
|
array(
|
|
|
|
'callback_id' => $callback->id,
|
|
|
|
'customer_number' => $callback->phone_number
|
|
|
|
)
|
|
|
|
);
|
|
|
|
|
|
|
|
if ($agent_call_result['success']) {
|
|
|
|
// Update callback status
|
|
|
|
$wpdb->update(
|
|
|
|
$table_name,
|
|
|
|
array(
|
|
|
|
'status' => 'calling',
|
|
|
|
'attempts' => $callback->attempts + 1,
|
|
|
|
'last_attempt' => current_time('mysql'),
|
|
|
|
'callback_call_sid' => $agent_call_result['call_sid']
|
|
|
|
),
|
|
|
|
array('id' => $callback->id),
|
|
|
|
array('%s', '%d', '%s', '%s'),
|
|
|
|
array('%d')
|
|
|
|
);
|
|
|
|
|
|
|
|
// Set agent to busy
|
|
|
|
TWP_Agent_Manager::set_agent_status($agent->user_id, 'busy', $agent_call_result['call_sid']);
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handle callback agent answered
|
|
|
|
*/
|
|
|
|
public static function handle_agent_answered($callback_id, $agent_call_sid) {
|
|
|
|
global $wpdb;
|
|
|
|
$callbacks_table = $wpdb->prefix . 'twp_callbacks';
|
|
|
|
|
|
|
|
$callback = $wpdb->get_row($wpdb->prepare(
|
|
|
|
"SELECT * FROM $callbacks_table WHERE id = %d",
|
|
|
|
$callback_id
|
|
|
|
));
|
|
|
|
|
|
|
|
if (!$callback) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Now call the customer and conference them in
|
|
|
|
$twilio = new TWP_Twilio_API();
|
|
|
|
|
|
|
|
$customer_call_result = $twilio->make_call(
|
|
|
|
$callback->phone_number,
|
|
|
|
home_url('/wp-json/twilio-webhook/v1/callback-customer'),
|
|
|
|
array(
|
|
|
|
'agent_call_sid' => $agent_call_sid,
|
|
|
|
'callback_id' => $callback_id
|
|
|
|
)
|
|
|
|
);
|
|
|
|
|
|
|
|
if ($customer_call_result['success']) {
|
|
|
|
// Update callback status
|
|
|
|
$wpdb->update(
|
|
|
|
$callbacks_table,
|
|
|
|
array('status' => 'connecting'),
|
|
|
|
array('id' => $callback_id),
|
|
|
|
array('%s'),
|
|
|
|
array('%d')
|
|
|
|
);
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Complete callback
|
|
|
|
*/
|
|
|
|
public static function complete_callback($callback_id) {
|
|
|
|
global $wpdb;
|
|
|
|
$table_name = $wpdb->prefix . 'twp_callbacks';
|
|
|
|
|
|
|
|
$wpdb->update(
|
|
|
|
$table_name,
|
|
|
|
array(
|
|
|
|
'status' => 'completed',
|
|
|
|
'completed_at' => current_time('mysql')
|
|
|
|
),
|
|
|
|
array('id' => $callback_id),
|
|
|
|
array('%s', '%s'),
|
|
|
|
array('%d')
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Initiate outbound call (click-to-call)
|
|
|
|
*/
|
|
|
|
public static function initiate_outbound_call($to_number, $agent_user_id) {
|
|
|
|
$agent_phone = get_user_meta($agent_user_id, 'twp_phone_number', true);
|
|
|
|
|
|
|
|
if (!$agent_phone) {
|
|
|
|
return array('success' => false, 'error' => 'No phone number configured');
|
|
|
|
}
|
|
|
|
|
|
|
|
$twilio = new TWP_Twilio_API();
|
|
|
|
|
|
|
|
// First call the agent
|
|
|
|
$agent_call_result = $twilio->make_call(
|
|
|
|
$agent_phone,
|
|
|
|
home_url('/wp-json/twilio-webhook/v1/outbound-agent'),
|
|
|
|
array(
|
|
|
|
'target_number' => $to_number,
|
|
|
|
'agent_user_id' => $agent_user_id
|
|
|
|
)
|
|
|
|
);
|
|
|
|
|
|
|
|
if ($agent_call_result['success']) {
|
|
|
|
// Set agent to busy
|
|
|
|
TWP_Agent_Manager::set_agent_status($agent_user_id, 'busy', $agent_call_result['call_sid']);
|
|
|
|
|
|
|
|
// Log the outbound call
|
|
|
|
TWP_Call_Logger::log_call(array(
|
|
|
|
'call_sid' => $agent_call_result['call_sid'],
|
|
|
|
'from_number' => $agent_phone,
|
|
|
|
'to_number' => $to_number,
|
|
|
|
'status' => 'outbound_initiated',
|
|
|
|
'workflow_name' => 'Outbound Call',
|
|
|
|
'actions_taken' => json_encode(array(
|
|
|
|
'agent_id' => $agent_user_id,
|
|
|
|
'agent_name' => get_userdata($agent_user_id)->display_name,
|
|
|
|
'type' => 'click_to_call'
|
|
|
|
))
|
|
|
|
));
|
|
|
|
|
|
|
|
return array('success' => true, 'call_sid' => $agent_call_result['call_sid']);
|
|
|
|
}
|
|
|
|
|
|
|
|
return array('success' => false, 'error' => $agent_call_result['error']);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handle outbound agent answered
|
|
|
|
*/
|
|
|
|
public static function handle_outbound_agent_answered($target_number, $agent_call_sid) {
|
|
|
|
// Create TwiML to call the target number
|
|
|
|
$twiml = new \Twilio\TwiML\VoiceResponse();
|
|
|
|
$twiml->say('Connecting your call...', ['voice' => 'alice']);
|
2025-08-07 15:24:29 -07:00
|
|
|
$twiml->dial($target_number, [
|
|
|
|
'callerId' => get_option('twp_caller_id_number', ''),
|
2025-08-06 15:25:47 -07:00
|
|
|
'timeout' => 30
|
|
|
|
]);
|
|
|
|
|
|
|
|
// If no answer, leave a message
|
|
|
|
$twiml->say('The number you called is not available. Please try again later.', ['voice' => 'alice']);
|
|
|
|
|
|
|
|
return $twiml->asXML();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create callback option TwiML for queue
|
|
|
|
*/
|
|
|
|
public static function create_callback_twiml($queue_id, $caller_number) {
|
|
|
|
$twiml = new \Twilio\TwiML\VoiceResponse();
|
|
|
|
|
|
|
|
$gather = $twiml->gather([
|
|
|
|
'numDigits' => 1,
|
|
|
|
'timeout' => 10,
|
2025-08-07 15:24:29 -07:00
|
|
|
'action' => home_url('/wp-json/twilio-webhook/v1/callback-choice?' . http_build_query([
|
|
|
|
'queue_id' => $queue_id,
|
|
|
|
'phone_number' => $caller_number
|
|
|
|
])),
|
2025-08-06 15:25:47 -07:00
|
|
|
'method' => 'POST'
|
|
|
|
]);
|
|
|
|
|
|
|
|
$gather->say(
|
|
|
|
'You are currently in the queue. Press 1 to wait on the line, or press 2 to request a callback.',
|
|
|
|
['voice' => 'alice']
|
|
|
|
);
|
|
|
|
|
|
|
|
// Default to callback if no input
|
|
|
|
$twiml->say('No input received. Requesting callback for you.', ['voice' => 'alice']);
|
|
|
|
$twiml->redirect(home_url('/wp-json/twilio-webhook/v1/request-callback?' . http_build_query([
|
|
|
|
'queue_id' => $queue_id,
|
|
|
|
'phone_number' => $caller_number
|
|
|
|
])));
|
|
|
|
|
|
|
|
return $twiml->asXML();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Send SMS notification
|
|
|
|
*/
|
|
|
|
private static function send_sms($to_number, $message) {
|
|
|
|
$twilio = new TWP_Twilio_API();
|
|
|
|
return $twilio->send_sms($to_number, $message);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get callback statistics
|
|
|
|
*/
|
|
|
|
public static function get_callback_stats($days = 7) {
|
|
|
|
global $wpdb;
|
|
|
|
$table_name = $wpdb->prefix . 'twp_callbacks';
|
|
|
|
|
|
|
|
$since_date = date('Y-m-d H:i:s', strtotime("-$days days"));
|
|
|
|
|
|
|
|
$stats = array(
|
|
|
|
'total_requests' => $wpdb->get_var($wpdb->prepare(
|
|
|
|
"SELECT COUNT(*) FROM $table_name WHERE requested_at >= %s",
|
|
|
|
$since_date
|
|
|
|
)),
|
|
|
|
'completed' => $wpdb->get_var($wpdb->prepare(
|
|
|
|
"SELECT COUNT(*) FROM $table_name WHERE requested_at >= %s AND status = 'completed'",
|
|
|
|
$since_date
|
|
|
|
)),
|
|
|
|
'pending' => $wpdb->get_var("SELECT COUNT(*) FROM $table_name WHERE status = 'pending'"),
|
|
|
|
'avg_completion_time' => $wpdb->get_var($wpdb->prepare(
|
|
|
|
"SELECT AVG(TIMESTAMPDIFF(MINUTE, requested_at, completed_at))
|
|
|
|
FROM $table_name
|
|
|
|
WHERE requested_at >= %s AND status = 'completed'",
|
|
|
|
$since_date
|
|
|
|
))
|
|
|
|
);
|
|
|
|
|
|
|
|
$stats['success_rate'] = $stats['total_requests'] > 0 ?
|
|
|
|
round(($stats['completed'] / $stats['total_requests']) * 100, 1) : 0;
|
|
|
|
|
|
|
|
return $stats;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get pending callbacks for admin
|
|
|
|
*/
|
|
|
|
public static function get_pending_callbacks() {
|
|
|
|
global $wpdb;
|
|
|
|
$table_name = $wpdb->prefix . 'twp_callbacks';
|
|
|
|
$queues_table = $wpdb->prefix . 'twp_call_queues';
|
|
|
|
|
|
|
|
return $wpdb->get_results("
|
|
|
|
SELECT
|
|
|
|
c.*,
|
|
|
|
q.queue_name,
|
|
|
|
TIMESTAMPDIFF(MINUTE, c.requested_at, NOW()) as wait_minutes
|
|
|
|
FROM $table_name c
|
|
|
|
LEFT JOIN $queues_table q ON c.queue_id = q.id
|
|
|
|
WHERE c.status IN ('pending', 'calling', 'connecting')
|
|
|
|
ORDER BY c.requested_at ASC
|
|
|
|
");
|
|
|
|
}
|
|
|
|
}
|