Update: 2026-08-07 02:19:07

This commit is contained in:
Hamza-Ayed
2026-08-07 02:19:07 +03:00
parent b492034bad
commit 3081aea802
3 changed files with 274 additions and 4 deletions
+181
View File
@@ -0,0 +1,181 @@
<?php
// ============================================================
// cancel_fee_helper.php — رسم إلغاء الراكب بعد قبول السائق
//
// ‏المبدأ نفسه المطبَّق في تعويض عدم الحضور: من ضيّع وقت الآخر يعوّضه،
// ‏والمال يذهب للسائق لا للشركة. الفرق الوحيد أن الزمن هنا يُحتسب من
// ‏لحظة القبول — السائق كان يقود نحو الراكب لا واقفاً ينتظره.
//
// ‏لا شيء من المبلغ يأتي من العميل. كل المدخلات من الخادم:
// • مسافة الاقتراب ← Redis (سجّلها acceptRide.php لحظة القبول)
// • زمن الانتظار ← ride.rideTimeStart (يكتبه acceptRide.php)
// • الأسعار ← جدول kazan عبر pricing_helper.php
// ============================================================
// ‏سقف صلب فوق أي قيمة في القاعدة — يمنع خطأً إدارياً من خصم كارثي.
const PASSENGER_CANCEL_ABSOLUTE_CAP = 50000.0;
// ‏حد أقصى للدقائق المحتسبة: سائق قَبِل ونسي الرحلة ساعة لا يتحول
// ‏نسيانه إلى فاتورة على الراكب.
const PASSENGER_CANCEL_MAX_BILLABLE_MINUTES = 20;
/**
* ‏يحسب رسم الإلغاء، أو صفراً مع سبب الإعفاء.
*
* @param string $exemptReason يُملأ بسبب الإعفاء إن كان الرسم صفراً (بالمرجع)
* @return float ‏المبلغ المستحق للسائق
*/
function computePassengerCancelFee(
PDO $con,
$redisLocation,
$rideId,
array $ride,
$driverId,
string &$exemptReason
): float {
// ── إعفاء ١: لا سائق قَبِل بعد ──────────────────────────
// ‏لا أحد تحرّك، فلا أحد يستحق تعويضاً. وهذه أغلب حالات الإلغاء.
if (empty($driverId) || (string) $driverId === '0') {
$exemptReason = 'no_driver_assigned';
return 0.0;
}
// ── سياسة الدولة ────────────────────────────────────────
$stmtKazan = $con->prepare("SELECT * FROM kazan LIMIT 1");
$stmtKazan->execute();
$kazan = $stmtKazan->fetch(PDO::FETCH_ASSOC) ?: [];
$cap = (float) ($kazan['passengerCancelCap'] ?? 0);
if ($cap <= 0) {
// ‏الرسم معطّل لهذه الدولة — الافتراضي حتى تضبطه الإدارة.
$exemptReason = 'policy_disabled';
return 0.0;
}
$freeMinutes = (int) ($kazan['passengerCancelFreeMinutes'] ?? 2);
$lateExemptMinutes = (int) ($kazan['driverLateExemptMinutes'] ?? 10);
// ── الزمن منذ القبول ────────────────────────────────────
// ‏acceptRide.php يكتب rideTimeStart = NOW() لحظة فوز السائق بالقفل.
$acceptedAt = !empty($ride['rideTimeStart']) ? strtotime($ride['rideTimeStart']) : 0;
if ($acceptedAt <= 0) {
$exemptReason = 'accept_time_unknown';
return 0.0;
}
$minutesSinceAccept = (int) floor((time() - $acceptedAt) / 60);
// ── إعفاء ٢: نافذة الإلغاء المجاني ──────────────────────
// ‏الراكب يغيّر رأيه أو يخطئ الوجهة — دقيقتان لتصحيح ذلك بلا عقاب.
if ($minutesSinceAccept < $freeMinutes) {
$exemptReason = 'within_free_window';
return 0.0;
}
// ── حالة الوصول من Redis ────────────────────────────────
$approachKm = 0.0;
$arrivedTs = 0;
if ($redisLocation) {
try {
$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 $e) {
error_log("[cancel_fee] تعذّرت قراءة حالة الرحلة: " . $e->getMessage());
}
}
// ── إعفاء ٣: السائق تأخّر ولم يصل ───────────────────────
// ‏أهم الإعفاءات الثلاثة: بدونه نعاقب الراكب على بطء السائق، وهذا
// ‏يطرد الركاب أسرع مما يوفّره الرسم.
if ($arrivedTs <= 0 && $minutesSinceAccept >= $lateExemptMinutes) {
$exemptReason = 'driver_late';
return 0.0;
}
// ── الحساب ──────────────────────────────────────────────
$billableMinutes = min($minutesSinceAccept, PASSENGER_CANCEL_MAX_BILLABLE_MINUTES);
$carType = (string) ($ride['carType'] ?? $ride['car_type'] ?? 'Speed');
$fee = ($approachKm * getPerKmRate($carType, $kazan))
+ ($billableMinutes * getPerMinRate($kazan));
$fee = round(min($fee, $cap, PASSENGER_CANCEL_ABSOLUTE_CAP), 2);
error_log("[cancel_fee] ride=$rideId رسم إلغاء: {$approachKm}كم"
. " + {$billableMinutes}د = $fee (سقف $cap)");
return max(0.0, $fee);
}
/**
* ‏ينفّذ التحويل: خصم من الراكب وإضافة للسائق.
*
* ‏يُستدعى بعد الـcommit — نداءات S2S شبكية لا تُترك داخل معاملة مفتوحة.
* ‏لا يرمي أبداً: فشل التحويل يُسجَّل للتسوية ولا يُبطل إلغاءً تمّ فعلاً.
*/
function applyCancelFeeTransfer(PDO $con, $passengerId, $driverId, $rideId, float $fee): void
{
if ($fee <= 0 || empty($passengerId) || empty($driverId)) {
return;
}
try {
$stmtKazan = $con->prepare("SELECT country FROM kazan LIMIT 1");
$stmtKazan->execute();
$country = strtolower((string) ($stmtKazan->fetchColumn() ?: 'jordan'));
} catch (PDOException $e) {
$country = 'jordan';
}
$walletServer = "https://walletintaleq.intaleq.xyz";
if ($country === 'jordan') {
$walletServer = getenv('WALLET_SERVER_JORDAN') ?: $walletServer;
} elseif ($country === 'egypt') {
$walletServer = getenv('WALLET_SERVER_EGYPT') ?: $walletServer;
} else {
$walletServer = getenv('WALLET_SERVER_SYRIA') ?: $walletServer;
}
$s2s = function (string $url, array $fields): int {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($fields),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 5,
CURLOPT_HTTPHEADER => [
'Content-Type: application/x-www-form-urlencoded',
'X-S2S-Api-Key: ' . getenv('S2S_SHARED_KEY'),
],
]);
curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
return (int) $code;
};
// ‏خصم من الراكب (دين سالب)
$debtCode = $s2s(
"$walletServer/v2/main/ride/passengerWallet/add_s2s_debt.php",
['passengerID' => $passengerId, 'amount' => -$fee]
);
// ‏إضافة للسائق — paymentID مشتق من الرحلة فيمنع ازدواج الصرف
$rewardCode = $s2s(
"$walletServer/v2/main/ride/driverWallet/add_s2s_reward.php",
[
'driverID' => $driverId,
'paymentID' => "pax_cancel_$rideId",
'amount' => $fee,
'paymentMethod' => 'passenger_cancel_compensation',
]
);
if ($debtCode !== 200 || $rewardCode !== 200) {
// ‏أي طرف فشل يعني اختلالاً يجب أن يظهر في التسوية، لا أن يمرّ صامتاً.
error_log("[cancel_fee] MONEY: رسم $fee للرحلة $rideId —"
. " خصم الراكب=$debtCode، رصيد السائق=$rewardCode"
. " (راكب=$passengerId سائق=$driverId)");
}
}