Files
nabeh/backend/app/Controllers/SallaController.php
2026-05-22 04:05:29 +03:00

336 lines
13 KiB
PHP

<?php
namespace App\Controllers;
use App\Core\Request;
use App\Core\Response;
use App\Core\Security;
use App\Models\SallaMerchant;
use App\Models\WhatsAppSession;
class SallaController extends BaseController
{
/**
* Redirect user to Salla OAuth login page.
* Accessible via GET /api/integrations/salla/auth?token=JWT_TOKEN
*/
public function auth(Request $request, Response $response)
{
$token = $_GET['token'] ?? '';
if (empty($token)) {
$response->status(400)->html("<h3>Error: Token is required</h3>");
return;
}
$payload = Security::verifyJWT($token);
if (!$payload) {
$response->status(401)->html("<h3>Error: Invalid token</h3>");
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',
'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)
{
// Handle Salla returning an error redirect (e.g. invalid_scope, access_denied)
if (!empty($_GET['error'])) {
$errorCode = $_GET['error'] ?? 'unknown_error';
$errorDesc = $_GET['error_description'] ?? 'An unknown error occurred during Salla authorization.';
error_log("[Salla OAuth Error] $errorCode: $errorDesc");
$appUrl = rtrim(getenv('APP_URL') ?: 'https://nabeh.intaleqapp.com', '/');
header("Location: {$appUrl}/?salla_connect=error&reason=" . urlencode($errorDesc));
exit;
}
$code = $_GET['code'] ?? '';
$companyId = $_GET['state'] ?? '';
if (empty($code) || empty($companyId)) {
$response->status(400)->html("<h3>Error: Missing authorization code or state (company_id).</h3>");
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("<h3>Failed to exchange code for Salla token. Status code: $httpCode</h3>");
return;
}
$tokenData = json_decode($res, true);
if (empty($tokenData['access_token'])) {
$response->status(500)->html("<h3>Failed to parse Salla token response.</h3>");
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("<h3>Failed to fetch merchant profile from Salla.</h3>");
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);
}
}
}