527 lines
18 KiB
PHP
527 lines
18 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();
|
||
|
|
||
|
// 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')
|
||
|
);
|
||
|
}
|
||
|
}
|