From 8b52d2f11577ebf643aa68a69542afb9422f7312 Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Thu, 18 Jun 2026 14:59:24 +0300 Subject: [PATCH] feat: add Nabeh integration with phone-to-user resolution and environment configuration support --- backend/nabeh/resolve_user.php | 137 ++++++++ walletintaleq.intaleq.xyz/v2/.env.example | 41 +++ .../v2/main/jwtconnect.php | 17 +- .../v2/main/ride/nabeh/verify_payment.php | 292 ++++++++++++++++++ 4 files changed, 484 insertions(+), 3 deletions(-) create mode 100644 backend/nabeh/resolve_user.php create mode 100644 walletintaleq.intaleq.xyz/v2/.env.example create mode 100644 walletintaleq.intaleq.xyz/v2/main/ride/nabeh/verify_payment.php diff --git a/backend/nabeh/resolve_user.php b/backend/nabeh/resolve_user.php new file mode 100644 index 0000000..fe2d67c --- /dev/null +++ b/backend/nabeh/resolve_user.php @@ -0,0 +1,137 @@ + 'failure', 'message' => 'Method not allowed']); + exit; +} + +$apiKey = $_SERVER['HTTP_X_API_KEY'] ?? ''; +$expectedKey = getenv('NABEH_API_KEY') ?: ''; + +if (empty($apiKey) || $apiKey !== $expectedKey) { + http_response_code(401); + echo json_encode(['status' => 'failure', 'message' => 'Unauthorized']); + exit; +} + +$input = json_decode(file_get_contents('php://input'), true); +$rawPhone = preg_replace('/\D+/', '', $input['phone'] ?? ''); + +if (empty($rawPhone)) { + http_response_code(400); + echo json_encode(['status' => 'failure', 'message' => 'Phone number is required']); + exit; +} + +// تطبيع رقم الهاتف حسب الدولة (بدون +، بدون أصفار زائدة) +// حتى يتطابق مع التخزين في قاعدة البيانات (مثال: 9639XXXXXXX) +function normalizePhone($phone) { + $clean = preg_replace('/\D+/', '', $phone); + // Syria: 099XXXXXXX → 9639XXXXXXX + if (strlen($clean) === 10 && strpos($clean, '09') === 0) return '963' . substr($clean, 1); + if (strlen($clean) === 12 && strpos($clean, '963') === 0) return $clean; + if (strlen($clean) === 9 && strpos($clean, '9') === 0) return '963' . $clean; + // Jordan: 079XXXXXXX → 9627XXXXXXX + if (strlen($clean) === 10 && strpos($clean, '07') === 0) return '962' . substr($clean, 1); + if (strlen($clean) === 12 && strpos($clean, '962') === 0) return $clean; + if (strlen($clean) === 9 && strpos($clean, '7') === 0) return '962' . $clean; + // Egypt: 010XXXXXXXX → 2010XXXXXXXX + if (strlen($clean) === 11 && strpos($clean, '01') === 0) return '20' . substr($clean, 1); + if (strlen($clean) === 13 && strpos($clean, '20') === 0) return $clean; + return $clean; +} +$phone = normalizePhone($rawPhone); + +try { + $db = Database::get('main'); + global $encryptionHelper; + + $encryptedPhone = $encryptionHelper->encryptData($phone); + + // Look for driver first + $stmt = $db->prepare( + "SELECT id, phone, first_name, last_name FROM driver WHERE phone = :phone LIMIT 1" + ); + $stmt->execute([':phone' => $encryptedPhone]); + $driver = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($driver) { + echo json_encode([ + 'status' => 'success', + 'data' => [ + 'user_id' => $driver['id'], + 'phone' => $encryptionHelper->decryptData($driver['phone']), + 'name' => trim( + $encryptionHelper->decryptData($driver['first_name']) + . ' ' . + $encryptionHelper->decryptData($driver['last_name']) + ), + 'type' => 'driver', + ], + ], JSON_UNESCAPED_UNICODE); + exit; + } + + // Fallback: look for passenger + $stmt = $db->prepare( + "SELECT id, phone, first_name, last_name FROM passengers WHERE phone = :phone LIMIT 1" + ); + $stmt->execute([':phone' => $encryptedPhone]); + $passenger = $stmt->fetch(PDO::FETCH_ASSOC); + + if ($passenger) { + echo json_encode([ + 'status' => 'success', + 'data' => [ + 'user_id' => $passenger['id'], + 'phone' => $encryptionHelper->decryptData($passenger['phone']), + 'name' => trim( + $encryptionHelper->decryptData($passenger['first_name']) + . ' ' . + $encryptionHelper->decryptData($passenger['last_name']) + ), + 'type' => 'passenger', + ], + ], JSON_UNESCAPED_UNICODE); + exit; + } + + echo json_encode([ + 'status' => 'success', + 'data' => null, + 'message' => 'User not found', + ]); + +} catch (\Exception $e) { + error_log("[ResolveUser Error] " . $e->getMessage()); + http_response_code(500); + echo json_encode(['status' => 'failure', 'message' => 'Internal server error']); +} diff --git a/walletintaleq.intaleq.xyz/v2/.env.example b/walletintaleq.intaleq.xyz/v2/.env.example new file mode 100644 index 0000000..7ccb3bc --- /dev/null +++ b/walletintaleq.intaleq.xyz/v2/.env.example @@ -0,0 +1,41 @@ +# ============================================================================= +# Wallet Payment Server - Environment Configuration +# ============================================================================= +# Copy this to .env and fill in values: +# cp .env.example .env +# Or deploy to: /home/intaleq-walletintaleq/env/.env +# ============================================================================= + +# Database +dbname=WalletIntaleqDB +USER=root +PASS= + +# JWT / Security +SECRET_KEY= +SECRET_KEY_HMAC= +FP_PEPPER= +S2S_SHARED_KEY= +PAYMENT_KEY= +WEBHOOK_AUTH_TOKEN= +CRON_KEY= + +# Encryption +keyOfApp=<32_byte_hex> +initializationVector=<16_byte_hex> + +# Gemini AI (receipt analysis) +GEMINI_API_KEY= + +# Nabeh Integration (must match Nabeh's .env) +NABEH_API_KEY= + +# Siro Backend URL (for phone→driverID resolution) +# Used by verify_payment.php to call resolve_user.php +# Example: https://api-syria.siromove.com/siro +SIRO_BACKEND_URL=https://api-syria.siromove.com/siro + +# Admin login +passwordnewpassenger= +allowedWallet1=Tripz-Wallet +allowedWallet2=Intaleq-Wallet diff --git a/walletintaleq.intaleq.xyz/v2/main/jwtconnect.php b/walletintaleq.intaleq.xyz/v2/main/jwtconnect.php index 1eb5fbf..87b2a6f 100755 --- a/walletintaleq.intaleq.xyz/v2/main/jwtconnect.php +++ b/walletintaleq.intaleq.xyz/v2/main/jwtconnect.php @@ -10,7 +10,8 @@ * Path 2: Payment Key → PAYMENT_KEY header * Path 3: Webhook Token → X-Auth-Token header * Path 4: Cron Key / CLI → X-Cron-Key header أو CLI execution - * Path 5: JWT (default) → Authorization: Bearer + * Path 5: Nabeh API Key → X-API-Key header (server-to-server من منصة نبه) + * Path 6: JWT (default) → Authorization: Bearer * * أي طلب بدون أي مصادقة → يُرفض تلقائياً من authenticateJWT() * ═══════════════════════════════════════════════════════════════ @@ -40,7 +41,7 @@ if (in_array($origin, $allowedOrigins)) { header("Access-Control-Allow-Origin: https://walletintaleq.intaleq.xyz"); } header("Access-Control-Allow-Methods: GET, POST, OPTIONS"); -header("Access-Control-Allow-Headers: Content-Type, Authorization, X-S2S-Api-Key, PAYMENT_KEY, X-Auth-Token, X-Cron-Key, X-HMAC-Auth, X-Device-FP"); +header("Access-Control-Allow-Headers: Content-Type, Authorization, X-S2S-Api-Key, PAYMENT_KEY, X-Auth-Token, X-Cron-Key, X-HMAC-Auth, X-Device-FP, X-API-Key"); header('Content-Type: application/json'); // Handle preflight requests (OPTIONS) @@ -118,7 +119,17 @@ try { } } - // --- Path 5 (DEFAULT): JWT Authentication --- + // --- Path 5: Nabeh API Key (server-to-server من منصة نبه) --- + if (!$authMethod) { + $nabehKey = $_SERVER['HTTP_X_API_KEY'] ?? ''; + $expectedNabeh = getenv('NABEH_API_KEY'); + + if (!empty($nabehKey) && !empty($expectedNabeh) && hash_equals($expectedNabeh, $nabehKey)) { + $authMethod = 'NABEH'; + } + } + + // --- Path 6 (DEFAULT): JWT Authentication --- // إذا لم يتم التعرف على أي مسار آخر، يُفرض JWT. // authenticateJWT() ستُرجع 401 وتوقف التنفيذ إذا لم يكن هناك JWT صالح. if (!$authMethod) { diff --git a/walletintaleq.intaleq.xyz/v2/main/ride/nabeh/verify_payment.php b/walletintaleq.intaleq.xyz/v2/main/ride/nabeh/verify_payment.php new file mode 100644 index 0000000..8bbe4c2 --- /dev/null +++ b/walletintaleq.intaleq.xyz/v2/main/ride/nabeh/verify_payment.php @@ -0,0 +1,292 @@ + 'failure', 'message' => 'Method not allowed']); + exit; +} + +$raw = file_get_contents('php://input'); +$data = json_decode($raw, true) ?: $_POST; + +$driverId = trim($data['driver_id'] ?? ''); +$phone = trim($data['phone'] ?? ''); +$paymentMethod = strtolower(trim($data['payment_method'] ?? '')); +$receiptImage = $data['receipt_image'] ?? ''; +$imageMimeType = $data['image_mime_type'] ?? 'image/jpeg'; + +// ── Step 1: Resolve driverID ────────────────────────────────── +// driver_id (from Nabeh's Siro API resolution) is preferred +// phone fallback calls Siro backend resolve_user endpoint via S2S +$userName = ''; +$userPhone = $phone; +$userType = 'driver'; + +if (empty($driverId) && empty($phone)) { + printFailure('driver_id or phone is required'); + exit; +} + +if (empty($driverId) && !empty($phone)) { + $siroBackendUrl = rtrim(getenv('SIRO_BACKEND_URL') ?: 'https://api-syria.siromove.com/siro', '/'); + $resolveUrl = $siroBackendUrl . '/nabeh/resolve_user.php'; + + $resolvePayload = json_encode(['phone' => $phone]); + $apiKey = getenv('NABEH_API_KEY') ?: ''; + + $ch = curl_init($resolveUrl); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $resolvePayload, + CURLOPT_HTTPHEADER => [ + 'Content-Type: application/json', + 'X-API-Key: ' . $apiKey, + ], + CURLOPT_TIMEOUT => 10, + ]); + $resolveRes = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($httpCode !== 200 || empty($resolveRes)) { + printFailure('Could not resolve user. Please ensure you are registered in Siro.'); + exit; + } + + $resolveData = json_decode($resolveRes, true); + if (($resolveData['status'] ?? '') !== 'success' || empty($resolveData['data']['user_id'] ?? '')) { + printFailure('User not found in Siro system.'); + exit; + } + + $driverId = $resolveData['data']['user_id']; + $userName = $resolveData['data']['name'] ?? ''; + $userPhone = $resolveData['data']['phone'] ?? $phone; + $userType = $resolveData['data']['type'] ?? 'driver'; +} + +$paymentMethod = $paymentMethod ?: 'shamcash'; + +// ═══════════════════════════════════════════════════════════════ +// SHAMCASH — AI Verification (auto-find pending invoice) +// ═══════════════════════════════════════════════════════════════ +if ($paymentMethod === 'shamcash') { + // Auto-find latest pending invoice for this driver + $stmt = $con->prepare(" + SELECT id, invoice_number, amount, status, created_at + FROM invoices_shamcash + WHERE driverID = ? AND status = 'pending' + ORDER BY created_at DESC + LIMIT 1 + "); + $stmt->execute([$driverId]); + $invoice = $stmt->fetch(); + + if (!$invoice) { + $stmt = $con->prepare(" + SELECT id, invoice_number, amount, status, created_at + FROM invoices_shamcash + WHERE driverID = ? AND status = 'completed' + ORDER BY created_at DESC + LIMIT 1 + "); + $stmt->execute([$driverId]); + $lastCompleted = $stmt->fetch(); + + if ($lastCompleted) { + echo json_encode([ + 'status' => 'success', + 'verified'=> true, + 'message' => 'آخر فاتورة لديك مكتملة بالفعل.', + 'invoice' => $lastCompleted, + ], JSON_UNESCAPED_UNICODE); + exit; + } + + echo json_encode([ + 'status' => 'success', + 'verified'=> false, + 'message' => 'لا توجد فاتورة معلقة. يرجى إنشاء فاتورة عبر تطبيق Siro أولاً.', + ], JSON_UNESCAPED_UNICODE); + exit; + } + + // ── If no receipt image, just return invoice info ───── + if (empty($receiptImage)) { + echo json_encode([ + 'status' => 'success', + 'verified' => false, + 'requires_image' => true, + 'message' => "تم العثور على فاتورة رقم {$invoice['invoice_number']} بمبلغ {$invoice['amount']} ل.س. يرجى إرسال صورة الإيصال.", + 'invoice' => $invoice, + ], JSON_UNESCAPED_UNICODE); + exit; + } + + // ── Run AI verification ───────────────────────────────── + $geminiKey = getenv('GEMINI_API_KEY'); + if (empty($geminiKey)) { + printFailure('AI verification service not configured'); + exit; + } + + try { + $gemini = new GeminiAi($geminiKey); + $aiResult = $gemini->verifyPayment( + $invoice['invoice_number'], + $invoice['amount'], + 'ShamCash', + '', + $receiptImage + ); + + if (!empty($aiResult['verified'])) { + // ── AI confirmed → finalize ───────────────────── + $con->beginTransaction(); + + $upd = $con->prepare(" + UPDATE invoices_shamcash + SET status = 'processing' + WHERE id = ? AND status = 'pending' + "); + $upd->execute([$invoice['id']]); + + if ($upd->rowCount() > 0) { + require_once __DIR__ . '/../shamcash/finalize_deposit.php'; + + $finalized = finalizeShamCashDeposit($con, $invoice['id']); + + if ($finalized) { + $con->commit(); + echo json_encode([ + 'status' => 'success', + 'verified' => true, + 'message' => '✅ تم التحقق من عملية الدفع بنجاح! تم تحديث رصيد حسابك.', + 'invoice' => [ + 'invoice_number' => $invoice['invoice_number'], + 'amount' => $invoice['amount'], + 'status' => 'completed', + ], + 'ai_reason' => $aiResult['reason'] ?? null, + ], JSON_UNESCAPED_UNICODE); + } else { + $con->rollBack(); + echo json_encode([ + 'status' => 'error', + 'message' => 'Verification passed but wallet update failed. Contact support.', + ], JSON_UNESCAPED_UNICODE); + } + } else { + $con->rollBack(); + echo json_encode([ + 'status' => 'success', + 'verified'=> false, + 'message' => 'These funds have already been credited.', + ], JSON_UNESCAPED_UNICODE); + } + } else { + $reason = $aiResult['reason'] ?? 'لم يتم التأكيد'; + echo json_encode([ + 'status' => 'success', + 'verified' => false, + 'message' => "⚠️ $reason", + 'ai_reason' => $reason, + ], JSON_UNESCAPED_UNICODE); + } + } catch (Exception $e) { + error_log("[Nabeh ShamCash AI] " . $e->getMessage()); + printFailure('AI verification service error'); + } + exit; +} + +// ═══════════════════════════════════════════════════════════════ +// OTHER METHODS — Status query (find pending invoice by phone) +// ═══════════════════════════════════════════════════════════════ +$table = ''; +$columns = ''; +$conditions = ''; + +switch ($paymentMethod) { + case 'sms': + case 'syriatel': + $table = 'invoices_sms'; + $columns = "id, invoice_number, amount, status, NULL AS transaction_id, created_at, paid_at"; + $conditions = "driverID = ? AND status = 'pending'"; + break; + + case 'cliq': + $table = 'cliq_invoices'; + $columns = "id, invoice_number, amount, status, NULL AS transaction_id, created_at, updated_at AS paid_at"; + $conditions = "user_id = ? AND user_type = 'driver' AND status = 'pending'"; + break; + + case 'mtn': + $table = 'mtn_invoices'; + $columns = "id, invoice_number, amount, status, mtn_transaction_id AS transaction_id, created_at, updated_at AS paid_at"; + $conditions = "user_id = ? AND user_type = 'driver' AND status = 'pending'"; + break; + + default: + printFailure("Invalid payment method: $paymentMethod"); + exit; +} + +$stmt = $con->prepare(" + SELECT $columns, ? AS payment_method + FROM $table + WHERE $conditions + ORDER BY created_at DESC + LIMIT 5 +"); +$stmt->execute([$paymentMethod, $driverId]); +$invoices = $stmt->fetchAll(); + +echo json_encode([ + 'status' => 'success', + 'verified' => !empty($invoices), + 'message' => empty($invoices) ? 'لا توجد فواتير معلقة.' : null, + 'user' => [ + 'id' => $driverId, + 'phone' => $userPhone, + 'name' => $userName, + ], + 'invoices' => $invoices, +], JSON_UNESCAPED_UNICODE);