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

@@ -217,6 +217,27 @@ class TWP_Webhooks {
'callback' => array($this, 'handle_agent_action'),
'permission_callback' => '__return_true'
));
// Conference status webhook
register_rest_route('twilio-webhook/v1', '/conference-status', array(
'methods' => 'POST',
'callback' => array($this, 'handle_conference_status'),
'permission_callback' => '__return_true'
));
// Agent conference join webhook
register_rest_route('twilio-webhook/v1', '/agent-conference-join', array(
'methods' => 'POST',
'callback' => array($this, 'handle_agent_conference_join'),
'permission_callback' => '__return_true'
));
// Agent call status webhook (for conference calls)
register_rest_route('twilio-webhook/v1', '/agent-call-status-new', array(
'methods' => 'POST',
'callback' => array($this, 'handle_agent_call_status_new'),
'permission_callback' => '__return_true'
));
// Request callback webhook
register_rest_route('twilio-webhook/v1', '/request-callback', array(
@@ -2842,4 +2863,129 @@ class TWP_Webhooks {
return $this->send_twiml_response($response->asXML());
}
/**
* Handle conference status events
*/
public function handle_conference_status($request) {
$params = $request->get_params();
error_log('TWP Conference Status: ' . print_r($params, true));
$status_callback_event = isset($params['StatusCallbackEvent']) ? $params['StatusCallbackEvent'] : '';
$conference_sid = isset($params['ConferenceSid']) ? $params['ConferenceSid'] : '';
$friendly_name = isset($params['FriendlyName']) ? $params['FriendlyName'] : '';
// Log conference events for debugging
switch ($status_callback_event) {
case 'conference-start':
error_log('TWP Conference: Conference started: ' . $friendly_name);
break;
case 'participant-join':
error_log('TWP Conference: Participant joined: ' . $friendly_name);
break;
case 'participant-leave':
error_log('TWP Conference: Participant left: ' . $friendly_name);
break;
case 'conference-end':
error_log('TWP Conference: Conference ended: ' . $friendly_name);
break;
}
return new WP_REST_Response('OK', 200);
}
/**
* Handle agent joining conference with features
*/
public function handle_agent_conference_join($request) {
$params = $request->get_params();
error_log('TWP Agent Conference Join: ' . print_r($params, true));
$conference_name = isset($_GET['conference_name']) ? $_GET['conference_name'] : '';
$caller_number = isset($_GET['caller_number']) ? $_GET['caller_number'] : '';
$response = new \Twilio\TwiML\VoiceResponse();
if (empty($conference_name)) {
$response->say('Conference not found', ['voice' => 'alice']);
$response->hangup();
return $this->send_twiml_response($response->asXML());
}
// Announce the incoming call to the agent
if (!empty($caller_number)) {
$response->say('Incoming call from ' . $this->format_phone_number_for_speech($caller_number), ['voice' => 'alice']);
} else {
$response->say('Incoming call', ['voice' => 'alice']);
}
// Set up agent conference with features
$dial = $response->dial();
$conference = $dial->conference($conference_name, [
'startConferenceOnEnter' => true, // Start when agent joins
'endConferenceOnExit' => false, // Don't end when agent leaves (let caller stay)
'muted' => false,
'beep' => false,
'waitUrl' => '',
// Enable DTMF detection for agent features
'eventCallbackUrl' => home_url('/wp-json/twilio-webhook/v1/conference-events'),
'record' => false // We'll control recording via DTMF
]);
error_log('TWP Agent Conference Join: Joining agent to conference: ' . $conference_name);
return $this->send_twiml_response($response->asXML());
}
/**
* Handle agent call status for conference calls
*/
public function handle_agent_call_status_new($request) {
$params = $request->get_params();
error_log('TWP Agent Call Status: ' . print_r($params, true));
$call_status = isset($params['CallStatus']) ? $params['CallStatus'] : '';
$call_sid = isset($params['CallSid']) ? $params['CallSid'] : '';
switch ($call_status) {
case 'initiated':
error_log('TWP Agent Call: Call initiated to agent: ' . $call_sid);
break;
case 'ringing':
error_log('TWP Agent Call: Agent phone ringing: ' . $call_sid);
break;
case 'answered':
error_log('TWP Agent Call: Agent answered: ' . $call_sid);
break;
case 'completed':
error_log('TWP Agent Call: Agent call completed: ' . $call_sid);
break;
case 'busy':
case 'no-answer':
case 'failed':
error_log('TWP Agent Call: Agent call failed (' . $call_status . '): ' . $call_sid);
// TODO: Could implement fallback logic here (try next agent, voicemail, etc.)
break;
}
return new WP_REST_Response('OK', 200);
}
/**
* Format phone number for speech (adds pauses between digits)
*/
private function format_phone_number_for_speech($number) {
// Remove +1 country code and format for speech
$cleaned = preg_replace('/^\+1/', '', $number);
if (strlen($cleaned) == 10) {
// Format as (xxx) xxx-xxxx with pauses
return substr($cleaned, 0, 3) . ' ' . substr($cleaned, 3, 3) . ' ' . substr($cleaned, 6, 4);
}
return $number;
}
}