feat: استيراد كود سيرو إلى تريبز (سيرو @ecfe7568) — بلا تعديل

قرار المالك 2026-07-27: باك إند سيرو PHP هو المعتمد، وتطبيقاته المجرّبة
ميدانياً تحل محل إعادة البناء المؤرشفة. سيرو نفسه لم يُمسّ.

الخريطة:
  backend · payment_server · loction_server · ride_server ·
  passenger_server · docker · dashboard · stress_test  → الجذر
  siro_rider  → apps/rider          siro_driver  → apps/driver
  siro_admin  → dashboards/admin    siro_service → dashboards/service
  android_bot → apps/android_bot    socialBot    → apps/socialBot

نُسخ المتعقَّب في git سيرو فقط عبر `git archive` (3,198 ملفاً / ~169 م.ب)
لا `cp -r` — فاستُثنيت مخلفات البناء تلقائياً. بلا أي تعديل محتوى عمداً:
كل ما يلي يصير فرقاً مقروءاً مقابل المصدر.

لم يُستورد وسببه: siromove.com (الموقع التسويقي يبقى marketing/ في تريبز،
سيرو فيه 8 ملفات) · docs و planning (تريبز له docs/ الخاص) · deploy.sh
(ليس نشراً على سيرفر بل `git add . && git push origin --all` — فخّ في
مستودع آخر) · transit_dashboard (بانتظار قرار مصير backend-transit و
dashboards/transit-web).

⚠️ لا يبني بعد — ثلاثة نواقص متوقعة ومقصودة:
1. `.env` و `lib/env/env.g.dart` غير متعقَّبين في سيرو (أسرار لكل مستأجر):
   كل تطبيق فلاتر يحتاج .env خاصاً ثم توليد env.g.dart بـ build_runner.
2. إعدادات Firebase (9 ملفات google-services.json و GoogleService-Info.plist)
   يستبعدها .gitignore تريبز — ولكل مستأجر مشروع Firebase خاص أصلاً.
3. apps/driver في سيرو يشير إلى `../../Intaleq/packages/get` خارج المستودع →
   يجب ضمّ الحزم داخله أسوة بـ apps/rider.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hamza-Ayed
2026-07-27 05:14:13 +03:00
co-authored by Claude Opus 5
parent 9909d9b4c1
commit 4d8414c96b
3198 changed files with 766859 additions and 0 deletions
+292
View File
@@ -0,0 +1,292 @@
<?php
// ═══════════════════════════════════════════════════════════════
// driver/ride/accept_ride.php
// PURPOSE : قبول رحلة — ride DB هو المرجع، primary DB يتزامن بعده
// RACE : Optimistic lock عبر WHERE status IN ('waiting','wait')
// ═══════════════════════════════════════════════════════════════
include "../../connect.php";
try {
$con_ride = Database::get('ride');
} catch (Exception $e) {
error_log("[accept_ride] Failed to connect to Ride Database: " . $e->getMessage());
printFailure("Database connection failed");
exit;
}
// ── 1. Input & Validation ──────────────────────────────────────
$rideId = filterRequest("id");
// Force driver_id from JWT — never trust user-supplied driver_id
$driverId = $user_id;
$status = filterRequest("status"); // القيمة التي يرسلها التطبيق: 'accepted'
$passengerToken = filterRequest("passengerToken");
$passengerFingerprint = filterRequest("passengerFingerprint");
$passengerIdValue = filterRequest("passenger_id");
if (empty($rideId) || empty($driverId)) {
printFailure("Missing required parameters");
exit;
}
// Self-ride validation
$driverFingerprint = isset($_SERVER['HTTP_X_DEVICE_FP']) ? $_SERVER['HTTP_X_DEVICE_FP'] : '';
if (!empty($driverFingerprint) && $driverFingerprint === $passengerFingerprint) {
error_log("[accept_ride] Self-ride attempt blocked. DriverID=$driverId, Fingerprint=$driverFingerprint");
printFailure("Self-matching is not allowed");
exit;
}
// status whitelist — لا نقبل قيمة عشوائية من التطبيق
$allowedStatuses = ['accepted', 'Apply'];
if (!in_array($status, $allowedStatuses, true)) {
$status = 'accepted'; // fallback آمن
}
error_log("[accept_ride] DriverID=$driverId attempting RideID=$rideId");
try {
// ═══════════════════════════════════════════════════════════
// STEP A — القفل على ride DB (المرجع الأساسي)
// Optimistic lock: نغير فقط إذا status لا يزال 'waiting' أو 'wait'
// السائق الأول الذي يصل يربح — الباقي يجدون rowCount=0
// ═══════════════════════════════════════════════════════════
$stmtLock = $con_ride->prepare("
UPDATE `ride`
SET `status` = ?,
`driver_id` = ?,
`rideTimeStart` = NOW()
WHERE `id` = ?
AND `status` IN ('waiting', 'wait')
");
$stmtLock->execute([$status, $driverId, $rideId]);
if ($stmtLock->rowCount() === 0) {
// الرحلة غير متاحة — سائق آخر سبق أو الرحلة ألغيت
error_log("[accept_ride] RideID=$rideId not available for DriverID=$driverId (rowCount=0)");
printFailure("Ride not available");
exit;
}
error_log("[accept_ride] ride DB locked. RideID=$rideId → DriverID=$driverId");
// ═══════════════════════════════════════════════════════════
// STEP B — تزامن primary DB (بعد نجاح القفل)
// ═══════════════════════════════════════════════════════════
try {
$con->prepare("
UPDATE `ride`
SET `driver_id` = ?,
`status` = ?,
`rideTimeStart` = NOW()
WHERE `id` = ?
")->execute([$driverId, $status, $rideId]);
error_log("[accept_ride] primary DB synced. RideID=$rideId");
} catch (PDOException $eSync) {
// لا نوقف — ride DB هو المرجع
error_log("[accept_ride] primary DB sync WARNING: " . $eSync->getMessage());
}
// ═══════════════════════════════════════════════════════════
// STEP C — driver_orders (INSERT أو UPDATE بسطر واحد آمن)
// ON DUPLICATE KEY يمنع race condition ثانية على هذا الجدول
// ═══════════════════════════════════════════════════════════
try {
$con->prepare("
INSERT INTO `driver_orders` (`driver_id`, `order_id`, `status`, `created_at`)
VALUES (?, ?, ?, NOW())
ON DUPLICATE KEY UPDATE
`driver_id` = VALUES(`driver_id`),
`status` = VALUES(`status`),
`created_at` = NOW()
")->execute([$driverId, $rideId, $status]);
} catch (PDOException $eOrders) {
error_log("[accept_ride] driver_orders WARNING: " . $eOrders->getMessage());
}
// ═══════════════════════════════════════════════════════════
// STEP C.1 — تحديث جدول waitingRides حتى لا تظهر للكباتن الآخرين
// ═══════════════════════════════════════════════════════════
try {
$con->prepare("UPDATE `waitingRides` SET `status` = 'Apply' WHERE `id` = ?")->execute([$rideId]);
} catch (PDOException $eWaiting) {
error_log("[accept_ride] waitingRides WARNING: " . $eWaiting->getMessage());
}
// ═══════════════════════════════════════════════════════════
// STEP D — جلب بيانات السائق للراكب
// ═══════════════════════════════════════════════════════════
$driverInfo = [];
$stmtDriver = $con->prepare("
SELECT
d.id AS driver_id,
d.first_name,
d.last_name,
d.gender,
d.phone,
c.make,
c.model,
c.car_plate,
c.year,
c.color,
c.color_hex,
(SELECT ROUND(AVG(rating), 2) FROM ratingDriver WHERE driver_id = d.id) AS ratingDriver,
(SELECT COUNT(*) FROM ratingDriver WHERE driver_id = d.id) AS ratingCount,
(SELECT COUNT(*) FROM ride WHERE driver_id = d.id AND status IN ('Finished', 'finished')) AS completedRides,
dt.token
FROM driver d
LEFT JOIN CarRegistration c ON c.driverID = d.id
LEFT JOIN driverToken dt ON dt.captain_id = d.id
WHERE d.id = ?
LIMIT 1
");
$stmtDriver->execute([$driverId]);
$driverRaw = $stmtDriver->fetch(PDO::FETCH_ASSOC);
if ($driverRaw) {
$encryptedFields = ['first_name', 'last_name', 'gender', 'phone', 'car_plate', 'token'];
foreach ($driverRaw as $key => $value) {
if (in_array($key, $encryptedFields, true) && !empty($value)) {
$decrypted = false;
try {
$decrypted = $encryptionHelper->decryptData($value);
} catch (\Throwable $e) {
$decrypted = false;
}
$driverInfo[$key] = ($decrypted !== false && $decrypted !== null && $decrypted !== '') ? $decrypted : $value;
} else {
$driverInfo[$key] = $value;
}
}
$driverInfo['driverName'] = trim(($driverInfo['first_name'] ?? '') . ' ' . ($driverInfo['last_name'] ?? ''));
if (empty($driverInfo['driverName']) && !empty($driverInfo['phone'])) {
$driverInfo['driverName'] = $driverInfo['phone'];
}
$driverInfo['ratingDriver'] = !empty($driverInfo['ratingDriver']) ? (string)$driverInfo['ratingDriver'] : "5.0";
$ratingValue = (float) $driverInfo['ratingDriver'];
$ratingCount = (int) ($driverInfo['ratingCount'] ?? 0);
$completedRides = (int) ($driverInfo['completedRides'] ?? 0);
if ($ratingValue >= 4.8 && $ratingCount >= 50 && $completedRides >= 100) {
$driverInfo['driverTier'] = 'Professional driver';
} elseif ($ratingValue >= 4.5 && $ratingCount >= 15 && $completedRides >= 30) {
$driverInfo['driverTier'] = 'Trusted driver';
} else {
$driverInfo['driverTier'] = 'Verified driver';
}
if (isset($redisLocation) && $redisLocation) {
try {
// ‏المفتاح الصحيح driver:public — و driver:location لا يُكتب في أي
// ‏مكان إطلاقاً فكانت القراءة ترجع فارغة دائماً، فيصل الراكب بلا
// ‏إحداثيات أولية للسائق. الكاتب driver_socket.php في معالج
// ‏الدفعات (hmset على driver:profile و driver:public معاً، بحقول
// ‏lat/lng/heading نفسها، وTTL أربعٍ وعشرين ساعة للعام).
$driverLoc = $redisLocation->hGetAll("driver:public:$driverId");
if (!empty($driverLoc)) {
$driverInfo['lat'] = (float)($driverLoc['lat'] ?? 0);
$driverInfo['lng'] = (float)($driverLoc['lng'] ?? 0);
$driverInfo['heading'] = (float)($driverLoc['heading'] ?? 0);
}
} catch (Exception $eLoc) {
// Ignore location fetch error
}
}
}
// ═══════════════════════════════════════════════════════════
// STEP E — جلب passenger_id وإرسال الإشعارات
// ═══════════════════════════════════════════════════════════
if (empty($passengerIdValue)) {
$passengerId = $con->prepare("SELECT passenger_id FROM ride WHERE id = ? LIMIT 1");
$passengerId->execute([$rideId]);
$passengerIdValue = $passengerId->fetchColumn();
}
// 🆕 نحلّ توكن الراكب من القاعدة دائماً ولا نعتمد على ما يرسله التطبيق:
// • مسار الـ dispatch/FCM يرسل التوكن سليماً (نص صريح من add_ride.php).
// • مسار "سوق الرحلات" يرسله كما خرج من getRideWaiting.php أي **مشفّراً**
// (جدول tokens يخزّنه مشفّراً — انظر ride/firebase/addToken.php)، فكان
// FCM يرفضه بـ 400 ولا يصل الراكب أي إشعار قبول.
// القيمة القادمة من العميل تبقى fallback أخيراً فقط.
$passengerTokenFromClient = $passengerToken;
$passengerToken = '';
if ($passengerIdValue) {
try {
$stmtTok = $con->prepare("SELECT token FROM tokens WHERE passengerID = ? LIMIT 1");
$stmtTok->execute([$passengerIdValue]);
$rawPassengerToken = $stmtTok->fetchColumn();
if (!empty($rawPassengerToken)) {
$decryptedToken = false;
try {
$decryptedToken = $encryptionHelper->decryptData($rawPassengerToken);
} catch (\Throwable $eTok) {
$decryptedToken = false;
}
$passengerToken = ($decryptedToken !== false && $decryptedToken !== null && $decryptedToken !== '')
? trim($decryptedToken)
: $rawPassengerToken;
}
} catch (PDOException $eTok) {
error_log("[accept_ride] passenger token lookup WARNING: " . $eTok->getMessage());
}
}
if (empty($passengerToken)) {
$passengerToken = $passengerTokenFromClient;
}
if ($passengerIdValue) {
// Socket — real-time update على خريطة الراكب
if (function_exists('notifyPassengerOnRideServer')) {
notifyPassengerOnRideServer($passengerIdValue, [
'status' => 'accepted',
'ride_id' => $rideId,
'driver_id' => $driverId,
'driver_info' => $driverInfo,
]);
}
// FCM — push notification صامت
if (!empty($passengerToken)) {
sendFCM_Internal(
$passengerToken,
"", // تفريغ العنوان للإرسال الصامت
"", // تفريغ المحتوى للإرسال الصامت
['ride_id' => (string) $rideId, 'driver_info' => $driverInfo, 'status' => 'accepted'],
"Accepted Ride",
false
);
}
}
// ═══════════════════════════════════════════════════════════
// STEP F — تنظيف السوق + Cache ride state (أبلغ location server)
// ═══════════════════════════════════════════════════════════
sendToLocationServer('ride_taken_event', [
'ride_id' => $rideId,
'taken_by_driver_id' => $driverId,
]);
// 🆕 Cache ride state in Redis
sendToLocationServer('update_ride_state', [
'ride_id' => $rideId,
'status' => $status,
'driver_id' => $driverId,
'passenger_id' => $passengerIdValue ?? '',
]);
error_log("[accept_ride] SUCCESS. RideID=$rideId accepted by DriverID=$driverId");
// ═══════════════════════════════════════════════════════════
// STEP G — رد النجاح للسائق (نفس بنية الرد القديمة)
// ═══════════════════════════════════════════════════════════
echo json_encode([
"status" => "success",
"message" => "Ride Accepted",
"data" => $driverInfo,
]);
} catch (PDOException $e) {
error_log("[accept_ride] CRITICAL: " . $e->getMessage());
printFailure("Server error");
}
+447
View File
@@ -0,0 +1,447 @@
<?php
// ═══════════════════════════════════════════════════════════════
// passenger/ride/add_ride.php
// PURPOSE : إنشاء رحلة جديدة — ride DB أولاً، primary DB ثانياً
// ═══════════════════════════════════════════════════════════════
include "../../connect.php";
try {
$con_ride = Database::get('ride');
} catch (Exception $e) {
error_log("[add_ride] Failed to connect to Ride Database: " . $e->getMessage());
printFailure("Database connection failed");
exit;
}
// =================================================================================
// 🛠️ دالة مساعدة: إرسال الرحلة لسوق السائقين (Marketplace Broadcast)
// =================================================================================
function broadcastRideToMarket($rideId, $lat, $lng, $payloadData, $extraMarketData = []) {
$url = getenv('LOCATION_SOCKET_URL') ?: 'http://socket_driver:2021';
if (strpos($url, 'localhost') !== false || strpos($url, '127.0.0.1') !== false) {
if (file_exists('/.dockerenv')) {
$url = str_replace(['localhost', '127.0.0.1'], 'socket_driver', $url);
}
}
$INTERNAL_KEY = function_exists('getInternalSocketKey') ? getInternalSocketKey() : '';
// ⚠️ هذه الحمولة تُبَثّ لكل سائق قريب (وليس للفائز فقط) — لا نضع فيها أي
// بيانات راكب حسّاسة (هاتف/إيميل/FCM token). ما ينقص السائق من تفاصيل
// الراكب يجلبه بعد نجاح القبول، والسيرفر يحلّ التوكن بنفسه في acceptRide.php.
//
// أسماء الحقول تطابق getRideWaiting.php لأن available_rides_page.dart يحشر
// رحلات الـ socket في نفس القائمة ويقرأ منها بنفس المفاتيح — أي حقل ناقص
// يصل للسيرفر لاحقاً كنص "null".
$marketPayload = array_merge([
'id' => (string)$rideId,
'start_lat' => $lat,
'start_lng' => $lng,
'end_lat' => $payloadData[3],
'end_lng' => $payloadData[4],
'start_location' => $lat . ',' . $lng,
'end_location' => $payloadData[3] . ',' . $payloadData[4],
'price' => $payloadData[2],
'carType' => $payloadData[31],
'startName' => $payloadData[29],
'endName' => $payloadData[30],
'distance' => $payloadData[11],
'duration' => $payloadData[15],
'passengerRate' => $payloadData[33],
'passengerId' => $payloadData[7],
], $extraMarketData);
$postData = [
'action' => 'market_new_ride',
'payload' => json_encode($marketPayload)
];
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($postData));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT_MS, 2000);
if ($INTERNAL_KEY) {
curl_setopt($ch, CURLOPT_HTTPHEADER, ["x-internal-key: $INTERNAL_KEY"]);
}
curl_exec($ch);
curl_close($ch);
}
error_log("[add_ride] Request started. passenger_id=" . ($_POST['passenger_id'] ?? '?'));
// ── 1. Input ───────────────────────────────────────────────────
$start_location = filterRequest("start_location");
$end_location = filterRequest("end_location");
$price = filterRequest("price");
$price_token = filterRequest("price_token");
// Force passenger_id from JWT — never trust user-supplied passenger_id
$passenger_id = $user_id;
$driver_id = (string)(filterRequest("driver_id") ?: '0');
$status = filterRequest("status") ?: 'nothing';
$price_for_driver = filterRequest("price_for_driver") ?: ($price ?: '0');
$price_for_passenger = filterRequest("price_for_passenger") ?: ($price ?: '0');
$distance = filterRequest("distance") ?: 0;
$carType = filterRequest("carType");
$passenger_name = filterRequest("passenger_name");
$passenger_phone = filterRequest("passenger_phone");
$passenger_token = filterRequest("passenger_token");
$passenger_email = filterRequest("passenger_email");
$passenger_wallet = filterRequest("passenger_wallet");
$passenger_rating = filterRequest("passenger_rating");
$start_name_loc = filterRequest("start_name");
$end_name_loc = filterRequest("end_name");
$duration_text = filterRequest("duration_text");
$distance_text = filterRequest("distance_text");
$is_wallet = filterRequest("is_wallet");
$has_steps = filterRequest("has_steps");
$step0 = filterRequest("step0");
$step1 = filterRequest("step1");
$step2 = filterRequest("step2");
$step3 = filterRequest("step3");
$step4 = filterRequest("step4");
// Helper to compare coordinates (allowing slight GPS precision drift up to ~500m)
function coordsMatch($coordStr1, $coordStr2, $tolerance = 0.005) {
if (empty($coordStr1) || empty($coordStr2)) return false;
$c1 = array_map('floatval', explode(',', $coordStr1));
$c2 = array_map('floatval', explode(',', $coordStr2));
if (count($c1) < 2 || count($c2) < 2) return false;
return (abs($c1[0] - $c2[0]) < $tolerance) && (abs($c1[1] - $c2[1]) < $tolerance);
}
// Validation
if (empty($passenger_id) || empty($start_location) || empty($end_location) || empty($price)) {
error_log("[add_ride] Validation failed — missing required fields.");
printFailure("Missing required fields");
exit;
}
// SECURE PRICE TOKEN VERIFICATION
if (empty($price_token)) {
error_log("[add_ride] Security failed — price_token is missing.");
printFailure("Secure price token is required");
exit;
}
$decrypted = isset($encryptionHelper) ? $encryptionHelper->decryptData($price_token) : false;
if (!$decrypted) {
error_log("[add_ride] Security failed — failed to decrypt price_token.");
printFailure("Invalid or tampered price token");
exit;
}
$tokenData = json_decode($decrypted, true);
if (!$tokenData || !isset($tokenData['expires']) || $tokenData['expires'] < time()) {
error_log("[add_ride] Security failed — token is expired or invalid JSON.");
printFailure("Price token has expired, please request estimation again");
exit;
}
if ($tokenData['passenger_id'] != $passenger_id) {
error_log("[add_ride] Security failed — passenger_id mismatch.");
printFailure("Tampered price token (passenger mismatch)");
exit;
}
if (!coordsMatch($tokenData['start_location'], $start_location) || !coordsMatch($tokenData['end_location'], $end_location)) {
error_log("[add_ride] Security failed — coordinates mismatch. Token: " . ($tokenData['start_location'] . " / " . $tokenData['end_location']) . " Request: " . ($start_location . " / " . $end_location));
printFailure("Tampered price token (route mismatch)");
exit;
}
// ✅ FIX P6: خريطة أسماء car types بين التطبيق والـ token
// التطبيق يرسل أسماء عرض (Fixed Price, Scooter...) لكن الـ token يخزن أماً داخلية (Speed, Delivery...)
$displayToTokenCarType = [
'Fixed Price' => 'Speed',
'Rayeh Gai' => 'Speed',
'Scooter' => 'Delivery',
'Pink Bike' => 'Delivery',
];
$tokenCarType = isset($displayToTokenCarType[$carType]) ? $displayToTokenCarType[$carType] : $carType;
if (!isset($tokenData['prices'][$tokenCarType])) {
error_log("[add_ride] Security failed — car type $carType (token key: $tokenCarType) not found in token.");
printFailure("Invalid car type for this token");
exit;
}
// ✅ FIX P2: تم حذف التحقق من distance و duration
// السبب: token['distance'] هو الإحداثيات بينما $distance هو المسافة بالكيلومتر (0.x)
// وtoken['duration'] هو الثواني بينما $duration_text هو الدقائق — mismatch دائم يكسر جميع الرحلات
// الإحداثيات كافية للتحقق من سلامة الطلب عبر coordsMatch() أعلاه
// Securely override pricing from the cryptographically signed token
$price = $tokenData['prices'][$tokenCarType]['price'];
$price_for_driver = $tokenData['prices'][$tokenCarType]['driver_price'];
$price_for_passenger = $price;
// 🆕 Destination Matching
$is_destination_match = isset($tokenData['is_destination_match']) ? (int)$tokenData['is_destination_match'] : 0;
$matched_driver_id = isset($tokenData['matched_driver_id']) ? $tokenData['matched_driver_id'] : 0;
// 👑 Siro Prime Status
$is_prime = isset($tokenData['is_prime']) ? (int)$tokenData['is_prime'] : 0;
// ── 2. تنسيق التواريخ ─────────────────────────────────────────
$date_formatted = date("Y-m-d");
$time_formatted = date("H:i:s");
$endtime_formatted = filterRequest("endtime")
? date("H:i:s", strtotime(filterRequest("endtime")))
: "00:00:00";
// ── 3. إحداثيات البداية والنهاية ──────────────────────────────
$startLat = $startLng = $endLat = $endLng = "";
if (!empty($start_location)) {
[$startLat, $startLng] = array_map('trim', explode(',', $start_location, 2));
}
if (!empty($end_location)) {
[$endLat, $endLng] = array_map('trim', explode(',', $end_location, 2));
}
// ── 4. مصفوفة بيانات الإدخال ──────────────────────────────────
$insertData = [
':start_location' => $start_location,
':end_location' => $end_location,
':date' => $date_formatted,
':time' => $time_formatted,
':endtime' => $endtime_formatted,
':price' => $price,
':passenger_id' => $passenger_id,
':driver_id' => $driver_id,
':status' => $status,
':carType' => $carType,
':price_for_driver' => $price_for_driver,
':price_for_passenger' => $price_for_passenger,
':distance' => $distance,
':is_destination_match' => $is_destination_match,
];
$sqlInsert = "INSERT INTO `ride`
(`start_location`,`end_location`,`date`,`time`,`endtime`,
`price`,`passenger_id`,`driver_id`,`status`,`carType`,
`price_for_driver`,`price_for_passenger`,`distance`,`is_destination_match`)
VALUES
(:start_location,:end_location,:date,:time,:endtime,
:price,:passenger_id,:driver_id,:status,:carType,
:price_for_driver,:price_for_passenger,:distance,:is_destination_match)";
try {
// ═══════════════════════════════════════════════════════════
// STEP A — ride DB أولاً (هو المرجع الأساسي)
// ═══════════════════════════════════════════════════════════
$stmtRide = $con_ride->prepare($sqlInsert);
$stmtRide->execute($insertData);
$insertedId = $con_ride->lastInsertId();
if (!$insertedId) {
error_log("[add_ride] ride DB insert returned no ID.");
printFailure("Failed to create ride");
exit;
}
error_log("[add_ride] ride DB insert success. RideID=$insertedId");
// 🆕 Seed initial ride state in Redis — fired as early as possible so
// getRideStatus polling has a cache entry from the first poll onward.
// Uses $status/$driver_id/$passenger_id, the exact values just written
// to MySQL above, not hardcoded literals.
sendToLocationServer('update_ride_state', [
'ride_id' => $insertedId,
'status' => $status,
'driver_id' => $driver_id,
'passenger_id' => $passenger_id,
]);
// ═══════════════════════════════════════════════════════════
// STEP B — primary DB ثانياً (نسخة أرشيفية بنفس الـ ID)
// ═══════════════════════════════════════════════════════════
$sqlInsertWithId = "INSERT INTO `ride`
(`id`,`start_location`,`end_location`,`date`,`time`,`endtime`,
`price`,`passenger_id`,`driver_id`,`status`,`carType`,
`price_for_driver`,`price_for_passenger`,`distance`,`is_destination_match`)
VALUES
(:id,:start_location,:end_location,:date,:time,:endtime,
:price,:passenger_id,:driver_id,:status,:carType,
:price_for_driver,:price_for_passenger,:distance,:is_destination_match)";
try {
$primaryData = $insertData;
$primaryData[':id'] = $insertedId;
$stmtPrimary = $con->prepare($sqlInsertWithId);
$stmtPrimary->execute($primaryData);
error_log("[add_ride] primary DB sync success. RideID=$insertedId");
} catch (PDOException $ePrimary) {
// لا نوقف العملية — ride DB هو المرجع
error_log("[add_ride] primary DB sync WARNING: " . $ePrimary->getMessage());
}
// ═══════════════════════════════════════════════════════════
// STEP C — بناء الـ payload وإرسال الرحلة للسائقين
// ═══════════════════════════════════════════════════════════
$kazan = (float) $price - (float) $price_for_driver;
$passengerFp = isset($_SERVER['HTTP_X_DEVICE_FP']) ? $_SERVER['HTTP_X_DEVICE_FP'] : '';
// ── 🆕 "أرباحك أعلى" — مقارنة أجرة السائق عندنا مع أقرب منافس ─────
require_once __DIR__ . '/../pricing/pricing_helper.php';
$extraDispatchData = [];
try {
$rideCountryCode = 'JO';
$stmtCountry = $con->prepare("
SELECT country_code FROM passenger_opening_locations
WHERE passenger_id = :pid
ORDER BY created_at DESC LIMIT 1
");
$stmtCountry->execute([':pid' => $passenger_id]);
$foundCountry = $stmtCountry->fetchColumn();
if ($foundCountry) $rideCountryCode = strtoupper($foundCountry);
$durationMinForEarnings = is_numeric($duration_text) ? (float)$duration_text : 0.0;
$earnings = estimateDriverEarningsAdvantage(
$rideCountryCode,
(float)$distance,
$durationMinForEarnings,
(float)$price_for_driver,
$redis ?? null
);
if ($earnings) {
$extraDispatchData['driver_earnings_extra'] = (string)$earnings['extra'];
$extraDispatchData['driver_earnings_currency'] = $earnings['currency'];
}
} catch (Exception $e) {
error_log("[add_ride] Driver earnings estimate failed: " . $e->getMessage());
}
$payload = [
(string) $startLat,
(string) $startLng,
number_format((float) $price, 2, '.', ''),
(string) $endLat,
(string) $endLng,
(string) $distance_text,
(string) $passengerFp,
(string) $passenger_id,
(string) $passenger_name,
(string) $passenger_token,
(string) $passenger_phone,
(string) $distance,
"1",
(string) $is_wallet,
(string) $distance,
(string) $duration_text,
(string) $insertedId,
"",
"",
(string) $duration_text,
$has_steps ?: 'false',
(string) $step0,
(string) $step1,
(string) $step2,
(string) $step3,
(string) $step4,
number_format((float) $price_for_driver, 2, '.', ''),
(string) $passenger_wallet,
(string) $passenger_email,
(string) $start_name_loc,
(string) $end_name_loc,
(string) $carType,
number_format($kazan, 2, '.', ''),
(string) $passenger_rating,
(string) $is_prime, // 👑 Index 34: Prime Status
// 🆕 Index 35/36: "أرباحك أعلى" — تُقرأ من نافذة الـ Overlay (order_over_lay.dart)
// اللي بتوصلها البيانات كـ List مش كـ Map مسمّى مثل FCM
isset($extraDispatchData['driver_earnings_extra']) ? $extraDispatchData['driver_earnings_extra'] : '',
isset($extraDispatchData['driver_earnings_currency']) ? $extraDispatchData['driver_earnings_currency'] : '',
];
// Direct dispatch للسائقين القريبين
$driversData = findBestDrivers($con, $startLat, $startLng, $carType, $endLat, $endLng);
// ═══════════════════════════════════════════════════════════════
// 🆕 Destination Matching Priority — Multi-Driver Support
// نبحث عن كل السائقين الذين وجهتهم تطابق منطقة الإنزال
// (ليس فقط الأول، بل جميعهم) ونضعهم في مقدمة القائمة
// الخصم 14% تم تطبيقه مسبقاً في التسعير — لا خصم إضافي هنا
// ═══════════════════════════════════════════════════════════════
if ($is_destination_match && isset($redis)) {
try {
// Query ALL drivers whose destination is near the drop-off (up to 10)
$allMatchedIds = $redis->geoRadius(
'geo:driver:destinations',
(float)$destLng,
(float)$destLat,
3.5,
'km',
['COUNT' => 10, 'ASC']
);
if (!empty($allMatchedIds)) {
// Build a lookup set for fast de-duplication
$alreadyInList = [];
foreach ($driversData as $d) {
$alreadyInList[$d['captain_id'] ?? $d['driver_id'] ?? ''] = true;
}
$matchedToFront = [];
foreach ($allMatchedIds as $mid) {
$mid = (string)$mid;
// Validate still active today
$detailJson = $redis->get("driver:destination:{$mid}");
$detail = $detailJson ? json_decode($detailJson, true) : null;
if (!$detail || empty($detail['is_active']) || ($detail['usage_date'] ?? '') !== date('Y-m-d')) {
$redis->zRem('geo:driver:destinations', $mid);
continue;
}
// Mark with destination flag for special FCM title "في طريقك 📍"
if (isset($alreadyInList[$mid])) {
// Already in list — mark it and move to front
foreach ($driversData as $k => $d) {
$did = $d['captain_id'] ?? $d['driver_id'] ?? '';
if ((string)$did === $mid) {
$driversData[$k]['is_destination_match'] = 1;
$matchedToFront[] = $driversData[$k];
unset($driversData[$k]);
break;
}
}
} else {
// Not nearby but their route matches — fetch token and add
$stmtD = $con->prepare("SELECT token FROM driverToken WHERE captain_id = :d LIMIT 1");
$stmtD->execute([':d' => $mid]);
$dT = $stmtD->fetchColumn();
if ($dT) {
$matchedToFront[] = [
'captain_id' => $mid,
'driver_id' => $mid,
'token' => $dT,
'is_destination_match' => 1
];
}
}
}
// Place all destination-matched drivers at the front
$driversData = array_values($driversData);
$driversData = array_merge($matchedToFront, $driversData);
error_log("[add_ride] " . count($matchedToFront) . " destination-matched driver(s) moved to front.");
}
} catch (Exception $e) {
error_log("[add_ride] Destination priority error: " . $e->getMessage());
}
}
if (!empty($driversData)) {
dispatchRideToDrivers($driversData, $insertedId, $payload, $start_name_loc, $encryptionHelper, $extraDispatchData);
error_log("[add_ride] Dispatched RideID=$insertedId to " . count($driversData) . " drivers.");
} else {
error_log("[add_ride] No direct drivers found for RideID=$insertedId — market only.");
}
// Broadcast للـ marketplace دائماً
broadcastRideToMarket($insertedId, $startLat, $startLng, $payload, $extraDispatchData);
// رد النجاح للتطبيق
printSuccess($insertedId);
} catch (PDOException $e) {
error_log("[add_ride] CRITICAL ride DB error: " . $e->getMessage());
printFailure("Database error: " . $e->getMessage());
}
+84
View File
@@ -0,0 +1,84 @@
<?php
// arrive_ride.php
require_once __DIR__ . '/../../connect.php';
try {
$con_ride = Database::get('ride');
} catch (Exception $e) {
error_log("[arrive_ride] Failed to connect to Ride Database: " . $e->getMessage());
}
$rideId = filterRequest("ride_id");
$driverId = filterRequest("driver_id");
$passengerToken = filterRequest("passengerToken");
if (!$rideId || !$driverId) {
jsonError("Missing required parameters.");
exit;
}
try {
// 1. تحديث الحالة في السيرفر البعيد (Remote DB - con_ride)
$stmtRemote = $con_ride->prepare("UPDATE ride SET status = 'arrived', updated_at = NOW() WHERE id = ? AND driver_id = ? AND status = 'Apply'");
$stmtRemote->execute([$rideId, $driverId]);
// 2. تحديث الحالة في السيرفر المحلي (Local DB - con)
if (isset($con)) {
$stmtLocal = $con->prepare("UPDATE ride SET status = 'arrived', updated_at = NOW() WHERE id = ? AND driver_id = ? AND status = 'Apply'");
$stmtLocal->execute([$rideId, $driverId]);
}
// 3. جلب بيانات الراكب للإرسال
// نستخدم con_ride لضمان الدقة
$stmtPas = $con_ride->prepare("SELECT passenger_id FROM ride WHERE id = ?");
$stmtPas->execute([$rideId]);
$passenger_id = $stmtPas->fetchColumn();
if ($passenger_id) {
// أ) إرسال Socket (الأسرع)
$payload = [
'status' => 'arrived',
'ride_id' => $rideId,
'msg' => 'السائق وصل إلى موقعك 🚖'
];
if (function_exists('notifyPassengerOnRideServer')) {
notifyPassengerOnRideServer($passenger_id, $payload);
}
// ب) إرسال FCM (باستخدام الدالة الجديدة)
if (!empty($passengerToken)) {
$fcmData = [
'category' => 'Arrive Ride', // نفس الاسم القديم لضمان عمل التطبيق
'ride_id' => (string)$rideId,
'status' => 'arrived'
];
// 🔥 استخدام sendFCM_Internal كرسالة صامتة
sendFCM_Internal(
$passengerToken, // الهدف
"", // تفريغ العنوان
"", // تفريغ النص
$fcmData, // البيانات
"Arrive Ride", // التصنيف
false // ليس Topic
);
}
}
// 🆕 Cache ride state in Redis
sendToLocationServer('update_ride_state', [
'ride_id' => $rideId,
'status' => 'arrived',
'driver_id' => $driverId,
'passenger_id' => $passenger_id ?? '',
]);
jsonSuccess(null, "Arrival notified successfully");
} catch (Exception $e) {
error_log("[arrive_ride] " . $e->getMessage());
jsonError("Error notifying arrival");
}
?>
@@ -0,0 +1,238 @@
<?php
// cancel_ride_by_driver.php
require_once __DIR__ . '/../../connect.php';
try {
$con_ride = Database::get('ride');
} catch (Exception $e) {
error_log("[cancel_ride_by_driver] Failed to connect to Ride Database: " . $e->getMessage());
}
require_once __DIR__ . '/streak_helper.php';
$rideId = filterRequest("ride_id");
$driverId = filterRequest("driver_id");
$reason = filterRequest("reason");
$passengerToken = filterRequest("passenger_token");
$penaltyFee = (float) filterRequest("penalty_fee");
// تثبيت الحالة
$statusText = "CancelFromDriverAfterApply";
if (!$rideId || !$driverId) {
jsonError("Missing parameters");
exit;
}
try {
$con->beginTransaction();
// ---------------------------------------------------------
// 1. معالجة driver_orders (Insert or Update)
// ---------------------------------------------------------
$checkStmt = $con->prepare("SELECT order_id FROM driver_orders WHERE order_id = ? AND driver_id = ?");
$checkStmt->execute([$rideId, $driverId]);
if ($checkStmt->rowCount() > 0) {
// موجود: تحديث
$stmtLog = $con->prepare("UPDATE driver_orders SET status = ?, notes = ?, created_at = NOW() WHERE order_id = ? AND driver_id = ?");
$stmtLog->execute([$statusText, $reason, $rideId, $driverId]);
} else {
// غير موجود: إدخال
$stmtLog = $con->prepare("INSERT INTO driver_orders (driver_id, order_id, status, created_at, notes) VALUES (?, ?, ?, NOW(), ?)");
$stmtLog->execute([$driverId, $rideId, $statusText, $reason]);
}
// ---------------------------------------------------------
// 2. منطق الحظر (Business Logic)
// ---------------------------------------------------------
$stmtCount = $con->prepare("
SELECT COUNT(*) FROM driver_orders
WHERE driver_id = ?
AND status = ?
AND created_at >= NOW() - INTERVAL 1 DAY
");
$stmtCount->execute([$driverId, $statusText]);
$cancelCount = $stmtCount->fetchColumn();
$isBlocked = false;
$blockUntil = "";
if ($cancelCount >= 3) {
$isBlocked = true;
$blockUntil = date('Y-m-d H:i:s', strtotime('+4 hours'));
// يمكنك هنا تحديث حالة السائق في جدول driver إذا لزم الأمر
}
// ---------------------------------------------------------
// 3. تحديث حالة الرحلة في جدول ride
// ---------------------------------------------------------
$sqlRide = "UPDATE ride SET status = ?, driver_id = 0 WHERE id = ?";
// Local DB
$con->prepare($sqlRide)->execute([$statusText, $rideId]);
// Remote DB (إن وجد)
if (isset($con_ride)) {
$con_ride->prepare($sqlRide)->execute([$statusText, $rideId]);
}
// 🆕 تصفير التتابع لأن السائق ألغى الرحلة
handleDriverStreak($con, $driverId, 'reset');
// ---------------------------------------------------------
// 4. إشعار الراكب
// ---------------------------------------------------------
// أ) Socket (يحتاج Passenger ID)
$stmtPas = $con->prepare("SELECT passenger_id FROM ride WHERE id = ?");
$stmtPas->execute([$rideId]);
$passenger_id = $stmtPas->fetchColumn();
if ($passenger_id) {
$socketPayload = [
'ride_id' => $rideId,
'status' => 'cancelled_by_driver',
'msg' => 'تم إلغاء الرحلة من قبل السائق'
];
if (function_exists('notifyPassengerOnRideServer')) {
notifyPassengerOnRideServer($passenger_id, $socketPayload);
}
// 4.1. إضافة غرامة الإلغاء على الراكب (الدين) إذا كانت موجودة
if ($penaltyFee > 0) {
// إضافة القيمة كدين سالب في المحفظة
$negativeDebt = -$penaltyFee;
// Resolve country and wallet server
$stmtKazan = $con->prepare("SELECT country FROM kazan LIMIT 1");
$stmtKazan->execute();
$kazan = $stmtKazan->fetch(PDO::FETCH_ASSOC) ?: ["country" => "Jordan"];
$country = $kazan['country'] ?? 'Jordan';
$walletServer = "https://walletintaleq.intaleq.xyz";
if (strtolower($country) == 'jordan') {
$walletServer = getenv('WALLET_SERVER_JORDAN') ?: "https://walletintaleq.intaleq.xyz";
} elseif (strtolower($country) == 'egypt') {
$walletServer = getenv('WALLET_SERVER_EGYPT') ?: "https://walletintaleq.intaleq.xyz";
} else {
$walletServer = getenv('WALLET_SERVER_SYRIA') ?: "https://walletintaleq.intaleq.xyz";
}
// S2S call to add debt to passenger wallet on the payment server
$walletUrl = "$walletServer/v2/main/ride/passengerWallet/add_s2s_debt.php";
$ch = curl_init($walletUrl);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query([
"passengerID" => $passenger_id,
"amount" => $negativeDebt
]),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 5,
CURLOPT_HTTPHEADER => [
'Content-Type: application/x-www-form-urlencoded',
'X-S2S-Api-Key: ' . getenv('S2S_SHARED_KEY')
]
]);
$s2sRes = curl_exec($ch);
$s2sCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($s2sCode !== 200) {
error_log("Failed to add passenger debt via S2S: Code $s2sCode, Res: $s2sRes");
}
// تخزين الدين في الـ Redis لمدة 6 شهور (15552000 ثانية)
try {
$redisInstance = null;
if (isset($redis) && $redis !== null) {
$redisInstance = $redis;
} else if (extension_loaded('redis')) {
$localRedis = new Redis();
$redisHost = getenv('REDIS_MAIN_HOST') ?: getenv('REDIS_HOST') ?: '127.0.0.1';
$redisPort = (int)(getenv('REDIS_MAIN_PORT') ?: getenv('REDIS_PORT') ?: 6379);
$redisPass = getenv('REDIS_MAIN_PASSWORD') ?: getenv('REDIS_MAIN_AUTH') ?: getenv('REDIS_PASSWORD') ?: getenv('REDIS_AUTH');
if ($localRedis->connect($redisHost, $redisPort, 1.5)) {
if ($redisPass) $localRedis->auth($redisPass);
$localRedis->setOption(Redis::OPT_PREFIX, 'siro:');
$redisInstance = $localRedis;
}
}
if ($redisInstance !== null) {
$redisKey = "passenger_debt_" . $passenger_id;
// إضافة الدين الجديد إلى الدين السابق إن وجد
$currentDebt = (float) $redisInstance->get($redisKey);
$newDebt = $currentDebt + $negativeDebt;
$redisInstance->setex($redisKey, 15552000, $newDebt);
}
} catch (Exception $e) {
error_log("Redis Error in cancel_ride_by_driver: " . $e->getMessage());
}
}
}
// ب) FCM (Internal)
if (empty($passengerToken) && $passenger_id) {
$stmtToken = $con->prepare("SELECT token FROM tokens WHERE passengerID = ? ORDER BY id DESC LIMIT 1");
$stmtToken->execute([$passenger_id]);
$rawToken = $stmtToken->fetchColumn();
if ($rawToken) {
$passengerToken = $rawToken;
if (!empty($encryptionHelper)) {
try {
$decrypted = $encryptionHelper->decryptData($rawToken);
if ($decrypted !== false && !empty($decrypted)) {
$passengerToken = trim($decrypted);
}
} catch (Exception $e) {
// Fallback
}
}
}
}
if (!empty($passengerToken)) {
$fcmData = [
'category' => 'Cancel Trip from driver',
'ride_id' => (string)$rideId
];
// 🔥 استخدام الدالة الجديدة
sendFCM_Internal(
$passengerToken, // الهدف
"تم إلغاء الرحلة ❌", // العنوان
"عذراً، قام السائق بإلغاء الرحلة.", // النص
$fcmData, // البيانات
"Cancel Trip from driver", // التصنيف (تأكد أنه يطابق ما في تطبيق الراكب)
false // ليس Topic
);
}
$con->commit();
// 🆕 Cache ride state in Redis — use $statusText (the exact value just
// written to MySQL above), not a hardcoded label, so a cache hit can
// never disagree with the row it was seeded from.
sendToLocationServer('update_ride_state', [
'ride_id' => $rideId,
'status' => $statusText,
'driver_id' => $driverId,
'passenger_id' => $passenger_id ?? '',
]);
// 5. الرد للفلاتر
echo json_encode([
"status" => "success",
"cancel_count" => $cancelCount,
"is_blocked" => $isBlocked,
"block_until" => $blockUntil
]);
} catch (PDOException $e) {
if ($con->inTransaction()) $con->rollBack();
error_log("[cancel_ride_by_driver] " . $e->getMessage());
jsonError("DB Error");
}
?>
@@ -0,0 +1,165 @@
<?php
require_once __DIR__ . '/../../connect.php';
try {
$con_ride = Database::get('ride');
} catch (Exception $e) {
error_log("[cancel_ride_by_passenger] Failed to connect to Ride Database: " . $e->getMessage());
}
// cancel_ride_by_passenger.php
$rideId = filterRequest("ride_id");
$reason = filterRequest("reason");
if (!$rideId) {
jsonError("Missing Ride ID");
exit;
}
try {
// جلب بيانات الرحلة للتحقق (مع passenger_id للإدراج في canecl)
$stmt = $con->prepare("SELECT driver_id, passenger_id, status FROM ride WHERE id = ?");
$stmt->execute([$rideId]);
$ride = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$ride) {
jsonError("Ride not found");
exit;
}
$driverId = $ride['driver_id'];
$currentStatus = $ride['status'];
if ($currentStatus == 'Begin') {
jsonError("Cannot cancel started ride");
exit;
}
// =================================================================
// 1. تحديث قواعد البيانات (Transaction)
// =================================================================
$con->beginTransaction();
// تحديث waitingRides
$updateWaiting = $con->prepare("UPDATE waitingRides SET status = ? WHERE id = ?");
$updateWaiting->execute(['cancelled_by_passenger', $rideId]);
// تحديث ride (محلي)
$updateRide = $con->prepare("UPDATE ride SET status = ?, updated_at = NOW() WHERE id = ?");
$updateRide->execute(['cancelled_by_passenger', $rideId]);
// تحديث driver_orders
if ($driverId > 0) {
$updateOrder = $con->prepare("UPDATE driver_orders SET status = 'cancelled_by_passenger', notes = ? WHERE order_id = ? AND driver_id = ?");
$updateOrder->execute([$reason, $rideId, $driverId]);
}
// إدراج سبب الإلغاء في جدول canecl المخصص
if ($driverId > 0 && !empty($reason)) {
$passengerId = $ride['passenger_id'] ?? '0';
$insertCanecl = $con->prepare(
"INSERT INTO canecl (driverID, passengerID, rideID, note) VALUES (?, ?, ?, ?)"
);
$insertCanecl->execute([$driverId, $passengerId, $rideId, $reason]);
}
$con->commit();
// تحديث السيرفر البعيد (Remote DB)
if (isset($con_ride)) {
try {
$updateRide2 = $con_ride->prepare("UPDATE ride SET status = ?, updated_at = NOW() WHERE id = ?");
$updateRide2->execute(['cancelled_by_passenger', $rideId]);
} catch (PDOException $e) {
error_log("Secondary DB update failed: " . $e->getMessage());
}
}
// =================================================================
// 2. إشعار السائق/السائقين (Socket + FCM)
// =================================================================
// 🔥 يُرسل دائماً بغض النظر عن driver_id — إذا كانت الرحلة لم تُقبل بعد
// (driver_id = 0)، لوكيشن سيرفر يستخدم ride:offered_drivers:{rideId}
// من Redis لإشعار كل السائقين الذين وصلهم عرض هذه الرحلة أصلاً.
$socketUrl = getenv('LOCATION_SERVER_URL') ?: 'http://socket_driver:2021';
$internalKeyPath = getenv('INTERNAL_SOCKET_KEY_PATH') ?: '';
$internalKey = ($internalKeyPath && file_exists($internalKeyPath)) ? trim(file_get_contents($internalKeyPath)) : (getenv('INTERNAL_SOCKET_KEY') ?: '');
$ch = curl_init($socketUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
'action' => 'cancel_ride',
'driver_id' => $driverId,
'ride_id' => $rideId,
'reason' => $reason
]));
if (!empty($internalKey)) curl_setopt($ch, CURLOPT_HTTPHEADER, ["x-internal-key: $internalKey"]);
curl_setopt($ch, CURLOPT_TIMEOUT_MS, 500);
curl_setopt($ch, CURLOPT_NOSIGNAL, 1);
@curl_exec($ch);
curl_close($ch);
if ($driverId > 0) {
// FCM للسائق الذي قَبِل الرحلة فعلاً (باستخدام الدالة الجديدة مع فك التشفير)
$driverToken = filterRequest("driver_token");
if (empty($driverToken)) {
$stmtToken = $con->prepare("SELECT token FROM driverToken WHERE captain_id = ?");
$stmtToken->execute([$driverId]);
$rawToken = $stmtToken->fetchColumn();
if ($rawToken) {
$driverToken = $rawToken;
// 🔥 محاولة فك التشفير (لأن التوكنات غالباً مشفرة)
if (!empty($encryptionHelper)) {
try {
$decrypted = $encryptionHelper->decryptData($rawToken);
if ($decrypted !== false && !empty($decrypted)) {
$driverToken = trim($decrypted);
}
} catch (Exception $e) {
// في حال الفشل نستخدم الخام
}
}
}
}
if (!empty($driverToken)) {
// تجهيز البيانات
$fcmData = [
'category' => 'Cancel Trip',
'ride_id' => (string)$rideId,
'reason' => $reason
];
// إرسال الإشعار
sendFCM_Internal(
$driverToken, // الهدف
"إلغاء الرحلة 🚫", // العنوان
"قام الراكب بإلغاء الرحلة: $reason", // النص
$fcmData, // البيانات
'Cancel Trip', // التصنيف
false // ليس Topic
);
}
}
// 🆕 Cache ride state in Redis
sendToLocationServer('update_ride_state', [
'ride_id' => $rideId,
'status' => 'cancelled_by_passenger',
'driver_id' => $driverId ?? '',
'passenger_id' => $ride['passenger_id'] ?? '',
]);
jsonSuccess(null, "Ride cancelled successfully");
} catch (PDOException $e) {
if ($con->inTransaction()) $con->rollBack();
error_log("Cancel ride error: " . $e->getMessage());
jsonError("Database error occurred");
}
?>
+96
View File
@@ -0,0 +1,96 @@
<?php
// cron_ride_timeout.php
// 🔥 أُعيدت كتابته بالكامل: كان يعتمد على جدول waitingRides الذي لم يعد
// يُملأ من تدفق add_ride.php الحقيقي (addWaitingRide.php لا يُستدعى من هناك)،
// فكانت الرحلات العالقة بحالة 'waiting' لا تُصفَّر أبداً. الآن يعمل مباشرة
// على جدول ride باستخدام أعمدة date/time المكتوبة فعلياً عند الإنشاء.
require_once __DIR__ . '/../../core/bootstrap.php';
require_once __DIR__ . '/../../functions.php';
error_log("⏰ [Cleanup Cron] Started cleaning old waiting rides...");
$minutesLimit = 15;
try {
$con = Database::get('main');
// =========================================================
// الخطوة 1: جلب الرحلات العالقة (status='waiting' منذ أكثر من 15 دقيقة)
// =========================================================
$sqlSelect = "SELECT id, passenger_id FROM ride
WHERE status = 'waiting'
AND TIMESTAMP(`date`, `time`) < DATE_SUB(NOW(), INTERVAL $minutesLimit MINUTE)";
$stmtSelect = $con->prepare($sqlSelect);
$stmtSelect->execute();
$staleRides = $stmtSelect->fetchAll(PDO::FETCH_ASSOC);
$updatedCount = 0;
if (!empty($staleRides)) {
$sqlUpdate = "UPDATE ride SET status = 'timeout', updated_at = NOW() WHERE id = ? AND status = 'waiting'";
$stmtUpdate = $con->prepare($sqlUpdate);
// نسخة أرشيفية (best-effort)
$con_ride = null;
try {
$con_ride = Database::get('ride');
} catch (Exception $e) {
error_log("[cron_ride_timeout] Secondary ride DB unavailable: " . $e->getMessage());
}
$stmtUpdate2 = $con_ride ? $con_ride->prepare(
"UPDATE ride SET status = 'timeout', updated_at = NOW() WHERE id = ? AND status = 'waiting'"
) : null;
foreach ($staleRides as $ride) {
$rideId = $ride['id'];
$stmtUpdate->execute([$rideId]);
$updatedCount += $stmtUpdate->rowCount();
if ($stmtUpdate2) {
try {
$stmtUpdate2->execute([$rideId]);
} catch (PDOException $e) {
error_log("[cron_ride_timeout] Secondary DB update failed for #$rideId: " . $e->getMessage());
}
}
// 🆕 تنظيف Redis + إعلام أي سائقين ما زالوا يرون هذا الطلب على أنه ملغى
sendToLocationServer('cancel_ride', [
'ride_id' => $rideId,
'driver_id' => 0,
'reason' => 'timeout',
]);
}
}
// =========================================================
// الخطوة 2: تنظيف جدول waitingRides القديم (إن وُجدت صفوف فيه من مسار قديم)
// =========================================================
$deletedCount = 0;
try {
$sqlDelete = "DELETE FROM waitingRides WHERE created_at < DATE_SUB(NOW(), INTERVAL $minutesLimit MINUTE)";
$stmtDelete = $con->prepare($sqlDelete);
$stmtDelete->execute();
$deletedCount = $stmtDelete->rowCount();
} catch (PDOException $e) {
error_log("[cron_ride_timeout] waitingRides cleanup skipped: " . $e->getMessage());
}
if ($updatedCount > 0 || $deletedCount > 0) {
$msg = "✅ [Cleanup Cron] Success: Timed out $updatedCount ride(s) in ride table, and cleaned $deletedCount legacy waitingRides row(s).";
error_log($msg);
echo json_encode(["status" => "success", "message" => $msg]);
} else {
$msg = "💤 [Cleanup Cron] No expired rides found.";
error_log($msg);
echo json_encode(["status" => "success", "message" => "Nothing to clean."]);
}
} catch (PDOException $e) {
$errorMsg = "❌ [Cleanup Cron] Error: " . $e->getMessage();
error_log($errorMsg);
echo json_encode(["status" => "failure", "message" => "An internal error occurred."]);
}
?>
+19
View File
@@ -0,0 +1,19 @@
<?php
require_once __DIR__ . '/../../connect.php';
// استلام قيمة ID بعد التصفية
$id = filterRequest("id");
// استخدام استعلام محضّر لتفادي SQL Injection
$sql = "DELETE FROM `ride` WHERE `id` = :id";
$stmt = $con->prepare($sql);
$stmt->bindParam(':id', $id, PDO::PARAM_INT);
$stmt->execute();
// التحقق من نجاح العملية
if ($stmt->rowCount() > 0) {
jsonSuccess(null, "Ride deleted successfully");
} else {
jsonError("Failed to delete ride");
}
?>
@@ -0,0 +1,89 @@
<?php
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\Exception;
// الاتصال بقاعدة البيانات والدوال المشتركة
require_once __DIR__ . '/../../connect.php';
// تضمين autoload من Composer (مجلد vendor في الجذر)
require_once __DIR__ . '/../../../vendor/autoload.php';
// استقبال البيانات من الطلب
$passengerName = filterRequest("name");
$passengerEmail = filterRequest("email");
$passengerPhone = filterRequest("phone");
$fee = floatval(filterRequest("fee"));
$startLocation = filterRequest("startLocation");
$endLocation = filterRequest("endLocation");
$startNameLocation = filterRequest("startNameLocation");
$endNameLocation = filterRequest("endNameLocation");
$timeOfTrip = filterRequest("timeOfTrip");
$duration = intval(filterRequest("duration"));
$duration = floor($duration / 60); // تحويل للـ minutes
// حساب الخصم
$discount = 0;
if ($startNameLocation && $endNameLocation) {
$discount = $fee * 0.12;
$beforDiscount = $fee;
$fee -= $discount;
}
// بناء الإيميل
$bodyEmail = "
<html>
<head>
<style>
body { font-family: 'Arial', sans-serif; background-color: #f8f9fa; color: #333; line-height: 1.6; }
table { border-collapse: collapse; width: 100%; background-color: white; margin: 20px 0; box-shadow: 0 4px 6px rgba(0,0,0,0.1); }
th, td { padding: 12px; text-align: left; border-bottom: 1px solid #ddd; }
th { background-color: #007bff; color: white; }
tr:nth-child(even) { background-color: #f2f2f2; }
</style>
</head>
<body>
<img src='https://siromove.com/assets/logo.png' alt='Siro Logo' style='width: 150px; margin: 20px auto; display: block;'>
<p>Hi $passengerName,</p>
<p>Thank you for booking your ride with <strong>Tripz</strong>. Here are the details of your recent trip:</p>
<table>
<tr><th>Detail</th><th>Value</th></tr>
<tr><td>Passenger</td><td>$passengerName</td></tr>
<tr><td>Email</td><td>$passengerEmail</td></tr>
<tr><td>Phone</td><td>$passengerPhone</td></tr>
<tr><td>Fee</td><td>$$fee</td></tr>
<tr><td>Start Location</td><td>$startLocation ($startNameLocation)</td></tr>
<tr><td>End Location</td><td>$endLocation ($endNameLocation)</td></tr>
<tr><td>Time of Trip</td><td>$timeOfTrip</td></tr>
<tr><td>Duration</td><td>$duration minutes</td></tr>
</table>";
if ($discount > 0) {
$bodyEmail .= "<p>You have received a 12% discount on your trip from $startNameLocation to $endNameLocation. The original fee was $$beforDiscount. Your discounted fee is $$fee.</p>";
}
$bodyEmail .= "<p>Thank you for using <strong>Tripz</strong>. We hope you have a great day!</p><p>Best regards,<br>Tripz Team</p></body></html>";
// إعداد البريد
$mail = new PHPMailer(true);
try {
$mail->isSMTP();
$mail->Host = 'smtp.hostinger.com';
$mail->SMTPAuth = true;
$mail->Username = getenv('MAIL_USERNAME') ?: 'noreply@siromove.com';
$mail->Password = getenv('MAIL_PASSWORD') ?: '';
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
$mail->Port = 587;
$mail->setFrom(getenv('MAIL_FROM_ADDRESS') ?: 'noreply@siromove.com', getenv('MAIL_FROM_NAME') ?: 'Siro');
$mail->addAddress($passengerEmail, $passengerName);
$mail->isHTML(true);
$mail->Subject = 'Your Tripz Trip Details';
$mail->Body = $bodyEmail;
$mail->send();
echo json_encode(["status" => "success", "message" => "Email sent successfully"]);
} catch (Exception $e) {
echo json_encode(["status" => "error", "message" => $mail->ErrorInfo]);
}
+554
View File
@@ -0,0 +1,554 @@
<?php
require_once __DIR__ . '/../../connect.php';
try {
$con_ride = Database::get('ride');
} catch (Exception $e) {
error_log("[finish_ride_updates] Failed to connect to Ride Database: " . $e->getMessage());
}
require_once __DIR__ . '/streak_helper.php';
// ============================================================
// finish_ride_updates.php — Atomic Server-to-Server
// ============================================================
// Driver App calls this ONCE with raw ride data (NOT the price).
// Server calculates price securely, processes payment via S2S,
// and atomically updates all databases within a transaction.
//
// Flow:
// 1. Receive raw params from driver app (including country_code)
// 2. Load country pricing from `kazan` table
// 3. Calculate price server-side (from DB + actual distance)
// 4. BEGIN TRANSACTION (local DB)
// 5. Update ride on local DB + remote DB (con_ride)
// 6. Update driver_orders
// 7. S2S cURL → Wallet Payment Server (process_ride_payments.php)
// 8. If payment OK → COMMIT, notify passenger (Socket + FCM)
// 9. If payment FAIL → ROLLBACK, ride stays 'Begin', safe retry
// ============================================================
// --- Secure S2S Configuration ---
define('S2S_SHARED_KEY', getenv('S2S_SHARED_KEY') );
define('WALLET_PAYMENT_URL', getenv('WALLET_PAYMENT_URL') ?: 'http://nginx/v2/main/ride/payment/process_ride_payments.php');
// ============================================================
// 1. Receive Raw Parameters (NO price from client)
// ============================================================
$rideId = filterRequest("rideId");
// Force driver_id from JWT — never trust user-supplied driver_id
$driver_id = $user_id;
$passengerId = filterRequest("passengerId");
$newStatus = filterRequest("status"); // Expected: "Finished"
$actualDistance = filterRequest("actualDistance");
$actualDuration = filterRequest("actualDuration");
$passengerToken = filterRequest("passengerToken");
$driver_token = filterRequest("driver_token");
$walletChecked = filterRequest("walletChecked");
$passengerWalletBurc = filterRequest("passengerWalletBurc");
$countryCode = filterRequest("country_code"); // 🆕 الدولة: Syria, Egypt, ...
if (empty($rideId) || empty($newStatus) || empty($driver_id) || empty($passengerId)) {
jsonError("Missing required parameters: rideId, driver_id, passengerId, status");
exit;
}
if ($newStatus !== 'Finished') {
jsonError("Invalid status. Expected: Finished");
exit;
}
// 🆕 إذا لم يتم إرسال country_code، نأخذه من قاعدة بيانات الرحلة
if (empty($countryCode)) {
try {
$stmtCountry = $con->prepare("SELECT r.id, d.site AS country_code
FROM ride r
LEFT JOIN driver d ON r.driver_id = d.id
WHERE r.id = ? LIMIT 1");
$stmtCountry->execute([$rideId]);
$rowCountry = $stmtCountry->fetch(PDO::FETCH_ASSOC);
$countryCode = $rowCountry['country_code'] ?? 'Syria';
} catch (Exception $e) {
$countryCode = 'Syria'; // fallback
}
}
// ============================================================
// 2. Load Country Pricing from `kazan` Table
// ============================================================
try {
$stmtKazan = $con->prepare("SELECT * FROM kazan WHERE country = ? LIMIT 1");
$stmtKazan->execute([$countryCode]);
$countryPricing = $stmtKazan->fetch(PDO::FETCH_ASSOC);
if (!$countryPricing) {
// Fallback: إذا لم نجد سعر للدولة، نستخدم Syria كافتراضي
error_log("[finish_ride_updates] No pricing found for country: $countryCode. Falling back to Syria.");
$stmtKazan->execute(['Syria']);
$countryPricing = $stmtKazan->fetch(PDO::FETCH_ASSOC);
$countryCode = 'Syria';
}
} catch (PDOException $e) {
error_log("[finish_ride_updates] Failed to load country pricing: " . $e->getMessage());
jsonError("Failed to load pricing configuration.");
exit;
}
// ============================================================
// 3. Server-Side Price Calculation (Secure — NOT from client)
// ============================================================
try {
// Fetch ride data from remote/local DB for server-side calculation
$stmtRideData = $con->prepare("
SELECT id, price AS quoted_price, car_type,
distance AS planned_distance, passenger_id, driver_id, price_for_driver
FROM ride WHERE id = ? AND driver_id = ?
LIMIT 1
");
$stmtRideData->execute([$rideId, $driver_id]);
$rideData = $stmtRideData->fetch(PDO::FETCH_ASSOC);
if (!$rideData) {
jsonError("Ride not found or driver mismatch.");
exit;
}
$quotedPrice = floatval($rideData['quoted_price'] ?? 0);
$kazanPercent = floatval($countryPricing['kazanPercent'] ?? $countryPricing['kazan'] ?? 10); // 🆕 من جدول kazan (kazanPercent هو الاسم الجديد)
$carType = $rideData['car_type'] ?? 'Fixed Price';
// 🔥 [Fix Driver Commission] عند عرض السعر (pricing/get.php) قد تُطبَّق نسبة
// خصم عمولة خاصة بالمنطقة (kazanDiscountFactor من surge:kazan_discounts)
// فتُحفَظ نتيجتها في عمود ride.price_for_driver — وهو "الوعد" الذي رآه
// السائق كـ"أرباحك أعلى" وقت قبول الطلب. إعادة جلب kazanPercent الخام هنا
// كانت تتجاهل ذلك الخصم بالكامل. نشتق نسبة العمولة الفعلية من الفرق
// المحفوظ فعلياً بين السعر المُقتبَس وحصة السائق منه، ونستخدمها بدل
// النسبة الخام، حتى تبقى النسبة المطبقة عند التسوية مطابقة لما وُعد به.
$priceForDriver = floatval($rideData['price_for_driver'] ?? 0);
if ($quotedPrice > 0 && $priceForDriver > 0 && $priceForDriver <= $quotedPrice) {
$effectiveKazanPercent = (($quotedPrice - $priceForDriver) / $quotedPrice) * 100;
error_log("[finish_ride_updates] Using locked commission rate for ride $rideId: {$effectiveKazanPercent}% (was raw {$kazanPercent}%)");
$kazanPercent = $effectiveKazanPercent;
}
// Fixed-price types, Speed & Awfar: use quoted price as-is
$fixedPriceTypes = ['Speed', 'Fixed Price', 'Awfar Car'];
if (in_array($carType, $fixedPriceTypes)) {
$finalPrice = $quotedPrice; // Fallback if Redis fails
// 🆕 Immutable Fare Lock: Force use of Redis locked price
try {
global $redis;
if ($redis) {
$lockedPrice = $redis->get("ride_locked_price_{$rideId}");
if ($lockedPrice !== false) {
$finalPrice = floatval($lockedPrice);
error_log("[finish_ride_updates] Using Redis Locked Price for ride {$rideId}: {$finalPrice}");
} else {
error_log("[finish_ride_updates] Redis Locked Price not found for ride {$rideId}. Using DB quoted price.");
}
} else {
error_log("[finish_ride_updates] Global Redis instance not available, using DB quoted price.");
}
} catch (Exception $e) {
error_log("[finish_ride_updates] Redis Error (reading locked price): " . $e->getMessage());
}
} else {
// Variable pricing: calculate from actual distance
$cleanDist = preg_replace('/[^0-9.]/', '', $actualDistance);
$distanceKm = floatval($cleanDist);
// 🔥 [Fix Price Cap] سقف أعلى على الانحراف عن المسافة المخططة —
// actualDistance يأتي من العميل، بدون هذا السقف يمكن لانجراف GPS
// أو قيمة مُتلاعَب بها أن تُضخّم السعر النهائي بلا حدود. نسمح بهامش
// معقول للانحرافات الحقيقية (تحويلة، إغلاق طريق...) ونقصّ الباقي.
$plannedDistanceKm = floatval($rideData['planned_distance'] ?? 0);
if ($plannedDistanceKm > 0) {
$maxAllowedDistanceKm = max($plannedDistanceKm * 1.5, $plannedDistanceKm + 5);
if ($distanceKm > $maxAllowedDistanceKm) {
error_log("[finish_ride_updates] ⚠️ actualDistance ($distanceKm km) exceeds cap ($maxAllowedDistanceKm km) for ride $rideId — planned was $plannedDistanceKm km. Clamping.");
$distanceKm = $maxAllowedDistanceKm;
}
}
if ($distanceKm <= 0) {
$finalPrice = $quotedPrice; // fallback
} else {
// 🆕 استخدام الأسعار من جدول kazan حسب الدولة (كل نوع سيارة له عمود سعره الخاص)
$perKmRate = getPerKmRate($carType, $countryPricing);
$perMinRate = getPerMinRate($countryPricing);
$durationMin = intval(preg_replace('/[^0-9]/', '', $actualDuration));
// نفس فكرة السقف على المدة: لا نسمح بمدة أكبر من ضعف زمن الرحلة
// المعقول (نفترض حد أقصى واسع 3 ساعات إذا لم تتوفر مدة مخططة)
$maxAllowedDurationMin = 180;
if ($durationMin > $maxAllowedDurationMin) {
error_log("[finish_ride_updates] ⚠️ actualDuration ($durationMin min) exceeds cap ($maxAllowedDurationMin min) for ride $rideId. Clamping.");
$durationMin = $maxAllowedDurationMin;
}
$calculated = ($distanceKm * $perKmRate) + ($durationMin * $perMinRate);
// 🆕 تطبيق خصم التتابع (إعفاء من العمولة) - نحدد المتغير فقط لكن لا نغير السعر على الراكب
$is_zero_commission = false;
if (hasZeroCommission($con, $driver_id)) {
$is_zero_commission = true;
error_log("[finish_ride_updates] Driver $driver_id has active 0% commission streak!");
}
// السعر النهائي يجب أن يتضمن العمولة دائماً لكي يدفعها الراكب،
// لكن إذا كانت العمولة صفر للسائق، يتم إعطاؤها للسائق بدلاً من الشركة عبر السيرفر المالي.
$calculated *= (1 + ($kazanPercent / 100));
// 🔥 [Fix Price Cap] سقف أعلى مطلق أيضاً على السعر النهائي نفسه
// (دفاع ثانٍ) — لا يتجاوز 1.6 * السعر المُقتبَس أصلاً بأي حال.
$maxAllowedPrice = $quotedPrice > 0 ? $quotedPrice * 1.6 : round($calculated, 2);
$finalPrice = max($quotedPrice, min(round($calculated, 2), $maxAllowedPrice));
}
}
// 🆕 تحديد رمز العملة حسب الدولة
$currency = getCurrencyByCountry($countryCode);
} catch (PDOException $e) {
error_log("[finish_ride_updates] " . $e->getMessage());
jsonError("Error calculating price");
exit;
}
// ============================================================
// 4. Atomic Transaction: Update DBs + Process Payment
// ============================================================
try {
// 🔥 [Fix Split-Brain] كان تحديث قاعدة البيانات البعيدة (con_ride) يحدث
// هنا قبل محاولة الدفع وبدون أي Rollback عليه — فإذا فشل الدفع لاحقاً،
// كانت con_ride تبقى 'Finished' بينما المحلية تُرجَع لـ 'Begin'،
// فإعادة محاولة لاحقة تفشل بصمت على con_ride (شرط WHERE status='Begin'
// لم يعد يتحقق) وتُنتج سعرين مختلفين بين القاعدتين. الآن نؤجل تحديث
// con_ride إلى ما بعد نجاح الدفع فعلياً (انظر الأسفل بعد commit()).
// --- BEGIN Local DB Transaction ---
$con->beginTransaction();
// 4a. Update ride (local DB)
$stmtLocal = $con->prepare(
"UPDATE ride SET status = ?, rideTimeFinish = NOW(), price = ? WHERE id = ? AND status = 'Begin'"
);
$stmtLocal->execute([$newStatus, $finalPrice, $rideId]);
if ($stmtLocal->rowCount() == 0) {
throw new Exception("Ride already finished or not found in local DB.");
}
// 4b. Update driver_orders (Optimized atomic query)
$stmtOrders = $con->prepare("
INSERT INTO `driver_orders` (`driver_id`, `order_id`, `status`, `created_at`)
VALUES (?, ?, ?, NOW())
ON DUPLICATE KEY UPDATE
`driver_id` = VALUES(`driver_id`),
`status` = VALUES(`status`),
`created_at` = NOW()
");
$stmtOrders->execute([$driver_id, $rideId, $newStatus]);
// ============================================================
// 4c. Server-to-Server Payment Processing (S2S)
// ============================================================
$paymentPayload = [
'rideId' => $rideId,
'driverId' => $driver_id,
'passengerId' => $passengerId,
'paymentAmount' => $finalPrice,
'paymentMethod' => ($walletChecked === 'true') ? 'wallet' : 'cash',
'walletChecked' => $walletChecked,
'passengerWalletBurc' => $passengerWalletBurc,
'authToken' => $driver_token,
'currency' => $currency, // 🆕 إرسال العملة لمخدم الدفع
'country_code' => $countryCode, // 🆕 إرسال الدولة لمخدم الدفع
'is_zero_commission' => isset($is_zero_commission) && $is_zero_commission ? 'true' : 'false', // 🆕 إرسال إعفاء العمولة
'kazanPercent' => $kazanPercent, // 🆕 إرسال نسبة العمولة متغيرة حسب الدولة
];
$ch = curl_init(WALLET_PAYMENT_URL);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($paymentPayload),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
CURLOPT_HTTPHEADER => [
'Content-Type: application/x-www-form-urlencoded',
'X-S2S-Api-Key: ' . S2S_SHARED_KEY,
],
]);
$paymentResponse = curl_exec($ch);
$httpStatusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlError = curl_error($ch);
curl_close($ch);
// Validate payment response
$paymentSuccess = false;
$paymentError = '';
if ($curlError) {
$paymentError = "S2S connection error: " . $curlError;
} elseif ($httpStatusCode !== 200) {
$paymentError = "Payment server returned HTTP $httpStatusCode";
} else {
$paymentResult = json_decode($paymentResponse, true);
if ($paymentResult && isset($paymentResult['status']) && $paymentResult['status'] === 'success') {
$paymentSuccess = true;
} else {
$paymentError = $paymentResult['error'] ?? 'Payment server returned failure';
}
}
if (!$paymentSuccess) {
// ❌ Payment failed — ROLLBACK everything
$con->rollBack();
error_log("[finish_ride_updates] Payment FAILED for ride $rideId: $paymentError");
jsonError("Payment processing failed: $paymentError");
exit;
}
// ✅ Payment succeeded — COMMIT
$con->commit();
// 🆕 Cache ride state in Redis — placed immediately after commit(), before
// the best-effort remote-DB sync / streak / notification code below, so a
// later exception in any of those non-critical steps can never suppress
// this write for a ride that's genuinely finished in MySQL.
sendToLocationServer('update_ride_state', [
'ride_id' => $rideId,
'status' => $newStatus,
'driver_id' => $driver_id,
'passenger_id' => $passengerId,
]);
// 🔥 [Fix Split-Brain] تحديث القاعدة البعيدة الآن فقط، بعد أن أصبح الدفع
// والتحديث المحلي مؤكدَين نجاحهما — يبقي الحالتين متطابقتين دائماً.
// فشل هذا التحديث best-effort فقط (لا يُرجع الرحلة المحلية المُنجَزة فعلاً).
if (isset($con_ride)) {
try {
$stmtRemote = $con_ride->prepare(
"UPDATE ride SET status = ?, rideTimeFinish = NOW(), price = ? WHERE id = ? AND status = 'Begin'"
);
$stmtRemote->execute([$newStatus, $finalPrice, $rideId]);
} catch (PDOException $e) {
error_log("[finish_ride_updates] Remote DB (con_ride) update failed for ride $rideId: " . $e->getMessage());
}
}
// 🆕 Update Driver Streak (Increment because ride is finished successfully)
handleDriverStreak($con, $driver_id, 'increment');
// ============================================================
// 5. Notifications (After successful commit)
// ============================================================
$passenger_id = $passengerId; // alias for legacy code
if (!empty($passenger_id)) {
// Legacy list for backward compatibility
$legacyList = [
(string)$driver_id,
(string)$rideId,
(string)$driver_token,
(string)$finalPrice
];
// a) Socket notification
$socketPayload = [
'ride_id' => $rideId,
'status' => 'finished',
'price' => $finalPrice,
'currency' => $currency, // 🆕
'DriverList' => $legacyList
];
if (function_exists('notifyPassengerOnRideServer')) {
notifyPassengerOnRideServer($passenger_id, $socketPayload);
}
// b) FCM notification
if (!empty($passengerToken)) {
$fcmData = [
'ride_id' => (string)$rideId,
'price' => (string)$finalPrice,
'currency' => $currency, // 🆕
'DriverList' => $legacyList
];
sendFCM_Internal(
$passengerToken,
"تم إنهاء الرحلة 🏁",
"المبلغ المطلوب: " . $finalPrice . " " . $currency,
$fcmData,
'Driver Finish Trip',
$passengerId
);
}
}
// 🆕 c) Driver notification for Zero Commission
if (isset($is_zero_commission) && $is_zero_commission && !empty($driver_token)) {
$fcmDriverData = [
'ride_id' => (string)$rideId,
'status' => 'streak_reward'
];
sendAndSaveDriverNotification(
$con,
$driver_id,
$driver_token,
"🔥 مكافأة التتابع",
"أنت بطل! لم يتم خصم عمولة لهذه الرحلة لأنك حافظت على تتابع قبول الرحلات.",
$fcmDriverData,
'Zero Commission Reward'
);
}
// ============================================================
// 6. Return Success with server-calculated price + currency
// ============================================================
jsonSuccess([
'price' => $finalPrice,
'currency' => $currency, // 🆕 إرجاع العملة للتطبيق
'rideId' => $rideId
], "Ride finished and payment processed successfully.");
} catch (Exception $e) {
if (isset($con) && $con->inTransaction()) {
$con->rollBack();
}
error_log("[finish_ride_updates] Error for ride $rideId: " . $e->getMessage());
jsonError("Transaction failed");
}
// ============================================================
// Helper Functions — الآن تقرأ الأسعار من جدول kazan حسب الدولة
// ============================================================
/**
* الحصول على سعر الكيلومتر حسب نوع السيارة من جدول أسعار الدولة
*
* 🆕 كل نوع سيارة له عمود مستقل في جدول kazan:
* Speed → speedPrice | Comfort → comfortPrice | Lady → ladyPrice
* Electric → electricPrice | Van → vanPrice | Delivery → deliveryPrice
* Mishwar Vip → mishwarVipPrice | Fixed Price → fixedPrice | Awfar → awfarPrice
*
* @param string $carType نوع السيارة
* @param array $countryPricing صف من جدول kazan
* @return float
*/
function getPerKmRate(string $carType, array $countryPricing): float {
// 🆕 الخريطة المباشرة: كل نوع سيارة يقابله عمود بنفس الاسم + "Price"
$rateColumns = [
'Comfort' => 'comfortPrice',
'Speed' => 'speedPrice',
'Lady' => 'ladyPrice',
'Electric' => 'electricPrice',
'Van' => 'vanPrice',
'Delivery' => 'deliveryPrice',
'Mishwar Vip' => 'mishwarVipPrice',
'Fixed Price' => 'fixedPrice',
'Awfar Car' => 'awfarPrice',
];
$column = $rateColumns[$carType] ?? 'speedPrice';
// دعم التوافق مع الإصدارات القديمة (backward compatibility)
$rate = floatval($countryPricing[$column] ?? 0);
// إذا كان السعر صفر أو غير موجود، نبحث في الأسماء القديمة
if ($rate <= 0) {
$oldColumnMap = [
'Lady' => 'familyPrice',
'Mishwar Vip' => 'freePrice',
'Electric' => 'naturePrice',
'Van' => 'heavyPrice',
];
$oldColumn = $oldColumnMap[$carType] ?? null;
if ($oldColumn && isset($countryPricing[$oldColumn])) {
$rate = floatval($countryPricing[$oldColumn]);
}
}
// Fallback أخير
if ($rate <= 0) {
$rate = floatval($countryPricing['speedPrice'] ?? 36);
}
return $rate;
}
/**
* الحصول على سعر الدقيقة حسب وقت اليوم من جدول kazan
*
* 🆕 الآن يقرأ من أعمدة الدقيقة المنفصلة:
* normalMinPrice → Normal (9 ص - 2 م / 6 م - 9 م)
* peakMinPrice → Peak (2 م - 5 م)
* lateMinPrice → Late (9 م - 1 ص)
*
* @param array $countryPricing صف من جدول kazan
* @return float
*/
function getPerMinRate(array $countryPricing): float {
$hour = (int)date('H');
// 🆕 قراءة الأسعار من الأعمدة الجديدة للدقيقة
$normalMinPrice = floatval($countryPricing['normalMinPrice'] ?? 0);
$peakMinPrice = floatval($countryPricing['peakMinPrice'] ?? 0);
$lateMinPrice = floatval($countryPricing['lateMinPrice'] ?? 0);
// 🆕 دعم التوافق مع الإصدارات القديمة (latePrice, naturePrice)
if ($lateMinPrice <= 0) $lateMinPrice = floatval($countryPricing['latePrice'] ?? 0);
if ($normalMinPrice <= 0) $normalMinPrice = floatval($countryPricing['naturePrice'] ?? 0);
// Fallback: حساب سعر الدقيقة من speedPrice إذا كانت الأعمدة الجديدة فارغة
if ($normalMinPrice <= 0) {
$speedPrice = floatval($countryPricing['speedPrice'] ?? 36);
$normalMinPrice = $speedPrice / 4;
}
if ($peakMinPrice <= 0) $peakMinPrice = $normalMinPrice * 1.15; // 15% زيادة
if ($lateMinPrice <= 0) $lateMinPrice = $normalMinPrice * 1.25; // 25% زيادة
if ($hour >= 21 || $hour < 1) {
return round($lateMinPrice, 2); // Late Night
}
if ($hour >= 14 && $hour <= 17) {
return round($peakMinPrice, 2); // Peak
}
return round($normalMinPrice, 2); // Normal
}
/**
* تحديد رمز العملة حسب الدولة
*
* @param string $countryCode رمز الدولة (Syria, Egypt, Jordan, ...)
* @return string رمز العملة (SYP, EGP, JOD, ...)
*/
function getCurrencyByCountry(string $countryCode): string {
$currencies = [
'Syria' => 'SYP',
'Egypt' => 'EGP',
'Jordan' => 'JOD',
'Iraq' => 'IQD',
'UAE' => 'AED',
'Saudi Arabia' => 'SAR',
'Qatar' => 'QAR',
'Kuwait' => 'KWD',
'Bahrain' => 'BHD',
'Oman' => 'OMR',
'Turkey' => 'TRY',
'Lebanon' => 'LBP',
'Palestine' => 'ILS',
'Yemen' => 'YER',
'Libya' => 'LYD',
'Tunisia' => 'TND',
'Algeria' => 'DZD',
'Morocco' => 'MAD',
'Sudan' => 'SDG',
];
return $currencies[$countryCode] ?? 'SYP'; // افتراضي: ليرة سورية
}
?>
+49
View File
@@ -0,0 +1,49 @@
<?php
require_once __DIR__ . '/../../connect.php';
$passenger_id = filterRequest("passenger_id");
$driver_id = filterRequest("driver_id");
try {
// Step 1: Count rides for passenger or driver
$baseSql = "SELECT COUNT(*) AS total_rows FROM `ride`";
$params = [];
if (!empty($passenger_id)) {
$baseSql .= " WHERE passenger_id = :passenger_id";
$params[':passenger_id'] = $passenger_id;
} elseif (!empty($driver_id)) {
$baseSql .= " WHERE driver_id = :driver_id";
$params[':driver_id'] = $driver_id;
}
$stmt = $con->prepare($baseSql);
$stmt->execute($params);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
$total_rows = $row['total_rows'] ?? 0;
if ($total_rows > 0) {
// Step 2: Fetch the latest 10 ride records
$rideSql = "SELECT * FROM `ride`";
if (!empty($passenger_id)) {
$rideSql .= " WHERE passenger_id = :passenger_id ORDER BY created_at DESC LIMIT 10";
} elseif (!empty($driver_id)) {
$rideSql .= " WHERE driver_id = :driver_id ORDER BY created_at DESC LIMIT 10";
}
$rideStmt = $con->prepare($rideSql);
$rideStmt->execute($params);
$rides = $rideStmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode([
"status" => "success",
"data" => $rides
]);
} else {
jsonSuccess([], "No rides found");
}
} catch (PDOException $e) {
error_log("[rides/get] " . $e->getMessage());
jsonError("Database error");
}
?>
+92
View File
@@ -0,0 +1,92 @@
<?php
// getRealTimeHeatmap.php
require_once __DIR__ . '/../../get_connect.php';
header('Content-Type: application/json; charset=utf-8');
try {
$precision = 2; // دقة الشبكة (تقريباً 1 كم)
// الأوزان
$WEIGHT_WAITING = 5; // طلب انتظار = 5 نقاط
$WEIGHT_MISSED = 8; // طلب ضائع = 8 نقاط (أهمية قصوى)
$WEIGHT_ACTIVE = 1; // طلب نشط = 1 نقطة
$grid = [];
// 1. طلبات الانتظار (Waiting)
$stmtW = $con->prepare("SELECT start_lat, start_lng FROM waitingRides WHERE status IN ('wait', 'waiting')");
$stmtW->execute();
while ($row = $stmtW->fetch(PDO::FETCH_ASSOC)) {
addToGrid($grid, $row['start_lat'], $row['start_lng'], $precision, $WEIGHT_WAITING);
}
// 2. طلبات ضائعة (Timeout)
$stmtM = $con->prepare("SELECT start_location FROM ride WHERE (status = 'timeout' OR status = 'cancelled_no_driver_found') AND created_at >= DATE_SUB(NOW(), INTERVAL 20 MINUTE)");
$stmtM->execute();
while ($row = $stmtM->fetch(PDO::FETCH_ASSOC)) {
$parts = explode(',', $row['start_location']);
if (count($parts) == 2) addToGrid($grid, $parts[0], $parts[1], $precision, $WEIGHT_MISSED);
}
// 3. طلبات نشطة (Active)
$stmtA = $con->prepare("SELECT start_location FROM ride WHERE created_at >= DATE_SUB(NOW(), INTERVAL 15 MINUTE) AND status NOT IN ('timeout', 'cancelled_no_driver_found')");
$stmtA->execute();
while ($row = $stmtA->fetch(PDO::FETCH_ASSOC)) {
$parts = explode(',', $row['start_location']);
if (count($parts) == 2) addToGrid($grid, $parts[0], $parts[1], $precision, $WEIGHT_ACTIVE);
}
// تجهيز البيانات النهائية
$finalData = [];
foreach ($grid as $cell) {
$score = $cell['score']; // مجموع النقاط (الوزن)
$count = $cell['count']; // العدد الحقيقي للطلبات
// 🧠 المنطق المزدوج: نحدد اللون بناءً على النقاط أو العدد
$intensity = "low";
$surge = 1.0;
// المعادلة: منطقة حمراء إذا كان السكور عالي جداً (مشاكل) أو العدد كبير جداً (زحمة)
if ($score >= 15 || $count >= 5) {
$intensity = "high"; // أحمر (خطر/فرصة ذهبية)
$surge = 1.5;
} elseif ($score >= 8 || $count >= 3) {
$intensity = "medium"; // برتقالي
$surge = 1.2;
} elseif ($score >= 3 || $count >= 1) {
$intensity = "normal"; // أصفر
}
if ($score > 0) {
$finalData[] = [
'lat' => $cell['lat'],
'lng' => $cell['lng'],
'count' => $count, // ✅ العدد الحقيقي (مهم للعرض)
'intensity' => $intensity, // ✅ التصنيف الذكي
'surge' => $surge
];
}
}
file_put_contents('heatmap_live.json', json_encode($finalData)); // الكاش
echo json_encode(["status" => "success", "data" => $finalData]);
} catch (Exception $e) {
error_log("[getRealTimeHeatmap] Error: " . $e->getMessage());
echo json_encode(["status" => "failure", "message" => "An internal error occurred."]);
}
function addToGrid(&$grid, $lat, $lng, $precision, $weight) {
if (empty($lat) || empty($lng)) return;
$rLat = round(floatval($lat), $precision);
$rLng = round(floatval($lng), $precision);
$key = "$rLat,$rLng";
if (!isset($grid[$key])) {
$grid[$key] = ['lat' => $rLat, 'lng' => $rLng, 'count' => 0, 'score' => 0];
}
$grid[$key]['count']++; // زيادة العدد (+1 دائماً)
$grid[$key]['score'] += $weight; // زيادة الوزن (حسب نوع الطلب)
}
?>
+204
View File
@@ -0,0 +1,204 @@
<?php
// ═══════════════════════════════════════════════════════════════
// ride/rides/getRideOrderID.php
// PURPOSE : بيانات السائق/السيارة للرحلة الحالية — المسار الاحتياطي للراكب
// حين لا يوصل السوكيت driver_info (ride_lifecycle_controller.dart
// → getUpdatedRideForDriverApply).
//
// لماذا هذا الملف: التطبيق يطلب getRideOrderID.php منذ البداية، والموجود
// كان getRideOrderIDNew.php فقط ⇒ 404 صامت. ونتيجته أن الـ polling يسبق
// الـ FCM فيضبط _isAcceptanceProcessed=true بلا بيانات، فيُرفض لاحقاً
// payload الـ FCM الذي يحمل driver_info كاملاً ("Already processed")،
// فتظهر الرحلة مقبولة بلا معلومات سائق، ويبقى driverToken فارغاً فلا
// تُرسَل رسائل الراكب للسائق.
//
// getRideOrderIDNew.php لا يصلح بديلاً مباشراً: هو داخلي (get_connect.php →
// validateInternalKey) ولا يستطيع التطبيق مناداته، ولا يرجع
// ratingCount/completedRides/driverTier.
//
// الرد يطابق شكل driverInfo في acceptRide.php حرفياً حتى يقرأه
// _fillDriverDataLocally و getUpdatedRideForDriverApply بنفس المفاتيح.
// ═══════════════════════════════════════════════════════════════
include "../../connect.php";
// الراكب من الـ JWT فقط — لا نثق بأي passengerID قادم من الطلب (حماية IDOR)
$passengerId = $user_id;
$rideIdParam = filterRequest("id");
if (empty($passengerId)) {
printFailure("Unauthorized");
exit;
}
try {
// ── 1. الرحلة: ride DB هو المرجع، مع fallback على primary ──────
$rideRow = null;
$sqlRide = "SELECT id, driver_id, passenger_id, status
FROM `ride`
WHERE passenger_id = ? ";
$params = [$passengerId];
if (!empty($rideIdParam)) {
$sqlRide .= "AND id = ? ";
$params[] = $rideIdParam;
}
$sqlRide .= "ORDER BY id DESC LIMIT 1";
try {
$con_ride = Database::get('ride');
$stmtRide = $con_ride->prepare($sqlRide);
$stmtRide->execute($params);
$rideRow = $stmtRide->fetch(PDO::FETCH_ASSOC);
} catch (Exception $eRideDb) {
error_log("[getRideOrderID] ride DB unavailable: " . $eRideDb->getMessage());
}
if (!$rideRow) {
$stmtRide = $con->prepare($sqlRide);
$stmtRide->execute($params);
$rideRow = $stmtRide->fetch(PDO::FETCH_ASSOC);
}
if (!$rideRow) {
printFailure("No ride found");
exit;
}
$driverId = $rideRow['driver_id'] ?? '';
if (empty($driverId)) {
// رحلة لم يقبلها سائق بعد — ليست حالة خطأ
echo json_encode([
"status" => "success",
"message" => "No driver assigned yet",
"data" => ["ride_id" => (string)$rideRow['id'], "status" => $rideRow['status']],
]);
exit;
}
// ── 2. بيانات السائق — نفس استعلام acceptRide.php ──────────────
$stmtDriver = $con->prepare("
SELECT
d.id AS driver_id,
d.first_name,
d.last_name,
d.gender,
d.phone,
c.make,
c.model,
c.car_plate,
c.year,
c.color,
c.color_hex,
(SELECT ROUND(AVG(rating), 2) FROM ratingDriver WHERE driver_id = d.id) AS ratingDriver,
(SELECT COUNT(*) FROM ratingDriver WHERE driver_id = d.id) AS ratingCount,
(SELECT COUNT(*) FROM ride WHERE driver_id = d.id AND status IN ('Finished', 'finished')) AS completedRides,
dt.token
FROM driver d
LEFT JOIN CarRegistration c ON c.driverID = d.id
LEFT JOIN driverToken dt ON dt.captain_id = d.id
WHERE d.id = ?
LIMIT 1
");
$stmtDriver->execute([$driverId]);
$driverRaw = $stmtDriver->fetch(PDO::FETCH_ASSOC);
if (!$driverRaw) {
printFailure("Driver not found");
exit;
}
// جدول driverToken يخزّن التوكن مشفّراً — بلا فك تشفير يصل التطبيق
// blob يستخدمه كـ FCM target فيرفضه FCM بـ 400 ولا تصل رسائل الراكب.
$driverInfo = [];
$encryptedFields = ['first_name', 'last_name', 'gender', 'phone', 'car_plate', 'token'];
foreach ($driverRaw as $key => $value) {
if (in_array($key, $encryptedFields, true) && !empty($value)) {
$decrypted = false;
try {
$decrypted = $encryptionHelper->decryptData($value);
} catch (\Throwable $e) {
$decrypted = false;
}
$driverInfo[$key] = ($decrypted !== false && $decrypted !== null && $decrypted !== '')
? $decrypted
: $value;
} else {
$driverInfo[$key] = $value;
}
}
$driverInfo['driverName'] = trim(($driverInfo['first_name'] ?? '') . ' ' . ($driverInfo['last_name'] ?? ''));
if (empty($driverInfo['driverName']) && !empty($driverInfo['phone'])) {
$driverInfo['driverName'] = $driverInfo['phone'];
}
$driverInfo['ratingDriver'] = !empty($driverInfo['ratingDriver']) ? (string)$driverInfo['ratingDriver'] : "5.0";
$ratingValue = (float) $driverInfo['ratingDriver'];
$ratingCount = (int) ($driverInfo['ratingCount'] ?? 0);
$completedRides = (int) ($driverInfo['completedRides'] ?? 0);
if ($ratingValue >= 4.8 && $ratingCount >= 50 && $completedRides >= 100) {
$driverInfo['driverTier'] = 'Professional driver';
} elseif ($ratingValue >= 4.5 && $ratingCount >= 15 && $completedRides >= 30) {
$driverInfo['driverTier'] = 'Trusted driver';
} else {
$driverInfo['driverTier'] = 'Verified driver';
}
// ── 3. آخر موقع للسائق من Redis (اختياري) ──────────────────────
if (isset($redisLocation) && $redisLocation) {
try {
// ‏المفتاح driver:public هو ما يكتبه driver_socket.php فعلاً
// ‏(driver:location لا وجود له — كان خطأً منسوخاً من acceptRide.php).
$driverLoc = $redisLocation->hGetAll("driver:public:$driverId");
if (!empty($driverLoc)) {
$driverInfo['lat'] = (float)($driverLoc['lat'] ?? 0);
$driverInfo['lng'] = (float)($driverLoc['lng'] ?? 0);
$driverInfo['heading'] = (float)($driverLoc['heading'] ?? 0);
}
} catch (Exception $eLoc) {
// تجاهل — الموقع يصل بالسوكيت أصلاً
}
}
// ⚠️ تصادم مفاتيح: getUpdatedRideForDriverApply يقرأ passengerName + last_name
// كاسم **الراكب**. لو تركنا last_name للسائق يظهر لقب السائق كاسم الراكب.
// ننقل لقب السائق إلى driver_last_name (وهو مفتاح يفهمه
// _fillDriverDataLocally أصلاً) ونرجّع اسم الراكب الحقيقي في مكانه.
$driverInfo['driver_last_name'] = $driverInfo['last_name'] ?? '';
unset($driverInfo['last_name']);
try {
$stmtPas = $con->prepare("SELECT first_name, last_name FROM passengers WHERE id = ? LIMIT 1");
$stmtPas->execute([$passengerId]);
$pasRow = $stmtPas->fetch(PDO::FETCH_ASSOC);
if ($pasRow) {
foreach (['first_name' => 'passengerName', 'last_name' => 'last_name'] as $src => $dst) {
$val = $pasRow[$src] ?? '';
if ($val === '' || $val === null) { $driverInfo[$dst] = ''; continue; }
$dec = false;
try {
$dec = $encryptionHelper->decryptData($val);
} catch (\Throwable $e) {
$dec = false;
}
$driverInfo[$dst] = ($dec !== false && $dec !== null && $dec !== '') ? $dec : $val;
}
}
} catch (PDOException $ePas) {
error_log("[getRideOrderID] passenger name lookup WARNING: " . $ePas->getMessage());
}
$driverInfo['ride_id'] = (string)$rideRow['id'];
$driverInfo['status'] = $rideRow['status'];
echo json_encode([
"status" => "success",
"message" => "Driver info",
"data" => $driverInfo,
]);
} catch (PDOException $e) {
error_log("[getRideOrderID] CRITICAL: " . $e->getMessage());
printFailure("Server error");
}
+156
View File
@@ -0,0 +1,156 @@
<?php
// نقوم بتضمين ملف الاتصال المعدل الذي يحتوي على $con (الرئيسي) و $con_ride (الرحلات)
require_once __DIR__ . '/../../get_connect.php';
// استلام البيانات (يمكن استلام ID الرحلة أو ID الراكب)
$passengerID = filterRequest("passengerID");
$rideID = filterRequest("id"); // إضافة استقبال متغير رقم الرحلة
// التحقق من أن الراكب يطلب بيانات رحلته هو فقط (حماية IDOR)
// get_connect.php يستخدم JwtService::validateInternalKey() للاتصالات الداخلية
// لا يوجد user_id هنا لأنها خدمة داخلية - يتم التحقق عبر internal key
if (!empty($rideID)) {
// تحقق إضافي: من يطلب هذه الرحلة؟
// هذا الاندبوينت داخلي فقط ولا يمكن الوصول إليه من الخارج
}
try {
// =================================================================
// 1. الخطوة الأولى: تحديد استراتيجية البحث (بواسطة رقم الرحلة أو الراكب)
// =================================================================
$sqlRide = "SELECT
id,
start_location,
end_location,
date,
driver_id,
passenger_id,
price,
status,
created_at,
DriverIsGoingToPassenger,
rideTimeStart,
rideTimeFinish,
price_for_driver,
distance
FROM ride ";
// المنطق الجديد:
// إذا تم إرسال rideID، نبحث عن الرحلة المحددة بدقة (تجنباً لأي تضارب)
// إذا لم يتم إرساله، نبحث عن أحدث رحلة للراكب (للتتبع المباشر)
if (!empty($rideID)) {
$sqlRide .= "WHERE id = :rideID";
} else {
$sqlRide .= "WHERE passenger_id = :passengerID ORDER BY id DESC LIMIT 1";
}
// نستخدم المتغير $con_ride (سيرفر الرحلات)
$stmtRide = $con_ride->prepare($sqlRide);
// ربط المتغيرات حسب نوع البحث
if (!empty($rideID)) {
$stmtRide->bindParam(':rideID', $rideID);
} else {
$stmtRide->bindParam(':passengerID', $passengerID);
}
$stmtRide->execute();
$rideData = $stmtRide->fetch(PDO::FETCH_ASSOC);
// إذا لم يتم العثور على رحلة في سيرفر الرحلات، نوقف العملية
if (!$rideData) {
echo json_encode(["status" => "failure", "message" => "No ride found"]);
exit;
}
// =================================================================
// 2. الخطوة الثانية: جلب البيانات الثابتة (سائق، سيارة، تقييم) من السيرفر الرئيسي ($con)
// نستخدم المعرفات التي حصلنا عليها من نتيجة الاستعلام الأول
// =================================================================
$driverID = $rideData['driver_id'];
$pID = $rideData['passenger_id']; // نأخذ معرف الراكب من الرحلة نفسها لضمان التطابق
// ملاحظة: استخدام :driverID_Sub في الاستعلام الفرعي لتجنب أخطاء PDO
$sqlDetails = "SELECT
passengers.first_name AS passengerName,
passengers.last_name,
CarRegistration.make,
CarRegistration.model,
CarRegistration.car_plate,
CarRegistration.year,
CarRegistration.color,
CarRegistration.color_hex,
driver.first_name AS driverName,
driver.gender,
driver.phone,
(
SELECT ROUND(AVG(ratingDriver.rating), 2)
FROM ratingDriver
WHERE ratingDriver.driver_id = :driverID_Sub
) AS ratingDriver,
driverToken.token AS token
FROM driver
LEFT JOIN passengers ON passengers.id = :passengerID
LEFT JOIN CarRegistration ON CarRegistration.driverID = driver.id
LEFT JOIN driverToken ON driverToken.captain_id = driver.id
WHERE driver.id = :driverID";
// نستخدم المتغير الأصلي $con للسيرفر الرئيسي
$stmtDetails = $con->prepare($sqlDetails);
// نربط المتغيرات
$stmtDetails->bindParam(':driverID', $driverID);
$stmtDetails->bindParam(':driverID_Sub', $driverID);
$stmtDetails->bindParam(':passengerID', $pID);
$stmtDetails->execute();
$detailsData = $stmtDetails->fetch(PDO::FETCH_ASSOC);
// =================================================================
// 3. الخطوة الثالثة: دمج البيانات وتجهيز الرد
// =================================================================
$finalData = [];
if ($detailsData) {
// دمج مصفوفة الرحلة (من سيرفر الرحلات) مع مصفوفة التفاصيل (من الرئيسي)
$finalData = array_merge($rideData, $detailsData);
} else {
// في حال كانت الرحلة بدون سائق بعد، نكتفي ببيانات الرحلة
$finalData = $rideData;
}
// =================================================================
// 4. فك التشفير (Decrypt)
// =================================================================
if ($finalData) {
$fieldsToDecrypt = ['driverName', 'gender', 'phone', 'car_plate', 'passengerName', 'last_name', 'token'];
foreach ($fieldsToDecrypt as $field) {
if (!empty($finalData[$field])) {
$finalData[$field] = $encryptionHelper->decryptData($finalData[$field]);
}
}
}
echo json_encode([
"status" => "success",
"data" => $finalData
]);
} catch (Exception $e) {
error_log("API Error: " . $e->getMessage());
http_response_code(500);
echo json_encode(["status" => "failure", "message" => "An internal server error occurred."]);
}
?>
+26
View File
@@ -0,0 +1,26 @@
<?php
require_once __DIR__ . '/../../connect.php';
$id = filterRequest("id");
if (empty($id)) {
jsonError("Missing ride ID.");
exit;
}
$sql = "SELECT `status` FROM `ride` WHERE `id` = :id";
$stmt = $con->prepare($sql);
$stmt->bindParam(':id', $id, PDO::PARAM_INT);
$stmt->execute();
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if ($row && isset($row['status'])) {
echo json_encode([
"status" => "success",
"data" => $row['status']
]);
} else {
jsonError("Ride not found.");
}
?>
+27
View File
@@ -0,0 +1,27 @@
<?php
//getRideStatusBegin.php
require_once __DIR__ . '/../../connect.php';
$ride_id = filterRequest("ride_id");
if (empty($ride_id)) {
jsonError("Ride ID is missing.");
exit;
}
$sql = "SELECT `status`, `DriverIsGoingToPassenger`, `rideTimeStart` FROM `ride` WHERE `id` = :ride_id";
$stmt = $con->prepare($sql);
$stmt->bindParam(':ride_id', $ride_id, PDO::PARAM_INT);
$stmt->execute();
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if ($row) {
echo json_encode([
"status" => "success",
"data" => $row
]);
} else {
jsonError("Ride not found.");
}
?>
@@ -0,0 +1,95 @@
<?php
require_once __DIR__ . '/../../connect.php';
$passenger_id = filterRequest("passenger_id");
try {
$con_ride = Database::get('ride');
// =========================================================
// 1. سيرفر الرحلات: جلب بيانات الرحلة
// =========================================================
$stmt = $con_ride->prepare("
SELECT
id AS rideId,
status,
start_location,
end_location,
carType,
driver_id,distance,
price,
created_at
FROM ride
WHERE passenger_id = ?
AND (
status IN ('waiting', 'wait', 'Apply', 'Applied', 'accepted', 'arrived', 'Arrived', 'Begin')
AND TIMESTAMP(date, time) >= NOW() - INTERVAL 1 HOUR
OR (status = 'Finished' AND updated_at >= NOW() - INTERVAL 1 HOUR)
)
ORDER BY TIMESTAMP(date, time) DESC
LIMIT 1
");
$stmt->execute([$passenger_id]);
$ride = $stmt->fetch(PDO::FETCH_ASSOC);
if (!$ride) {
echo json_encode(["status" => "failure", "message" => "No active ride found"]);
exit;
}
// =========================================================
// 2. السيرفر الرئيسي: جلب اسم السائق + متوسط تقييمه العام
// =========================================================
// ملاحظة: تم الحفاظ على الاستعلام كما هو
// rateDriver: هو الاسم الذي سنستخدمه في PHP
$stmt2 = $con->prepare("
SELECT
d.first_name AS driverName,
(SELECT AVG(rating) FROM ratingDriver WHERE driver_id = d.id) AS rateDriver,
(SELECT COUNT(*) FROM ratingDriver WHERE ride_id = ?) AS thisRideRated
FROM driver d
WHERE d.id = ?
");
$stmt2->execute([$ride['rideId'], $ride['driver_id']]);
$driverData = $stmt2->fetch(PDO::FETCH_ASSOC);
// =========================================================
// 3. المعالجة النهائية
// =========================================================
if ($driverData) {
// فك التشفير
$ride['driverName'] = $encryptionHelper->decryptData($driverData['driverName']);
// --- تصحيح الخطأ هنا ---
// كان يستدعي driverAvg وهو غير موجود، تم تغييره لـ rateDriver
$ride['rateDriver'] = $driverData['rateDriver'] ? round($driverData['rateDriver'], 2) : 5;
// --- منطق هل تحتاج الرحلة لتقييم (needsReview) ---
$isFinished = ($ride['status'] === 'Finished');
$isRated = ($driverData['thisRideRated'] > 0);
$ride['needsReview'] = ($isFinished && !$isRated) ? 1 : 0;
} else {
// حالة عدم وجود سائق (نادراً ما تحدث إذا كان driver_id موجوداً في جدول الرحلات)
$ride['driverName'] = null;
$ride['rateDriver'] = 5;
$ride['needsReview'] = 0;
}
// تنظيف البيانات
unset($ride['created_at']);
echo json_encode([
"status" => "success",
"data" => $ride
]);
} catch (Exception $e) {
error_log("[getRideStatusFromStartApp] Error: " . $e->getMessage());
echo json_encode(["status" => "failure", "message" => "An internal error occurred."]);
}
?>
@@ -0,0 +1,23 @@
<?php
require_once __DIR__ . '/../../connect.php';
$driver_id = filterRequest("driver_id");
$sql = "
SELECT COUNT(*) as count
FROM `ride`
WHERE driver_id = :driver_id AND status = 'Finished'
";
$stmt = $con->prepare($sql);
$stmt->bindParam(':driver_id', $driver_id, PDO::PARAM_INT); // أو PARAM_STR حسب نوع الـ ID
$stmt->execute();
if ($stmt->rowCount() > 0) {
$row = $stmt->fetch(PDO::FETCH_ASSOC);
jsonSuccess($row);
} else {
jsonError($message = "No finished ride records found for this driver");
}
?>
+154
View File
@@ -0,0 +1,154 @@
<?php
// تضمين ملف الاتصال الذي يحتوي على تعريف السيرفرات الثلاثة ($con, $con_ride, $con_tracking)
// وكائن التشفير $encryptionHelper
require_once __DIR__ . '/../../get_connect.php';
// السماح بالوصول من أي دومين (لأن الرابط سيفتح في متصفح العميل)
header("Content-Type: application/json; charset=UTF-8");
$rideID = filter_input(INPUT_GET, 'id', FILTER_SANITIZE_NUMBER_INT);
// تنظيف الـ Token: نتأكد من تعقيم الرموز الخاصة لمنع XSS
$token = filter_input(INPUT_GET, 'token', FILTER_SANITIZE_SPECIAL_CHARS);
// التحقق من وصول البيانات
if (!$rideID || !$token) {
http_response_code(400);
echo json_encode(["status" => "failure", "message" => "Missing Parameters"]);
exit;
}
try {
// =================================================================
// الخطوة 1: الاتصال بسيرفر الرحلات ($con_ride)
// الهدف: جلب driver_id وحالة الرحلة للتحقق
// =================================================================
$sqlRide = "SELECT driver_id, status FROM ride WHERE id = :rideID LIMIT 1";
$stmtRide = $con_ride->prepare($sqlRide);
$stmtRide->bindParam(':rideID', $rideID);
$stmtRide->execute();
$rideData = $stmtRide->fetch(PDO::FETCH_ASSOC);
// إذا لم توجد الرحلة
if (!$rideData) {
echo json_encode(["status" => "failure", "message" => "Ride not found"]);
exit;
}
$driverID = $rideData['driver_id'];
$status = $rideData['status'];
// =================================================================
// الخطوة 2: التحقق الأمني (Hashing Validation)
// القاعدة: Token = MD5(rideID + driverID + SecretSalt)
// هذا يضمن أن الرابط تم توليده بواسطة التطبيق ولم يتم تخمينه
// =================================================================
// * هام: هذه الكلمة السرية يجب أن تكون مطابقة تماماً للموجودة في تطبيق Flutter
$secretSalt = getenv("secretSaltParent");
// إعادة بناء الهاش للمقارنة (HMAC-SHA256 بدلاً من MD5)
$generatedToken = hash_hmac('sha256', $rideID . $driverID, $secretSalt);
if ($token !== $generatedToken) {
http_response_code(403);
echo json_encode(["status" => "failure", "message" => "Invalid Security Token"]);
exit;
}
// =================================================================
// الخطوة 3: التحقق من حالة الرحلة (Logic Check)
// الشرط: التتبع يعمل فقط إذا كانت الرحلة قد بدأت
// =================================================================
// يمكنك إضافة 'Applied' أو 'Arrived' إذا أردت التتبع قبل الركوب
$allowedStatuses = ['Begin', 'inProgress'];
if (!in_array($status, $allowedStatuses)) {
echo json_encode(["status" => "failure", "message" => "Ride is not active", "ride_status" => $status]);
exit;
}
// =================================================================
// الخطوة 4: الاتصال بسيرفر التتبع ($con_tracking)
// الهدف: جلب أحدث إحداثيات للسائق
// =================================================================
$sqlLoc = "SELECT latitude, longitude, heading, speed, updated_at
FROM car_locations
WHERE driver_id = :driverID
ORDER BY updated_at DESC LIMIT 1";
$stmtLoc = $con_tracking->prepare($sqlLoc);
$stmtLoc->bindParam(':driverID', $driverID);
$stmtLoc->execute();
$locData = $stmtLoc->fetch(PDO::FETCH_ASSOC);
if (!$locData) {
// السائق لم يرسل موقعه بعد
echo json_encode(["status" => "failure", "message" => "Waiting for driver signal..."]);
exit;
}
// =================================================================
// الخطوة 5: الاتصال بالسيرفر الرئيسي ($con)
// الهدف: جلب اسم السائق وموديل السيارة للعرض (اختياري لجمالية الصفحة)
// =================================================================
$sqlDriver = "SELECT
d.first_name,
d.last_name,
c.model,
c.color,
c.car_plate
FROM driver d
LEFT JOIN CarRegistration c ON d.id = c.driverID
WHERE d.id = :driverID LIMIT 1";
$stmtDriver = $con->prepare($sqlDriver);
$stmtDriver->bindParam(':driverID', $driverID);
$stmtDriver->execute();
$driverInfo = $stmtDriver->fetch(PDO::FETCH_ASSOC);
// فك التشفير إذا لزم الأمر (أسماء السائقين واللوحات غالباً مشفرة)
if ($driverInfo) {
// فك تشفير الاسم
if (!empty($driverInfo['first_name'])) {
$driverInfo['first_name'] = $encryptionHelper->decryptData($driverInfo['first_name']);
}
// فك تشفير اللوحة
if (!empty($driverInfo['car_plate'])) {
$driverInfo['car_plate'] = $encryptionHelper->decryptData($driverInfo['car_plate']);
}
// يمكنك فك تشفير باقي الحقول حسب الحاجة
}
// =================================================================
// الخطوة 6: تجميع البيانات وإرسال الرد النهائي
// =================================================================
$response = [
"status" => "success",
"data" => [
"lat" => $locData['latitude'],
"lng" => $locData['longitude'],
"heading" => $locData['heading'],
"speed" => $locData['speed'],
"last_update" => $locData['updated_at'],
"driver_name" => $driverInfo['first_name'] ?? "Captain",
"car_model" => $driverInfo['model'] ?? "",
"car_color" => $driverInfo['color'] ?? "",
"plate" => $driverInfo['car_plate'] ?? ""
]
];
echo json_encode($response);
} catch (Exception $e) {
// تسجيل الخطأ دون إظهاره للمستخدم العام
error_log("Tracking Error: " . $e->getMessage());
echo json_encode(["status" => "failure", "message" => "Server Error"]);
}
?>
@@ -0,0 +1,32 @@
<?php
require_once __DIR__ . '/../../connect.php';
// لا حاجة للمتغير id لأنه غير مستخدم فعليًا
// $id = filterRequest("id");
$sql = "
SELECT
(
SELECT COUNT(*)
FROM `ride`
WHERE `status` = 'Finished'
AND `created_at` BETWEEN CURRENT_DATE() + INTERVAL 7 HOUR AND CURRENT_DATE() + INTERVAL 10 HOUR
) AS morning_count,
(
SELECT COUNT(*)
FROM `ride`
WHERE `status` = 'Finished'
AND `created_at` BETWEEN CURRENT_DATE() + INTERVAL 15 HOUR AND CURRENT_DATE() + INTERVAL 18 HOUR
) AS afternoon_count
";
$stmt = $con->prepare($sql);
$stmt->execute();
$row = $stmt->fetch(PDO::FETCH_ASSOC);
echo json_encode([
"status" => "success",
"data" => $row
]);
?>
+7
View File
@@ -0,0 +1,7 @@
[
{"lat": 33.5100, "lng": 36.2700, "count": 5, "intensity": "high", "surge": 1.5},
{"lat": 33.5200, "lng": 36.2800, "count": 3, "intensity": "medium", "surge": 1.2},
{"lat": 33.5150, "lng": 36.2750, "count": 2, "intensity": "normal", "surge": 1.0},
{"lat": 33.5050, "lng": 36.2650, "count": 6, "intensity": "high", "surge": 1.5},
{"lat": 33.5250, "lng": 36.2850, "count": 1, "intensity": "low", "surge": 1.0}
]
@@ -0,0 +1,100 @@
<?php
// ابدأ التخزين المؤقت فوراً
ob_start();
require_once __DIR__ . '/../../get_connect.php';
// تنظيف *جميع* مستويات التخزين المؤقت (Loop)
// هذا يضمن التخلص من أي مسافات أو أخطاء ظهرت من ملفات الـ include
while (ob_get_level()) {
ob_end_clean();
}
// ابدأ مخزناً جديداً ونظيفاً لهذا الملف فقط
ob_start();
header("Content-Type: application/json; charset=UTF-8");
function sendError($message, $code = 400, $extra = []) {
// تنظيف ما قبل الخطأ
ob_clean();
http_response_code($code);
echo json_encode(array_merge(["status" => "failure", "message" => $message], $extra));
exit;
}
try {
$rideID = filter_input(INPUT_GET, 'id', FILTER_SANITIZE_NUMBER_INT);
$token = filter_input(INPUT_GET, 'token', FILTER_SANITIZE_SPECIAL_CHARS);
if (!$rideID || !$token) {
sendError("Missing parameters");
}
$stmtRide = $con_ride->prepare("SELECT driver_id, status FROM ride WHERE id = ? LIMIT 1");
$stmtRide->execute([$rideID]);
$rideData = $stmtRide->fetch(PDO::FETCH_ASSOC);
if (!$rideData) sendError("Ride not found");
$driverID = $rideData['driver_id'];
$status = $rideData['status'];
// ✅ FIX H-03: استبدال md5 بـ hash_hmac
$secretSalt = getenv('TRACKING_SECRET_SALT') ;
$generatedToken = hash_hmac('sha256', $rideID . $driverID, $secretSalt);
if ($token !== $generatedToken) sendError("Invalid Token");
$allowedStatuses = ['Applied', 'Arrived', 'Begin', 'inProgress'];
if (!in_array($status, $allowedStatuses)) {
sendError("Ride not active", 200, ["current_status" => $status]);
}
$stmtLoc = $con_tracking->prepare("SELECT latitude, longitude, heading, speed, updated_at FROM car_locations WHERE driver_id = ? ORDER BY updated_at DESC LIMIT 1");
$stmtLoc->execute([$driverID]);
$locData = $stmtLoc->fetch(PDO::FETCH_ASSOC);
if (!$locData) sendError("Waiting for driver signal...", 200);
$stmtDriver = $con->prepare("SELECT d.first_name, c.model, c.color, c.car_plate FROM driver d LEFT JOIN CarRegistration c ON d.id = c.driverID WHERE d.id = ? LIMIT 1");
$stmtDriver->execute([$driverID]);
$driverInfo = $stmtDriver->fetch(PDO::FETCH_ASSOC);
$driverName = "Captain";
$carModel = "Car";
$carColor = "";
$plate = "";
if ($driverInfo) {
if (!empty($driverInfo['first_name'])) $driverName = $encryptionHelper->decryptData($driverInfo['first_name']);
if (!empty($driverInfo['model'])) $carModel = $driverInfo['model'];
if (!empty($driverInfo['color'])) $carColor = $driverInfo['color'];
if (!empty($driverInfo['car_plate'])) $plate = $encryptionHelper->decryptData($driverInfo['car_plate']);
}
$response = [
"status" => "success",
"data" => [
"lat" => $locData['latitude'],
"lng" => $locData['longitude'],
"heading" => $locData['heading'],
"speed" => $locData['speed'],
"last_update" => $locData['updated_at'],
"driver_name" => $driverName,
"car_model" => $carModel,
"car_color" => $carColor,
"plate" => $plate,
"ride_status" => $status
]
];
// التنظيف النهائي قبل الطباعة
ob_clean();
echo json_encode($response);
} catch (Exception $e) {
error_log("Tracking API Error: " . $e->getMessage());
sendError("Server Error");
}
+298
View File
@@ -0,0 +1,298 @@
<?php
// ═══════════════════════════════════════════════════════════════════════════
// 🤖 AI Negotiator — نظام مفاوضة الذكاء الاصطناعي
//
// سياسة التشغيل:
// Phase 1 — T=20s (تلقائي، بصمت):
// Flutter يستدعي هذا الملف بعد 20 ثانية بدون قبول.
// النظام يزيد حصة السائق من عمولة التطبيق كجسر مؤقت.
// الراكب لا يرى شيئاً.
//
// Phase 2 — T=45s (بموافقة الراكب):
// Flutter يعرض Dialog يقترح زيادة 5% من الراكب.
// إذا قبل: يُرسل new_price جديد لهذا الملف.
// السعر الجديد يُطبَّق على الراكب والسائق معاً.
//
// الإرسال: dispatchRideToDrivers() يرسل عبر WebSocket أولاً ثم FCM
// (راجع functions.php — sendSocketNotification + sendFcmNotification)
// ═══════════════════════════════════════════════════════════════════════════
require_once __DIR__ . '/../../connect.php';
// 1. استقبال البيانات القادمة من الفلتر
$rideId = filterRequest("ride_id");
$passengerId = filterRequest("passenger_id");
$passengerName = filterRequest("passenger_name");
$passengerPhone = filterRequest("passenger_phone");
$passengerEmail = filterRequest("passenger_email");
$passengerToken = filterRequest("passenger_token");
$passengerWallet = filterRequest("passenger_wallet");
$isWallet = filterRequest("is_wallet");
$passengerRating = filterRequest("passenger_rating");
$startLat = filterRequest("start_lat");
$startLng = filterRequest("start_lng");
$endLat = filterRequest("end_lat");
$endLng = filterRequest("end_lng");
$startName = filterRequest("start_name");
$endName = filterRequest("end_name");
$distance = filterRequest("distance");
$distanceText = filterRequest("distance_text");
$durationText = filterRequest("duration_text");
$price = filterRequest("price");
$priceForDriver = filterRequest("price_for_driver");
$carType = filterRequest("car_type");
$isDestinationMatchFlag = filterRequest("is_destination_match") ?: "0";
$hasSteps = filterRequest("has_steps");
$step0 = filterRequest("step0");
$step1 = filterRequest("step1");
$step2 = filterRequest("step2");
$step3 = filterRequest("step3");
$step4 = filterRequest("step4");
// Phase 2: إذا أرسل الراكب new_price (وافق على الزيادة المقترحة من الـ AI)
// هذا يتجاوز Phase 1 ويطبق السعر الجديد مباشرةً على الراكب والسائق
$newPriceFromPassenger = filterRequest("new_price");
if ($newPriceFromPassenger && (float)$newPriceFromPassenger > (float)$price) {
$price = $newPriceFromPassenger;
// نحافظ على نسبة الكيزان (عمولة التطبيق) الأصلية — الزيادة للراكب والسائق معاً
$originalRatio = ((float)$priceForDriver > 0 && (float)$priceForDriver < (float)$price)
? (float)$priceForDriver / (float)$price
: 0.86; // default 86% للسائق
$priceForDriver = number_format((float)$price * $originalRatio, 2, '.', '');
error_log("[AI Negotiator Phase 2] Passenger accepted price increase: old=$price → new={$newPriceFromPassenger}");
}
if (!$rideId) {
jsonError("Missing Ride ID");
exit;
}
// ═══════════════════════════════════════════════════════════════════════════
// 🤖 STEP 0: AI Negotiator — Read & Increment Rejection Counter (Redis-first)
// ═══════════════════════════════════════════════════════════════════════════
$rejectionCount = 0;
$aiBonus = 0.0;
$aiRedisKey = "ai:ride_rejections:{$rideId}";
$originalKazan = (float)$price - (float)$priceForDriver;
// Bonus Tiers: after N rejections, give driver this much extra from our commission
// These values come from our share (kazan), never from the passenger's price
$bonusTiers = [
1 => 0.05, // 1st retry: 5% of price → added to driver share
2 => 0.10, // 2nd retry: 10%
3 => 0.15, // 3rd retry: 15%
];
$maxBonusFraction = 0.20; // Cap: never give away more than 20% of price as bonus
try {
// Read current rejection count from Redis (O(1))
if (isset($redis)) {
$storedCount = $redis->get($aiRedisKey);
if ($storedCount !== false) {
$rejectionCount = (int)$storedCount;
}
// Increment rejection count (this retry = another rejection)
$rejectionCount++;
$redis->setex($aiRedisKey, 3600, $rejectionCount); // TTL: 1 hour
}
// Calculate bonus based on rejection tier
$tierKey = min($rejectionCount, max(array_keys($bonusTiers)));
$bonusFraction = $bonusTiers[$tierKey] ?? $bonusTiers[max(array_keys($bonusTiers))];
$bonusFraction = min($bonusFraction, $maxBonusFraction);
$aiBonus = round((float)$price * $bonusFraction, 2);
// Never give more than what we actually earn as commission (kazan)
if ($aiBonus > $originalKazan) {
$aiBonus = round($originalKazan * 0.80, 2); // Keep 20% for ourselves minimum
}
// Apply bonus: increase driver's price, reduce our kazan
if ($aiBonus > 0) {
$priceForDriver = number_format((float)$priceForDriver + $aiBonus, 2, '.', '');
error_log("[AI Negotiator] RideID={$rideId} | Rejection #{$rejectionCount} | Bonus={$aiBonus} | New priceForDriver={$priceForDriver}");
}
} catch (Exception $e) {
error_log("[AI Negotiator] Redis error: " . $e->getMessage());
// Non-fatal: continue without bonus
}
try {
// 2. تحديث حالة الرحلة + تسجيل الـ bonus في قاعدة البيانات
$updateStmt = $con->prepare("
UPDATE ride
SET status = 'waiting',
driver_id = 0,
updated_at = NOW(),
ai_negotiated_bonus = :bonus,
price_for_driver = :new_driver_price
WHERE id = :id
");
$updateStmt->execute([
':bonus' => $aiBonus,
':new_driver_price' => $priceForDriver,
':id' => $rideId,
]);
// 🆕 Refresh Redis cache — this UPDATE just reset status back to 'waiting'
// and driver_id to 0. Without this, a passenger polling during a retry
// round would keep seeing a stale prior state (e.g. 'arrived') cached
// from before the reset.
sendToLocationServer('update_ride_state', [
'ride_id' => $rideId,
'status' => 'waiting',
'driver_id' => '0',
'passenger_id' => $passengerId,
]);
// 3. حساب العمولة (Kazan) المُحدَّثة بعد الـ bonus
$kazan = (float)$price - (float)$priceForDriver;
if ($kazan < 0) $kazan = 0; // Floor protection
$passengerFp = isset($_SERVER['HTTP_X_DEVICE_FP']) ? $_SERVER['HTTP_X_DEVICE_FP'] : '';
// 4. بناء Payload (0 - 33) — مطابق لـ add_ride.php
$payloadTemplate = [];
$payloadTemplate[0] = (string)$startLat;
$payloadTemplate[1] = (string)$startLng;
$payloadTemplate[2] = (string)number_format((float)$price, 2, '.', '');
$payloadTemplate[3] = (string)$endLat;
$payloadTemplate[4] = (string)$endLng;
$payloadTemplate[5] = (string)$distanceText;
$payloadTemplate[6] = (string)$passengerFp;
$payloadTemplate[7] = (string)$passengerId;
$payloadTemplate[8] = (string)$passengerName;
$payloadTemplate[9] = (string)$passengerToken;
$payloadTemplate[10] = (string)$passengerPhone;
$payloadTemplate[11] = (string)$distance;
$payloadTemplate[12] = "1";
$payloadTemplate[13] = (string)$isWallet;
$payloadTemplate[14] = (string)$distance;
$payloadTemplate[15] = (string)$durationText;
$payloadTemplate[16] = (string)$rideId;
$payloadTemplate[17] = "";
$payloadTemplate[18] = ""; // Driver ID placeholder
$payloadTemplate[19] = (string)$durationText;
$payloadTemplate[20] = (string)$hasSteps;
$payloadTemplate[21] = (string)$step0;
$payloadTemplate[22] = (string)$step1;
$payloadTemplate[23] = (string)$step2;
$payloadTemplate[24] = (string)$step3;
$payloadTemplate[25] = (string)$step4;
$payloadTemplate[26] = (string)number_format((float)$priceForDriver, 2, '.', ''); // ← Updated with bonus
$payloadTemplate[27] = (string)$passengerWallet;
$payloadTemplate[28] = (string)$passengerEmail;
$payloadTemplate[29] = (string)$startName;
$payloadTemplate[30] = (string)$endName;
$payloadTemplate[31] = (string)$carType;
// 👑 Check Prime Status from Redis
$isPrimeFlag = "0";
if (isset($redis)) {
try {
$cachedPrime = $redis->get("prime:passenger:{$passengerId}");
if ($cachedPrime) {
$primeData = json_decode($cachedPrime, true);
if (isset($primeData['is_prime']) && $primeData['is_prime'] == 1) {
$isPrimeFlag = "1";
}
}
} catch (Exception $e) {}
}
$payloadTemplate[32] = (string)number_format($kazan, 2, '.', ''); // ← Reduced kazan
$payloadTemplate[33] = (string)$passengerRating;
$payloadTemplate[34] = $isPrimeFlag; // 👑 Index 34: Prime Status
ksort($payloadTemplate);
$payloadTemplate = array_values($payloadTemplate);
// 5. البحث عن السائقين وإرسال الطلب
$latVal = doubleval($startLat);
$lngVal = doubleval($startLng);
$driversData = findBestDrivers($con, $latVal, $lngVal, $carType, $endLat, $endLng);
// ═══════════════════════════════════════════════════════════════
// 🆕 Destination Matching Priority — Multi-Driver (Retry Round)
// السياسة: لما ما في أحد يقبل الرحلة بعد 20 ثانية، Flutter
// يستدعي هذا الملف تلقائياً (AI Negotiator يزيد حصة السائق)
// في نفس الوقت، نضع السائقين ذوي الوجهة المطابقة في المقدمة
// بدون خصم إضافي (الخصم 14% تم مسبقاً في التسعير)
// إذا كان أكثر من سائق يملك نفس المنطقة كوجهة، نرسل للجميع
// ═══════════════════════════════════════════════════════════════
if ($isDestinationMatchFlag === "1" && isset($redis)) {
try {
$allMatchedIds = $redis->geoRadius(
'geo:driver:destinations',
(float)$endLng,
(float)$endLat,
3.5,
'km',
['COUNT' => 10, 'ASC']
);
if (!empty($allMatchedIds)) {
$alreadyInList = [];
foreach ($driversData as $d) {
$alreadyInList[$d['captain_id'] ?? $d['driver_id'] ?? ''] = true;
}
$matchedToFront = [];
foreach ($allMatchedIds as $mid) {
$mid = (string)$mid;
// Validate still active today
$detailJson = $redis->get("driver:destination:{$mid}");
$detail = $detailJson ? json_decode($detailJson, true) : null;
if (!$detail || empty($detail['is_active']) || ($detail['usage_date'] ?? '') !== date('Y-m-d')) {
$redis->zRem('geo:driver:destinations', $mid);
continue;
}
if (isset($alreadyInList[$mid])) {
foreach ($driversData as $k => $d) {
$did = $d['captain_id'] ?? $d['driver_id'] ?? '';
if ((string)$did === $mid) {
$driversData[$k]['is_destination_match'] = 1;
$matchedToFront[] = $driversData[$k];
unset($driversData[$k]);
break;
}
}
} else {
$stmtD = $con->prepare("SELECT token FROM driverToken WHERE captain_id = :d LIMIT 1");
$stmtD->execute([':d' => $mid]);
$dT = $stmtD->fetchColumn();
if ($dT) {
$matchedToFront[] = [
'captain_id' => $mid,
'driver_id' => $mid,
'token' => $dT,
'is_destination_match' => 1
];
}
}
}
$driversData = array_values($driversData);
$driversData = array_merge($matchedToFront, $driversData);
error_log("[retry] " . count($matchedToFront) . " destination-matched driver(s) moved to front.");
}
} catch (Exception $e) {
error_log("[retry] Destination priority error: " . $e->getMessage());
}
}
if (!empty($driversData)) {
dispatchRideToDrivers($driversData, $rideId, $payloadTemplate, $startName, $encryptionHelper);
}
// Return with bonus info so Flutter can log it
jsonSuccess([
'rejection_count' => $rejectionCount,
'ai_bonus_applied' => $aiBonus,
'new_driver_price' => $priceForDriver,
], "Ride reset and resent to drivers");
} catch (PDOException $e) {
error_log("[retry_search_drivers] " . $e->getMessage());
jsonError("DB Error");
}
?>
+163
View File
@@ -0,0 +1,163 @@
<?php
// start_ride.php
require_once __DIR__ . '/../../connect.php';
try {
$con_ride = Database::get('ride');
} catch (Exception $e) {
error_log("[start_ride] Failed to connect to Ride Database: " . $e->getMessage());
}
$ride_id = filterRequest("id");
$driver_id = filterRequest("driver_id");
$status = filterRequest("status"); // 'Begin'
$passengerToken = filterRequest("passengerToken");
if (!$ride_id || !$driver_id || !$status) {
jsonError("Missing parameters");
exit;
}
try {
// 1. تحديث سيرفر الرحلات (Remote DB - con_ride)
$stmtRemote = $con_ride->prepare("UPDATE ride SET status = ?, rideTimeStart = NOW() WHERE id = ?");
$stmtRemote->execute([$status, $ride_id]);
if ($stmtRemote->rowCount() == 0) {
// ملاحظة: أحياناً التحديث لا يؤثر بصفوف إذا كانت البيانات نفسها،
// لكن هنا نفترض الفشل إذا لم يجد الرحلة.
// يمكنك إكمال التنفيذ إذا كنت متأكداً أن الرحلة موجودة.
}
// 2. تحديث السيرفر المحلي (Local DB) والمعاملات
$con->beginTransaction();
// تحديث الرحلة محلياً
$stmtMainRide = $con->prepare("UPDATE ride SET status = ?, rideTimeStart = NOW() WHERE id = ?");
$stmtMainRide->execute([$status, $ride_id]);
// 🆕 Immutable Fare Lock: Save agreed fixed price in Redis
try {
$stmtRideInfo = $con->prepare("SELECT price, car_type FROM ride WHERE id = ? LIMIT 1");
$stmtRideInfo->execute([$ride_id]);
$rideInfo = $stmtRideInfo->fetch(PDO::FETCH_ASSOC);
if ($rideInfo) {
$agreedPrice = floatval($rideInfo['price']);
$carTypeStr = $rideInfo['car_type'] ?? 'Fixed Price';
$fixedPriceTypes = ['Speed', 'Fixed Price', 'Awfar Car'];
if (in_array($carTypeStr, $fixedPriceTypes)) {
global $redis;
if ($redis) {
$redis->setex("ride_locked_price_{$ride_id}", 86400, $agreedPrice);
error_log("[start_ride] Locked Fixed Price for ride {$ride_id}: {$agreedPrice}");
} else {
error_log("[start_ride] Failed to lock price: Global Redis instance not available.");
}
}
}
} catch (Exception $e) {
error_log("[start_ride] Redis Error (locking price): " . $e->getMessage());
}
// تحديث أو إدخال في جدول Driver Orders
$checkSql = "SELECT `order_id` FROM `driver_orders` WHERE `order_id` = ?";
$checkStmt = $con->prepare($checkSql);
$checkStmt->execute([$ride_id]);
if ($checkStmt->rowCount() > 0) {
$updateSql = "UPDATE `driver_orders` SET `driver_id` = ?, `status` = ?, `created_at` = NOW() WHERE `order_id` = ?";
$con->prepare($updateSql)->execute([$driver_id, $status, $ride_id]);
} else {
$insertSql = "INSERT INTO `driver_orders` (`driver_id`, `order_id`, `created_at`, `status`) VALUES (?, ?, NOW(), ?)";
$con->prepare($insertSql)->execute([$driver_id, $ride_id, $status]);
}
// =================================================================
// 🔥 الخطوة 3: إشعار الراكب (Socket + FCM)
// =================================================================
// جلب بيانات الراكب من قاعدة البيانات لضمان الدقة
$stmtPas = $con_ride->prepare("SELECT passenger_id FROM ride WHERE id = ?");
$stmtPas->execute([$ride_id]);
$passenger_id = $stmtPas->fetchColumn();
// 2.5 تصفير الدين من Redis عند بدء الرحلة (كما طلبت)
if ($passenger_id) {
try {
global $redis;
if ($redis) {
$redisKey = "passenger_debt_" . $passenger_id;
// قراءة الدين الحالي من Redis قبل الحذف (إن لزم الأمر للتسجيل مستقبلاً)
$currentDebt = (float) $redis->get($redisKey);
// تصفير / حذف الدين
$redis->del($redisKey);
// يمكنك هنا أيضاً إدراج حركة معاكسة في جدول passengerWallet إذا أردت تسوية قاعدة البيانات
if ($currentDebt < 0) {
$positiveOffset = abs($currentDebt);
$stmtWallet = $con->prepare("INSERT INTO `passengerWallet` (passenger_id, balance) VALUES (?, ?)");
$stmtWallet->execute([$passenger_id, $positiveOffset]);
}
}
} catch (Exception $e) {
error_log("Redis Error (zeroing debt): " . $e->getMessage());
}
}
if ($passenger_id) {
// أ) إرسال السوكيت (Socket)
// تم إلغاء التعليق عنه ليكون السيرفر هو المسؤول
$socketPayload = [
'ride_id' => $ride_id,
'status' => 'started', // أو 'Begin' حسب ما يتوقعه التطبيق
'msg' => 'بدأت الرحلة، نتمنى لك سلامة الوصول 🚀'
];
if (function_exists('notifyPassengerOnRideServer')) {
notifyPassengerOnRideServer($passenger_id, $socketPayload);
}
// ب) إرسال FCM (Internal)
if (!empty($passengerToken)) {
$fcmData = [
'category' => 'Trip is Begin',
'ride_id' => (string)$ride_id,
'status' => 'started'
];
// 🔥 استخدام sendFCM_Internal كرسالة صامتة
sendFCM_Internal(
$passengerToken, // الهدف
"", // تفريغ العنوان
"", // تفريغ النص
$fcmData, // البيانات
"Trip is Begin", // التصنيف
false // ليس Topic
);
}
}
$con->commit();
// 🆕 Cache ride state in Redis — use $status (the exact value just
// written to MySQL above), not a hardcoded label, so a cache hit can
// never disagree with the row it was seeded from.
sendToLocationServer('update_ride_state', [
'ride_id' => $ride_id,
'status' => $status,
'driver_id' => $driver_id,
'passenger_id' => $passenger_id ?? '',
]);
jsonSuccess(null, "Ride started successfully");
} catch (PDOException $e) {
if ($con->inTransaction()) {
$con->rollBack();
}
jsonError("An internal error occurred. Please try again later.");
}
?>
+76
View File
@@ -0,0 +1,76 @@
<?php
// backend/ride/rides/streak_helper.php
function handleDriverStreak($con, $driver_id, $action) {
try {
if ($action === 'reset') {
$stmt = $con->prepare("
INSERT INTO driver_streaks (driver_id, current_streak, zero_commission_until)
VALUES (?, 0, NULL)
ON DUPLICATE KEY UPDATE current_streak = 0
");
$stmt->execute([$driver_id]);
} elseif ($action === 'increment') {
$stmt = $con->prepare("SELECT current_streak FROM driver_streaks WHERE driver_id = ?");
$stmt->execute([$driver_id]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
$current = $row ? intval($row['current_streak']) + 1 : 1;
$zero_until = NULL;
if ($current >= 5) {
// Reward unlocked! 0% commission until end of today
$zero_until = date('Y-m-d 23:59:59');
$current = 0; // reset for next streak
}
$stmt2 = $con->prepare("
INSERT INTO driver_streaks (driver_id, current_streak, best_streak, zero_commission_until)
VALUES (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
current_streak = ?,
best_streak = GREATEST(best_streak, ?),
zero_commission_until = COALESCE(?, zero_commission_until)
");
$stmt2->execute([
$driver_id,
$current,
$current,
$zero_until,
$current,
$current,
$zero_until
]);
}
// 🆕 Invalidate Redis Cache (Cache-Aside pattern)
global $redis;
if (isset($redis)) {
try {
$redis->del("driver_streak:{$driver_id}");
$redis->del("gamification_dashboard:{$driver_id}");
} catch (Exception $e) {
error_log("[streak_helper] Redis Cache Invalidation Error: " . $e->getMessage());
}
}
} catch (Exception $e) {
error_log("[streak_helper] Error: " . $e->getMessage());
}
}
function hasZeroCommission($con, $driver_id) {
try {
$stmt = $con->prepare("SELECT zero_commission_until FROM driver_streaks WHERE driver_id = ?");
$stmt->execute([$driver_id]);
$row = $stmt->fetch(PDO::FETCH_ASSOC);
if ($row && !empty($row['zero_commission_until'])) {
return (strtotime($row['zero_commission_until']) > time());
}
} catch (Exception $e) {
error_log("[streak_helper] Error checking commission: " . $e->getMessage());
}
return false;
}
?>
+89
View File
@@ -0,0 +1,89 @@
<?php
require_once __DIR__ . '/../../connect.php';
// 🚀 تسجيل بداية العملية
error_log("🚀 [update.php] Request Started to update Ride Dynamic Data.");
$id = filterRequest("id");
if (!$id) {
error_log("❌ [update.php] Missing ID.");
jsonError("Missing ID");
exit;
}
$columnValues = [];
$params = [':id' => $id];
// قائمة الحقول القابلة للتحديث
$fields = [
"start_location", "end_location", "date", "time", "endtime", "price",
"passenger_id", "driver_id", "status", "created_at", "updated_at",
"rideTimeStart", "rideTimeFinish", "price_for_driver", "driverGoToPassengerTime",
"price_for_passenger", "distance"
];
// بناء الاستعلام ديناميكياً باستخدام filterRequest
foreach ($fields as $field) {
// نتحقق من وجود المفتاح في الـ POST
if (isset($_POST[$field])) {
// نستخدم دالة الفلترة الخاصة بك
$value = filterRequest($field);
$columnValues[] = "`$field` = :$field";
$params[":$field"] = $value;
}
}
// إذا لم يتم إرسال أي حقول للتحديث
if (empty($columnValues)) {
error_log("⚠️ [update.php] No data provided in request to update.");
jsonError("No data provided for update.");
exit;
}
// تجميع جملة SQL
$setClause = implode(", ", $columnValues);
$sql = "UPDATE `ride` SET $setClause WHERE `id` = :id";
try {
// ---------------------------------------------------------
// 1. التحديث على سيرفر التتبع (Remote DB) - هو الأساس
// ---------------------------------------------------------
error_log("🔄 [update.php] Attempting to update REMOTE Tracking DB for Ride ID: $id");
$stmtRemote = $con_ride->prepare($sql);
$stmtRemote->execute($params);
$count = $stmtRemote->rowCount();
error_log("ℹ️ [update.php] Remote DB Rows Affected: $count");
// التحقق: هل نجح التحديث هناك؟
if ($count > 0) {
// ---------------------------------------------------------
// 2. التحديث على السيرفر المحلي (Local DB) للمطابقة
// ---------------------------------------------------------
error_log("🔄 [update.php] Remote success. Updating LOCAL Main DB...");
$stmtLocal = $con->prepare($sql);
$stmtLocal->execute($params);
error_log("✅ [update.php] Update successful on both servers.");
// استخدام دالة النجاح الخاصة بك
jsonSuccess(null, "Ride data updated successfully");
} else {
// لم يتم التحديث (إما البيانات نفسها لم تتغير، أو المعرف غير موجود في السيرفر البعيد)
error_log("⚠️ [update.php] Remote Update returned 0 rows (Data same or ID not found).");
// استخدام دالة الفشل (يمكنك تغيير الرسالة لتكون success إذا كنت لا تعتبر عدم تغيير البيانات خطأ)
jsonError("No changes made (Remote DB affected 0 rows). Check ID or Data.");
}
} catch (PDOException $e) {
error_log("❌ [update.php] Database Error: " . $e->getMessage());
jsonError("Database Error");
}
?>
@@ -0,0 +1,47 @@
<?php
require_once __DIR__ . '/../../connect.php';
$rideId = filterRequest("id");
$rideTimeStart = filterRequest("rideTimeStart");
$driverId = filterRequest("driver_id");
// Step 1: تأكد أن الرحلة غير محجوزة مسبقًا
$sqlCheck = "SELECT `status` FROM `ride` WHERE `id` = :rideId LIMIT 1";
$stmtCheck = $con->prepare($sqlCheck);
$stmtCheck->bindParam(":rideId", $rideId);
$stmtCheck->execute();
$ride = $stmtCheck->fetch(PDO::FETCH_ASSOC);
if (!$ride) {
jsonError("Ride not found.");
exit;
}
if ($ride['status'] === 'Apply') {
jsonError("This ride is already applied by another driver.");
exit;
}
// Step 2: تحديث حالة الرحلة وربط السائق بها
$sqlUpdate = "UPDATE `ride`
SET `driver_id` = :driverId,
`status` = 'Apply',
`rideTimeStart` = :rideTimeStart
WHERE `id` = :rideId";
$stmtUpdate = $con->prepare($sqlUpdate);
$stmtUpdate->bindParam(":driverId", $driverId);
$stmtUpdate->bindParam(":rideTimeStart", $rideTimeStart);
$stmtUpdate->bindParam(":rideId", $rideId);
$stmtUpdate->execute();
if ($stmtUpdate->rowCount() > 0) {
jsonSuccess(null, "Ride data updated successfully");
// يمكنك هنا إرسال إشعار للسائقين الآخرين إذا أردت
// FirebaseMessagesController()->sendNotificationToOtherDrivers(...)
} else {
jsonError("Failed to update ride data.");
}
?>
@@ -0,0 +1,79 @@
<?php
//updateStausFromSpeed.php";
require_once __DIR__ . '/../../connect.php';
$rideId = filterRequest("id");
$status = filterRequest("status");
$driverId = filterRequest("driver_id");
// لم نعد نحتاج لمتغير $rideTimeStart لربطه بالـ SQL، لكن نتركه لاستلام القيمة من الطلب
$rideTimeStart = filterRequest("rideTimeStart");
// 🚀 1. تسجيل بداية الطلب والبيانات المستلمة
error_log("🚀 [accept_ride.php] Request Started. RideID: $rideId | DriverID: $driverId | Status: $status");
if (!$rideId || !$driverId || !$status) {
error_log("❌ [accept_ride.php] Missing required parameters.");
jsonError("Missing required parameters.");
exit;
}
try {
// ---------------------------------------------------------
// 1. التحديث على سيرفر التتبع (صاحب القرار)
// ---------------------------------------------------------
error_log("🔄 [accept_ride.php] Attempting to update REMOTE Tracking DB for Ride ID: $rideId");
$stmtRideRemote = $con_ride->prepare("UPDATE `ride`
SET `status` = :status,
`driver_id` = :driverId,
`rideTimeStart` = NOW()
WHERE `id` = :id
AND `status` IN ('waiting', 'wait')
");
$stmtRideRemote->execute([
':status' => $status,
':driverId' => $driverId,
':id' => $rideId
]);
$count = $stmtRideRemote->rowCount();
error_log("ℹ️ [accept_ride.php] Remote DB Rows Affected: $count");
// نتحقق: هل نجح التحديث في سيرفر التتبع؟
if ($count > 0) {
// ---------------------------------------------------------
// 2. التحديث على السيرفر الرئيسي (تثبيت السجل فقط)
// ---------------------------------------------------------
error_log("🔄 [accept_ride.php] Remote success. Updating LOCAL Main DB...");
$sqlUpdate = "UPDATE `ride`
SET `driver_id` = :driverId,
`status` = :status,
`rideTimeStart` = NOW()
WHERE id = :rideId
AND `status` IN ('waiting', 'wait') ";
$stmtUpdate = $con->prepare($sqlUpdate);
$stmtUpdate->bindParam(":driverId", $driverId);
$stmtUpdate->bindParam(":status", $status);
$stmtUpdate->bindParam(":rideId", $rideId);
$stmtUpdate->execute();
error_log("✅ [accept_ride.php] Ride accepted and started successfully for Driver: $driverId");
jsonSuccess(null, "Ride accepted and started successfully at " . date('Y-m-d H:i:s'));
} else {
error_log("⚠️ [accept_ride.php] Failed to accept ride. It might be already taken, canceled, or invalid status.");
jsonError("Ride cannot be accepted (Already taken, Canceled, or Invalid Status).");
}
} catch (PDOException $e) {
error_log("❌ [accept_ride.php] Database Error: " . $e->getMessage());
jsonError("An internal error occurred. Please try again later.");
}
?>
@@ -0,0 +1,30 @@
<?php
// update_ride_cancel_wait.php
// يوضع على السيرفر الرئيسي (Main Server)
require_once __DIR__ . '/../../connect.php';
$rideId = filterRequest("ride_id");
$driverId = filterRequest("driver_id");
$status = "CancelAfterWait";
try {
$con->beginTransaction();
// 1. تحديث جدول الرحلات
$stmtRide = $con->prepare("UPDATE ride SET status = ?, rideTimeStart = NOW() WHERE id = ?");
$stmtRide->execute([$status, $rideId]);
// 2. تحديث جدول طلبات السائقين
// نستخدم Check لضمان عدم تكرار التحديث إذا كان محدثاً مسبقاً
$stmtOrder = $con->prepare("UPDATE driver_orders SET status = ? WHERE order_id = ? AND driver_id = ?");
$stmtOrder->execute([$status, $rideId, $driverId]);
$con->commit();
jsonSuccess(null, "Ride status updated");
} catch (PDOException $e) {
$con->rollBack();
jsonError("DB Error");
}
?>