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(); // Process workflow steps foreach ($workflow_data['steps'] as $step) { switch ($step['type']) { case 'greeting': $twiml = self::create_greeting_twiml($step, $elevenlabs); break; case 'ivr_menu': $twiml = self::create_ivr_menu_twiml($step, $elevenlabs); break; case 'forward': $twiml = self::create_forward_twiml($step); break; case 'queue': $twiml = self::create_queue_twiml($step); break; case 'ring_group': $twiml = self::create_ring_group_twiml($step); break; case 'voicemail': $twiml = self::create_voicemail_twiml($step, $elevenlabs); break; case 'schedule_check': $twiml = self::handle_schedule_check($step, $call_data); break; case 'sms': self::send_sms_notification($step, $call_data); continue 2; default: continue 2; } // Check conditions if (isset($step['conditions'])) { if (!self::check_conditions($step['conditions'], $call_data)) { continue; } } // Execute step if ($twiml) { return $twiml; } } // Default response return self::create_default_response(); } /** * Create greeting TwiML */ private static function create_greeting_twiml($step, $elevenlabs) { $twiml = new SimpleXMLElement(''); 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'); } } else { $say = $twiml->addChild('Say', $step['message']); $say->addAttribute('voice', 'alice'); } 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['num_digits']) ? $step['num_digits'] : '1'); $gather->addAttribute('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 = 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'); } } else { $say = $gather->addChild('Say', $step['message']); $say->addAttribute('voice', 'alice'); } // 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->addChild('Number', $step['forward_number']); } break; } } return $twiml->asXML(); } /** * Create forward TwiML */ private static function create_forward_twiml($step) { $twiml = new SimpleXMLElement(''); $dial = $twiml->addChild('Dial'); 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) { $twiml = new SimpleXMLElement(''); if (isset($step['announce_message'])) { $say = $twiml->addChild('Say', $step['announce_message']); $say->addAttribute('voice', 'alice'); } $enqueue = $twiml->addChild('Enqueue', $step['queue_name']); if (isset($step['wait_url'])) { $enqueue->addAttribute('waitUrl', $step['wait_url']); } 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); } return $twiml->asXML(); } /** * 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'); if (isset($step['timeout'])) { $dial->addAttribute('timeout', $step['timeout']); } else { $dial->addAttribute('timeout', '30'); } if (isset($step['caller_id'])) { $dial->addAttribute('callerId', $step['caller_id']); } // Set action URL to handle no-answer scenarios $action_url = home_url('/wp-json/twilio-webhook/v1/ring-group-result?' . http_build_query([ 'group_id' => $group_id, 'queue_name' => isset($step['queue_name']) ? $step['queue_name'] : null, 'fallback_action' => isset($step['fallback_action']) ? $step['fallback_action'] : 'queue' ])); $dial->addAttribute('action', $action_url); // Add all group numbers for simultaneous ring foreach ($phone_numbers as $number) { if (!empty($number)) { $dial->addChild('Number', $number); } } return $twiml->asXML(); } /** * Create voicemail TwiML */ private static function create_voicemail_twiml($step, $elevenlabs) { $twiml = new SimpleXMLElement(''); 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']); $say->addAttribute('voice', 'alice'); } } else { $say = $twiml->addChild('Say', $step['greeting_message']); $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')); $callback_url = home_url('/wp-json/twilio-webhook/v1/voicemail-callback'); $callback_url = add_query_arg('workflow_id', $step['workflow_id'], $callback_url); $record->addAttribute('recordingStatusCallback', $callback_url); return $twiml->asXML(); } /** * Handle schedule check */ private static function handle_schedule_check($step, $call_data) { $schedule_id = $step['data']['schedule_id'] ?? $step['schedule_id'] ?? null; if (!$schedule_id) { // No schedule specified, return false to continue to next step return false; } $routing = TWP_Scheduler::get_schedule_routing($schedule_id); 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(); $dial = $twiml->dial(); $dial->number($routing['data']['forward_number']); return $twiml; } // 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); } } else { // Execute after-hours action if (isset($step['after_hours_action'])) { return self::execute_action($step['after_hours_action'], $call_data); } } return false; } /** * Execute action */ private static function execute_action($action, $call_data) { switch ($action['type']) { case 'forward': return self::create_forward_twiml($action); case 'voicemail': $elevenlabs = new TWP_ElevenLabs_API(); return self::create_voicemail_twiml($action, $elevenlabs); case 'queue': return self::create_queue_twiml($action); case 'ring_group': return self::create_ring_group_twiml($action); case 'message': $twiml = new SimpleXMLElement(''); $say = $twiml->addChild('Say', $action['message']); $say->addAttribute('voice', 'alice'); return $twiml->asXML(); default: return false; } } /** * Check conditions */ private static function check_conditions($conditions, $call_data) { foreach ($conditions as $condition) { switch ($condition['type']) { case 'time': $current_time = current_time('H:i'); if ($current_time < $condition['start_time'] || $current_time > $condition['end_time']) { return false; } break; case 'day_of_week': $current_day = strtolower(date('l')); if (!in_array($current_day, $condition['days'])) { return false; } break; case 'caller_id': if (!in_array($call_data['From'], $condition['numbers'])) { return false; } break; } } return true; } /** * Send SMS notification */ private static function send_sms_notification($step, $call_data) { $twilio = new TWP_Twilio_API(); $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); } /** * 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"); } /** * 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'])) { $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') ); } }