tenantId(); $pageUrl = $request->page_url; $pageSize = $request->page_size; if (!$pageSize) { $pageSize = 50; } //ray('Controller - pageUrl:', $pageUrl); //ray('Controller - pageSize:', $pageSize); try { $manager = new WhatsappManager($tenantId); if (!$manager->isConfigured()) { wp_send_json_error([ 'error' => __eva('WhatsApp service is not configured. Please configure Twilio credentials in WhatsApp Configuration.'), ]); return; } $result = $manager->getTemplates($pageSize, $pageUrl); if (!$result['success']) { wp_send_json_error([ 'error' => $result['message'], ]); return; } wp_send_json_success([ 'templates' => $result['data']['templates'] ?? [], 'meta' => $result['data']['meta'] ?? null, 'total' => $result['data']['total'] ?? 0, ]); } catch (\Exception $e) { wp_send_json_error([ 'error' => $e->getMessage(), ]); } } /** * Fetch a single template with full details (body, buttons, etc.) * * @param Request $request * @return void */ public function fetchTemplate(Request $request) { $tenantId = $request->tenantId(); $sid = $request->sid; if (empty($sid)) { wp_send_json_error([ 'error' => __eva('Template SID is required'), ]); return; } $force = $request->force; //ray('FORCE: ' . $force ? 'TRUE' : 'FALSE'); try { // First check if we already have the template with body cached if (!$force) { $cachedTemplate = WhatsappTemplate::getTemplateBySid($tenantId, $sid); if ($cachedTemplate && !empty($cachedTemplate['body'])) { wp_send_json_success([ 'template' => $cachedTemplate, 'cached' => true, ]); return; } } // Fetch from Twilio //ray('LLAMANDO A WHATSAPP MANAGER'); $manager = new WhatsappManager($tenantId); if (!$manager->isConfigured()) { wp_send_json_error([ 'error' => __eva('WhatsApp service is not configured.'), ]); return; } $result = $manager->getTemplate($sid); //ray($result); if (!$result['success']) { wp_send_json_error([ 'error' => $result['message'], ]); return; } // Update the cached template with body info $templateData = $result['data']['template']; WhatsappTemplate::updateTemplate($tenantId, $sid, $templateData); wp_send_json_success([ 'template' => $templateData, 'cached' => false, ]); } catch (\Exception $e) { wp_send_json_error([ 'error' => $e->getMessage(), ]); } } /** * Save all templates to the database * * @param Request $request * @return void */ public function saveTemplates(Request $request) { $tenantId = $request->tenantId(); $templates = $request->templates; if (!$templates) { $templates = []; } try { $success = WhatsappTemplate::saveTemplates($tenantId, $templates); if ($success) { wp_send_json_success([ 'message' => __eva('Templates saved successfully'), 'last_sync' => current_time('mysql'), ]); } else { wp_send_json_error([ 'error' => __eva('Failed to save templates'), ]); } } catch (\Exception $e) { wp_send_json_error([ 'error' => $e->getMessage(), ]); } } /** * Refresh templates - fetch all pages and save * This is called when user clicks "Refresh" button * * @param Request $request * @return void */ public function refreshTemplates(Request $request) { $tenantId = $request->tenantId(); try { $manager = new WhatsappManager($tenantId); if (!$manager->isConfigured()) { wp_send_json_error([ 'error' => __eva('WhatsApp service is not configured.'), ]); return; } // Use the getAllTemplates method if available, otherwise paginate manually if (method_exists($manager, 'getAllTemplates')) { $result = $manager->getAllTemplates(20); if (!$result['success']) { wp_send_json_error([ 'error' => $result['message'], ]); return; } $allTemplates = $result['data']['templates']; $pageCount = $result['data']['pages_fetched']; } else { // Manual pagination fallback $allTemplates = []; $pageCount = 0; $nextPageUrl = null; $maxPages = 20; do { $result = $manager->getTemplates(50, $nextPageUrl); if (!$result['success']) { wp_send_json_error([ 'error' => $result['message'], ]); return; } $allTemplates = array_merge($allTemplates, $result['data']['templates'] ?? []); $nextPageUrl = $result['data']['meta']['next_page_url'] ?? null; $pageCount++; } while ($nextPageUrl && $pageCount < $maxPages); } // Save all templates WhatsappTemplate::saveTemplates($tenantId, $allTemplates); wp_send_json_success([ 'templates' => $allTemplates, 'count' => count($allTemplates), 'pages_fetched' => $pageCount, 'last_sync' => current_time('mysql'), ]); } catch (\Exception $e) { wp_send_json_error([ 'error' => $e->getMessage(), ]); } } /** * Get template approval status from Twilio * * @param Request $request * @return void */ public function getApprovalStatus(Request $request) { $tenantId = $request->tenantId(); $sid = $request->sid; if (empty($sid)) { wp_send_json_error([ 'error' => __eva('Template SID is required'), ]); return; } try { $manager = new WhatsappManager($tenantId); if (!$manager->isConfigured()) { wp_send_json_error([ 'error' => __eva('WhatsApp service is not configured.'), ]); return; } $result = $manager->getTemplateApprovalStatus($sid); if (!$result['success']) { wp_send_json_error([ 'error' => $result['message'], ]); return; } wp_send_json_success([ 'approval' => $result['data'], ]); } catch (\Exception $e) { wp_send_json_error([ 'error' => $e->getMessage(), ]); } } /** * Send a test WhatsApp message using a template. * * Sends directly through the provider WITHOUT creating any * WhatsappMessage or WhatsappConversation records, so test messages * don't pollute the inbox or conversation history. * * Returns the message_sid immediately. The frontend then polls * checkTestStatus() to get the real delivery status from webhooks. * * @param Request $request * @return void */ public function sendTestMessage(Request $request) { $tenantId = $request->tenantId(); $sid = $request->sid; $phone = $request->phone; $variables = $request->variables; // Validate required fields if (empty($sid)) { wp_send_json_error([ 'error' => __eva('Template SID is required'), ]); return; } if (empty($phone)) { wp_send_json_error([ 'error' => __eva('Phone number is required'), ]); return; } // Clean phone number - remove spaces and ensure it has + prefix $phone = preg_replace('/[\s\-\(\)]/', '', $phone); $phone = str_replace('whatsapp:', '', $phone); if (strpos($phone, '+') !== 0) { $phone = '+' . $phone; } try { $manager = new WhatsappManager($tenantId); if (!$manager->isConfigured()) { wp_send_json_error([ 'error' => __eva('WhatsApp service is not configured. Please configure Twilio credentials in WhatsApp Configuration.'), ]); return; } $provider = $manager->getProvider(); if (!$provider) { wp_send_json_error([ 'error' => __eva('WhatsApp provider not available.'), ]); return; } // Build the request DTO and send directly through the provider. // This bypasses WhatsappManager->sendTemplate() so NO message // or conversation records are created in the database. $templateRequest = new SendTemplateRequest($phone, $sid); $templateRequest->variables = is_array($variables) ? $variables : []; $result = $provider->sendTemplate($templateRequest); if (!$result['success']) { wp_send_json_error([ 'error' => $result['message'] ?? __eva('Failed to send message'), 'error_code' => $result['data']['error_code'] ?? null, ]); return; } $messageSid = $result['data']['message_id'] ?? null; wp_send_json_success([ 'message' => __eva('Message sent. Waiting for delivery status...'), 'message_sid' => $messageSid, 'status' => 'sent', ]); } catch (\Exception $e) { wp_send_json_error([ 'error' => $e->getMessage(), ]); } } /** * Check the delivery status of a test message by polling whatsapp_webhooks. * * Called from the frontend after sendTestMessage() returns the message_sid. * Looks for a webhook with a definitive status (delivered, read, failed, undelivered). * * @param Request $request * @return void */ public function checkTestStatus(Request $request) { $messageSid = $request->message_sid; if (empty($messageSid)) { wp_send_json_error(['error' => 'message_sid is required']); return; } // Look for a webhook with a definitive status $webhook = WhatsappWebhook::where('message_sid', $messageSid) ->where('status', 'delivered') ->first(); if (!$webhook) { $webhook = WhatsappWebhook::where('message_sid', $messageSid) ->where('status', 'read') ->first(); } if (!$webhook) { $webhook = WhatsappWebhook::where('message_sid', $messageSid) ->where('status', 'failed') ->first(); } if (!$webhook) { $webhook = WhatsappWebhook::where('message_sid', $messageSid) ->where('status', 'undelivered') ->first(); } if (!$webhook) { // No definitive status yet wp_send_json_success([ 'status' => 'pending', 'delivered' => false, 'failed' => false, 'pending' => true, ]); return; } $status = $webhook->status; $isDelivered = in_array($status, ['delivered', 'read']); $isFailed = in_array($status, ['failed', 'undelivered']); if ($isDelivered) { $message = __eva('Message delivered successfully!'); } else { $message = __eva('Message could not be delivered.'); if ($webhook->error_code) { $message .= ' ' . __eva('Error') . ': ' . $webhook->error_code; } if ($webhook->error_message) { $message .= ' - ' . $webhook->error_message; } } wp_send_json_success([ 'message' => $message, 'status' => $status, 'error_code' => $webhook->error_code, 'error_message' => $webhook->error_message, 'delivered' => $isDelivered, 'failed' => $isFailed, 'pending' => false, ]); } /** * Configure Twilio webhook URLs automatically via Senders API v2. * * @param Request $request * @return void */ public function configureWebhooks(Request $request) { $tenantId = $request->tenantId(); $manager = new WhatsappManager($tenantId); if (!$manager->isConfigured()) { wp_send_json_error(['error' => __eva('WhatsApp is not configured')]); return; } $provider = $manager->getProvider(); if (!$provider || $provider->getProviderName() !== 'twilio') { wp_send_json_error(['error' => __eva('This feature is only available for Twilio provider')]); return; } $incomingUrl = $provider->getIncomingWebhookUrl(); $statusCallbackUrl = $provider->getStatusCallbackUrl(); $result = $provider->configureWebhooks($incomingUrl, $statusCallbackUrl); if ($result['success']) { wp_send_json_success([ 'message' => $result['message'], 'callback_url' => $result['data']['callback_url'] ?? $incomingUrl, 'status_callback_url' => $result['data']['status_callback_url'] ?? $statusCallbackUrl, ]); } else { wp_send_json_error([ 'error' => $result['message'], 'data' => $result['data'] ?? null, ]); } } const BALANCE_LOW_THRESHOLD = 5.00; const BALANCE_CACHE_TTL_OK = 1800; // 30 min const BALANCE_CACHE_TTL_AUTH_ERR = 300; // 5 min /** * Get the Twilio account balance configured for WhatsApp on this tenant. * Cached per (tenantId, sidHash) — see fetchTwilioBalance(). * * Query params: tenant, refresh (=1 to bypass cache). */ public function getBalance(Request $request) { $tenantId = $request->tenantId(); $forceRefresh = $request->refresh == '1'; $config = WhatsappConfig::where('restaurant_id', $tenantId)->first(); if (!$config) { wp_send_json_success(['available' => false, 'reason' => 'whatsapp_disabled']); return; } $cfg = $config->config ?? []; if (empty($cfg['enabled'])) { wp_send_json_success(['available' => false, 'reason' => 'whatsapp_disabled']); return; } if (($cfg['provider'] ?? '') !== 'twilio') { wp_send_json_success([ 'available' => false, 'reason' => 'provider_not_supported', 'provider' => $cfg['provider'] ?? null, ]); return; } $payload = $this->fetchTwilioBalance( $tenantId, 'wa', $cfg['twilio_sid'] ?? '', $cfg['twilio_token'] ?? '', $forceRefresh ); wp_send_json_success($payload); } /** * Get the SMS provider balance for this tenant. * Reads EmailConfig (which holds the SMS settings) and dispatches to the * configured provider — Twilio or Bulkgate. * * Query params: tenant, refresh (=1 to bypass cache). */ public function getSmsBalance(Request $request) { $tenantId = $request->tenantId(); $forceRefresh = $request->refresh == '1'; $config = EmailConfig::where('restaurant_id', $tenantId)->first(); if (!$config || !$config->use_sms) { wp_send_json_success(['available' => false, 'reason' => 'sms_disabled']); return; } $gateway = $config->sms_gateway ?: 'twilio'; if ($gateway === 'twilio') { $payload = $this->fetchTwilioBalance( $tenantId, 'sms', $config->twilio_sid ?? '', $config->twilio_token ?? '', $forceRefresh ); $payload['provider'] = $payload['provider'] ?? 'twilio'; wp_send_json_success($payload); return; } if ($gateway === 'bulkgate') { $payload = $this->fetchBulkgateBalance( $tenantId, $config->bulkgate_app_id ?? '', $config->bulkgate_app_token ?? '', $forceRefresh ); $payload['provider'] = $payload['provider'] ?? 'bulkgate'; wp_send_json_success($payload); return; } wp_send_json_success([ 'available' => false, 'reason' => 'provider_not_supported', 'provider' => $gateway, ]); } /** * Twilio Balance API — returns the same payload shape regardless of * which area (WhatsApp or SMS) called it. * * The cache key uses a $scope ('wa' / 'sms') so the WhatsApp and SMS * configurations of the same tenant don't share cached entries. */ protected function fetchTwilioBalance(int $tenantId, string $scope, string $sid, string $token, bool $forceRefresh): array { if (empty($sid) || empty($token)) { return ['available' => false, 'reason' => 'missing_credentials', 'provider' => 'twilio']; } $cacheKey = 'alexr_balance_' . $scope . '_twilio_' . $tenantId . '_' . substr(md5($sid), 0, 8); if (!$forceRefresh) { $cached = get_transient($cacheKey); if ($cached !== false) return $cached; } $url = 'https://api.twilio.com/2010-04-01/Accounts/' . urlencode($sid) . '/Balance.json'; $response = wp_remote_get($url, [ 'headers' => [ 'Authorization' => 'Basic ' . base64_encode($sid . ':' . $token), 'Accept' => 'application/json', ], 'timeout' => 10, ]); if (is_wp_error($response)) { return [ 'available' => false, 'reason' => 'network_error', 'provider' => 'twilio', 'error' => $response->get_error_message(), ]; } $status = wp_remote_retrieve_response_code($response); $body = json_decode(wp_remote_retrieve_body($response), true); if ($status === 401 || $status === 403) { $payload = ['available' => false, 'reason' => 'invalid_credentials', 'provider' => 'twilio']; set_transient($cacheKey, $payload, self::BALANCE_CACHE_TTL_AUTH_ERR); return $payload; } if ($status !== 200 || !is_array($body) || !isset($body['balance'])) { return [ 'available' => false, 'reason' => 'unexpected_response', 'provider' => 'twilio', 'status' => $status, ]; } $balance = (float) $body['balance']; $payload = [ 'available' => true, 'provider' => 'twilio', 'balance' => $body['balance'], 'currency' => $body['currency'] ?? 'USD', 'fetched_at' => gmdate('c'), 'low' => $balance < self::BALANCE_LOW_THRESHOLD, 'threshold' => self::BALANCE_LOW_THRESHOLD, ]; set_transient($cacheKey, $payload, self::BALANCE_CACHE_TTL_OK); return $payload; } /** * Bulkgate Info API — returns the SMS credit balance for the application. * Endpoint: POST https://portal.bulkgate.com/api/1.0/simple/info * Body: application_id, application_token (form-urlencoded). */ protected function fetchBulkgateBalance(int $tenantId, string $appId, string $appToken, bool $forceRefresh): array { if (empty($appId) || empty($appToken)) { return ['available' => false, 'reason' => 'missing_credentials', 'provider' => 'bulkgate']; } $cacheKey = 'alexr_balance_sms_bulkgate_' . $tenantId . '_' . substr(md5($appId . ':' . $appToken), 0, 8); if (!$forceRefresh) { $cached = get_transient($cacheKey); if ($cached !== false) return $cached; } $response = wp_remote_post('https://portal.bulkgate.com/api/1.0/simple/info', [ 'body' => [ 'application_id' => $appId, 'application_token' => $appToken, ], 'timeout' => 10, ]); if (is_wp_error($response)) { return [ 'available' => false, 'reason' => 'network_error', 'provider' => 'bulkgate', 'error' => $response->get_error_message(), ]; } $status = wp_remote_retrieve_response_code($response); $body = json_decode(wp_remote_retrieve_body($response), true); // Bulkgate returns the credit inside data.credit for the simple API $credit = $body['data']['credit'] ?? $body['credit'] ?? null; $currency = $body['data']['currency'] ?? $body['currency'] ?? 'EUR'; $errorMsg = $body['data']['error'] ?? $body['error'] ?? null; // Treat known auth-failure shapes as invalid credentials if ($status === 401 || $status === 403 || ($errorMsg && stripos((string) $errorMsg, 'token') !== false)) { $payload = ['available' => false, 'reason' => 'invalid_credentials', 'provider' => 'bulkgate']; set_transient($cacheKey, $payload, self::BALANCE_CACHE_TTL_AUTH_ERR); return $payload; } if ($status !== 200 || $credit === null) { return [ 'available' => false, 'reason' => 'unexpected_response', 'provider' => 'bulkgate', 'status' => $status, 'error' => $errorMsg, ]; } $balance = (float) $credit; $payload = [ 'available' => true, 'provider' => 'bulkgate', 'balance' => number_format($balance, 2, '.', ''), 'currency' => $currency, 'fetched_at' => gmdate('c'), 'low' => $balance < self::BALANCE_LOW_THRESHOLD, 'threshold' => self::BALANCE_LOW_THRESHOLD, ]; set_transient($cacheKey, $payload, self::BALANCE_CACHE_TTL_OK); return $payload; } }