Update: 2026-08-07 00:57:03
This commit is contained in:
@@ -10,22 +10,33 @@ try {
|
||||
|
||||
require_once __DIR__ . '/streak_helper.php';
|
||||
|
||||
// نفس أسعار الكيلومتر والدقيقة التي تُحسب بها الرحلة في finish_ride_updates.php
|
||||
require_once __DIR__ . '/../pricing/pricing_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") !== '') {
|
||||
// هذا المسار ليس تغريماً للسائق — هو تعويض له عن عدم حضور الراكب:
|
||||
// السائق وصل نقطة الالتقاط، انتظر المدة المقررة، ولم يحضر الراكب. فيُعوَّض
|
||||
// عن المسافة التي قطعها نحوه + دقائق الانتظار، ويُخصم المبلغ من الراكب.
|
||||
// عقوبة السائق كثير الإلغاء شيء آخر تماماً: إيقاف مؤقت، لا مال (القسم ٢).
|
||||
//
|
||||
// المبلغ لا يُقرأ من الطلب إطلاقاً. كان سابقاً filterRequest("penalty_fee")
|
||||
// فيُخصم من محفظة الراكب بلا سقف ولا تحقق — أي بيد العميل.
|
||||
if (filterRequest("penalty_fee") !== null) {
|
||||
error_log("[cancel_ride_by_driver] SECURITY: تجاهل penalty_fee من العميل"
|
||||
. " (ride=$rideId driver=$driverId) — السياسة تُقرأ من kazan فقط");
|
||||
. " (ride=$rideId driver=$driverId) — التعويض يُحسب على الخادم");
|
||||
}
|
||||
|
||||
// سقف صلب فوق أي قيمة في قاعدة البيانات — يمنع خطأ إدارياً أو صفاً تالفاً
|
||||
// من تحويل الغرامة إلى خصم كارثي.
|
||||
const CANCEL_PENALTY_ABSOLUTE_CAP = 50000.0;
|
||||
// من تحويل التعويض إلى خصم كارثي.
|
||||
const CANCEL_COMPENSATION_ABSOLUTE_CAP = 50000.0;
|
||||
|
||||
// حد أقصى لدقائق الانتظار المحتسبة: سائق نسي الرحلة مفتوحة ساعتين لا
|
||||
// يتحول انتظاره إلى فاتورة على الراكب.
|
||||
const CANCEL_MAX_BILLABLE_WAIT_MINUTES = 20;
|
||||
|
||||
// تثبيت الحالة
|
||||
$statusText = "CancelFromDriverAfterApply";
|
||||
@@ -42,9 +53,19 @@ try {
|
||||
// 0. التحقق من الملكية — الرحلة يجب أن تكون مُسندة لهذا السائق
|
||||
// بدون هذا الفحص كان أي driver_id يقدر يلغي أي رحلة ويغرّم راكبها.
|
||||
// ---------------------------------------------------------
|
||||
$stmtOwn = $con->prepare("SELECT driver_id FROM ride WHERE id = ? FOR UPDATE");
|
||||
// SELECT * عمداً لا قائمة أعمدة: أعمدة التعويض (driverArrivedAt /
|
||||
// pickupApproachKm) قد لا تكون مُرحَّلة بعد، واسم عمود نوع السيارة
|
||||
// نفسه متضارب بين الملفات (carType في الschema، car_type في
|
||||
// finish_ride_updates.php). النجمة تجعل الاستعلام يعمل في كل الحالات
|
||||
// وتترك الغياب يُعالَج كقيمة مفقودة بدل أن يُسقط الإلغاء كله.
|
||||
$stmtOwn = $con->prepare("SELECT * FROM ride WHERE id = ? FOR UPDATE");
|
||||
$stmtOwn->execute([$rideId]);
|
||||
$assignedDriver = $stmtOwn->fetchColumn();
|
||||
$rideRow = $stmtOwn->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
$assignedDriver = is_array($rideRow) ? $rideRow['driver_id'] : false;
|
||||
|
||||
// نوع السيارة تحت أي من الاسمين
|
||||
$rideCarType = (string) ($rideRow['carType'] ?? $rideRow['car_type'] ?? 'Speed');
|
||||
|
||||
if ($assignedDriver === false) {
|
||||
$con->rollBack();
|
||||
@@ -94,7 +115,20 @@ try {
|
||||
if ($cancelCount >= 3) {
|
||||
$isBlocked = true;
|
||||
$blockUntil = date('Y-m-d H:i:s', strtotime('+4 hours'));
|
||||
// يمكنك هنا تحديث حالة السائق في جدول driver إذا لزم الأمر
|
||||
|
||||
// تثبيت الإيقاف على الخادم. كان يُكتب في تخزين تطبيق السائق وحده
|
||||
// (statusDriverLocation='blocked')، فيُفكّ بمسح البيانات أو إعادة
|
||||
// التثبيت. الآن acceptRide.php يرفض القبول ما دام blockedUntil مستقبلياً.
|
||||
// GREATEST يمنع إلغاءً جديداً من تقصير إيقاف سارٍ أطول.
|
||||
try {
|
||||
$con->prepare("UPDATE driver
|
||||
SET blockedUntil = GREATEST(COALESCE(blockedUntil, ?), ?)
|
||||
WHERE id = ?")
|
||||
->execute([$blockUntil, $blockUntil, $driverId]);
|
||||
} catch (PDOException $eBlock) {
|
||||
error_log("[cancel_ride_by_driver] تعذّر تثبيت الإيقاف (شغّل"
|
||||
. " migrations/2026_08_07_driver_cancel_compensation.sql): " . $eBlock->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
@@ -132,34 +166,58 @@ try {
|
||||
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) ?: [];
|
||||
}
|
||||
// ─────────────────────────────────────────────────────────
|
||||
// 4.1. تعويض السائق عن عدم حضور الراكب
|
||||
// = مسافة الاقتراب × سعر الكيلومتر لنوع السيارة
|
||||
// + دقائق الانتظار × سعر الدقيقة حسب وقت اليوم
|
||||
// كل المدخلات مسجَّلة على الخادم؛ لا شيء منها من العميل.
|
||||
// ─────────────────────────────────────────────────────────
|
||||
$stmtKazan = $con->prepare("SELECT * 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;
|
||||
$compensationCap = (float) ($kazan['cancelCompensationCap'] ?? 0);
|
||||
$minWaitMinutes = (int) ($kazan['cancelMinWaitMinutes'] ?? 5);
|
||||
|
||||
$compensation = 0.0;
|
||||
$waitedMinutes = 0;
|
||||
$approachKm = (float) ($rideRow['pickupApproachKm'] ?? 0);
|
||||
$arrivedAt = $rideRow['driverArrivedAt'] ?? null;
|
||||
|
||||
if ($compensationCap <= 0) {
|
||||
// التعويض معطّل لهذه الدولة — الافتراضي حتى تضبطه الإدارة.
|
||||
error_log("[cancel_ride_by_driver] التعويض معطّل (cancelCompensationCap=0)"
|
||||
. " ride=$rideId");
|
||||
} elseif (empty($arrivedAt)) {
|
||||
// لا ختم وصول ⇒ السائق ألغى قبل أن يصل. لا انتظار، فلا تعويض.
|
||||
error_log("[cancel_ride_by_driver] إلغاء قبل تسجيل الوصول — بلا تعويض"
|
||||
. " ride=$rideId driver=$driverId");
|
||||
} else {
|
||||
$waitedMinutes = (int) floor((time() - strtotime($arrivedAt)) / 60);
|
||||
|
||||
if ($waitedMinutes < $minWaitMinutes) {
|
||||
error_log("[cancel_ride_by_driver] انتظار $waitedMinutes د < الحد"
|
||||
. " $minWaitMinutes د — بلا تعويض ride=$rideId");
|
||||
} else {
|
||||
$billableMinutes = min($waitedMinutes, CANCEL_MAX_BILLABLE_WAIT_MINUTES);
|
||||
|
||||
$perKm = getPerKmRate($rideCarType, $kazan);
|
||||
$perMin = getPerMinRate($kazan);
|
||||
|
||||
$compensation = ($approachKm * $perKm) + ($billableMinutes * $perMin);
|
||||
$compensation = round(min($compensation, $compensationCap,
|
||||
CANCEL_COMPENSATION_ABSOLUTE_CAP), 2);
|
||||
|
||||
error_log("[cancel_ride_by_driver] تعويض ride=$rideId:"
|
||||
. " {$approachKm}كم×$perKm + {$billableMinutes}د×$perMin"
|
||||
. " = $compensation (سقف $compensationCap)");
|
||||
}
|
||||
}
|
||||
|
||||
if ($penaltyFee > 0) {
|
||||
// إضافة القيمة كدين سالب في المحفظة
|
||||
$negativeDebt = -$penaltyFee;
|
||||
if ($compensation > 0) {
|
||||
// يُخصم من الراكب كدين سالب، ويُضاف للسائق كمكافأة — الطرفان معاً.
|
||||
$negativeDebt = -$compensation;
|
||||
|
||||
$walletServer = "https://walletintaleq.intaleq.xyz";
|
||||
if (strtolower($country) == 'jordan') {
|
||||
@@ -193,7 +251,40 @@ try {
|
||||
if ($s2sCode !== 200) {
|
||||
error_log("Failed to add passenger debt via S2S: Code $s2sCode, Res: $s2sRes");
|
||||
}
|
||||
|
||||
|
||||
// ─────────────────────────────────────────────────────
|
||||
// إيصال التعويض لمحفظة السائق — النصف المفقود سابقاً.
|
||||
// كان المبلغ يُخصم من الراكب ولا يصل السائق إطلاقاً.
|
||||
// paymentID مشتق من الرحلة فيمنع ازدواج الصرف لو أُعيد الطلب.
|
||||
// ─────────────────────────────────────────────────────
|
||||
$rewardUrl = "$walletServer/v2/main/ride/driverWallet/add_s2s_reward.php";
|
||||
$chReward = curl_init($rewardUrl);
|
||||
curl_setopt_array($chReward, [
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => http_build_query([
|
||||
"driverID" => $driverId,
|
||||
"paymentID" => "no_show_$rideId",
|
||||
"amount" => $compensation,
|
||||
"paymentMethod" => "passenger_no_show_compensation",
|
||||
]),
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => 5,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Content-Type: application/x-www-form-urlencoded',
|
||||
'X-S2S-Api-Key: ' . getenv('S2S_SHARED_KEY')
|
||||
]
|
||||
]);
|
||||
$rewardRes = curl_exec($chReward);
|
||||
$rewardCode = curl_getinfo($chReward, CURLINFO_HTTP_CODE);
|
||||
curl_close($chReward);
|
||||
|
||||
if ($rewardCode !== 200) {
|
||||
// الراكب خُصم والسائق لم يُرصد — فرق يجب أن يظهر في التسوية.
|
||||
error_log("[cancel_ride_by_driver] MONEY: خُصم $compensation من"
|
||||
. " الراكب $passenger_id ولم يصل السائق $driverId"
|
||||
. " (ride=$rideId code=$rewardCode res=$rewardRes)");
|
||||
}
|
||||
|
||||
// تخزين الدين في الـ Redis لمدة 6 شهور (15552000 ثانية)
|
||||
try {
|
||||
$redisInstance = null;
|
||||
@@ -278,7 +369,11 @@ try {
|
||||
"status" => "success",
|
||||
"cancel_count" => $cancelCount,
|
||||
"is_blocked" => $isBlocked,
|
||||
"block_until" => $blockUntil
|
||||
"block_until" => $blockUntil,
|
||||
// ما استحقه السائق فعلاً عن هذه الرحلة — يعرضه التطبيق كتعويض،
|
||||
// لا كغرامة. صفر يعني أنه ألغى قبل الوصول أو قبل مدة الانتظار.
|
||||
"compensation" => $compensation ?? 0,
|
||||
"waited_minutes" => $waitedMinutes ?? 0,
|
||||
]);
|
||||
|
||||
} catch (PDOException $e) {
|
||||
|
||||
Reference in New Issue
Block a user