189 lines
8.9 KiB
PHP
189 lines
8.9 KiB
PHP
<?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)");
|
||
}
|
||
}
|