Update: 2026-08-07 00:57:03
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
-- 2026_08_07_driver_cancel_compensation.sql
|
||||
--
|
||||
-- تعويض السائق عند عدم حضور الراكب (إلغاء السائق بعد الوصول والانتظار).
|
||||
-- النموذج: التعويض = مسافة الاقتراب إلى الراكب × سعر الكيلومتر لنوع السيارة
|
||||
-- + دقائق الانتظار الفعلية × سعر الدقيقة حسب وقت اليوم.
|
||||
-- يُخصم من محفظة الراكب ويُضاف لمحفظة السائق. الأسعار من kazan نفسه الذي
|
||||
-- تُسعَّر به الرحلة، عبر ride/rides/pricing_helper.php.
|
||||
--
|
||||
-- يستبدل هذا الملف مقاربة "الرسم الثابت" السابقة: المبلغ لم يعد يُقرأ من
|
||||
-- طلب العميل (كان filterRequest("penalty_fee") بلا سقف ولا تحقق)، ولم يعد
|
||||
-- رقماً ثابتاً — صار محسوباً من بيانات مسجَّلة على الخادم.
|
||||
--
|
||||
-- ⚠️ يُنفَّذ على قاعدتَي primary و ride معاً: الكود يكتب جدول ride في الاثنتين.
|
||||
|
||||
-- ══════════════════════════════════════════════════════════════
|
||||
-- ١) سياسة التعويض لكل دولة (قاعدة primary فقط — kazan يُقرأ عبر $con)
|
||||
-- ══════════════════════════════════════════════════════════════
|
||||
ALTER TABLE `kazan`
|
||||
ADD COLUMN `cancelCompensationCap` VARCHAR(10)
|
||||
CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci
|
||||
NOT NULL DEFAULT '0'
|
||||
COMMENT 'سقف تعويض عدم حضور الراكب بعملة الدولة. 0 = التعويض معطّل بالكامل'
|
||||
AFTER `startPrice`,
|
||||
ADD COLUMN `cancelMinWaitMinutes` TINYINT UNSIGNED
|
||||
NOT NULL DEFAULT 5
|
||||
COMMENT 'أقل دقائق انتظار بعد الوصول قبل أن يستحق السائق تعويضاً'
|
||||
AFTER `cancelCompensationCap`;
|
||||
|
||||
-- ══════════════════════════════════════════════════════════════
|
||||
-- ٢) بيانات الوصول والاقتراب (قاعدتا primary و ride)
|
||||
-- ══════════════════════════════════════════════════════════════
|
||||
ALTER TABLE `ride`
|
||||
ADD COLUMN `driverArrivedAt` DATETIME NULL DEFAULT NULL
|
||||
COMMENT 'لحظة تأكيد وصول السائق لنقطة الالتقاط — يختمها الخادم بعد تحقق الموقع',
|
||||
ADD COLUMN `pickupApproachKm` FLOAT NOT NULL DEFAULT 0
|
||||
COMMENT 'المسافة من موقع السائق لحظة القبول إلى نقطة الالتقاط (كم)';
|
||||
|
||||
-- ══════════════════════════════════════════════════════════════
|
||||
-- ٣) تطبيق الإيقاف على الخادم (قاعدتا primary و ride)
|
||||
-- كان الإيقاف يُكتب في تخزين تطبيق السائق وحده، فيُفكّ بإعادة التثبيت.
|
||||
-- ══════════════════════════════════════════════════════════════
|
||||
ALTER TABLE `driver`
|
||||
ADD COLUMN `blockedUntil` DATETIME NULL DEFAULT NULL
|
||||
COMMENT 'إيقاف استقبال الرحلات حتى هذا الوقت (إلغاءات متكررة). NULL = غير موقوف';
|
||||
|
||||
-- للتراجع:
|
||||
-- ALTER TABLE `kazan` DROP COLUMN `cancelCompensationCap`, DROP COLUMN `cancelMinWaitMinutes`;
|
||||
-- ALTER TABLE `ride` DROP COLUMN `driverArrivedAt`, DROP COLUMN `pickupApproachKm`;
|
||||
-- ALTER TABLE `driver` DROP COLUMN `blockedUntil`;
|
||||
@@ -1,22 +0,0 @@
|
||||
-- 2026_08_07_kazan_cancel_penalty_policy.sql
|
||||
--
|
||||
-- سبب الترحيل: كان مبلغ غرامة الإلغاء في ride/rides/cancel_ride_by_driver.php
|
||||
-- يُقرأ من طلب العميل مباشرة (filterRequest("penalty_fee")) ويُخصم من محفظة
|
||||
-- الراكب بلا سقف ولا تحقق — أي أن طلباً مصاغاً يدوياً كان يقدر يخصم أي مبلغ
|
||||
-- من أي راكب. صار المبلغ يُقرأ من هنا فقط، ولا يقبل الخادم أي قيمة من العميل.
|
||||
--
|
||||
-- القيمة الافتراضية '0' تحفظ السلوك الحالي حرفياً: لا عميل يرسل penalty_fee
|
||||
-- اليوم، فالغرامة لا تُطبَّق فعلياً. تفعيلها قرار إداري صريح بتحديث الصف.
|
||||
--
|
||||
-- القاعدة المستهدفة: primary فقط — الكود يقرأ kazan عبر $con، وجدول kazan في
|
||||
-- قاعدتي ride/tracking بنية أقدم ومختلفة الأعمدة (لا يُستخدم في هذا المسار).
|
||||
|
||||
ALTER TABLE `kazan`
|
||||
ADD COLUMN `cancelPenaltyFee` VARCHAR(10)
|
||||
CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci
|
||||
NOT NULL DEFAULT '0'
|
||||
COMMENT 'غرامة إلغاء السائق بعد القبول، تُخصم من محفظة الراكب. 0 = معطّلة'
|
||||
AFTER `startPrice`;
|
||||
|
||||
-- للتراجع:
|
||||
-- ALTER TABLE `kazan` DROP COLUMN `cancelPenaltyFee`;
|
||||
@@ -46,6 +46,29 @@ if (!in_array($status, $allowedStatuses, true)) {
|
||||
error_log("[accept_ride] DriverID=$driverId attempting RideID=$rideId");
|
||||
|
||||
try {
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// STEP A0 — تطبيق الإيقاف على الخادم
|
||||
// كان الإيقاف بعد ٣ إلغاءات يُكتب في تخزين تطبيق السائق وحده، فيُفكّ
|
||||
// بمسح بيانات التطبيق أو إعادة تثبيته. هذه هي البوابة الحقيقية: سائق
|
||||
// موقوف لا يستطيع أن يفوز برحلة مهما فعل بتطبيقه.
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
try {
|
||||
$stmtBlock = $con->prepare("SELECT blockedUntil FROM driver WHERE id = ?");
|
||||
$stmtBlock->execute([$driverId]);
|
||||
$blockedUntil = $stmtBlock->fetchColumn();
|
||||
|
||||
if (!empty($blockedUntil) && strtotime($blockedUntil) > time()) {
|
||||
error_log("[accept_ride] DriverID=$driverId موقوف حتى $blockedUntil — رُفض القبول");
|
||||
printFailure("Your account is temporarily suspended until $blockedUntil");
|
||||
exit;
|
||||
}
|
||||
} catch (PDOException $eBlock) {
|
||||
// العمود غير موجود بعد (قبل تشغيل ترحيل 2026_08_07) — لا نمنع القبول
|
||||
// بسبب فحصٍ لا يمكن إجراؤه، لكن نسجّله بوضوح.
|
||||
error_log("[accept_ride] تعذّر فحص blockedUntil (شغّل"
|
||||
. " migrations/2026_08_07_driver_cancel_compensation.sql): " . $eBlock->getMessage());
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// STEP A — القفل على ride DB (المرجع الأساسي)
|
||||
// Optimistic lock: نغير فقط إذا status لا يزال 'waiting' أو 'wait'
|
||||
@@ -87,6 +110,48 @@ try {
|
||||
error_log("[accept_ride] primary DB sync WARNING: " . $eSync->getMessage());
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// STEP B1 — تثبيت مسافة الاقتراب لحظة القبول
|
||||
// هذه هي المسافة التي سيقطعها السائق نحو الراكب، وهي أساس تعويض عدم
|
||||
// الحضور لاحقاً. تُقاس الآن حصراً: عند الإلغاء يكون السائق واقفاً عند
|
||||
// نقطة الالتقاط فتصير المسافة صفراً، فلا يمكن استنتاجها بأثر رجعي.
|
||||
// مصدر الموقع هو Redis لا الطلب — حتى لا يتحكم العميل بالمبلغ.
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
try {
|
||||
$stmtPickup = $con->prepare("SELECT start_location FROM ride WHERE id = ?");
|
||||
$stmtPickup->execute([$rideId]);
|
||||
$pickupRaw = (string) $stmtPickup->fetchColumn();
|
||||
|
||||
[$pickupLat, $pickupLng] = array_pad(
|
||||
array_map('trim', explode(',', $pickupRaw, 2)), 2, null
|
||||
);
|
||||
|
||||
$approachKm = 0.0;
|
||||
if (isset($redisLocation) && $redisLocation
|
||||
&& is_numeric($pickupLat) && is_numeric($pickupLng)) {
|
||||
$loc = $redisLocation->hGetAll("driver:public:$driverId");
|
||||
if (!empty($loc['lat']) && !empty($loc['lng'])) {
|
||||
$approachKm = round(getDistanceBetweenPoints(
|
||||
(float) $loc['lat'], (float) $loc['lng'],
|
||||
(float) $pickupLat, (float) $pickupLng
|
||||
), 3);
|
||||
}
|
||||
}
|
||||
|
||||
if ($approachKm > 0) {
|
||||
$sqlApproach = "UPDATE ride SET pickupApproachKm = ? WHERE id = ?";
|
||||
$con->prepare($sqlApproach)->execute([$approachKm, $rideId]);
|
||||
try {
|
||||
$con_ride->prepare($sqlApproach)->execute([$approachKm, $rideId]);
|
||||
} catch (PDOException $e) { /* primary يكفي لحساب التعويض */ }
|
||||
} else {
|
||||
error_log("[accept_ride] تعذّر قياس مسافة الاقتراب RideID=$rideId"
|
||||
. " — التعويض سيحتسب زمن الانتظار وحده");
|
||||
}
|
||||
} catch (PDOException $eApproach) {
|
||||
error_log("[accept_ride] pickupApproachKm WARNING: " . $eApproach->getMessage());
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════════════════
|
||||
// STEP C — driver_orders (INSERT أو UPDATE بسطر واحد آمن)
|
||||
// ON DUPLICATE KEY يمنع race condition ثانية على هذا الجدول
|
||||
|
||||
@@ -17,6 +17,10 @@ if (!$rideId || !$driverId) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// نصف قطر قبول ختم الوصول. أوسع من فحص الـ100م في التطبيق بهامش يستوعب
|
||||
// ضعف الـGPS بين المباني، وأضيق من أن يسمح بالختم من حي آخر.
|
||||
const ARRIVAL_STAMP_RADIUS_KM = 0.30;
|
||||
|
||||
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'");
|
||||
@@ -28,6 +32,68 @@ try {
|
||||
$stmtLocal->execute([$rideId, $driverId]);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
// 2.1 ختم لحظة الوصول — أساس تعويض عدم حضور الراكب
|
||||
//
|
||||
// فحص المسافة في التطبيق (100م) على العميل، فيمكن تجاوزه بموقع مزيَّف
|
||||
// ثم انتظار خمس دقائق وقبض تعويض بلا وجود حقيقي. لذلك يتحقق الخادم
|
||||
// هنا بنفسه من موقع السائق في Redis قبل أن يختم الوقت.
|
||||
//
|
||||
// إن تعذّر التحقق (Redis غير متاح أو الموقع مفقود) لا نمنع الوصول —
|
||||
// الحالة تُحدَّث والراكب يُشعَر كالمعتاد — لكن لا نختم وقتاً يترتب عليه
|
||||
// مال لا نستطيع إثباته. النتيجة: لا تعويض لهذه الرحلة، وسجلّ واضح.
|
||||
// ─────────────────────────────────────────────────────────────
|
||||
try {
|
||||
$stmtRide = $con->prepare("SELECT * FROM ride WHERE id = ? AND driver_id = ?");
|
||||
$stmtRide->execute([$rideId, $driverId]);
|
||||
$rideRow = $stmtRide->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (is_array($rideRow)
|
||||
&& array_key_exists('driverArrivedAt', $rideRow)
|
||||
&& empty($rideRow['driverArrivedAt'])) {
|
||||
|
||||
[$pLat, $pLng] = array_pad(
|
||||
array_map('trim', explode(',', (string) $rideRow['start_location'], 2)), 2, null
|
||||
);
|
||||
|
||||
$canStamp = false;
|
||||
if (isset($redisLocation) && $redisLocation
|
||||
&& is_numeric($pLat) && is_numeric($pLng)) {
|
||||
|
||||
$loc = $redisLocation->hGetAll("driver:public:$driverId");
|
||||
if (!empty($loc['lat']) && !empty($loc['lng'])) {
|
||||
$gap = getDistanceBetweenPoints(
|
||||
(float) $loc['lat'], (float) $loc['lng'],
|
||||
(float) $pLat, (float) $pLng
|
||||
);
|
||||
$canStamp = ($gap <= ARRIVAL_STAMP_RADIUS_KM);
|
||||
if (!$canStamp) {
|
||||
error_log("[arrive_ride] ختم الوصول مرفوض — السائق $driverId"
|
||||
. " يبعد " . round($gap, 3) . "كم عن نقطة الالتقاط (ride=$rideId)");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($canStamp) {
|
||||
// WHERE driverArrivedAt IS NULL يجعله ذرّياً: نداء ثانٍ لا
|
||||
// يعيد ضبط عدّاد الانتظار.
|
||||
$sqlStamp = "UPDATE ride SET driverArrivedAt = NOW()
|
||||
WHERE id = ? AND driver_id = ? AND driverArrivedAt IS NULL";
|
||||
$con->prepare($sqlStamp)->execute([$rideId, $driverId]);
|
||||
try {
|
||||
$con_ride->prepare($sqlStamp)->execute([$rideId, $driverId]);
|
||||
} catch (PDOException $e) { /* primary هو المرجع لهذا الحقل */ }
|
||||
} else {
|
||||
error_log("[arrive_ride] تعذّر إثبات الوصول — لا تعويض لهذه"
|
||||
. " الرحلة (ride=$rideId driver=$driverId)");
|
||||
}
|
||||
}
|
||||
} catch (PDOException $eStamp) {
|
||||
// العمود غير مُرحَّل بعد — الوصول يعمل، التعويض معطّل.
|
||||
error_log("[arrive_ride] تعذّر ختم driverArrivedAt (شغّل"
|
||||
. " migrations/2026_08_07_driver_cancel_compensation.sql): " . $eStamp->getMessage());
|
||||
}
|
||||
|
||||
// 3. جلب بيانات الراكب للإرسال
|
||||
// نستخدم con_ride لضمان الدقة
|
||||
$stmtPas = $con_ride->prepare("SELECT passenger_id FROM ride WHERE id = ?");
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -9,6 +9,10 @@ try {
|
||||
|
||||
require_once __DIR__ . '/streak_helper.php';
|
||||
|
||||
// getPerKmRate / getPerMinRate / getCurrencyByCountry — كانت معرَّفة هنا
|
||||
// وانتقلت إلى pricing_helper.php ليشاركها تعويض الإلغاء بنفس الأسعار.
|
||||
require_once __DIR__ . '/../pricing/pricing_helper.php';
|
||||
|
||||
// ============================================================
|
||||
// finish_ride_updates.php — Atomic Server-to-Server
|
||||
// ============================================================
|
||||
@@ -423,132 +427,3 @@ try {
|
||||
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'; // افتراضي: ليرة سورية
|
||||
}
|
||||
?>
|
||||
@@ -387,6 +387,7 @@ DROP TABLE IF EXISTS `driver`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `driver` (
|
||||
`blockedUntil` datetime DEFAULT NULL COMMENT 'إيقاف استقبال الرحلات حتى هذا الوقت (إلغاءات متكررة). NULL = غير موقوف',
|
||||
`idn` int NOT NULL AUTO_INCREMENT,
|
||||
`id` varchar(100) NOT NULL,
|
||||
`phone` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL,
|
||||
@@ -839,7 +840,8 @@ CREATE TABLE `kazan` (
|
||||
`fuelPrice` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'سعر الوقود المرجعي',
|
||||
`currency` varchar(5) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT 'SYP' COMMENT 'رمز العملة: SYP, EGP, JOD, IQD, ...',
|
||||
`startPrice` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '0' COMMENT 'فتحة العداد',
|
||||
`cancelPenaltyFee` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '0' COMMENT 'غرامة إلغاء السائق بعد القبول، تُخصم من محفظة الراكب. 0 = معطّلة',
|
||||
`cancelCompensationCap` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '0' COMMENT 'سقف تعويض عدم حضور الراكب بعملة الدولة. 0 = التعويض معطّل بالكامل',
|
||||
`cancelMinWaitMinutes` tinyint unsigned NOT NULL DEFAULT '5' COMMENT 'أقل دقائق انتظار بعد الوصول قبل أن يستحق السائق تعويضاً',
|
||||
|
||||
-- ========== 🚗 أسعار الكيلومتر لكل نوع سيارة ==========
|
||||
`speedPrice` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT 'سعر الكيلومتر - Speed',
|
||||
@@ -1457,6 +1459,8 @@ DROP TABLE IF EXISTS `ride`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `ride` (
|
||||
`driverArrivedAt` datetime DEFAULT NULL COMMENT 'لحظة تأكيد وصول السائق لنقطة الالتقاط — يختمها الخادم بعد تحقق الموقع',
|
||||
`pickupApproachKm` float NOT NULL DEFAULT '0' COMMENT 'المسافة من موقع السائق لحظة القبول إلى نقطة الالتقاط (كم)',
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`start_location` varchar(255) NOT NULL,
|
||||
`end_location` varchar(255) NOT NULL,
|
||||
|
||||
@@ -384,6 +384,7 @@ DROP TABLE IF EXISTS `driver`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `driver` (
|
||||
`blockedUntil` datetime DEFAULT NULL COMMENT 'إيقاف استقبال الرحلات حتى هذا الوقت (إلغاءات متكررة). NULL = غير موقوف',
|
||||
`idn` int NOT NULL AUTO_INCREMENT,
|
||||
`id` varchar(100) NOT NULL,
|
||||
`phone` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL,
|
||||
@@ -1346,6 +1347,8 @@ DROP TABLE IF EXISTS `ride`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `ride` (
|
||||
`driverArrivedAt` datetime DEFAULT NULL COMMENT 'لحظة تأكيد وصول السائق لنقطة الالتقاط — يختمها الخادم بعد تحقق الموقع',
|
||||
`pickupApproachKm` float NOT NULL DEFAULT '0' COMMENT 'المسافة من موقع السائق لحظة القبول إلى نقطة الالتقاط (كم)',
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`start_location` varchar(255) NOT NULL,
|
||||
`end_location` varchar(255) NOT NULL,
|
||||
|
||||
@@ -385,6 +385,7 @@ DROP TABLE IF EXISTS `driver`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `driver` (
|
||||
`blockedUntil` datetime DEFAULT NULL COMMENT 'إيقاف استقبال الرحلات حتى هذا الوقت (إلغاءات متكررة). NULL = غير موقوف',
|
||||
`idn` int NOT NULL AUTO_INCREMENT,
|
||||
`id` varchar(100) NOT NULL,
|
||||
`phone` varchar(255) CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci NOT NULL,
|
||||
@@ -1386,6 +1387,8 @@ DROP TABLE IF EXISTS `ride`;
|
||||
/*!40101 SET @saved_cs_client = @@character_set_client */;
|
||||
/*!50503 SET character_set_client = utf8mb4 */;
|
||||
CREATE TABLE `ride` (
|
||||
`driverArrivedAt` datetime DEFAULT NULL COMMENT 'لحظة تأكيد وصول السائق لنقطة الالتقاط — يختمها الخادم بعد تحقق الموقع',
|
||||
`pickupApproachKm` float NOT NULL DEFAULT '0' COMMENT 'المسافة من موقع السائق لحظة القبول إلى نقطة الالتقاط (كم)',
|
||||
`id` int NOT NULL AUTO_INCREMENT,
|
||||
`start_location` varchar(255) NOT NULL,
|
||||
`end_location` varchar(255) NOT NULL,
|
||||
|
||||
@@ -535,6 +535,18 @@ class MapDriverController extends GetxController
|
||||
);
|
||||
|
||||
if (response['status'] == 'success') {
|
||||
// 💰 التعويض عن عدم حضور الراكب — يحسبه الخادم من مسافة الاقتراب
|
||||
// ودقائق الانتظار، ويُضيفه لمحفظة السائق. صفر يعني أن الإلغاء وقع
|
||||
// قبل تسجيل الوصول أو قبل اكتمال مدة الانتظار المقررة.
|
||||
final num compensation = response['compensation'] ?? 0;
|
||||
if (compensation > 0) {
|
||||
final int waited = response['waited_minutes'] ?? 0;
|
||||
mySnackbarSuccess(
|
||||
"تم تعويضك $compensation عن المسافة و $waited دقيقة انتظار."
|
||||
.tr,
|
||||
);
|
||||
}
|
||||
|
||||
// 🔥🔥 معالجة الحظر (The Penalty Logic) 🔥🔥
|
||||
bool isBlocked = response['is_blocked'] ?? false;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user