From 6876d9bc544418b1838d5076ed8637dad9fa3283 Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Thu, 2 Jul 2026 17:44:59 +0300 Subject: [PATCH] Update: 2026-07-02 17:44:58 --- backend/ride/pricing/get.php | 11 +- backend/ride/rides/add_ride.php | 94 +++++--- backend/ride/rides/retry_search_drivers.php | 204 ++++++++++++++++-- .../home/captin/order_request_controller.dart | 6 + siro_rider/lib/constant/box_name.dart | 1 + .../home/map/ride_lifecycle_controller.dart | 179 +++++++++++---- 6 files changed, 405 insertions(+), 90 deletions(-) diff --git a/backend/ride/pricing/get.php b/backend/ride/pricing/get.php index ce68f516..0ccd5001 100644 --- a/backend/ride/pricing/get.php +++ b/backend/ride/pricing/get.php @@ -586,10 +586,11 @@ if (isset($encryptionHelper)) { } echo json_encode([ - 'status' => 'success', - 'data' => $prices, - 'price_token' => $priceToken, - 'applied_discount' => $discount, - 'added_negative_balance' => $negativeBalance + 'status' => 'success', + 'data' => $prices, + 'price_token' => $priceToken, + 'applied_discount' => $discount, + 'added_negative_balance' => $negativeBalance, + 'is_destination_match' => $isDestinationMatch ? 1 : 0, ]); ?> diff --git a/backend/ride/rides/add_ride.php b/backend/ride/rides/add_ride.php index abab843a..6b5df6b5 100644 --- a/backend/ride/rides/add_ride.php +++ b/backend/ride/rides/add_ride.php @@ -291,32 +291,76 @@ try { // Direct dispatch للسائقين القريبين $driversData = findBestDrivers($con, $startLat, $startLng, $carType, $endLat, $endLng); - // 🆕 Destination Matching Priority - if ($is_destination_match && $matched_driver_id) { - $found = false; - foreach ($driversData as $k => $d) { - if ($d['captain_id'] == $matched_driver_id) { - // Remove from current position and move to the very front - unset($driversData[$k]); - $d['is_destination_match'] = 1; - array_unshift($driversData, $d); - $found = true; - break; - } - } - - // If driver wasn't nearby but we still want to give it to them because it's their route - if (!$found) { - $stmtD = $con->prepare("SELECT token FROM driverToken WHERE captain_id = :d"); - $stmtD->execute([':d' => $matched_driver_id]); - $dT = $stmtD->fetchColumn(); - if ($dT) { - array_unshift($driversData, [ - 'captain_id' => $matched_driver_id, - 'token' => $dT, - 'is_destination_match' => 1 - ]); + // ═══════════════════════════════════════════════════════════════ + // 🆕 Destination Matching Priority — Multi-Driver Support + // نبحث عن كل السائقين الذين وجهتهم تطابق منطقة الإنزال + // (ليس فقط الأول، بل جميعهم) ونضعهم في مقدمة القائمة + // الخصم 14% تم تطبيقه مسبقاً في التسعير — لا خصم إضافي هنا + // ═══════════════════════════════════════════════════════════════ + if ($is_destination_match && isset($redis)) { + try { + // Query ALL drivers whose destination is near the drop-off (up to 10) + $allMatchedIds = $redis->geoRadius( + 'geo:driver:destinations', + (float)$destLng, + (float)$destLat, + 3.5, + 'km', + ['COUNT' => 10, 'ASC'] + ); + + if (!empty($allMatchedIds)) { + // Build a lookup set for fast de-duplication + $alreadyInList = []; + foreach ($driversData as $d) { + $alreadyInList[$d['captain_id'] ?? $d['driver_id'] ?? ''] = true; + } + + $matchedToFront = []; + foreach ($allMatchedIds as $mid) { + $mid = (string)$mid; + // Validate still active today + $detailJson = $redis->get("driver:destination:{$mid}"); + $detail = $detailJson ? json_decode($detailJson, true) : null; + if (!$detail || empty($detail['is_active']) || ($detail['usage_date'] ?? '') !== date('Y-m-d')) { + $redis->zRem('geo:driver:destinations', $mid); + continue; + } + // Mark with destination flag for special FCM title "في طريقك 📍" + if (isset($alreadyInList[$mid])) { + // Already in list — mark it and move to front + foreach ($driversData as $k => $d) { + $did = $d['captain_id'] ?? $d['driver_id'] ?? ''; + if ((string)$did === $mid) { + $driversData[$k]['is_destination_match'] = 1; + $matchedToFront[] = $driversData[$k]; + unset($driversData[$k]); + break; + } + } + } else { + // Not nearby but their route matches — fetch token and add + $stmtD = $con->prepare("SELECT token FROM driverToken WHERE captain_id = :d LIMIT 1"); + $stmtD->execute([':d' => $mid]); + $dT = $stmtD->fetchColumn(); + if ($dT) { + $matchedToFront[] = [ + 'captain_id' => $mid, + 'driver_id' => $mid, + 'token' => $dT, + 'is_destination_match' => 1 + ]; + } + } + } + + // Place all destination-matched drivers at the front + $driversData = array_values($driversData); + $driversData = array_merge($matchedToFront, $driversData); + error_log("[add_ride] " . count($matchedToFront) . " destination-matched driver(s) moved to front."); } + } catch (Exception $e) { + error_log("[add_ride] Destination priority error: " . $e->getMessage()); } } diff --git a/backend/ride/rides/retry_search_drivers.php b/backend/ride/rides/retry_search_drivers.php index 697c5db2..cf870d18 100644 --- a/backend/ride/rides/retry_search_drivers.php +++ b/backend/ride/rides/retry_search_drivers.php @@ -1,19 +1,32 @@ - (float)$price) { + $price = $newPriceFromPassenger; + // نحافظ على نسبة الكيزان (عمولة التطبيق) الأصلية — الزيادة للراكب والسائق معاً + $originalRatio = ((float)$priceForDriver > 0 && (float)$priceForDriver < (float)$price) + ? (float)$priceForDriver / (float)$price + : 0.86; // default 86% للسائق + $priceForDriver = number_format((float)$price * $originalRatio, 2, '.', ''); + error_log("[AI Negotiator Phase 2] Passenger accepted price increase: old=$price → new={$newPriceFromPassenger}"); +} + if (!$rideId) { jsonError("Missing Ride ID"); exit; } -try { - // 2. تحديث حالة الرحلة في قاعدة البيانات (Reset) - $updateStmt = $con->prepare("UPDATE ride SET status = 'waiting', driver_id = 0, updated_at = NOW() WHERE id = ?"); - $updateStmt->execute([$rideId]); +// ═══════════════════════════════════════════════════════════════════════════ +// 🤖 STEP 0: AI Negotiator — Read & Increment Rejection Counter (Redis-first) +// ═══════════════════════════════════════════════════════════════════════════ +$rejectionCount = 0; +$aiBonus = 0.0; +$aiRedisKey = "ai:ride_rejections:{$rideId}"; +$originalKazan = (float)$price - (float)$priceForDriver; - // 3. حساب العمولة (Kazan) - $kazan = (double)$price - (double)$priceForDriver; +// Bonus Tiers: after N rejections, give driver this much extra from our commission +// These values come from our share (kazan), never from the passenger's price +$bonusTiers = [ + 1 => 0.05, // 1st retry: 5% of price → added to driver share + 2 => 0.10, // 2nd retry: 10% + 3 => 0.15, // 3rd retry: 15% +]; +$maxBonusFraction = 0.20; // Cap: never give away more than 20% of price as bonus + +try { + // Read current rejection count from Redis (O(1)) + if (isset($redis)) { + $storedCount = $redis->get($aiRedisKey); + if ($storedCount !== false) { + $rejectionCount = (int)$storedCount; + } + // Increment rejection count (this retry = another rejection) + $rejectionCount++; + $redis->setex($aiRedisKey, 3600, $rejectionCount); // TTL: 1 hour + } + + // Calculate bonus based on rejection tier + $tierKey = min($rejectionCount, max(array_keys($bonusTiers))); + $bonusFraction = $bonusTiers[$tierKey] ?? $bonusTiers[max(array_keys($bonusTiers))]; + $bonusFraction = min($bonusFraction, $maxBonusFraction); + $aiBonus = round((float)$price * $bonusFraction, 2); + + // Never give more than what we actually earn as commission (kazan) + if ($aiBonus > $originalKazan) { + $aiBonus = round($originalKazan * 0.80, 2); // Keep 20% for ourselves minimum + } + + // Apply bonus: increase driver's price, reduce our kazan + if ($aiBonus > 0) { + $priceForDriver = number_format((float)$priceForDriver + $aiBonus, 2, '.', ''); + error_log("[AI Negotiator] RideID={$rideId} | Rejection #{$rejectionCount} | Bonus={$aiBonus} | New priceForDriver={$priceForDriver}"); + } + +} catch (Exception $e) { + error_log("[AI Negotiator] Redis error: " . $e->getMessage()); + // Non-fatal: continue without bonus +} + +try { + // 2. تحديث حالة الرحلة + تسجيل الـ bonus في قاعدة البيانات + $updateStmt = $con->prepare(" + UPDATE ride + SET status = 'waiting', + driver_id = 0, + updated_at = NOW(), + ai_negotiated_bonus = :bonus, + price_for_driver = :new_driver_price + WHERE id = :id + "); + $updateStmt->execute([ + ':bonus' => $aiBonus, + ':new_driver_price' => $priceForDriver, + ':id' => $rideId, + ]); + + // 3. حساب العمولة (Kazan) المُحدَّثة بعد الـ bonus + $kazan = (float)$price - (float)$priceForDriver; + if ($kazan < 0) $kazan = 0; // Floor protection $passengerFp = isset($_SERVER['HTTP_X_DEVICE_FP']) ? $_SERVER['HTTP_X_DEVICE_FP'] : ''; - // 4. بناء Payload مطابق لـ add_ride.php (0 - 33) + + // 4. بناء Payload (0 - 33) — مطابق لـ add_ride.php $payloadTemplate = []; $payloadTemplate[0] = (string)$startLat; $payloadTemplate[1] = (string)$startLng; @@ -77,30 +167,102 @@ try { $payloadTemplate[23] = (string)$step2; $payloadTemplate[24] = (string)$step3; $payloadTemplate[25] = (string)$step4; - $payloadTemplate[26] = (string)number_format((float)$priceForDriver, 2, '.', ''); + $payloadTemplate[26] = (string)number_format((float)$priceForDriver, 2, '.', ''); // ← Updated with bonus $payloadTemplate[27] = (string)$passengerWallet; $payloadTemplate[28] = (string)$passengerEmail; $payloadTemplate[29] = (string)$startName; $payloadTemplate[30] = (string)$endName; $payloadTemplate[31] = (string)$carType; - $payloadTemplate[32] = (string)number_format($kazan, 2, '.', ''); + $payloadTemplate[32] = (string)number_format($kazan, 2, '.', ''); // ← Reduced kazan $payloadTemplate[33] = (string)$passengerRating; ksort($payloadTemplate); $payloadTemplate = array_values($payloadTemplate); - // 5. البحث عن السائقين وإرسال الطلب (Using Helper Function) + // 5. البحث عن السائقين وإرسال الطلب $latVal = doubleval($startLat); $lngVal = doubleval($startLng); $driversData = findBestDrivers($con, $latVal, $lngVal, $carType, $endLat, $endLng); + // ═══════════════════════════════════════════════════════════════ + // 🆕 Destination Matching Priority — Multi-Driver (Retry Round) + // السياسة: لما ما في أحد يقبل الرحلة بعد 20 ثانية، Flutter + // يستدعي هذا الملف تلقائياً (AI Negotiator يزيد حصة السائق) + // في نفس الوقت، نضع السائقين ذوي الوجهة المطابقة في المقدمة + // بدون خصم إضافي (الخصم 14% تم مسبقاً في التسعير) + // إذا كان أكثر من سائق يملك نفس المنطقة كوجهة، نرسل للجميع + // ═══════════════════════════════════════════════════════════════ + if ($isDestinationMatchFlag === "1" && isset($redis)) { + try { + $allMatchedIds = $redis->geoRadius( + 'geo:driver:destinations', + (float)$endLng, + (float)$endLat, + 3.5, + 'km', + ['COUNT' => 10, 'ASC'] + ); + + if (!empty($allMatchedIds)) { + $alreadyInList = []; + foreach ($driversData as $d) { + $alreadyInList[$d['captain_id'] ?? $d['driver_id'] ?? ''] = true; + } + + $matchedToFront = []; + foreach ($allMatchedIds as $mid) { + $mid = (string)$mid; + // Validate still active today + $detailJson = $redis->get("driver:destination:{$mid}"); + $detail = $detailJson ? json_decode($detailJson, true) : null; + if (!$detail || empty($detail['is_active']) || ($detail['usage_date'] ?? '') !== date('Y-m-d')) { + $redis->zRem('geo:driver:destinations', $mid); + continue; + } + if (isset($alreadyInList[$mid])) { + foreach ($driversData as $k => $d) { + $did = $d['captain_id'] ?? $d['driver_id'] ?? ''; + if ((string)$did === $mid) { + $driversData[$k]['is_destination_match'] = 1; + $matchedToFront[] = $driversData[$k]; + unset($driversData[$k]); + break; + } + } + } else { + $stmtD = $con->prepare("SELECT token FROM driverToken WHERE captain_id = :d LIMIT 1"); + $stmtD->execute([':d' => $mid]); + $dT = $stmtD->fetchColumn(); + if ($dT) { + $matchedToFront[] = [ + 'captain_id' => $mid, + 'driver_id' => $mid, + 'token' => $dT, + 'is_destination_match' => 1 + ]; + } + } + } + $driversData = array_values($driversData); + $driversData = array_merge($matchedToFront, $driversData); + error_log("[retry] " . count($matchedToFront) . " destination-matched driver(s) moved to front."); + } + } catch (Exception $e) { + error_log("[retry] Destination priority error: " . $e->getMessage()); + } + } + if (!empty($driversData)) { - // استدعاء دالة الإرسال الموحدة (الموجودة في functions.php) dispatchRideToDrivers($driversData, $rideId, $payloadTemplate, $startName, $encryptionHelper); } - jsonSuccess(null, "Ride reset and resent to drivers"); + // Return with bonus info so Flutter can log it + jsonSuccess([ + 'rejection_count' => $rejectionCount, + 'ai_bonus_applied' => $aiBonus, + 'new_driver_price' => $priceForDriver, + ], "Ride reset and resent to drivers"); } catch (PDOException $e) { error_log("[retry_search_drivers] " . $e->getMessage()); diff --git a/siro_driver/lib/controller/home/captin/order_request_controller.dart b/siro_driver/lib/controller/home/captin/order_request_controller.dart index fb699446..04221a1f 100755 --- a/siro_driver/lib/controller/home/captin/order_request_controller.dart +++ b/siro_driver/lib/controller/home/captin/order_request_controller.dart @@ -169,6 +169,12 @@ class OrderRequestController extends GetxController // ب) هل هي قادمة من Socket بالمفاتيح الرقمية ("0", "1", ...)؟ else { myMapData = args; + // 🆕 Also check for Destination Match flag from Socket data + if (args['is_destination_match'] == '1' || + args['is_destination_match'] == 1 || + args['is_destination_match'] == true) { + isDestinationMatch = true; + } } } } diff --git a/siro_rider/lib/constant/box_name.dart b/siro_rider/lib/constant/box_name.dart index 7f7140e4..afee642e 100644 --- a/siro_rider/lib/constant/box_name.dart +++ b/siro_rider/lib/constant/box_name.dart @@ -110,4 +110,5 @@ class BoxName { static const String tripData = 'tripData'; static const String parentTripSelected = 'parentTripSelected'; static const String styleVersion = 'styleVersion'; + static const String isDestinationMatch = 'isDestinationMatch'; // 🆕 AI Destination Matching } diff --git a/siro_rider/lib/controller/home/map/ride_lifecycle_controller.dart b/siro_rider/lib/controller/home/map/ride_lifecycle_controller.dart index 4902c232..85094e5e 100644 --- a/siro_rider/lib/controller/home/map/ride_lifecycle_controller.dart +++ b/siro_rider/lib/controller/home/map/ride_lifecycle_controller.dart @@ -734,32 +734,107 @@ class RideLifecycleController extends GetxController { mapSocket.initConnectionWithSocket(); } - void _showIncreaseFeeDialog() { + void _showAiNegotiatorDialog() { + final double currentPrice = double.tryParse(totalPassenger) ?? 0; + final double suggestedPrice = currentPrice * 1.05; // 5% AI suggestion + final String currency = box.read(BoxName.serverChosen)?.toString().contains('Syria') == true + ? 'ل.س' : 'SAR'; + Get.dialog( - CupertinoAlertDialog( - title: Text("No drivers accepted your request yet".tr), - content: Text( - "Increasing the fare might attract more drivers. Would you like to increase the price?" - .tr), - actions: [ - CupertinoDialogAction( - child: Text("Cancel Ride".tr, - style: TextStyle(color: AppColor.redColor)), - onPressed: () { - Get.back(); - mapEngine.changeCancelRidePageShow(); - }, + Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)), + backgroundColor: const Color(0xFF1A1A2E), + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // AI Icon + Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: const Color(0xFF16213E), + borderRadius: BorderRadius.circular(50), + border: Border.all(color: const Color(0xFF0F3460), width: 2), + ), + child: const Icon(Icons.psychology_rounded, color: Color(0xFF4FC3F7), size: 36), + ), + const SizedBox(height: 16), + Text( + '🤖 مقترح الذكاء الاصطناعي'.tr, + style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold), + textAlign: TextAlign.center, + ), + const SizedBox(height: 10), + Text( + 'لم يقبل أي سائق طلبك بالسعر الحالي. يقترح AI رفع السعر قليلاً لتسريع القبول.'.tr, + style: const TextStyle(color: Color(0xFFB0BEC5), fontSize: 13), + textAlign: TextAlign.center, + ), + const SizedBox(height: 20), + // Price comparison + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + decoration: BoxDecoration( + color: const Color(0xFF0F3460), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + children: [ + Text('السعر الحالي'.tr, style: const TextStyle(color: Color(0xFF90A4AE), fontSize: 11)), + Text('${currentPrice.toStringAsFixed(0)} $currency', + style: const TextStyle(color: Colors.white70, fontSize: 15, decoration: TextDecoration.lineThrough)), + ], + ), + const Icon(Icons.arrow_forward_rounded, color: Color(0xFF4FC3F7)), + Column( + children: [ + Text('السعر المقترح'.tr, style: const TextStyle(color: Color(0xFF4FC3F7), fontSize: 11)), + Text('${suggestedPrice.toStringAsFixed(0)} $currency', + style: const TextStyle(color: Color(0xFF4FC3F7), fontSize: 17, fontWeight: FontWeight.bold)), + ], + ), + ], + ), + ), + const SizedBox(height: 20), + Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () { + Get.back(); + mapEngine.changeCancelRidePageShow(); + }, + style: OutlinedButton.styleFrom( + side: const BorderSide(color: Colors.red), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + ), + child: Text('إلغاء'.tr, style: const TextStyle(color: Colors.red)), + ), + ), + const SizedBox(width: 10), + Expanded( + child: ElevatedButton( + onPressed: () { + Get.back(); + increasePriceAndRestartSearch(suggestedPrice); + }, + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF4FC3F7), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)), + ), + child: Text('قبول +5%'.tr, style: const TextStyle(color: Colors.black, fontWeight: FontWeight.bold)), + ), + ), + ], + ), + ], ), - CupertinoDialogAction( - child: Text("Increase Fare".tr, - style: TextStyle(color: AppColor.greenColor)), - onPressed: () { - Get.back(); - double newPrice = double.parse(totalPassenger) * 1.10; - increasePriceAndRestartSearch(newPrice); - }, - ), - ], + ), ), barrierDismissible: false, ); @@ -767,23 +842,20 @@ class RideLifecycleController extends GetxController { Future increasePriceAndRestartSearch(double newPrice) async { totalPassenger = newPrice.toStringAsFixed(2); + costForDriver = newPrice * 0.86; // حفاظ على نفس نسبة السائق update(); - await CRUD() - .post(link: "${AppLink.server}/ride/rides/update.php", payload: { + // تحديث قاعدة البيانات بالسعر الجديد + await CRUD().post(link: "${AppLink.server}/ride/rides/update.php", payload: { "id": rideId, "price": newPrice.toStringAsFixed(2), }); - Log.print( - '[increasePrice] Price changed. Clearing notified list to resend.'); + Log.print('[AI Negotiator Phase 2] Price accepted by passenger: $newPrice — restarting search...'); notifiedDrivers.clear(); - _searchStartTime = DateTime.now(); - _currentSearchPhase = 0; - isSearchingWindow = true; - update(); - startMasterTimer(); + // إعادة البحث مع السعر الجديد (Phase 2 retry) + retrySearchForDrivers(newPrice: newPrice); } void _stopWaitPassengerTimer({bool resetUI = false}) { @@ -2018,6 +2090,12 @@ class RideLifecycleController extends GetxController { // Save price_token from server response priceToken = res['price_token']?.toString() ?? ''; + // 🆕 Save is_destination_match so retry_search_drivers can pass it forward + final int isMatchFlag = (res['is_destination_match'] ?? 0) is int + ? res['is_destination_match'] ?? 0 + : int.tryParse(res['is_destination_match']?.toString() ?? '0') ?? 0; + box.write(BoxName.isDestinationMatch, isMatchFlag.toString()); + totalPassenger = totalPassengerSpeed; totalCostPassenger = totalPassenger; } @@ -3979,7 +4057,7 @@ class RideLifecycleController extends GetxController { dataCarsLocationByPassenger['message'] != null; } - void retrySearchForDrivers() async { + void retrySearchForDrivers({double? newPrice}) async { _isCancelProcessed = false; isSearchingWindow = true; currentRideState.value = RideState.searching; @@ -3987,7 +4065,8 @@ class RideLifecycleController extends GetxController { update(); try { - Log.print("🔄 Retrying search for ride ID: $rideId"); + final bool isPhase2 = newPrice != null && newPrice > 0; + Log.print("🔄 ${isPhase2 ? '[Phase 2]' : '[Phase 1]'} Retrying search for ride ID: $rideId"); var payload = { "ride_id": rideId.toString(), @@ -4011,9 +4090,12 @@ class RideLifecycleController extends GetxController { "price_for_driver": costForDriver.toString(), "car_type": box.read(BoxName.carType).toString(), "is_wallet": Get.find().isWalletChecked.toString(), + "is_destination_match": box.read(BoxName.isDestinationMatch)?.toString() ?? "0", "has_steps": Get.find().wayPoints.length > 1 ? "true" : "false", + // 🤖 Phase 2: سعر جديد وافق عليه الراكب + if (isPhase2) "new_price": newPrice!.toStringAsFixed(2), }; var response = await CRUD().post( @@ -4023,7 +4105,7 @@ class RideLifecycleController extends GetxController { if (response['status'] == 'success') { Log.print("✅ Search reset successfully."); - startSearchingTimer(); + if (!isPhase2) startSearchingTimer(); // Phase 1 يعيد العداد، Phase 2 يُدار من increasePriceAndRestartSearch } else { Log.print("❌ Failed to reset search: $response"); handleNoDriverFound(); @@ -4037,8 +4119,9 @@ class RideLifecycleController extends GetxController { Future startSearchingTimer() async { _searchTimer?.cancel(); int seconds = 0; + bool _aiSilentRetryFired = false; - Log.print("⏳ Search Timer Started (90s)..."); + Log.print("⏳ Search Timer Started..."); await RideLiveNotification.showSearching(driversStatusForSearchWindow); _searchTimer = Timer.periodic(const Duration(seconds: 1), (timer) { @@ -4049,9 +4132,27 @@ class RideLifecycleController extends GetxController { return; } - if (seconds >= 90) { + // ───────────────────────────────────────────────────────────── + // 🤖 AI Negotiator Phase 1 — T=20s: Silent retry + // النظام يزيد حصة السائق من عمولة التطبيق كجسر مؤقت + // الراكب لا يرى شيئاً — فقط السائق التالي يرى سعراً أفضل + // ───────────────────────────────────────────────────────────── + if (seconds == 20 && !_aiSilentRetryFired) { + _aiSilentRetryFired = true; + Log.print("🤖 [AI Negotiator Phase 1] 20s — silent retry with driver bonus..."); + retrySearchForDrivers(); + return; + } + + // ───────────────────────────────────────────────────────────── + // 🤖 AI Negotiator Phase 2 — T=45s: Suggest passenger price bump + // يقترح الذكاء الاصطناعي على الراكب زيادة 5% لتسريع القبول + // الراكب يختار: يقبل الزيادة أو يلغي الرحلة + // ───────────────────────────────────────────────────────────── + if (seconds >= 45) { timer.cancel(); - handleNoDriverFound(); + Log.print("🤖 [AI Negotiator Phase 2] 45s — showing AI price suggestion dialog..."); + _showAiNegotiatorDialog(); } }); }