Update: 2026-07-02 17:44:58

This commit is contained in:
Hamza-Ayed
2026-07-02 17:44:59 +03:00
parent 5d14493ff4
commit 6876d9bc54
6 changed files with 405 additions and 90 deletions
+6 -5
View File
@@ -586,10 +586,11 @@ if (isset($encryptionHelper)) {
} }
echo json_encode([ echo json_encode([
'status' => 'success', 'status' => 'success',
'data' => $prices, 'data' => $prices,
'price_token' => $priceToken, 'price_token' => $priceToken,
'applied_discount' => $discount, 'applied_discount' => $discount,
'added_negative_balance' => $negativeBalance 'added_negative_balance' => $negativeBalance,
'is_destination_match' => $isDestinationMatch ? 1 : 0,
]); ]);
?> ?>
+69 -25
View File
@@ -291,32 +291,76 @@ try {
// Direct dispatch للسائقين القريبين // Direct dispatch للسائقين القريبين
$driversData = findBestDrivers($con, $startLat, $startLng, $carType, $endLat, $endLng); $driversData = findBestDrivers($con, $startLat, $startLng, $carType, $endLat, $endLng);
// 🆕 Destination Matching Priority // ═══════════════════════════════════════════════════════════════
if ($is_destination_match && $matched_driver_id) { // 🆕 Destination Matching Priority — Multi-Driver Support
$found = false; // نبحث عن كل السائقين الذين وجهتهم تطابق منطقة الإنزال
foreach ($driversData as $k => $d) { // (ليس فقط الأول، بل جميعهم) ونضعهم في مقدمة القائمة
if ($d['captain_id'] == $matched_driver_id) { // الخصم 14% تم تطبيقه مسبقاً في التسعير — لا خصم إضافي هنا
// Remove from current position and move to the very front // ═══════════════════════════════════════════════════════════════
unset($driversData[$k]); if ($is_destination_match && isset($redis)) {
$d['is_destination_match'] = 1; try {
array_unshift($driversData, $d); // Query ALL drivers whose destination is near the drop-off (up to 10)
$found = true; $allMatchedIds = $redis->geoRadius(
break; 'geo:driver:destinations',
} (float)$destLng,
} (float)$destLat,
3.5,
// If driver wasn't nearby but we still want to give it to them because it's their route 'km',
if (!$found) { ['COUNT' => 10, 'ASC']
$stmtD = $con->prepare("SELECT token FROM driverToken WHERE captain_id = :d"); );
$stmtD->execute([':d' => $matched_driver_id]);
$dT = $stmtD->fetchColumn(); if (!empty($allMatchedIds)) {
if ($dT) { // Build a lookup set for fast de-duplication
array_unshift($driversData, [ $alreadyInList = [];
'captain_id' => $matched_driver_id, foreach ($driversData as $d) {
'token' => $dT, $alreadyInList[$d['captain_id'] ?? $d['driver_id'] ?? ''] = true;
'is_destination_match' => 1 }
]);
$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());
} }
} }
+183 -21
View File
@@ -1,19 +1,32 @@
<?php // ═══════════════════════════════════════════════════════════════════════════
// retry_search_drivers.php // 🤖 AI Negotiator — نظام مفاوضة الذكاء الاصطناعي
//
// سياسة التشغيل:
// Phase 1 — T=20s (تلقائي، بصمت):
// Flutter يستدعي هذا الملف بعد 20 ثانية بدون قبول.
// النظام يزيد حصة السائق من عمولة التطبيق كجسر مؤقت.
// الراكب لا يرى شيئاً.
//
// Phase 2 — T=45s (بموافقة الراكب):
// Flutter يعرض Dialog يقترح زيادة 5% من الراكب.
// إذا قبل: يُرسل new_price جديد لهذا الملف.
// السعر الجديد يُطبَّق على الراكب والسائق معاً.
//
// الإرسال: dispatchRideToDrivers() يرسل عبر WebSocket أولاً ثم FCM
// (راجع functions.php — sendSocketNotification + sendFcmNotification)
// ═══════════════════════════════════════════════════════════════════════════
require_once __DIR__ . '/../../connect.php'; require_once __DIR__ . '/../../connect.php';
// 1. استقبال البيانات القادمة من الفلتر (لتوفير الاستعلامات) // 1. استقبال البيانات القادمة من الفلتر
$rideId = filterRequest("ride_id"); $rideId = filterRequest("ride_id");
$passengerId = filterRequest("passenger_id"); $passengerId = filterRequest("passenger_id");
$passengerName = filterRequest("passenger_name"); $passengerName = filterRequest("passenger_name");
$passengerPhone = filterRequest("passenger_phone"); $passengerPhone = filterRequest("passenger_phone");
$passengerEmail = filterRequest("passenger_email"); $passengerEmail = filterRequest("passenger_email");
$passengerToken = filterRequest("passenger_token"); $passengerToken = filterRequest("passenger_token");
$passengerWallet = filterRequest("passenger_wallet"); // الرصيد $passengerWallet = filterRequest("passenger_wallet");
$isWallet = filterRequest("is_wallet"); // هل الدفع بالمحفظة؟ (true/false) $isWallet = filterRequest("is_wallet");
$passengerRating = filterRequest("passenger_rating"); $passengerRating = filterRequest("passenger_rating");
// بيانات الموقع والرحلة (يفضل إرسالها أيضاً لضمان الدقة)
$startLat = filterRequest("start_lat"); $startLat = filterRequest("start_lat");
$startLng = filterRequest("start_lng"); $startLng = filterRequest("start_lng");
$endLat = filterRequest("end_lat"); $endLat = filterRequest("end_lat");
@@ -26,8 +39,7 @@ $durationText = filterRequest("duration_text");
$price = filterRequest("price"); $price = filterRequest("price");
$priceForDriver = filterRequest("price_for_driver"); $priceForDriver = filterRequest("price_for_driver");
$carType = filterRequest("car_type"); $carType = filterRequest("car_type");
$isDestinationMatchFlag = filterRequest("is_destination_match") ?: "0";
// بيانات الخطوات (إن وجدت)
$hasSteps = filterRequest("has_steps"); $hasSteps = filterRequest("has_steps");
$step0 = filterRequest("step0"); $step0 = filterRequest("step0");
$step1 = filterRequest("step1"); $step1 = filterRequest("step1");
@@ -35,21 +47,99 @@ $step2 = filterRequest("step2");
$step3 = filterRequest("step3"); $step3 = filterRequest("step3");
$step4 = filterRequest("step4"); $step4 = filterRequest("step4");
// Phase 2: إذا أرسل الراكب new_price (وافق على الزيادة المقترحة من الـ AI)
// هذا يتجاوز Phase 1 ويطبق السعر الجديد مباشرةً على الراكب والسائق
$newPriceFromPassenger = filterRequest("new_price");
if ($newPriceFromPassenger && (float)$newPriceFromPassenger > (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) { if (!$rideId) {
jsonError("Missing Ride ID"); jsonError("Missing Ride ID");
exit; exit;
} }
try { // ═══════════════════════════════════════════════════════════════════════════
// 2. تحديث حالة الرحلة في قاعدة البيانات (Reset) // 🤖 STEP 0: AI Negotiator — Read & Increment Rejection Counter (Redis-first)
$updateStmt = $con->prepare("UPDATE ride SET status = 'waiting', driver_id = 0, updated_at = NOW() WHERE id = ?"); // ═══════════════════════════════════════════════════════════════════════════
$updateStmt->execute([$rideId]); $rejectionCount = 0;
$aiBonus = 0.0;
$aiRedisKey = "ai:ride_rejections:{$rideId}";
$originalKazan = (float)$price - (float)$priceForDriver;
// 3. حساب العمولة (Kazan) // Bonus Tiers: after N rejections, give driver this much extra from our commission
$kazan = (double)$price - (double)$priceForDriver; // 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'] : ''; $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 = [];
$payloadTemplate[0] = (string)$startLat; $payloadTemplate[0] = (string)$startLat;
$payloadTemplate[1] = (string)$startLng; $payloadTemplate[1] = (string)$startLng;
@@ -77,30 +167,102 @@ try {
$payloadTemplate[23] = (string)$step2; $payloadTemplate[23] = (string)$step2;
$payloadTemplate[24] = (string)$step3; $payloadTemplate[24] = (string)$step3;
$payloadTemplate[25] = (string)$step4; $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[27] = (string)$passengerWallet;
$payloadTemplate[28] = (string)$passengerEmail; $payloadTemplate[28] = (string)$passengerEmail;
$payloadTemplate[29] = (string)$startName; $payloadTemplate[29] = (string)$startName;
$payloadTemplate[30] = (string)$endName; $payloadTemplate[30] = (string)$endName;
$payloadTemplate[31] = (string)$carType; $payloadTemplate[31] = (string)$carType;
$payloadTemplate[32] = (string)number_format($kazan, 2, '.', ''); $payloadTemplate[32] = (string)number_format($kazan, 2, '.', ''); // ← Reduced kazan
$payloadTemplate[33] = (string)$passengerRating; $payloadTemplate[33] = (string)$passengerRating;
ksort($payloadTemplate); ksort($payloadTemplate);
$payloadTemplate = array_values($payloadTemplate); $payloadTemplate = array_values($payloadTemplate);
// 5. البحث عن السائقين وإرسال الطلب (Using Helper Function) // 5. البحث عن السائقين وإرسال الطلب
$latVal = doubleval($startLat); $latVal = doubleval($startLat);
$lngVal = doubleval($startLng); $lngVal = doubleval($startLng);
$driversData = findBestDrivers($con, $latVal, $lngVal, $carType, $endLat, $endLng); $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)) { if (!empty($driversData)) {
// استدعاء دالة الإرسال الموحدة (الموجودة في functions.php)
dispatchRideToDrivers($driversData, $rideId, $payloadTemplate, $startName, $encryptionHelper); 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) { } catch (PDOException $e) {
error_log("[retry_search_drivers] " . $e->getMessage()); error_log("[retry_search_drivers] " . $e->getMessage());
@@ -169,6 +169,12 @@ class OrderRequestController extends GetxController
// ب) هل هي قادمة من Socket بالمفاتيح الرقمية ("0", "1", ...)؟ // ب) هل هي قادمة من Socket بالمفاتيح الرقمية ("0", "1", ...)؟
else { else {
myMapData = args; 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;
}
} }
} }
} }
+1
View File
@@ -110,4 +110,5 @@ class BoxName {
static const String tripData = 'tripData'; static const String tripData = 'tripData';
static const String parentTripSelected = 'parentTripSelected'; static const String parentTripSelected = 'parentTripSelected';
static const String styleVersion = 'styleVersion'; static const String styleVersion = 'styleVersion';
static const String isDestinationMatch = 'isDestinationMatch'; // 🆕 AI Destination Matching
} }
@@ -734,32 +734,107 @@ class RideLifecycleController extends GetxController {
mapSocket.initConnectionWithSocket(); 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( Get.dialog(
CupertinoAlertDialog( Dialog(
title: Text("No drivers accepted your request yet".tr), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
content: Text( backgroundColor: const Color(0xFF1A1A2E),
"Increasing the fare might attract more drivers. Would you like to increase the price?" child: Padding(
.tr), padding: const EdgeInsets.all(24),
actions: [ child: Column(
CupertinoDialogAction( mainAxisSize: MainAxisSize.min,
child: Text("Cancel Ride".tr, children: [
style: TextStyle(color: AppColor.redColor)), // AI Icon
onPressed: () { Container(
Get.back(); padding: const EdgeInsets.all(14),
mapEngine.changeCancelRidePageShow(); 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, barrierDismissible: false,
); );
@@ -767,23 +842,20 @@ class RideLifecycleController extends GetxController {
Future<void> increasePriceAndRestartSearch(double newPrice) async { Future<void> increasePriceAndRestartSearch(double newPrice) async {
totalPassenger = newPrice.toStringAsFixed(2); totalPassenger = newPrice.toStringAsFixed(2);
costForDriver = newPrice * 0.86; // حفاظ على نفس نسبة السائق
update(); 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, "id": rideId,
"price": newPrice.toStringAsFixed(2), "price": newPrice.toStringAsFixed(2),
}); });
Log.print( Log.print('[AI Negotiator Phase 2] Price accepted by passenger: $newPrice — restarting search...');
'[increasePrice] Price changed. Clearing notified list to resend.');
notifiedDrivers.clear(); notifiedDrivers.clear();
_searchStartTime = DateTime.now(); // إعادة البحث مع السعر الجديد (Phase 2 retry)
_currentSearchPhase = 0; retrySearchForDrivers(newPrice: newPrice);
isSearchingWindow = true;
update();
startMasterTimer();
} }
void _stopWaitPassengerTimer({bool resetUI = false}) { void _stopWaitPassengerTimer({bool resetUI = false}) {
@@ -2018,6 +2090,12 @@ class RideLifecycleController extends GetxController {
// Save price_token from server response // Save price_token from server response
priceToken = res['price_token']?.toString() ?? ''; 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; totalPassenger = totalPassengerSpeed;
totalCostPassenger = totalPassenger; totalCostPassenger = totalPassenger;
} }
@@ -3979,7 +4057,7 @@ class RideLifecycleController extends GetxController {
dataCarsLocationByPassenger['message'] != null; dataCarsLocationByPassenger['message'] != null;
} }
void retrySearchForDrivers() async { void retrySearchForDrivers({double? newPrice}) async {
_isCancelProcessed = false; _isCancelProcessed = false;
isSearchingWindow = true; isSearchingWindow = true;
currentRideState.value = RideState.searching; currentRideState.value = RideState.searching;
@@ -3987,7 +4065,8 @@ class RideLifecycleController extends GetxController {
update(); update();
try { 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 = { var payload = {
"ride_id": rideId.toString(), "ride_id": rideId.toString(),
@@ -4011,9 +4090,12 @@ class RideLifecycleController extends GetxController {
"price_for_driver": costForDriver.toString(), "price_for_driver": costForDriver.toString(),
"car_type": box.read(BoxName.carType).toString(), "car_type": box.read(BoxName.carType).toString(),
"is_wallet": Get.find<PaymentController>().isWalletChecked.toString(), "is_wallet": Get.find<PaymentController>().isWalletChecked.toString(),
"is_destination_match": box.read(BoxName.isDestinationMatch)?.toString() ?? "0",
"has_steps": Get.find<WayPointController>().wayPoints.length > 1 "has_steps": Get.find<WayPointController>().wayPoints.length > 1
? "true" ? "true"
: "false", : "false",
// 🤖 Phase 2: سعر جديد وافق عليه الراكب
if (isPhase2) "new_price": newPrice!.toStringAsFixed(2),
}; };
var response = await CRUD().post( var response = await CRUD().post(
@@ -4023,7 +4105,7 @@ class RideLifecycleController extends GetxController {
if (response['status'] == 'success') { if (response['status'] == 'success') {
Log.print("✅ Search reset successfully."); Log.print("✅ Search reset successfully.");
startSearchingTimer(); if (!isPhase2) startSearchingTimer(); // Phase 1 يعيد العداد، Phase 2 يُدار من increasePriceAndRestartSearch
} else { } else {
Log.print("❌ Failed to reset search: $response"); Log.print("❌ Failed to reset search: $response");
handleNoDriverFound(); handleNoDriverFound();
@@ -4037,8 +4119,9 @@ class RideLifecycleController extends GetxController {
Future<void> startSearchingTimer() async { Future<void> startSearchingTimer() async {
_searchTimer?.cancel(); _searchTimer?.cancel();
int seconds = 0; int seconds = 0;
bool _aiSilentRetryFired = false;
Log.print("⏳ Search Timer Started (90s)..."); Log.print("⏳ Search Timer Started...");
await RideLiveNotification.showSearching(driversStatusForSearchWindow); await RideLiveNotification.showSearching(driversStatusForSearchWindow);
_searchTimer = Timer.periodic(const Duration(seconds: 1), (timer) { _searchTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
@@ -4049,9 +4132,27 @@ class RideLifecycleController extends GetxController {
return; 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(); timer.cancel();
handleNoDriverFound(); Log.print("🤖 [AI Negotiator Phase 2] 45s — showing AI price suggestion dialog...");
_showAiNegotiatorDialog();
} }
}); });
} }