Fixed critical bug where forward tasks in workflows would immediately disconnect calls instead of forwarding them properly. Changes: - Fixed append_twiml_element function to properly handle Dial elements with child Number elements - Enhanced create_forward_twiml to extract numbers from nested data structures - Added comprehensive error handling for missing forward numbers - Added detailed logging throughout workflow execution for debugging - Set default timeout of 30 seconds for forward operations The issue was caused by the Dial element being converted to string which lost all child Number elements, resulting in an empty dial that would immediately disconnect. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1307 lines
56 KiB
PHP
1307 lines
56 KiB
PHP
<?php
|
|
/**
|
|
* Workflow management class
|
|
*/
|
|
class TWP_Workflow {
|
|
|
|
/**
|
|
* Create workflow
|
|
*/
|
|
public static function create_workflow($data) {
|
|
global $wpdb;
|
|
$table_name = $wpdb->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':
|
|
error_log('TWP Workflow: Processing forward step: ' . json_encode($step));
|
|
$step_twiml = self::create_forward_twiml($step);
|
|
error_log('TWP Workflow: Forward step TwiML generated: ' . $step_twiml);
|
|
$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) {
|
|
error_log('TWP Workflow: Appending step TwiML to combined response');
|
|
error_log('TWP Workflow: Step TwiML before append: ' . $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;
|
|
|
|
error_log('TWP Workflow: Combined response after append: ' . $response->asXML());
|
|
} else {
|
|
error_log('TWP Workflow: ERROR - Failed to parse step TwiML: ' . $step_twiml);
|
|
}
|
|
|
|
// Stop processing if this step type should end the workflow
|
|
if ($stop_after_step) {
|
|
error_log('TWP Workflow: Stopping after this step (stop_after_step = true)');
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Return combined response or default
|
|
if ($has_response) {
|
|
$final_twiml = $response->asXML();
|
|
error_log('TWP Workflow: Final workflow TwiML response: ' . $final_twiml);
|
|
return $final_twiml;
|
|
}
|
|
|
|
// Default response
|
|
error_log('TWP Workflow: No response generated, returning 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':
|
|
// Create dial instance
|
|
$dial = $response->dial('', $attributes);
|
|
// Add child Number elements
|
|
foreach ($element->children() as $child) {
|
|
$child_name = $child->getName();
|
|
if ($child_name === 'Number') {
|
|
$dial->number((string) $child, self::get_attributes($child));
|
|
} elseif ($child_name === 'Client') {
|
|
$dial->client((string) $child, self::get_attributes($child));
|
|
} elseif ($child_name === 'Queue') {
|
|
$dial->queue((string) $child, self::get_attributes($child));
|
|
} elseif ($child_name === 'Conference') {
|
|
$dial->conference((string) $child, self::get_attributes($child));
|
|
} elseif ($child_name === 'Sip') {
|
|
$dial->sip((string) $child, self::get_attributes($child));
|
|
}
|
|
}
|
|
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>');
|
|
|
|
// 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('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
|
|
|
|
$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('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
|
|
|
|
// Get announcement message
|
|
$message = '';
|
|
if (isset($step_data['announce_message']) && !empty($step_data['announce_message'])) {
|
|
$message = $step_data['announce_message'];
|
|
} else {
|
|
$message = 'Connecting you to our agent.';
|
|
}
|
|
|
|
// Handle audio type for announcement
|
|
$audio_type = isset($step_data['audio_type']) ? $step_data['audio_type'] : 'say';
|
|
|
|
switch ($audio_type) {
|
|
case 'tts':
|
|
$voice_id = isset($step_data['voice_id']) ? $step_data['voice_id'] : null;
|
|
$audio_result = $elevenlabs->text_to_speech($message, [
|
|
'voice_id' => $voice_id
|
|
]);
|
|
|
|
if ($audio_result['success']) {
|
|
$play = $twiml->addChild('Play', $audio_result['file_url']);
|
|
} else {
|
|
$say = $twiml->addChild('Say', $message);
|
|
$say->addAttribute('voice', 'alice');
|
|
}
|
|
break;
|
|
|
|
case 'audio':
|
|
$audio_url = isset($step_data['audio_url']) ? $step_data['audio_url'] : null;
|
|
|
|
if ($audio_url && !empty($audio_url)) {
|
|
$play = $twiml->addChild('Play', $audio_url);
|
|
} else {
|
|
$say = $twiml->addChild('Say', $message);
|
|
$say->addAttribute('voice', 'alice');
|
|
}
|
|
break;
|
|
|
|
default: // 'say'
|
|
$say = $twiml->addChild('Say', $message);
|
|
$say->addAttribute('voice', 'alice');
|
|
break;
|
|
}
|
|
|
|
// Dial browser clients
|
|
$dial = $twiml->addChild('Dial');
|
|
$dial->addAttribute('timeout', '30');
|
|
|
|
// Get browser client names (agents who might be online)
|
|
$browser_clients = isset($step_data['browser_clients']) ? $step_data['browser_clients'] : [];
|
|
|
|
if (empty($browser_clients)) {
|
|
// Default: try to call any available browser clients
|
|
// Get all users with agent capabilities
|
|
$agents = get_users(array(
|
|
'meta_key' => 'twp_phone_number',
|
|
'meta_compare' => 'EXISTS'
|
|
));
|
|
|
|
foreach ($agents as $agent) {
|
|
// Twilio requires alphanumeric characters only
|
|
$clean_name = preg_replace('/[^a-zA-Z0-9]/', '', $agent->display_name);
|
|
if (empty($clean_name)) {
|
|
$clean_name = 'user';
|
|
}
|
|
$client_name = 'agent' . $agent->ID . $clean_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) {
|
|
error_log('TWP Workflow Forward: Creating forward TwiML with step data: ' . print_r($step, true));
|
|
|
|
$twiml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
|
|
|
|
// Check if data is nested (workflow steps have data nested)
|
|
$step_data = isset($step['data']) ? $step['data'] : $step;
|
|
|
|
// Get the forward number(s) from the proper location
|
|
$forward_numbers = array();
|
|
|
|
// Check various possible locations for forward numbers
|
|
if (isset($step_data['forward_numbers']) && is_array($step_data['forward_numbers'])) {
|
|
$forward_numbers = $step_data['forward_numbers'];
|
|
error_log('TWP Workflow Forward: Found forward_numbers array in data: ' . print_r($forward_numbers, true));
|
|
} elseif (isset($step_data['forward_number']) && !empty($step_data['forward_number'])) {
|
|
$forward_numbers = array($step_data['forward_number']);
|
|
error_log('TWP Workflow Forward: Found single forward_number in data: ' . $step_data['forward_number']);
|
|
} elseif (isset($step_data['number']) && !empty($step_data['number'])) {
|
|
$forward_numbers = array($step_data['number']);
|
|
error_log('TWP Workflow Forward: Found number in data: ' . $step_data['number']);
|
|
} elseif (isset($step['forward_numbers']) && is_array($step['forward_numbers'])) {
|
|
$forward_numbers = $step['forward_numbers'];
|
|
error_log('TWP Workflow Forward: Found forward_numbers array: ' . print_r($forward_numbers, true));
|
|
} elseif (isset($step['forward_number']) && !empty($step['forward_number'])) {
|
|
$forward_numbers = array($step['forward_number']);
|
|
error_log('TWP Workflow Forward: Found single forward_number: ' . $step['forward_number']);
|
|
} elseif (isset($step['number']) && !empty($step['number'])) {
|
|
$forward_numbers = array($step['number']);
|
|
error_log('TWP Workflow Forward: Found number: ' . $step['number']);
|
|
}
|
|
|
|
// Filter out empty numbers
|
|
$forward_numbers = array_filter($forward_numbers, function($num) {
|
|
return !empty($num);
|
|
});
|
|
|
|
if (empty($forward_numbers)) {
|
|
error_log('TWP Workflow Forward: ERROR - No forward numbers found in step data');
|
|
// Return error message instead of empty dial
|
|
$say = $twiml->addChild('Say', 'Sorry, the forwarding destination is not configured. Please try again later.');
|
|
$say->addAttribute('voice', 'alice');
|
|
$twiml->addChild('Hangup');
|
|
return $twiml->asXML();
|
|
}
|
|
|
|
error_log('TWP Workflow Forward: Forwarding to numbers: ' . implode(', ', $forward_numbers));
|
|
|
|
$dial = $twiml->addChild('Dial');
|
|
$dial->addAttribute('answerOnBridge', 'true');
|
|
|
|
// Set timeout (default to 30 seconds if not specified)
|
|
$timeout = isset($step_data['timeout']) ? $step_data['timeout'] :
|
|
(isset($step['timeout']) ? $step['timeout'] : '30');
|
|
$dial->addAttribute('timeout', $timeout);
|
|
error_log('TWP Workflow Forward: Using timeout: ' . $timeout);
|
|
|
|
// Add all forward numbers
|
|
foreach ($forward_numbers as $number) {
|
|
error_log('TWP Workflow Forward: Adding number to Dial: ' . $number);
|
|
$dial->addChild('Number', $number);
|
|
}
|
|
|
|
$result = $twiml->asXML();
|
|
error_log('TWP Workflow Forward: Final Forward TwiML: ' . $result);
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* 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('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
|
|
|
|
// 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('<?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');
|
|
}
|
|
|
|
// 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
|
|
*/
|
|
public static function create_voicemail_twiml($step, $elevenlabs) {
|
|
$twiml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
|
|
|
|
// 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('<?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
|
|
*/
|
|
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':
|
|
$elevenlabs = new TWP_ElevenLabs_API();
|
|
return self::create_queue_twiml($action, $elevenlabs);
|
|
|
|
case 'ring_group':
|
|
return self::create_ring_group_twiml($action);
|
|
|
|
case 'message':
|
|
$twiml = new SimpleXMLElement('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
|
|
$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('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
|
|
$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
|
|
* Now checks both the legacy phone_number field and the new junction table
|
|
*/
|
|
public static function get_workflow_by_phone_number($phone_number) {
|
|
global $wpdb;
|
|
$workflows_table = $wpdb->prefix . 'twp_workflows';
|
|
$phones_table = $wpdb->prefix . 'twp_workflow_phones';
|
|
|
|
// First check the new junction table
|
|
$workflow = $wpdb->get_row($wpdb->prepare(
|
|
"SELECT w.* FROM $workflows_table w
|
|
INNER JOIN $phones_table p ON w.id = p.workflow_id
|
|
WHERE p.phone_number = %s AND w.is_active = 1
|
|
ORDER BY w.created_at DESC LIMIT 1",
|
|
$phone_number
|
|
));
|
|
|
|
// If not found, fall back to legacy phone_number field
|
|
if (!$workflow) {
|
|
$workflow = $wpdb->get_row($wpdb->prepare(
|
|
"SELECT * FROM $workflows_table WHERE phone_number = %s AND is_active = 1 ORDER BY created_at DESC LIMIT 1",
|
|
$phone_number
|
|
));
|
|
}
|
|
|
|
return $workflow;
|
|
}
|
|
|
|
/**
|
|
* 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';
|
|
$phones_table = $wpdb->prefix . 'twp_workflow_phones';
|
|
|
|
// Delete phone numbers first
|
|
$wpdb->delete($phones_table, array('workflow_id' => $workflow_id), array('%d'));
|
|
|
|
// Then delete workflow
|
|
return $wpdb->delete(
|
|
$table_name,
|
|
array('id' => $workflow_id),
|
|
array('%d')
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Set phone numbers for a workflow
|
|
*/
|
|
public static function set_workflow_phone_numbers($workflow_id, $phone_numbers) {
|
|
global $wpdb;
|
|
$phones_table = $wpdb->prefix . 'twp_workflow_phones';
|
|
|
|
// Remove existing phone numbers for this workflow
|
|
$wpdb->delete($phones_table, array('workflow_id' => $workflow_id));
|
|
|
|
// Add new phone numbers
|
|
foreach ($phone_numbers as $phone_number) {
|
|
if (!empty($phone_number)) {
|
|
$wpdb->insert(
|
|
$phones_table,
|
|
array(
|
|
'workflow_id' => $workflow_id,
|
|
'phone_number' => $phone_number
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Get phone numbers for a workflow
|
|
*/
|
|
public static function get_workflow_phone_numbers($workflow_id) {
|
|
global $wpdb;
|
|
$phones_table = $wpdb->prefix . 'twp_workflow_phones';
|
|
|
|
$numbers = $wpdb->get_col($wpdb->prepare(
|
|
"SELECT phone_number FROM $phones_table WHERE workflow_id = %d",
|
|
$workflow_id
|
|
));
|
|
|
|
// If no numbers in junction table, check legacy field
|
|
if (empty($numbers)) {
|
|
$workflows_table = $wpdb->prefix . 'twp_workflows';
|
|
$legacy_number = $wpdb->get_var($wpdb->prepare(
|
|
"SELECT phone_number FROM $workflows_table WHERE id = %d",
|
|
$workflow_id
|
|
));
|
|
if ($legacy_number) {
|
|
$numbers = array($legacy_number);
|
|
// Optionally migrate to new structure
|
|
self::set_workflow_phone_numbers($workflow_id, $numbers);
|
|
}
|
|
}
|
|
|
|
return $numbers;
|
|
}
|
|
} |