Files
saqel/backend/app/Services/NabehService.php
T

236 lines
9.1 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace App\Services;
use App\Core\RedisClient;
class NabehService
{
private string $authUrl;
private string $sendUrl;
private string $email;
private string $password;
private string $defaultType;
public function __construct()
{
$this->authUrl = (string)getenv('NABEH_AUTH_URL');
$this->sendUrl = (string)getenv('NABEH_SEND_URL');
$this->email = (string)getenv('NABEH_EMAIL');
$this->password = (string)getenv('NABEH_PASSWORD');
$this->defaultType = (string)(getenv('NABEH_OTP_TYPE') ?: 'text');
$missing = [];
if (empty($this->authUrl)) $missing[] = 'NABEH_AUTH_URL';
if (empty($this->sendUrl)) $missing[] = 'NABEH_SEND_URL';
if (empty($this->email)) $missing[] = 'NABEH_EMAIL';
if (empty($this->password)) $missing[] = 'NABEH_PASSWORD';
if (!empty($missing)) {
throw new \RuntimeException("Nabeh Service configuration error: Missing environment variable(s): " . implode(', ', $missing));
}
}
/**
* Retrieve Nabeh JWT Bearer Token, caching it in Redis for 24 hours.
*/
public function getBearerToken(): ?string
{
// 1. Try fetching from Redis first
try {
$redis = RedisClient::getInstance();
$cachedToken = $redis->get('nabeh_bearer_token');
if ($cachedToken && strlen((string)$cachedToken) > 20) {
return (string)$cachedToken;
}
} catch (\Exception $e) {
error_log("⚠️ [Nabeh Auth Redis] Error reading token: " . $e->getMessage());
}
// 2. Token not cached, authenticate via Nabeh Login API
$payload = json_encode([
'email' => $this->email,
'password' => $this->password,
]);
$ch = curl_init($this->authUrl);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);
if ($curlError) {
error_log("❌ [Nabeh Auth cURL Error] " . $curlError);
return null;
}
if ($httpCode === 200 && $response) {
$decoded = json_decode($response, true);
$token = $decoded['token'] ?? $decoded['message']['token'] ?? $decoded['jwt'] ?? $decoded['access_token'] ?? null;
if ($token) {
// Cache token in Redis for 24h (86400 seconds)
try {
$redis = RedisClient::getInstance();
$redis->setex('nabeh_bearer_token', 86400, (string)$token);
error_log("✅ [Nabeh Auth] Token cached in Redis successfully.");
} catch (\Exception $e) {
error_log("⚠️ [Nabeh Auth Redis Cache Save] Error saving token: " . $e->getMessage());
}
return (string)$token;
}
}
error_log("❌ [Nabeh Auth Login Failed] Code: {$httpCode} | Response: {$response}");
return null;
}
/**
* Send OTP via Nabeh JWT Auth Gateway (WhatsApp text/image OTP)
* Text mode is ultra-fast (1-2s). Image mode generates dynamic card (10-25s).
*/
public function sendOtp(string $receiver, string $otp, ?string $method = null, string $appName = 'منصة صَقِل'): array
{
$bearerToken = $this->getBearerToken();
if (!$bearerToken) {
return [
'success' => false,
'error' => 'فشل الحصول على توكن المصادقة من منصة نبيه. تأكد من صحة NABEH_EMAIL و NABEH_PASSWORD في .env.'
];
}
// Clean phone format: ensure Jordan numbers are standardized (9627XXXXXXXX)
$phoneRaw = preg_replace('/\D+/', '', $receiver);
if (str_starts_with($phoneRaw, '07')) {
$phoneRaw = '962' . substr($phoneRaw, 1);
} elseif (str_starts_with($phoneRaw, '7') && strlen($phoneRaw) === 9) {
$phoneRaw = '962' . $phoneRaw;
}
// Selected type: provided method > defaultType > 'text'
$selectedType = $method ?: $this->defaultType;
if (!in_array($selectedType, ['text', 'voice', 'image'], true)) {
$selectedType = 'text';
}
// 1. First attempt with selected type (text or image)
$result = $this->attemptSend($phoneRaw, $selectedType, $otp, $appName, $bearerToken);
if ($result['success']) {
return $result;
}
// 2. If image failed or timed out, fallback to instant text OTP
if ($selectedType === 'image') {
error_log("ℹ️ [Nabeh OTP Fallback] Image mode failed/timed out for {$phoneRaw}. Retrying instantly with Text mode...");
$textResult = $this->attemptSend($phoneRaw, 'text', $otp, $appName, $bearerToken);
if ($textResult['success']) {
$textResult['note'] = 'Image OTP timed out, delivered via fast text fallback';
return $textResult;
}
return $textResult;
}
return $result;
}
private function attemptSend(string $phone, string $type, string $otp, string $appName, string $bearerToken): array
{
$payload = json_encode([
'phone' => $phone,
'type' => $type,
'code' => $otp,
'message' => "رمز التحقق الخاص بك لمنصة {$appName} هو: *{$otp}* \n الرجاء عدم مشاركته مع أي شخص لحماية حسابك.",
]);
$timeout = ($type === 'image') ? 50 : 25; // 50s for image rendering, 25s for text
$ch = curl_init($this->sendUrl);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $payload,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => $timeout,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_SSL_VERIFYHOST => false,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
"Authorization: Bearer {$bearerToken}",
],
]);
$startTime = microtime(true);
$response = curl_exec($ch);
$duration = round(microtime(true) - $startTime, 2);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);
if ($curlError) {
error_log("❌ [Nabeh OTP cURL Error (type={$type}, duration={$duration}s)] " . $curlError);
return [
'success' => false,
'type_used' => $type,
'duration' => "{$duration}s",
'http_code' => $httpCode,
'error' => 'cURL Error: ' . $curlError,
'response' => null
];
}
error_log("ℹ️ [Nabeh OTP Response (type={$type}, duration={$duration}s, HTTP {$httpCode})] " . $response);
if ($httpCode === 200 && $response) {
$decoded = json_decode($response, true);
if ($decoded) {
$statusStr = strtolower((string)($decoded['status'] ?? ''));
$msgStr = strtolower((string)($decoded['message'] ?? ''));
$errStr = strtolower((string)($decoded['error'] ?? ''));
if (
!empty($decoded['success']) ||
in_array($statusStr, ['success', 'ok', 'true', '200', 'sent', 'queued', '1'], true) ||
($decoded['status'] ?? false) === true ||
($decoded['code'] ?? 0) === 200 ||
!empty($decoded['message_id']) ||
!empty($decoded['id']) ||
!empty($decoded['token']) ||
str_contains($msgStr, 'success') ||
str_contains($msgStr, 'sent') ||
str_contains($msgStr, 'تم') ||
str_contains($errStr, 'via gateway')
) {
return [
'success' => true,
'type_used' => $type,
'duration' => "{$duration}s",
'http_code' => 200,
'message' => 'تم إرسال رمز التحقق بنجاح عبر الواتساب',
'raw' => $decoded
];
}
}
}
return [
'success' => false,
'type_used' => $type,
'duration' => "{$duration}s",
'http_code' => $httpCode,
'error' => 'Nabeh Gateway rejected request (HTTP ' . $httpCode . ')',
'response' => $response
];
}
}