// ═══════════════════════════════════════════════════════════════════════════ // 🤖 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'; // 1. استقبال البيانات القادمة من الفلتر $rideId = filterRequest("ride_id"); $passengerId = filterRequest("passenger_id"); $passengerName = filterRequest("passenger_name"); $passengerPhone = filterRequest("passenger_phone"); $passengerEmail = filterRequest("passenger_email"); $passengerToken = filterRequest("passenger_token"); $passengerWallet = filterRequest("passenger_wallet"); $isWallet = filterRequest("is_wallet"); $passengerRating = filterRequest("passenger_rating"); $startLat = filterRequest("start_lat"); $startLng = filterRequest("start_lng"); $endLat = filterRequest("end_lat"); $endLng = filterRequest("end_lng"); $startName = filterRequest("start_name"); $endName = filterRequest("end_name"); $distance = filterRequest("distance"); $distanceText = filterRequest("distance_text"); $durationText = filterRequest("duration_text"); $price = filterRequest("price"); $priceForDriver = filterRequest("price_for_driver"); $carType = filterRequest("car_type"); $isDestinationMatchFlag = filterRequest("is_destination_match") ?: "0"; $hasSteps = filterRequest("has_steps"); $step0 = filterRequest("step0"); $step1 = filterRequest("step1"); $step2 = filterRequest("step2"); $step3 = filterRequest("step3"); $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) { jsonError("Missing Ride ID"); exit; } // ═══════════════════════════════════════════════════════════════════════════ // 🤖 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; // 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 (0 - 33) — مطابق لـ add_ride.php $payloadTemplate = []; $payloadTemplate[0] = (string)$startLat; $payloadTemplate[1] = (string)$startLng; $payloadTemplate[2] = (string)number_format((float)$price, 2, '.', ''); $payloadTemplate[3] = (string)$endLat; $payloadTemplate[4] = (string)$endLng; $payloadTemplate[5] = (string)$distanceText; $payloadTemplate[6] = (string)$passengerFp; $payloadTemplate[7] = (string)$passengerId; $payloadTemplate[8] = (string)$passengerName; $payloadTemplate[9] = (string)$passengerToken; $payloadTemplate[10] = (string)$passengerPhone; $payloadTemplate[11] = (string)$distance; $payloadTemplate[12] = "1"; $payloadTemplate[13] = (string)$isWallet; $payloadTemplate[14] = (string)$distance; $payloadTemplate[15] = (string)$durationText; $payloadTemplate[16] = (string)$rideId; $payloadTemplate[17] = ""; $payloadTemplate[18] = ""; // Driver ID placeholder $payloadTemplate[19] = (string)$durationText; $payloadTemplate[20] = (string)$hasSteps; $payloadTemplate[21] = (string)$step0; $payloadTemplate[22] = (string)$step1; $payloadTemplate[23] = (string)$step2; $payloadTemplate[24] = (string)$step3; $payloadTemplate[25] = (string)$step4; $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, '.', ''); // ← Reduced kazan $payloadTemplate[33] = (string)$passengerRating; ksort($payloadTemplate); $payloadTemplate = array_values($payloadTemplate); // 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)) { dispatchRideToDrivers($driversData, $rideId, $payloadTemplate, $startName, $encryptionHelper); } // 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()); jsonError("DB Error"); } ?>