234 lines
8.9 KiB
PHP
234 lines
8.9 KiB
PHP
<?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') ?: 'image'); // Default: Luxury Image Card
|
||
|
||
$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 image/text OTP)
|
||
*/
|
||
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;
|
||
}
|
||
|
||
$selectedType = $method ?: $this->defaultType;
|
||
if (!in_array($selectedType, ['text', 'voice', 'image'], true)) {
|
||
$selectedType = 'image';
|
||
}
|
||
|
||
// 1. Attempt with selected type (image card)
|
||
$result = $this->attemptSend($phoneRaw, $selectedType, $otp, $appName, $bearerToken);
|
||
if ($result['success']) {
|
||
return $result;
|
||
}
|
||
|
||
// 2. If image failed, fallback to text mode
|
||
if ($selectedType === 'image') {
|
||
error_log("ℹ️ [Nabeh OTP Fallback] Image card attempt failed for {$phoneRaw}. Retrying with text mode...");
|
||
$textResult = $this->attemptSend($phoneRaw, 'text', $otp, $appName, $bearerToken);
|
||
if ($textResult['success']) {
|
||
$textResult['note'] = 'Delivered via 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') ? 45 : 20;
|
||
|
||
$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
|
||
];
|
||
}
|
||
}
|