$callerPhone, 'destinationPhone' => $destinationPhone, 'callId' => $callId, 'pbxProvider' => $pbxProvider]); if (empty($callerPhone)) { $this->logError('Missing caller phone'); return; } // Find restaurant by destination phone or by query param $restaurantId = $this->resolveRestaurantId($destinationPhone); //ray('restaurantId', $restaurantId); if (!$restaurantId) { $this->logError('Restaurant not found for phone: ' . $destinationPhone); return; } // Load missed call config $config = MissedCallConfig::where('restaurant_id', $restaurantId)->first(); if (!$config || !$config->enabled) { $this->logError('Missed call module not enabled for restaurant: ' . $restaurantId); return; } $restaurant = Restaurant::find($restaurantId); if (!$restaurant) { $this->logError('Restaurant not found: ' . $restaurantId); return; } // Resolve language (default to restaurant language) $lang = $restaurant->language ?? 'en'; // widget_id is now resolved centrally by BookNowWidgetResolver $widgetId = null; // Create log entry $log = new MissedCallLog(); $log->restaurant_id = $restaurantId; $log->caller_phone = $callerPhone; $log->destination_phone = $destinationPhone; $log->call_id = $callId; $log->pbx_provider = $pbxProvider; $log->save(); // Send WhatsApp if ($config->send_whatsapp) { $whatsappShortCode = $this->sendWhatsApp($callerPhone, $restaurant, $lang, $widgetId, $log->id); if ($whatsappShortCode) { $log->whatsapp_sent = true; $log->whatsapp_short_code = $whatsappShortCode; } } // Send SMS if ($config->send_sms) { ray('SEND SMS'); $smsShortCode = $this->sendSms($callerPhone, $restaurant, $lang, $widgetId, $log->id); if ($smsShortCode) { $log->sms_sent = true; $log->sms_short_code = $smsShortCode; } } $log->save(); do_action('alexr_missed_call_processed', $callerPhone, $restaurantId, $log, $pbxProvider); } /** * Send WhatsApp message to the caller * * @param string $callerPhone * @param Restaurant $restaurant * @param string $lang * @param string|null $widgetId * @param int $missedCallLogId * @return string|null The short code used in {book_now}, for tracking */ protected function sendWhatsApp(string $callerPhone, Restaurant $restaurant, string $lang, ?string $widgetId, int $missedCallLogId): ?string { try { $manager = WhatsappManager::forRestaurant($restaurant->id); if (!$manager->getProvider() || !$manager->getProvider()->isConfigured()) { $this->logError('WhatsApp provider not configured for restaurant ' . $restaurant->id); return null; } $templateSettings = MissedCallWhatsappTemplate::where('restaurant_id', $restaurant->id)->first(); if (!$templateSettings) { $this->logError('No WhatsApp template configured for missed call | restaurant ' . $restaurant->id); return null; } $templateConfig = $templateSettings->getTemplateConfig($lang); if (!$templateConfig || empty($templateConfig['template_sid'])) { // Try first available language $templateConfig = $this->findFirstAvailableTemplate($templateSettings, $lang); if (!$templateConfig) { $this->logError('No WhatsApp template for lang ' . $lang . ' | restaurant ' . $restaurant->id); return null; } } // Generate book_now link with missed_call tracking $bookNowUrl = evavel_action_book_now($restaurant->id, $widgetId, [ 'missed_call' => $missedCallLogId, 'utm_source' => 'whatsapp', 'type' => 'whatsapp', ]); // Extract short code from the URL for tracking $shortCode = $this->extractShortCode($bookNowUrl); // Resolve template variables using pre-generated bookNowUrl to avoid a second call $variables = RestaurantVariableResolver::resolveVariables( $templateConfig['field_mapping'] ?? [], $restaurant, $widgetId, [], // bookNowExtraParams not needed, we have the override $bookNowUrl // bookNowOverride — prevents duplicate evavel_action_book_now call ); $manager->sendTemplate( $callerPhone, $templateConfig['template_sid'], $variables, null, null, $templateConfig['template_name'] ?? null, ['sender_type' => 'bot'] ); return $shortCode; } catch (\Throwable $e) { $this->logError('Failed to send WhatsApp: ' . $e->getMessage()); return null; } } /** * Send SMS to the caller * * @param string $callerPhone * @param Restaurant $restaurant * @param string $lang * @param string|null $widgetId * @param int $missedCallLogId * @return string|null The short code used in {book_now}, for tracking */ protected function sendSms(string $callerPhone, Restaurant $restaurant, string $lang, ?string $widgetId, int $missedCallLogId): ?string { try { ray('Restaurant: ' . $restaurant->id . ' | lang: ' . $lang . ' | widgetId: ' . $widgetId . ' | missedCallLogId: ' . $missedCallLogId); $smsManager = new SmsManager($restaurant->id); if (!$smsManager->is_configured()) { $this->logError('SMS not configured for restaurant ' . $restaurant->id); return null; } $smsSettings = MissedCallSmsTemplate::where('restaurant_id', $restaurant->id)->first(); ray($smsSettings); if (!$smsSettings) { $this->logError('No SMS template configured for missed call | restaurant ' . $restaurant->id); return null; } $content = $smsSettings->getSmsContent($lang); ray($content); if (empty($content)) { // Try first available language $content = $this->findFirstAvailableSmsContent($smsSettings, $lang); if (empty($content)) { $this->logError('No SMS content for lang ' . $lang . ' | restaurant ' . $restaurant->id); return null; } } // Generate book_now link with SMS tracking $bookNowUrl = evavel_action_book_now($restaurant->id, $widgetId, [ 'missed_call' => $missedCallLogId, 'utm_source' => 'sms', 'type' => 'sms', ]); $shortCode = $this->extractShortCode($bookNowUrl); // Replace tags in the SMS content using centralized resolver $content = RestaurantVariableResolver::replaceTags($content, $restaurant, $widgetId, [], $bookNowUrl); ray('SEND SMS', $content); $smsManager->send_message($callerPhone, 'missed_call', $content); return $shortCode; } catch (\Throwable $e) { $this->logError('Failed to send SMS: ' . $e->getMessage()); return null; } } /** * Resolve restaurant ID from destination phone or query param * * @param string $destinationPhone * @return int|null */ protected function resolveRestaurantId(string $destinationPhone): ?int { // 1. From query string ?restaurant_id=X if (!empty($_GET['restaurant_id'])) { return (int) $_GET['restaurant_id']; } // 2. By phone number in MissedCallConfig if ($destinationPhone) { $restaurantId = MissedCallConfig::findRestaurantByPhone($destinationPhone); if ($restaurantId) { return $restaurantId; } } // 3. Filter fallback $default = apply_filters('alexr_missed_call_default_restaurant_id', null); return $default ? (int) $default : null; } /** * Extract the short code from a shortened URL * * @param string $url * @return string|null */ protected function extractShortCode(string $url): ?string { $param = defined('ALEXR_SHORT_URL_PARAM') ? ALEXR_SHORT_URL_PARAM : 'arx'; $parsed = parse_url($url); if (!isset($parsed['query'])) { return null; } parse_str($parsed['query'], $params); return $params[$param] ?? null; } /** * Find first available WhatsApp template across languages */ protected function findFirstAvailableTemplate(MissedCallWhatsappTemplate $settings, string $preferredLang): ?array { $languages = evavel_languages_allowed(); foreach ($languages as $lang => $label) { if ($lang === $preferredLang) continue; $config = $settings->getTemplateConfig($lang); if ($config && !empty($config['template_sid'])) { return $config; } } return null; } /** * Find first available SMS content across languages */ protected function findFirstAvailableSmsContent(MissedCallSmsTemplate $settings, string $preferredLang): ?string { $languages = evavel_languages_allowed(); foreach ($languages as $lang => $label) { if ($lang === $preferredLang) continue; $content = $settings->getSmsContent($lang); if (!empty($content)) { return $content; } } return null; } /** * Log errors */ protected function logError(string $message): void { if (function_exists('ray')) { ray('MissedCallService ERROR', $message); } error_log('[MissedCallService] ' . $message); } }