This commit is contained in:
Hamza-Ayed
2026-04-28 15:34:43 +03:00
parent 86de67a41d
commit f28264351e
2 changed files with 318 additions and 405 deletions

View File

@@ -1,185 +1,181 @@
<?php
// acceptRide.php
// ═══════════════════════════════════════════════════════════════
// driver/ride/accept_ride.php
// PURPOSE : قبول رحلة — ride DB هو المرجع، primary DB يتزامن بعده
// RACE : Optimistic lock عبر WHERE status IN ('waiting','wait')
// ═══════════════════════════════════════════════════════════════
// 1. Include Database Connection
// This file connects to both the Main DB ($con) and the Ride DB ($con_ride).
require_once __DIR__ . '/../../connect.php';
include "../../connect.php";
// 2. Input Validation & Filtering
$rideId = filterRequest("id");
$driverId = filterRequest("driver_id");
$status = filterRequest("status"); // Expected: 'Apply' or 'accepted'
// ── 1. Input & Validation ──────────────────────────────────────
$rideId = filterRequest("id");
$driverId = filterRequest("driver_id");
$status = filterRequest("status"); // القيمة التي يرسلها التطبيق: 'accepted'
$passengerToken = filterRequest("passengerToken");
// Log incoming data for debugging
error_log(" [ACCEPT_RIDE_TRY] RideID: '$rideId' | DriverID: '$driverId' | Status: '$status' | PToken: '$passengerToken'");
// Check if critical data is missing
if (!$rideId || !$driverId) {
error_log("⛔ [ACCEPT_RIDE_FAIL] Missing parameters.");
jsonError("Missing required parameters.");
if (empty($rideId) || empty($driverId)) {
printFailure("Missing required parameters");
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 {
// =================================================================================
// 3. 🔒 ATOMIC UPDATE (The Race Condition Solver)
// We attempt to update the ride status ONLY if it is currently 'waiting'.
// This prevents two drivers from accepting the same ride simultaneously.
// We execute this on the Remote/Ride Database first ($con_ride).
// =================================================================================
$stmtRemote = $con_ride->prepare("
UPDATE `ride`
SET `status` = ?, `driver_id` = ?, `rideTimeStart` = NOW()
WHERE `id` = ? AND `status` IN ('waiting', 'wait')
// ═══════════════════════════════════════════════════════════
// 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')
");
$stmtRemote->execute([$status, $driverId, $rideId]);
// Check if the update actually changed a row.
// If rowCount > 0, IT MEANS SUCCESS! This driver won the ride.
if ($stmtRemote->rowCount() > 0) {
// 4. Synchronization: Update Local Database
// Now that we secured the ride, we update the main server's DB ($con) to match.
if (isset($con)) {
$stmtLocal = $con->prepare("UPDATE `ride` SET `driver_id` = ?, `status` = ?, `rideTimeStart` = NOW() WHERE id = ?");
$stmtLocal->execute([$driverId, $status, $rideId]);
}
$stmtLock->execute([$status, $driverId, $rideId]);
// 5. Update/Insert Driver Orders Table
// This tracks the driver's history or active orders.
$checkSql = "SELECT `order_id` FROM `driver_orders` WHERE `order_id` = ?";
$checkStmt = $con->prepare($checkSql);
$checkStmt->execute([$rideId]);
if ($stmtLock->rowCount() === 0) {
// الرحلة غير متاحة — سائق آخر سبق أو الرحلة ألغيت
error_log("[accept_ride] RideID=$rideId not available for DriverID=$driverId (rowCount=0)");
printFailure("Ride not available");
exit;
}
if ($checkStmt->rowCount() > 0) {
// If entry exists, update it
$updateSql = "UPDATE `driver_orders` SET `driver_id` = ?, `status` = ?, `created_at` = NOW() WHERE `order_id` = ?";
$con->prepare($updateSql)->execute([$driverId, $status, $rideId]);
} else {
// If not, insert new record
$insertSql = "INSERT INTO `driver_orders` (`driver_id`, `order_id`, `created_at`, `status`) VALUES (?, ?, NOW(), ?)";
$con->prepare($insertSql)->execute([$driverId, $rideId, $status]);
}
error_log("[accept_ride] ride DB locked. RideID=$rideId → DriverID=$driverId");
// =================================================================
// 6. 👤 GET DRIVER INFO (For the Passenger)
// We need to fetch driver details (Car, Name, Rating) to show to the passenger.
// =================================================================
$driverInfo = [];
$sqlDetails = "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,
// ═══════════════════════════════════════════════════════════
// 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 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,
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 = ?";
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);
$stmtDetails = $con->prepare($sqlDetails);
$stmtDetails->execute([$driverId]);
$driverRawData = $stmtDetails->fetch(PDO::FETCH_ASSOC);
if ($driverRawData) {
// List of encrypted fields that need decryption
$fieldsToDecrypt = ['first_name', 'last_name', 'gender', 'phone', 'car_plate', 'token'];
foreach ($driverRawData as $key => $value) {
if (in_array($key, $fieldsToDecrypt) && !empty($value)) {
// Decrypt sensitive data
$driverInfo[$key] = $encryptionHelper->decryptData($value);
} else {
$driverInfo[$key] = $value;
}
}
// Format Full Name
$driverInfo['driverName'] = trim(($driverInfo['first_name'] ?? '') . ' ' . ($driverInfo['last_name'] ?? ''));
// Default rating if null
if (empty($driverInfo['ratingDriver'])) {
$driverInfo['ratingDriver'] = "5.0";
}
if ($driverRaw) {
$encryptedFields = ['first_name', 'last_name', 'gender', 'phone', 'car_plate', 'token'];
foreach ($driverRaw as $key => $value) {
$driverInfo[$key] = (in_array($key, $encryptedFields) && !empty($value))
? $encryptionHelper->decryptData($value)
: $value;
}
// =================================================================
// 7. 🔔 NOTIFY PASSENGER (Socket + FCM)
// Inform the passenger that a driver has been found.
// =================================================================
// Fetch Passenger ID based on Ride ID
$stmtPas = $con->prepare("SELECT passenger_id FROM ride WHERE id = ?");
$stmtPas->execute([$rideId]);
$passenger_id = $stmtPas->fetchColumn();
if ($passenger_id) {
// A. Send Socket Notification (Real-time update on map)
if (function_exists('notifyPassengerOnRideServer')) {
notifyPassengerOnRideServer($passenger_id, [
'status' => 'accepted',
'ride_id' => $rideId,
'driver_id' => $driverId,
'driver_info' => $driverInfo
]);
}
// B. Send FCM Notification (Push Notification)
if (!empty($passengerToken)) {
// Using the standardized FCM function
sendFCM_Internal(
$passengerToken,
"Ride Accepted 🚖", // Title
"Captain " . ($driverInfo['driverName'] ?? 'Driver') . " is coming to you.", // Body
['ride_id' => (string)$rideId, 'driver_info' => $driverInfo], // Data Payload
"Accepted Ride", // Category
false // Not a topic
);
}
}
// =================================================================
// 8. 🧹 MARKETPLACE CLEANUP (Notify Location Server)
// Crucial Step: We tell the Location Server that this ride is taken.
// The Location Server will:
// 1. Remove the ride from Redis (geo:rides:waiting).
// 2. Broadcast 'ride_taken' to other drivers to remove it from their screens.
// =================================================================
sendToLocationServer('ride_taken_event', [
'ride_id' => $rideId,
'taken_by_driver_id' => $driverId
]);
// 9. Final Response to the Driver App
echo json_encode([
"status" => "success",
"message" => "Ride Accepted",
"data" => $driverInfo
]);
} else {
// Failure: This means rowCount was 0.
// Reason: The ride status was NOT 'waiting' (another driver took it milliseconds ago).
error_log("⛔ [ACCEPT_RIDE_FAIL] Row count 0 for RideID: '$rideId'. Status wasn't 'waiting'/'wait' or ID is wrong.");
jsonError("Ride not available (Already taken)");
$driverInfo['driverName'] = trim(($driverInfo['first_name'] ?? '') . ' ' . ($driverInfo['last_name'] ?? ''));
$driverInfo['ratingDriver'] = $driverInfo['ratingDriver'] ?: "5.0";
}
} catch (Exception $e) {
// Handle unexpected errors
error_log("⛔ [ACCEPT_RIDE_EXCEPTION] " . $e->getMessage());
jsonError("Error: " . $e->getMessage());
}
?>
// ═══════════════════════════════════════════════════════════
// STEP E — جلب passenger_id وإرسال الإشعارات
// ═══════════════════════════════════════════════════════════
$passengerId = $con->prepare("SELECT passenger_id FROM ride WHERE id = ? LIMIT 1");
$passengerId->execute([$rideId]);
$passengerIdValue = $passengerId->fetchColumn();
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,
"تم قبول رحلتك",
"الكابتن " . ($driverInfo['driverName'] ?? '') . " في طريقه إليك",
['ride_id' => (string) $rideId, 'driver_info' => $driverInfo],
"ride_accepted",
false
);
}
}
// ═══════════════════════════════════════════════════════════
// STEP F — تنظيف السوق (أبلغ location server إن الرحلة محجوزة)
// ═══════════════════════════════════════════════════════════
sendToLocationServer('ride_taken_event', [
'ride_id' => $rideId,
'taken_by_driver_id' => $driverId,
]);
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");
}