code revision

This commit is contained in:
2025-08-06 15:25:47 -07:00
parent fc220beeac
commit c6edbbeba7
19 changed files with 10039 additions and 0 deletions

View File

@@ -0,0 +1,527 @@
<?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();
// 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('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
if (isset($step['use_tts']) && $step['use_tts']) {
// Generate TTS audio
$audio_result = $elevenlabs->text_to_speech($step['message']);
if ($audio_result['success']) {
$play = $twiml->addChild('Play', $audio_result['file_url']);
} else {
$say = $twiml->addChild('Say', $step['message']);
$say->addAttribute('voice', 'alice');
}
} 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('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
$gather = $twiml->addChild('Gather');
$gather->addAttribute('numDigits', isset($step['num_digits']) ? $step['num_digits'] : '1');
$gather->addAttribute('timeout', isset($step['timeout']) ? $step['timeout'] : '10');
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('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
$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('<?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');
}
$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('<?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');
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('<?xml version="1.0" encoding="UTF-8"?><Response></Response>');
if (isset($step['greeting_message'])) {
if (isset($step['use_tts']) && $step['use_tts']) {
$audio_result = $elevenlabs->text_to_speech($step['greeting_message']);
if ($audio_result['success']) {
$play = $twiml->addChild('Play', $audio_result['file_url']);
} else {
$say = $twiml->addChild('Say', $step['greeting_message']);
$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('<?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();
$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('<?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");
}
/**
* 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')
);
}
}