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"); } ?>