From 8448f13dfc74acb354d8a82fd157a2b5043f882b Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Fri, 22 May 2026 03:37:23 +0300 Subject: [PATCH] Deploy: 2026-05-22 03:37:23 --- backend/app/Controllers/SallaController.php | 326 ++++++++++++++++++ .../app/Controllers/WhatsAppController.php | 129 +++++++ backend/app/Models/SallaMerchant.php | 161 +++++++++ backend/public/index.html | 139 +++++++- backend/public/index.php | 8 + 5 files changed, 761 insertions(+), 2 deletions(-) create mode 100644 backend/app/Controllers/SallaController.php create mode 100644 backend/app/Models/SallaMerchant.php diff --git a/backend/app/Controllers/SallaController.php b/backend/app/Controllers/SallaController.php new file mode 100644 index 0000000..06ef2e9 --- /dev/null +++ b/backend/app/Controllers/SallaController.php @@ -0,0 +1,326 @@ +status(400)->html("

Error: Token is required

"); + return; + } + + $payload = Security::verifyJWT($token); + if (!$payload) { + $response->status(401)->html("

Error: Invalid token

"); + return; + } + + $companyId = $payload['company_id']; + $clientId = getenv('SALLA_CLIENT_ID') ?: '69ea789c-f611-4ea7-a3ee-7ead41420225'; + $redirectUri = getenv('APP_URL') . '/api/integrations/salla/callback'; + + $authUrl = "https://accounts.salla.sa/oauth2/auth?" . http_build_query([ + 'client_id' => $clientId, + 'redirect_uri' => $redirectUri, + 'response_type' => 'code', + 'scope' => 'read_orders read_customers', + 'state' => $companyId + ]); + + header("Location: " . $authUrl); + exit; + } + + /** + * Handle Salla OAuth callback. + * Accessible via GET /api/integrations/salla/callback?code=CODE&state=COMPANY_ID + */ + public function callback(Request $request, Response $response) + { + $code = $_GET['code'] ?? ''; + $companyId = $_GET['state'] ?? ''; + + if (empty($code) || empty($companyId)) { + $response->status(400)->html("

Error: Missing authorization code or state (company_id).

"); + return; + } + + $clientId = getenv('SALLA_CLIENT_ID') ?: '69ea789c-f611-4ea7-a3ee-7ead41420225'; + $clientSecret = getenv('SALLA_CLIENT_SECRET') ?: ''; + $redirectUri = getenv('APP_URL') . '/api/integrations/salla/callback'; + + // 1. Swap authorization code for token + $ch = curl_init('https://accounts.salla.sa/oauth2/token'); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([ + 'client_id' => $clientId, + 'client_secret' => $clientSecret, + 'grant_type' => 'authorization_code', + 'code' => $code, + 'redirect_uri' => $redirectUri + ])); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Content-Type: application/x-www-form-urlencoded' + ]); + curl_setopt($ch, CURLOPT_TIMEOUT, 15); + + $res = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpCode !== 200) { + error_log("Salla Token Error: HTTP $httpCode - Response: $res"); + $response->status(500)->html("

Failed to exchange code for Salla token. Status code: $httpCode

"); + return; + } + + $tokenData = json_decode($res, true); + if (empty($tokenData['access_token'])) { + $response->status(500)->html("

Failed to parse Salla token response.

"); + return; + } + + $accessToken = $tokenData['access_token']; + $refreshToken = $tokenData['refresh_token']; + $expiresIn = $tokenData['expires_in'] ?? 2592000; + $expiresAt = date('Y-m-d H:i:s', time() + $expiresIn); + + // 2. Fetch merchant/user info + $chInfo = curl_init('https://accounts.salla.sa/oauth2/user/info'); + curl_setopt($chInfo, CURLOPT_RETURNTRANSFER, true); + curl_setopt($chInfo, CURLOPT_HTTPHEADER, [ + 'Authorization: Bearer ' . $accessToken + ]); + curl_setopt($chInfo, CURLOPT_TIMEOUT, 10); + + $resInfo = curl_exec($chInfo); + $infoHttpCode = curl_getinfo($chInfo, CURLINFO_HTTP_CODE); + curl_close($chInfo); + + $storeName = 'Salla Store'; + $merchantId = ''; + if ($infoHttpCode === 200) { + $infoData = json_decode($resInfo, true); + $merchantId = $infoData['merchant']['id'] ?? $infoData['id'] ?? ''; + $storeName = $infoData['merchant']['name'] ?? 'Salla Store'; + } + + if (empty($merchantId)) { + error_log("Salla Info Fetch Error: HTTP $infoHttpCode - Response: $resInfo"); + $response->status(500)->html("

Failed to fetch merchant profile from Salla.

"); + return; + } + + // 3. Save or update Salla merchant settings in DB + SallaMerchant::saveSecure([ + 'company_id' => (int)$companyId, + 'merchant_id' => (string)$merchantId, + 'store_name' => $storeName, + 'access_token' => $accessToken, + 'refresh_token' => $refreshToken, + 'expires_at' => $expiresAt + ]); + + // Redirect back to frontend + header("Location: " . getenv('APP_URL') . "/?salla_connect=success"); + exit; + } + + /** + * Get Salla connection status for current company. + * Protected by AuthMiddleware. + * Accessible via GET /api/integrations/salla/status + */ + public function status(Request $request, Response $response) + { + $merchant = SallaMerchant::findByCompany($request->company_id); + if ($merchant) { + $response->json([ + 'status' => 'success', + 'connected' => true, + 'store_name' => $merchant['store_name'], + 'merchant_id' => $merchant['merchant_id'], + 'expires_at' => $merchant['expires_at'] + ]); + } else { + $response->json([ + 'status' => 'success', + 'connected' => false + ]); + } + } + + /** + * Disconnect Salla integration. + * Protected by AuthMiddleware. + * Accessible via POST /api/integrations/salla/disconnect + */ + public function disconnect(Request $request, Response $response) + { + SallaMerchant::deleteByCompany($request->company_id); + $response->json([ + 'status' => 'success', + 'message' => 'Salla integration disconnected successfully' + ]); + } + + /** + * Handle incoming Webhook from Salla. + * Accessible via POST /api/webhooks/salla + */ + public function webhook(Request $request, Response $response) + { + // 1. Get raw input and signature + $rawPayload = file_get_contents('php://input'); + $signature = $request->getHeader('x-salla-signature') ?: ''; + + // 2. Verify signature + $secret = getenv('SALLA_WEBHOOK_SECRET') ?: '406550c38b782a50e9a3e0687f564107f8a60135b072b1f819470dda5333a65d'; + $calculated = hash_hmac('sha256', $rawPayload, $secret); + + if (!hash_equals($calculated, $signature)) { + error_log("[Salla Webhook Error] Invalid signature. Received: $signature, Calculated: $calculated"); + $response->status(401)->json(['error' => 'Invalid signature']); + return; + } + + // 3. Parse JSON payload + $payload = json_decode($rawPayload, true); + if (!$payload) { + $response->status(400)->json(['error' => 'Invalid JSON']); + return; + } + + $event = $payload['event'] ?? ''; + $merchantId = $payload['merchant'] ?? ''; + + if (empty($event) || empty($merchantId)) { + $response->status(400)->json(['error' => 'Missing event or merchant information']); + return; + } + + // 4. Find the merchant config to get company_id + $merchant = SallaMerchant::findByMerchantId((string)$merchantId); + if (!$merchant) { + error_log("[Salla Webhook Warning] Webhook received for unlinked merchant ID: " . $merchantId); + $response->status(404)->json(['error' => 'Store is not linked to any company in Nabeh']); + return; + } + + $companyId = (int)$merchant['company_id']; + $orderData = $payload['data'] ?? []; + + // 5. Process events + if ($event === 'order.created' || $event === 'order.status.updated') { + $this->handleOrderWebhook($companyId, $event, $orderData); + } + + $response->json(['status' => 'success']); + } + + /** + * Process order webhook events and send automated WhatsApp message + */ + private function handleOrderWebhook(int $companyId, string $event, array $orderData) + { + $orderId = $orderData['id'] ?? $orderData['reference_id'] ?? ''; + $customerName = $orderData['customer']['name'] ?? 'عميلنا العزيز'; + $customerPhone = $orderData['customer']['mobile'] ?? $orderData['customer']['phone'] ?? ''; + $statusName = $orderData['status']['name'] ?? ''; + + if (empty($customerPhone) || empty($orderId)) { + return; + } + + // Format phone: remove leading zero or +, ensure only digits + $customerPhone = preg_replace('/\D/', '', $customerPhone); + if (substr($customerPhone, 0, 2) === '00') { + $customerPhone = substr($customerPhone, 2); + } + // Normalize Saudi numbers starting with 05 + if (strlen($customerPhone) === 9 && $customerPhone[0] === '5') { + $customerPhone = '966' . $customerPhone; + } elseif (strlen($customerPhone) === 10 && substr($customerPhone, 0, 2) === '05') { + $customerPhone = '966' . substr($customerPhone, 1); + } + + // Construct message based on event + $message = ''; + if ($event === 'order.created') { + $message = "مرحباً {$customerName}،\nتم استلام طلبك رقم ({$orderId}) بنجاح! شكرًا لتسوقك معنا. سنقوم بتحديثك فور تغيير حالة الطلب."; + } elseif ($event === 'order.status.updated') { + $message = "مرحباً {$customerName}،\nتم تحديث حالة طلبك رقم ({$orderId}) إلى: *{$statusName}*."; + + // Check for shipping details + $courier = $orderData['shipment']['courier_name'] ?? ''; + $tracking = $orderData['shipment']['tracking_link'] ?? ''; + if (!empty($courier) && !empty($tracking)) { + $message .= "\n🚚 الشحن عبر: {$courier}\nرابط التتبع: {$tracking}"; + } + } + + if (empty($message)) { + return; + } + + // Send WhatsApp Message via active session + $session = WhatsAppSession::findByCompany($companyId); + if (!$session || $session['status'] !== 'connected') { + error_log("[Salla Webhook Warning] Cannot send auto-notification: No active connected WhatsApp session for company: " . $companyId); + return; + } + + $this->sendWhatsAppNotification($session['session_key'], $customerPhone, $message); + } + + /** + * Send a WhatsApp message through the gateway + */ + private function sendWhatsAppNotification(string $sessionKey, string $phone, string $message) + { + $gatewayUrl = rtrim(getenv('WHATSAPP_GATEWAY_URL') ?: 'http://localhost:3722', '/'); + if (substr($gatewayUrl, -4) === '/api') { + $sendUrl = $gatewayUrl . '/messages/send'; + } else { + $sendUrl = $gatewayUrl . '/api/messages/send'; + } + + $payload = json_encode([ + 'session_key' => $sessionKey, + 'phone' => $phone, + 'message' => $message + ]); + + $ch = curl_init($sendUrl); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Content-Type: application/json', + 'X-Webhook-Secret: ' . getenv('WEBHOOK_SECRET') + ]); + curl_setopt($ch, CURLOPT_TIMEOUT, 10); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpCode !== 200) { + error_log("[Salla Webhook Gateway Error] Failed to send WhatsApp notification. Gateway response: " . $response); + } + } +} diff --git a/backend/app/Controllers/WhatsAppController.php b/backend/app/Controllers/WhatsAppController.php index c074d29..12619a4 100644 --- a/backend/app/Controllers/WhatsAppController.php +++ b/backend/app/Controllers/WhatsAppController.php @@ -343,12 +343,21 @@ class WhatsAppController extends BaseController $infoContext = $this->fetchUserInfoFromEndpoint($infoEndpoint, $msgData['phone']); } + // Dynamically fetch Salla order context if connected + $sallaContext = ""; + if (!empty($msgData['phone'])) { + $sallaContext = $this->fetchSallaOrderContext($session['company_id'], $msgData['phone'], $incomingText); + } + $systemPrompt = $rule['ai_prompt'] ?: 'You are a helpful customer support assistant.'; // Append real-time info context to Gemini system prompt if (!empty($infoContext)) { $systemPrompt .= "\n\n" . $infoContext; } + if (!empty($sallaContext)) { + $systemPrompt .= "\n\n" . $sallaContext; + } // Enforce language matching rule dynamically $systemPrompt .= "\n\nIMPORTANT LANGUAGE RULE: Detect the language of the incoming message. If the incoming message is in English, you MUST reply in English. If the incoming message is in Arabic, you MUST reply in Arabic. Override any default language instruction to match the user's language."; @@ -578,4 +587,124 @@ class WhatsAppController extends BaseController } return ""; } + + /** + * Fetch order info context from Salla e-commerce platform for the company + */ + private function fetchSallaOrderContext(int $companyId, string $phone, string $incomingText): string + { + try { + $accessToken = \App\Models\SallaMerchant::getOrRefreshAccessToken($companyId); + if (!$accessToken) { + return ""; // Salla is not integrated + } + + // Standardize customer phone to compare last 9 digits + $cleanPhone = preg_replace('/\D/', '', $phone); + if (empty($cleanPhone)) { + return ""; + } + + // 1. Check if user is asking about a specific order ID (e.g. sequence of 5-12 digits) + if (preg_match('/\b(\d{5,12})\b/', $incomingText, $matches)) { + $orderId = $matches[1]; + + $ch = curl_init("https://api.salla.dev/admin/v2/orders/{$orderId}"); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Authorization: Bearer ' . $accessToken + ]); + curl_setopt($ch, CURLOPT_TIMEOUT, 8); + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpCode === 200) { + $orderRes = json_decode($response, true); + $order = $orderRes['data'] ?? null; + if ($order) { + $orderCustPhone = preg_replace('/\D/', '', $order['customer']['mobile'] ?? $order['customer']['phone'] ?? ''); + // Security check: match last 9 digits of WhatsApp phone to order customer phone + if (substr($orderCustPhone, -9) === substr($cleanPhone, -9)) { + $status = $order['status']['name'] ?? 'غير معروف'; + $total = $order['amounts']['total']['amount'] ?? $order['total'] ?? ''; + $currency = $order['amounts']['total']['currency'] ?? 'SAR'; + $courier = $order['shipment']['courier_name'] ?? ''; + $tracking = $order['shipment']['tracking_link'] ?? ''; + $itemsCount = count($order['items'] ?? []); + + $context = "\n\n[تفاصيل طلب سلة المستعلم عنه للعميل:\n"; + $context .= "- رقم الطلب: {$orderId}\n"; + $context .= "- حالة الطلب الحالية: {$status}\n"; + $context .= "- إجمالي الطلب: {$total} {$currency}\n"; + $context .= "- عدد المنتجات: {$itemsCount}\n"; + if (!empty($courier)) { + $context .= "- شركة الشحن: {$courier}\n"; + } + if (!empty($tracking)) { + $context .= "- رابط تتبع الشحنة: {$tracking}\n"; + } + $context .= "الرجاء صياغة رد ودود ومختصر باللغة العربية لإخبار العميل بحالة هذا الطلب بالتحديد]"; + return $context; + } else { + // Order ID exists but phone mismatch + return "\n\n[تنبيه أمني للذكاء الاصطناعي: العميل سأل عن الطلب رقم {$orderId} ولكن هذا الطلب مسجل برقم هاتف مختلف في سلة. لحماية الخصوصية والأمان، يمنع منعاً باتاً عرض تفاصيل هذا الطلب له. أخبر العميل بلطف أن رقم الهاتف الحالي لا يتطابق مع رقم الهاتف المسجل في تفاصيل هذا الطلب ولا يمكنك كشف تفاصيله]"; + } + } + } + } + + // 2. Fetch list of recent orders for the merchant and search by customer phone number + $ch = curl_init("https://api.salla.dev/admin/v2/orders?page=1"); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Authorization: Bearer ' . $accessToken + ]); + curl_setopt($ch, CURLOPT_TIMEOUT, 8); + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpCode === 200) { + $ordersRes = json_decode($response, true); + $orders = $ordersRes['data'] ?? []; + if (is_array($orders)) { + foreach ($orders as $order) { + $orderCustPhone = preg_replace('/\D/', '', $order['customer']['mobile'] ?? $order['customer']['phone'] ?? ''); + if (substr($orderCustPhone, -9) === substr($cleanPhone, -9)) { + // Found the most recent order for this customer + $orderId = $order['id'] ?? $order['reference_id'] ?? ''; + $status = $order['status']['name'] ?? 'غير معروف'; + $total = $order['amounts']['total']['amount'] ?? $order['total'] ?? ''; + $currency = $order['amounts']['total']['currency'] ?? 'SAR'; + $courier = $order['shipment']['courier_name'] ?? ''; + $tracking = $order['shipment']['tracking_link'] ?? ''; + $itemsCount = count($order['items'] ?? []); + + $context = "\n\n[آخر طلب للعميل في متجر سلة:\n"; + $context .= "- رقم الطلب: {$orderId}\n"; + $context .= "- حالة الطلب الحالية: {$status}\n"; + $context .= "- إجمالي الطلب: {$total} {$currency}\n"; + $context .= "- عدد المنتجات: {$itemsCount}\n"; + if (!empty($courier)) { + $context .= "- شركة الشحن: {$courier}\n"; + } + if (!empty($tracking)) { + $context .= "- رابط تتبع الشحنة: {$tracking}\n"; + } + $context .= "الرجاء استخدام هذه التفاصيل للإجابة على استفساره حول حالة طلبه الأخير بدقة وود باللغة العربية]"; + return $context; + } + } + } + } + + return "\n\n[سياق المتجر: العميل متصل بمتجر سلة ولكن لم يتم العثور على أي طلبات سابقة له برقم الهاتف هذا. أخبره بلطف أنه لا توجد طلبات سابقة مسجلة برقم هاتفه الحالي في المتجر، واطلب منه تزويدك برقم الطلب أو البريد الإلكتروني للبحث]"; + + } catch (\Exception $e) { + error_log("[Fetch Salla Order Exception] " . $e->getMessage()); + } + return ""; + } } + diff --git a/backend/app/Models/SallaMerchant.php b/backend/app/Models/SallaMerchant.php new file mode 100644 index 0000000..b378f55 --- /dev/null +++ b/backend/app/Models/SallaMerchant.php @@ -0,0 +1,161 @@ + 5 mins remaining) + $expiresAt = strtotime($merchant['expires_at']); + if ($expiresAt && ($expiresAt - time()) > 300) { + return $merchant['access_token']; + } + + // Token expired or close to it, refresh + $clientId = getenv('SALLA_CLIENT_ID') ?: '69ea789c-f611-4ea7-a3ee-7ead41420225'; + $clientSecret = getenv('SALLA_CLIENT_SECRET') ?: ''; + + $ch = curl_init('https://accounts.salla.sa/oauth2/token'); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([ + 'client_id' => $clientId, + 'client_secret' => $clientSecret, + 'grant_type' => 'refresh_token', + 'refresh_token' => $merchant['refresh_token'] + ])); + curl_setopt($ch, CURLOPT_HTTPHEADER, [ + 'Content-Type: application/x-www-form-urlencoded' + ]); + curl_setopt($ch, CURLOPT_TIMEOUT, 15); + + $res = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpCode !== 200) { + error_log("Failed to refresh Salla token for company $companyId. Response: $res"); + return null; + } + + $tokenData = json_decode($res, true); + if (empty($tokenData['access_token'])) { + return null; + } + + $accessToken = $tokenData['access_token']; + $refreshToken = $tokenData['refresh_token'] ?? $merchant['refresh_token']; + $expiresIn = $tokenData['expires_in'] ?? 2592000; + $newExpiresAt = date('Y-m-d H:i:s', time() + $expiresIn); + + self::saveSecure([ + 'company_id' => $companyId, + 'merchant_id' => $merchant['merchant_id'], + 'store_name' => $merchant['store_name'], + 'access_token' => $accessToken, + 'refresh_token' => $refreshToken, + 'expires_at' => $newExpiresAt + ]); + + return $accessToken; + } + + + /** + * Ensure the salla_merchants table exists + */ + public static function ensureTableExists(): void + { + static $checked = false; + if ($checked) return; + + try { + Database::execute(" + CREATE TABLE IF NOT EXISTS `salla_merchants` ( + `id` INT AUTO_INCREMENT PRIMARY KEY, + `company_id` INT NOT NULL, + `merchant_id` VARCHAR(100) NOT NULL UNIQUE, + `store_name` VARCHAR(255) NULL, + `access_token` TEXT NOT NULL, + `refresh_token` TEXT NOT NULL, + `expires_at` TIMESTAMP NULL DEFAULT NULL, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (`company_id`) REFERENCES `companies`(`id`) ON DELETE CASCADE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + "); + $checked = true; + } catch (\Exception $e) { + error_log("Failed to ensure salla_merchants table: " . $e->getMessage()); + } + } +} diff --git a/backend/public/index.html b/backend/public/index.html index 0cd99cd..5585ebd 100644 --- a/backend/public/index.html +++ b/backend/public/index.html @@ -774,13 +774,26 @@ -
+ + +
@@ -1113,6 +1126,50 @@

+ +
+
+
+
+ 🛍️ +
+
+

+ + + +

+

+
+
+
+ + + +
+
+
+
@@ -1390,6 +1447,12 @@ }, endpoints: [], + // Salla Integration States + sallaStatus: null, + sallaLoading: false, + dashboardSuccess: '', + dashboardError: '', + // Forms contactName: '', contactPhone: '', @@ -1522,12 +1585,25 @@ this.templates = []; this.campaigns = []; this.endpoints = []; + this.sallaStatus = null; + this.dashboardSuccess = ''; + this.dashboardError = ''; }, initializeDashboard() { this.fetchWhatsappStatus(); + this.fetchSallaStatus(); // Set up persistent background status check this.startPolling(); + + // Detect Salla success connect + const urlParams = new URLSearchParams(window.location.search); + if (urlParams.get('salla_connect') === 'success') { + this.dashboardSuccess = this.lang === 'ar' + ? 'تم ربط متجر سلة بنجاح!' + : 'Salla store connected successfully!'; + window.history.replaceState({}, document.title, window.location.pathname); + } }, async fetchWhatsappStatus() { @@ -1716,6 +1792,65 @@ } }, + // Salla Integration Methods + async fetchSallaStatus() { + if (!this.token) return; + this.sallaLoading = true; + try { + const response = await fetch('/api/integrations/salla/status', { + headers: { 'Authorization': `Bearer ${this.token}` } + }); + const data = await response.json(); + if (response.ok && data.status === 'success') { + this.sallaStatus = data; + } else { + this.sallaStatus = { connected: false }; + } + } catch (err) { + console.error('Error fetching Salla status:', err); + this.sallaStatus = { connected: false }; + } finally { + this.sallaLoading = false; + } + }, + + connectSalla() { + if (!this.token) return; + window.location.href = `/api/integrations/salla/auth?token=${encodeURIComponent(this.token)}`; + }, + + async disconnectSalla() { + const confirmMsg = this.lang === 'ar' + ? 'هل أنت متأكد من رغبتك في إلغاء ربط متجر سلة؟' + : 'Are you sure you want to disconnect your Salla store?'; + if (!confirm(confirmMsg)) return; + + this.sallaLoading = true; + try { + const response = await fetch('/api/integrations/salla/disconnect', { + method: 'POST', + headers: { + 'Authorization': `Bearer ${this.token}`, + 'Content-Type': 'application/json' + } + }); + const data = await response.json(); + if (response.ok && data.status === 'success') { + this.dashboardSuccess = this.lang === 'ar' + ? 'تم إلغاء ربط متجر سلة بنجاح.' + : 'Salla store disconnected successfully.'; + await this.fetchSallaStatus(); + } else { + this.dashboardError = data.message || 'Failed to disconnect Salla integration.'; + } + } catch (err) { + console.error('Error disconnecting Salla:', err); + this.dashboardError = 'Failed to disconnect Salla integration.'; + } finally { + this.sallaLoading = false; + } + }, + // CRM List Fetchers async fetchContacts() { this.selectedContactIds = []; diff --git a/backend/public/index.php b/backend/public/index.php index fb3957a..eb027bc 100644 --- a/backend/public/index.php +++ b/backend/public/index.php @@ -73,6 +73,14 @@ $router->get('/api/endpoints', [\App\Controllers\EndpointController::class, $router->post('/api/endpoints', [\App\Controllers\EndpointController::class, 'store'], [\App\Middlewares\AuthMiddleware::class]); $router->delete('/api/endpoints', [\App\Controllers\EndpointController::class, 'delete'], [\App\Middlewares\AuthMiddleware::class]); +// Salla Platform Integration Routes (Phase 6+) +$router->get('/api/integrations/salla/auth', [\App\Controllers\SallaController::class, 'auth']); +$router->get('/api/integrations/salla/callback', [\App\Controllers\SallaController::class, 'callback']); +$router->get('/api/integrations/salla/status', [\App\Controllers\SallaController::class, 'status'], [\App\Middlewares\AuthMiddleware::class]); +$router->post('/api/integrations/salla/disconnect',[\App\Controllers\SallaController::class, 'disconnect'], [\App\Middlewares\AuthMiddleware::class]); +$router->post('/api/webhooks/salla', [\App\Controllers\SallaController::class, 'webhook']); + + // Mock External API for Entaleq Driver Info (Used to fetch real-time driver data) $router->post('/api/external/driver-info', function ($request, $response) { $body = $request->getBody();