Files
Siro/backend/ride/rides/retry_search_drivers.php
T

287 lines
14 KiB
PHP

<?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';
// 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;
// 👑 Check Prime Status from Redis
$isPrimeFlag = "0";
if (isset($redis)) {
try {
$cachedPrime = $redis->get("prime:passenger:{$passengerId}");
if ($cachedPrime) {
$primeData = json_decode($cachedPrime, true);
if (isset($primeData['is_prime']) && $primeData['is_prime'] == 1) {
$isPrimeFlag = "1";
}
}
} catch (Exception $e) {}
}
$payloadTemplate[32] = (string)number_format($kazan, 2, '.', ''); // ← Reduced kazan
$payloadTemplate[33] = (string)$passengerRating;
$payloadTemplate[34] = $isPrimeFlag; // 👑 Index 34: Prime Status
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");
}
?>