Files
Siro/backend/ride/rides/cancel_ride_by_driver.php
T

289 lines
12 KiB
PHP

<?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");
// ‏مبلغ الغرامة لا يُقرأ من الطلب إطلاقاً. كان سابقاً filterRequest("penalty_fee")
// ‏فيُخصم من محفظة الراكب بلا سقف ولا تحقق. صار يُحسم من سياسة الخادم أدناه.
// ‏إن وصل المفتاح من عميل فهو إما نسخة قديمة أو محاولة عبث — نسجّله ونتجاهله.
if (filterRequest("penalty_fee") !== null && filterRequest("penalty_fee") !== '') {
error_log("[cancel_ride_by_driver] SECURITY: تجاهل penalty_fee من العميل"
. " (ride=$rideId driver=$driverId) — السياسة تُقرأ من kazan فقط");
}
// ‏سقف صلب فوق أي قيمة في قاعدة البيانات — يمنع خطأ إدارياً أو صفاً تالفاً
// ‏من تحويل الغرامة إلى خصم كارثي.
const CANCEL_PENALTY_ABSOLUTE_CAP = 50000.0;
// تثبيت الحالة
$statusText = "CancelFromDriverAfterApply";
if (!$rideId || !$driverId) {
jsonError("Missing parameters");
exit;
}
try {
$con->beginTransaction();
// ---------------------------------------------------------
// 0. التحقق من الملكية — الرحلة يجب أن تكون مُسندة لهذا السائق
// ‏بدون هذا الفحص كان أي driver_id يقدر يلغي أي رحلة ويغرّم راكبها.
// ---------------------------------------------------------
$stmtOwn = $con->prepare("SELECT driver_id FROM ride WHERE id = ? FOR UPDATE");
$stmtOwn->execute([$rideId]);
$assignedDriver = $stmtOwn->fetchColumn();
if ($assignedDriver === false) {
$con->rollBack();
jsonError("Ride not found");
exit;
}
if ((string) $assignedDriver !== (string) $driverId) {
$con->rollBack();
error_log("[cancel_ride_by_driver] SECURITY: رفض إلغاء رحلة غير مُسندة"
. " (ride=$rideId caller=$driverId assigned=$assignedDriver)");
jsonError("Ride is not assigned to this driver");
exit;
}
// ---------------------------------------------------------
// 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. غرامة الإلغاء على الراكب — المبلغ من سياسة الخادم لا من العميل
// Resolve country + penalty policy (kazan هو مصدر الحقيقة الوحيد)
// ‏قراءة متسامحة: لو نزل الكود قبل ترحيل cancelPenaltyFee، لا نُسقط
// ‏الإلغاء كله — نرجع لقراءة الدولة وحدها ونعتبر الغرامة صفراً.
try {
$stmtKazan = $con->prepare("SELECT country, cancelPenaltyFee FROM kazan LIMIT 1");
$stmtKazan->execute();
$kazan = $stmtKazan->fetch(PDO::FETCH_ASSOC) ?: [];
} catch (PDOException $eKazan) {
error_log("[cancel_ride_by_driver] cancelPenaltyFee غير موجود بعد"
. " (شغّل migrations/2026_08_07_kazan_cancel_penalty_policy.sql): "
. $eKazan->getMessage());
$stmtKazan = $con->prepare("SELECT country FROM kazan LIMIT 1");
$stmtKazan->execute();
$kazan = $stmtKazan->fetch(PDO::FETCH_ASSOC) ?: [];
}
$country = $kazan['country'] ?? 'Jordan';
$penaltyFee = (float) ($kazan['cancelPenaltyFee'] ?? 0);
if ($penaltyFee > CANCEL_PENALTY_ABSOLUTE_CAP) {
error_log("[cancel_ride_by_driver] kazan.cancelPenaltyFee=$penaltyFee"
. " تجاوز السقف الصلب — قُصّ إلى " . CANCEL_PENALTY_ABSOLUTE_CAP);
$penaltyFee = CANCEL_PENALTY_ABSOLUTE_CAP;
}
if ($penaltyFee > 0) {
// إضافة القيمة كدين سالب في المحفظة
$negativeDebt = -$penaltyFee;
$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");
}
?>