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