Major Fixes: - Fixed extension transfers going directly to voicemail for available agents - Resolved browser phone call disconnections during transfers - Fixed voicemail transcription placeholder text issue - Added Firefox compatibility with automatic media permissions Extension Transfer Improvements: - Changed from active client dialing to proper queue-based system - Fixed client name generation consistency (user_login vs display_name) - Added 2-minute timeout with automatic voicemail fallback - Enhanced agent availability detection for browser phone users Browser Phone Enhancements: - Added automatic microphone/speaker permission requests - Improved Firefox compatibility with explicit getUserMedia calls - Fixed client naming consistency across capability tokens and call acceptance - Added comprehensive error handling for permission denials Database & System Updates: - Added auto_busy_at column for automatic agent status reversion - Implemented 1-minute auto-revert system for busy agents with cron job - Updated database version to 1.6.2 for automatic migration - Fixed voicemail user_id association for extension voicemails Call Statistics & Logging: - Fixed browser phone calls not appearing in agent statistics - Enhanced call logging with proper agent_id association in JSON format - Improved customer number detection for complex call topologies - Added comprehensive debugging for call leg detection Voicemail & Transcription: - Replaced placeholder transcription with real Twilio API integration - Added manual transcription request capability for existing voicemails - Enhanced voicemail callback handling with user_id support - Fixed transcription webhook processing for extension voicemails Technical Improvements: - Standardized client name generation across all components - Added ElevenLabs TTS integration to agent connection messages - Enhanced error handling and logging throughout transfer system - Fixed TwiML generation syntax errors in dial() methods 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
344 lines
11 KiB
PHP
344 lines
11 KiB
PHP
<?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'], true);
|
|
|
|
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'], true);
|
|
|
|
// 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']);
|
|
$twiml->dial($target_number, [
|
|
'callerId' => get_option('twp_caller_id_number', ''),
|
|
'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,
|
|
'action' => home_url('/wp-json/twilio-webhook/v1/callback-choice?' . http_build_query([
|
|
'queue_id' => $queue_id,
|
|
'phone_number' => $caller_number
|
|
])),
|
|
'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();
|
|
|
|
// Get SMS from number with proper priority (no workflow context here)
|
|
$from_number = TWP_Twilio_API::get_sms_from_number();
|
|
|
|
return $twilio->send_sms($to_number, $message, $from_number);
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
");
|
|
}
|
|
} |