From f13faa8c31e2660245e207766e05e1a303caea75 Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Thu, 18 Jun 2026 16:46:30 +0300 Subject: [PATCH] Update: 2026-06-18 16:46:30 --- backend/nabeh/get_user_rides.php | 93 +++++ backend/nabeh/submit_complaint.php | 335 ++++++++++++++++ walletintaleq.intaleq.xyz/v2/.env.example | 5 - .../v2/main/ride/cliq/create_cliq_invoice.php | 8 +- .../v2/main/ride/cliq/finalize_payment.php | 2 +- .../main/ride/mtn_new/create_mtn_invoice.php | 8 +- .../main/ride/nabeh/migration_add_phone.sql | 27 ++ .../v2/main/ride/nabeh/verify_payment.php | 359 ++++++++++-------- .../ride/shamcash/create_invoice_shamcash.php | 5 +- .../shamcash/passenger/create_invoice.php | 5 +- .../v2/main/sms_webhook/create_invoice.php | 7 +- .../sms_webhook/create_invoice_passenger.php | 8 +- 12 files changed, 693 insertions(+), 169 deletions(-) create mode 100644 backend/nabeh/get_user_rides.php create mode 100644 backend/nabeh/submit_complaint.php create mode 100644 walletintaleq.intaleq.xyz/v2/main/ride/nabeh/migration_add_phone.sql diff --git a/backend/nabeh/get_user_rides.php b/backend/nabeh/get_user_rides.php new file mode 100644 index 0000000..3b0f03c --- /dev/null +++ b/backend/nabeh/get_user_rides.php @@ -0,0 +1,93 @@ + 'failure', 'message' => 'Unauthorized']); + exit; +} + +$raw = file_get_contents('php://input'); +$input = json_decode($raw, true) ?: ($_SERVER['REQUEST_METHOD'] === 'GET' ? $_GET : []); +$phone = preg_replace('/\D+/', '', $input['phone'] ?? ''); +$limit = min(max((int)($input['limit'] ?? 5), 1), 20); + +if (empty($phone)) { + http_response_code(400); + echo json_encode(['status' => 'failure', 'message' => 'phone is required']); + exit; +} + +$mainDb = Database::get('main'); +$rideDb = Database::get('ride'); +global $encryptionHelper; + +// Resolve user +$encryptedPhone = $encryptionHelper->encryptData($phone); +$driver = $mainDb->prepare("SELECT id, 'driver' AS type FROM driver WHERE phone = :p LIMIT 1"); +$driver->execute([':p' => $encryptedPhone]); +$user = $driver->fetch(PDO::FETCH_ASSOC); + +if (!$user) { + $passenger = $mainDb->prepare("SELECT id, 'passenger' AS type FROM passengers WHERE phone = :p LIMIT 1"); + $passenger->execute([':p' => $encryptedPhone]); + $user = $passenger->fetch(PDO::FETCH_ASSOC); +} + +if (!$user) { + http_response_code(404); + echo json_encode(['status' => 'failure', 'message' => 'User not found']); + exit; +} + +$col = $user['type'] === 'driver' ? 'driver_id' : 'passenger_id'; +$stmt = $rideDb->prepare(" + SELECT id, start_location, end_location, date, time, endtime, + price, price_for_driver, price_for_passenger, + status, paymentMethod, carType, distance, created_at + FROM ride + WHERE $col = :uid + ORDER BY created_at DESC + LIMIT :lim +"); +$stmt->bindValue(':uid', $user['id'], PDO::PARAM_STR); +$stmt->bindValue(':lim', $limit, PDO::PARAM_INT); +$stmt->execute(); +$rides = $stmt->fetchAll(PDO::FETCH_ASSOC); + +echo json_encode([ + 'status' => 'success', + 'user' => [ + 'id' => $user['id'], + 'type' => $user['type'], + ], + 'rides' => $rides, +], JSON_UNESCAPED_UNICODE); diff --git a/backend/nabeh/submit_complaint.php b/backend/nabeh/submit_complaint.php new file mode 100644 index 0000000..6507d4f --- /dev/null +++ b/backend/nabeh/submit_complaint.php @@ -0,0 +1,335 @@ + '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); +$phone = preg_replace('/\D+/', '', $input['phone'] ?? ''); +$rideId = trim($input['ride_id'] ?? ''); +$complaintText = trim($input['complaint_text'] ?? ''); +$audioLink = trim($input['audio_link'] ?? ''); +$userType = trim($input['user_type'] ?? ''); + +if (empty($phone) || empty($rideId) || empty($complaintText)) { + http_response_code(400); + echo json_encode(['status' => 'failure', 'message' => 'phone, ride_id, and complaint_text are required']); + exit; +} + +$mainDb = Database::get('main'); +$rideDb = Database::get('ride'); +global $encryptionHelper; + +// ── Resolve user by phone ──────────────────────────────────── +$encryptedPhone = $encryptionHelper->encryptData($phone); +$driverRow = $mainDb->prepare("SELECT id, first_name, last_name FROM driver WHERE phone = :p LIMIT 1"); +$driverRow->execute([':p' => $encryptedPhone]); +$driver = $driverRow->fetch(PDO::FETCH_ASSOC); + +$passengerRow = null; +if (!$driver) { + $passengerRow = $mainDb->prepare("SELECT id, first_name, last_name FROM passengers WHERE phone = :p LIMIT 1"); + $passengerRow->execute([':p' => $encryptedPhone]); + $passenger = $passengerRow->fetch(PDO::FETCH_ASSOC); +} + +if (!$driver && !$passenger) { + http_response_code(404); + echo json_encode(['status' => 'failure', 'message' => 'User not found']); + exit; +} + +$userId = $driver ? $driver['id'] : $passenger['id']; +$detectedType = $driver ? 'driver' : 'passenger'; +if (empty($userType)) $userType = $detectedType; + +// ── Validate ride exists ───────────────────────────────────── +$stmt = $rideDb->prepare("SELECT * FROM ride WHERE id = :id"); +$stmt->execute([':id' => $rideId]); +$ride = $stmt->fetch(PDO::FETCH_ASSOC); + +if (!$ride) { + http_response_code(404); + echo json_encode(['status' => 'failure', 'message' => 'Ride not found']); + exit; +} + +// ── Fetch full context ────────────────────────────────────── +$passengerId = $ride['passenger_id']; +$driverId = $ride['driver_id']; + +/** + * Fetch user profile + full rating history (received + given) + */ +function getEnhancedProfile($db, $table, $id, $enc, $ratingReceivedTable, $ratingReceivedCol, $ratingGivenTable, $ratingGivenCol, $ratingsDb) { + $profile = ['info' => null, 'ratings_received' => [], 'ratings_given' => [], 'stats' => []]; + + // Profile info + $stmt = $db->prepare("SELECT id, first_name, last_name, created_at FROM $table WHERE id = :id LIMIT 1"); + $stmt->execute([':id' => $id]); + $info = $stmt->fetch(PDO::FETCH_ASSOC); + if ($info) { + $fn = $enc->decryptData($info['first_name']); + $ln = $enc->decryptData($info['last_name']); + $info['full_name'] = trim("$fn $ln"); + $info['account_age_days'] = $info['created_at'] ? round((time() - strtotime($info['created_at'])) / 86400) : 0; + unset($info['first_name'], $info['last_name']); + $profile['info'] = $info; + } + + // Ratings received (others rated this user) + $stmt = $ratingsDb->prepare(" + SELECT rating, comment, created_at + FROM $ratingReceivedTable + WHERE $ratingReceivedCol = :id + ORDER BY created_at DESC + LIMIT 10 + "); + $stmt->execute([':id' => $id]); + $profile['ratings_received'] = $stmt->fetchAll(PDO::FETCH_ASSOC); + + // Ratings given (this user rated others) + $stmt = $ratingsDb->prepare(" + SELECT rating, comment, created_at + FROM $ratingGivenTable + WHERE $ratingGivenCol = :id + ORDER BY created_at DESC + LIMIT 10 + "); + $stmt->execute([':id' => $id]); + $profile['ratings_given'] = $stmt->fetchAll(PDO::FETCH_ASSOC); + + // Aggregate stats for received ratings + $stmt = $ratingsDb->prepare(" + SELECT + COUNT(id) AS total, + AVG(rating) AS avg_rating, + SUM(CASE WHEN rating <= 2 THEN 1 ELSE 0 END) AS low_count, + SUM(CASE WHEN rating = 3 THEN 1 ELSE 0 END) AS mid_count, + SUM(CASE WHEN rating >= 4 THEN 1 ELSE 0 END) AS high_count + FROM $ratingReceivedTable + WHERE $ratingReceivedCol = :id + "); + $stmt->execute([':id' => $id]); + $profile['stats'] = $stmt->fetch(PDO::FETCH_ASSOC); + + return $profile; +} + +// Driver profile: received ratings from ratingDriver (by driver_id), given ratings in ratingPassenger (by driverID) +$driverProfile = getEnhancedProfile( + $mainDb, 'driver', $driverId, $encryptionHelper, + 'ratingDriver', 'driver_id', // received: passengers rate driver + 'ratingPassenger', 'driverID', // given: driver rates passenger + $mainDb +); + +// Passenger profile: received ratings from ratingPassenger (by passenger_id), given ratings in ratingDriver (by passenger_id) +$passengerProfile = getEnhancedProfile( + $mainDb, 'passengers', $passengerId, $encryptionHelper, + 'ratingPassenger', 'passenger_id', // received: drivers rate passenger + 'ratingDriver', 'passenger_id', // given: passenger rates driver + $mainDb +); + +// Driver behavior data +$behavior = null; +$bStmt = $rideDb->prepare("SELECT max_speed, avg_speed, hard_brakes, behavior_score FROM driver_behavior WHERE trip_id = :trip AND driver_id = :did LIMIT 1"); +$bStmt->execute([':trip' => $rideId, ':did' => $driverId]); +$behavior = $bStmt->fetch(PDO::FETCH_ASSOC) ?: null; + +// ── Gemini AI Analysis ────────────────────────────────────── +$geminiKey = getenv('GEMINI_API_KEY'); +if (!$geminiKey) { + http_response_code(500); + echo json_encode(['status' => 'failure', 'message' => 'AI service not configured']); + exit; +} + +// Check existing complaints for the same ride +$existingStmt = $mainDb->prepare("SELECT id, statusComplaint FROM complaint WHERE ride_id = :rid ORDER BY id DESC LIMIT 1"); +$existingStmt->execute([':rid' => $rideId]); +$existingComplaint = $existingStmt->fetch(PDO::FETCH_ASSOC); + +$prompt = " +أنت خبير في حل النزاعات في خدمات نقل الركاب لتطبيق Siro. قم بتحليل الشكوى التالية بناءً على البيانات الشاملة: + +**1. تفاصيل الرحلة:** +" . json_encode($ride, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . " + +**2. ملف الراكب (بيانات الحساب + سجل التقييمات):** +" . json_encode($passengerProfile, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . " + +**3. ملف السائق (بيانات الحساب + سجل التقييمات + سلوك القيادة):** +" . json_encode([ + 'info' => $driverProfile['info'], + 'ratings_received' => $driverProfile['ratings_received'], + 'ratings_given' => $driverProfile['ratings_given'], + 'stats' => $driverProfile['stats'], + 'behavior' => $behavior, + ], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . " + +**4. الشكوى:** +- نص الشكوى: '" . $complaintText . "' +- رابط تسجيل صوتي: " . ($audioLink ?: 'لا يوجد') . " +- مقدم الشكوى: " . $userType . " +" . ($existingComplaint ? "- شكوى سابقة موجودة للرحلة: ID={$existingComplaint['id']}, status={$existingComplaint['statusComplaint']}" : '') . " + +**تعليمات التحليل الذكي (التقييمات):** +- حلل سجل تقييمات السائق: هل يتكرر حصوله على تقييمات منخفضة (1-2)؟ ماذا تقول تعليقات الركاب السابقين عنه؟ +- حلل سجل تقييمات الراكب: هل يميل لإعطاء تقييمات منخفضة للسائقين؟ +- ادرس توزيع التقييمات: average + low/mid/high counts يعطي صورة عن سلوك كل طرف +- اربط التعليقات السابقة بمضمون الشكوى الحالية: هل هناك نمط متكرر؟ +- استخدم عمر الحساب (account_age_days) لتقييم مصداقية المستخدم + +**المطلوب:** +1. تحديد الطرف المخطئ على الأرجح بناءً على: تفاصيل الرحلة + تاريخ التقييمات + سلوك القيادة. +2. تحديد ما إذا كانت الشكوى حقيقية أم كيدية. +3. تصنيف الشكوى (سلوك السائق، مشكلة أجرة، مسار، حالة السيارة، غير ذلك). +4. اقتراح حلين واضحين ومحددين لخدمة العملاء. +5. كتابة تقرير مناسب لمقدم الشكوى (دون إحراج). +6. كتابة تقرير مناسب للطرف الآخر (مهذب ومحترم). + +**الخرج المطلوب (JSON فقط، بالعربية):** +{ + \"customerServiceSolutions\": [\"حل 1\", \"حل 2\"], + \"passengerReport\": {\"title\": \"...\", \"body\": \"...\"}, + \"driverReport\": {\"title\": \"...\", \"body\": \"...\"}, + \"fault_determination\": \"الراكب/السائق/كلاهما/غير واضح\", + \"complaint_nature\": \"حقيقية/كيدية/نزاع بسيط\", + \"complaint_type\": \"تصنيف الشكوى\" +} +"; + +$apiURL = "https://generativelanguage.googleapis.com/v1beta/models/gemini-flash-lite-latest:generateContent?key=$geminiKey"; +$ch = curl_init($apiURL); +curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_POST => true, + CURLOPT_HTTPHEADER => ['Content-Type: application/json'], + CURLOPT_POSTFIELDS => json_encode(['contents' => [['parts' => [['text' => $prompt]]]]]), + CURLOPT_TIMEOUT => 60, +]); +$response = curl_exec($ch); +$curlErr = curl_error($ch); +curl_close($ch); + +if ($curlErr) { + http_response_code(500); + echo json_encode(['status' => 'failure', 'message' => 'AI service error: ' . $curlErr]); + exit; +} + +$data = json_decode($response, true); +$rawText = $data['candidates'][0]['content']['parts'][0]['text'] ?? ''; +$cleanJson = trim(preg_replace('/```json|```/', '', $rawText)); +$analysis = json_decode($cleanJson, true); + +if (!$analysis || !isset($analysis['passengerReport']) || !isset($analysis['driverReport'])) { + http_response_code(500); + echo json_encode(['status' => 'failure', 'message' => 'Failed to parse AI response']); + exit; +} + +// ── Save to complaint table ────────────────────────────────── +$fullDesc = $complaintText; +if ($audioLink) $fullDesc .= "\n\n[audio: $audioLink]"; + +$stmt = $mainDb->prepare(" + INSERT INTO complaint + (ride_id, passenger_id, driver_id, complaint_type, description, + date_filed, statusComplaint, resolution, + passenger_report, driver_report, cs_solutions, + fault_determination, complaint_nature, date_resolved) + VALUES + (:rid, :pid, :did, :ctype, :desc, + NOW(), 'Resolved', :res, + :preport, :dreport, :cssol, + :fault, :nature, NOW()) +"); +$stmt->execute([ + ':rid' => $rideId, + ':pid' => $passengerId, + ':did' => $driverId, + ':ctype' => $analysis['complaint_type'] ?? 'General', + ':desc' => $fullDesc, + ':res' => $cleanJson, + ':preport'=> json_encode($analysis['passengerReport'] ?? null, JSON_UNESCAPED_UNICODE), + ':dreport'=> json_encode($analysis['driverReport'] ?? null, JSON_UNESCAPED_UNICODE), + ':cssol' => json_encode($analysis['customerServiceSolutions'] ?? null, JSON_UNESCAPED_UNICODE), + ':fault' => $analysis['fault_determination'] ?? 'N/A', + ':nature' => $analysis['complaint_nature'] ?? 'N/A', +]); +$complaintId = $mainDb->lastInsertId(); + +// ── Notify customer service ────────────────────────────────── +$csPhone = getenv('SERVICE_PHONE1'); +$sendFn = getenv('SEND_WHATSAPP_FN_PATH'); +if (!empty($csPhone) && $sendFn && file_exists($sendFn)) { + require_once $sendFn; + if (function_exists('sendWhatsAppFromServer')) { + $csMsg = "*شكوى جديدة (#$complaintId)*\n" + . "*- الرحلة:* $rideId\n" + . "*- مقدمها:* $userType\n" + . "*- تصنيف:* {$analysis['complaint_type']}\n" + . "*- المخطئ:* {$analysis['fault_determination']}\n" + . "*- الحلول:* {$analysis['customerServiceSolutions'][0]} / {$analysis['customerServiceSolutions'][1]}"; + sendWhatsAppFromServer($csPhone, $csMsg); + } +} + +// ── Response ───────────────────────────────────────────────── +$report = $userType === 'driver' ? $analysis['driverReport'] : $analysis['passengerReport']; +echo json_encode([ + 'status' => 'success', + 'message' => 'Complaint submitted and analyzed.', + 'complaint_id'=> $complaintId, + 'report' => $report, + 'ai_result' => [ + 'fault_determination' => $analysis['fault_determination'], + 'complaint_nature' => $analysis['complaint_nature'], + 'complaint_type' => $analysis['complaint_type'], + ], +], JSON_UNESCAPED_UNICODE); diff --git a/walletintaleq.intaleq.xyz/v2/.env.example b/walletintaleq.intaleq.xyz/v2/.env.example index 7ccb3bc..a3aa54b 100644 --- a/walletintaleq.intaleq.xyz/v2/.env.example +++ b/walletintaleq.intaleq.xyz/v2/.env.example @@ -30,11 +30,6 @@ 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 diff --git a/walletintaleq.intaleq.xyz/v2/main/ride/cliq/create_cliq_invoice.php b/walletintaleq.intaleq.xyz/v2/main/ride/cliq/create_cliq_invoice.php index 90abc18..6365bee 100755 --- a/walletintaleq.intaleq.xyz/v2/main/ride/cliq/create_cliq_invoice.php +++ b/walletintaleq.intaleq.xyz/v2/main/ride/cliq/create_cliq_invoice.php @@ -8,6 +8,7 @@ try { $userType = filterRequest("user_type"); $amount = filterRequest("amount"); $cliqPhone = filterRequest("cliq_phone"); + $phone = filterRequest("phone"); if (empty($userId) || empty($userType) || !is_numeric($amount) || $amount <= 0 || empty($cliqPhone)) { echo json_encode(["status" => "failure", "message" => "Invalid input provided."]); @@ -36,12 +37,14 @@ try { $upd = $con->prepare(" UPDATE cliq_invoices SET amount = :amount, + phone = :phone, cliq_phone = :cliq_phone, updated_at = NOW() WHERE id = :id "); $upd->execute([ ':amount' => $amount, + ':phone' => $phone ?: null, ':cliq_phone' => $cliqPhone, ':id' => $existing['id'], ]); @@ -59,14 +62,15 @@ try { $ins = $con->prepare(" INSERT INTO cliq_invoices - (invoice_number, user_id, user_type, amount, cliq_phone, status, created_at, updated_at) + (invoice_number, user_id, user_type, phone, amount, cliq_phone, status, created_at, updated_at) VALUES - (:invoice_number, :user_id, :user_type, :amount, :cliq_phone, 'pending', NOW(), NOW()) + (:invoice_number, :user_id, :user_type, :phone, :amount, :cliq_phone, 'pending', NOW(), NOW()) "); $ins->execute([ ':invoice_number' => $invoiceNumber, ':user_id' => $userId, ':user_type' => $userType, + ':phone' => $phone ?: null, ':amount' => $amount, ':cliq_phone' => $cliqPhone ]); diff --git a/walletintaleq.intaleq.xyz/v2/main/ride/cliq/finalize_payment.php b/walletintaleq.intaleq.xyz/v2/main/ride/cliq/finalize_payment.php index adb7862..97b8a9f 100755 --- a/walletintaleq.intaleq.xyz/v2/main/ride/cliq/finalize_payment.php +++ b/walletintaleq.intaleq.xyz/v2/main/ride/cliq/finalize_payment.php @@ -14,7 +14,7 @@ function finalizeClickPayment(PDO $con, int $invoiceId): array { try { // جلب تفاصيل الفاتورة - $stmt = $con->prepare("SELECT * FROM `click_invoices` WHERE id = :id AND status = 'completed' LIMIT 1"); + $stmt = $con->prepare("SELECT * FROM `cliq_invoices` WHERE id = :id AND status = 'completed' LIMIT 1"); $stmt->execute([':id' => $invoiceId]); $invoice = $stmt->fetch(PDO::FETCH_ASSOC); diff --git a/walletintaleq.intaleq.xyz/v2/main/ride/mtn_new/create_mtn_invoice.php b/walletintaleq.intaleq.xyz/v2/main/ride/mtn_new/create_mtn_invoice.php index ed7acd3..c885049 100755 --- a/walletintaleq.intaleq.xyz/v2/main/ride/mtn_new/create_mtn_invoice.php +++ b/walletintaleq.intaleq.xyz/v2/main/ride/mtn_new/create_mtn_invoice.php @@ -11,6 +11,7 @@ try { $userType = filterRequest("user_type"); // 'driver' أو 'passenger' $amount = filterRequest("amount"); $mtnPhone = filterRequest("mtn_phone"); + $phone = filterRequest("phone"); if (empty($userId) || empty($userType) || !is_numeric($amount) || $amount <= 0 || empty($mtnPhone)) { echo json_encode(["status" => "failure", "message" => "Invalid input provided."]); @@ -43,12 +44,14 @@ try { $upd = $con->prepare(" UPDATE mtn_invoices SET amount = :amount, + phone = :phone, mtn_phone = :mtn_phone, updated_at = NOW() WHERE id = :id "); $upd->execute([ ':amount' => $amount, + ':phone' => $phone ?: null, ':mtn_phone'=> $mtnPhone, ':id' => $existing['id'], ]); @@ -67,14 +70,15 @@ try { $ins = $con->prepare(" INSERT INTO mtn_invoices - (invoice_number, user_id, user_type, amount, mtn_phone, status, created_at, updated_at) + (invoice_number, user_id, user_type, phone, amount, mtn_phone, status, created_at, updated_at) VALUES - (:invoice_number, :user_id, :user_type, :amount, :mtn_phone, 'pending', NOW(), NOW()) + (:invoice_number, :user_id, :user_type, :phone, :amount, :mtn_phone, 'pending', NOW(), NOW()) "); $ins->execute([ ':invoice_number' => $invoiceNumber, ':user_id' => $userId, ':user_type' => $userType, + ':phone' => $phone ?: null, ':amount' => $amount, ':mtn_phone' => $mtnPhone ]); diff --git a/walletintaleq.intaleq.xyz/v2/main/ride/nabeh/migration_add_phone.sql b/walletintaleq.intaleq.xyz/v2/main/ride/nabeh/migration_add_phone.sql new file mode 100644 index 0000000..0c1a97d --- /dev/null +++ b/walletintaleq.intaleq.xyz/v2/main/ride/nabeh/migration_add_phone.sql @@ -0,0 +1,27 @@ +-- Migration: Add phone column to all invoice tables +-- Allows direct phone lookup instead of S2S resolve_user +-- Run: mysql -u root WalletIntaleqDB < migration_add_phone.sql + +ALTER TABLE invoices_shamcash + ADD COLUMN phone VARCHAR(20) AFTER driverID, + ADD INDEX idx_phone_status (phone, status); + +ALTER TABLE invoices_shamcash_passenger + ADD COLUMN phone VARCHAR(20) AFTER passengerID, + ADD INDEX idx_phone_status (phone, status); + +ALTER TABLE cliq_invoices + ADD COLUMN phone VARCHAR(20) AFTER user_type, + ADD INDEX idx_phone_status (phone, status); + +ALTER TABLE invoices_sms + ADD COLUMN phone VARCHAR(20) AFTER driverID, + ADD INDEX idx_phone_status (phone, status); + +ALTER TABLE invoices_sms_passenger + ADD COLUMN phone VARCHAR(20) AFTER passengerID, + ADD INDEX idx_phone_status (phone, status); + +ALTER TABLE mtn_invoices + ADD COLUMN phone VARCHAR(20) AFTER user_type, + ADD INDEX idx_phone_status (phone, status); diff --git a/walletintaleq.intaleq.xyz/v2/main/ride/nabeh/verify_payment.php b/walletintaleq.intaleq.xyz/v2/main/ride/nabeh/verify_payment.php index 8bbe4c2..d2d1279 100644 --- a/walletintaleq.intaleq.xyz/v2/main/ride/nabeh/verify_payment.php +++ b/walletintaleq.intaleq.xyz/v2/main/ride/nabeh/verify_payment.php @@ -2,33 +2,25 @@ /** * Nabeh Payment Verification Endpoint * - * Auto-detects the user's pending invoice and uses Gemini AI to verify - * the receipt image against the invoice. No manual invoice number needed. + * Simplified: uses phone directly to find pending invoice (no S2S resolve_user). + * Added Cliq AI verification with receipt image. * * =============================== * INPUT (JSON body) * =============================== - * driver_id (optional) — from Nabeh's Siro API resolution (preferred) - * phone (required if no driver_id) — lookup via Siro backend resolve_user - * payment_method (required) — shamcash / cliq / mtn / sms - * receipt_image (optional for AI verification) - * image_mime_type (optional, default: image/jpeg) + * phone (required) — User's phone number + * payment_method (req) — shamcash / cliq / sms / mtn + * receipt_image (opt) — Receipt screenshot for AI verification + * image_mime_type (opt) — Default: image/jpeg * * =============================== * FLOW * =============================== * 1. Auth via jwtconnect.php (X-API-Key → NABEH_API_KEY) - * 2. Resolve driverID: - * a. Use driver_id directly if provided - * b. Otherwise call Siro backend resolve_user.php (phone → driverID) - * 3. Auto-find latest pending invoice for that driver - * 4. If shamcash + receipt_image: - * a. Call GeminiAi::verifyPayment(invoice_number, amount, "ShamCash", "", receipt_image) - * b. Gemini returns {"verified": true/false, "reason": "..."} - * c. If verified → UPDATE status='processing' → finalizeShamCashDeposit() - * d. Return result - * 5. If other methods or no receipt_image: - * - Return invoice status info + * 2. Find latest pending invoice by phone + payment_method + * 3. If shamcash/cliq + receipt_image → Gemini AI verification + * AI confirms → update status → finalize deposit → return success + * 4. Otherwise return invoice status * * Auth: X-API-Key header → NABEH_API_KEY (via jwtconnect.php Path 5) */ @@ -45,110 +37,72 @@ if ($_SERVER['REQUEST_METHOD'] !== 'POST') { $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'; +$phone = preg_replace('/\D+/', '', $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'); +if (empty($phone)) { + printFailure('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) +// HELPER: find pending invoice by phone // ═══════════════════════════════════════════════════════════════ -if ($paymentMethod === 'shamcash') { - // Auto-find latest pending invoice for this driver +function findPendingByPhone(PDO $con, string $table, string $phone, string $orderCol = 'created_at'): ?array +{ $stmt = $con->prepare(" SELECT id, invoice_number, amount, status, created_at - FROM invoices_shamcash - WHERE driverID = ? AND status = 'pending' + FROM $table + WHERE phone = ? AND status = 'pending' + ORDER BY $orderCol DESC + LIMIT 1 + "); + $stmt->execute([$phone]); + return $stmt->fetch(PDO::FETCH_ASSOC) ?: null; +} + +function findLastCompletedByPhone(PDO $con, string $table, string $phone): ?array +{ + $stmt = $con->prepare(" + SELECT id, invoice_number, amount, status, created_at + FROM $table + WHERE phone = ? AND status = 'completed' ORDER BY created_at DESC LIMIT 1 "); - $stmt->execute([$driverId]); - $invoice = $stmt->fetch(); + $stmt->execute([$phone]); + return $stmt->fetch(PDO::FETCH_ASSOC) ?: null; +} + +// ═══════════════════════════════════════════════════════════════ +// SHAMCASH — AI Verification +// ═══════════════════════════════════════════════════════════════ +if ($paymentMethod === 'shamcash') { + $invoice = findPendingByPhone($con, 'invoices_shamcash', $phone); 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(); - + $lastCompleted = findLastCompletedByPhone($con, 'invoices_shamcash', $phone); if ($lastCompleted) { echo json_encode([ - 'status' => 'success', - 'verified'=> true, - 'message' => 'آخر فاتورة لديك مكتملة بالفعل.', - 'invoice' => $lastCompleted, + 'status' => 'success', + 'verified' => true, + 'message' => 'آخر فاتورة لديك مكتملة بالفعل.', + 'invoice' => $lastCompleted, ], JSON_UNESCAPED_UNICODE); exit; } - echo json_encode([ - 'status' => 'success', - 'verified'=> false, - 'message' => 'لا توجد فاتورة معلقة. يرجى إنشاء فاتورة عبر تطبيق Siro أولاً.', + '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', @@ -160,7 +114,7 @@ if ($paymentMethod === 'shamcash') { exit; } - // ── Run AI verification ───────────────────────────────── + // ── AI verify ─────────────────────────────────────────── $geminiKey = getenv('GEMINI_API_KEY'); if (empty($geminiKey)) { printFailure('AI verification service not configured'); @@ -178,9 +132,7 @@ if ($paymentMethod === 'shamcash') { ); if (!empty($aiResult['verified'])) { - // ── AI confirmed → finalize ───────────────────── $con->beginTransaction(); - $upd = $con->prepare(" UPDATE invoices_shamcash SET status = 'processing' @@ -190,9 +142,7 @@ if ($paymentMethod === 'shamcash') { if ($upd->rowCount() > 0) { require_once __DIR__ . '/../shamcash/finalize_deposit.php'; - $finalized = finalizeShamCashDeposit($con, $invoice['id']); - if ($finalized) { $con->commit(); echo json_encode([ @@ -216,9 +166,9 @@ if ($paymentMethod === 'shamcash') { } else { $con->rollBack(); echo json_encode([ - 'status' => 'success', - 'verified'=> false, - 'message' => 'These funds have already been credited.', + 'status' => 'success', + 'verified' => false, + 'message' => 'These funds have already been credited.', ], JSON_UNESCAPED_UNICODE); } } else { @@ -238,55 +188,162 @@ if ($paymentMethod === 'shamcash') { } // ═══════════════════════════════════════════════════════════════ -// OTHER METHODS — Status query (find pending invoice by phone) +// CLIQ — AI Verification (same pattern as ShamCash) // ═══════════════════════════════════════════════════════════════ -$table = ''; -$columns = ''; -$conditions = ''; +if ($paymentMethod === 'cliq') { + $invoice = findPendingByPhone($con, 'cliq_invoices', $phone); -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"); + if (!$invoice) { + $lastCompleted = findLastCompletedByPhone($con, 'cliq_invoices', $phone); + 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 (empty($receiptImage)) { + echo json_encode([ + 'status' => 'success', + 'verified' => false, + 'requires_image' => true, + 'message' => "تم العثور على فاتورة رقم {$invoice['invoice_number']} بمبلغ {$invoice['amount']} دينار. يرجى إرسال صورة الإيصال.", + 'invoice' => $invoice, + ], JSON_UNESCAPED_UNICODE); + exit; + } + + // ── AI verify ─────────────────────────────────────────── + $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'], + 'Cliq', + '', + $receiptImage + ); + + if (!empty($aiResult['verified'])) { + $con->beginTransaction(); + $upd = $con->prepare(" + UPDATE cliq_invoices + SET status = 'completed', updated_at = NOW() + WHERE id = ? AND status = 'pending' + "); + $upd->execute([$invoice['id']]); + + if ($upd->rowCount() > 0) { + require_once __DIR__ . '/../cliq/finalize_payment.php'; + $finalized = finalizeClickPayment($con, $invoice['id']); + if ($finalized['success']) { + $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 Cliq AI] " . $e->getMessage()); + printFailure('AI verification service error'); + } + 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(); +// ═══════════════════════════════════════════════════════════════ +// SMS / SYRIATEL — Status query by phone +// ═══════════════════════════════════════════════════════════════ +if ($paymentMethod === 'sms' || $paymentMethod === 'syriatel') { + $stmt = $con->prepare(" + SELECT id, invoice_number, user_phone AS method_phone, amount, status, created_at, ? AS payment_method + FROM invoices_sms + WHERE phone = ? AND status = 'pending' + ORDER BY created_at DESC + LIMIT 5 + "); + $stmt->execute([$paymentMethod, $phone]); + $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); + echo json_encode([ + 'status' => 'success', + 'verified' => !empty($invoices), + 'message' => empty($invoices) ? 'لا توجد فواتير معلقة.' : null, + 'invoices' => $invoices, + ], JSON_UNESCAPED_UNICODE); + exit; +} + +// ═══════════════════════════════════════════════════════════════ +// MTN — Status query by phone +// ═══════════════════════════════════════════════════════════════ +if ($paymentMethod === 'mtn') { + $stmt = $con->prepare(" + SELECT id, invoice_number, mtn_phone AS method_phone, amount, status, + mtn_transaction_id AS transaction_id, created_at, updated_at AS paid_at, ? AS payment_method + FROM mtn_invoices + WHERE phone = ? AND status = 'pending' + ORDER BY created_at DESC + LIMIT 5 + "); + $stmt->execute([$paymentMethod, $phone]); + $invoices = $stmt->fetchAll(); + + echo json_encode([ + 'status' => 'success', + 'verified' => !empty($invoices), + 'message' => empty($invoices) ? 'لا توجد فواتير معلقة.' : null, + 'invoices' => $invoices, + ], JSON_UNESCAPED_UNICODE); + exit; +} + +// ═══════════════════════════════════════════════════════════════ +// UNKNOWN METHOD +// ═══════════════════════════════════════════════════════════════ +printFailure("Invalid payment method: $paymentMethod"); diff --git a/walletintaleq.intaleq.xyz/v2/main/ride/shamcash/create_invoice_shamcash.php b/walletintaleq.intaleq.xyz/v2/main/ride/shamcash/create_invoice_shamcash.php index 517ee63..66cbfef 100755 --- a/walletintaleq.intaleq.xyz/v2/main/ride/shamcash/create_invoice_shamcash.php +++ b/walletintaleq.intaleq.xyz/v2/main/ride/shamcash/create_invoice_shamcash.php @@ -7,6 +7,7 @@ include "../../jwtconnect.php"; try { $driverID = filterRequest("driverID"); $amount_raw = filterRequest("amount"); + $phone = filterRequest("phone"); $amount = is_numeric($amount_raw) ? (float) $amount_raw : 0.0; @@ -29,8 +30,8 @@ try { } else { // إنشاء فاتورة جديدة برقم عشوائي $invoice_number = random_int(100000, 999999); - $stmtIns = $con->prepare("INSERT INTO invoices_shamcash (invoice_number, driverID, amount, status, created_at) VALUES (?, ?, ?, 'pending', NOW())"); - $stmtIns->execute([$invoice_number, $driverID, $amount]); + $stmtIns = $con->prepare("INSERT INTO invoices_shamcash (invoice_number, driverID, phone, amount, status, created_at) VALUES (?, ?, ?, ?, 'pending', NOW())"); + $stmtIns->execute([$invoice_number, $driverID, $phone ?: null, $amount]); } echo json_encode([ diff --git a/walletintaleq.intaleq.xyz/v2/main/ride/shamcash/passenger/create_invoice.php b/walletintaleq.intaleq.xyz/v2/main/ride/shamcash/passenger/create_invoice.php index e64f8f8..6751a60 100755 --- a/walletintaleq.intaleq.xyz/v2/main/ride/shamcash/passenger/create_invoice.php +++ b/walletintaleq.intaleq.xyz/v2/main/ride/shamcash/passenger/create_invoice.php @@ -6,6 +6,7 @@ include "../../../jwtconnect.php"; try { $passengerID = filterRequest("passengerID"); $amount_raw = filterRequest("amount"); + $phone = filterRequest("phone"); $amount = is_numeric($amount_raw) ? (float) $amount_raw : 0.0; if (empty($passengerID) || $amount <= 0) { @@ -25,8 +26,8 @@ try { $con->prepare("UPDATE invoices_shamcash_passenger SET created_at=NOW() WHERE id=?")->execute([$existing['id']]); } else { $invoice_number = random_int(100000, 999999); - $stmtIns = $con->prepare("INSERT INTO invoices_shamcash_passenger (invoice_number, passengerID, amount, status, created_at) VALUES (?, ?, ?, 'pending', NOW())"); - $stmtIns->execute([$invoice_number, $passengerID, $amount]); + $stmtIns = $con->prepare("INSERT INTO invoices_shamcash_passenger (invoice_number, passengerID, phone, amount, status, created_at) VALUES (?, ?, ?, ?, 'pending', NOW())"); + $stmtIns->execute([$invoice_number, $passengerID, $phone ?: null, $amount]); } echo json_encode([ diff --git a/walletintaleq.intaleq.xyz/v2/main/sms_webhook/create_invoice.php b/walletintaleq.intaleq.xyz/v2/main/sms_webhook/create_invoice.php index 5198a37..e7c1e99 100755 --- a/walletintaleq.intaleq.xyz/v2/main/sms_webhook/create_invoice.php +++ b/walletintaleq.intaleq.xyz/v2/main/sms_webhook/create_invoice.php @@ -16,6 +16,7 @@ try { $driverID = filterRequest("driverID"); $user_phone = filterRequest("user_phone"); + $phone = filterRequest("phone"); $amount_raw = filterRequest("amount"); // تسجيل البيانات بعد الفلترة @@ -46,10 +47,11 @@ try { // --- 4a. تحديث الفاتورة المعلقة الحالية --- // error_log("[CreateInvoice] Found existing pending invoice (ID: {$existing['id']}). Updating it."); - $sql_update = "UPDATE invoices_sms SET invoice_number = :invoice_number, amount = :amount, created_at = NOW() WHERE id = :id"; + $sql_update = "UPDATE invoices_sms SET invoice_number = :invoice_number, phone = :phone, amount = :amount, created_at = NOW() WHERE id = :id"; $stmt_update = $con->prepare($sql_update); $stmt_update->execute([ ':invoice_number' => $new_invoice_number, + ':phone' => $phone ?: null, ':amount' => $amount, ':id' => $existing['id'] ]); @@ -65,11 +67,12 @@ try { // --- 4b. إنشاء فاتورة جديدة --- // error_log("[CreateInvoice] No pending invoice found. Creating a new one."); - $sql_insert = "INSERT INTO invoices_sms (invoice_number, driverID, user_phone, amount, status) VALUES (:invoice_number, :driverID, :user_phone, :amount, 'pending')"; + $sql_insert = "INSERT INTO invoices_sms (invoice_number, driverID, phone, user_phone, amount, status) VALUES (:invoice_number, :driverID, :phone, :user_phone, :amount, 'pending')"; $stmt_insert = $con->prepare($sql_insert); $ok = $stmt_insert->execute([ ':invoice_number' => $new_invoice_number, ':driverID' => $driverID, + ':phone' => $phone ?: null, ':user_phone' => $user_phone, ':amount' => $amount ]); diff --git a/walletintaleq.intaleq.xyz/v2/main/sms_webhook/create_invoice_passenger.php b/walletintaleq.intaleq.xyz/v2/main/sms_webhook/create_invoice_passenger.php index cd96d6c..cb64323 100755 --- a/walletintaleq.intaleq.xyz/v2/main/sms_webhook/create_invoice_passenger.php +++ b/walletintaleq.intaleq.xyz/v2/main/sms_webhook/create_invoice_passenger.php @@ -17,6 +17,7 @@ try { // ------------------------------------- $passengerID = filterRequest("passengerID"); $user_phone = filterRequest("user_phone"); + $phone = filterRequest("phone"); $amount_raw = filterRequest("amount"); error_log("[CreateInvoicePassenger] Read inputs (passengerID, user_phone, amount)"); @@ -65,6 +66,7 @@ try { $sql_update = " UPDATE invoices_sms_passenger SET invoice_number = :invoice_number, + phone = :phone, amount = :amount, created_at = NOW() WHERE id = :id @@ -72,6 +74,7 @@ try { $stmt_update = $con->prepare($sql_update); $stmt_update->execute([ ':invoice_number' => $new_invoice_number, + ':phone' => $phone ?: null, ':amount' => $amount, ':id' => $existing['id'] ]); @@ -89,13 +92,14 @@ try { // ------------------------------------- error_log("[CreateInvoicePassenger] No existing invoice. Creating new one..."); $sql_insert = " - INSERT INTO invoices_sms_passenger (invoice_number, passengerID, user_phone, amount, status) - VALUES (:invoice_number, :passengerID, :user_phone, :amount, 'pending') + INSERT INTO invoices_sms_passenger (invoice_number, passengerID, phone, user_phone, amount, status) + VALUES (:invoice_number, :passengerID, :phone, :user_phone, :amount, 'pending') "; $stmt_insert = $con->prepare($sql_insert); $ok = $stmt_insert->execute([ ':invoice_number' => $new_invoice_number, ':passengerID' => $passengerID, + ':phone' => $phone ?: null, ':user_phone' => $user_phone, ':amount' => $amount ]);