Update: 2026-07-06 17:00:43

This commit is contained in:
Hamza-Ayed
2026-07-06 17:00:43 +03:00
parent 61cb615ae7
commit e42d700245
21 changed files with 3212 additions and 119 deletions
+159 -104
View File
@@ -1,13 +1,11 @@
<?php
/**
* cron_ai_engine.php
* المحرك الرئيسي للذكاء الاصطناعي (AI Engine)
* يتم تشغيله كـ Cron Job كل 30 دقيقة أو ساعة لتقليل الضغط على السيرفر.
* cron_ai_engine.php - AI Pricing Engine v2
*
* يدمج 3 وحدات (Modules) ذكية:
* 1. AI Pricing (Total Price Math): تعديل جدول kazan ليكون السعر الإجمالي أرخص بـ 6.5% من المنافس الأقوى بدقة.
* 2. AI Dispatch: تحديد مناطق الذروة وتوجيه السائقين إليها.
* 3. AI Retention: اصطياد الركاب الخاملين.
* يقرأ معادلات المنافسين من Node.js Pricing Engine (competitor_secret_formulas)
* ويطبّق تسعيراً ذكياً بناءً على النتائج الإحصائية بدلاً من الحسابات البدائية.
*
* لم يعُد هذا الملف يحسب بنفسه - بل يعتمد على التحليل الإحصائي من pricing-engine.
*/
require_once __DIR__ . '/../core/bootstrap.php';
@@ -20,67 +18,105 @@ try {
die("Connection failed: " . $e->getMessage() . "\n");
}
echo "Starting Siro AI Engine...\n";
// نسبة الخصم المستهدفة (6.5% من إجمالي سعر الرحلة)
$targetMargin = 0.065;
echo "Starting Siro AI Engine v2 (Powered by Pricing Engine)...\n";
// ==========================================
// 1. وحدة التسعير الديناميكي بناءً على السعر الإجمالي
// 1. التسعير الديناميكي بناءً على المعادلات الإحصائية
// ==========================================
echo "1. Running Smart Pricing Module (Total Price Formula)...\n";
echo "1. Reading competitor formulas from Pricing Engine...\n";
try {
$sql = "SELECT country_code,
AVG(price_per_km) as avg_price_km,
MIN(price_per_km) as min_price_km
FROM scraped_competitor_prices
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 3 HOUR)
AND price_per_km > 0
GROUP BY country_code";
// قراءة آخر المعادلات من التحليل الإحصائي
$sql = "SELECT * FROM competitor_secret_formulas
WHERE last_updated >= DATE_SUB(NOW(), INTERVAL 24 HOUR)
ORDER BY last_updated DESC";
$stmt = $con->query($sql);
$competitorRates = $stmt->fetchAll(PDO::FETCH_ASSOC);
$formulas = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($competitorRates as $rate) {
$country = $rate['country_code'];
$countryNameMap = ['JO' => 'Jordan', 'SY' => 'Syria', 'EG' => 'Egypt', 'IQ' => 'Iraq'];
$countryName = $countryNameMap[$country] ?? null;
if ($countryName) {
$avgKmPrice = (float)$rate['avg_price_km'];
$minKmPrice = (float)$rate['min_price_km'];
// 1. حساب السعر الفعّال للكيلومتر بناءً على المتوسط والأرخص
$effectiveCompetitorPrice = round($avgKmPrice * (1 - $targetMargin), 2);
if ($effectiveCompetitorPrice > $minKmPrice) {
$effectiveCompetitorPrice = round(($effectiveCompetitorPrice + $minKmPrice) / 2, 2);
if (empty($formulas)) {
echo " ⚠️ No recent formulas found. Run pricing-engine first.\n";
} else {
// تجميع المعادلات حسب الدولة
$byCountry = [];
foreach ($formulas as $f) {
$country = $f['country_code'];
if (!isset($byCountry[$country])) $byCountry[$country] = [];
$byCountry[$country][] = $f;
}
foreach ($byCountry as $country => $countryFormulas) {
$countryNameMap = ['JO' => 'Jordan', 'SY' => 'Syria', 'EG' => 'Egypt', 'IQ' => 'Iraq'];
$countryName = $countryNameMap[$country] ?? $country;
// اختيار أفضل معادلة للتسعير حسب الأولوية:
// 1. Economy tier (الأكثر تنافسية)
// 2. Standard tier (إذا ما في Economy)
// 3. أي tier ثاني بأعلى R²
$tierPriority = ['economy', 'standard', 'premium'];
$bestFormula = null;
$bestTierIdx = 999;
foreach ($countryFormulas as $f) {
$tier = $f['tier'] ?? 'standard';
$tierIdx = array_search($tier, $tierPriority);
if ($tierIdx === false) $tierIdx = 999;
$currentRSq = (float)($f['r_squared'] ?? 0);
$currentKm = (float)($f['price_per_km'] ?? 0);
// أفضلية: Tier أعلى أولوية، ثم R² أعلى
if ($currentKm > 0 && (
$bestFormula === null ||
$tierIdx < $bestTierIdx ||
($tierIdx === $bestTierIdx && $currentRSq > (float)($bestFormula['r_squared'] ?? 0))
)) {
$bestTierIdx = $tierIdx;
$bestFormula = $f;
}
}
// 2. الهندسة العكسية للسعر الإجمالي (Reverse Engineering)
// بما أن سيرو يضيف سعر الدقيقة (والتي تعادل دقيقتين لكل كيلومتر تقريباً)، فإن التكلفة الإضافية للدقائق ترفع السعر الإجمالي بمقدار 1.5x
// لضمان أن يكون السعر النهائي أقل بـ 6%، نقسم الناتج على 1.5 ليمتص تكلفة الدقائق.
$calculatedSpeedPrice = round($effectiveCompetitorPrice / 1.5, 3);
// 3. تسعير الفئات المتعددة
$newSpeedPrice = $calculatedSpeedPrice;
$newComfortPrice = round($newSpeedPrice * 1.30, 3);
$newLadyPrice = round($newSpeedPrice * 1.10, 3);
$newElectricPrice = round($newSpeedPrice * 1.20, 3);
$newVanPrice = round($newSpeedPrice * 1.50, 3);
$newDeliveryPrice = round($newSpeedPrice * 0.90, 3);
$newMishwarVipPrice = round($newSpeedPrice * 1.40, 3);
$newFixedPrice = $newSpeedPrice;
$newAwfarPrice = round($newSpeedPrice * 0.85, 3);
if ($bestFormula) {
$bestKmRate = (float)$bestFormula['price_per_km'];
$bestMinRate = (float)$bestFormula['price_per_min'];
$bestBaseFare = (float)$bestFormula['base_fare'];
$bestMinFare = (float)$bestFormula['min_fare'];
$bestRSq = (float)($bestFormula['r_squared'] ?? 0);
}
if ($bestKmRate === null) {
echo " ⚠️ No valid formula for $countryName. Skipping.\n";
continue;
}
// تسعير Siro: أرخص بنسبة 6.5% من المنافس مع الحفاظ على هيكل التسعير
$discountFactor = 1 - 0.065;
$ourKmRate = round($bestKmRate * $discountFactor, 3);
$ourMinRate = round($bestMinRate * $discountFactor, 3);
$ourBase = round($bestBaseFare * $discountFactor, 3);
echo " 📊 $countryName: Using formula (R²=$bestRSq)\n";
echo " Competitor: KM=$bestKmRate, MIN=$bestMinRate, Base=$bestBaseFare, MinFare=$bestMinFare\n";
echo " Siro (6.5% less): KM=$ourKmRate, MIN=$ourMinRate, Base=$ourBase\n";
// تحديث جدول kazan
$speedPrice = $ourKmRate;
$comfortPrice = round($ourKmRate * 1.30, 3);
$ladyPrice = round($ourKmRate * 1.10, 3);
$electricPrice = round($ourKmRate * 1.20, 3);
$vanPrice = round($ourKmRate * 1.50, 3);
$deliveryPrice = round($ourKmRate * 0.90, 3);
$mishwarVipPrice = round($ourKmRate * 1.40, 3);
$fixedPrice = $speedPrice;
$awfarPrice = round($ourKmRate * 0.85, 3);
// أسعار الدقائق
if ($country === 'JO') {
$newNormalMin = 0.05;
$newPeakMin = 0.06;
$newLateMin = 0.05;
$normalMin = $ourMinRate > 0 ? $ourMinRate : 0.05;
$peakMin = round($normalMin * 1.15, 3);
$lateMin = $normalMin;
} else {
$newNormalMin = round($newSpeedPrice / 4, 3);
$newPeakMin = round($newNormalMin * 1.15, 3);
$newLateMin = round($newNormalMin * 1.25, 3);
$normalMin = $ourMinRate > 0 ? $ourMinRate : round($speedPrice / 4, 3);
$peakMin = round($normalMin * 1.15, 3);
$lateMin = round($normalMin * 1.25, 3);
}
$updateSql = "UPDATE kazan
@@ -97,65 +133,86 @@ try {
peakMinPrice = :peakMin,
lateMinPrice = :lateMin
WHERE country = :countryName";
$upStmt = $con->prepare($updateSql);
$upStmt->execute([
':speedPrice' => $newSpeedPrice,
':comfortPrice' => $newComfortPrice,
':ladyPrice' => $newLadyPrice,
':electricPrice' => $newElectricPrice,
':vanPrice' => $newVanPrice,
':deliveryPrice' => $newDeliveryPrice,
':mishwarVipPrice' => $newMishwarVipPrice,
':fixedPrice' => $newFixedPrice,
':awfarPrice' => $newAwfarPrice,
':normalMin' => $newNormalMin,
':peakMin' => $newPeakMin,
':lateMin' => $newLateMin,
':speedPrice' => $speedPrice,
':comfortPrice' => $comfortPrice,
':ladyPrice' => $ladyPrice,
':electricPrice' => $electricPrice,
':vanPrice' => $vanPrice,
':deliveryPrice' => $deliveryPrice,
':mishwarVipPrice' => $mishwarVipPrice,
':fixedPrice' => $fixedPrice,
':awfarPrice' => $awfarPrice,
':normalMin' => $normalMin,
':peakMin' => $peakMin,
':lateMin' => $lateMin,
':countryName' => $countryName
]);
echo " -> Updated $countryName (Total Price Math applied): Speed=$newSpeedPrice JOD/KM, NormalMin=$newNormalMin JOD/MIN\n";
echo " ✅ Updated $countryName pricing in kazan table.\n";
}
}
} catch (Exception $e) {
echo " Error in Pricing Module: " . $e->getMessage() . "\n";
echo " ❌ Error in Pricing Module: " . $e->getMessage() . "\n";
}
// ==========================================
// 2. وحدة توجيه السائقين (Demand Predictor)
// 2. قراءة Surge Insights من الـ Pricing Engine
// ==========================================
echo "2. Running Demand Predictor Module...\n";
echo "2. Reading surge insights from Pricing Engine...\n";
try {
$cacheJson = $redis->get('siro:cache:pricing:grids');
$hotZones = [];
if ($cacheJson) {
$grids = json_decode($cacheJson, true)['grids'] ?? [];
foreach ($grids as $key => $data) {
if (strpos($key, 'FALLBACK') !== false) continue;
if ($data['avg_price'] > 0) {
$parts = explode('_', $key);
if (count($parts) == 3) {
$hotZones[] = [
'latitude' => (float)$parts[1],
'longitude' => (float)$parts[2],
'avg_price' => $data['avg_price'],
'top_competitor' => $data['top_competitor'],
'timestamp' => time()
];
}
$sql = "SELECT * FROM competitor_surge_insights
WHERE detected_at >= DATE_SUB(NOW(), INTERVAL 12 HOUR)
ORDER BY surge_multiplier DESC
LIMIT 20";
$stmt = $con->query($sql);
$surgeRecords = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (!empty($surgeRecords)) {
$surgeByCountry = [];
foreach ($surgeRecords as $sr) {
$country = $sr['country_code'];
if (!isset($surgeByCountry[$country])) {
$surgeByCountry[$country] = [
'avg_multiplier' => 0,
'count' => 0,
'peak_hours' => []
];
}
$surgeByCountry[$country]['avg_multiplier'] += $sr['surge_multiplier'];
$surgeByCountry[$country]['count']++;
$surgeByCountry[$country]['peak_hours'][] = "{$sr['peak_start_hour']}:00-{$sr['peak_end_hour']}:00";
}
$redis->set('siro:cache:ai:hotzones', json_encode(['status' => 'success', 'data' => $hotZones], JSON_UNESCAPED_UNICODE));
echo " -> Saved " . count($hotZones) . " Hot Zones to Redis for Driver Map Guidance.\n";
foreach ($surgeByCountry as $country => $data) {
$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)
$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));
echo " ⚡ $country: Avg competitor surge = {$avgMult}x, peak hours: {$peakHoursStr}\n";
}
} else {
echo " ℹ️ No recent surge insights found.\n";
}
} catch (Exception $e) {
echo " Error in Demand Predictor: " . $e->getMessage() . "\n";
echo " ❌ Error in Surge Module: " . $e->getMessage() . "\n";
}
// ==========================================
// 3. وحدة استهداف الركاب الخاملين (Smart Retention)
// 3. وحدة استهداف الركاب الخاملين (Smart Retention) - كما هي
// ==========================================
echo "3. Running Smart Retention Module...\n";
try {
@@ -164,15 +221,13 @@ try {
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 3 HOUR)
GROUP BY source
HAVING opens >= 3";
$stmt = $con->query($sql);
$idleRiders = $stmt->fetchAll(PDO::FETCH_ASSOC);
$notifiedCount = count($idleRiders);
echo " -> Identified $notifiedCount idle riders requiring push notifications.\n";
echo " 📱 Identified $notifiedCount idle riders requiring push notifications.\n";
} catch (Exception $e) {
echo " Error in Smart Retention: " . $e->getMessage() . "\n";
echo " ❌ Error in Smart Retention: " . $e->getMessage() . "\n";
}
echo "AI Engine finished successfully.\n";
?>
echo "AI Engine v2 finished successfully.\n";