$voiceMessage, 'ivr_lang' => 'es-ES', 'hangup' => 'true', ]); // Opción B: MP3 subido en My PBX → IVR → Sounds + colgar // self::respondWithJson(['playfile' => 'nombre_archivo', 'hangup' => 'true']); // Opción C: URL directa a MP3 externo + colgar // self::respondWithJson(['ivr_play' => 'https://yoursite.com/audio/bienvenida.mp3', 'hangup' => 'true']); } /** * NOTIFY_ANSWER — alguien descolgó la llamada. */ protected static function handleCallAnswer(array $payload): void { $callId = sanitize_text_field($payload['pbx_call_id'] ?? ''); do_action('alexr_zadarma_call_answer', $callId, $payload); status_header(200); wp_die('ok'); } /** * NOTIFY_END — llamada terminada. * Aquí enviamos el WhatsApp al restaurante. * * Zadarma payload: * caller_id, called_did, pbx_call_id, duration, disposition * disposition: answered | no answer | busy */ protected static function handleCallEnd(array $payload): void { $callerPhone = sanitize_text_field($payload['caller_id'] ?? 'desconocido'); $calledNumber = sanitize_text_field($payload['called_did'] ?? ''); $callId = sanitize_text_field($payload['pbx_call_id'] ?? ''); $duration = intval($payload['duration'] ?? 0); $disposition = sanitize_text_field($payload['disposition'] ?? ''); $restaurantId = self::getRestaurantId($payload); do_action('alexr_zadarma_call_end', $callerPhone, $calledNumber, $callId, $duration, $disposition, $restaurantId, $payload); // WhatsApp ya enviado en NOTIFY_START, aquí solo disparamos el action por si alguien lo necesita status_header(200); wp_die('ok'); } /** * NOTIFY_IVR — el cliente pulsó una tecla en el menú. * * Zadarma payload: * caller_id, pbx_call_id, dtmf (tecla pulsada) */ protected static function handleIvr(array $payload): void { $callerPhone = sanitize_text_field($payload['caller_id'] ?? ''); $dtmf = sanitize_text_field($payload['dtmf'] ?? ''); $callId = sanitize_text_field($payload['pbx_call_id'] ?? ''); $restaurantId = self::getRestaurantId($payload); do_action('alexr_zadarma_ivr_input', $dtmf, $callerPhone, $callId, $restaurantId, $payload); // Responder según tecla pulsada $response = apply_filters('alexr_zadarma_ivr_response', [ 'ivr_say' => 'Conectando con el restaurante, por favor espere.', 'ivr_lang' => 'es-ES', ], $dtmf, $restaurantId, $payload); self::respondWithJson($response); } // ------------------------------------------------------------------------- // WHATSAPP // ------------------------------------------------------------------------- /** * Envía un WhatsApp al caller usando el fallback template configurado * en WhatsappAction (el mismo que usa KeywordHandler cuando no hay keyword match). * Se resuelve el idioma por: idioma del restaurante → primer idioma con respuesta. */ protected static function sendWhatsAppToRestaurant( string $callerPhone, int $restaurantId, int $duration, string $disposition, array $payload ): void { try { $manager = WhatsappManager::forRestaurant($restaurantId); if (! $manager->getProvider() || ! $manager->getProvider()->isConfigured()) { self::logError('Provider no configurado para restaurante ' . $restaurantId); return; } // Cargar la configuración de acciones por defecto del restaurante $defaultActions = \Alexr\Settings\WhatsappAction::where('restaurant_id', $restaurantId)->first(); if (! $defaultActions || ! $defaultActions->isActionEnabled('action_fallback')) { self::logError('action_fallback no configurado para restaurante ' . $restaurantId); return; } // Resolver idioma: igual que resolveFallbackLanguage pero con body vacío // (sin keywords, irá directo al idioma del restaurante o al primero disponible) $lang = \Alexr\Whatsapp\WhatsappKeywordHandler::resolveFallbackLanguagePublic( $defaultActions, 'hola', $restaurantId ); if (! $lang) { self::logError('No se pudo resolver idioma para fallback | restaurante ' . $restaurantId); return; } $responseType = $defaultActions->getResponseType('action_fallback', $lang); if ($responseType === 'template') { $templateConfig = $defaultActions->getTemplateConfig('action_fallback', $lang); if (! $templateConfig || empty($templateConfig['template_sid'])) { self::logError('Template SID vacío para action_fallback | restaurante ' . $restaurantId); return; } $bookNowWidgetId = $templateConfig['book_now_widget_id'] ?? null; $restaurant = \Alexr\Models\Restaurant::find($restaurantId); $variables = $restaurant ? \Alexr\Whatsapp\WhatsappKeywordHandler::resolveRestaurantVariablesPublic( $templateConfig['field_mapping'] ?? [], $restaurant, $bookNowWidgetId ) : []; $result = $manager->sendTemplate( $callerPhone, $templateConfig['template_sid'], $variables, null, null, $templateConfig['template_name'] ?? null, ['sender_type' => 'bot'] ); } else { $customMessage = $defaultActions->getCustomMessage('action_fallback', $lang); if (empty($customMessage)) { self::logError('Mensaje fallback vacío para restaurante ' . $restaurantId); return; } $actionConfig = $defaultActions->getActionConfig('action_fallback', $lang); $bookNowWidgetId = $actionConfig['book_now_widget_id'] ?? null; $customMessage = \Alexr\Whatsapp\WhatsappKeywordHandler::replaceRestaurantTagsPublic( $customMessage, $restaurantId, $bookNowWidgetId ); $result = $manager->sendMessage( $callerPhone, $customMessage, null, null, null, ['sender_type' => 'bot'] ); } if (function_exists('ray')) { ray('Zadarma → WhatsApp fallback enviado', [ 'to' => $callerPhone, 'lang' => $lang, 'result' => $result, ])->green(); } do_action('alexr_zadarma_wa_sent', $callerPhone, $restaurantId, $result, $payload); } catch (\Throwable $e) { self::logError('Fallo al enviar WhatsApp: ' . $e->getMessage(), [ 'caller' => $callerPhone, 'restaurant_id' => $restaurantId, ]); } } // ------------------------------------------------------------------------- // HELPERS // ------------------------------------------------------------------------- /** * Determinar el restaurant ID a partir del número llamado (called_did). * Por defecto devuelve 1 si no se encuentra otro. */ protected static function getRestaurantId(array $payload): ?int { // 1. Desde query string (?restaurant_id=X) if (! empty($_GET['restaurant_id'])) { return (int) $_GET['restaurant_id']; } // 2. Buscar por el número de Zadarma que recibió la llamada $toPhone = sanitize_text_field($payload['called_did'] ?? ''); if ($toPhone) { $restaurantId = WhatsappProviderFactory::findRestaurantByPhone($toPhone); if ($restaurantId) { return $restaurantId; } } // 3. Filtro fallback $default = apply_filters('alexr_zadarma_default_restaurant_id', 1); return (int) $default; } /** * Verifica la firma HMAC-SHA1 enviada por Zadarma en el parámetro 'sign'. */ protected static function verifySignature(array $params): bool { return true; $secret = defined('ALEXR_ZADARMA_SECRET') ? ALEXR_ZADARMA_SECRET : ''; if (empty($secret)) { // Sin secret configurado: skip (útil en desarrollo) return true; } $signReceived = $params['sign'] ?? ''; if (empty($signReceived)) { return false; } $data = $params; unset($data['sign']); ksort($data); $queryString = http_build_query($data); $signExpected = base64_encode( hash_hmac('sha1', $queryString . md5($queryString), $secret, true) ); return hash_equals($signExpected, $signReceived); } /** * Responde a Zadarma con un JSON de instrucciones. */ protected static function respondWithJson(array $data): void { ray($data); header('Content-Type: application/json; charset=UTF-8'); echo wp_json_encode($data); wp_die(); } /** * Obtiene el payload del POST de Zadarma. */ protected static function getPayload(): array { return $_POST ?: []; } /** * Log de webhooks recibidos (solo con debug activo). */ protected static function logWebhook(string $event, array $payload): void { if (! defined('ALEXR_WHATSAPP_DEBUG') || ! ALEXR_WHATSAPP_DEBUG) { return; } $logData = [ 'type' => 'zadarma_webhook', 'event' => $event, 'caller_id' => $payload['caller_id'] ?? null, 'called' => $payload['called_did'] ?? null, 'call_id' => $payload['pbx_call_id'] ?? null, 'timestamp' => current_time('mysql'), ]; if (function_exists('ray')) { ray('Zadarma Webhook', $logData)->purple(); } do_action('alexr_zadarma_webhook_log', $logData); } /** * Log de errores. */ protected static function logError(string $message, array $context = []): void { $logData = array_merge(['error' => $message], $context); if (function_exists('ray')) { ray('Zadarma ERROR', $logData)->red(); } error_log('[ZadarmaVoiceRedirection] ' . json_encode($logData)); do_action('alexr_zadarma_error', $logData); } } // Register the AJAX endpoints automatically when this file is loaded ZadarmaVoiceRedirection::register();