213 lines
9.0 KiB
PHP
213 lines
9.0 KiB
PHP
<?php
|
||
require_once __DIR__ . '/../../connect.php';
|
||
require_once __DIR__ . '/../pricing/pricing_helper.php';
|
||
require_once __DIR__ . '/cancel_fee_helper.php';
|
||
|
||
try {
|
||
$con_ride = Database::get('ride');
|
||
} catch (Exception $e) {
|
||
error_log("[cancel_ride_by_passenger] Failed to connect to Ride Database: " . $e->getMessage());
|
||
}
|
||
|
||
// cancel_ride_by_passenger.php
|
||
|
||
$rideId = filterRequest("ride_id");
|
||
$reason = filterRequest("reason");
|
||
|
||
if (!$rideId) {
|
||
jsonError("Missing Ride ID");
|
||
exit;
|
||
}
|
||
|
||
try {
|
||
// SELECT * لا قائمة أعمدة: نحتاج rideTimeStart و carType لحساب الرسم،
|
||
// وأسماء بعض الأعمدة متضاربة بين الschema والقاعدة الحية (carType /
|
||
// car_type). النجمة تجعل الاستعلام يعمل في كل الحالات.
|
||
$stmt = $con->prepare("SELECT * FROM ride WHERE id = ?");
|
||
$stmt->execute([$rideId]);
|
||
$ride = $stmt->fetch(PDO::FETCH_ASSOC);
|
||
|
||
if (!$ride) {
|
||
jsonError("Ride not found");
|
||
exit;
|
||
}
|
||
|
||
$driverId = $ride['driver_id'];
|
||
$currentStatus = $ride['status'];
|
||
|
||
if ($currentStatus == 'Begin') {
|
||
jsonError("Cannot cancel started ride");
|
||
exit;
|
||
}
|
||
|
||
// =================================================================
|
||
// 1. تحديث قواعد البيانات (Transaction)
|
||
// =================================================================
|
||
$con->beginTransaction();
|
||
|
||
// تحديث waitingRides
|
||
$updateWaiting = $con->prepare("UPDATE waitingRides SET status = ? WHERE id = ?");
|
||
$updateWaiting->execute(['cancelled_by_passenger', $rideId]);
|
||
|
||
// تحديث ride (محلي)
|
||
$updateRide = $con->prepare("UPDATE ride SET status = ?, updated_at = NOW() WHERE id = ?");
|
||
$updateRide->execute(['cancelled_by_passenger', $rideId]);
|
||
|
||
// تحديث driver_orders
|
||
if ($driverId > 0) {
|
||
$updateOrder = $con->prepare("UPDATE driver_orders SET status = 'cancelled_by_passenger', notes = ? WHERE order_id = ? AND driver_id = ?");
|
||
$updateOrder->execute([$reason, $rideId, $driverId]);
|
||
}
|
||
|
||
// إدراج سبب الإلغاء في جدول canecl المخصص
|
||
if ($driverId > 0 && !empty($reason)) {
|
||
$passengerId = $ride['passenger_id'] ?? '0';
|
||
$insertCanecl = $con->prepare(
|
||
"INSERT INTO canecl (driverID, passengerID, rideID, note) VALUES (?, ?, ?, ?)"
|
||
);
|
||
$insertCanecl->execute([$driverId, $passengerId, $rideId, $reason]);
|
||
}
|
||
|
||
// =================================================================
|
||
// 1.5 رسم الإلغاء — تعويض للسائق لا إيراد للشركة
|
||
//
|
||
// متماثل مع تعويض عدم الحضور في cancel_ride_by_driver.php: من ضيّع
|
||
// وقت الآخر يعوّضه. الفرق أن الزمن يُحتسب من لحظة القبول لا من لحظة
|
||
// الوصول — السائق كان في طريقه لا واقفاً ينتظر.
|
||
//
|
||
// المبلغ لا يأتي من العميل بأي حال، ويُحسب من أسعار kazan نفسها.
|
||
// =================================================================
|
||
$cancelFee = 0.0;
|
||
$feeExemptReason = '';
|
||
try {
|
||
$cancelFee = computePassengerCancelFee(
|
||
$con, $redisLocation ?? null, $rideId, $ride, $driverId, $feeExemptReason
|
||
);
|
||
} catch (Throwable $eFee) {
|
||
// فشل حساب الرسم لا يمنع الإلغاء أبداً — الإلغاء حق للراكب.
|
||
error_log("[cancel_ride_by_passenger] تعذّر حساب الرسم: " . $eFee->getMessage());
|
||
}
|
||
|
||
if ($cancelFee > 0) {
|
||
// عدّاد عدم الحضور — للتقارير وقرارات الحظر اليدوية.
|
||
try {
|
||
$con->prepare("UPDATE passengers SET noShowCount = noShowCount + 1 WHERE id = ?")
|
||
->execute([$ride['passenger_id'] ?? '']);
|
||
} catch (PDOException $eCnt) {
|
||
error_log("[cancel_ride_by_passenger] عدّاد عدم الحضور غير مُرحَّل بعد: "
|
||
. $eCnt->getMessage());
|
||
}
|
||
}
|
||
|
||
$con->commit();
|
||
|
||
// التحويل المالي بعد الـcommit لا داخله: نداءات S2S شبكية قد تستغرق
|
||
// ثوانٍ، وإبقاء المعاملة مفتوحة خلالها يقفل صفوف ride تحت ضغط.
|
||
if ($cancelFee > 0) {
|
||
applyCancelFeeTransfer(
|
||
$con, $ride['passenger_id'] ?? '', $driverId, $rideId, $cancelFee
|
||
);
|
||
}
|
||
|
||
// تحديث السيرفر البعيد (Remote DB)
|
||
if (isset($con_ride)) {
|
||
try {
|
||
$updateRide2 = $con_ride->prepare("UPDATE ride SET status = ?, updated_at = NOW() WHERE id = ?");
|
||
$updateRide2->execute(['cancelled_by_passenger', $rideId]);
|
||
} catch (PDOException $e) {
|
||
error_log("Secondary DB update failed: " . $e->getMessage());
|
||
}
|
||
}
|
||
|
||
// =================================================================
|
||
// 2. إشعار السائق/السائقين (Socket + FCM)
|
||
// =================================================================
|
||
// 🔥 يُرسل دائماً بغض النظر عن driver_id — إذا كانت الرحلة لم تُقبل بعد
|
||
// (driver_id = 0)، لوكيشن سيرفر يستخدم ride:offered_drivers:{rideId}
|
||
// من Redis لإشعار كل السائقين الذين وصلهم عرض هذه الرحلة أصلاً.
|
||
$socketUrl = getenv('LOCATION_SERVER_URL') ?: 'http://socket_driver:2021';
|
||
$internalKeyPath = getenv('INTERNAL_SOCKET_KEY_PATH') ?: '';
|
||
$internalKey = ($internalKeyPath && file_exists($internalKeyPath)) ? trim(file_get_contents($internalKeyPath)) : (getenv('INTERNAL_SOCKET_KEY') ?: '');
|
||
|
||
$ch = curl_init($socketUrl);
|
||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||
curl_setopt($ch, CURLOPT_POST, true);
|
||
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
|
||
'action' => 'cancel_ride',
|
||
'driver_id' => $driverId,
|
||
'ride_id' => $rideId,
|
||
'reason' => $reason
|
||
]));
|
||
if (!empty($internalKey)) curl_setopt($ch, CURLOPT_HTTPHEADER, ["x-internal-key: $internalKey"]);
|
||
curl_setopt($ch, CURLOPT_TIMEOUT_MS, 500);
|
||
curl_setopt($ch, CURLOPT_NOSIGNAL, 1);
|
||
@curl_exec($ch);
|
||
curl_close($ch);
|
||
|
||
if ($driverId > 0) {
|
||
// FCM للسائق الذي قَبِل الرحلة فعلاً (باستخدام الدالة الجديدة مع فك التشفير)
|
||
$driverToken = filterRequest("driver_token");
|
||
|
||
if (empty($driverToken)) {
|
||
$stmtToken = $con->prepare("SELECT token FROM driverToken WHERE captain_id = ?");
|
||
$stmtToken->execute([$driverId]);
|
||
$rawToken = $stmtToken->fetchColumn();
|
||
|
||
if ($rawToken) {
|
||
$driverToken = $rawToken;
|
||
|
||
// 🔥 محاولة فك التشفير (لأن التوكنات غالباً مشفرة)
|
||
if (!empty($encryptionHelper)) {
|
||
try {
|
||
$decrypted = $encryptionHelper->decryptData($rawToken);
|
||
if ($decrypted !== false && !empty($decrypted)) {
|
||
$driverToken = trim($decrypted);
|
||
}
|
||
} catch (Exception $e) {
|
||
// في حال الفشل نستخدم الخام
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if (!empty($driverToken)) {
|
||
// تجهيز البيانات
|
||
$fcmData = [
|
||
'category' => 'Cancel Trip',
|
||
'ride_id' => (string)$rideId,
|
||
'reason' => $reason
|
||
];
|
||
|
||
// إرسال الإشعار
|
||
sendFCM_Internal(
|
||
$driverToken, // الهدف
|
||
"إلغاء الرحلة 🚫", // العنوان
|
||
"قام الراكب بإلغاء الرحلة: $reason", // النص
|
||
$fcmData, // البيانات
|
||
'Cancel Trip', // التصنيف
|
||
false // ليس Topic
|
||
);
|
||
}
|
||
}
|
||
|
||
// 🆕 Cache ride state in Redis
|
||
sendToLocationServer('update_ride_state', [
|
||
'ride_id' => $rideId,
|
||
'status' => 'cancelled_by_passenger',
|
||
'driver_id' => $driverId ?? '',
|
||
'passenger_id' => $ride['passenger_id'] ?? '',
|
||
]);
|
||
|
||
// الرسم يُرجَع للتطبيق ليعرضه صراحةً. خصم صامت من المحفظة يكتشفه
|
||
// الراكب لاحقاً هو أسرع طريق لشكوى ولمراجعة سيئة.
|
||
jsonSuccess([
|
||
'cancel_fee' => $cancelFee ?? 0,
|
||
'fee_exempt' => ($cancelFee ?? 0) > 0 ? '' : ($feeExemptReason ?? ''),
|
||
], "Ride cancelled successfully");
|
||
|
||
} catch (PDOException $e) {
|
||
if ($con->inTransaction()) $con->rollBack();
|
||
error_log("Cancel ride error: " . $e->getMessage());
|
||
jsonError("Database error occurred");
|
||
}
|
||
?>
|