code revision
This commit is contained in:
527
includes/class-twp-workflow.php
Normal file
527
includes/class-twp-workflow.php
Normal 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')
|
||||
);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user