Update: 2026-08-07 02:32:14

This commit is contained in:
Hamza-Ayed
2026-08-07 02:32:15 +03:00
parent 3081aea802
commit 70772cf0d2
6 changed files with 283 additions and 5 deletions
+19 -2
View File
@@ -31,12 +31,24 @@ function computePassengerCancelFee(
$rideId, $rideId,
array $ride, array $ride,
$driverId, $driverId,
string &$exemptReason string &$exemptReason,
&$currency = null
): float { ): float {
// ‏العملة تُحسم من الدولة دائماً، حتى مع الإعفاء: التطبيق يحتاجها ليعرض
// ‏"الإلغاء مجاني" أو "سيُخصم 1.20 دينار" بوحدة صحيحة.
$currency = 'SYP';
// ── إعفاء ١: لا سائق قَبِل بعد ────────────────────────── // ── إعفاء ١: لا سائق قَبِل بعد ──────────────────────────
// ‏لا أحد تحرّك، فلا أحد يستحق تعويضاً. وهذه أغلب حالات الإلغاء. // ‏لا أحد تحرّك، فلا أحد يستحق تعويضاً. وهذه أغلب حالات الإلغاء.
if (empty($driverId) || (string) $driverId === '0') { if (empty($driverId) || (string) $driverId === '0') {
$exemptReason = 'no_driver_assigned'; $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; return 0.0;
} }
@@ -45,6 +57,11 @@ function computePassengerCancelFee(
$stmtKazan->execute(); $stmtKazan->execute();
$kazan = $stmtKazan->fetch(PDO::FETCH_ASSOC) ?: []; $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); $cap = (float) ($kazan['passengerCancelCap'] ?? 0);
if ($cap <= 0) { if ($cap <= 0) {
// ‏الرسم معطّل لهذه الدولة — الافتراضي حتى تضبطه الإدارة. // ‏الرسم معطّل لهذه الدولة — الافتراضي حتى تضبطه الإدارة.
@@ -103,7 +120,7 @@ function computePassengerCancelFee(
$fee = round(min($fee, $cap, PASSENGER_CANCEL_ABSOLUTE_CAP), 2); $fee = round(min($fee, $cap, PASSENGER_CANCEL_ABSOLUTE_CAP), 2);
error_log("[cancel_fee] ride=$rideId رسم إلغاء: {$approachKm}كم" error_log("[cancel_fee] ride=$rideId رسم إلغاء: {$approachKm}كم"
. " + {$billableMinutes}د = $fee (سقف $cap)"); . " + {$billableMinutes}د = $fee $currency (سقف $cap)");
return max(0.0, $fee); return max(0.0, $fee);
} }
@@ -79,9 +79,11 @@ try {
// ================================================================= // =================================================================
$cancelFee = 0.0; $cancelFee = 0.0;
$feeExemptReason = ''; $feeExemptReason = '';
$feeCurrency = 'SYP';
try { try {
$cancelFee = computePassengerCancelFee( $cancelFee = computePassengerCancelFee(
$con, $redisLocation ?? null, $rideId, $ride, $driverId, $feeExemptReason $con, $redisLocation ?? null, $rideId, $ride, $driverId,
$feeExemptReason, $feeCurrency
); );
} catch (Throwable $eFee) { } catch (Throwable $eFee) {
// ‏فشل حساب الرسم لا يمنع الإلغاء أبداً — الإلغاء حق للراكب. // ‏فشل حساب الرسم لا يمنع الإلغاء أبداً — الإلغاء حق للراكب.
@@ -202,6 +204,7 @@ try {
// ‏الراكب لاحقاً هو أسرع طريق لشكوى ولمراجعة سيئة. // ‏الراكب لاحقاً هو أسرع طريق لشكوى ولمراجعة سيئة.
jsonSuccess([ jsonSuccess([
'cancel_fee' => $cancelFee ?? 0, 'cancel_fee' => $cancelFee ?? 0,
'fee_currency' => $feeCurrency ?? 'SYP',
'fee_exempt' => ($cancelFee ?? 0) > 0 ? '' : ($feeExemptReason ?? ''), 'fee_exempt' => ($cancelFee ?? 0) > 0 ? '' : ($feeExemptReason ?? ''),
], "Ride cancelled successfully"); ], "Ride cancelled successfully");
+54
View File
@@ -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);
}
+145
View File
@@ -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` |
+5
View File
@@ -228,6 +228,11 @@ class AppLink {
static String get addCancelRideFromPassenger => static String get addCancelRideFromPassenger =>
"$rideServerSide/cancelRide/add.php"; "$rideServerSide/cancelRide/add.php";
static String get cancelRide => "$rideServerSide/cancelRide/get.php"; static String get cancelRide => "$rideServerSide/cancelRide/get.php";
/// معاينة رسم الإلغاء قبل تنفيذه — قراءة خالصة لا تلغي شيئاً.
/// تستخدم نفس دالة الحساب التي يستخدمها الإلغاء الفعلي، فما يُعرض
/// للراكب هو ما سيُخصم منه بالضبط.
static String get previewCancelFee => "$server/ride/rides/preview_cancel_fee.php";
//-----------------ridessss------------------ //-----------------ridessss------------------
static String get addRides => "$rideServerSide/ride/rides/add.php"; static String get addRides => "$rideServerSide/ride/rides/add.php";
static String get getRides => "$rideServerSide/ride/rides/get.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 { Future<void> cancelRide() async {
if (selectedReasonIndex == -1) { if (selectedReasonIndex == -1) {
mySnackbarWarning('Please select a reason first'.tr); mySnackbarWarning('Please select a reason first'.tr);
@@ -4006,6 +4055,11 @@ class RideLifecycleController extends GetxController {
finalReason = otherReasonController.text.trim(); finalReason = otherReasonController.text.trim();
} }
// ‏تحذير الرسم قبل أي تفكيك للحالة. لو جاء بعده، ورفض الراكب
// ‏الاستمرار، لوجد رحلته وقد أُلغيت على الشاشة بلا رجعة.
final bool proceed = await _confirmCancelFee();
if (!proceed) return;
Get.back(); Get.back();
if (isCancelRidePageShown) { if (isCancelRidePageShown) {
mapEngine.changeCancelRidePageShow(); mapEngine.changeCancelRidePageShow();