From ae92ea2c81e9bb88e090171aedd456663a7df0e0 Mon Sep 17 00:00:00 2001 From: jknapp Date: Mon, 1 Sep 2025 09:34:07 -0700 Subject: [PATCH] Fix hold/resume functionality and customer number detection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CRITICAL FIXES: - Fixed hold/resume functions to use proper Hold Queue system instead of empty TwiML - Enhanced customer number detection for voicemail and call recording interfaces - Resolved customer disconnections during hold/resume operations on outbound calls Hold/Resume System Improvements: - Updated ajax_toggle_hold() to use TWP_User_Queue_Manager for proper queue management - Fixed resume function to redirect calls back to appropriate queues (personal/shared) - Added comprehensive logging for hold queue operations and call leg detection - Enhanced hold experience with proper TTS messages and queue tracking Customer Number Detection Enhancements: - Enhanced handle_voicemail_callback() with fallback logic for missing From parameters - Added browser phone detection to identify client: calls and find real customer numbers - Enhanced call recording to use find_customer_call_leg() for proper customer identification - Fixed admin interfaces to show actual phone numbers instead of "client:agentname" Technical Improvements: - Integrated Hold Queue system with existing call leg detection infrastructure - Added proper TwiML generation for hold/resume operations - Enhanced error handling and logging for debugging complex call topologies - Maintains database consistency with queue position tracking πŸ€– Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- CLAUDE.md | 671 ++++++++++---- README.md | 40 +- admin/class-twp-admin.php | 1406 +++++++++++++++++++---------- includes/class-twp-tts-helper.php | 220 +++++ includes/class-twp-webhooks.php | 79 +- 5 files changed, 1734 insertions(+), 682 deletions(-) create mode 100644 includes/class-twp-tts-helper.php 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 {

My Assigned Queues

-
- $queue): ?> - + + Your personal queue is being set up. Please refresh the page. - - () - - - -
- -
- $queue): ?> -
- - - - - - - - - - - - - -
PositionCaller NumberWait TimeStatusActions
Loading...
-
- -
+

+
+ +
+ $queue): ?> + + +
+ +
+ $queue): ?> +
+ + + + + + + + + + + + + +
PositionCaller NumberWait TimeStatusActions
Loading...
+
+ +
+
@@ -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... - -
- +
+

πŸ“ž Your Queues

+ +
+ πŸ“ž Your Extension: extension); ?>
- + +
+
+
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 += ''; + 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);