diff --git a/CLAUDE.md b/CLAUDE.md
index 72dbf6d..572d34b 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1,19 +1,20 @@
-# CLAUDE.md
+# CLAUDE.md - Twilio WordPress Plugin Documentation
-This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
+This file provides comprehensive guidance to Claude Code (claude.ai/code) when working with the Twilio WordPress Plugin codebase.
## π¨ CRITICAL: Testing & Deployment Environment
**THIS PLUGIN RUNS ON A REMOTE SERVER IN A DOCKER CONTAINER - NOT LOCALLY**
- **Production Server Path**: `/home/shadowdao/public_html/wp-content/plugins/twilio-wp-plugin/`
-- **Website URL**: `https://phone.cloud-hosting.io/`
+- **Production URL**: `https://phone.cloud-hosting.io/`
- **Development Path**: `/home/jknapp/code/twilio-wp-plugin/`
- **Deployment Method**: Files synced via rsync from development to Docker container
-**IMPORTANT**:
+**IMPORTANT NOTES**:
- NEVER assume local testing - all tests must work on remote server
- Direct PHP tests work (`php test-twilio-direct.php send`)
- WordPress admin context has issues that need investigation
+- The plugin requires Twilio PHP SDK v8.7.0 to function
## π Standardized Phone Number Variable Names
@@ -38,164 +39,318 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
- **Test Agent Number**: `+19095737372`
- **Fake Test Number**: `+19512345678` (DO NOT SEND SMS TO THIS)
-### Webhook URLs:
+### Legacy Webhook URLs:
- **SMS**: `https://www.streamers.channel/wp-json/twilio-webhook/v1/sms`
- **Voice**: `https://www.streamers.channel/wp-json/twilio-webhook/v1/voice`
-## Project Overview
+## ποΈ Plugin Architecture Overview
-This is a comprehensive WordPress plugin for Twilio voice and SMS integration, featuring:
-- **Call Center Functionality**: Agent groups, call queues, call distribution
-- **Business Hours Management**: Schedules for workflow routing
-- **Outbound Calling**: Click-to-call with proper caller ID
-- **SMS Notifications**: Agent notifications and callback management
-- **Workflow Builder**: Visual call flow creation
-- **Voicemail System**: Recording and transcription
-- **Real-time Dashboard**: Agent queue management
-
-## Plugin Architecture
-
-### Core Classes (`includes/` directory)
-- **TWP_Core**: Main plugin initialization and hook registration
-- **TWP_Activator**: Database table creation and plugin activation
-- **TWP_Twilio_API**: Official Twilio PHP SDK wrapper (requires SDK v8.7.0)
-- **TWP_Webhooks**: Handles all Twilio webhook endpoints
-- **TWP_Scheduler**: Business hours and schedule management
-- **TWP_Workflow**: Call flow processing and TwiML generation
-- **TWP_Call_Queue**: Queue management and position tracking
-- **TWP_Agent_Groups**: Group management for call distribution
-- **TWP_Agent_Manager**: Agent status and call acceptance
-- **TWP_Callback_Manager**: Callback requests and processing
-- **TWP_Call_Logger**: Call logging and statistics
-
-### Database Tables
-Created by `TWP_Activator::create_tables()`:
-- `twp_phone_schedules` - Business hours definitions
-- `twp_call_queues` - Queue configurations
-- `twp_queued_calls` - Active calls in queues
-- `twp_workflows` - Call flow definitions
-- `twp_call_log` - Complete call history
-- `twp_sms_log` - SMS message tracking
-- `twp_voicemails` - Voicemail recordings and transcriptions
-- `twp_agent_groups` - Agent group definitions
-- `twp_group_members` - User-to-group relationships
-- `twp_agent_status` - Real-time agent availability
-- `twp_callbacks` - Callback request queue
-
-### Admin Interface (`admin/` directory)
-- **TWP_Admin**: Complete admin interface with pages for:
- - Dashboard with real-time statistics
- - Business Hours Schedules management
- - Workflow Builder with drag-drop interface
- - Call Queues configuration
- - Phone Numbers management (purchase/configure)
- - Voicemail inbox with playback
- - Call Logs with filtering
- - Agent Groups management
- - Agent Queue dashboard
- - **Outbound Calls interface** (click-to-call)
-
-### Webhook Endpoints
-All registered in `TWP_Webhooks::register_endpoints()`:
-- `/voice` - Main voice webhook for incoming calls
-- `/sms` - SMS webhook (handles agent "1" responses)
-- `/ivr-response` - IVR menu selections
-- `/queue-wait` - Queue hold music and position announcements
-- `/ring-group-result` - Handle no-answer scenarios and requeuing
-- `/agent-connect` - Connect SMS-responding agents to calls
-- `/callback-*` - Callback system webhooks
-- `/outbound-agent-with-from` - Outbound calling with from number
-
-## Key Features Implementation
-
-### Agent Group System
-- **Simultaneous Ring**: All group members called at once
-- **Priority Ordering**: Members have priority levels
-- **SMS Notifications**: Automatic alerts when no agents available
-- **One-Click Accept**: Agents can text "1" to receive calls
-- **Real-time Status**: Available/busy/offline tracking
-
-### Call Queue Management
-- **Position Tracking**: Callers know their queue position
-- **Timeout Handling**: Offers callback instead of hanging up
-- **Auto-requeue**: Failed agent connections return to queue
-- **Real-time Dashboard**: Agents see waiting calls
-
-### Callback System
-- **Smart Callbacks**: Replace timeout hangups
-- **SMS Confirmations**: Notify customers of callback requests
-- **Automatic Processing**: Cron-based callback execution
-- **Statistics Tracking**: Success rates and timing
-
-### Outbound Calling
-- **From Number Selection**: Choose business line for caller ID
-- **Conference Routing**: Agent-to-customer connection
-- **Call Logging**: Track all outbound attempts
-- **Proper Caller ID**: Target sees business number
-
-## WordPress Plugin Conventions
-
-### File Structure
+### Core Plugin Structure
```
twilio-wp-plugin/
-βββ twilio-wp-plugin.php (main plugin file)
-βββ includes/ (core functionality)
-βββ admin/ (admin interface)
-βββ assets/ (CSS/JS/images)
-βββ CLAUDE.md (this file)
+βββ twilio-wp-plugin.php # Main plugin file (entry point)
+βββ includes/ # Core functionality classes
+β βββ class-twp-core.php # Main plugin initialization
+β βββ class-twp-activator.php # Database setup
+β βββ class-twp-twilio-api.php # Twilio SDK wrapper
+β βββ class-twp-webhooks.php # REST API endpoints
+β βββ class-twp-tts-helper.php # TTS with ElevenLabs/Twilio
+β βββ ... # Other core classes
+βββ admin/ # Admin interface
+β βββ class-twp-admin.php # Admin pages & AJAX handlers
+βββ assets/ # Frontend resources
+β βββ css/ # Stylesheets
+β βββ js/ # JavaScript files
+β βββ audio/ # Audio resources
+βββ CLAUDE.md # This documentation file
```
-### Class Naming
-- Prefix: `TWP_` for all classes
-- Database tables: `twp_` prefix
-- Options: `twp_` prefix
-- AJAX actions: `twp_` prefix
+## π¦ Core Classes Documentation
-### Database Operations
-- Use WordPress `$wpdb` global for all database operations
-- Always sanitize inputs with `sanitize_text_field()`, `intval()`, etc.
-- Use prepared statements with `$wpdb->prepare()`
-- Call `TWP_Activator::ensure_tables_exist()` before database operations
+### Main Classes (`includes/` directory)
-### AJAX Handling
-All AJAX endpoints registered in `TWP_Core::define_admin_hooks()`:
-```php
-$this->loader->add_action('wp_ajax_twp_action_name', $plugin_admin, 'ajax_action_name');
-```
+#### TWP_Core
+- **Purpose**: Main plugin initialization and hook registration
+- **Key Methods**:
+ - `define_admin_hooks()`: Registers all admin AJAX actions
+ - `define_public_hooks()`: Registers frontend hooks
+ - `define_webhook_hooks()`: Registers REST API endpoints
-### User Profile Integration
-Agent phone numbers stored as user meta:
-- Meta key: `twp_phone_number`
-- Validation: `TWP_Agent_Manager::validate_phone_number()`
-- Duplicate checking: `TWP_Agent_Manager::is_phone_number_duplicate()`
+#### TWP_Activator
+- **Purpose**: Database table creation and plugin activation
+- **Tables Created**: 15 database tables (see Database Schema section)
+- **Key Methods**:
+ - `ensure_tables_exist()`: Checks and creates missing tables
+ - `migrate_tables()`: Handles schema migrations
+ - `add_missing_columns()`: Updates table structures
-## Twilio Integration
+#### TWP_Twilio_API
+- **Purpose**: Wrapper for Twilio PHP SDK v8.7.0
+- **Important**: Uses direct instantiation (`new TWP_Twilio_API()`)
+- **Key Methods**:
+ - `make_call()`: Initiates outbound calls
+ - `send_sms()`: Sends SMS messages
+ - `update_call()`: Updates call state (hold/resume)
+ - `create_queue_twiml()`: Generates queue TwiML
-### Current Implementation (SDK-Only)
-- **Official Twilio SDK**: Uses `twilio/sdk` v8.7.0 for all operations
-- **TwiML Generation**: Uses `\Twilio\TwiML\VoiceResponse` classes
-- **Response Handling**: Native Twilio SDK response objects
-- **Error Handling**: Proper `\Twilio\Exceptions\TwilioException` handling
+#### TWP_Webhooks
+- **Purpose**: Handles all Twilio webhook endpoints
+- **Namespace**: `twilio-webhook/v1`
+- **Total Endpoints**: 26 REST API routes
-### Installation Requirements
-**IMPORTANT**: The Twilio PHP SDK v8.7.0 is **REQUIRED** for this plugin to function.
+#### TWP_TTS_Helper (NEW)
+- **Purpose**: Text-to-Speech with ElevenLabs integration and caching
+- **Features**:
+ - Automatic ElevenLabs detection
+ - 30-day cache for generated audio
+ - Fallback to Twilio voice
+- **Key Methods**:
+ - `add_tts_to_twiml()`: Adds TTS to TwiML response
+ - `generate_tts_audio()`: Pre-generates cached audio
-**Installation Methods**:
-1. **Script Installation** (Recommended):
- ```bash
- chmod +x install-twilio-sdk.sh
- ./install-twilio-sdk.sh
- ```
+#### TWP_ElevenLabs_API
+- **Purpose**: Integration with ElevenLabs TTS service
+- **Configuration**: Uses `twp_elevenlabs_*` options
+- **Features**: Voice selection, model configuration, audio generation
-2. **Composer Installation**:
- ```bash
- composer install
- ```
+#### TWP_Workflow
+- **Purpose**: Call flow processing and TwiML generation
+- **Features**: IVR menus, queue routing, schedule checking
-**PHP Requirements**: PHP 8.0+ required for SDK compatibility
+#### TWP_Call_Queue
+- **Purpose**: Queue management and position tracking
+- **Features**: User-specific queues, hold queues, priority handling
+
+#### TWP_Agent_Manager
+- **Purpose**: Agent status and call acceptance
+- **Features**: Phone number validation, availability tracking
+
+#### TWP_Callback_Manager
+- **Purpose**: Callback request handling
+- **Features**: SMS confirmations, automatic processing
+
+#### TWP_Call_Logger
+- **Purpose**: Call logging and statistics
+- **Features**: Detailed call records, duration tracking
+
+#### TWP_Shortcodes
+- **Purpose**: WordPress shortcodes for frontend features
+- **Shortcode**: `[twp_browser_phone]` - Browser phone interface
+
+## πΎ Database Schema
+
+### Complete Table List (15 tables)
+1. `twp_phone_schedules` - Business hours definitions
+2. `twp_call_queues` - Queue configurations (includes user-specific)
+3. `twp_queued_calls` - Active calls in queues
+4. `twp_workflows` - Call flow definitions
+5. `twp_workflow_phones` - Phone-to-workflow mappings
+6. `twp_call_log` - Complete call history
+7. `twp_sms_log` - SMS message tracking
+8. `twp_voicemails` - Voicemail recordings and transcriptions
+9. `twp_agent_groups` - Agent group definitions
+10. `twp_group_members` - User-to-group relationships
+11. `twp_agent_status` - Real-time agent availability
+12. `twp_callbacks` - Callback request queue
+13. `twp_call_recordings` - Call recording metadata
+14. `twp_user_extensions` - User extension numbers
+15. `twp_queue_assignments` - User queue assignments
+
+### Key Table Structures
+
+#### twp_call_queues (Enhanced)
+- Supports user-specific queues (`user_id` field)
+- Queue types: `general`, `personal`, `hold`
+- Extension support for direct dialing
+- TTS message configuration
+
+#### twp_queued_calls
+- Uses `enqueued_at` timestamp (migrated from `joined_at`)
+- No `customer_number` field (uses `from_number`/`to_number`)
+- Tracks agent assignment and status
+
+## π REST API Endpoints (Webhooks)
+
+### Voice Endpoints
+- `/voice` - Main incoming call handler
+- `/browser-voice` - Browser phone calls
+- `/smart-routing` - Intelligent call routing
+- `/agent-screen` - Agent screening before connection
+- `/agent-confirm` - Agent confirmation handler
+- `/ring-group-result` - Handle ring group outcomes
+- `/agent-connect` - Connect accepted agents
+
+### SMS Endpoints
+- `/sms` - Main SMS handler (includes "1" responses)
+- `/status` - Call/SMS status updates
+
+### Queue Management
+- `/queue-wait` - Queue hold music and announcements
+- `/queue-action` - Queue-specific actions
+- `/ivr-response` - IVR menu selections
+
+### Voicemail
+- `/voicemail-callback` - Voicemail recording handler
+- `/voicemail-complete` - Post-recording processing
+- `/voicemail-audio/{id}` - Audio playback proxy
+
+### Recording
+- `/recording-status` - Recording status callbacks
+- `/recording-audio/{id}` - Recording playback proxy
+- `/transcription` - Transcription webhooks
+
+### Callback System
+- `/callback-choice` - Customer callback selection
+- `/request-callback` - Callback request handler
+- `/callback-agent` - Agent-side callback
+- `/callback-customer` - Customer-side callback
+
+### Outbound Calling
+- `/outbound-agent` - Basic outbound calls
+- `/outbound-agent-with-from` - Outbound with caller ID selection
+
+### Utility
+- `/resume-call` - Resume held calls
+- `/smart-routing-fallback` - Routing error handler
+- `/browser-fallback` - Browser phone fallback
+
+## ποΈ AJAX Endpoints (Admin & Frontend)
+
+### Total: 68 AJAX Actions
+
+### Categories:
+1. **Schedule Management** (4 actions)
+2. **Workflow Management** (5 actions)
+3. **Phone Number Management** (5 actions)
+4. **Queue Management** (6 actions)
+5. **Agent Management** (11 actions)
+6. **Call Control** (15 actions)
+7. **Voicemail** (5 actions)
+8. **Recording** (4 actions)
+9. **SMS Management** (4 actions)
+10. **ElevenLabs Integration** (3 actions)
+11. **Browser Phone** (4 actions)
+12. **Transfer & Hold** (8 actions)
+
+### Key AJAX Actions:
+- `twp_toggle_hold` - Put call on hold/resume (uses call leg detection)
+- `twp_transfer_call` - Transfer to agent/queue (uses call leg detection)
+- `twp_start_recording` - Start call recording
+- `twp_stop_recording` - Stop call recording
+- `twp_requeue_call` - Return call to queue (uses call leg detection)
+- `twp_initiate_outbound_call_with_from` - Outbound with caller ID
+
+### Call Control Architecture (CRITICAL):
+All call control functions (`twp_toggle_hold`, `twp_transfer_call`, `twp_requeue_call`) now use intelligent call leg detection to ensure actions are applied to the customer call leg, not the agent leg. This prevents customer disconnections in complex call topologies.
+
+## π¨ Frontend Components
+
+### JavaScript Files
+1. **admin.js** (116KB) - Admin interface functionality
+2. **browser-phone-frontend.js** (85KB) - Browser phone implementation
+3. **twp-service-worker.js** (2.5KB) - Push notifications
+
+### Browser Phone Features
+- Twilio Device SDK integration
+- Real-time call controls (hold, transfer, record)
+- Queue monitoring dashboard
+- Agent status management
+- Call history display
+- Visual call state indicators
+
+### CSS Files
+- **admin.css** - Admin interface styling
+- **browser-phone-frontend.css** - Browser phone UI
+
+## π§ Recent Fixes & Improvements
+
+### CRITICAL: Outbound Call Issues RESOLVED (September 2025)
+- **Issue**: Hold, transfer, and requeue functions were applying actions to wrong call leg (agent instead of customer), causing customer disconnections
+- **Root Cause**: Complex call topologies in outbound calls create agent and customer call legs with different SIDs
+- **Solution**: New intelligent call leg detection system
+- **Functions Fixed**: `ajax_toggle_hold()`, `ajax_transfer_call()`, `ajax_requeue_call()`
+- **Result**: All functions now work correctly for both inbound and outbound calls without disconnecting customers
+
+### Customer Number Detection Issues RESOLVED (September 2025)
+- **Issue**: Customer numbers showing as "client:agentname" instead of actual phone numbers in voicemail and call recording admin interfaces
+- **Root Cause**: Browser phone calls create complex call topologies where customer information is stored in different call legs
+- **Solution**: Enhanced customer number detection with fallback mechanisms
+- **Areas Fixed**: Voicemail callback handling and call recording customer identification
+- **Result**: Both inbound and outbound calls now properly identify real customer phone numbers
+
+### Call Leg Detection System (NEW)
+- **Function**: `find_customer_call_leg($call_sid, $api)` in `TWP_Admin` class
+- **Purpose**: Identifies customer vs agent call legs in complex call topologies
+- **Detection Logic**:
+ - Detects browser phone calls by checking for `client:` prefixes
+ - Uses parent call relationships to find customer leg
+ - Searches active calls for related customer connections
+ - Comprehensive fallback mechanisms
+- **Logging**: Extensive debugging output for call relationship tracking
+
+### Enhanced Customer Number Detection (NEW)
+- **Voicemail Callback Enhancement** (`TWP_Webhooks::handle_voicemail_callback()`):
+ - Fallback logic retrieves customer numbers from call log when From parameter missing
+ - Browser phone detection identifies `client:` calls and finds real customer numbers
+ - Parent call analysis and related call search functionality
+ - Comprehensive logging for customer number detection process
+- **Call Recording Enhancement** (`TWP_Admin::ajax_start_recording()`):
+ - Browser phone detection using `client:` prefix identification
+ - Integration with `find_customer_call_leg()` helper for proper customer identification
+ - Smart number extraction from appropriate call legs (from/to field analysis)
+ - Enhanced logging for recording customer number detection
+
+### Browser Phone Call Support Enhanced
+- **Client Transfer Support**: Fixed "Invalid phone number format" errors for `client:` transfers
+- **Detection**: Automatically identifies `client:agentname` format calls
+- **Transfer Methods**:
+ - Client transfers: `$dial->client($agent_name)`
+ - Phone transfers: `$twiml->dial($target)`
+ - Queue transfers: Uses customer leg for proper queue placement
+- **Customer Number Resolution**: Properly identifies real phone numbers in complex call topologies
+- **Admin Interface Fixes**: Voicemail and recording interfaces now show actual customer numbers instead of client identifiers
+
+### Hold Functionality (Fixed)
+- **Issue**: `TWP_Twilio_API::get_instance()` error
+- **Fix**: Changed to direct instantiation + call leg detection
+- **Enhancement**: Added ElevenLabs TTS with caching
+- **Customer Impact**: Hold now affects customer (not agent) with proper music/messages
+
+### Transfer System (Fixed)
+- **Issue**: Transfers were disconnecting customers in outbound calls
+- **Fix**: All transfer types now use correct customer call leg
+- **Types Supported**: Queue, client (browser phone), phone number transfers
+- **Enhancement**: Proper TwiML generation for each transfer type
+
+### Requeue Functionality (Fixed)
+- **Issue**: Requeue was disconnecting customers instead of placing them back in queue
+- **Fix**: Uses customer call leg for queue placement
+- **Enhancement**: Maintains proper call tracking with customer SID
+- **Database**: Uses `enqueued_at` column when available, falls back to `joined_at`
+
+### Recording Management (Fixed)
+- **Issue**: "Unknown subresource update" error
+- **Fix**: Proper SDK v8 syntax using call recordings subresource
+- **Fallback**: Tries `Twilio.CURRENT` if specific SID fails
+
+### Queue Management (Fixed)
+- **Issue**: `Enqueue::waitUrl()` undefined method
+- **Fix**: Pass `waitUrl` as option: `$response->enqueue($queue_name, ['waitUrl' => $url])`
+
+### TTS Integration (New)
+- **Feature**: ElevenLabs integration with intelligent caching
+- **Cache Duration**: 30 days for identical text
+- **Fallback**: Automatic fallback to Twilio voice
+- **Performance**: Cached audio loads instantly
+
+## π Twilio Integration Details
+
+### SDK Version
+- **Required**: Twilio PHP SDK v8.7.0
+- **Installation**: Via Composer or install script
+- **PHP Requirement**: PHP 8.0+
### API Response Structure
-Current Twilio API responses follow this pattern:
```php
// Success response
[
@@ -209,79 +364,199 @@ Current Twilio API responses follow this pattern:
'error' => 'Error message',
'code' => 400
]
-
-// Call SID location
-$call_sid = $response['data']['sid']; // NOT $response['call_sid']
```
-### TwiML Best Practices
-Always include XML declaration:
+### TwiML Generation Best Practices
+- Always use SDK classes for TwiML generation
+- Include proper XML headers when needed
+- Use TTS helper for voice synthesis
+- Implement proper error handling
+
+### Recording Stop Methods (SDK v8)
```php
-$twiml = '';
-$twiml .= '';
-$twiml .= 'Message';
-$twiml .= '';
+// Method 1: Specific recording
+$client->calls($call_sid)
+ ->recordings($recording_sid)
+ ->update(['status' => 'stopped']);
+
+// Method 2: Single active recording
+$client->calls($call_sid)
+ ->recordings('Twilio.CURRENT')
+ ->update(['status' => 'stopped']);
```
-## Development Commands
+## π οΈ Development Guidelines
-```bash
-# Install WordPress development dependencies
-composer install
+### Call Control Functions (CRITICAL)
+**ALWAYS use call leg detection for hold, transfer, and requeue operations:**
+```php
+// Correct pattern for call control functions
+private function find_customer_call_leg($call_sid, $api) {
+ // Detects browser phone vs regular calls
+ // Uses parent call relationships
+ // Searches active calls for customer leg
+ // Returns correct SID for operations
+}
-# Install JavaScript dependencies
-npm install
-
-# Run PHP CodeSniffer for WordPress coding standards
-vendor/bin/phpcs
-
-# Fix PHP coding standards automatically
-vendor/bin/phpcbf
-
-# Run PHPUnit tests
-vendor/bin/phpunit
-
-# Install Twilio SDK (recommended migration)
-composer require twilio/sdk
+// Usage in AJAX handlers
+$customer_call_sid = $this->find_customer_call_leg($call_sid, $twilio);
+$result = $api->update_call($customer_call_sid, ['twiml' => $twiml_xml]);
```
-## Testing Approach
+**Why This Matters:**
+- Outbound calls create separate agent and customer call legs
+- Applying actions to agent leg disconnects customer
+- Browser phone calls use `client:` identifiers
+- Customer leg must be identified for proper call control
-### Unit Testing
-- Use PHPUnit for PHP code testing
-- Mock WordPress functions using Brain Monkey or WP_Mock
-- Mock Twilio API responses for reliable testing
+### Database Operations
+- Always use `$wpdb` global
+- Sanitize with `sanitize_text_field()`, `intval()`
+- Use prepared statements: `$wpdb->prepare()`
+- Call `TWP_Activator::ensure_tables_exist()` before operations
-### Integration Testing
-- Test webhook endpoints with Twilio webhook simulator
-- Test complete call flows end-to-end
-- Verify database operations and data integrity
+### AJAX Handler Pattern
+```php
+public function ajax_handler_name() {
+ if (!$this->verify_ajax_nonce()) {
+ wp_send_json_error('Invalid nonce');
+ return;
+ }
+
+ // Handler logic
+
+ wp_send_json_success($data);
+}
+```
-### Manual Testing
-- Use Twilio Console to monitor webhook calls
-- Check WordPress error logs for debugging
-- Test with real phone numbers for user experience
+### Error Handling
+- Log errors with `error_log()`
+- Return structured error responses
+- Implement fallback mechanisms
+- Handle Twilio exceptions properly
-## Common Issues & Solutions
+### Naming Conventions
+- Classes: `TWP_Class_Name`
+- Tables: `twp_table_name`
+- Options: `twp_option_name`
+- AJAX actions: `twp_action_name`
+- Nonces: `twp_ajax_nonce` or `twp_frontend_nonce`
+
+## π Common Issues & Solutions
### Database Issues
-- **Missing Tables**: Call `TWP_Activator::ensure_tables_exist()`
-- **White Admin Modals**: Check for PHP errors in workflow loading
+- **Missing Tables**: Run `TWP_Activator::ensure_tables_exist()`
+- **Schema Changes**: Check `add_missing_columns()` method
+- **Migration Issues**: Review `migrate_tables()` implementation
### Webhook Issues
-- **500 Errors**: Check PHP error logs for fatal errors
-- **Missing Parameters**: Parameters must be in webhook URL, not just POST data
-- **TwiML Parse Errors**: Always include XML declaration
+- **500 Errors**: Check PHP error logs
+- **TwiML Errors**: Verify XML structure
+- **Authentication**: Webhooks use `__return_true` permission
### API Issues
-- **Call SID Access**: Use `$response['data']['sid']` not `$response['call_sid']`
-- **Phone Number Format**: Store with + prefix (e.g., "+1234567890")
-- **From Number**: Must be a verified Twilio number
+- **Instantiation**: Use `new TWP_Twilio_API()` not singleton
+- **Phone Format**: Always use E.164 format (+1XXXXXXXXXX)
+- **Call SID**: Access via `$response['data']['sid']`
-### Agent Management
-- **SMS Responses**: Agents text "1" to receive calls
-- **Phone Validation**: Auto-formats US numbers to +1 format
-- **Duplicate Prevention**: Checks phone numbers across all users
+### Call Control Issues (Outbound Calls)
+- **Customer Disconnections**: Use `find_customer_call_leg()` before hold/transfer/requeue
+- **Wrong Call Leg**: Check error logs for "Call Leg Detection" messages
+- **Browser Phone Issues**: Look for `client:` prefix detection in logs
+- **Transfer Failures**: Verify customer call leg is being used for TwiML updates
+- **Customer Number Display**: Check for "TWP Voicemail Callback" or "TWP Recording" log entries for customer number detection
+- **Client Identifier Issues**: Search logs for "client:" identifier handling and real customer number detection
-This plugin provides a complete call center solution with professional-grade features suitable for business use.
-- our production url is https://phone.cloud-hosting.io
\ No newline at end of file
+### Hold/Transfer Issues
+- **Hold Music**: Default URL provided, customizable via settings
+- **TTS Caching**: 30-day cache, auto-cleanup available
+- **Transfer**: Supports agent queues and phone numbers
+- **Call Topology**: Complex outbound calls require call leg detection
+
+## π§ͺ Testing Approach
+
+### Unit Testing
+- PHPUnit for PHP code
+- Mock WordPress functions
+- Mock Twilio API responses
+
+### Integration Testing
+- Test webhook endpoints
+- Verify database operations
+- Test complete call flows
+
+### Manual Testing
+- Monitor Twilio Console
+- Check WordPress debug logs
+- Test with real phone numbers
+
+## π Key Features Implementation
+
+### Agent System
+- **SMS Accept**: Agents text "1" to accept calls
+- **Real-time Status**: Available/busy/offline states
+- **Group Management**: Priority-based call distribution
+- **Personal Queues**: Agent-specific call queues
+
+### Call Queue System
+- **Position Tracking**: Real-time queue position
+- **Timeout Handling**: Automatic callback offers
+- **Hold Queues**: Temporary call parking
+- **User Queues**: Personal agent queues
+
+### Browser Phone
+- **Twilio Device**: Full SDK integration
+- **Call Controls**: Hold, transfer, record, mute
+- **Visual Interface**: Real-time status updates
+- **Queue Dashboard**: Monitor waiting calls
+
+### Recording System
+- **Start/Stop**: Dynamic recording control
+- **Storage**: Database tracking with Twilio URLs
+- **Playback**: Authenticated proxy endpoints
+- **Transcription**: Automatic with callbacks
+
+### ElevenLabs TTS
+- **Auto-detection**: Uses ElevenLabs when configured
+- **Caching**: 30-day cache for repeated phrases
+- **Fallback**: Seamless Twilio voice fallback
+- **Performance**: Instant cached audio delivery
+
+## π Configuration Requirements
+
+### WordPress Settings
+- **Twilio Credentials**: Account SID, Auth Token
+- **TwiML App**: For browser phone functionality
+- **Phone Numbers**: At least one Twilio number
+- **Webhook URLs**: Configure in Twilio Console
+
+### Optional Settings
+- **ElevenLabs**: API key and voice selection
+- **Hold Music**: Custom URL support
+- **SMS Notifications**: Agent alert numbers
+- **Business Hours**: Schedule configurations
+
+## π Debugging Tips
+
+1. **Enable WordPress Debug**: `WP_DEBUG = true`
+2. **Check Error Logs**: `/wp-content/debug.log`
+3. **Monitor Twilio Console**: Real-time webhook debugging
+4. **Database Queries**: Use `$wpdb->last_error`
+5. **Browser Console**: Check JavaScript errors
+6. **Network Tab**: Monitor AJAX requests
+7. **Call Leg Detection**: Look for "TWP Call Leg Detection" log entries
+8. **Outbound Call Issues**: Check for agent vs customer call SID usage
+9. **Browser Phone Debugging**: Search logs for "client:" identifier handling
+
+## π External Resources
+
+- **Twilio PHP SDK**: https://www.twilio.com/docs/libraries/reference/twilio-php/
+- **WordPress REST API**: https://developer.wordpress.org/rest-api/
+- **ElevenLabs API**: https://api.elevenlabs.io/docs
+- **Twilio TwiML**: https://www.twilio.com/docs/voice/twiml
+
+---
+
+*Last Updated: September 2025*
+*Plugin Version: Production Ready*
+*Maintained for: phone.cloud-hosting.io*
\ No newline at end of file
diff --git a/README.md b/README.md
index 0054712..c6e1acb 100644
--- a/README.md
+++ b/README.md
@@ -109,6 +109,20 @@ This plugin **requires** the Twilio PHP SDK v8.7.0 to function. The plugin will
## Recent Updates
+### CRITICAL: Outbound Call Issues Fixed (September 2025)
+- **Customer Disconnection Issue Resolved**: Fixed major problem where hold, transfer, and requeue functions were disconnecting customers in outbound calls
+- **Call Leg Detection System**: New intelligent system identifies customer vs agent call legs in complex call topologies
+- **Browser Phone Transfers**: Fixed "Invalid phone number format" errors when transferring to browser phone agents
+- **Outbound Call Stability**: All call control functions now work correctly for both inbound and outbound scenarios
+- **Enhanced Logging**: Comprehensive debugging for call relationship tracking
+
+### Customer Number Detection Improvements (September 2025)
+- **Voicemail Interface Fix**: Customer numbers now display correctly instead of showing "client:agentname" for browser phone calls
+- **Call Recording Interface Fix**: Recording admin interface now shows actual customer phone numbers for all call types
+- **Smart Number Detection**: Enhanced fallback logic retrieves customer numbers from call logs and parent call analysis
+- **Browser Phone Support**: Proper handling of complex call topologies created by browser phone calls
+- **User Experience**: Admin interfaces now consistently display meaningful customer information
+
### Browser Phone Upgrade (v2.0)
- **Migrated to Twilio Voice SDK v2**: Replaced deprecated Client SDK v1.14
- **Improved Stability**: Better error handling and automatic recovery
@@ -331,6 +345,19 @@ php test-sdk.php
- Review workflow step configuration
- Check notification_number field (not phone_number)
+#### Outbound Call Control Issues
+- **Customer Disconnections**: Fixed in latest version with call leg detection
+- **Hold Not Working**: Ensure you have the latest version with `find_customer_call_leg()` function
+- **Transfer Failures**: Check error logs for "Call Leg Detection" messages
+- **Browser Phone Transfers**: Use latest version that supports `client:` identifier transfers
+- **Requeue Problems**: Verify customer call leg is being used, not agent leg
+
+#### Customer Number Display Issues
+- **"client:agentname" in Voicemails**: Fixed in latest version with enhanced customer number detection
+- **Wrong Numbers in Recording Interface**: Fixed with browser phone detection and call leg analysis
+- **Missing Customer Info**: Check error logs for "TWP Voicemail Callback" or "TWP Recording" customer detection messages
+- **Browser Phone Call Issues**: Enhanced detection now properly identifies real customer numbers in complex call topologies
+
#### SMS Not Sending from Admin
- Test with direct PHP script: `php test-twilio-direct.php send`
- Verify Twilio credentials in settings
@@ -413,7 +440,18 @@ All webhooks are REST API endpoints under `/wp-json/twilio-webhook/v1/`:
## Version History
-### v2.1.0 (Current)
+### v2.2.0 (Current - September 2025)
+- **CRITICAL FIXES**: Resolved major outbound call issues
+- **Call Leg Detection**: New intelligent system for complex call topologies
+- **Customer Disconnection Fix**: Hold, transfer, and requeue now work correctly
+- **Browser Phone Transfer Support**: Fixed `client:` identifier handling
+- **Enhanced Debugging**: Comprehensive call relationship tracking
+- **Outbound Call Stability**: All functions work for both inbound and outbound calls
+- **Customer Number Detection**: Enhanced voicemail and recording interfaces to show real customer numbers
+- **Browser Phone Interface Fixes**: Admin interfaces now properly identify customers in complex call scenarios
+- **Fallback Logic**: Comprehensive customer number detection with call log and parent call analysis
+
+### v2.1.0
- **Multiple Phone Numbers**: Workflows can now handle multiple phone numbers
- **Discord & Slack Integration**: Real-time notifications for call events
- **Enhanced Monitoring**: Automated queue timeout tracking and alerts
diff --git a/admin/class-twp-admin.php b/admin/class-twp-admin.php
index adc9a80..1744a26 100644
--- a/admin/class-twp-admin.php
+++ b/admin/class-twp-admin.php
@@ -2560,8 +2560,23 @@ class TWP_Admin {
$agent_status = TWP_Agent_Manager::get_agent_status($current_user_id);
$agent_stats = TWP_Agent_Manager::get_agent_stats($current_user_id);
- // Get user's extension and assigned queues
+ // Ensure database tables exist
+ TWP_Activator::ensure_tables_exist();
+
+ // Get user's extension and assigned queues - create if they don't exist
$extension_data = TWP_User_Queue_Manager::get_user_extension_data($current_user_id);
+
+ // If user doesn't have queues yet, create them
+ if (!$extension_data) {
+ $user_phone = get_user_meta($current_user_id, 'twp_phone_number', true);
+ if ($user_phone) {
+ $creation_result = TWP_User_Queue_Manager::create_user_queues($current_user_id);
+ if ($creation_result['success']) {
+ $extension_data = TWP_User_Queue_Manager::get_user_extension_data($current_user_id);
+ }
+ }
+ }
+
$assigned_queues = TWP_User_Queue_Manager::get_user_assigned_queues($current_user_id);
// Check login status
@@ -2596,43 +2611,58 @@ class TWP_Admin {
@@ -2787,7 +2817,7 @@ class TWP_Admin {
// Refresh queue data every 5 seconds
let refreshInterval;
let currentUser = ;
- let assignedQueues = ;
+ let assignedQueues = ;
// Get the appropriate nonce based on context
let ajaxNonce = '';
@@ -2797,6 +2827,25 @@ class TWP_Admin {
refreshInterval = setInterval(refreshQueues, 5000);
}
+ function initializeUserQueues() {
+ jQuery.ajax({
+ url: ajaxurl,
+ method: 'POST',
+ data: {
+ action: 'twp_initialize_user_queues',
+ nonce: ajaxNonce
+ },
+ success: function(response) {
+ if (response.success) {
+ alert('Queues initialized successfully! The page will now refresh.');
+ location.reload();
+ } else {
+ alert('Failed to initialize queues: ' + response.data);
+ }
+ }
+ });
+ }
+
function refreshQueues() {
assignedQueues.forEach(queueId => {
jQuery.ajax({
@@ -4475,74 +4524,6 @@ class TWP_Admin {
}
}
- /**
- * AJAX handler for transferring a call
- */
- public function ajax_transfer_call() {
- // Check for either admin or frontend nonce
- if (!$this->verify_ajax_nonce()) {
- wp_send_json_error('Invalid nonce');
- return;
- }
-
- // Check permissions - allow both admin and agent queue access
- if (!current_user_can('manage_options') && !current_user_can('twp_access_agent_queue')) {
- wp_send_json_error('Insufficient permissions');
- return;
- }
-
- $call_sid = sanitize_text_field($_POST['call_sid']);
- $current_queue_id = intval($_POST['current_queue_id']);
- $target = sanitize_text_field($_POST['target_queue_id']); // Can be queue ID or extension
-
- global $wpdb;
-
- // Check if target is an extension
- if (!is_numeric($target) || strlen($target) <= 4) {
- // It's an extension, find the user's queue
- $user_id = TWP_User_Queue_Manager::get_user_by_extension($target);
-
- if (!$user_id) {
- wp_send_json_error('Extension not found');
- return;
- }
-
- $extension_data = TWP_User_Queue_Manager::get_user_extension_data($user_id);
- $target_queue_id = $extension_data['personal_queue_id'];
- } else {
- $target_queue_id = intval($target);
- }
-
- // Move call to new queue
- $next_position = $wpdb->get_var($wpdb->prepare(
- "SELECT COALESCE(MAX(position), 0) + 1 FROM {$wpdb->prefix}twp_queued_calls
- WHERE queue_id = %d AND status = 'waiting'",
- $target_queue_id
- ));
-
- $result = $wpdb->update(
- $wpdb->prefix . 'twp_queued_calls',
- array(
- 'queue_id' => $target_queue_id,
- 'position' => $next_position
- ),
- array('call_sid' => $call_sid),
- array('%d', '%d'),
- array('%s')
- );
-
- if ($result !== false) {
- // Update call with new queue wait URL
- $twilio = new TWP_Twilio_API();
- $twilio->update_call($call_sid, array(
- 'url' => site_url('/wp-json/twilio-webhook/v1/queue-wait?queue_id=' . $target_queue_id)
- ));
-
- wp_send_json_success();
- } else {
- wp_send_json_error('Failed to transfer call');
- }
- }
/**
* AJAX handler for sending call to voicemail
@@ -4713,6 +4694,45 @@ class TWP_Admin {
));
}
+ /**
+ * AJAX handler for initializing user queues
+ */
+ public function ajax_initialize_user_queues() {
+ // Check for either admin or frontend nonce
+ if (!$this->verify_ajax_nonce()) {
+ wp_send_json_error('Invalid nonce');
+ return;
+ }
+
+ // Check permissions - allow both admin and agent queue access
+ if (!current_user_can('manage_options') && !current_user_can('twp_access_agent_queue')) {
+ wp_send_json_error('Insufficient permissions');
+ return;
+ }
+
+ $user_id = get_current_user_id();
+ $user_phone = get_user_meta($user_id, 'twp_phone_number', true);
+
+ if (!$user_phone) {
+ wp_send_json_error('Please configure your phone number in your user profile first');
+ return;
+ }
+
+ // Create user queues
+ $result = TWP_User_Queue_Manager::create_user_queues($user_id);
+
+ if ($result['success']) {
+ wp_send_json_success(array(
+ 'message' => 'User queues created successfully',
+ 'extension' => $result['extension'],
+ 'personal_queue_id' => $result['personal_queue_id'],
+ 'hold_queue_id' => $result['hold_queue_id']
+ ));
+ } else {
+ wp_send_json_error($result['error']);
+ }
+ }
+
/**
* AJAX handler for getting Eleven Labs voices
*/
@@ -5337,7 +5357,18 @@ class TWP_Admin {
$groups_table = $wpdb->prefix . 'twp_group_members';
$calls_table = $wpdb->prefix . 'twp_queued_calls';
- // Get queues where user is a member of the assigned agent group
+ // Auto-create personal queues if they don't exist
+ $extensions_table = $wpdb->prefix . 'twp_user_extensions';
+ $existing_extension = $wpdb->get_row($wpdb->prepare(
+ "SELECT extension FROM $extensions_table WHERE user_id = %d",
+ $user_id
+ ));
+
+ if (!$existing_extension) {
+ TWP_User_Queue_Manager::create_user_queues($user_id);
+ }
+
+ // Get queues where user is a member of the assigned agent group OR personal/hold queues
$user_queues = $wpdb->get_results($wpdb->prepare("
SELECT DISTINCT q.*,
COUNT(c.id) as waiting_count,
@@ -5345,10 +5376,17 @@ class TWP_Admin {
FROM $queues_table q
LEFT JOIN $groups_table gm ON gm.group_id = q.agent_group_id
LEFT JOIN $calls_table c ON c.queue_id = q.id AND c.status = 'waiting'
- WHERE gm.user_id = %d AND gm.is_active = 1
+ WHERE (gm.user_id = %d AND gm.is_active = 1)
+ OR (q.user_id = %d AND q.queue_type IN ('personal', 'hold'))
GROUP BY q.id
- ORDER BY q.queue_name ASC
- ", $user_id));
+ ORDER BY
+ CASE
+ WHEN q.queue_type = 'personal' THEN 1
+ WHEN q.queue_type = 'hold' THEN 2
+ ELSE 3
+ END,
+ q.queue_name ASC
+ ", $user_id, $user_id));
wp_send_json_success($user_queues);
}
@@ -6647,8 +6685,22 @@ class TWP_Admin {
// Check if smart routing is configured on any phone numbers
$smart_routing_configured = $this->check_smart_routing_status();
- // Get user's queue memberships
- $user_queues = $this->get_user_queue_memberships(get_current_user_id());
+ // Get user extension data and create personal queues if needed
+ $current_user_id = get_current_user_id();
+ global $wpdb;
+ $extensions_table = $wpdb->prefix . 'twp_user_extensions';
+ $extension_data = $wpdb->get_row($wpdb->prepare(
+ "SELECT extension FROM $extensions_table WHERE user_id = %d",
+ $current_user_id
+ ));
+
+ if (!$extension_data) {
+ TWP_User_Queue_Manager::create_user_queues($current_user_id);
+ $extension_data = $wpdb->get_row($wpdb->prepare(
+ "SELECT extension FROM $extensions_table WHERE user_id = %d",
+ $current_user_id
+ ));
+ }
?>
Browser Phone
@@ -6795,30 +6847,25 @@ class TWP_Admin {
-
+
-
π Call Queues
-
Queues you're a member of:
-
-
-
-
-
-
- Loading...
-
-
-
+
+
+
Loading your queues...
+
+
+
-
-
@@ -7004,28 +7051,90 @@ class TWP_Admin {
margin-top: 0;
color: #1976D2;
}
+ .queue-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 15px;
+ }
+ .user-extension-admin {
+ background: #e8f4f8;
+ padding: 6px 12px;
+ border-radius: 4px;
+ font-size: 13px;
+ color: #2c5282;
+ }
.queue-item {
display: flex;
justify-content: space-between;
align-items: center;
- padding: 10px;
+ padding: 12px;
background: white;
border: 1px solid #ddd;
border-radius: 4px;
margin-bottom: 10px;
+ position: relative;
+ }
+ .queue-item.queue-type-personal {
+ border-left: 4px solid #28a745;
+ }
+ .queue-item.queue-type-hold {
+ border-left: 4px solid #ffc107;
+ }
+ .queue-item.queue-type-general {
+ border-left: 4px solid #007bff;
+ }
+ .queue-item.has-calls {
+ background: #fff3cd;
+ border-color: #ffeaa7;
+ }
+ .queue-name {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-weight: 600;
+ color: #333;
+ }
+ .queue-type-icon {
+ font-size: 16px;
+ }
+ .queue-type-personal .queue-name {
+ color: #155724;
+ }
+ .queue-type-hold .queue-name {
+ color: #856404;
}
.queue-info {
flex: 1;
}
- .queue-waiting {
- display: block;
+ .queue-details {
font-size: 12px;
color: #666;
- margin-top: 2px;
+ margin-top: 4px;
+ }
+ .queue-waiting {
+ display: inline-block;
+ font-size: 12px;
+ color: #666;
+ margin-right: 10px;
}
.queue-waiting.has-calls {
color: #d63384;
font-weight: bold;
+ background: #fff;
+ padding: 2px 6px;
+ border-radius: 3px;
+ border: 1px solid #f8d7da;
+ }
+ .queue-loading {
+ text-align: center;
+ color: #666;
+ font-style: italic;
+ padding: 20px;
+ }
+ .queue-actions {
+ margin-top: 15px;
+ text-align: center;
}
@@ -7466,47 +7575,82 @@ class TWP_Admin {
}
});
- // Queue management functionality
- function loadQueueStatus() {
-
+ // Enhanced queue management functionality
+ var adminUserQueues = [];
+
+ function loadAdminQueues() {
$.post(ajaxurl, {
- action: 'twp_get_waiting_calls',
+ action: 'twp_get_agent_queues',
nonce: ''
}, function(response) {
- if (response.success && response.data) {
- var waitingCalls = response.data || [];
-
- // Update each queue
-
- var queueId = ;
- var queueCalls = waitingCalls.filter(function(call) {
- return call.queue_id == queueId;
- });
-
- var $waitingSpan = $('#queue-waiting-' + queueId);
- var $acceptBtn = $('[data-queue-id="' + queueId + '"]');
-
- if (queueCalls.length > 0) {
- $waitingSpan.text(queueCalls.length + ' call(s) waiting')
- .addClass('has-calls');
- $acceptBtn.prop('disabled', false);
- } else {
- $waitingSpan.text('No calls waiting')
- .removeClass('has-calls');
- $acceptBtn.prop('disabled', true);
- }
-
+ if (response.success) {
+ adminUserQueues = response.data;
+ displayAdminQueues();
+ } else {
+ $('#admin-queue-list').html('
Failed to load queues: ' + response.data + '
');
}
+ }).fail(function() {
+ $('#admin-queue-list').html('
Failed to load queues
');
});
-
}
- // Accept queue call
- $('.accept-queue-call').on('click', function() {
+ function displayAdminQueues() {
+ var $queueList = $('#admin-queue-list');
+
+ if (adminUserQueues.length === 0) {
+ $queueList.html('
No queues assigned to you.
');
+ return;
+ }
+
+ var html = '';
+ adminUserQueues.forEach(function(queue) {
+ var hasWaiting = parseInt(queue.current_waiting) > 0;
+ var waitingCount = queue.current_waiting || 0;
+ var queueType = queue.queue_type || 'general';
+
+ // Generate queue type indicator
+ var typeIndicator = '';
+ var typeDescription = '';
+ if (queueType === 'personal') {
+ typeIndicator = 'π€';
+ typeDescription = queue.extension ? ' (Ext: ' + queue.extension + ')' : '';
+ } else if (queueType === 'hold') {
+ typeIndicator = 'βΈοΈ';
+ typeDescription = ' (Hold)';
+ } else {
+ typeIndicator = 'π';
+ typeDescription = ' (Team)';
+ }
+
+ html += '
';
+ html += '
';
+ html += '
';
+ html += '' + typeIndicator + '';
+ html += queue.queue_name + typeDescription;
+ html += '
';
+ html += '
';
+ html += '';
+ html += waitingCount + ' waiting';
+ html += '';
+ html += 'Max: ' + queue.max_size + '';
+ html += '
';
+ html += '
';
+ html += '
';
+ html += '
';
+ });
+
+ $queueList.html(html);
+ }
+
+ // Accept queue call functionality (using event delegation)
+ $(document).on('click', '.accept-queue-call', function() {
var queueId = $(this).data('queue-id');
var $button = $(this);
- $button.prop('disabled', true).text('Accepting...');
+ $button.prop('disabled', true).text('Connecting...');
$.post(ajaxurl, {
action: 'twp_accept_next_queue_call',
@@ -7514,24 +7658,27 @@ class TWP_Admin {
nonce: ''
}, function(response) {
if (response.success) {
- $('#queue-status').html('
Call accepted! Connecting...
');
- // Refresh queue status
- setTimeout(loadQueueStatus, 1000);
+ showNotice('Connecting to next caller...', 'success');
+ // Refresh queue status after accepting call
+ setTimeout(loadAdminQueues, 1000);
} else {
- $('#queue-status').html('
Failed to accept call: ' + (response.data || 'Unknown error') + '
');
+ showNotice(response.data || 'No calls waiting in this queue', 'info');
}
}).fail(function() {
- $('#queue-status').html('
Failed to accept call. Please try again.
');
+ showNotice('Failed to accept queue call', 'error');
}).always(function() {
$button.prop('disabled', false).text('Accept Next Call');
});
});
+ // Refresh queues button
+ $('#admin-refresh-queues').on('click', function() {
+ loadAdminQueues();
+ });
+
// Load queue status on page load and refresh every 5 seconds
-
- loadQueueStatus();
- setInterval(loadQueueStatus, 5000);
-
+ loadAdminQueues();
+ setInterval(loadAdminQueues, 5000);
// Save mode button
$('#save-mode-btn').on('click', function() {
@@ -7603,21 +7750,143 @@ class TWP_Admin {
function adminShowTransferDialog() {
if (!currentCall) return;
- // Load available agents
+ // Try enhanced transfer system first
$.post(ajaxurl, {
- action: 'twp_get_online_agents',
+ action: 'twp_get_transfer_targets',
nonce: ''
}, function(response) {
- if (response.success && response.data.length > 0) {
- adminShowAgentTransferDialog(response.data);
+ if (response.success && response.data && (response.data.users || response.data.queues)) {
+ adminShowEnhancedTransferDialog(response.data);
} else {
- adminShowManualTransferDialog();
+ // Fallback to legacy system
+ $.post(ajaxurl, {
+ action: 'twp_get_online_agents',
+ nonce: ''
+ }, function(legacyResponse) {
+ if (legacyResponse.success && legacyResponse.data.length > 0) {
+ adminShowAgentTransferDialog(legacyResponse.data);
+ } else {
+ adminShowManualTransferDialog();
+ }
+ }).fail(function() {
+ adminShowManualTransferDialog();
+ });
}
}).fail(function() {
adminShowManualTransferDialog();
});
}
+ function adminShowEnhancedTransferDialog(data) {
+ var agentOptions = '
';
+
+ // Add users with extensions
+ if (data.users && data.users.length > 0) {
+ agentOptions += '
Transfer to Agent
';
+ data.users.forEach(function(user) {
+ var statusClass = user.is_logged_in ? 'available' : 'offline';
+ var statusText = user.is_logged_in ? 'π’ Online' : 'π΄ Offline';
+ var statusColor = user.is_logged_in ? '#28a745' : '#dc3545';
+
+ agentOptions += '
';
+ agentOptions += '
';
+ agentOptions += '
' + user.display_name + '
Ext: ' + user.extension + '
';
+ agentOptions += '
' + statusText + '
';
+ agentOptions += '
';
+ agentOptions += '
';
+ });
+ agentOptions += '
';
+ }
+
+ // Add general queues
+ if (data.queues && data.queues.length > 0) {
+ agentOptions += '
Transfer to Queue
';
+ data.queues.forEach(function(queue) {
+ agentOptions += '
';
+ agentOptions += '
';
+ agentOptions += '
' + queue.queue_name + '
';
+ agentOptions += '
' + queue.waiting_calls + ' waiting
';
+ agentOptions += '
';
+ agentOptions += '
';
+ });
+ agentOptions += '
';
+ }
+
+ agentOptions += '
';
+
+ var dialogHtml = '
';
+ dialogHtml += '
Transfer Call
';
+ dialogHtml += '
Select an agent or queue:
';
+ dialogHtml += agentOptions;
+ dialogHtml += '
';
+ dialogHtml += '
Manual Transfer
';
+ dialogHtml += '
Or enter extension or phone number:
';
+ dialogHtml += '
';
+ dialogHtml += '
';
+ dialogHtml += '
';
+ dialogHtml += '';
+ dialogHtml += '';
+ dialogHtml += '
';
+ dialogHtml += '
';
+ dialogHtml += '
';
+
+ $('body').append(dialogHtml);
+
+ var selectedTransfer = null;
+
+ $('.agent-option, .queue-option').on('click', function() {
+ $('.agent-option, .queue-option').css('background', 'white');
+ $(this).css('background', '#e7f3ff');
+
+ selectedTransfer = {
+ type: $(this).data('transfer-type'),
+ target: $(this).data('transfer-target'),
+ agentId: $(this).data('agent-id'),
+ queueId: $(this).data('queue-id')
+ };
+
+ $('#admin-transfer-manual').val('');
+ $('#admin-confirm-transfer').prop('disabled', false);
+ });
+
+ $('#admin-transfer-manual').on('input', function() {
+ var input = $(this).val().trim();
+ if (input) {
+ $('.agent-option, .queue-option').css('background', 'white');
+
+ // Determine if it's an extension or phone number
+ var transferType, transferTarget;
+ if (/^\d{3,4}$/.test(input)) {
+ transferType = 'extension';
+ transferTarget = input;
+ } else {
+ transferType = 'phone';
+ transferTarget = input;
+ }
+
+ selectedTransfer = { type: transferType, target: transferTarget };
+ $('#admin-confirm-transfer').prop('disabled', false);
+ } else {
+ $('#admin-confirm-transfer').prop('disabled', !selectedTransfer);
+ }
+ });
+
+ $('#admin-confirm-transfer').on('click', function() {
+ console.log('Transfer button clicked, selectedTransfer:', selectedTransfer);
+ if (selectedTransfer) {
+ console.log('Calling adminTransferToTarget with:', selectedTransfer.type, selectedTransfer.target);
+ adminTransferToTarget(selectedTransfer.type, selectedTransfer.target);
+ } else {
+ console.error('No transfer selected');
+ alert('Please select a transfer target first');
+ }
+ });
+
+ $('#admin-cancel-transfer, #admin-transfer-overlay').on('click', function() {
+ adminHideTransferDialog();
+ });
+ }
+
function adminShowAgentTransferDialog(agents) {
var agentOptions = '
';
@@ -7714,6 +7983,60 @@ class TWP_Admin {
});
}
+ function adminTransferToTarget(transferType, transferTarget) {
+ console.log('adminTransferToTarget called with:', transferType, transferTarget);
+
+ if (!currentCall) {
+ console.error('No current call for transfer');
+ alert('No active call to transfer');
+ return;
+ }
+
+ var callSid = currentCall.parameters.CallSid ||
+ currentCall.customParameters.CallSid ||
+ currentCall.outgoingConnectionId ||
+ currentCall.sid;
+
+ console.log('Transfer call SID:', callSid);
+
+ if (!callSid) {
+ alert('Unable to identify call for transfer');
+ return;
+ }
+
+ // Use the correct parameter format expected by ajax_transfer_call
+ var requestData = {
+ action: 'twp_transfer_call',
+ call_sid: callSid,
+ nonce: ''
+ };
+
+ // Determine if it's an extension or phone number
+ if (/^\d{3,4}$/.test(transferTarget)) {
+ // It's an extension - use new format
+ requestData.target_queue_id = transferTarget;
+ } else {
+ // It's a phone number - use legacy format
+ requestData.transfer_type = 'phone';
+ requestData.transfer_target = transferTarget;
+ }
+
+ console.log('Sending transfer request:', requestData);
+
+ $.post(ajaxurl, requestData, function(response) {
+ console.log('Transfer response:', response);
+ if (response.success) {
+ alert('Call transferred successfully');
+ adminHideTransferDialog();
+ } else {
+ alert('Failed to transfer call: ' + (response.data || response.error || 'Unknown error'));
+ }
+ }).fail(function(xhr, status, error) {
+ console.error('Transfer request failed:', xhr, status, error);
+ alert('Failed to transfer call - network error');
+ });
+ }
+
function adminTransferCall(phoneNumber) {
if (!currentCall) return;
@@ -7997,6 +8320,88 @@ class TWP_Admin {
return array_values($queues);
}
+ /**
+ * Helper function to identify the customer call leg for browser phone calls
+ *
+ * @param string $call_sid The call SID to analyze
+ * @param TWP_Twilio_API $api Twilio API instance
+ * @return string|null The customer call SID or null if not found
+ */
+ private function find_customer_call_leg($call_sid, $api) {
+ try {
+ $client = $api->get_client();
+ $call = $client->calls($call_sid)->fetch();
+ $target_call_sid = null;
+
+ error_log("TWP Call Leg Detection: Call SID {$call_sid} - From: {$call->from}, To: {$call->to}, Direction: {$call->direction}, Parent: " . ($call->parentCallSid ?: 'none'));
+
+ // For browser phone calls (outbound), we need to find the customer leg
+ if (strpos($call->from, 'client:') === 0 || strpos($call->to, 'client:') === 0) {
+ error_log("TWP Call Leg Detection: Browser phone call detected");
+
+ // This is a browser phone call, find the customer leg
+ if ($call->parentCallSid) {
+ // Check parent call
+ try {
+ $parent_call = $client->calls($call->parentCallSid)->fetch();
+ if (strpos($parent_call->from, 'client:') === false && strpos($parent_call->to, 'client:') === false) {
+ $target_call_sid = $parent_call->sid;
+ error_log("TWP Call Leg Detection: Using parent call as customer leg: {$target_call_sid}");
+ }
+ } catch (Exception $e) {
+ error_log("TWP Call Leg Detection: Could not fetch parent call: " . $e->getMessage());
+ }
+ }
+
+ // If no parent or parent is also client, search for related customer call
+ if (!$target_call_sid) {
+ $active_calls = $client->calls->read(['status' => 'in-progress'], 50);
+ foreach ($active_calls as $active_call) {
+ if ($active_call->sid === $call_sid) continue; // Skip current call
+
+ // Check if calls are related and this one doesn't involve a client
+ $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->from, 'client:') === false &&
+ strpos($active_call->to, 'client:') === false) {
+ $target_call_sid = $active_call->sid;
+ error_log("TWP Call Leg Detection: Found related customer call: {$target_call_sid}");
+ break;
+ }
+ }
+ }
+
+ // Store the relationship for future use
+ if ($target_call_sid) {
+ error_log("TWP Call Leg Detection: Agent leg {$call_sid} -> Customer leg {$target_call_sid}");
+ }
+
+ } else {
+ // Regular inbound call - current call IS the customer
+ $target_call_sid = $call_sid;
+ error_log("TWP Call Leg Detection: Regular inbound call, current call is customer");
+ }
+
+ if (!$target_call_sid) {
+ error_log("TWP Call Leg Detection: Could not determine customer leg, using current call as fallback");
+ $target_call_sid = $call_sid;
+ }
+
+ return $target_call_sid;
+
+ } catch (Exception $e) {
+ error_log("TWP Call Leg Detection Error: " . $e->getMessage());
+ return $call_sid; // Fallback to original call
+ }
+ }
+
/**
* AJAX handler for toggling call hold
*/
@@ -8010,219 +8415,160 @@ class TWP_Admin {
$hold = filter_var($_POST['hold'], FILTER_VALIDATE_BOOLEAN);
try {
- $twilio = new TWP_Twilio_API();
- $client = $twilio->get_client();
+ // Get Twilio API instance
+ require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-twilio-api.php';
+ $api = new TWP_Twilio_API();
if ($hold) {
- // For browser phone calls, we need to find the customer's call leg and put THAT on hold
- $hold_music_url = get_option('twp_hold_music_url', '');
- if (empty($hold_music_url)) {
- $hold_music_url = get_option('twp_default_queue_music_url', 'https://www.soundjay.com/misc/sounds/bell-ringing-05.wav');
+ // Put call on hold using Hold Queue system
+ error_log("TWP: Putting call on hold - SID: {$call_sid}");
+
+ // Use helper function to identify the customer call leg
+ $target_call_sid = $this->find_customer_call_leg($call_sid, $api);
+
+ // Get current user ID for hold queue management
+ $current_user_id = get_current_user_id();
+ if (!$current_user_id) {
+ error_log("TWP: Hold failed - no current user");
+ wp_send_json_error('Failed to hold call: No user context');
+ return;
}
- // Get the call details to understand the call structure
- $call = $client->calls($call_sid)->fetch();
- error_log("TWP Hold: Initial call SID $call_sid - From: {$call->from}, To: {$call->to}, Direction: {$call->direction}, Parent: {$call->parentCallSid}");
+ // Use the Hold Queue system to properly hold the call
+ require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-user-queue-manager.php';
+ $queue_result = TWP_User_Queue_Manager::transfer_to_hold_queue($current_user_id, $target_call_sid);
- // 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 ($queue_result['success']) {
+ // Get the hold queue details
+ global $wpdb;
+ $hold_queue = $wpdb->get_row($wpdb->prepare(
+ "SELECT * FROM {$wpdb->prefix}twp_call_queues WHERE id = %d",
+ $queue_result['hold_queue_id']
+ ));
- // 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");
+ if ($hold_queue) {
+ // Create TwiML for hold experience
+ $twiml = new \Twilio\TwiML\VoiceResponse();
- 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 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([
+ // Use TTS helper with caching for hold message
+ $hold_message = $hold_queue->tts_message ?: 'Your call is on hold. Please wait.';
+
+ require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-tts-helper.php';
+ $tts_helper = TWP_TTS_Helper::get_instance();
+ $tts_helper->add_tts_to_twiml($twiml, $hold_message);
+
+ // Use default hold music URL or custom one from settings
+ $hold_music_url = get_option('twp_hold_music_url', 'http://com.twilio.sounds.music.s3.amazonaws.com/MARKOVICHAMP-Borghestral.mp3');
+ $twiml->play($hold_music_url, ['loop' => 0]); // Loop indefinitely
+
+ // Update the customer call leg with hold experience
+ $result = $api->update_call($target_call_sid, [
'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;
+
+ if ($result['success']) {
+ error_log("TWP: Successfully put call on hold in queue - Target: {$target_call_sid} -> Hold Queue: {$queue_result['hold_queue_id']}");
+ wp_send_json_success(array(
+ 'message' => 'Call placed on hold',
+ 'target_call_sid' => $target_call_sid,
+ 'hold_queue_id' => $queue_result['hold_queue_id']
+ ));
+ } else {
+ error_log("TWP: Failed to update call for hold - " . $result['error']);
+ wp_send_json_error('Failed to place call on hold: ' . $result['error']);
+ }
+ } else {
+ error_log("TWP: Hold failed - hold queue not found: " . $queue_result['hold_queue_id']);
+ wp_send_json_error('Failed to hold call: Hold queue not found');
}
} 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()
- ]);
+ error_log("TWP: Failed to transfer to hold queue - " . $queue_result['error']);
+ wp_send_json_error('Failed to hold call: ' . $queue_result['error']);
}
+
} else {
- // 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}");
+ // Resume call from hold queue
+ error_log("TWP: Resuming call from hold - SID: {$call_sid}");
- $target_sid = null;
+ // Use helper function to identify the customer call leg
+ $target_call_sid = $this->find_customer_call_leg($call_sid, $api);
- // 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 {
- $target_sid = $call_sid;
+ // Get current user ID for hold queue management
+ $current_user_id = get_current_user_id();
+ if (!$current_user_id) {
+ error_log("TWP: Resume failed - no current user");
+ wp_send_json_error('Failed to resume call: No user context');
+ return;
}
- // Resume the determined target
- if ($target_sid) {
- error_log("TWP Resume: Resuming call - Target SID: {$target_sid}");
+ // Use the Hold Queue system to properly resume the call
+ require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-user-queue-manager.php';
+ $queue_result = TWP_User_Queue_Manager::resume_from_hold($current_user_id, $target_call_sid);
+
+ if ($queue_result['success']) {
+ // Get the target queue details to redirect the call properly
+ global $wpdb;
+ $queue = $wpdb->get_row($wpdb->prepare(
+ "SELECT * FROM {$wpdb->prefix}twp_call_queues WHERE id = %d",
+ $queue_result['target_queue_id']
+ ));
- // For resuming, we need to stop the hold music and restore the conversation
- // The key is to return control to the normal call flow
-
- try {
- // Get the target call details to understand the call structure
- $target_call = $client->calls($target_sid)->fetch();
- error_log("TWP Resume: Target call - From: {$target_call->from}, To: {$target_call->to}, Parent: {$target_call->parentCallSid}");
-
- // For resuming, the simplest approach is often the best
- // Just send empty TwiML to stop hold music and resume normal call flow
+ if ($queue) {
+ // Create TwiML to redirect to the target queue
$twiml = new \Twilio\TwiML\VoiceResponse();
- // Don't add anything - empty TwiML should resume the call
- // The connection between parties should still exist
+ // If it's a personal queue, try to connect directly to agent
+ if ($queue->queue_type === 'personal') {
+ $twiml->say('Resuming your call.', ['voice' => 'alice']);
+
+ // Get the agent's phone number
+ $agent_number = get_user_meta($current_user_id, 'twp_phone_number', true);
+ if ($agent_number) {
+ $dial = $twiml->dial(['timeout' => 30]);
+ $dial->number($agent_number);
+ } else {
+ $twiml->say('Unable to locate agent. Please try again.', ['voice' => 'alice']);
+ $twiml->hangup();
+ }
+ } else {
+ // Regular queue - redirect to queue wait
+ $queue_wait_url = home_url('/wp-json/twilio-webhook/v1/queue-wait');
+ $queue_wait_url = add_query_arg(array(
+ 'queue_id' => $queue_result['target_queue_id']
+ ), $queue_wait_url);
+
+ $twiml->redirect($queue_wait_url, ['method' => 'POST']);
+ }
- // Update the call with resume TwiML
- $result = $client->calls($target_sid)->update([
+ // Update the customer call leg with resume TwiML
+ $result = $api->update_call($target_call_sid, [
'twiml' => $twiml->asXML()
]);
- error_log("TWP Resume: Successfully updated call {$target_sid} with resume TwiML");
- } catch (Exception $e) {
- error_log("TWP Resume: Error resuming call: " . $e->getMessage());
-
- // Simple fallback - just stop hold music with empty TwiML
- $twiml = new \Twilio\TwiML\VoiceResponse();
- $client->calls($target_sid)->update([
- 'twiml' => $twiml->asXML()
- ]);
- error_log("TWP Resume: Used simple fallback for {$target_sid}");
+ if ($result['success']) {
+ error_log("TWP: Successfully resumed call from hold queue - Target: {$target_call_sid} -> Queue: {$queue_result['target_queue_id']}");
+ wp_send_json_success(array(
+ 'message' => 'Call resumed from hold',
+ 'target_call_sid' => $target_call_sid,
+ 'target_queue_id' => $queue_result['target_queue_id']
+ ));
+ } else {
+ error_log("TWP: Failed to update call for resume - " . $result['error']);
+ wp_send_json_error('Failed to resume call: ' . $result['error']);
+ }
+ } else {
+ error_log("TWP: Resume failed - target queue not found: " . $queue_result['target_queue_id']);
+ wp_send_json_error('Failed to resume call: Target queue not found');
}
} else {
- error_log("TWP Resume: WARNING - Could not determine target, resuming current call");
-
- // Resume current call as fallback
- try {
- $twiml = new \Twilio\TwiML\VoiceResponse();
- $client->calls($call_sid)->update([
- 'twiml' => $twiml->asXML()
- ]);
- } catch (Exception $e) {
- error_log("TWP Resume: Failed to resume current call: " . $e->getMessage());
- throw $e;
- }
+ error_log("TWP: Failed to resume from hold queue - " . $queue_result['error']);
+ wp_send_json_error('Failed to resume call: ' . $queue_result['error']);
}
}
- wp_send_json_success(['message' => $hold ? 'Call on hold' : 'Call resumed']);
} catch (Exception $e) {
- wp_send_json_error('Failed to toggle hold: ' . $e->getMessage());
+ error_log("TWP Hold Error: " . $e->getMessage());
+ wp_send_json_error('Hold operation failed: ' . $e->getMessage());
}
}
@@ -8302,73 +8648,154 @@ class TWP_Admin {
}
$call_sid = sanitize_text_field($_POST['call_sid']);
- $transfer_type = sanitize_text_field($_POST['transfer_type']); // 'phone' or 'queue'
- $transfer_target = sanitize_text_field($_POST['transfer_target']);
+
+ // Handle both old and new parameter formats
+ if (isset($_POST['target_queue_id'])) {
+ // New format from enhanced queue system
+ $current_queue_id = isset($_POST['current_queue_id']) ? intval($_POST['current_queue_id']) : null;
+ $target = sanitize_text_field($_POST['target_queue_id']); // Can be queue ID or extension
+ } else {
+ // Legacy format
+ $transfer_type = sanitize_text_field($_POST['transfer_type'] ?? 'queue');
+ $target = sanitize_text_field($_POST['transfer_target'] ?? '');
+ }
try {
$twilio = new TWP_Twilio_API();
- $client = $twilio->get_client();
+ global $wpdb;
- if ($transfer_type === 'queue') {
- // Create TwiML using the working TWP_Twilio_API method for queue transfers
- $wait_url = home_url('/wp-json/twilio-webhook/v1/queue-wait');
- $twiml_xml = $twilio->create_queue_twiml($transfer_target, 'Transferring your call to another agent. Please hold.', $wait_url);
+ // Check if target is an extension (3-4 digits)
+ if (is_numeric($target) && strlen($target) <= 4) {
+ // It's an extension, find the user's queue
+ $user_id = TWP_User_Queue_Manager::get_user_by_extension($target);
- // 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');
+ if (!$user_id) {
+ wp_send_json_error('Extension not found');
return;
}
- // Create TwiML for phone transfer
- $twiml = new \Twilio\TwiML\VoiceResponse();
- $twiml->say('Transferring your call. Please hold.');
- $twiml->dial($transfer_target);
- $twiml_xml = $twiml->asXML();
+ $extension_data = TWP_User_Queue_Manager::get_user_extension_data($user_id);
+ $target_queue_id = $extension_data['personal_queue_id'];
+
+ // Move call to new queue using our queue system
+ $next_position = $wpdb->get_var($wpdb->prepare(
+ "SELECT COALESCE(MAX(position), 0) + 1 FROM {$wpdb->prefix}twp_queued_calls
+ WHERE queue_id = %d AND status = 'waiting'",
+ $target_queue_id
+ ));
+
+ $result = $wpdb->update(
+ $wpdb->prefix . 'twp_queued_calls',
+ array(
+ 'queue_id' => $target_queue_id,
+ 'position' => $next_position
+ ),
+ array('call_sid' => $call_sid),
+ array('%d', '%d'),
+ array('%s')
+ );
+
+ if ($result !== false) {
+ // Update call with new queue wait URL
+ $twilio->update_call($call_sid, array(
+ 'url' => site_url('/wp-json/twilio-webhook/v1/queue-wait?queue_id=' . $target_queue_id)
+ ));
+
+ wp_send_json_success(['message' => 'Call transferred to extension ' . $target]);
+ } else {
+ wp_send_json_error('Failed to transfer call to queue');
+ }
+
+ } elseif (is_numeric($target) && strlen($target) > 4) {
+ // It's a queue ID
+ $target_queue_id = intval($target);
+
+ // Move call to new queue
+ $next_position = $wpdb->get_var($wpdb->prepare(
+ "SELECT COALESCE(MAX(position), 0) + 1 FROM {$wpdb->prefix}twp_queued_calls
+ WHERE queue_id = %d AND status = 'waiting'",
+ $target_queue_id
+ ));
+
+ $result = $wpdb->update(
+ $wpdb->prefix . 'twp_queued_calls',
+ array(
+ 'queue_id' => $target_queue_id,
+ 'position' => $next_position
+ ),
+ array('call_sid' => $call_sid),
+ array('%d', '%d'),
+ array('%s')
+ );
+
+ if ($result !== false) {
+ // Find customer call leg for transfer (important for outbound calls)
+ $customer_call_sid = $this->find_customer_call_leg($call_sid, $twilio);
+ error_log("TWP Transfer: Using customer call leg {$customer_call_sid} for queue transfer (original: {$call_sid})");
+
+ // Update customer call with new queue wait URL
+ $twilio->update_call($customer_call_sid, array(
+ 'url' => site_url('/wp-json/twilio-webhook/v1/queue-wait?queue_id=' . $target_queue_id)
+ ));
+
+ wp_send_json_success(['message' => 'Call transferred to queue']);
+ } else {
+ wp_send_json_error('Failed to transfer call to queue');
+ }
+
+ } else {
+ // Transfer to phone number or client endpoint
+
+ // Check if it's a client endpoint (browser phone)
+ if (strpos($target, 'client:') === 0) {
+ // Extract agent name from client identifier
+ $agent_name = substr($target, 7); // Remove 'client:' prefix
+
+ // Create TwiML for client transfer
+ $twiml = new \Twilio\TwiML\VoiceResponse();
+ $twiml->say('Transferring your call to ' . $agent_name . '. Please hold.');
+
+ // Use Dial with client endpoint
+ $dial = $twiml->dial();
+ $dial->client($agent_name);
+
+ $twiml_xml = $twiml->asXML();
+
+ // Find customer call leg for transfer (important for outbound calls)
+ $customer_call_sid = $this->find_customer_call_leg($call_sid, $twilio);
+ error_log("TWP Transfer: Using customer call leg {$customer_call_sid} for client transfer (original: {$call_sid})");
+
+ // Update the customer call with the transfer TwiML
+ $client = $twilio->get_client();
+ $call = $client->calls($customer_call_sid)->update([
+ 'twiml' => $twiml_xml
+ ]);
+
+ wp_send_json_success(['message' => 'Call transferred to agent ' . $agent_name]);
+
+ } elseif (preg_match('/^\+?[1-9]\d{1,14}$/', $target)) {
+ // Transfer to phone number
+ $twiml = new \Twilio\TwiML\VoiceResponse();
+ $twiml->say('Transferring your call. Please hold.');
+ $twiml->dial($target);
+ $twiml_xml = $twiml->asXML();
+
+ // Find customer call leg for transfer (important for outbound calls)
+ $customer_call_sid = $this->find_customer_call_leg($call_sid, $twilio);
+ error_log("TWP Transfer: Using customer call leg {$customer_call_sid} for phone transfer (original: {$call_sid})");
+
+ // Update the customer call with the transfer TwiML
+ $client = $twilio->get_client();
+ $call = $client->calls($customer_call_sid)->update([
+ 'twiml' => $twiml_xml
+ ]);
+
+ wp_send_json_success(['message' => 'Call transferred to ' . $target]);
+ } else {
+ wp_send_json_error('Invalid transfer target format. Expected phone number or client endpoint.');
+ }
}
- // Update the call with the transfer TwiML
- $call = $client->calls($call_sid)->update([
- 'twiml' => $twiml_xml
- ]);
-
- wp_send_json_success(['message' => 'Call transferred successfully']);
} catch (Exception $e) {
wp_send_json_error('Failed to transfer call: ' . $e->getMessage());
}
@@ -8414,12 +8841,16 @@ class TWP_Admin {
$twilio = new TWP_Twilio_API();
$client = $twilio->get_client();
+ // Find the customer call leg for requeue (important for outbound calls)
+ $customer_call_sid = $this->find_customer_call_leg($call_sid, $twilio);
+ error_log("TWP Requeue: Using customer call leg {$customer_call_sid} for requeue (original: {$call_sid})");
+
// Create TwiML using the TWP_Twilio_API method that works
$wait_url = home_url('/wp-json/twilio-webhook/v1/queue-wait');
$twiml_xml = $twilio->create_queue_twiml($queue->queue_name, 'Placing you back in the queue. Please hold.', $wait_url);
- // Update the call with the requeue TwiML
- $call = $client->calls($call_sid)->update([
+ // Update the customer call with the requeue TwiML
+ $call = $client->calls($customer_call_sid)->update([
'twiml' => $twiml_xml
]);
@@ -8429,7 +8860,7 @@ class TWP_Admin {
// Use enqueued_at if available, fallback to joined_at for compatibility
$insert_data = [
'queue_id' => $queue_id,
- 'call_sid' => $call_sid,
+ 'call_sid' => $customer_call_sid, // Use customer call SID for tracking
'from_number' => $call->from,
'to_number' => $call->to ?: '',
'position' => 1, // Will be updated by queue manager
@@ -8507,73 +8938,64 @@ class TWP_Admin {
global $wpdb;
$recordings_table = $wpdb->prefix . 'twp_call_recordings';
- // For outbound calls from browser phone, determine the customer number
+ // Enhanced customer number detection using our call leg detection system
$from_number = $call->from;
$to_number = $call->to;
error_log("TWP Recording: Initial call data - From: {$call->from}, To: {$call->to}, Direction: {$call->direction}");
- // If this is a browser phone call (from contains 'client:'), find the customer number
- if (strpos($call->from, 'client:') === 0) {
- // This is an outbound call from browser phone
- error_log("TWP Recording: Detected browser phone outbound call");
+ // If this is a browser phone call, use our helper to find the customer number
+ if (strpos($call->from, 'client:') === 0 || strpos($call->to, 'client:') === 0) {
+ error_log("TWP Recording: Detected browser phone call, finding customer number");
- // For browser phone calls, we need to find the customer number
- // It might be in 'to' field, or we might need to look at related calls
- $customer_number = null;
+ // Find the customer call leg using our helper function
+ $customer_call_sid = $this->find_customer_call_leg($call_sid, $twilio);
- // First try the 'to' field
- if (!empty($call->to) && strpos($call->to, 'client:') === false) {
- $customer_number = $call->to;
- error_log("TWP Recording: Found customer number in 'to' field: {$customer_number}");
- } else {
- // If 'to' is empty or also a client, look for related calls
- error_log("TWP Recording: 'to' field empty or client, looking for related calls");
-
+ if ($customer_call_sid && $customer_call_sid !== $call_sid) {
+ // Get the customer call details
try {
- $twilio = new TWP_Twilio_API();
- $client_api = $twilio->get_client();
+ $customer_call = $client->calls($customer_call_sid)->fetch();
- // Get all recent calls to find the customer leg
- $related_calls = $client_api->calls->read(['status' => 'in-progress'], 20);
+ // Determine which field has the customer number
+ $customer_number = null;
- foreach ($related_calls as $related_call) {
- // Skip the current call
- if ($related_call->sid === $call_sid) continue;
-
- // Look for calls with same parent or that are our parent/child
- if (($call->parentCallSid && $related_call->parentCallSid === $call->parentCallSid) ||
- $related_call->parentCallSid === $call_sid ||
- $related_call->sid === $call->parentCallSid) {
-
- // Check if this call has a real phone number (not client)
- if (strpos($related_call->from, 'client:') === false &&
- strpos($related_call->from, '+') === 0) {
- $customer_number = $related_call->from;
- error_log("TWP Recording: Found customer number in related call 'from': {$customer_number}");
- break;
- } elseif (strpos($related_call->to, 'client:') === false &&
- strpos($related_call->to, '+') === 0) {
- $customer_number = $related_call->to;
- error_log("TWP Recording: Found customer number in related call 'to': {$customer_number}");
- break;
- }
- }
+ // For outbound calls, customer is usually in 'to' of the customer leg
+ // For inbound calls, customer is usually in 'from' of the customer leg
+ if (strpos($customer_call->from, 'client:') === false && strpos($customer_call->from, '+') === 0) {
+ $customer_number = $customer_call->from;
+ error_log("TWP Recording: Found customer number in customer leg 'from': {$customer_number}");
+ } elseif (strpos($customer_call->to, 'client:') === false && strpos($customer_call->to, '+') === 0) {
+ $customer_number = $customer_call->to;
+ error_log("TWP Recording: Found customer number in customer leg 'to': {$customer_number}");
}
+
+ if ($customer_number) {
+ // Store in database with customer number as 'from' for consistency
+ $from_number = $customer_number;
+ $to_number = $call->from; // Agent/browser client
+ error_log("TWP Recording: Browser phone call - Customer: {$customer_number}, Agent: {$call->from}");
+ } else {
+ error_log("TWP Recording: WARNING - Customer call leg found but no customer number detected");
+ }
+
} catch (Exception $e) {
- error_log("TWP Recording: Error looking for related calls: " . $e->getMessage());
+ error_log("TWP Recording: Error fetching customer call details: " . $e->getMessage());
+ }
+ } else {
+ error_log("TWP Recording: Could not find separate customer call leg");
+
+ // Fallback: if 'to' is not a client, use it as customer number
+ if (!empty($call->to) && strpos($call->to, 'client:') === false && strpos($call->to, '+') === 0) {
+ $from_number = $call->to; // Customer number
+ $to_number = $call->from; // Agent client
+ error_log("TWP Recording: Using 'to' field as customer number: {$call->to}");
+ } else {
+ error_log("TWP Recording: WARNING - Could not determine customer number for browser phone call");
}
}
-
- if ($customer_number) {
- // Store customer number in 'from' for display purposes
- $from_number = $customer_number;
- $to_number = $call->from; // Browser phone client
- error_log("TWP Recording: Outbound call - Customer: {$customer_number}, Agent: {$call->from}");
- } else {
- error_log("TWP Recording: WARNING - Could not determine customer number for outbound call");
- // Keep original values but log the issue
- }
+ } else {
+ // Regular inbound call - customer is 'from', agent is 'to'
+ error_log("TWP Recording: Regular call - keeping original from/to values");
}
$insert_result = $wpdb->insert($recordings_table, [
@@ -8639,8 +9061,9 @@ class TWP_Admin {
$twilio = new TWP_Twilio_API();
$client = $twilio->get_client();
- $recording = $client->recordings($recording_sid)->update(['status' => 'stopped']);
- error_log("TWP: Successfully stopped recording $recording_sid in Twilio despite DB issue");
+ // We don't have the call SID, so we can't stop the recording
+ // Log the issue and return an appropriate error
+ error_log("TWP: Cannot stop recording $recording_sid - not found in database and need call SID to stop via API");
wp_send_json_success(['message' => 'Recording stopped (was not tracked in database)']);
return;
@@ -8662,8 +9085,31 @@ class TWP_Admin {
$client = $twilio->get_client();
// Try to stop the recording in Twilio
+ // In Twilio SDK v8, you stop a recording via the call's recordings subresource
try {
- $recording = $client->recordings($recording_sid)->update(['status' => 'stopped']);
+ // If we have multiple recordings, we need the specific recording SID
+ // If there's only one recording, we can use 'Twilio.CURRENT'
+ if ($recording_info && $recording_info->call_sid) {
+ try {
+ // First try with the specific recording SID
+ $client->calls($recording_info->call_sid)
+ ->recordings($recording_sid)
+ ->update(['status' => 'stopped']);
+ error_log("TWP: Successfully stopped recording $recording_sid for call {$recording_info->call_sid}");
+ } catch (Exception $e) {
+ // If that fails, try with Twilio.CURRENT (for single recording)
+ try {
+ $client->calls($recording_info->call_sid)
+ ->recordings('Twilio.CURRENT')
+ ->update(['status' => 'stopped']);
+ error_log("TWP: Stopped recording using Twilio.CURRENT for call {$recording_info->call_sid}");
+ } catch (Exception $e2) {
+ error_log('TWP: Could not stop recording - it may already be stopped: ' . $e2->getMessage());
+ }
+ }
+ } else {
+ error_log('TWP: Could not find call SID for recording ' . $recording_sid);
+ }
} catch (Exception $twilio_error) {
// Recording might already be stopped or completed on Twilio's side
error_log('TWP: Could not stop recording in Twilio (may already be stopped): ' . $twilio_error->getMessage());
diff --git a/includes/class-twp-tts-helper.php b/includes/class-twp-tts-helper.php
new file mode 100644
index 0000000..2dec60f
--- /dev/null
+++ b/includes/class-twp-tts-helper.php
@@ -0,0 +1,220 @@
+use_elevenlabs = true;
+ require_once plugin_dir_path(dirname(__FILE__)) . 'includes/class-twp-elevenlabs-api.php';
+ $this->elevenlabs_api = new TWP_ElevenLabs_API();
+ }
+ }
+
+ /**
+ * Get cache key for text
+ */
+ private function get_cache_key($text) {
+ $voice_id = get_option('twp_elevenlabs_voice_id');
+ $model_id = get_option('twp_elevenlabs_model_id', 'eleven_multilingual_v2');
+ return 'twp_tts_' . md5($text . $voice_id . $model_id);
+ }
+
+ /**
+ * Get cached audio URL if exists
+ */
+ private function get_cached_audio($text) {
+ $cache_key = $this->get_cache_key($text);
+ $cached_data = get_transient($cache_key);
+
+ if ($cached_data !== false) {
+ // Verify the file still exists
+ if (file_exists($cached_data['file_path'])) {
+ error_log("TWP TTS: Using cached audio for text: " . substr($text, 0, 50) . "...");
+ return $cached_data;
+ } else {
+ // File was deleted, remove from cache
+ delete_transient($cache_key);
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Save audio to cache
+ */
+ private function cache_audio($text, $audio_data) {
+ $cache_key = $this->get_cache_key($text);
+ // Cache for 30 days
+ set_transient($cache_key, $audio_data, 30 * DAY_IN_SECONDS);
+ }
+
+ /**
+ * Add TTS to TwiML Response
+ *
+ * @param \Twilio\TwiML\VoiceResponse $twiml The TwiML response object
+ * @param string $text The text to speak
+ * @param array $options Options for voice settings (used for Twilio fallback)
+ * @return bool Success status
+ */
+ public function add_tts_to_twiml($twiml, $text, $options = []) {
+ // Default Twilio voice options
+ $default_options = [
+ 'voice' => 'alice',
+ 'language' => 'en-US'
+ ];
+ $options = array_merge($default_options, $options);
+
+ if ($this->use_elevenlabs) {
+ // First check cache
+ $cached_audio = $this->get_cached_audio($text);
+
+ if ($cached_audio !== false) {
+ $twiml->play($cached_audio['file_url']);
+ return true;
+ }
+
+ // Not in cache, generate new audio
+ $audio_result = $this->elevenlabs_api->text_to_speech($text);
+
+ if ($audio_result && isset($audio_result['success']) && $audio_result['success']) {
+ // Cache the result
+ $this->cache_audio($text, [
+ 'file_url' => $audio_result['file_url'],
+ 'file_path' => $audio_result['file_path']
+ ]);
+
+ // Use the generated audio file
+ $twiml->play($audio_result['file_url']);
+ error_log("TWP TTS: Generated new ElevenLabs audio for text: " . substr($text, 0, 50) . "...");
+ return true;
+ } else {
+ // Log the failure and fall back to Twilio
+ error_log("TWP TTS: ElevenLabs failed, falling back to Twilio. Error: " .
+ (isset($audio_result['error']) ? $audio_result['error'] : 'Unknown error'));
+ }
+ }
+
+ // Fall back to Twilio's built-in TTS
+ $twiml->say($text, $options);
+ error_log("TWP TTS: Using Twilio voice for text: " . substr($text, 0, 50) . "...");
+ return true;
+ }
+
+ /**
+ * Generate TTS audio file (for pre-generation)
+ *
+ * @param string $text The text to convert
+ * @return array|false Array with file_url on success, false on failure
+ */
+ public function generate_tts_audio($text) {
+ if (!$this->use_elevenlabs) {
+ return false;
+ }
+
+ // First check cache
+ $cached_audio = $this->get_cached_audio($text);
+ if ($cached_audio !== false) {
+ return [
+ 'success' => true,
+ 'file_url' => $cached_audio['file_url'],
+ 'file_path' => $cached_audio['file_path'],
+ 'cached' => true
+ ];
+ }
+
+ // Not in cache, generate new audio
+ $result = $this->elevenlabs_api->text_to_speech($text);
+
+ if ($result && isset($result['success']) && $result['success']) {
+ // Cache the result
+ $this->cache_audio($text, [
+ 'file_url' => $result['file_url'],
+ 'file_path' => $result['file_path']
+ ]);
+
+ return [
+ 'success' => true,
+ 'file_url' => $result['file_url'],
+ 'file_path' => $result['file_path'],
+ 'cached' => false
+ ];
+ }
+
+ return false;
+ }
+
+ /**
+ * Check if ElevenLabs is configured and available
+ *
+ * @return bool
+ */
+ public function is_elevenlabs_available() {
+ return $this->use_elevenlabs;
+ }
+
+ /**
+ * Get configured voice info
+ *
+ * @return array
+ */
+ public function get_voice_info() {
+ if ($this->use_elevenlabs) {
+ return [
+ 'provider' => 'elevenlabs',
+ 'voice_id' => get_option('twp_elevenlabs_voice_id'),
+ 'model_id' => get_option('twp_elevenlabs_model_id', 'eleven_multilingual_v2')
+ ];
+ }
+
+ return [
+ 'provider' => 'twilio',
+ 'voice' => 'alice',
+ 'language' => 'en-US'
+ ];
+ }
+
+ /**
+ * Clean up old TTS files (maintenance)
+ */
+ public function cleanup_old_files($hours = 24) {
+ $upload_dir = wp_upload_dir();
+ $path = $upload_dir['path'];
+
+ // Only clean up TTS files older than specified hours
+ $expire_time = time() - ($hours * 3600);
+
+ $files = glob($path . '/tts_*.mp3');
+ if ($files) {
+ foreach ($files as $file) {
+ if (filemtime($file) < $expire_time) {
+ unlink($file);
+ error_log("TWP TTS: Cleaned up old file: " . basename($file));
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/includes/class-twp-webhooks.php b/includes/class-twp-webhooks.php
index 4767fb6..5a84515 100644
--- a/includes/class-twp-webhooks.php
+++ b/includes/class-twp-webhooks.php
@@ -1297,8 +1297,11 @@ class TWP_Webhooks {
$from = isset($params['From']) ? $params['From'] : '';
$workflow_id = isset($params['workflow_id']) ? intval($params['workflow_id']) : 0;
+ // Enhanced customer number detection for voicemails
+ $customer_number = $from;
+
// If From is not provided in the callback, try to get it from the call log
- if (empty($from) && !empty($call_sid)) {
+ if (empty($customer_number) && !empty($call_sid)) {
global $wpdb;
$call_log_table = $wpdb->prefix . 'twp_call_log';
$call_record = $wpdb->get_row($wpdb->prepare(
@@ -1306,11 +1309,81 @@ class TWP_Webhooks {
$call_sid
));
if ($call_record && $call_record->from_number) {
- $from = $call_record->from_number;
- error_log('TWP Voicemail Callback: Retrieved from_number from call log: ' . $from);
+ $customer_number = $call_record->from_number;
+ error_log('TWP Voicemail Callback: Retrieved from_number from call log: ' . $customer_number);
}
}
+ // If we got a client identifier (browser phone), try to find the real customer number
+ if (!empty($customer_number) && strpos($customer_number, 'client:') === 0) {
+ error_log('TWP Voicemail Callback: Detected client identifier, looking for customer number');
+
+ try {
+ // Initialize Twilio API to find the customer call leg
+ $twilio_api = new TWP_Twilio_API();
+ $client = $twilio_api->get_client();
+ $call = $client->calls($call_sid)->fetch();
+
+ // Use similar logic to find_customer_call_leg but adapted for voicemail
+ $real_customer_number = null;
+
+ // Check if this call has a parent that contains a real phone number
+ if ($call->parentCallSid) {
+ try {
+ $parent_call = $client->calls($call->parentCallSid)->fetch();
+ if (strpos($parent_call->from, 'client:') === false && strpos($parent_call->from, '+') === 0) {
+ $real_customer_number = $parent_call->from;
+ } elseif (strpos($parent_call->to, 'client:') === false && strpos($parent_call->to, '+') === 0) {
+ $real_customer_number = $parent_call->to;
+ }
+ } catch (Exception $e) {
+ error_log("TWP Voicemail Callback: Could not fetch parent call: " . $e->getMessage());
+ }
+ }
+
+ // If no parent success, search related calls
+ if (!$real_customer_number) {
+ $related_calls = $client->calls->read(['status' => 'completed'], 20);
+ foreach ($related_calls as $related_call) {
+ if ($related_call->sid === $call_sid) continue;
+
+ // Check if calls are related
+ $is_related = false;
+ if ($call->parentCallSid && $related_call->parentCallSid === $call->parentCallSid) {
+ $is_related = true;
+ } elseif ($related_call->parentCallSid === $call_sid) {
+ $is_related = true;
+ } elseif ($related_call->sid === $call->parentCallSid) {
+ $is_related = true;
+ }
+
+ if ($is_related) {
+ if (strpos($related_call->from, 'client:') === false && strpos($related_call->from, '+') === 0) {
+ $real_customer_number = $related_call->from;
+ break;
+ } elseif (strpos($related_call->to, 'client:') === false && strpos($related_call->to, '+') === 0) {
+ $real_customer_number = $related_call->to;
+ break;
+ }
+ }
+ }
+ }
+
+ if ($real_customer_number) {
+ $customer_number = $real_customer_number;
+ error_log("TWP Voicemail Callback: Found real customer number: {$customer_number}");
+ } else {
+ error_log("TWP Voicemail Callback: WARNING - Could not find real customer number, keeping client identifier");
+ }
+
+ } catch (Exception $e) {
+ error_log("TWP Voicemail Callback: Error finding customer number: " . $e->getMessage());
+ }
+ }
+
+ // Update $from to use the detected customer number
+ $from = $customer_number;
+
// Debug what we extracted
error_log('TWP Voicemail Callback: recording_url=' . $recording_url . ', from=' . $from . ', workflow_id=' . $workflow_id . ', call_sid=' . $call_sid);