* 3. We send a WhatsApp message to the caller's number * * Register your Twilio phone number's "A call comes in" webhook URL to: * https://yoursite.com/wp-admin/admin-ajax.php?action=alexr_whatsapp_voice&restaurant_id=1 * * @package Alexr\Whatsapp */ class TwilioVoiceRedirection { /** * The TwiML message read aloud to the caller (Spanish). * Override via filter: alexr_whatsapp_voice_message */ protected const DEFAULT_VOICE_MESSAGE = 'Hola, gracias por llamar. En este momento no podemos atender su llamada. ' . 'Le enviaremos un mensaje de WhatsApp a su número para que podamos ayudarle. ' . 'Hasta pronto.'; /** * The WhatsApp message sent after the call. * Override via filter: alexr_whatsapp_voice_whatsapp_message */ protected const DEFAULT_WHATSAPP_MESSAGE = '👋 Hola, ha llamado al restaurante y no hemos podido atenderle. ' . '¿En qué podemos ayudarle? Responda a este mensaje y le atenderemos encantados.'; /** * Register WordPress AJAX endpoints. */ public static function register(): void { add_action('wp_ajax_nopriv_alexr_whatsapp_voice', [self::class, 'handleVoiceCall']); add_action('wp_ajax_alexr_whatsapp_voice', [self::class, 'handleVoiceCall']); } /** * Main handler: called by Twilio when a voice call comes in. * * Twilio sends (among others) these POST params: * - From: caller's phone number (e.g. +34612345678) * - To: restaurant's Twilio number * - CallSid, Direction, CallStatus, etc. */ public static function handleVoiceCall(): void { $payload = self::getPayload(); ray($payload); // Log for debugging self::logVoiceCall($payload); $callerPhone = self::extractCallerPhone($payload); $restaurantId = self::getRestaurantId($payload); // Send WhatsApp message to caller (best-effort, non-blocking) if ($callerPhone && $restaurantId) { ray('Enviar whatsapp al cliente: ' . $callerPhone . ' - ' . $restaurantId . ' -'); //self::sendWhatsAppToCall($callerPhone, $restaurantId, $payload); } // Always respond with TwiML so Twilio reads the message and hangs up self::respondWithTwiml($restaurantId); } // ------------------------------------------------------------------------- // Private helpers // ------------------------------------------------------------------------- /** * Extract the caller's E.164 phone number from the Twilio payload. */ protected static function extractCallerPhone(array $payload): ?string { $from = $payload['From'] ?? ''; // Remove any whatsapp: prefix just in case $from = str_replace('whatsapp:', '', $from); // Must start with + to be a valid E.164 number if (empty($from) || $from[0] !== '+') { return null; } return $from; } /** * Determine the restaurant ID from the request. * Tries query-string first, then matches by 'To' phone number. */ protected static function getRestaurantId(array $payload): ?int { // 1. From query string (?restaurant_id=X) if (!empty($_GET['restaurant_id'])) { return (int) $_GET['restaurant_id']; } // 2. Match by 'To' phone number (the Twilio number that was called) $toPhone = $payload['To'] ?? ''; $toPhone = str_replace('whatsapp:', '', $toPhone); if ($toPhone) { $restaurantId = WhatsappProviderFactory::findRestaurantByPhone($toPhone); if ($restaurantId) { return $restaurantId; } } // 3. WordPress filter fallback $default = apply_filters('alexr_whatsapp_default_restaurant_id', null); if ($default) { return (int) $default; } return null; } /** * Send a WhatsApp message to the person who called. */ protected static function sendWhatsAppToCall( string $callerPhone, int $restaurantId, array $payload ): void { try { $manager = WhatsappManager::forRestaurant($restaurantId); if (! $manager->getProvider() || ! $manager->getProvider()->isConfigured()) { self::logError('Provider not configured for restaurant ' . $restaurantId); return; } $whatsappMessage = apply_filters( 'alexr_whatsapp_voice_whatsapp_message', self::DEFAULT_WHATSAPP_MESSAGE, $callerPhone, $restaurantId, $payload ); $request = new SendMessageRequest(); $request->toPhone = $callerPhone; $request->body = $whatsappMessage; $result = $manager->sendMessage($request); if (function_exists('ray')) { ray('WhatsApp Voice Redirection - WA sent', [ 'to' => $callerPhone, 'result' => $result, ])->green(); } do_action('alexr_whatsapp_voice_wa_sent', $callerPhone, $restaurantId, $result, $payload); } catch (\Throwable $e) { self::logError('Failed to send WhatsApp after call: ' . $e->getMessage(), [ 'caller' => $callerPhone, 'restaurant_id' => $restaurantId, ]); } } /** * Output TwiML response that reads a Spanish message and hangs up. * * TwiML reference: * reads text in Spanish. * ends the call cleanly. */ protected static function respondWithTwiml(?int $restaurantId): void { $voiceMessage = apply_filters( 'alexr_whatsapp_voice_message', self::DEFAULT_VOICE_MESSAGE, $restaurantId ); // Sanitize to avoid breaking the XML $voiceMessage = htmlspecialchars($voiceMessage, ENT_XML1, 'UTF-8'); $twiml = << {$voiceMessage} XML; header('Content-Type: text/xml; charset=UTF-8'); echo $twiml; wp_die(); } /** * Get the POST payload sent by Twilio. */ protected static function getPayload(): array { return $_POST ?: []; } /** * Log the incoming voice call (only when debug is enabled). */ protected static function logVoiceCall(array $payload): void { if (! defined('ALEXR_WHATSAPP_DEBUG') || ! ALEXR_WHATSAPP_DEBUG) { return; } $logData = [ 'type' => 'voice_call', 'from' => $payload['From'] ?? null, 'to' => $payload['To'] ?? null, 'call_sid' => $payload['CallSid'] ?? null, 'status' => $payload['CallStatus'] ?? null, 'timestamp' => current_time('mysql'), ]; if (function_exists('ray')) { ray('WhatsApp Voice Redirection', $logData); } do_action('alexr_whatsapp_voice_log', $logData); } /** * Log an error. */ protected static function logError(string $message, array $context = []): void { $logData = array_merge(['error' => $message], $context); if (function_exists('ray')) { ray('WhatsApp Voice Redirection ERROR', $logData); } error_log('[WhatsApp Voice Redirection] ' . json_encode($logData)); do_action('alexr_whatsapp_voice_error', $logData); } } // Register the AJAX endpoints automatically when this file is loaded WhatsappVoiceRedirection::register();