Update: 2026-07-02 17:44:58
This commit is contained in:
@@ -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,
|
||||
]);
|
||||
?>
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
// 1. استقبال البيانات القادمة من الفلتر (لتوفير الاستعلامات)
|
||||
// 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"); // هل الدفع بالمحفظة؟ (true/false)
|
||||
$passengerWallet = filterRequest("passenger_wallet");
|
||||
$isWallet = filterRequest("is_wallet");
|
||||
$passengerRating = filterRequest("passenger_rating");
|
||||
|
||||
// بيانات الموقع والرحلة (يفضل إرسالها أيضاً لضمان الدقة)
|
||||
$startLat = filterRequest("start_lat");
|
||||
$startLng = filterRequest("start_lng");
|
||||
$endLat = filterRequest("end_lat");
|
||||
@@ -26,8 +39,7 @@ $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");
|
||||
@@ -35,21 +47,99 @@ $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;
|
||||
}
|
||||
|
||||
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());
|
||||
|
||||
Reference in New Issue
Block a user