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); } } }