Switch to conference-based forwarding with agent features

Replaced problematic Number URL approach with conference-based forwarding to eliminate the "call cannot be completed" issue.

Key improvements:
- Forward calls now use Conference instead of direct Dial with URL
- Caller is placed in conference with hold music while waiting for agent
- Agent receives outbound call to join conference with proper caller ID
- Agent hears "Incoming call from XXX XXX XXXX" announcement
- Conference-based architecture enables future DTMF features
- Proper call flow without TwiML interference

Technical details:
- Added conference status monitoring webhooks
- Agent call includes proper caller announcement
- Conference starts when agent joins, ends when caller leaves
- Hold music plays while waiting for agent
- Eliminated URL attribute on Number elements that caused audio issues
- Added Conference element support in append_twiml_element function

This resolves the voicemail and "call cannot be completed" issues while maintaining call forwarding functionality and preparing for advanced agent features.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-09-18 18:41:24 -07:00
parent e475e68a5f
commit 349840840b
2 changed files with 242 additions and 41 deletions

View File

@@ -233,6 +233,9 @@ class TWP_Workflow {
case 'Queue':
$response->queue((string) $element, $attributes);
break;
case 'Conference':
$response->dial()->conference((string) $element, $attributes);
break;
case 'Redirect':
$response->redirect((string) $element, $attributes);
break;
@@ -564,56 +567,108 @@ class TWP_Workflow {
error_log('TWP Workflow Forward: Forwarding to numbers: ' . implode(', ', $forward_numbers));
$dial = $twiml->addChild('Dial');
$dial->addAttribute('answerOnBridge', 'true');
// Check if we should use conference mode with agent features
$use_conference_mode = isset($step_data['enable_agent_features']) ? $step_data['enable_agent_features'] : true;
// Set timeout (default to 30 seconds if not specified)
$timeout = isset($step_data['timeout']) ? $step_data['timeout'] :
(isset($step['timeout']) ? $step['timeout'] : '30');
$dial->addAttribute('timeout', $timeout);
error_log('TWP Workflow Forward: Using timeout: ' . $timeout);
if ($use_conference_mode) {
// Use conference mode for better call control
error_log('TWP Workflow Forward: Using conference mode for forwarding');
// Set caller ID to the number that was called (To number from call data)
// This makes the outbound call appear to come from the number the caller dialed
$caller_id = null;
if (isset($GLOBALS['call_data']['To']) && !empty($GLOBALS['call_data']['To'])) {
$caller_id = $GLOBALS['call_data']['To'];
error_log('TWP Workflow Forward: Using incoming number as caller ID: ' . $caller_id);
} elseif (isset($_POST['To']) && !empty($_POST['To'])) {
$caller_id = $_POST['To'];
error_log('TWP Workflow Forward: Using POST To as caller ID: ' . $caller_id);
} elseif (isset($_REQUEST['To']) && !empty($_REQUEST['To'])) {
$caller_id = $_REQUEST['To'];
error_log('TWP Workflow Forward: Using REQUEST To as caller ID: ' . $caller_id);
}
// Generate a unique conference name using call SID
$call_sid = isset($_POST['CallSid']) ? $_POST['CallSid'] :
(isset($_REQUEST['CallSid']) ? $_REQUEST['CallSid'] :
(isset($GLOBALS['call_data']['CallSid']) ? $GLOBALS['call_data']['CallSid'] : uniqid('conf_')));
$conference_name = 'Forward_' . $call_sid;
// Put the caller into a conference
$dial = $twiml->addChild('Dial');
$dial->addAttribute('action', home_url('/wp-json/twilio-webhook/v1/forward-result'));
$dial->addAttribute('method', 'POST');
$conference = $dial->addChild('Conference', $conference_name);
$conference->addAttribute('startConferenceOnEnter', 'false'); // Wait for agent
$conference->addAttribute('endConferenceOnExit', 'true'); // End when caller leaves
$conference->addAttribute('waitUrl', 'http://twimlets.com/holdmusic?Bucket=com.twilio.music.classical');
$conference->addAttribute('beep', 'false');
// Add event callbacks to monitor conference
$conference->addAttribute('statusCallback', home_url('/wp-json/twilio-webhook/v1/conference-status'));
$conference->addAttribute('statusCallbackEvent', 'start join leave end');
$conference->addAttribute('statusCallbackMethod', 'POST');
error_log('TWP Workflow Forward: Placing caller in conference: ' . $conference_name);
// Immediately call the agent to join the conference
// We'll use the Twilio API to make an outbound call
$twilio = new TWP_Twilio_API();
// Set caller ID to the business number
$caller_id = isset($_POST['To']) ? $_POST['To'] : null;
if (!$caller_id && isset($GLOBALS['call_data']['To'])) {
$caller_id = $GLOBALS['call_data']['To'];
}
// Create TwiML for the agent call that joins them to conference with features
$agent_twiml_url = home_url('/wp-json/twilio-webhook/v1/agent-conference-join?' . http_build_query([
'conference_name' => $conference_name,
'caller_number' => isset($_POST['From']) ? $_POST['From'] : ''
]));
try {
error_log('TWP Workflow Forward: Calling agent ' . $forward_numbers[0] . ' to join conference');
$agent_call = $twilio->get_client()->calls->create(
$forward_numbers[0], // to
$caller_id, // from (business number)
[
'url' => $agent_twiml_url,
'method' => 'POST',
'statusCallback' => home_url('/wp-json/twilio-webhook/v1/agent-call-status'),
'statusCallbackEvent' => ['initiated', 'ringing', 'answered', 'completed'],
'statusCallbackMethod' => 'POST'
]
);
error_log('TWP Workflow Forward: Agent call created with SID: ' . $agent_call->sid);
} catch (Exception $e) {
error_log('TWP Workflow Forward: Failed to create agent call: ' . $e->getMessage());
}
if ($caller_id) {
$dial->addAttribute('callerId', $caller_id);
error_log('TWP Workflow Forward: Set callerId attribute to: ' . $caller_id);
} else {
error_log('TWP Workflow Forward: No To number found for caller ID, will use account default');
}
// Use standard dial forwarding (simpler but less features)
error_log('TWP Workflow Forward: Using standard dial forwarding');
// Check if we should use bridging with agent features
$use_bridge_features = isset($step_data['enable_agent_features']) ? $step_data['enable_agent_features'] : true;
$dial = $twiml->addChild('Dial');
$dial->addAttribute('answerOnBridge', 'true');
// Add action URL to handle dial result (for tracking and potential fallback)
$action_url = home_url('/wp-json/twilio-webhook/v1/forward-result');
$dial->addAttribute('action', $action_url);
$dial->addAttribute('method', 'POST');
// Set timeout (default to 30 seconds if not specified)
$timeout = isset($step_data['timeout']) ? $step_data['timeout'] :
(isset($step['timeout']) ? $step['timeout'] : '30');
$dial->addAttribute('timeout', $timeout);
// Add all forward numbers with agent features if enabled
foreach ($forward_numbers as $number) {
error_log('TWP Workflow Forward: Adding number to Dial: ' . $number);
$number_element = $dial->addChild('Number', $number);
// Set caller ID to the number that was called
$caller_id = null;
if (isset($GLOBALS['call_data']['To']) && !empty($GLOBALS['call_data']['To'])) {
$caller_id = $GLOBALS['call_data']['To'];
} elseif (isset($_POST['To']) && !empty($_POST['To'])) {
$caller_id = $_POST['To'];
} elseif (isset($_REQUEST['To']) && !empty($_REQUEST['To'])) {
$caller_id = $_REQUEST['To'];
}
if ($use_bridge_features) {
// Add URL to handle agent-side features (DTMF detection)
$agent_url = home_url('/wp-json/twilio-webhook/v1/agent-features');
$number_element->addAttribute('url', $agent_url);
$number_element->addAttribute('method', 'POST');
if ($caller_id) {
$dial->addAttribute('callerId', $caller_id);
}
error_log('TWP Workflow Forward: Added agent features URL for DTMF detection');
// Add action URL to handle dial result
$dial->addAttribute('action', home_url('/wp-json/twilio-webhook/v1/forward-result'));
$dial->addAttribute('method', 'POST');
// Add all forward numbers
foreach ($forward_numbers as $number) {
error_log('TWP Workflow Forward: Adding number to Dial: ' . $number);
$dial->addChild('Number', $number);
}
}