From d452f9baa98c2436566127df7d26379d3cd54ce2 Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Wed, 8 Jul 2026 23:23:44 +0300 Subject: [PATCH] feat: integrate seasonal surge multipliers and grid-specific commission discounts into pricing logic --- backend/bot/cron_ai_engine.php | 14 +++++---- backend/bot/cron_surge_opportunity.php | 25 ++++++++++++++++ backend/ride/pricing/get.php | 40 ++++++++++++++++++++++---- 3 files changed, 67 insertions(+), 12 deletions(-) diff --git a/backend/bot/cron_ai_engine.php b/backend/bot/cron_ai_engine.php index 9b0fe7b6..87b6fd60 100644 --- a/backend/bot/cron_ai_engine.php +++ b/backend/bot/cron_ai_engine.php @@ -51,7 +51,7 @@ try { // 1. Economy tier (الأكثر تنافسية) // 2. Standard tier (إذا ما في Economy) // 3. أي tier ثاني بأعلى R² - $tierPriority = ['standard', 'economy', 'premium']; + $tierPriority = ['economy', 'standard', 'premium']; $bestFormula = null; $bestTierIdx = 999; @@ -192,17 +192,19 @@ try { $avgMult = round($data['avg_multiplier'] / $data['count'], 3); $peakHoursStr = implode(', ', array_unique($data['peak_hours'])); - // تخزين معلومات الـ Surge في Redis — مفتاحين: - // 1. surge:opportunities (متوافق مع get.php الحالي) - // 2. surge:opportunities:{country} (متوافق مع cron_kazan_adjuster.php) + // تخزين ملخص إحصائي (object) على مفتاح خاص ومستقل — لا نكتب على + // surge:opportunities / surge:opportunities:{country} لأنها خرائط + // grid_id → multiplier يملأها ويقرأها نظام آخر بالكامل + // (cron_surge_opportunity.php / get.php / get_surge_heatmap.php / + // winback_hotspot_targets.php / cron_kazan_adjuster.php). كتابة + // object مسطّح على نفس المفتاح كانت تبهدل تلك الخريطة كل 3 ساعات. $surgeData = [ 'avg_competitor_surge' => $avgMult, 'suggested_multiplier' => round(1.0 + ($avgMult - 1.0) * 0.6, 3), 'peak_hours' => $data['peak_hours'], 'updated_at' => date('Y-m-d H:i:s') ]; - $redis->setex("surge:opportunities", 7200, json_encode($surgeData)); - $redis->setex("surge:opportunities:{$country}", 7200, json_encode($surgeData)); + $redis->setex("pricing_engine:competitor_surge_summary:{$country}", 7200, json_encode($surgeData)); echo " ⚡ $country: Avg competitor surge = {$avgMult}x, peak hours: {$peakHoursStr}\n"; } diff --git a/backend/bot/cron_surge_opportunity.php b/backend/bot/cron_surge_opportunity.php index 7be7f1ef..55718bb0 100644 --- a/backend/bot/cron_surge_opportunity.php +++ b/backend/bot/cron_surge_opportunity.php @@ -89,6 +89,7 @@ try { // 3. تحديد فرص الذروة $opportunities = []; $gridSurgeZones = []; + $gridSurgeZonesByCountry = []; foreach ($zones as $key => &$zone) { $zone['opportunity'] = ( @@ -118,6 +119,15 @@ try { // حفظ المنطقة مع المضاعف المقترح في Redis (للقراءة من get.php بعدين) $gridSurgeZones[$key] = $suggestedMultiplier; + + // نفس البيانات مجمّعة حسب الدولة — يقرأها get_surge_heatmap.php، + // winback_hotspot_targets.php، و cron_kazan_adjuster.php كخريطة + // grid_id → multiplier لكل دولة على حدة + $countryCode = $zone['country_code']; + if (!isset($gridSurgeZonesByCountry[$countryCode])) { + $gridSurgeZonesByCountry[$countryCode] = []; + } + $gridSurgeZonesByCountry[$countryCode][$key] = $suggestedMultiplier; } } unset($zone); @@ -137,6 +147,21 @@ try { } } + // 5. تخزين/مسح النسخة المقسّمة حسب الدولة — نفس مفتاح surge:opportunities:{CC} + // اللي يقرأه get_surge_heatmap.php و winback_hotspot_targets.php و cron_kazan_adjuster.php + if (isset($redis) && $redis !== null) { + $knownCountries = ['SY', 'JO', 'EG', 'IQ']; + foreach ($knownCountries as $cc) { + $countryKey = "surge:opportunities:{$cc}"; + if (!empty($gridSurgeZonesByCountry[$cc])) { + $redis->setex($countryKey, 600, json_encode($gridSurgeZonesByCountry[$cc])); + echo "[".date('Y-m-d H:i:s')."] [$cc] Stored ".count($gridSurgeZonesByCountry[$cc])." surge zones.\n"; + } else { + $redis->del($countryKey); + } + } + } + echo "[".date('Y-m-d H:i:s')."] cron_surge_opportunity completed successfully.\n"; } catch (Exception $e) { diff --git a/backend/ride/pricing/get.php b/backend/ride/pricing/get.php index 8dfef256..a65c9514 100644 --- a/backend/ride/pricing/get.php +++ b/backend/ride/pricing/get.php @@ -118,6 +118,12 @@ function calculateDynamicPrice($country, $minFare, $distance, $duration, $kazanR global $redis, $redisLocation, $con; $surgeMultiplier = 1.0; + $kazanDiscountFactor = 1.0; + + // كود الدولة المستخدم بمفاتيح Redis (surge:seasonal:{CC}, surge:kazan_discounts:{CC}) + $countryCodeMap = ['Jordan' => 'JO', 'Syria' => 'SY', 'Egypt' => 'EG', 'Iraq' => 'IQ']; + $countryCode = $countryCodeMap[$kazanRow['country'] ?? ''] ?? strtoupper(substr($kazanRow['country'] ?? '', 0, 2)); + if (isset($redis) && $redis !== null) { try { $refLat = 33.5; @@ -125,11 +131,11 @@ function calculateDynamicPrice($country, $minFare, $distance, $duration, $kazanR $grid_lat = round((float)$passengerLat / $grid_size) * $grid_size; $grid_lng = round((float)$passengerLng / $grid_size) * $grid_size; $grid_id = $grid_lat . "_" . $grid_lng; - + // Demand is handled by Main Redis (prefix automatically applied) $demandCount = (int)$redis->get("demand:grid:" . $grid_id); $availableDrivers = 0; - + // Check competitor surge opportunities $competitorSurgeMultiplier = 1.0; $surgeOpsJson = $redis->get("surge:opportunities"); @@ -139,7 +145,7 @@ function calculateDynamicPrice($country, $minFare, $distance, $duration, $kazanR $competitorSurgeMultiplier = (float)$surgeOps[$grid_id]; } } - + // Driver locations are handled by Location Redis (no prefix) try { if (isset($redisLocation) && $redisLocation !== null) { @@ -147,7 +153,7 @@ function calculateDynamicPrice($country, $minFare, $distance, $duration, $kazanR $availableDrivers = count($drivers); } } catch (Exception $e) {} - + if ($demandCount > 0) { $surgeRatio = ($availableDrivers > 0) ? ($demandCount / $availableDrivers) : $demandCount; if ($surgeRatio > 1.2) { @@ -155,18 +161,40 @@ function calculateDynamicPrice($country, $minFare, $distance, $duration, $kazanR $surgeMultiplier = min(3.0, $surgeMultiplier); // Cap at 3.0 } } - + // Auto-Adaptive Pricing: Apply competitor surge if it's higher if ($competitorSurgeMultiplier > $surgeMultiplier) { $surgeMultiplier = $competitorSurgeMultiplier; } + + // التسعير الموسمي (رمضان/عيد/طقس سيء) — cron_seasonal_pricing.php + // نطبّق الأعلى بين مضاعف الذروة الحالي والمضاعف الموسمي، بنفس منطق + // "خذ الأعلى" المستخدم أصلاً بين الطلب الداخلي وذروة المنافسين + $seasonalJson = $redis->get("surge:seasonal:{$countryCode}"); + if ($seasonalJson) { + $seasonalData = json_decode($seasonalJson, true); + $seasonalMultiplier = (float)($seasonalData['multiplier'] ?? 1.0); + if ($seasonalMultiplier > $surgeMultiplier) { + $surgeMultiplier = $seasonalMultiplier; + } + } + + // خصم العمولة بمناطق ذروة المنافسين — cron_kazan_adjuster.php + // خريطة grid_id → معامل خصم (0.70 أو 0.85) لنفس الـ grid_id المحسوب فوق + $kazanDiscountsJson = $redis->get("surge:kazan_discounts:{$countryCode}"); + if ($kazanDiscountsJson) { + $kazanDiscounts = json_decode($kazanDiscountsJson, true); + if (is_array($kazanDiscounts) && isset($kazanDiscounts[$grid_id])) { + $kazanDiscountFactor = (float)$kazanDiscounts[$grid_id]; + } + } } catch (Exception $e) {} } $naturePrice = (float) ($kazanRow['normalMinPrice'] ?? 0); $heavyPrice = (float) ($kazanRow['peakMinPrice'] ?? 0); $latePrice = (float) ($kazanRow['lateMinPrice'] ?? 0); - $kazanPercent = (float) ($kazanRow['kazanPercent'] ?? 10); + $kazanPercent = (float) ($kazanRow['kazanPercent'] ?? 10) * $kazanDiscountFactor; $startPrice = (float) ($kazanRow['startPrice'] ?? 0); // === General Settings ===