progress made
This commit is contained in:
@@ -43,35 +43,78 @@ class TWP_Workflow {
|
||||
$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':
|
||||
$twiml = self::create_greeting_twiml($step, $elevenlabs);
|
||||
$step_twiml = self::create_greeting_twiml($step, $elevenlabs);
|
||||
break;
|
||||
|
||||
case 'ivr_menu':
|
||||
$twiml = self::create_ivr_menu_twiml($step, $elevenlabs);
|
||||
$step_twiml = self::create_ivr_menu_twiml($step, $elevenlabs);
|
||||
$stop_after_step = true; // IVR menu needs user input, stop here
|
||||
break;
|
||||
|
||||
case 'forward':
|
||||
$twiml = self::create_forward_twiml($step);
|
||||
$step_twiml = self::create_forward_twiml($step);
|
||||
$stop_after_step = true; // Forward ends the workflow
|
||||
break;
|
||||
|
||||
case 'queue':
|
||||
$twiml = self::create_queue_twiml($step);
|
||||
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 'ring_group':
|
||||
$twiml = self::create_ring_group_twiml($step);
|
||||
$step_twiml = self::create_ring_group_twiml($step);
|
||||
$stop_after_step = true; // Ring group ends the workflow
|
||||
break;
|
||||
|
||||
case 'voicemail':
|
||||
$twiml = self::create_voicemail_twiml($step, $elevenlabs);
|
||||
// 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':
|
||||
$twiml = self::handle_schedule_check($step, $call_data);
|
||||
$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':
|
||||
@@ -82,42 +125,155 @@ class TWP_Workflow {
|
||||
continue 2;
|
||||
}
|
||||
|
||||
// Check conditions
|
||||
if (isset($step['conditions'])) {
|
||||
if (!self::check_conditions($step['conditions'], $call_data)) {
|
||||
continue;
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
// Execute step
|
||||
if ($twiml) {
|
||||
return $twiml;
|
||||
}
|
||||
}
|
||||
|
||||
// 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('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
|
||||
|
||||
if (isset($step['use_tts']) && $step['use_tts']) {
|
||||
// Generate TTS audio
|
||||
$audio_result = $elevenlabs->text_to_speech($step['message']);
|
||||
|
||||
if ($audio_result['success']) {
|
||||
$play = $twiml->addChild('Play', $audio_result['file_url']);
|
||||
} else {
|
||||
$say = $twiml->addChild('Say', $step['message']);
|
||||
$say->addAttribute('voice', 'alice');
|
||||
}
|
||||
// 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 {
|
||||
$say = $twiml->addChild('Say', $step['message']);
|
||||
$say->addAttribute('voice', 'alice');
|
||||
$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();
|
||||
@@ -130,31 +286,72 @@ class TWP_Workflow {
|
||||
$twiml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
|
||||
|
||||
$gather = $twiml->addChild('Gather');
|
||||
$gather->addAttribute('numDigits', isset($step['num_digits']) ? $step['num_digits'] : '1');
|
||||
$gather->addAttribute('timeout', isset($step['timeout']) ? $step['timeout'] : '10');
|
||||
$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('/twilio-webhook/ivr-response');
|
||||
$webhook_url = home_url('/wp-json/twilio-webhook/v1/ivr-response');
|
||||
$webhook_url = add_query_arg('workflow_id', $step['workflow_id'], $webhook_url);
|
||||
$webhook_url = add_query_arg('step_id', $step['id'], $webhook_url);
|
||||
$gather->addAttribute('action', $webhook_url);
|
||||
}
|
||||
|
||||
if (isset($step['use_tts']) && $step['use_tts']) {
|
||||
// Generate TTS for menu options
|
||||
$audio_result = $elevenlabs->text_to_speech($step['message']);
|
||||
|
||||
if ($audio_result['success']) {
|
||||
$play = $gather->addChild('Play', $audio_result['file_url']);
|
||||
} else {
|
||||
$say = $gather->addChild('Say', $step['message']);
|
||||
$say->addAttribute('voice', 'alice');
|
||||
}
|
||||
// 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 {
|
||||
$say = $gather->addChild('Say', $step['message']);
|
||||
$say->addAttribute('voice', 'alice');
|
||||
$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
|
||||
@@ -173,6 +370,7 @@ class TWP_Workflow {
|
||||
case 'forward':
|
||||
if (isset($step['forward_number'])) {
|
||||
$dial = $twiml->addChild('Dial');
|
||||
$dial->addAttribute('answerOnBridge', 'true');
|
||||
$dial->addChild('Number', $step['forward_number']);
|
||||
}
|
||||
break;
|
||||
@@ -189,6 +387,7 @@ class TWP_Workflow {
|
||||
$twiml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
|
||||
|
||||
$dial = $twiml->addChild('Dial');
|
||||
$dial->addAttribute('answerOnBridge', 'true');
|
||||
|
||||
if (isset($step['timeout'])) {
|
||||
$dial->addAttribute('timeout', $step['timeout']);
|
||||
@@ -209,25 +408,141 @@ class TWP_Workflow {
|
||||
/**
|
||||
* Create queue TwiML
|
||||
*/
|
||||
private static function create_queue_twiml($step) {
|
||||
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('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
|
||||
|
||||
if (isset($step['announce_message'])) {
|
||||
$say = $twiml->addChild('Say', $step['announce_message']);
|
||||
$say->addAttribute('voice', 'alice');
|
||||
}
|
||||
// Check if data is nested (workflow steps have data nested)
|
||||
$step_data = isset($step['data']) ? $step['data'] : $step;
|
||||
|
||||
$enqueue = $twiml->addChild('Enqueue', $step['queue_name']);
|
||||
// Get the actual queue name from the database if we have a queue_id
|
||||
$queue_name = '';
|
||||
$queue_id = null;
|
||||
|
||||
if (isset($step['wait_url'])) {
|
||||
$enqueue->addAttribute('waitUrl', $step['wait_url']);
|
||||
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 {
|
||||
$wait_url = home_url('/twilio-webhook/queue-wait');
|
||||
$wait_url = add_query_arg('queue_id', $step['queue_id'], $wait_url);
|
||||
$enqueue->addAttribute('waitUrl', $wait_url);
|
||||
error_log('TWP Workflow: No queue_id or queue_name in step data');
|
||||
}
|
||||
|
||||
return $twiml->asXML();
|
||||
// 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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -253,6 +568,7 @@ class TWP_Workflow {
|
||||
}
|
||||
|
||||
$dial = $twiml->addChild('Dial');
|
||||
$dial->addAttribute('answerOnBridge', 'true');
|
||||
|
||||
if (isset($step['timeout'])) {
|
||||
$dial->addAttribute('timeout', $step['timeout']);
|
||||
@@ -288,33 +604,125 @@ class TWP_Workflow {
|
||||
private static function create_voicemail_twiml($step, $elevenlabs) {
|
||||
$twiml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
|
||||
|
||||
if (isset($step['greeting_message'])) {
|
||||
if (isset($step['use_tts']) && $step['use_tts']) {
|
||||
$audio_result = $elevenlabs->text_to_speech($step['greeting_message']);
|
||||
|
||||
if ($audio_result['success']) {
|
||||
$play = $twiml->addChild('Play', $audio_result['file_url']);
|
||||
} else {
|
||||
$say = $twiml->addChild('Say', $step['greeting_message']);
|
||||
// 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');
|
||||
}
|
||||
} else {
|
||||
$say = $twiml->addChild('Say', $step['greeting_message']);
|
||||
$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('transcribe', 'true');
|
||||
$record->addAttribute('transcribeCallback', home_url('/wp-json/twilio-webhook/v1/transcription'));
|
||||
$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');
|
||||
$callback_url = add_query_arg('workflow_id', $step['workflow_id'], $callback_url);
|
||||
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');
|
||||
|
||||
return $twiml->asXML();
|
||||
// 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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -323,44 +731,141 @@ class TWP_Workflow {
|
||||
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;
|
||||
}
|
||||
|
||||
$routing = TWP_Scheduler::get_schedule_routing($schedule_id);
|
||||
// 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 ($routing['action'] === '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']) {
|
||||
// Forward call
|
||||
$twiml = new \Twilio\TwiML\VoiceResponse();
|
||||
$twiml->dial($routing['data']['forward_number']);
|
||||
return $twiml->asXML();
|
||||
}
|
||||
|
||||
// Fallback to legacy behavior if new routing doesn't work
|
||||
if (TWP_Scheduler::is_schedule_active($schedule_id)) {
|
||||
// Execute in-hours action
|
||||
if (isset($step['in_hours_action'])) {
|
||||
return self::execute_action($step['in_hours_action'], $call_data);
|
||||
}
|
||||
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 {
|
||||
// Execute after-hours action
|
||||
if (isset($step['after_hours_action'])) {
|
||||
return self::execute_action($step['after_hours_action'], $call_data);
|
||||
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('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
|
||||
|
||||
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
|
||||
*/
|
||||
@@ -427,13 +932,17 @@ class TWP_Workflow {
|
||||
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);
|
||||
$twilio->send_sms($step['to_number'], $message, $from_number);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -492,7 +1001,12 @@ class TWP_Workflow {
|
||||
}
|
||||
|
||||
if (isset($data['workflow_data'])) {
|
||||
$update_data['workflow_data'] = json_encode($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';
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user