From a526dae0426667936f98c1e11061e0cffd25ab60 Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Thu, 9 Jul 2026 04:13:14 +0300 Subject: [PATCH] Update: 2026-07-09 04:13:14 --- backend/core/Auth/RateLimiter.php | 1 + backend/ride/feedBack/add_solve_all.php | 7 ++ backend/ride/rate/add.php | 91 ++++++++++++++----- backend/ride/rate/addRateToDriver.php | 31 +++++++ backend/ride/rides/finish_ride_updates.php | 74 ++++++++++++--- .../home/captin/map_driver_controller.dart | 15 ++- .../lib/controller/rate/rate_conroller.dart | 8 +- siro_rider/pubspec.lock | 7 +- siro_rider/pubspec.yaml | 6 +- 9 files changed, 198 insertions(+), 42 deletions(-) diff --git a/backend/core/Auth/RateLimiter.php b/backend/core/Auth/RateLimiter.php index d3a11056..dd594850 100644 --- a/backend/core/Auth/RateLimiter.php +++ b/backend/core/Auth/RateLimiter.php @@ -17,6 +17,7 @@ class RateLimiter 'api' => ['requests' => 120, 'window' => 60], // 120 طلب / دقيقة 'ride' => ['requests' => 30, 'window' => 60], // 30 طلب / دقيقة 'upload' => ['requests' => 10, 'window' => 300], // 10 رفع / 5 دقائق + 'complaint' => ['requests' => 5, 'window' => 600], // 5 شكاوى / 10 دقائق (كل شكوى تستدعي Gemini + واتساب) ]; public function __construct(?Redis $redis) diff --git a/backend/ride/feedBack/add_solve_all.php b/backend/ride/feedBack/add_solve_all.php index 922c7d30..a410f7d2 100644 --- a/backend/ride/feedBack/add_solve_all.php +++ b/backend/ride/feedBack/add_solve_all.php @@ -5,6 +5,13 @@ // ! تأكد من أن هذا المسار صحيح بالنسبة لهيكل مشروعك require_once __DIR__ . '/../../connect.php'; +// 🔥 [Fix Rate Limit] كل شكوى تستدعي Gemini API (مدفوع) وترسل رسالة واتساب +// لخدمة العملاء — بدون حد كانت قابلة للإغراق (spam) من أي مستخدم مسجّل. +global $limiter, $user_id; +if (isset($limiter)) { + $limiter->enforce(RateLimiter::identifier($user_id), 'complaint'); +} + // --- إعدادات النظام --- $geminiApiKey = getenv("GEMINI_API_KEY"); $customerServiceWhatsapp = getenv("SERVICE_PHONE1"); // يُفترض أن هذا مُعرّف في connect.php أو متغيرات البيئة diff --git a/backend/ride/rate/add.php b/backend/ride/rate/add.php index 2361c992..0e8dc2fd 100644 --- a/backend/ride/rate/add.php +++ b/backend/ride/rate/add.php @@ -1,35 +1,82 @@ prepare($sql); -$stmt->bindParam(':passenger_id', $passenger_id); -$stmt->bindParam(':driverID', $driverID); -$stmt->bindParam(':rideId', $rideId); -$stmt->bindParam(':rating', $rating); -$stmt->bindParam(':comment', $comment); + // 🔥 التحقق من ملكية الرحلة: يجب أن تخص هذا السائق وهذا الراكب فعلاً، + // وأن تكون منتهية، قبل قبول التقييم — يمنع تقييم رحلات عشوائية (IDOR). + $stmtRide = $con->prepare("SELECT driver_id, passenger_id, status FROM ride WHERE id = ?"); + $stmtRide->execute([$rideId]); + $ride = $stmtRide->fetch(PDO::FETCH_ASSOC); -$stmt->execute(); + if (!$ride) { + jsonError("Ride not found"); + exit; + } + if ((string)$ride['driver_id'] !== (string)$driverID) { + jsonError("This ride does not belong to you"); + exit; + } + if ((string)$ride['passenger_id'] !== (string)$passenger_id) { + jsonError("Passenger does not match this ride"); + exit; + } + if ($ride['status'] !== 'Finished') { + jsonError("Ride must be finished before rating"); + exit; + } -if ($stmt->rowCount() > 0) { - jsonSuccess(null, "Rate inserted successfully"); -} else { - jsonError("Failed to save rating information"); + // 🔥 منع تكرار التقييم لنفس الرحلة من نفس السائق + $stmtDup = $con->prepare("SELECT COUNT(*) FROM `ratingPassenger` WHERE rideId = ? AND driverID = ?"); + $stmtDup->execute([$rideId, $driverID]); + if ($stmtDup->fetchColumn() > 0) { + jsonError("This ride has already been rated"); + exit; + } + + $sql = "INSERT INTO `ratingPassenger` ( + `passenger_id`, `driverID`, `rideId`, `rating`, `comment` + ) VALUES ( + :passenger_id, :driverID, :rideId, :rating, :comment + )"; + + $stmt = $con->prepare($sql); + $stmt->bindParam(':passenger_id', $passenger_id); + $stmt->bindParam(':driverID', $driverID); + $stmt->bindParam(':rideId', $rideId); + $stmt->bindParam(':rating', $rating); + $stmt->bindParam(':comment', $comment); + + $stmt->execute(); + + if ($stmt->rowCount() > 0) { + jsonSuccess(null, "Rate inserted successfully"); + } else { + jsonError("Failed to save rating information"); + } + +} catch (PDOException $e) { + error_log("[rate/add] DB Error: " . $e->getMessage() . " | RideID: $rideId"); + jsonError("Database Error: Could not save rating"); +} catch (Exception $e) { + error_log("[rate/add] General Error: " . $e->getMessage()); + jsonError("Error: Could not save rating"); } -?> \ No newline at end of file +?> diff --git a/backend/ride/rate/addRateToDriver.php b/backend/ride/rate/addRateToDriver.php index 886e4598..eaf556b9 100644 --- a/backend/ride/rate/addRateToDriver.php +++ b/backend/ride/rate/addRateToDriver.php @@ -19,6 +19,37 @@ try { throw new Exception("Required fields are missing"); } + // 🔥 التحقق من ملكية الرحلة: يجب أن تخص هذا الراكب وهذا السائق فعلاً، + // وأن تكون منتهية، قبل قبول التقييم — يمنع تقييم رحلات عشوائية (IDOR). + $stmtRide = $con->prepare("SELECT driver_id, passenger_id, status FROM ride WHERE id = ?"); + $stmtRide->execute([$ride_id]); + $ride = $stmtRide->fetch(PDO::FETCH_ASSOC); + + if (!$ride) { + jsonError("Ride not found"); + exit; + } + if ((string)$ride['passenger_id'] !== (string)$passenger_id) { + jsonError("This ride does not belong to you"); + exit; + } + if ((string)$ride['driver_id'] !== (string)$driver_id) { + jsonError("Driver does not match this ride"); + exit; + } + if ($ride['status'] !== 'Finished') { + jsonError("Ride must be finished before rating"); + exit; + } + + // 🔥 منع تكرار التقييم لنفس الرحلة من نفس الراكب + $stmtDup = $con->prepare("SELECT COUNT(*) FROM `ratingDriver` WHERE ride_id = ? AND passenger_id = ?"); + $stmtDup->execute([$ride_id, $passenger_id]); + if ($stmtDup->fetchColumn() > 0) { + jsonError("This ride has already been rated"); + exit; + } + $sql = "INSERT INTO `ratingDriver`( `passenger_id`, `driver_id`, `ride_id`, `rating`, `comment` ) VALUES ( diff --git a/backend/ride/rides/finish_ride_updates.php b/backend/ride/rides/finish_ride_updates.php index b5d1406d..f55f7423 100644 --- a/backend/ride/rides/finish_ride_updates.php +++ b/backend/ride/rides/finish_ride_updates.php @@ -100,8 +100,8 @@ try { try { // Fetch ride data from remote/local DB for server-side calculation $stmtRideData = $con->prepare(" - SELECT id, price AS quoted_price, car_type, - distance AS planned_distance, passenger_id, driver_id + SELECT id, price AS quoted_price, car_type, + distance AS planned_distance, passenger_id, driver_id, price_for_driver FROM ride WHERE id = ? AND driver_id = ? LIMIT 1 "); @@ -117,6 +117,20 @@ try { $kazanPercent = floatval($countryPricing['kazanPercent'] ?? $countryPricing['kazan'] ?? 10); // 🆕 من جدول kazan (kazanPercent هو الاسم الجديد) $carType = $rideData['car_type'] ?? 'Fixed Price'; + // 🔥 [Fix Driver Commission] عند عرض السعر (pricing/get.php) قد تُطبَّق نسبة + // خصم عمولة خاصة بالمنطقة (kazanDiscountFactor من surge:kazan_discounts) + // فتُحفَظ نتيجتها في عمود ride.price_for_driver — وهو "الوعد" الذي رآه + // السائق كـ"أرباحك أعلى" وقت قبول الطلب. إعادة جلب kazanPercent الخام هنا + // كانت تتجاهل ذلك الخصم بالكامل. نشتق نسبة العمولة الفعلية من الفرق + // المحفوظ فعلياً بين السعر المُقتبَس وحصة السائق منه، ونستخدمها بدل + // النسبة الخام، حتى تبقى النسبة المطبقة عند التسوية مطابقة لما وُعد به. + $priceForDriver = floatval($rideData['price_for_driver'] ?? 0); + if ($quotedPrice > 0 && $priceForDriver > 0 && $priceForDriver <= $quotedPrice) { + $effectiveKazanPercent = (($quotedPrice - $priceForDriver) / $quotedPrice) * 100; + error_log("[finish_ride_updates] Using locked commission rate for ride $rideId: {$effectiveKazanPercent}% (was raw {$kazanPercent}%)"); + $kazanPercent = $effectiveKazanPercent; + } + // Fixed-price types, Speed & Awfar: use quoted price as-is $fixedPriceTypes = ['Speed', 'Fixed Price', 'Awfar Car']; if (in_array($carType, $fixedPriceTypes)) { @@ -144,6 +158,19 @@ try { $cleanDist = preg_replace('/[^0-9.]/', '', $actualDistance); $distanceKm = floatval($cleanDist); + // 🔥 [Fix Price Cap] سقف أعلى على الانحراف عن المسافة المخططة — + // actualDistance يأتي من العميل، بدون هذا السقف يمكن لانجراف GPS + // أو قيمة مُتلاعَب بها أن تُضخّم السعر النهائي بلا حدود. نسمح بهامش + // معقول للانحرافات الحقيقية (تحويلة، إغلاق طريق...) ونقصّ الباقي. + $plannedDistanceKm = floatval($rideData['planned_distance'] ?? 0); + if ($plannedDistanceKm > 0) { + $maxAllowedDistanceKm = max($plannedDistanceKm * 1.5, $plannedDistanceKm + 5); + if ($distanceKm > $maxAllowedDistanceKm) { + error_log("[finish_ride_updates] ⚠️ actualDistance ($distanceKm km) exceeds cap ($maxAllowedDistanceKm km) for ride $rideId — planned was $plannedDistanceKm km. Clamping."); + $distanceKm = $maxAllowedDistanceKm; + } + } + if ($distanceKm <= 0) { $finalPrice = $quotedPrice; // fallback } else { @@ -151,6 +178,13 @@ try { $perKmRate = getPerKmRate($carType, $countryPricing); $perMinRate = getPerMinRate($countryPricing); $durationMin = intval(preg_replace('/[^0-9]/', '', $actualDuration)); + // نفس فكرة السقف على المدة: لا نسمح بمدة أكبر من ضعف زمن الرحلة + // المعقول (نفترض حد أقصى واسع 3 ساعات إذا لم تتوفر مدة مخططة) + $maxAllowedDurationMin = 180; + if ($durationMin > $maxAllowedDurationMin) { + error_log("[finish_ride_updates] ⚠️ actualDuration ($durationMin min) exceeds cap ($maxAllowedDurationMin min) for ride $rideId. Clamping."); + $durationMin = $maxAllowedDurationMin; + } $calculated = ($distanceKm * $perKmRate) + ($durationMin * $perMinRate); @@ -161,11 +195,14 @@ try { error_log("[finish_ride_updates] Driver $driver_id has active 0% commission streak!"); } - // السعر النهائي يجب أن يتضمن العمولة دائماً لكي يدفعها الراكب، + // السعر النهائي يجب أن يتضمن العمولة دائماً لكي يدفعها الراكب، // لكن إذا كانت العمولة صفر للسائق، يتم إعطاؤها للسائق بدلاً من الشركة عبر السيرفر المالي. $calculated *= (1 + ($kazanPercent / 100)); - $finalPrice = max($quotedPrice, round($calculated, 2)); + // 🔥 [Fix Price Cap] سقف أعلى مطلق أيضاً على السعر النهائي نفسه + // (دفاع ثانٍ) — لا يتجاوز 1.6 * السعر المُقتبَس أصلاً بأي حال. + $maxAllowedPrice = $quotedPrice > 0 ? $quotedPrice * 1.6 : round($calculated, 2); + $finalPrice = max($quotedPrice, min(round($calculated, 2), $maxAllowedPrice)); } } @@ -182,13 +219,12 @@ try { // 4. Atomic Transaction: Update DBs + Process Payment // ============================================================ try { - // --- Update Remote DB (con_ride) FIRST --- - if (isset($con_ride)) { - $stmtRemote = $con_ride->prepare( - "UPDATE ride SET status = ?, rideTimeFinish = NOW(), price = ? WHERE id = ? AND status = 'Begin'" - ); - $stmtRemote->execute([$newStatus, $finalPrice, $rideId]); - } + // 🔥 [Fix Split-Brain] كان تحديث قاعدة البيانات البعيدة (con_ride) يحدث + // هنا قبل محاولة الدفع وبدون أي Rollback عليه — فإذا فشل الدفع لاحقاً، + // كانت con_ride تبقى 'Finished' بينما المحلية تُرجَع لـ 'Begin'، + // فإعادة محاولة لاحقة تفشل بصمت على con_ride (شرط WHERE status='Begin' + // لم يعد يتحقق) وتُنتج سعرين مختلفين بين القاعدتين. الآن نؤجل تحديث + // con_ride إلى ما بعد نجاح الدفع فعلياً (انظر الأسفل بعد commit()). // --- BEGIN Local DB Transaction --- $con->beginTransaction(); @@ -276,7 +312,21 @@ try { // ✅ Payment succeeded — COMMIT $con->commit(); - + + // 🔥 [Fix Split-Brain] تحديث القاعدة البعيدة الآن فقط، بعد أن أصبح الدفع + // والتحديث المحلي مؤكدَين نجاحهما — يبقي الحالتين متطابقتين دائماً. + // فشل هذا التحديث best-effort فقط (لا يُرجع الرحلة المحلية المُنجَزة فعلاً). + if (isset($con_ride)) { + try { + $stmtRemote = $con_ride->prepare( + "UPDATE ride SET status = ?, rideTimeFinish = NOW(), price = ? WHERE id = ? AND status = 'Begin'" + ); + $stmtRemote->execute([$newStatus, $finalPrice, $rideId]); + } catch (PDOException $e) { + error_log("[finish_ride_updates] Remote DB (con_ride) update failed for ride $rideId: " . $e->getMessage()); + } + } + // 🆕 Update Driver Streak (Increment because ride is finished successfully) handleDriverStreak($con, $driver_id, 'increment'); diff --git a/siro_driver/lib/controller/home/captin/map_driver_controller.dart b/siro_driver/lib/controller/home/captin/map_driver_controller.dart index 9d3d3c79..40f3b027 100755 --- a/siro_driver/lib/controller/home/captin/map_driver_controller.dart +++ b/siro_driver/lib/controller/home/captin/map_driver_controller.dart @@ -1396,14 +1396,25 @@ class MapDriverController extends GetxController box.remove(BoxName.passengerID); box.remove(BoxName.rideId); + // 🔥 [Fix Actual Distance] نُرسل المسافة/المدة الفعليتين المُتراكمتين + // من GPS الحي (currentRideDistanceKm/_rideStartTime) بدل الحقلين + // الثابتين (distance/duration) اللذين يمثلان المسار المخطط أصلاً + // ولا يُحدَّثان أبداً أثناء الرحلة — كانا يُبطلان فعلياً حماية + // "إعادة حساب السعر من المسافة الفعلية" في الباك إند. + final double actualDistanceKm = + currentRideDistanceKm > 0 ? currentRideDistanceKm : safeParseDouble(distance); + final int actualDurationMinutes = _rideStartTime != null + ? (DateTime.now().difference(_rideStartTime!).inSeconds / 60).ceil() + : safeParseInt(duration); + // تجهيز البيانات الخام الموحدة للسيرفر ليقوم بمعالجة الدفع والإنهاء معاً بنظام المعاملة الواحدة final finishPayload = { 'rideId': rideId.toString(), 'driver_id': box.read(BoxName.driverID).toString(), 'passengerId': passengerId.toString(), 'status': 'Finished', - 'actualDistance': distance.toString(), - 'actualDuration': duration.toString(), + 'actualDistance': actualDistanceKm.toString(), + 'actualDuration': actualDurationMinutes.toString(), 'walletChecked': walletChecked.toString(), 'passengerWalletBurc': passengerWalletBurc.toString(), 'passengerToken': tokenPassenger.toString(), diff --git a/siro_driver/lib/controller/rate/rate_conroller.dart b/siro_driver/lib/controller/rate/rate_conroller.dart index cd6686d7..5aa4f642 100755 --- a/siro_driver/lib/controller/rate/rate_conroller.dart +++ b/siro_driver/lib/controller/rate/rate_conroller.dart @@ -1,5 +1,6 @@ import 'package:siro_driver/controller/firebase/firbase_messge.dart'; import 'package:siro_driver/controller/home/captin/map_driver_controller.dart'; +import 'package:siro_driver/print.dart'; import 'package:siro_driver/views/widgets/error_snakbar.dart'; import 'package:flutter/cupertino.dart'; import 'package:get/get.dart'; @@ -112,13 +113,18 @@ class RateController extends GetxController { middleText: '', confirm: MyElevatedButton(title: 'Ok', onPressed: () => Get.back())); } else { - await CRUD().post(link: "${AppLink.server}/ride/rate/add.php", payload: { + final rateResponse = + await CRUD().post(link: "${AppLink.server}/ride/rate/add.php", payload: { 'passenger_id': passengerId, 'driverID': box.read(BoxName.driverID).toString(), 'rideId': rideId.toString(), 'rating': selectedRateItemId.toString(), 'comment': comment.text ?? 'none', }); + // 🔥 لا نمنع إكمال الرحلة إذا فشل التقييم، لكن يجب تسجيله بدل تجاهله بصمت + if (rateResponse is Map && rateResponse['status'] != 'success') { + Log.print('⚠️ Failed to submit passenger rating: $rateResponse'); + } CRUD().sendEmail(AppLink.sendEmailToPassengerForTripDetails, { 'startLocation': diff --git a/siro_rider/pubspec.lock b/siro_rider/pubspec.lock index 2f1e1ab2..ea58b2d8 100644 --- a/siro_rider/pubspec.lock +++ b/siro_rider/pubspec.lock @@ -1056,10 +1056,9 @@ packages: intaleq_maps: dependency: "direct main" description: - name: intaleq_maps - sha256: b74c4e6f1d890f81bf253c4d3996db53149b64fcbf9869b279d417067f73128b - url: "https://pub.dev" - source: hosted + path: "../../map-saas/packages/flutter-sdk" + relative: true + source: path version: "2.2.0" internet_connection_checker: dependency: "direct main" diff --git a/siro_rider/pubspec.yaml b/siro_rider/pubspec.yaml index 1a400a2c..131e43f6 100644 --- a/siro_rider/pubspec.yaml +++ b/siro_rider/pubspec.yaml @@ -78,7 +78,11 @@ dependencies: internet_connection_checker: ^3.0.1 connectivity_plus: ^6.1.5 app_links: ^7.0.0 - intaleq_maps: ^2.2.0 + # 🔧 مؤقتاً: نستخدم نفس نسخة الحزمة المحلية المُصلَحة التي يستخدمها تطبيق + # السائق (بدل النسخة 2.2.0 المنشورة على pub.dev) لاختبار إصلاح مقارنة + # Polyline/Marker على التطبيقين معاً قبل رفع نسخة رسمية جديدة على pub.dev. + intaleq_maps: + path: ../../map-saas/packages/flutter-sdk/ socket_io_client: 1.0.2 # home_widget: ^0.7.0+1