Update: 2026-06-18 16:46:30

This commit is contained in:
Hamza-Ayed
2026-06-18 16:46:30 +03:00
parent 8b52d2f115
commit f13faa8c31
12 changed files with 693 additions and 169 deletions

View File

@@ -0,0 +1,93 @@
<?php
/**
* Nabeh Integration — Get User Recent Rides
*
* Returns the most recent rides for a user (driver or passenger)
* identified by phone number. Used by the complaint workflow to
* let the user pick which trip they're complaining about.
*
* Auth: X-API-Key header → NABEH_API_KEY
*
* Input:
* phone (required) — User's phone number
* limit (opt) — Max rides to return (default 5, max 20)
*
* Output:
* List of rides with id, date, time, price, locations, status, etc.
*/
require_once __DIR__ . '/../core/bootstrap.php';
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, X-API-Key');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
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;
}
$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);

View File

@@ -0,0 +1,335 @@
<?php
/**
* Nabeh Integration — Submit Complaint with AI Analysis
*
* Called by Nabeh WhatsApp bot. Accepts a complaint from driver or passenger,
* auto-resolves user from phone, fetches full trip context (ride, ratings,
* driver/passenger profiles, behavior data), analyzes via Gemini AI,
* and stores in the complaint table.
*
* Auth: X-API-Key header → NABEH_API_KEY
*
* Input:
* phone (required) — User's phone number (resolve via resolve_user.php)
* ride_id (required) — The trip ID this complaint is about
* complaint_text (req) — Description of the issue
* audio_link (opt) — Voice note link (if user recorded one)
* user_type (opt) — 'driver' or 'passenger' (auto-detected if possible)
*
* Output:
* status, message, complaint_id, passenger_report, driver_report
*/
require_once __DIR__ . '/../core/bootstrap.php';
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: POST, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, X-API-Key');
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
http_response_code(200);
exit;
}
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
http_response_code(405);
echo json_encode(['status' => '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);

View File

@@ -30,11 +30,6 @@ GEMINI_API_KEY=<CHANGE_ME>
# Nabeh Integration (must match Nabeh's .env)
NABEH_API_KEY=<CHANGE_ME_SHARED_SECRET>
# 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=<CHANGE_ME>
allowedWallet1=Tripz-Wallet

View File

@@ -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
]);

View File

@@ -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);

View File

@@ -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
]);

View File

@@ -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);

View File

@@ -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");

View File

@@ -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([

View File

@@ -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([

View File

@@ -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
]);

View File

@@ -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
]);