prefix . 'twp_workflows';
$workflow_data = array(
'steps' => $data['steps'],
'conditions' => $data['conditions'],
'actions' => $data['actions']
);
return $wpdb->insert(
$table_name,
array(
'workflow_name' => sanitize_text_field($data['workflow_name']),
'phone_number' => sanitize_text_field($data['phone_number']),
'workflow_data' => json_encode($workflow_data),
'is_active' => isset($data['is_active']) ? 1 : 0
),
array('%s', '%s', '%s', '%d')
);
}
/**
* Execute workflow
*/
public static function execute_workflow($workflow_id, $call_data) {
$workflow = self::get_workflow($workflow_id);
if (!$workflow || !$workflow->is_active) {
return false;
}
$workflow_data = json_decode($workflow->workflow_data, true);
$twilio = new TWP_Twilio_API();
$elevenlabs = new TWP_ElevenLabs_API();
// Store call data globally for access in step functions
$GLOBALS['call_data'] = $call_data;
// Initialize combined TwiML response
$response = new \Twilio\TwiML\VoiceResponse();
$has_response = false;
error_log('TWP Workflow: Starting execution for workflow ID: ' . $workflow_id);
error_log('TWP Workflow: Call data: ' . json_encode($call_data));
error_log('TWP Workflow: Steps count: ' . count($workflow_data['steps']));
// Process workflow steps
foreach ($workflow_data['steps'] as $step) {
// Check conditions first
if (isset($step['conditions'])) {
if (!self::check_conditions($step['conditions'], $call_data)) {
continue;
}
}
$step_twiml = null;
$stop_after_step = false;
switch ($step['type']) {
case 'greeting':
$step_twiml = self::create_greeting_twiml($step, $elevenlabs);
break;
case 'ivr_menu':
// Add workflow_id to the step data
$step['workflow_id'] = $workflow_id;
$step_twiml = self::create_ivr_menu_twiml($step, $elevenlabs);
$stop_after_step = true; // IVR menu needs user input, stop here
break;
case 'forward':
$step_twiml = self::create_forward_twiml($step);
$stop_after_step = true; // Forward ends the workflow
break;
case 'queue':
error_log('TWP Workflow: Processing queue step: ' . json_encode($step));
$step_twiml = self::create_queue_twiml($step, $elevenlabs);
$stop_after_step = true; // Queue ends the workflow
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':
$step_twiml = self::create_ring_group_twiml($step);
$stop_after_step = true; // Ring group ends the workflow
break;
case 'voicemail':
// Add workflow_id to the step data
$step['workflow_id'] = $workflow_id;
$step_twiml = self::create_voicemail_twiml($step, $elevenlabs);
$stop_after_step = true; // Voicemail recording ends the workflow
break;
case 'schedule_check':
$schedule_result = self::handle_schedule_check($step, $call_data);
if ($schedule_result === false) {
// Continue to next step (within business hours)
error_log('TWP Schedule Check: Within business hours, continuing to next step');
continue 2;
} elseif ($schedule_result) {
// After-hours steps returned TwiML - execute and stop
error_log('TWP Schedule Check: After hours, executing after-hours steps');
$step_twiml = $schedule_result;
$stop_after_step = true;
} else {
// No schedule or no after-hours steps - continue with next step
error_log('TWP Schedule Check: No schedule configured or no after-hours steps, continuing');
continue 2;
}
break;
case 'sms':
self::send_sms_notification($step, $call_data);
continue 2;
default:
continue 2;
}
// Add step TwiML to combined response
if ($step_twiml) {
// Parse the step TwiML and append to combined response
$step_xml = simplexml_load_string($step_twiml);
if ($step_xml) {
foreach ($step_xml->children() as $element) {
self::append_twiml_element($response, $element);
}
$has_response = true;
}
// Stop processing if this step type should end the workflow
if ($stop_after_step) {
break;
}
}
}
// Return combined response or default
if ($has_response) {
return $response->asXML();
}
// Default response
return self::create_default_response();
}
/**
* Helper function to append SimpleXMLElement to TwiML Response
*/
private static function append_twiml_element($response, $element) {
$name = $element->getName();
$text = (string) $element;
$attributes = array();
foreach ($element->attributes() as $key => $value) {
$attributes[$key] = (string) $value;
}
// Handle different TwiML verbs
switch ($name) {
case 'Say':
$response->say($text, $attributes);
break;
case 'Play':
$response->play($text, $attributes);
break;
case 'Gather':
$gather = $response->gather($attributes);
// Add child elements to gather
foreach ($element->children() as $child) {
$child_name = $child->getName();
if ($child_name === 'Say') {
$gather->say((string) $child, self::get_attributes($child));
} elseif ($child_name === 'Play') {
$gather->play((string) $child, self::get_attributes($child));
}
}
break;
case 'Record':
$response->record($attributes);
break;
case 'Dial':
$response->dial((string) $element, $attributes);
break;
case 'Queue':
$response->queue((string) $element, $attributes);
break;
case 'Redirect':
$response->redirect((string) $element, $attributes);
break;
case 'Pause':
$response->pause($attributes);
break;
case 'Hangup':
$response->hangup();
break;
}
}
/**
* Helper to get attributes as array
*/
private static function get_attributes($element) {
$attributes = array();
foreach ($element->attributes() as $key => $value) {
$attributes[$key] = (string) $value;
}
return $attributes;
}
/**
* Create greeting TwiML
*/
private static function create_greeting_twiml($step, $elevenlabs) {
$twiml = new SimpleXMLElement('');
// Get message from either data array or direct property
$message = null;
if (isset($step['data']['message']) && !empty($step['data']['message'])) {
$message = $step['data']['message'];
} elseif (isset($step['message']) && !empty($step['message'])) {
$message = $step['message'];
} else {
$message = 'Welcome to our phone system.';
}
// Check for new audio_type structure or legacy use_tts
$audio_type = isset($step['data']['audio_type']) ? $step['data']['audio_type'] :
(isset($step['audio_type']) ? $step['audio_type'] :
(isset($step['data']['use_tts']) && $step['data']['use_tts'] ? 'tts' :
(isset($step['use_tts']) && $step['use_tts'] ? 'tts' : 'say')));
switch ($audio_type) {
case 'tts':
// Generate TTS audio
$voice_id = isset($step['data']['voice_id']) ? $step['data']['voice_id'] :
(isset($step['voice_id']) ? $step['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 {
// Fallback to Say
$say = $twiml->addChild('Say', $message);
$say->addAttribute('voice', 'alice');
}
break;
case 'audio':
// Use provided audio file
$audio_url = isset($step['data']['audio_url']) ? $step['data']['audio_url'] :
(isset($step['audio_url']) ? $step['audio_url'] : null);
if ($audio_url && !empty($audio_url)) {
$play = $twiml->addChild('Play', $audio_url);
} else {
// Fallback to Say if no audio URL provided
$say = $twiml->addChild('Say', $message);
$say->addAttribute('voice', 'alice');
}
break;
default: // 'say' or fallback
$say = $twiml->addChild('Say', $message);
$say->addAttribute('voice', 'alice');
break;
}
return $twiml->asXML();
}
/**
* Create IVR menu TwiML
*/
private static function create_ivr_menu_twiml($step, $elevenlabs) {
$twiml = new SimpleXMLElement('');
$gather = $twiml->addChild('Gather');
$gather->addAttribute('numDigits', isset($step['data']['num_digits']) ? $step['data']['num_digits'] :
(isset($step['num_digits']) ? $step['num_digits'] : '1'));
$gather->addAttribute('timeout', isset($step['data']['timeout']) ? $step['data']['timeout'] :
(isset($step['timeout']) ? $step['timeout'] : '10'));
if (isset($step['action_url'])) {
$gather->addAttribute('action', $step['action_url']);
} else {
$webhook_url = home_url('/wp-json/twilio-webhook/v1/ivr-response');
if (isset($step['workflow_id'])) {
$webhook_url = add_query_arg('workflow_id', $step['workflow_id'], $webhook_url);
}
if (isset($step['id'])) {
$webhook_url = add_query_arg('step_id', $step['id'], $webhook_url);
}
$gather->addAttribute('action', $webhook_url);
}
// Get message from either data array or direct property
$message = null;
if (isset($step['data']['message']) && !empty($step['data']['message'])) {
$message = $step['data']['message'];
} elseif (isset($step['message']) && !empty($step['message'])) {
$message = $step['message'];
} else {
$message = 'Please select an option.';
}
// Check for new audio_type structure or legacy use_tts
$audio_type = isset($step['data']['audio_type']) ? $step['data']['audio_type'] :
(isset($step['audio_type']) ? $step['audio_type'] :
(isset($step['data']['use_tts']) && $step['data']['use_tts'] ? 'tts' :
(isset($step['use_tts']) && $step['use_tts'] ? 'tts' : 'say')));
switch ($audio_type) {
case 'tts':
// Generate TTS audio
$voice_id = isset($step['data']['voice_id']) ? $step['data']['voice_id'] :
(isset($step['voice_id']) ? $step['voice_id'] : null);
$audio_result = $elevenlabs->text_to_speech($message, [
'voice_id' => $voice_id
]);
if ($audio_result['success']) {
$play = $gather->addChild('Play', $audio_result['file_url']);
} else {
// Fallback to Say
$say = $gather->addChild('Say', $message);
$say->addAttribute('voice', 'alice');
}
break;
case 'audio':
// Use provided audio file
$audio_url = isset($step['data']['audio_url']) ? $step['data']['audio_url'] :
(isset($step['audio_url']) ? $step['audio_url'] : null);
if ($audio_url && !empty($audio_url)) {
$play = $gather->addChild('Play', $audio_url);
} else {
// Fallback to Say if no audio URL provided
$say = $gather->addChild('Say', $message);
$say->addAttribute('voice', 'alice');
}
break;
default: // 'say' or fallback
$say = $gather->addChild('Say', $message);
$say->addAttribute('voice', 'alice');
break;
}
// Fallback if no input
if (isset($step['no_input_action'])) {
switch ($step['no_input_action']) {
case 'repeat':
$redirect = $twiml->addChild('Redirect');
break;
case 'hangup':
$say = $twiml->addChild('Say', 'Goodbye');
$say->addAttribute('voice', 'alice');
$twiml->addChild('Hangup');
break;
case 'forward':
if (isset($step['forward_number'])) {
$dial = $twiml->addChild('Dial');
$dial->addAttribute('answerOnBridge', 'true');
$dial->addChild('Number', $step['forward_number']);
}
break;
}
}
return $twiml->asXML();
}
/**
* Create browser call TwiML
*/
private static function create_browser_call_twiml($step, $elevenlabs) {
$step_data = $step['data'];
$twiml = new SimpleXMLElement('');
// 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
*/
private static function create_forward_twiml($step) {
$twiml = new SimpleXMLElement('');
$dial = $twiml->addChild('Dial');
$dial->addAttribute('answerOnBridge', 'true');
if (isset($step['timeout'])) {
$dial->addAttribute('timeout', $step['timeout']);
}
if (isset($step['forward_numbers']) && is_array($step['forward_numbers'])) {
// Sequential forwarding
foreach ($step['forward_numbers'] as $number) {
$dial->addChild('Number', $number);
}
} elseif (isset($step['forward_number'])) {
$dial->addChild('Number', $step['forward_number']);
}
return $twiml->asXML();
}
/**
* Create queue TwiML
*/
private static function create_queue_twiml($step, $elevenlabs) {
error_log('TWP Workflow: Creating queue TwiML with step data: ' . print_r($step, true));
$twiml = new SimpleXMLElement('');
// Check if data is nested (workflow steps have data nested)
$step_data = isset($step['data']) ? $step['data'] : $step;
// Get the actual queue name from the database if we have a queue_id
$queue_name = '';
$queue_id = null;
if (isset($step_data['queue_id']) && !empty($step_data['queue_id'])) {
$queue_id = $step_data['queue_id'];
error_log('TWP Workflow: Looking up queue with ID: ' . $queue_id);
$queue = TWP_Call_Queue::get_queue($queue_id);
if ($queue) {
$queue_name = $queue->queue_name;
error_log('TWP Workflow: Found queue name: ' . $queue_name);
} else {
error_log('TWP Workflow: Queue not found in database for ID: ' . $queue_id);
}
} elseif (isset($step_data['queue_name']) && !empty($step_data['queue_name'])) {
// Fallback to queue_name if provided directly
$queue_name = $step_data['queue_name'];
error_log('TWP Workflow: Using queue_name directly: ' . $queue_name);
} else {
error_log('TWP Workflow: No queue_id or queue_name in step data');
}
// Log error if no queue name found
if (empty($queue_name)) {
error_log('TWP Workflow: ERROR - Queue name is empty after lookup');
// Return error message instead of empty queue
$say = $twiml->addChild('Say', 'Sorry, the queue is not configured properly. Please try again later.');
$say->addAttribute('voice', 'alice');
return $twiml->asXML();
}
error_log('TWP Workflow: Using queue name for Enqueue: ' . $queue_name);
// Add call to queue database BEFORE generating Enqueue TwiML
// Get call info from current request context or call_data parameter
$call_sid = isset($_POST['CallSid']) ? $_POST['CallSid'] :
(isset($_REQUEST['CallSid']) ? $_REQUEST['CallSid'] :
(isset($GLOBALS['call_data']['CallSid']) ? $GLOBALS['call_data']['CallSid'] : ''));
$from_number = isset($_POST['From']) ? $_POST['From'] :
(isset($_REQUEST['From']) ? $_REQUEST['From'] :
(isset($GLOBALS['call_data']['From']) ? $GLOBALS['call_data']['From'] : ''));
$to_number = isset($_POST['To']) ? $_POST['To'] :
(isset($_REQUEST['To']) ? $_REQUEST['To'] :
(isset($GLOBALS['call_data']['To']) ? $GLOBALS['call_data']['To'] : ''));
error_log('TWP Queue: Call data - SID: ' . $call_sid . ', From: ' . $from_number . ', To: ' . $to_number);
if ($call_sid && $queue_id) {
error_log('TWP Workflow: Adding call to queue database - CallSid: ' . $call_sid . ', Queue ID: ' . $queue_id);
$add_result = TWP_Call_Queue::add_to_queue($queue_id, array(
'call_sid' => $call_sid,
'from_number' => $from_number,
'to_number' => $to_number
));
error_log('TWP Workflow: Add to queue result: ' . ($add_result ? 'success' : 'failed'));
} else {
error_log('TWP Workflow: Cannot add to queue - missing CallSid (' . $call_sid . ') or queue_id (' . $queue_id . ')');
}
// Instead of using Twilio's Enqueue, redirect to our queue wait handler
// This gives us complete control over the queue experience
// Get announcement message
$message = '';
if (isset($step_data['announce_message']) && !empty($step_data['announce_message'])) {
$message = $step_data['announce_message'];
} else {
$message = 'Please hold while we connect you to the next available agent.';
}
// Handle audio type for queue announcement (same logic as other steps)
$audio_type = isset($step_data['audio_type']) ? $step_data['audio_type'] : 'say';
switch ($audio_type) {
case 'tts':
// Generate TTS audio
$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 {
// Fallback to Say
$say = $twiml->addChild('Say', $message);
$say->addAttribute('voice', 'alice');
}
break;
case 'audio':
// Use provided audio file
$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 {
// Fallback to Say if no audio URL provided
$say = $twiml->addChild('Say', $message);
$say->addAttribute('voice', 'alice');
}
break;
default: // 'say'
$say = $twiml->addChild('Say', $message);
$say->addAttribute('voice', 'alice');
break;
}
// Build the redirect URL properly
$wait_url = home_url('/wp-json/twilio-webhook/v1/queue-wait');
$wait_url = add_query_arg(array(
'queue_id' => $queue_id,
'call_sid' => urlencode($call_sid) // URL encode to handle special characters
), $wait_url);
// Set the text content of Redirect element properly
$redirect = $twiml->addChild('Redirect');
$redirect[0] = $wait_url; // Set the URL as the text content
$redirect->addAttribute('method', 'POST');
error_log('TWP Workflow: Redirecting to custom queue wait handler: ' . $wait_url);
$result = $twiml->asXML();
error_log('TWP Workflow: Final Queue TwiML: ' . $result);
return $result;
}
/**
* Create ring group TwiML
*/
private static function create_ring_group_twiml($step) {
$twiml = new SimpleXMLElement('');
if (isset($step['announce_message'])) {
$say = $twiml->addChild('Say', $step['announce_message']);
$say->addAttribute('voice', 'alice');
}
// Get group phone numbers
$group_id = intval($step['group_id']);
$phone_numbers = TWP_Agent_Groups::get_group_phone_numbers($group_id);
if (empty($phone_numbers)) {
$say = $twiml->addChild('Say', 'No agents are available in this group. Please try again later.');
$say->addAttribute('voice', 'alice');
$twiml->addChild('Hangup');
return $twiml->asXML();
}
$dial = $twiml->addChild('Dial');
$dial->addAttribute('answerOnBridge', 'true');
if (isset($step['timeout'])) {
$dial->addAttribute('timeout', $step['timeout']);
} else {
$dial->addAttribute('timeout', '30');
}
if (isset($step['caller_id'])) {
$dial->addAttribute('callerId', $step['caller_id']);
}
// Set action URL to handle no-answer scenarios
$action_url = home_url('/wp-json/twilio-webhook/v1/ring-group-result?' . http_build_query([
'group_id' => $group_id,
'queue_name' => isset($step['queue_name']) ? $step['queue_name'] : null,
'fallback_action' => isset($step['fallback_action']) ? $step['fallback_action'] : 'queue'
]));
$dial->addAttribute('action', $action_url);
// Add all group numbers for simultaneous ring
foreach ($phone_numbers as $number) {
if (!empty($number)) {
$dial->addChild('Number', $number);
}
}
return $twiml->asXML();
}
/**
* Create voicemail TwiML
*/
private static function create_voicemail_twiml($step, $elevenlabs) {
$twiml = new SimpleXMLElement('');
// Debug logging
error_log('TWP Voicemail Step Data: ' . json_encode($step));
// Check for greeting message in different possible field names
// The step data might be nested in a 'data' object
$greeting = null;
if (isset($step['data']['greeting_message']) && !empty($step['data']['greeting_message'])) {
$greeting = $step['data']['greeting_message'];
error_log('TWP Voicemail: Using data.greeting_message: ' . $greeting);
} elseif (isset($step['greeting_message']) && !empty($step['greeting_message'])) {
$greeting = $step['greeting_message'];
error_log('TWP Voicemail: Using greeting_message: ' . $greeting);
} elseif (isset($step['data']['message']) && !empty($step['data']['message'])) {
$greeting = $step['data']['message'];
error_log('TWP Voicemail: Using data.message: ' . $greeting);
} elseif (isset($step['message']) && !empty($step['message'])) {
$greeting = $step['message'];
error_log('TWP Voicemail: Using message: ' . $greeting);
} elseif (isset($step['data']['prompt']) && !empty($step['data']['prompt'])) {
$greeting = $step['data']['prompt'];
error_log('TWP Voicemail: Using data.prompt: ' . $greeting);
} elseif (isset($step['prompt']) && !empty($step['prompt'])) {
$greeting = $step['prompt'];
error_log('TWP Voicemail: Using prompt: ' . $greeting);
} elseif (isset($step['data']['text']) && !empty($step['data']['text'])) {
$greeting = $step['data']['text'];
error_log('TWP Voicemail: Using data.text: ' . $greeting);
} elseif (isset($step['text']) && !empty($step['text'])) {
$greeting = $step['text'];
error_log('TWP Voicemail: Using text: ' . $greeting);
}
// Add greeting message if provided
if ($greeting) {
error_log('TWP Voicemail: Found greeting: ' . $greeting);
// Check for new audio_type structure or legacy use_tts
$audio_type = isset($step['data']['audio_type']) ? $step['data']['audio_type'] :
(isset($step['audio_type']) ? $step['audio_type'] :
(isset($step['data']['use_tts']) && $step['data']['use_tts'] ? 'tts' :
(isset($step['use_tts']) && $step['use_tts'] ? 'tts' : 'say')));
error_log('TWP Voicemail: audio_type = ' . $audio_type);
switch ($audio_type) {
case 'tts':
error_log('TWP Voicemail: Attempting ElevenLabs TTS');
// Check for voice_id in data object or root
$voice_id = isset($step['data']['voice_id']) && !empty($step['data']['voice_id']) ? $step['data']['voice_id'] :
(isset($step['voice_id']) && !empty($step['voice_id']) ? $step['voice_id'] : null);
error_log('TWP Voicemail: voice_id = ' . ($voice_id ?: 'default'));
$audio_result = $elevenlabs->text_to_speech($greeting, [
'voice_id' => $voice_id
]);
if ($audio_result && isset($audio_result['success']) && $audio_result['success']) {
error_log('TWP Voicemail: ElevenLabs TTS successful, using audio file: ' . $audio_result['file_url']);
$play = $twiml->addChild('Play', $audio_result['file_url']);
} else {
error_log('TWP Voicemail: ElevenLabs TTS failed, falling back to Say: ' . json_encode($audio_result));
$say = $twiml->addChild('Say', $greeting);
$say->addAttribute('voice', 'alice');
}
break;
case 'audio':
// Use provided audio file
$audio_url = isset($step['data']['audio_url']) ? $step['data']['audio_url'] :
(isset($step['audio_url']) ? $step['audio_url'] : null);
if ($audio_url && !empty($audio_url)) {
error_log('TWP Voicemail: Using audio file: ' . $audio_url);
$play = $twiml->addChild('Play', $audio_url);
} else {
error_log('TWP Voicemail: No audio URL provided, falling back to Say');
$say = $twiml->addChild('Say', $greeting);
$say->addAttribute('voice', 'alice');
}
break;
default: // 'say' or fallback
error_log('TWP Voicemail: Using standard Say for greeting');
$say = $twiml->addChild('Say', $greeting);
$say->addAttribute('voice', 'alice');
break;
}
} else {
error_log('TWP Voicemail: No custom greeting found, using default');
// Default greeting if none provided
$say = $twiml->addChild('Say', 'Please leave your message after the beep. Press the pound key when finished.');
$say->addAttribute('voice', 'alice');
}
$record = $twiml->addChild('Record');
$record->addAttribute('maxLength', isset($step['max_length']) ? $step['max_length'] : '120');
$record->addAttribute('playBeep', 'true');
$record->addAttribute('finishOnKey', '#');
$record->addAttribute('timeout', '10');
// Add action URL to handle what happens after recording
$action_url = home_url('/wp-json/twilio-webhook/v1/voicemail-complete');
$record->addAttribute('action', $action_url);
// Add recording status callback for saving the voicemail
$callback_url = home_url('/wp-json/twilio-webhook/v1/voicemail-callback');
if (isset($step['workflow_id'])) {
$callback_url = add_query_arg('workflow_id', $step['workflow_id'], $callback_url);
}
$record->addAttribute('recordingStatusCallback', $callback_url);
$record->addAttribute('recordingStatusCallbackMethod', 'POST');
// Add transcription (enabled by default unless explicitly disabled)
if (!isset($step['transcribe']) || $step['transcribe'] !== false) {
$record->addAttribute('transcribe', 'true');
$record->addAttribute('transcribeCallback', home_url('/wp-json/twilio-webhook/v1/transcription'));
}
$twiml_output = $twiml->asXML();
error_log('TWP Voicemail: Generated TwiML: ' . $twiml_output);
return $twiml_output;
}
/**
* Handle schedule check
*/
private static function handle_schedule_check($step, $call_data) {
$schedule_id = $step['data']['schedule_id'] ?? $step['schedule_id'] ?? null;
error_log('TWP Schedule Check: Processing schedule check with ID: ' . ($schedule_id ?: 'none'));
error_log('TWP Schedule Check: Step data: ' . json_encode($step));
if (!$schedule_id) {
error_log('TWP Schedule Check: No schedule ID specified, continuing to next step');
// No schedule specified, return false to continue to next step
return false;
}
// Check if we're within business hours first
$is_active = TWP_Scheduler::is_schedule_active($schedule_id);
error_log('TWP Schedule Check: Schedule active status: ' . ($is_active ? 'true' : 'false'));
if ($is_active) {
error_log('TWP Schedule Check: Within business hours, continuing to next workflow step');
// Within business hours - continue with normal workflow
return false; // Continue to next workflow step
} else {
error_log('TWP Schedule Check: Outside business hours, checking for after-hours steps');
// After hours - execute after-hours steps
$after_hours_steps = null;
if (isset($step['data']['after_hours_steps']) && !empty($step['data']['after_hours_steps'])) {
$after_hours_steps = $step['data']['after_hours_steps'];
} elseif (isset($step['after_hours_steps']) && !empty($step['after_hours_steps'])) {
$after_hours_steps = $step['after_hours_steps'];
}
if ($after_hours_steps) {
error_log('TWP Schedule Check: Found after-hours steps, executing: ' . json_encode($after_hours_steps));
return self::execute_after_hours_steps($after_hours_steps, $call_data);
} else {
error_log('TWP Schedule Check: No after-hours steps configured');
// Fall back to schedule routing if no after-hours steps in workflow
$routing = TWP_Scheduler::get_schedule_routing($schedule_id);
if ($routing['action'] === 'workflow' && $routing['data']['workflow_id']) {
error_log('TWP Schedule Check: Using schedule routing to workflow: ' . $routing['data']['workflow_id']);
// Route to different workflow
$workflow_id = $routing['data']['workflow_id'];
$workflow = self::get_workflow($workflow_id);
if ($workflow && $workflow->is_active) {
return self::execute_workflow($workflow_id, $call_data);
}
} else if ($routing['action'] === 'forward' && $routing['data']['forward_number']) {
error_log('TWP Schedule Check: Using schedule routing to forward: ' . $routing['data']['forward_number']);
// Forward call
$twiml = new \Twilio\TwiML\VoiceResponse();
$twiml->dial($routing['data']['forward_number']);
return $twiml->asXML();
}
}
}
error_log('TWP Schedule Check: No action taken, continuing to next step');
return false;
}
/**
* Execute after-hours steps
*/
private static function execute_after_hours_steps($steps, $call_data) {
$twiml = new SimpleXMLElement('');
foreach ($steps as $step) {
switch ($step['type']) {
case 'greeting':
if (isset($step['message']) && !empty($step['message'])) {
$say = $twiml->addChild('Say', $step['message']);
$say->addAttribute('voice', 'alice');
}
break;
case 'forward':
if (isset($step['number']) && !empty($step['number'])) {
$dial = $twiml->addChild('Dial');
$dial->addAttribute('answerOnBridge', 'true');
$dial->addChild('Number', $step['number']);
return $twiml->asXML(); // End here for forward
}
break;
case 'voicemail':
// Add greeting if provided
if (isset($step['greeting']) && !empty($step['greeting'])) {
$say = $twiml->addChild('Say', $step['greeting']);
$say->addAttribute('voice', 'alice');
}
// Add record
$record = $twiml->addChild('Record');
$record->addAttribute('maxLength', '120');
$record->addAttribute('playBeep', 'true');
$record->addAttribute('finishOnKey', '#');
$record->addAttribute('timeout', '10');
$record->addAttribute('action', home_url('/wp-json/twilio-webhook/v1/voicemail-complete'));
$record->addAttribute('recordingStatusCallback', home_url('/wp-json/twilio-webhook/v1/voicemail-callback'));
$record->addAttribute('recordingStatusCallbackMethod', 'POST');
$record->addAttribute('transcribe', 'true');
$record->addAttribute('transcribeCallback', home_url('/wp-json/twilio-webhook/v1/transcription'));
return $twiml->asXML(); // End here for voicemail
case 'queue':
if (isset($step['queue_name']) && !empty($step['queue_name'])) {
$enqueue = $twiml->addChild('Enqueue', $step['queue_name']);
return $twiml->asXML(); // End here for queue
}
break;
case 'sms':
if (isset($step['to_number']) && !empty($step['to_number']) &&
isset($step['message']) && !empty($step['message'])) {
// Send SMS notification
$twilio = new TWP_Twilio_API();
// Get the from number using proper priority
$workflow_id = isset($step['workflow_id']) ? $step['workflow_id'] : null;
$from_number = TWP_Twilio_API::get_sms_from_number($workflow_id);
$message = str_replace(
array('{from}', '{to}', '{time}'),
array($call_data['From'], $call_data['To'], current_time('g:i A')),
$step['message']
);
$twilio->send_sms($step['to_number'], $message, $from_number);
}
break;
}
}
return $twiml->asXML();
}
/**
* Execute action
*/
private static function execute_action($action, $call_data) {
switch ($action['type']) {
case 'forward':
return self::create_forward_twiml($action);
case 'voicemail':
$elevenlabs = new TWP_ElevenLabs_API();
return self::create_voicemail_twiml($action, $elevenlabs);
case 'queue':
return self::create_queue_twiml($action);
case 'ring_group':
return self::create_ring_group_twiml($action);
case 'message':
$twiml = new SimpleXMLElement('');
$say = $twiml->addChild('Say', $action['message']);
$say->addAttribute('voice', 'alice');
return $twiml->asXML();
default:
return false;
}
}
/**
* Check conditions
*/
private static function check_conditions($conditions, $call_data) {
foreach ($conditions as $condition) {
switch ($condition['type']) {
case 'time':
$current_time = current_time('H:i');
if ($current_time < $condition['start_time'] || $current_time > $condition['end_time']) {
return false;
}
break;
case 'day_of_week':
$current_day = strtolower(date('l'));
if (!in_array($current_day, $condition['days'])) {
return false;
}
break;
case 'caller_id':
if (!in_array($call_data['From'], $condition['numbers'])) {
return false;
}
break;
}
}
return true;
}
/**
* Send SMS notification
*/
private static function send_sms_notification($step, $call_data) {
$twilio = new TWP_Twilio_API();
// Get the from number - priority: workflow phone > default SMS number > first Twilio number
$workflow_id = isset($step['workflow_id']) ? $step['workflow_id'] : null;
$from_number = TWP_Twilio_API::get_sms_from_number($workflow_id);
$message = str_replace(
array('{from}', '{to}', '{time}'),
array($call_data['From'], $call_data['To'], current_time('g:i A')),
$step['message']
);
$twilio->send_sms($step['to_number'], $message, $from_number);
}
/**
* Create default response
*/
private static function create_default_response() {
$twiml = new SimpleXMLElement('');
$say = $twiml->addChild('Say', 'Thank you for calling. Goodbye.');
$say->addAttribute('voice', 'alice');
$twiml->addChild('Hangup');
return $twiml->asXML();
}
/**
* Get workflow
*/
public static function get_workflow($workflow_id) {
global $wpdb;
$table_name = $wpdb->prefix . 'twp_workflows';
return $wpdb->get_row($wpdb->prepare(
"SELECT * FROM $table_name WHERE id = %d",
$workflow_id
));
}
/**
* Get workflows
*/
public static function get_workflows() {
global $wpdb;
$table_name = $wpdb->prefix . 'twp_workflows';
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
*/
public static function update_workflow($workflow_id, $data) {
global $wpdb;
$table_name = $wpdb->prefix . 'twp_workflows';
$update_data = array();
$update_format = array();
if (isset($data['workflow_name'])) {
$update_data['workflow_name'] = sanitize_text_field($data['workflow_name']);
$update_format[] = '%s';
}
if (isset($data['phone_number'])) {
$update_data['phone_number'] = sanitize_text_field($data['phone_number']);
$update_format[] = '%s';
}
if (isset($data['workflow_data'])) {
// Check if workflow_data is already JSON string or needs encoding
if (is_string($data['workflow_data'])) {
$update_data['workflow_data'] = $data['workflow_data'];
} else {
$update_data['workflow_data'] = json_encode($data['workflow_data']);
}
$update_format[] = '%s';
}
if (isset($data['is_active'])) {
$update_data['is_active'] = $data['is_active'] ? 1 : 0;
$update_format[] = '%d';
}
return $wpdb->update(
$table_name,
$update_data,
array('id' => $workflow_id),
$update_format,
array('%d')
);
}
/**
* Delete workflow
*/
public static function delete_workflow($workflow_id) {
global $wpdb;
$table_name = $wpdb->prefix . 'twp_workflows';
return $wpdb->delete(
$table_name,
array('id' => $workflow_id),
array('%d')
);
}
}