Fix critical hold issue preventing call disconnections

- Enhanced call leg detection logic for browser phone calls
- Added comprehensive logging for debugging hold/resume operations
- Fixed hold to properly identify customer vs agent call legs
- Added fallback mechanisms when call relationships are unclear
- Improved resume logic to match hold behavior
- Prevents customer disconnections when agent puts call on hold

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-08-30 16:20:16 -07:00
parent 315a774447
commit 3f1d115aa0
2 changed files with 257 additions and 53 deletions

View File

@@ -6963,69 +6963,173 @@ class TWP_Admin {
// Get the call details to understand the call structure
$call = $client->calls($call_sid)->fetch();
error_log("TWP Hold: Call SID $call_sid - From: {$call->from}, To: {$call->to}, Status: {$call->status}");
error_log("TWP Hold: Initial call SID $call_sid - From: {$call->from}, To: {$call->to}, Direction: {$call->direction}, Parent: {$call->parentCallSid}");
// For browser phone calls, we need to find the parent call or customer leg
if (strpos($call->to, 'client:') === 0) {
// This is the browser client leg - we need to find the customer leg instead
// Look for the parent call or related calls
$conference_sid = $call->parentCallSid;
// Determine which call to put on hold
$target_sid = null;
// Check if this is a browser phone (client) call
if (strpos($call->to, 'client:') === 0 || strpos($call->from, 'client:') === 0) {
// This is the browser phone leg - we need to find the customer leg
error_log("TWP Hold: Detected browser phone call, looking for customer leg");
if ($conference_sid) {
// Use the parent call (which should be the customer)
$customer_call = $client->calls($conference_sid)->fetch();
error_log("TWP Hold: Found parent call $conference_sid - From: {$customer_call->from}, To: {$customer_call->to}");
// For outbound calls from browser phone, the structure is usually:
// - Browser phone initiates call (this call)
// - System dials customer (separate call with same parent or as child)
// First check if there's a parent call that might be a conference
if ($call->parentCallSid) {
try {
$parent_call = $client->calls($call->parentCallSid)->fetch();
error_log("TWP Hold: Parent call {$parent_call->sid} - From: {$parent_call->from}, To: {$parent_call->to}");
// Check if parent is the customer call (not a client call)
if (strpos($parent_call->to, 'client:') === false && strpos($parent_call->from, 'client:') === false) {
$target_sid = $parent_call->sid;
error_log("TWP Hold: Using parent call as target");
}
} catch (Exception $e) {
error_log("TWP Hold: Could not fetch parent: " . $e->getMessage());
}
}
// If no target found yet, search for related calls
if (!$target_sid) {
// Get all in-progress calls and find the related customer call
$active_calls = $client->calls->read(['status' => 'in-progress'], 50);
error_log("TWP Hold: Searching " . count($active_calls) . " active calls for customer leg");
$twiml = new \Twilio\TwiML\VoiceResponse();
$twiml->say('Please hold while we assist you.', ['voice' => 'alice']);
$twiml->play($hold_music_url, ['loop' => 0]);
$client->calls($conference_sid)->update([
'twiml' => $twiml->asXML()
]);
} else {
// Fallback: put this call on hold (might be wrong leg but better than nothing)
$twiml = new \Twilio\TwiML\VoiceResponse();
$twiml->play($hold_music_url, ['loop' => 0]);
$client->calls($call_sid)->update([
'twiml' => $twiml->asXML()
]);
foreach ($active_calls as $active_call) {
// Skip the current call
if ($active_call->sid === $call_sid) continue;
// Log each call for debugging
error_log("TWP Hold: Checking call {$active_call->sid} - From: {$active_call->from}, To: {$active_call->to}, Parent: {$active_call->parentCallSid}");
// Check if this call is related (same parent, or parent/child relationship)
$is_related = false;
if ($call->parentCallSid && $active_call->parentCallSid === $call->parentCallSid) {
$is_related = true; // Same parent
} elseif ($active_call->parentCallSid === $call_sid) {
$is_related = true; // This call is child of our call
} elseif ($active_call->sid === $call->parentCallSid) {
$is_related = true; // This call is parent of our call
}
if ($is_related) {
// Make sure this is not a client call
if (strpos($active_call->to, 'client:') === false &&
strpos($active_call->from, 'client:') === false) {
$target_sid = $active_call->sid;
error_log("TWP Hold: Found related customer call: {$active_call->sid}");
break;
}
}
}
}
} else {
// This appears to be the customer call - put it on hold
// This is not a client call, so it's likely the customer call itself
$target_sid = $call_sid;
error_log("TWP Hold: Using current call as target (not a client call)");
}
// Apply hold to the determined target
if ($target_sid) {
error_log("TWP Hold: Putting call on hold - Target SID: {$target_sid}");
// Create hold TwiML
$twiml = new \Twilio\TwiML\VoiceResponse();
$twiml->say('Please hold while we assist you.', ['voice' => 'alice']);
$twiml->play($hold_music_url, ['loop' => 0]);
try {
$result = $client->calls($target_sid)->update([
'twiml' => $twiml->asXML()
]);
error_log("TWP Hold: Successfully updated call {$target_sid} with hold music");
} catch (Exception $e) {
error_log("TWP Hold: Error updating call: " . $e->getMessage());
throw $e;
}
} else {
error_log("TWP Hold: WARNING - Could not determine target call, putting current call on hold as fallback");
// Fallback: put current call on hold
$twiml = new \Twilio\TwiML\VoiceResponse();
$twiml->say('Please hold.', ['voice' => 'alice']);
$twiml->play($hold_music_url, ['loop' => 0]);
$client->calls($call_sid)->update([
'twiml' => $twiml->asXML()
]);
}
} else {
// Resume call - similar logic to find the right leg
// Resume call - use similar logic to find the right leg
$call = $client->calls($call_sid)->fetch();
error_log("TWP Resume: Initial call SID $call_sid - From: {$call->from}, To: {$call->to}");
if (strpos($call->to, 'client:') === 0) {
$conference_sid = $call->parentCallSid;
if ($conference_sid) {
$twiml = new \Twilio\TwiML\VoiceResponse();
$twiml->say('Thank you for holding.', ['voice' => 'alice']);
// Empty TwiML after message allows call to continue
$client->calls($conference_sid)->update([
'twiml' => $twiml->asXML()
]);
} else {
$twiml = new \Twilio\TwiML\VoiceResponse();
$client->calls($call_sid)->update([
'twiml' => $twiml->asXML()
]);
$target_sid = null;
// Check if this is a browser phone call
if (strpos($call->to, 'client:') === 0 || strpos($call->from, 'client:') === 0) {
// Find the customer leg using same logic as hold
if ($call->parentCallSid) {
try {
$parent_call = $client->calls($call->parentCallSid)->fetch();
if (strpos($parent_call->to, 'client:') === false && strpos($parent_call->from, 'client:') === false) {
$target_sid = $parent_call->sid;
}
} catch (Exception $e) {
error_log("TWP Resume: Could not fetch parent: " . $e->getMessage());
}
}
if (!$target_sid) {
// Search for related customer call
$active_calls = $client->calls->read(['status' => 'in-progress'], 50);
foreach ($active_calls as $active_call) {
if ($active_call->sid === $call_sid) continue;
$is_related = false;
if ($call->parentCallSid && $active_call->parentCallSid === $call->parentCallSid) {
$is_related = true;
} elseif ($active_call->parentCallSid === $call_sid) {
$is_related = true;
} elseif ($active_call->sid === $call->parentCallSid) {
$is_related = true;
}
if ($is_related && strpos($active_call->to, 'client:') === false &&
strpos($active_call->from, 'client:') === false) {
$target_sid = $active_call->sid;
error_log("TWP Resume: Found related customer call: {$active_call->sid}");
break;
}
}
}
} else {
$twiml = new \Twilio\TwiML\VoiceResponse();
$twiml->say('Thank you for holding.', ['voice' => 'alice']);
$target_sid = $call_sid;
}
// Resume the determined target
if ($target_sid) {
error_log("TWP Resume: Resuming call - Target SID: {$target_sid}");
// Empty TwiML resumes the call
$twiml = new \Twilio\TwiML\VoiceResponse();
// No content - just empty response to resume
try {
$result = $client->calls($target_sid)->update([
'twiml' => $twiml->asXML()
]);
error_log("TWP Resume: Successfully resumed call {$target_sid}");
} catch (Exception $e) {
error_log("TWP Resume: Error resuming call: " . $e->getMessage());
throw $e;
}
} else {
error_log("TWP Resume: WARNING - Could not determine target, resuming current call");
$twiml = new \Twilio\TwiML\VoiceResponse();
$client->calls($call_sid)->update([
'twiml' => $twiml->asXML()
]);
@@ -7038,6 +7142,62 @@ class TWP_Admin {
}
}
/**
* AJAX handler for getting available agents for transfer
*/
public function ajax_get_transfer_agents() {
if (!$this->verify_ajax_nonce()) {
wp_send_json_error('Invalid nonce');
return;
}
global $wpdb;
$users_table = $wpdb->prefix . 'users';
$usermeta_table = $wpdb->prefix . 'usermeta';
$status_table = $wpdb->prefix . 'twp_agent_status';
// Get all users with the twp_access_browser_phone capability or admins
$all_users = get_users([
'orderby' => 'display_name',
'order' => 'ASC'
]);
$agents = [];
$current_user_id = get_current_user_id();
foreach ($all_users as $user) {
// Skip current user
if ($user->ID == $current_user_id) {
continue;
}
// Check if user can access browser phone or is admin
if (!user_can($user->ID, 'twp_access_browser_phone') && !user_can($user->ID, 'manage_options')) {
continue;
}
// Get user's phone number
$phone_number = get_user_meta($user->ID, 'twp_phone_number', true);
// Get user's status
$status = $wpdb->get_var($wpdb->prepare(
"SELECT status FROM $status_table WHERE user_id = %d",
$user->ID
));
$agents[] = [
'id' => $user->ID,
'name' => $user->display_name,
'phone' => $phone_number,
'status' => $status ?: 'offline',
'has_phone' => !empty($phone_number),
'queue_name' => 'agent_' . $user->ID // Personal queue name
];
}
wp_send_json_success($agents);
}
/**
* AJAX handler for transferring a call
*/
@@ -7048,22 +7208,65 @@ class TWP_Admin {
}
$call_sid = sanitize_text_field($_POST['call_sid']);
$agent_number = sanitize_text_field($_POST['agent_number']);
// Validate phone number format
if (!preg_match('/^\+?[1-9]\d{1,14}$/', $agent_number)) {
wp_send_json_error('Invalid phone number format');
return;
}
$transfer_type = sanitize_text_field($_POST['transfer_type']); // 'phone' or 'queue'
$transfer_target = sanitize_text_field($_POST['transfer_target']);
try {
$twilio = new TWP_Twilio_API();
$client = $twilio->get_client();
// Create TwiML to transfer the call
// Create TwiML based on transfer type
$twiml = new \Twilio\TwiML\VoiceResponse();
$twiml->say('Transferring your call. Please hold.');
$twiml->dial($agent_number);
if ($transfer_type === 'queue') {
// Transfer to agent's personal queue
$enqueue = $twiml->enqueue($transfer_target);
$enqueue->waitUrl(home_url('/wp-json/twilio-webhook/v1/queue-wait'));
// Extract agent ID from queue name (format: agent_123)
if (preg_match('/agent_(\d+)/', $transfer_target, $matches)) {
$agent_id = intval($matches[1]);
// Add to personal queue tracking in database
global $wpdb;
$table = $wpdb->prefix . 'twp_personal_queue_calls';
// Create table if it doesn't exist
$charset_collate = $wpdb->get_charset_collate();
$sql = "CREATE TABLE IF NOT EXISTS $table (
id int(11) NOT NULL AUTO_INCREMENT,
agent_id bigint(20) NOT NULL,
call_sid varchar(100) NOT NULL,
from_number varchar(20),
enqueued_at datetime DEFAULT CURRENT_TIMESTAMP,
status varchar(20) DEFAULT 'waiting',
PRIMARY KEY (id),
KEY agent_id (agent_id),
KEY call_sid (call_sid)
) $charset_collate;";
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
dbDelta($sql);
// Get call details
$call = $client->calls($call_sid)->fetch();
// Insert into personal queue tracking
$wpdb->insert($table, [
'agent_id' => $agent_id,
'call_sid' => $call_sid,
'from_number' => $call->from,
'status' => 'waiting'
]);
}
} else {
// Transfer to phone number
if (!preg_match('/^\+?[1-9]\d{1,14}$/', $transfer_target)) {
wp_send_json_error('Invalid phone number format');
return;
}
$twiml->dial($transfer_target);
}
// Update the call with the transfer TwiML
$call = $client->calls($call_sid)->update([