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:
		@@ -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([
 | 
			
		||||
 
 | 
			
		||||
@@ -204,6 +204,7 @@ class TWP_Core {
 | 
			
		||||
        // Call control actions
 | 
			
		||||
        $this->loader->add_action('wp_ajax_twp_toggle_hold', $plugin_admin, 'ajax_toggle_hold');
 | 
			
		||||
        $this->loader->add_action('wp_ajax_twp_transfer_call', $plugin_admin, 'ajax_transfer_call');
 | 
			
		||||
        $this->loader->add_action('wp_ajax_twp_get_transfer_agents', $plugin_admin, 'ajax_get_transfer_agents');
 | 
			
		||||
        $this->loader->add_action('wp_ajax_twp_requeue_call', $plugin_admin, 'ajax_requeue_call');
 | 
			
		||||
        $this->loader->add_action('wp_ajax_twp_start_recording', $plugin_admin, 'ajax_start_recording');
 | 
			
		||||
        $this->loader->add_action('wp_ajax_twp_stop_recording', $plugin_admin, 'ajax_stop_recording');
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user