testing progress
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -135,6 +135,18 @@ class TWP_Core {
|
|||||||
// Phone number maintenance
|
// Phone number maintenance
|
||||||
$this->loader->add_action('wp_ajax_twp_update_phone_status_callbacks', $plugin_admin, 'ajax_update_phone_status_callbacks');
|
$this->loader->add_action('wp_ajax_twp_update_phone_status_callbacks', $plugin_admin, 'ajax_update_phone_status_callbacks');
|
||||||
$this->loader->add_action('wp_ajax_twp_toggle_number_status_callback', $plugin_admin, 'ajax_toggle_number_status_callback');
|
$this->loader->add_action('wp_ajax_twp_toggle_number_status_callback', $plugin_admin, 'ajax_toggle_number_status_callback');
|
||||||
|
|
||||||
|
// Browser phone
|
||||||
|
$this->loader->add_action('wp_ajax_twp_generate_capability_token', $plugin_admin, 'ajax_generate_capability_token');
|
||||||
|
$this->loader->add_action('wp_ajax_twp_save_call_mode', $plugin_admin, 'ajax_save_call_mode');
|
||||||
|
$this->loader->add_action('wp_ajax_twp_auto_configure_twiml_app', $plugin_admin, 'ajax_auto_configure_twiml_app');
|
||||||
|
$this->loader->add_action('wp_ajax_twp_configure_phone_numbers_only', $plugin_admin, 'ajax_configure_phone_numbers_only');
|
||||||
|
|
||||||
|
// SMS management
|
||||||
|
$this->loader->add_action('wp_ajax_twp_delete_sms', $plugin_admin, 'ajax_delete_sms');
|
||||||
|
$this->loader->add_action('wp_ajax_twp_delete_conversation', $plugin_admin, 'ajax_delete_conversation');
|
||||||
|
$this->loader->add_action('wp_ajax_twp_get_conversation', $plugin_admin, 'ajax_get_conversation');
|
||||||
|
$this->loader->add_action('wp_ajax_twp_send_sms_reply', $plugin_admin, 'ajax_send_sms_reply');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -654,4 +654,55 @@ class TWP_Twilio_API {
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate capability token for Browser Phone
|
||||||
|
*/
|
||||||
|
public function generate_capability_token($client_name = null) {
|
||||||
|
$account_sid = get_option('twp_twilio_account_sid');
|
||||||
|
$auth_token = get_option('twp_twilio_auth_token');
|
||||||
|
$twiml_app_sid = get_option('twp_twiml_app_sid');
|
||||||
|
|
||||||
|
if (empty($account_sid) || empty($auth_token)) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'Twilio credentials not configured'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($twiml_app_sid)) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'TwiML App SID not configured. Please set up a TwiML App in your Twilio Console.'
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Create client name if not provided
|
||||||
|
if (!$client_name) {
|
||||||
|
$current_user = wp_get_current_user();
|
||||||
|
$client_name = 'agent_' . $current_user->ID . '_' . sanitize_title($current_user->display_name);
|
||||||
|
}
|
||||||
|
|
||||||
|
$capability = new \Twilio\Jwt\ClientToken($account_sid, $auth_token);
|
||||||
|
$capability->allowClientOutgoing($twiml_app_sid);
|
||||||
|
$capability->allowClientIncoming($client_name);
|
||||||
|
|
||||||
|
$token = $capability->generateToken(3600); // Valid for 1 hour
|
||||||
|
|
||||||
|
return [
|
||||||
|
'success' => true,
|
||||||
|
'data' => [
|
||||||
|
'token' => $token,
|
||||||
|
'client_name' => $client_name,
|
||||||
|
'expires_in' => 3600
|
||||||
|
]
|
||||||
|
];
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
return [
|
||||||
|
'success' => false,
|
||||||
|
'error' => 'Failed to generate capability token: ' . $e->getMessage()
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@@ -86,6 +86,34 @@ class TWP_Webhooks {
|
|||||||
'permission_callback' => '__return_true'
|
'permission_callback' => '__return_true'
|
||||||
));
|
));
|
||||||
|
|
||||||
|
// Browser phone voice webhook
|
||||||
|
register_rest_route('twilio-webhook/v1', '/browser-voice', array(
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => array($this, 'handle_browser_voice'),
|
||||||
|
'permission_callback' => '__return_true'
|
||||||
|
));
|
||||||
|
|
||||||
|
// Browser phone fallback webhook
|
||||||
|
register_rest_route('twilio-webhook/v1', '/browser-fallback', array(
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => array($this, 'handle_browser_fallback'),
|
||||||
|
'permission_callback' => '__return_true'
|
||||||
|
));
|
||||||
|
|
||||||
|
// Smart routing webhook (checks user preference)
|
||||||
|
register_rest_route('twilio-webhook/v1', '/smart-routing', array(
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => array($this, 'handle_smart_routing'),
|
||||||
|
'permission_callback' => '__return_true'
|
||||||
|
));
|
||||||
|
|
||||||
|
// Smart routing fallback webhook
|
||||||
|
register_rest_route('twilio-webhook/v1', '/smart-routing-fallback', array(
|
||||||
|
'methods' => 'POST',
|
||||||
|
'callback' => array($this, 'handle_smart_routing_fallback'),
|
||||||
|
'permission_callback' => '__return_true'
|
||||||
|
));
|
||||||
|
|
||||||
// Agent screening webhook (screen agent before connecting)
|
// Agent screening webhook (screen agent before connecting)
|
||||||
register_rest_route('twilio-webhook/v1', '/agent-screen', array(
|
register_rest_route('twilio-webhook/v1', '/agent-screen', array(
|
||||||
'methods' => 'POST',
|
'methods' => 'POST',
|
||||||
@@ -200,6 +228,341 @@ class TWP_Webhooks {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle browser phone voice webhook
|
||||||
|
*/
|
||||||
|
public function handle_browser_voice($request) {
|
||||||
|
$params = $request->get_params();
|
||||||
|
|
||||||
|
$call_data = array(
|
||||||
|
'CallSid' => isset($params['CallSid']) ? $params['CallSid'] : '',
|
||||||
|
'From' => isset($params['From']) ? $params['From'] : '',
|
||||||
|
'To' => isset($params['To']) ? $params['To'] : '',
|
||||||
|
'CallStatus' => isset($params['CallStatus']) ? $params['CallStatus'] : ''
|
||||||
|
);
|
||||||
|
|
||||||
|
// Log the browser call
|
||||||
|
TWP_Call_Logger::log_call(array(
|
||||||
|
'call_sid' => $call_data['CallSid'],
|
||||||
|
'from_number' => $call_data['From'],
|
||||||
|
'to_number' => $call_data['To'],
|
||||||
|
'status' => 'browser_call_initiated',
|
||||||
|
'actions_taken' => 'Browser phone call initiated'
|
||||||
|
));
|
||||||
|
|
||||||
|
// For outbound calls from browser, handle caller ID properly
|
||||||
|
$twiml = '<?xml version="1.0" encoding="UTF-8"?>';
|
||||||
|
$twiml .= '<Response>';
|
||||||
|
|
||||||
|
if (isset($params['To']) && !empty($params['To'])) {
|
||||||
|
$to_number = $params['To'];
|
||||||
|
$from_number = isset($params['From']) ? $params['From'] : '';
|
||||||
|
|
||||||
|
// If it's an outgoing call to a phone number
|
||||||
|
if (strpos($to_number, 'client:') !== 0) {
|
||||||
|
$twiml .= '<Dial timeout="30"';
|
||||||
|
|
||||||
|
// Add caller ID if provided
|
||||||
|
if (!empty($from_number) && strpos($from_number, 'client:') !== 0) {
|
||||||
|
$twiml .= ' callerId="' . htmlspecialchars($from_number) . '"';
|
||||||
|
}
|
||||||
|
|
||||||
|
$twiml .= '>';
|
||||||
|
$twiml .= '<Number>' . htmlspecialchars($to_number) . '</Number>';
|
||||||
|
$twiml .= '</Dial>';
|
||||||
|
} else {
|
||||||
|
// Incoming call to browser client
|
||||||
|
$twiml .= '<Dial timeout="30">';
|
||||||
|
$twiml .= '<Client>' . htmlspecialchars(str_replace('client:', '', $to_number)) . '</Client>';
|
||||||
|
$twiml .= '</Dial>';
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$twiml .= '<Say voice="alice">No destination number provided.</Say>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$twiml .= '</Response>';
|
||||||
|
|
||||||
|
return $this->send_twiml_response($twiml);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle browser phone fallback when no browser clients answer
|
||||||
|
*/
|
||||||
|
public function handle_browser_fallback($request) {
|
||||||
|
$params = $request->get_params();
|
||||||
|
|
||||||
|
$call_data = array(
|
||||||
|
'CallSid' => isset($params['CallSid']) ? $params['CallSid'] : '',
|
||||||
|
'From' => isset($params['From']) ? $params['From'] : '',
|
||||||
|
'To' => isset($params['To']) ? $params['To'] : '',
|
||||||
|
'DialCallStatus' => isset($params['DialCallStatus']) ? $params['DialCallStatus'] : ''
|
||||||
|
);
|
||||||
|
|
||||||
|
error_log('TWP Browser Fallback: No browser clients answered, status: ' . $call_data['DialCallStatus']);
|
||||||
|
|
||||||
|
// Log the fallback
|
||||||
|
TWP_Call_Logger::log_call(array(
|
||||||
|
'call_sid' => $call_data['CallSid'],
|
||||||
|
'from_number' => $call_data['From'],
|
||||||
|
'to_number' => $call_data['To'],
|
||||||
|
'status' => 'browser_fallback',
|
||||||
|
'actions_taken' => 'Browser clients did not answer, using fallback'
|
||||||
|
));
|
||||||
|
|
||||||
|
$twiml = '<?xml version="1.0" encoding="UTF-8"?>';
|
||||||
|
$twiml .= '<Response>';
|
||||||
|
|
||||||
|
// Fallback options based on call status
|
||||||
|
if ($call_data['DialCallStatus'] === 'no-answer' || $call_data['DialCallStatus'] === 'busy') {
|
||||||
|
// Try SMS notification to agents as fallback
|
||||||
|
$this->send_agent_notification_sms($call_data['From'], $call_data['To']);
|
||||||
|
|
||||||
|
$twiml .= '<Say voice="alice">All our agents are currently busy. We have been notified of your call and will get back to you shortly.</Say>';
|
||||||
|
$twiml .= '<Say voice="alice">Please stay on the line for voicemail, or hang up and we will call you back.</Say>';
|
||||||
|
|
||||||
|
// Redirect to voicemail
|
||||||
|
$voicemail_url = home_url('/wp-json/twilio-webhook/v1/voicemail-callback');
|
||||||
|
$twiml .= '<Redirect>' . $voicemail_url . '</Redirect>';
|
||||||
|
} else {
|
||||||
|
// Other statuses - generic message
|
||||||
|
$twiml .= '<Say voice="alice">We apologize, but we are unable to connect your call at this time. Please try again later.</Say>';
|
||||||
|
$twiml .= '<Hangup/>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$twiml .= '</Response>';
|
||||||
|
|
||||||
|
return $this->send_twiml_response($twiml);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send SMS notification to agents about missed browser call
|
||||||
|
*/
|
||||||
|
private function send_agent_notification_sms($customer_number, $twilio_number) {
|
||||||
|
// Get agents with phone numbers
|
||||||
|
$agents = get_users(array(
|
||||||
|
'meta_key' => 'twp_phone_number',
|
||||||
|
'meta_compare' => 'EXISTS'
|
||||||
|
));
|
||||||
|
|
||||||
|
$message = "Missed call from {$customer_number}. Browser phone did not answer. Please call back or check voicemail.";
|
||||||
|
|
||||||
|
foreach ($agents as $agent) {
|
||||||
|
$agent_phone = get_user_meta($agent->ID, 'twp_phone_number', true);
|
||||||
|
if (!empty($agent_phone)) {
|
||||||
|
$twilio_api = new TWP_Twilio_API();
|
||||||
|
$twilio_api->send_sms($agent_phone, $message, $twilio_number);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle smart routing based on user preferences
|
||||||
|
*/
|
||||||
|
public function handle_smart_routing($request) {
|
||||||
|
$params = $request->get_params();
|
||||||
|
|
||||||
|
$call_data = array(
|
||||||
|
'CallSid' => isset($params['CallSid']) ? $params['CallSid'] : '',
|
||||||
|
'From' => isset($params['From']) ? $params['From'] : '',
|
||||||
|
'To' => isset($params['To']) ? $params['To'] : '',
|
||||||
|
'CallStatus' => isset($params['CallStatus']) ? $params['CallStatus'] : ''
|
||||||
|
);
|
||||||
|
|
||||||
|
// Log the incoming call
|
||||||
|
TWP_Call_Logger::log_call(array(
|
||||||
|
'call_sid' => $call_data['CallSid'],
|
||||||
|
'from_number' => $call_data['From'],
|
||||||
|
'to_number' => $call_data['To'],
|
||||||
|
'status' => 'smart_routing',
|
||||||
|
'actions_taken' => 'Smart routing - checking workflows first, then user preferences'
|
||||||
|
));
|
||||||
|
|
||||||
|
// FIRST: Check if there's a workflow assigned to this phone number
|
||||||
|
$workflow = TWP_Workflow::get_workflow_by_phone_number($call_data['To']);
|
||||||
|
if ($workflow) {
|
||||||
|
error_log('TWP Smart Routing: Found workflow for ' . $call_data['To'] . ', executing workflow ID: ' . $workflow->id);
|
||||||
|
|
||||||
|
// Execute the workflow instead of direct routing
|
||||||
|
$workflow_twiml = TWP_Workflow::execute_workflow($workflow->id, $call_data);
|
||||||
|
if ($workflow_twiml) {
|
||||||
|
header('Content-Type: application/xml');
|
||||||
|
echo $workflow_twiml;
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FALLBACK: If no workflow found or workflow failed, use direct agent routing
|
||||||
|
error_log('TWP Smart Routing: No workflow found for ' . $call_data['To'] . ', falling back to direct agent routing');
|
||||||
|
|
||||||
|
// Check for any active agents and their preferences
|
||||||
|
$agents = get_users(array(
|
||||||
|
'meta_key' => 'twp_phone_number',
|
||||||
|
'meta_compare' => 'EXISTS'
|
||||||
|
));
|
||||||
|
|
||||||
|
$browser_agents = [];
|
||||||
|
$cell_agents = [];
|
||||||
|
|
||||||
|
foreach ($agents as $agent) {
|
||||||
|
$call_mode = get_user_meta($agent->ID, 'twp_call_mode', true);
|
||||||
|
$agent_phone = get_user_meta($agent->ID, 'twp_phone_number', true);
|
||||||
|
|
||||||
|
if ($call_mode === 'browser') {
|
||||||
|
$client_name = 'agent_' . $agent->ID . '_' . sanitize_title($agent->display_name);
|
||||||
|
$browser_agents[] = $client_name;
|
||||||
|
} elseif (!empty($agent_phone)) {
|
||||||
|
$cell_agents[] = $agent_phone;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$twiml = '<?xml version="1.0" encoding="UTF-8"?>';
|
||||||
|
$twiml .= '<Response>';
|
||||||
|
$twiml .= '<Say voice="alice">Please hold while we connect you to an agent.</Say>';
|
||||||
|
|
||||||
|
// Try browser agents first, then cell agents
|
||||||
|
if (!empty($browser_agents)) {
|
||||||
|
$twiml .= '<Dial timeout="20" action="' . home_url('/wp-json/twilio-webhook/v1/smart-routing-fallback') . '" method="POST">';
|
||||||
|
foreach ($browser_agents as $client_name) {
|
||||||
|
$twiml .= '<Client>' . htmlspecialchars($client_name) . '</Client>';
|
||||||
|
}
|
||||||
|
$twiml .= '</Dial>';
|
||||||
|
} elseif (!empty($cell_agents)) {
|
||||||
|
// No browser agents, try cell phones
|
||||||
|
$twiml .= '<Dial timeout="20" action="' . home_url('/wp-json/twilio-webhook/v1/smart-routing-fallback') . '" method="POST">';
|
||||||
|
foreach ($cell_agents as $cell_phone) {
|
||||||
|
$twiml .= '<Number>' . htmlspecialchars($cell_phone) . '</Number>';
|
||||||
|
}
|
||||||
|
$twiml .= '</Dial>';
|
||||||
|
} else {
|
||||||
|
// No agents available
|
||||||
|
$twiml .= '<Say voice="alice">All agents are currently unavailable. Please leave a voicemail.</Say>';
|
||||||
|
$twiml .= '<Redirect>' . home_url('/wp-json/twilio-webhook/v1/voicemail-callback') . '</Redirect>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$twiml .= '</Response>';
|
||||||
|
|
||||||
|
return $this->send_twiml_response($twiml);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle smart routing fallback when initial routing fails
|
||||||
|
*/
|
||||||
|
public function handle_smart_routing_fallback($request) {
|
||||||
|
$params = $request->get_params();
|
||||||
|
|
||||||
|
$call_data = array(
|
||||||
|
'CallSid' => isset($params['CallSid']) ? $params['CallSid'] : '',
|
||||||
|
'From' => isset($params['From']) ? $params['From'] : '',
|
||||||
|
'To' => isset($params['To']) ? $params['To'] : '',
|
||||||
|
'DialCallStatus' => isset($params['DialCallStatus']) ? $params['DialCallStatus'] : ''
|
||||||
|
);
|
||||||
|
|
||||||
|
error_log('TWP Smart Routing Fallback: Initial routing failed, status: ' . $call_data['DialCallStatus']);
|
||||||
|
|
||||||
|
// Log the fallback
|
||||||
|
TWP_Call_Logger::log_call(array(
|
||||||
|
'call_sid' => $call_data['CallSid'],
|
||||||
|
'from_number' => $call_data['From'],
|
||||||
|
'to_number' => $call_data['To'],
|
||||||
|
'status' => 'routing_fallback',
|
||||||
|
'actions_taken' => 'Smart routing failed, trying alternative methods'
|
||||||
|
));
|
||||||
|
|
||||||
|
$twiml = '<?xml version="1.0" encoding="UTF-8"?>';
|
||||||
|
$twiml .= '<Response>';
|
||||||
|
|
||||||
|
// Get agents and their preferences for fallback routing
|
||||||
|
$agents = get_users(array(
|
||||||
|
'meta_key' => 'twp_phone_number',
|
||||||
|
'meta_compare' => 'EXISTS'
|
||||||
|
));
|
||||||
|
|
||||||
|
$browser_agents = [];
|
||||||
|
$cell_agents = [];
|
||||||
|
|
||||||
|
foreach ($agents as $agent) {
|
||||||
|
$call_mode = get_user_meta($agent->ID, 'twp_call_mode', true);
|
||||||
|
$agent_phone = get_user_meta($agent->ID, 'twp_phone_number', true);
|
||||||
|
|
||||||
|
if ($call_mode === 'browser') {
|
||||||
|
$client_name = 'agent_' . $agent->ID . '_' . sanitize_title($agent->display_name);
|
||||||
|
$browser_agents[] = $client_name;
|
||||||
|
} elseif (!empty($agent_phone)) {
|
||||||
|
$cell_agents[] = $agent_phone;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback strategy based on initial failure
|
||||||
|
if ($call_data['DialCallStatus'] === 'no-answer' || $call_data['DialCallStatus'] === 'busy') {
|
||||||
|
// If browsers didn't answer, try cell phones; if cells didn't answer, try queue or voicemail
|
||||||
|
if (!empty($cell_agents) && !empty($browser_agents)) {
|
||||||
|
// We tried browsers first, now try cell phones
|
||||||
|
$twiml .= '<Say voice="alice">Trying to connect you to another agent.</Say>';
|
||||||
|
$twiml .= '<Dial timeout="20">';
|
||||||
|
foreach ($cell_agents as $cell_phone) {
|
||||||
|
$twiml .= '<Number>' . htmlspecialchars($cell_phone) . '</Number>';
|
||||||
|
}
|
||||||
|
$twiml .= '</Dial>';
|
||||||
|
|
||||||
|
// If this also fails, fall through to final fallback below
|
||||||
|
$twiml .= '<Say voice="alice">All agents are currently busy.</Say>';
|
||||||
|
} else {
|
||||||
|
// No alternative agents available - go to final fallback
|
||||||
|
$twiml .= '<Say voice="alice">All agents are currently busy.</Say>';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send SMS notification to agents about missed call
|
||||||
|
$this->send_missed_call_notification($call_data['From'], $call_data['To']);
|
||||||
|
|
||||||
|
// Offer callback or voicemail options
|
||||||
|
$twiml .= '<Gather timeout="10" numDigits="1" action="' . home_url('/wp-json/twilio-webhook/v1/callback-choice') . '" method="POST">';
|
||||||
|
$twiml .= '<Say voice="alice">Press 1 to request a callback, or press 2 to leave a voicemail.</Say>';
|
||||||
|
$twiml .= '</Gather>';
|
||||||
|
|
||||||
|
// Default to voicemail if no input
|
||||||
|
$twiml .= '<Say voice="alice">No response received. Transferring you to voicemail.</Say>';
|
||||||
|
$twiml .= '<Redirect>' . home_url('/wp-json/twilio-webhook/v1/voicemail-callback') . '</Redirect>';
|
||||||
|
|
||||||
|
} elseif ($call_data['DialCallStatus'] === 'failed') {
|
||||||
|
// Technical failure - provide different message
|
||||||
|
$twiml .= '<Say voice="alice">We are experiencing technical difficulties. Please try again later or leave a voicemail.</Say>';
|
||||||
|
$twiml .= '<Redirect>' . home_url('/wp-json/twilio-webhook/v1/voicemail-callback') . '</Redirect>';
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Other statuses or unknown - generic fallback
|
||||||
|
$twiml .= '<Say voice="alice">We apologize, but we are unable to connect your call at this time.</Say>';
|
||||||
|
$twiml .= '<Gather timeout="10" numDigits="1" action="' . home_url('/wp-json/twilio-webhook/v1/callback-choice') . '" method="POST">';
|
||||||
|
$twiml .= '<Say voice="alice">Press 1 to request a callback, or press 2 to leave a voicemail.</Say>';
|
||||||
|
$twiml .= '</Gather>';
|
||||||
|
$twiml .= '<Redirect>' . home_url('/wp-json/twilio-webhook/v1/voicemail-callback') . '</Redirect>';
|
||||||
|
}
|
||||||
|
|
||||||
|
$twiml .= '</Response>';
|
||||||
|
|
||||||
|
return $this->send_twiml_response($twiml);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send SMS notification to agents about missed call
|
||||||
|
*/
|
||||||
|
private function send_missed_call_notification($customer_number, $twilio_number) {
|
||||||
|
// Get agents with phone numbers
|
||||||
|
$agents = get_users(array(
|
||||||
|
'meta_key' => 'twp_phone_number',
|
||||||
|
'meta_compare' => 'EXISTS'
|
||||||
|
));
|
||||||
|
|
||||||
|
$message = "Missed call from {$customer_number}. All agents were unavailable. Customer offered callback/voicemail options.";
|
||||||
|
|
||||||
|
foreach ($agents as $agent) {
|
||||||
|
$agent_phone = get_user_meta($agent->ID, 'twp_phone_number', true);
|
||||||
|
if (!empty($agent_phone)) {
|
||||||
|
$twilio_api = new TWP_Twilio_API();
|
||||||
|
$twilio_api->send_sms($agent_phone, $message, $twilio_number);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verify Twilio signature
|
* Verify Twilio signature
|
||||||
*/
|
*/
|
||||||
|
@@ -87,6 +87,12 @@ class TWP_Workflow {
|
|||||||
$stop_after_step = true; // Queue ends the workflow
|
$stop_after_step = true; // Queue ends the workflow
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'browser_call':
|
||||||
|
error_log('TWP Workflow: Processing browser call step: ' . json_encode($step));
|
||||||
|
$step_twiml = self::create_browser_call_twiml($step, $elevenlabs);
|
||||||
|
$stop_after_step = true; // Browser call ends the workflow
|
||||||
|
break;
|
||||||
|
|
||||||
case 'ring_group':
|
case 'ring_group':
|
||||||
$step_twiml = self::create_ring_group_twiml($step);
|
$step_twiml = self::create_ring_group_twiml($step);
|
||||||
$stop_after_step = true; // Ring group ends the workflow
|
$stop_after_step = true; // Ring group ends the workflow
|
||||||
@@ -380,6 +386,90 @@ class TWP_Workflow {
|
|||||||
return $twiml->asXML();
|
return $twiml->asXML();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create browser call TwiML
|
||||||
|
*/
|
||||||
|
private static function create_browser_call_twiml($step, $elevenlabs) {
|
||||||
|
$step_data = $step['data'];
|
||||||
|
$twiml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
|
||||||
|
|
||||||
|
// Get announcement message
|
||||||
|
$message = '';
|
||||||
|
if (isset($step_data['announce_message']) && !empty($step_data['announce_message'])) {
|
||||||
|
$message = $step_data['announce_message'];
|
||||||
|
} else {
|
||||||
|
$message = 'Connecting you to our agent.';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle audio type for announcement
|
||||||
|
$audio_type = isset($step_data['audio_type']) ? $step_data['audio_type'] : 'say';
|
||||||
|
|
||||||
|
switch ($audio_type) {
|
||||||
|
case 'tts':
|
||||||
|
$voice_id = isset($step_data['voice_id']) ? $step_data['voice_id'] : null;
|
||||||
|
$audio_result = $elevenlabs->text_to_speech($message, [
|
||||||
|
'voice_id' => $voice_id
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($audio_result['success']) {
|
||||||
|
$play = $twiml->addChild('Play', $audio_result['file_url']);
|
||||||
|
} else {
|
||||||
|
$say = $twiml->addChild('Say', $message);
|
||||||
|
$say->addAttribute('voice', 'alice');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'audio':
|
||||||
|
$audio_url = isset($step_data['audio_url']) ? $step_data['audio_url'] : null;
|
||||||
|
|
||||||
|
if ($audio_url && !empty($audio_url)) {
|
||||||
|
$play = $twiml->addChild('Play', $audio_url);
|
||||||
|
} else {
|
||||||
|
$say = $twiml->addChild('Say', $message);
|
||||||
|
$say->addAttribute('voice', 'alice');
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
default: // 'say'
|
||||||
|
$say = $twiml->addChild('Say', $message);
|
||||||
|
$say->addAttribute('voice', 'alice');
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dial browser clients
|
||||||
|
$dial = $twiml->addChild('Dial');
|
||||||
|
$dial->addAttribute('timeout', '30');
|
||||||
|
|
||||||
|
// Get browser client names (agents who might be online)
|
||||||
|
$browser_clients = isset($step_data['browser_clients']) ? $step_data['browser_clients'] : [];
|
||||||
|
|
||||||
|
if (empty($browser_clients)) {
|
||||||
|
// Default: try to call any available browser clients
|
||||||
|
// Get all users with agent capabilities
|
||||||
|
$agents = get_users(array(
|
||||||
|
'meta_key' => 'twp_phone_number',
|
||||||
|
'meta_compare' => 'EXISTS'
|
||||||
|
));
|
||||||
|
|
||||||
|
foreach ($agents as $agent) {
|
||||||
|
$client_name = 'agent_' . $agent->ID . '_' . sanitize_title($agent->display_name);
|
||||||
|
$client = $dial->addChild('Client', $client_name);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Call specific browser clients
|
||||||
|
foreach ($browser_clients as $client_name) {
|
||||||
|
$client = $dial->addChild('Client', $client_name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add fallback if no browser clients answer
|
||||||
|
$action_url = home_url('/wp-json/twilio-webhook/v1/browser-fallback');
|
||||||
|
$dial->addAttribute('action', $action_url);
|
||||||
|
$dial->addAttribute('method', 'POST');
|
||||||
|
|
||||||
|
return $twiml->asXML();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create forward TwiML
|
* Create forward TwiML
|
||||||
*/
|
*/
|
||||||
@@ -980,6 +1070,19 @@ class TWP_Workflow {
|
|||||||
return $wpdb->get_results("SELECT * FROM $table_name ORDER BY created_at DESC");
|
return $wpdb->get_results("SELECT * FROM $table_name ORDER BY created_at DESC");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get workflow by phone number
|
||||||
|
*/
|
||||||
|
public static function get_workflow_by_phone_number($phone_number) {
|
||||||
|
global $wpdb;
|
||||||
|
$table_name = $wpdb->prefix . 'twp_workflows';
|
||||||
|
|
||||||
|
return $wpdb->get_row($wpdb->prepare(
|
||||||
|
"SELECT * FROM $table_name WHERE phone_number = %s AND is_active = 1 ORDER BY created_at DESC LIMIT 1",
|
||||||
|
$phone_number
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update workflow
|
* Update workflow
|
||||||
*/
|
*/
|
||||||
|
Reference in New Issue
Block a user