Files
Siro/backend/ride/rides/cancel_fee_helper.php
T

189 lines
8.9 KiB
PHP
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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,
&$currency = null
): float {
// ‏العملة تُحسم من الدولة دائماً، حتى مع الإعفاء: التطبيق يحتاجها ليعرض
// ‏"الإلغاء مجاني" أو "سيُخصم 1.20 دينار" بوحدة صحيحة.
$currency = 'SYP';
// ── إعفاء ١: لا سائق قَبِل بعد ──────────────────────────
// ‏لا أحد تحرّك، فلا أحد يستحق تعويضاً. وهذه أغلب حالات الإلغاء.
if (empty($driverId) || (string) $driverId === '0') {
$exemptReason = 'no_driver_assigned';
$rowCur = getKazanForRide($con, $rideId);
$currency = !empty($rowCur['currency'])
? (string) $rowCur['currency']
: getCurrencyByCountry((string) ($rowCur['country'] ?? 'Syria'));
return 0.0;
}
// ── سياسة الدولة ────────────────────────────────────────
// ‏أسعار دولة هذه الرحلة تحديداً — لا أول صف في الجدول.
$kazan = getKazanForRide($con, $rideId, 'Syria', $driverId);
// ‏العمود currency هو المرجع، وgetCurrencyByCountry احتياط إن كان فارغاً.
$currency = !empty($kazan['currency'])
? (string) $kazan['currency']
: getCurrencyByCountry((string) ($kazan['country'] ?? 'Syria'));
$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 $currency (سقف $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;
}
$kazanRow = getKazanForRide($con, $rideId, 'Syria', $driverId);
$country = strtolower((string) ($kazanRow['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)");
}
}