Update codebase

This commit is contained in:
Hamza-Ayed
2026-08-09 16:56:13 +03:00
parent 95e2e4f35d
commit b64debaa88
1058 changed files with 164327 additions and 113928 deletions
+220 -20
View File
@@ -10,14 +10,32 @@ 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");
$penaltyFee = (float) filterRequest("penalty_fee");
$passengerToken = filterRequest("passenger_token");
// ‏هذا المسار ليس تغريماً للسائق — هو تعويض له عن عدم حضور الراكب:
// ‏السائق وصل نقطة الالتقاط، انتظر المدة المقررة، ولم يحضر الراكب. فيُعوَّض
// ‏عن المسافة التي قطعها نحوه + دقائق الانتظار، ويُخصم المبلغ من الراكب.
// ‏عقوبة السائق كثير الإلغاء شيء آخر تماماً: إيقاف مؤقت، لا مال (القسم ٢).
//
// ‏المبلغ لا يُقرأ من الطلب إطلاقاً. كان سابقاً filterRequest("penalty_fee")
// ‏فيُخصم من محفظة الراكب بلا سقف ولا تحقق — أي بيد العميل.
if (filterRequest("penalty_fee") !== null) {
error_log("[cancel_ride_by_driver] SECURITY: تجاهل penalty_fee من العميل"
. " (ride=$rideId driver=$driverId) — التعويض يُحسب على الخادم");
}
// ‏حد أقصى لدقائق الانتظار المحتسبة: سائق نسي الرحلة مفتوحة ساعتين لا
// ‏يتحول انتظاره إلى فاتورة على الراكب.
const CANCEL_MAX_BILLABLE_WAIT_MINUTES = 20;
// تثبيت الحالة
$statusText = "CancelFromDriverAfterApply";
$statusText = "CancelFromDriverAfterApply";
if (!$rideId || !$driverId) {
jsonError("Missing parameters");
@@ -27,6 +45,54 @@ if (!$rideId || !$driverId) {
try {
$con->beginTransaction();
// ---------------------------------------------------------
// 0. التحقق من الملكية — الرحلة يجب أن تكون مُسندة لهذا السائق
// ‏بدون هذا الفحص كان أي driver_id يقدر يلغي أي رحلة ويغرّم راكبها.
// ---------------------------------------------------------
// ‏SELECT * عمداً لا قائمة أعمدة: أعمدة التعويض (driverArrivedAt /
// ‏pickupApproachKm) قد لا تكون مُرحَّلة بعد، واسم عمود نوع السيارة
// ‏نفسه متضارب بين الملفات (carType في الschema، car_type في
// ‏finish_ride_updates.php). النجمة تجعل الاستعلام يعمل في كل الحالات
// ‏وتترك الغياب يُعالَج كقيمة مفقودة بدل أن يُسقط الإلغاء كله.
$stmtOwn = $con->prepare("SELECT * FROM ride WHERE id = ? FOR UPDATE");
$stmtOwn->execute([$rideId]);
$rideRow = $stmtOwn->fetch(PDO::FETCH_ASSOC);
$assignedDriver = is_array($rideRow) ? $rideRow['driver_id'] : false;
// ‏نوع السيارة تحت أي من الاسمين
$rideCarType = (string) ($rideRow['carType'] ?? $rideRow['car_type'] ?? 'Speed');
// ‏مدخلا التعويض عاشا في Redis طوال الرحلة ولم يلمسا MySQL. نقرأهما
// ‏هنا قراءةً واحدة يشترك فيها الحساب (القسم ٤٫١) والتوثيق (القسم ٣).
$approachKm = 0.0;
$arrivedTs = 0;
try {
if (isset($redisLocation) && $redisLocation) {
$state = $redisLocation->hMGet("ride:$rideId:state",
['pickup_approach_km', 'driver_arrived_at']) ?: [];
$approachKm = (float) ($state['pickup_approach_km'] ?? 0);
$arrivedTs = (int) ($state['driver_arrived_at'] ?? 0);
}
} catch (Throwable $eState) {
error_log("[cancel_ride_by_driver] تعذّرت قراءة حالة الرحلة من Redis: "
. $eState->getMessage());
}
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)
// ---------------------------------------------------------
@@ -61,20 +127,71 @@ try {
if ($cancelCount >= 3) {
$isBlocked = true;
$blockUntil = date('Y-m-d H:i:s', strtotime('+4 hours'));
// يمكنك هنا تحديث حالة السائق في جدول driver إذا لزم الأمر
// ‏تثبيت الإيقاف على الخادم. كان يُكتب في تخزين تطبيق السائق وحده
// ‏(statusDriverLocation='blocked')، فيُفكّ بمسح البيانات أو إعادة
// ‏التثبيت. الآن acceptRide.php يرفض القبول ما دام المفتاح حياً.
//
// ‏Redis هو مسار الفحص (TTL ينهي الإيقاف تلقائياً بلا كرون تنظيف)،
// ‏وMySQL سجل دائم للمراجعة الإدارية لا يُقرأ في المسار الحار.
// ‏هذه الكتابة تحدث في الإلغاء الثالث فقط — لا على كل رحلة.
try {
if (isset($redisLocation) && $redisLocation) {
$blockKey = "driver:$driverId:blocked";
$remaining = $redisLocation->ttl($blockKey);
// ‏لا نُقصّر إيقافاً سارياً أطول بإلغاء جديد.
if ($remaining < 4 * 3600) {
$redisLocation->setex($blockKey, 4 * 3600, $blockUntil);
}
}
} catch (Throwable $eRedisBlock) {
error_log("[cancel_ride_by_driver] تعذّر تثبيت الإيقاف في Redis: "
. $eRedisBlock->getMessage());
}
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] تعذّر تسجيل الإيقاف في MySQL (شغّل"
. " migrations/2026_08_07_driver_cancel_compensation.sql): " . $eBlock->getMessage());
}
}
// ---------------------------------------------------------
// 3. تحديث حالة الرحلة في جدول ride
// ---------------------------------------------------------
$sqlRide = "UPDATE ride SET status = ?, driver_id = 0 WHERE id = ?";
// ‏التوثيق النهائي: بيانات التعويض التي عاشت في Redis طوال الرحلة
// ‏تُثبَّت هنا داخل نفس جملة تحديث الحالة — لا كتابة إضافية.
// ‏تُقرأ من Redis قبل هذه النقطة (القسم ٤٫١ يقرأها للحساب)، لذا نقرأها
// ‏مرة واحدة هنا ونمرّرها للطرفين.
$stampArrivedAt = $arrivedTs > 0 ? date('Y-m-d H:i:s', $arrivedTs) : null;
$sqlRide = "UPDATE ride SET status = ?, driver_id = 0,
pickupApproachKm = ?, driverArrivedAt = ?
WHERE id = ?";
$sqlRideLegacy = "UPDATE ride SET status = ?, driver_id = 0 WHERE id = ?";
$rideParams = [$statusText, $approachKm, $stampArrivedAt, $rideId];
// Local DB
$con->prepare($sqlRide)->execute([$statusText, $rideId]);
try {
$con->prepare($sqlRide)->execute($rideParams);
} catch (PDOException $eCols) {
// ‏أعمدة التعويض غير مُرحَّلة بعد — الإلغاء نفسه لا يتوقف.
error_log("[cancel_ride_by_driver] أعمدة التعويض غير موجودة (شغّل"
. " migrations/2026_08_07_driver_cancel_compensation.sql): " . $eCols->getMessage());
$con->prepare($sqlRideLegacy)->execute([$statusText, $rideId]);
}
// Remote DB (إن وجد)
if (isset($con_ride)) {
$con_ride->prepare($sqlRide)->execute([$statusText, $rideId]);
try {
$con_ride->prepare($sqlRide)->execute($rideParams);
} catch (PDOException $eCols) {
$con_ride->prepare($sqlRideLegacy)->execute([$statusText, $rideId]);
}
}
// 🆕 تصفير التتابع لأن السائق ألغى الرحلة
@@ -99,16 +216,62 @@ try {
notifyPassengerOnRideServer($passenger_id, $socketPayload);
}
// 4.1. إضافة غرامة الإلغاء على الراكب (الدين) إذا كانت موجودة
if ($penaltyFee > 0) {
// إضافة القيمة كدين سالب في المحفظة
$negativeDebt = -$penaltyFee;
// ─────────────────────────────────────────────────────────
// 4.1. تعويض السائق عن عدم حضور الراكب
// ‏= مسافة الاقتراب × سعر الكيلومتر لنوع السيارة
// + دقائق الانتظار × سعر الدقيقة حسب وقت اليوم
// ‏كل المدخلات مسجَّلة على الخادم؛ لا شيء منها من العميل.
// ─────────────────────────────────────────────────────────
// ‏أسعار دولة هذه الرحلة تحديداً. كان "LIMIT 1" يعيد أول صف
// ‏(سوريا) فيطبّق 40 SYP/كم على رحلة أردنية سعرها 0.284 JOD/كم.
// ‏$driverId يُمرَّر صراحةً: القسم ٣ صفّر ride.driver_id قبل هذه
// ‏النقطة، فالبحث عبر الرحلة كان سيرجع بلا دولة ويقع على سوريا.
$kazan = getKazanForRide($con, $rideId, 'Syria', $driverId);
$country = $kazan['country'] ?? 'Jordan';
// 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';
$compensationCap = (float) ($kazan['cancelCompensationCap'] ?? 0);
$minWaitMinutes = (int) ($kazan['cancelMinWaitMinutes'] ?? 5);
// ‏$approachKm و$arrivedTs قُرئا من Redis قراءةً واحدة في القسم صفر.
$compensation = 0.0;
$waitedMinutes = 0;
if ($compensationCap <= 0) {
// ‏التعويض معطّل لهذه الدولة — الافتراضي حتى تضبطه الإدارة.
error_log("[cancel_ride_by_driver] التعويض معطّل (cancelCompensationCap=0)"
. " ride=$rideId");
} elseif ($arrivedTs <= 0) {
// ‏لا ختم وصول ⇒ السائق ألغى قبل أن يصل، أو تعذّر إثبات وصوله.
error_log("[cancel_ride_by_driver] إلغاء بلا ختم وصول — بلا تعويض"
. " ride=$rideId driver=$driverId");
} else {
$waitedMinutes = (int) floor((time() - $arrivedTs) / 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);
// ‏سقف صلب بعملة الدولة لا رقماً مجرّداً: 50000 كانت
// ‏بلا أثر على الليرة بعد حذف صفرين، وكارثية على الدينار.
$currencyCode = (string) ($kazan['currency'] ?? 'SYP');
$compensation = round(min($compensation, $compensationCap,
getMoneyHardCap($currencyCode)), 2);
error_log("[cancel_ride_by_driver] تعويض ride=$rideId:"
. " {$approachKm}كم×$perKm + {$billableMinutes}د×$perMin"
. " = $compensation (سقف $compensationCap)");
}
}
if ($compensation > 0) {
// ‏يُخصم من الراكب كدين سالب، ويُضاف للسائق كمكافأة — الطرفان معاً.
$negativeDebt = -$compensation;
$walletServer = "https://walletintaleq.intaleq.xyz";
if (strtolower($country) == 'jordan') {
@@ -142,7 +305,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;
@@ -227,7 +423,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) {