progress made

This commit is contained in:
2025-08-11 20:31:48 -07:00
parent 805af2f199
commit 304b5de40b
15 changed files with 4028 additions and 404 deletions

View File

@@ -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';
}