Update: 2026-08-07 02:32:14
This commit is contained in:
@@ -31,12 +31,24 @@ function computePassengerCancelFee(
|
||||
$rideId,
|
||||
array $ride,
|
||||
$driverId,
|
||||
string &$exemptReason
|
||||
string &$exemptReason,
|
||||
&$currency = null
|
||||
): float {
|
||||
// العملة تُحسم من الدولة دائماً، حتى مع الإعفاء: التطبيق يحتاجها ليعرض
|
||||
// "الإلغاء مجاني" أو "سيُخصم 1.20 دينار" بوحدة صحيحة.
|
||||
$currency = 'SYP';
|
||||
// ── إعفاء ١: لا سائق قَبِل بعد ──────────────────────────
|
||||
// لا أحد تحرّك، فلا أحد يستحق تعويضاً. وهذه أغلب حالات الإلغاء.
|
||||
if (empty($driverId) || (string) $driverId === '0') {
|
||||
$exemptReason = 'no_driver_assigned';
|
||||
try {
|
||||
$stmtCur = $con->prepare("SELECT currency, country FROM kazan LIMIT 1");
|
||||
$stmtCur->execute();
|
||||
$rowCur = $stmtCur->fetch(PDO::FETCH_ASSOC) ?: [];
|
||||
$currency = !empty($rowCur['currency'])
|
||||
? (string) $rowCur['currency']
|
||||
: getCurrencyByCountry((string) ($rowCur['country'] ?? 'Syria'));
|
||||
} catch (PDOException $e) { /* الافتراضي يكفي */ }
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
@@ -45,6 +57,11 @@ function computePassengerCancelFee(
|
||||
$stmtKazan->execute();
|
||||
$kazan = $stmtKazan->fetch(PDO::FETCH_ASSOC) ?: [];
|
||||
|
||||
// العمود currency هو المرجع، وgetCurrencyByCountry احتياط إن كان فارغاً.
|
||||
$currency = !empty($kazan['currency'])
|
||||
? (string) $kazan['currency']
|
||||
: getCurrencyByCountry((string) ($kazan['country'] ?? 'Syria'));
|
||||
|
||||
$cap = (float) ($kazan['passengerCancelCap'] ?? 0);
|
||||
if ($cap <= 0) {
|
||||
// الرسم معطّل لهذه الدولة — الافتراضي حتى تضبطه الإدارة.
|
||||
@@ -103,7 +120,7 @@ function computePassengerCancelFee(
|
||||
$fee = round(min($fee, $cap, PASSENGER_CANCEL_ABSOLUTE_CAP), 2);
|
||||
|
||||
error_log("[cancel_fee] ride=$rideId رسم إلغاء: {$approachKm}كم"
|
||||
. " + {$billableMinutes}د = $fee (سقف $cap)");
|
||||
. " + {$billableMinutes}د = $fee $currency (سقف $cap)");
|
||||
|
||||
return max(0.0, $fee);
|
||||
}
|
||||
|
||||
@@ -79,9 +79,11 @@ try {
|
||||
// =================================================================
|
||||
$cancelFee = 0.0;
|
||||
$feeExemptReason = '';
|
||||
$feeCurrency = 'SYP';
|
||||
try {
|
||||
$cancelFee = computePassengerCancelFee(
|
||||
$con, $redisLocation ?? null, $rideId, $ride, $driverId, $feeExemptReason
|
||||
$con, $redisLocation ?? null, $rideId, $ride, $driverId,
|
||||
$feeExemptReason, $feeCurrency
|
||||
);
|
||||
} catch (Throwable $eFee) {
|
||||
// فشل حساب الرسم لا يمنع الإلغاء أبداً — الإلغاء حق للراكب.
|
||||
@@ -201,8 +203,9 @@ try {
|
||||
// الرسم يُرجَع للتطبيق ليعرضه صراحةً. خصم صامت من المحفظة يكتشفه
|
||||
// الراكب لاحقاً هو أسرع طريق لشكوى ولمراجعة سيئة.
|
||||
jsonSuccess([
|
||||
'cancel_fee' => $cancelFee ?? 0,
|
||||
'fee_exempt' => ($cancelFee ?? 0) > 0 ? '' : ($feeExemptReason ?? ''),
|
||||
'cancel_fee' => $cancelFee ?? 0,
|
||||
'fee_currency' => $feeCurrency ?? 'SYP',
|
||||
'fee_exempt' => ($cancelFee ?? 0) > 0 ? '' : ($feeExemptReason ?? ''),
|
||||
], "Ride cancelled successfully");
|
||||
|
||||
} catch (PDOException $e) {
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
// ============================================================
|
||||
// preview_cancel_fee.php — كم سيكلّف الإلغاء؟ (بلا إلغاء)
|
||||
//
|
||||
// نقطة قراءة خالصة: تحسب الرسم بنفس دالة الإلغاء الفعلي ولا تغيّر شيئاً.
|
||||
// بدونها لا يستطيع التطبيق تحذير الراكب قبل الضغط، فيكتشف الخصم بعد
|
||||
// فوات الأوان — وهذا أسرع طريق لشكوى ومراجعة سيئة.
|
||||
//
|
||||
// تستخدم computePassengerCancelFee نفسها عمداً: أي تحذير يخالف ما يُخصم
|
||||
// فعلاً أسوأ من غياب التحذير أصلاً.
|
||||
// ============================================================
|
||||
|
||||
require_once __DIR__ . '/../../connect.php';
|
||||
require_once __DIR__ . '/../pricing/pricing_helper.php';
|
||||
require_once __DIR__ . '/cancel_fee_helper.php';
|
||||
|
||||
$rideId = filterRequest("ride_id");
|
||||
|
||||
if (!$rideId) {
|
||||
jsonError("Missing Ride ID");
|
||||
}
|
||||
|
||||
try {
|
||||
$stmt = $con->prepare("SELECT * FROM ride WHERE id = ?");
|
||||
$stmt->execute([$rideId]);
|
||||
$ride = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$ride) {
|
||||
jsonError("Ride not found", 404);
|
||||
}
|
||||
|
||||
// رحلة بدأت فعلاً لا تُلغى أصلاً — نفس شرط الإلغاء الحقيقي.
|
||||
if (($ride['status'] ?? '') === 'Begin') {
|
||||
jsonError("Cannot cancel started ride");
|
||||
}
|
||||
|
||||
$exemptReason = '';
|
||||
$currency = 'SYP';
|
||||
$fee = computePassengerCancelFee(
|
||||
$con, $redisLocation ?? null, $rideId, $ride,
|
||||
$ride['driver_id'] ?? '0', $exemptReason, $currency
|
||||
);
|
||||
|
||||
jsonSuccess([
|
||||
'cancel_fee' => $fee,
|
||||
'fee_currency' => $currency,
|
||||
'fee_exempt' => $fee > 0 ? '' : $exemptReason,
|
||||
'will_charge' => $fee > 0,
|
||||
], "ok");
|
||||
|
||||
} catch (PDOException $e) {
|
||||
error_log("[preview_cancel_fee] " . $e->getMessage());
|
||||
jsonError("DB Error", 500);
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
# سياسة الإلغاء والتعويض — سيرو
|
||||
|
||||
> **الغرض:** مرجع واحد لخدمة العملاء، وأساس صفحة ويب عامة تُشرح للمستخدمين.
|
||||
> **آخر تحديث:** 2026-08-07
|
||||
> **الحالة:** مبنية في الكود، **معطّلة افتراضياً** حتى تضبط الإدارة السقوف.
|
||||
|
||||
---
|
||||
|
||||
## المبدأ الحاكم
|
||||
|
||||
**من ضيّع وقت الآخر يعوّضه، والمال يذهب للطرف المتضرر لا للشركة.**
|
||||
|
||||
هذا يفرّق سيرو عن نموذج "غرامة تذهب للمنصة". السائق الذي انتظر يُعوَّض، والراكب الذي انتظر سائقاً متأخراً لا يُغرَّم.
|
||||
|
||||
---
|
||||
|
||||
## أولاً: إلغاء الراكب بعد قبول السائق
|
||||
|
||||
### متى يكون الإلغاء مجانياً؟
|
||||
|
||||
| الحالة | الشرح للمستخدم |
|
||||
|---|---|
|
||||
| **لم يقبل أي سائق بعد** | لم يتحرك أحد نحوك، فلا أحد يستحق تعويضاً. وهذه أغلب حالات الإلغاء. |
|
||||
| **خلال أول دقيقتين من قبول السائق** | مهلة لتصحيح خطأ في الوجهة أو تغيير الرأي. |
|
||||
| **مرّت 10 دقائق ولم يصل السائق** | التأخير من السائق لا منك — الإلغاء مجاني. |
|
||||
| **الرسم معطّل في بلدك** | الإدارة لم تفعّل السياسة بعد. |
|
||||
|
||||
### متى يُحتسب رسم؟
|
||||
|
||||
عندما يكون السائق قد **قَبِل الرحلة وتحرّك نحوك**، ومضت مهلة الدقيقتين، ولم يتأخر عليك.
|
||||
|
||||
### كيف يُحسب؟
|
||||
|
||||
```
|
||||
الرسم = المسافة التي قطعها السائق نحوك × سعر الكيلومتر لنوع السيارة
|
||||
+ الدقائق منذ قبوله × سعر الدقيقة حسب وقت اليوم
|
||||
```
|
||||
|
||||
- الأسعار هي **نفسها** المستخدمة في تسعير الرحلات (جدول `kazan` لكل دولة).
|
||||
- **العملة:** بعملة بلد التشغيل — ليرة سورية (SYP)، دينار أردني (JOD)، جنيه مصري (EGP)…
|
||||
- **حد أقصى للدقائق المحتسبة: 20 دقيقة.** سائق قَبِل ونسي الرحلة ساعة لا يتحول نسيانه إلى فاتورة عليك.
|
||||
- **سقف أعلى** يحدده كل بلد (`passengerCancelCap`)، وفوقه سقف صلب في الكود يمنع أي خطأ إداري من خصم كارثي.
|
||||
|
||||
### ماذا يحدث بعد الإلغاء؟
|
||||
|
||||
1. يُخصم المبلغ من محفظتك كدَين.
|
||||
2. **يُضاف نفس المبلغ لمحفظة السائق** — ليس إيراداً للشركة.
|
||||
3. يزيد عدّاد "عدم الحضور" في حسابك (للمراجعة الإدارية فقط، لا عقوبة آلية).
|
||||
|
||||
### التحذير المسبق
|
||||
|
||||
التطبيق **يعرض المبلغ ويطلب تأكيدك قبل الإلغاء**. لا خصم صامت. إن تعذّر الاتصال بالخادم لحظتها، يمضي الإلغاء بلا تحذير — لأن حبسك في رحلة بسبب عطل تقني أسوأ.
|
||||
|
||||
---
|
||||
|
||||
## ثانياً: تعويض السائق عند عدم حضور الراكب
|
||||
|
||||
### متى يستحق السائق تعويضاً؟
|
||||
|
||||
عندما يصل نقطة الالتقاط، **ينتظر 5 دقائق على الأقل**، ولا يحضر الراكب، فيلغي الرحلة.
|
||||
|
||||
### كيف يُحسب؟
|
||||
|
||||
```
|
||||
التعويض = مسافة الاقتراب إلى الراكب × سعر الكيلومتر
|
||||
+ دقائق الانتظار × سعر الدقيقة
|
||||
```
|
||||
|
||||
نفس المعادلة والأسعار. الحد الأقصى للدقائق المحتسبة 20 دقيقة أيضاً.
|
||||
|
||||
### شروط مهمة
|
||||
|
||||
- **الوصول يتحقق منه الخادم** من موقع السائق الفعلي (ضمن 300 متر من نقطة الالتقاط). لا يكفي أن يضغط السائق "وصلت".
|
||||
- **إن تعذّر إثبات الوصول** (خدمة الموقع معطّلة مثلاً) فلا تعويض — لا يُصرف مال لا يمكن إثبات استحقاقه.
|
||||
- إلغاء السائق **قبل** الوصول: لا تعويض إطلاقاً.
|
||||
|
||||
---
|
||||
|
||||
## ثالثاً: عقوبة السائق كثير الإلغاء
|
||||
|
||||
منفصلة تماماً عن المال. **لا غرامة على السائق أبداً.**
|
||||
|
||||
| الشرط | العقوبة |
|
||||
|---|---|
|
||||
| 3 إلغاءات بعد القبول خلال 24 ساعة | إيقاف استقبال الطلبات **4 ساعات** |
|
||||
|
||||
الإيقاف مطبَّق على الخادم: السائق الموقوف لا يستطيع قبول أي رحلة حتى لو أعاد تثبيت التطبيق.
|
||||
|
||||
---
|
||||
|
||||
## رابعاً: التسرب خارج المنصة
|
||||
|
||||
سلوك يتكرر فيه: قَبِل السائق ← أُلغيت الرحلة ← ولم تبدأ قط، مع **نفس الراكب مراراً**.
|
||||
|
||||
| التكرار | درجة الاشتباه |
|
||||
|---|---|
|
||||
| مرة واحدة | منخفضة (للإلغاء أسباب مشروعة كثيرة) |
|
||||
| مرتان | متوسطة |
|
||||
| ثلاث فأكثر | مرتفعة → **مراجعة بشرية** |
|
||||
|
||||
**العقوبة:** خفض أولوية السائق في توزيع الرحلات — يخسر الطلبات الخاصة والقريبة تدريجياً. **لا حظر ولا خصم مالي.** وتزول العقوبة تلقائياً حين ينظف سجله خلال 30 يوماً.
|
||||
|
||||
---
|
||||
|
||||
## المعاملات القابلة للضبط (لكل دولة، جدول `kazan`)
|
||||
|
||||
| المعامل | الافتراضي | المعنى |
|
||||
|---|---|---|
|
||||
| `passengerCancelCap` | `0` (معطّل) | سقف رسم إلغاء الراكب |
|
||||
| `passengerCancelFreeMinutes` | `2` | دقائق الإلغاء المجاني بعد القبول |
|
||||
| `driverLateExemptMinutes` | `10` | بعدها يصير الإلغاء مجانياً لتأخر السائق |
|
||||
| `cancelCompensationCap` | `0` (معطّل) | سقف تعويض عدم حضور الراكب |
|
||||
| `cancelMinWaitMinutes` | `5` | أقل انتظار يستحق السائق بعده تعويضاً |
|
||||
|
||||
> ⚠️ القيمة `0` في السقوف تعني **تعطيلاً كاملاً**. لا يُخصم ولا يُصرف شيء حتى تضبطها الإدارة صراحةً.
|
||||
|
||||
---
|
||||
|
||||
## أسئلة متوقعة من العملاء
|
||||
|
||||
**«ألغيت لأن السائق لم يتحرك من مكانه — لماذا خُصم مني؟»**
|
||||
إن مرّت 10 دقائق ولم يصل، الإلغاء مجاني تلقائياً. إن خُصم رغم ذلك فالرحلة تحتاج مراجعة — ارفعها للفريق التقني بمعرّف الرحلة.
|
||||
|
||||
**«لماذا الرسم مختلف كل مرة؟»**
|
||||
لأنه ليس رقماً ثابتاً — يعتمد على المسافة التي قطعها السائق فعلاً وعلى الوقت المنقضي. رحلة ألغيت بعد 3 دقائق تختلف عن أخرى بعد 12.
|
||||
|
||||
**«هل تأخذ الشركة هذا المبلغ؟»**
|
||||
لا. يذهب كاملاً لمحفظة السائق.
|
||||
|
||||
**«ألغى السائق عليّ — هل أُعوَّض؟»**
|
||||
لا يوجد تعويض للراكب حالياً عند إلغاء السائق. لكن السائق كثير الإلغاء يُوقَف 4 ساعات آلياً.
|
||||
|
||||
---
|
||||
|
||||
## مراجع تقنية
|
||||
|
||||
| الموضوع | الملف |
|
||||
|---|---|
|
||||
| رسم إلغاء الراكب | `backend/ride/rides/cancel_fee_helper.php` |
|
||||
| معاينة الرسم قبل الإلغاء | `backend/ride/rides/preview_cancel_fee.php` |
|
||||
| تعويض عدم الحضور + الإيقاف | `backend/ride/rides/cancel_ride_by_driver.php` |
|
||||
| إثبات الوصول | `backend/ride/rides/arrive_ride.php` |
|
||||
| كشف التسرب | `backend/bot/cron_leak_detection.php` |
|
||||
| الأسعار المشتركة | `backend/ride/pricing/pricing_helper.php` |
|
||||
| الترحيلات | `backend/migrations/2026_08_07_*.sql` |
|
||||
@@ -228,6 +228,11 @@ class AppLink {
|
||||
static String get addCancelRideFromPassenger =>
|
||||
"$rideServerSide/cancelRide/add.php";
|
||||
static String get cancelRide => "$rideServerSide/cancelRide/get.php";
|
||||
|
||||
/// معاينة رسم الإلغاء قبل تنفيذه — قراءة خالصة لا تلغي شيئاً.
|
||||
/// تستخدم نفس دالة الحساب التي يستخدمها الإلغاء الفعلي، فما يُعرض
|
||||
/// للراكب هو ما سيُخصم منه بالضبط.
|
||||
static String get previewCancelFee => "$server/ride/rides/preview_cancel_fee.php";
|
||||
//-----------------ridessss------------------
|
||||
static String get addRides => "$rideServerSide/ride/rides/add.php";
|
||||
static String get getRides => "$rideServerSide/ride/rides/get.php";
|
||||
|
||||
@@ -3991,6 +3991,55 @@ class RideLifecycleController extends GetxController {
|
||||
}
|
||||
}
|
||||
|
||||
/// يسأل الخادم كم سيكلّف الإلغاء، ويعرض تأكيداً إن كان هناك رسم.
|
||||
///
|
||||
/// يرجع true للمتابعة وfalse للتراجع. الإعفاءات الثلاثة (لا سائق قَبِل،
|
||||
/// ضمن الدقائق المجانية، السائق تأخّر) يحسمها الخادم فلا حاجة لتكرار
|
||||
/// المنطق هنا — وتكراره كان سيسمح للنسختين بالتباعد.
|
||||
///
|
||||
/// عند أي فشل شبكي نُكمل الإلغاء بلا تحذير: حبس الراكب في رحلة بسبب
|
||||
/// عطل عندنا أسوأ من خصم لم يُحذَّر منه.
|
||||
Future<bool> _confirmCancelFee() async {
|
||||
if (rideId == 'yet' || rideId == null) return true;
|
||||
|
||||
try {
|
||||
final res = await CRUD().post(
|
||||
link: AppLink.previewCancelFee,
|
||||
payload: {"ride_id": rideId.toString()},
|
||||
);
|
||||
|
||||
final data = res is Map ? (res['message'] ?? res['data']) : null;
|
||||
if (data is! Map) return true;
|
||||
|
||||
final bool willCharge = data['will_charge'] == true;
|
||||
if (!willCharge) return true;
|
||||
|
||||
final String amount = data['cancel_fee']?.toString() ?? '0';
|
||||
final String currency = data['fee_currency']?.toString() ?? '';
|
||||
|
||||
bool confirmed = false;
|
||||
await Get.defaultDialog(
|
||||
title: 'تأكيد الإلغاء'.tr,
|
||||
middleText:
|
||||
'السائق في طريقه إليك. سيُخصم $amount $currency كتعويض له عن'
|
||||
' المسافة والوقت.'
|
||||
.tr,
|
||||
textConfirm: 'إلغاء الرحلة'.tr,
|
||||
textCancel: 'تراجع'.tr,
|
||||
onConfirm: () {
|
||||
confirmed = true;
|
||||
Get.back();
|
||||
},
|
||||
onCancel: () => confirmed = false,
|
||||
);
|
||||
|
||||
return confirmed;
|
||||
} catch (e) {
|
||||
Log.print("⚠️ تعذّرت معاينة رسم الإلغاء: $e");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> cancelRide() async {
|
||||
if (selectedReasonIndex == -1) {
|
||||
mySnackbarWarning('Please select a reason first'.tr);
|
||||
@@ -4006,6 +4055,11 @@ class RideLifecycleController extends GetxController {
|
||||
finalReason = otherReasonController.text.trim();
|
||||
}
|
||||
|
||||
// تحذير الرسم قبل أي تفكيك للحالة. لو جاء بعده، ورفض الراكب
|
||||
// الاستمرار، لوجد رحلته وقد أُلغيت على الشاشة بلا رجعة.
|
||||
final bool proceed = await _confirmCancelFee();
|
||||
if (!proceed) return;
|
||||
|
||||
Get.back();
|
||||
if (isCancelRidePageShown) {
|
||||
mapEngine.changeCancelRidePageShow();
|
||||
|
||||
Reference in New Issue
Block a user