265 lines
11 KiB
PHP
265 lines
11 KiB
PHP
<?php
|
||
/**
|
||
* cron_ai_engine.php - AI Pricing Engine v2
|
||
*
|
||
* يقرأ معادلات المنافسين من Node.js Pricing Engine (competitor_secret_formulas)
|
||
* ويطبّق تسعيراً ذكياً بناءً على النتائج الإحصائية بدلاً من الحسابات البدائية.
|
||
*
|
||
* لم يعُد هذا الملف يحسب بنفسه - بل يعتمد على التحليل الإحصائي من pricing-engine.
|
||
*/
|
||
|
||
require_once __DIR__ . '/../core/bootstrap.php';
|
||
require_once __DIR__ . '/../functions.php';
|
||
|
||
try {
|
||
$con = Database::get('main');
|
||
$redis = getRedisConnection();
|
||
} catch (Exception $e) {
|
||
die("Connection failed: " . $e->getMessage() . "\n");
|
||
}
|
||
|
||
echo "Starting Siro AI Engine v2 (Powered by Pricing Engine)...\n";
|
||
|
||
// ==========================================
|
||
// 1. التسعير الديناميكي بناءً على المعادلات الإحصائية
|
||
// ==========================================
|
||
echo "1. Reading competitor formulas from Pricing Engine...\n";
|
||
try {
|
||
// قراءة آخر المعادلات من التحليل الإحصائي
|
||
$sql = "SELECT * FROM competitor_secret_formulas
|
||
WHERE last_updated >= DATE_SUB(NOW(), INTERVAL 24 HOUR)
|
||
ORDER BY last_updated DESC";
|
||
$stmt = $con->query($sql);
|
||
$formulas = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||
|
||
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 = ['standard', 'economy', '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;
|
||
}
|
||
}
|
||
|
||
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') {
|
||
$normalMin = $ourMinRate > 0 ? $ourMinRate : 0.05;
|
||
$peakMin = round($normalMin * 1.15, 3);
|
||
$lateMin = $normalMin;
|
||
} else {
|
||
$normalMin = $ourMinRate > 0 ? $ourMinRate : round($speedPrice / 4, 3);
|
||
$peakMin = round($normalMin * 1.15, 3);
|
||
$lateMin = round($normalMin * 1.25, 3);
|
||
}
|
||
|
||
$updateSql = "UPDATE kazan
|
||
SET speedPrice = :speedPrice,
|
||
comfortPrice = :comfortPrice,
|
||
ladyPrice = :ladyPrice,
|
||
electricPrice = :electricPrice,
|
||
vanPrice = :vanPrice,
|
||
deliveryPrice = :deliveryPrice,
|
||
mishwarVipPrice = :mishwarVipPrice,
|
||
fixedPrice = :fixedPrice,
|
||
awfarPrice = :awfarPrice,
|
||
normalMinPrice = :normalMin,
|
||
peakMinPrice = :peakMin,
|
||
lateMinPrice = :lateMin,
|
||
startPrice = :startPrice
|
||
WHERE country = :countryName";
|
||
|
||
$upStmt = $con->prepare($updateSql);
|
||
$upStmt->execute([
|
||
':speedPrice' => $speedPrice,
|
||
':comfortPrice' => $comfortPrice,
|
||
':ladyPrice' => $ladyPrice,
|
||
':electricPrice' => $electricPrice,
|
||
':vanPrice' => $vanPrice,
|
||
':deliveryPrice' => $deliveryPrice,
|
||
':mishwarVipPrice' => $mishwarVipPrice,
|
||
':fixedPrice' => $fixedPrice,
|
||
':awfarPrice' => $awfarPrice,
|
||
':normalMin' => $normalMin,
|
||
':peakMin' => $peakMin,
|
||
':lateMin' => $lateMin,
|
||
':startPrice' => $ourBase,
|
||
':countryName' => $countryName
|
||
]);
|
||
|
||
echo " ✅ Updated $countryName pricing in kazan table.\n";
|
||
}
|
||
}
|
||
} catch (Exception $e) {
|
||
echo " ❌ Error in Pricing Module: " . $e->getMessage() . "\n";
|
||
}
|
||
|
||
// ==========================================
|
||
// 2. قراءة Surge Insights من الـ Pricing Engine
|
||
// ==========================================
|
||
echo "2. Reading surge insights from Pricing Engine...\n";
|
||
try {
|
||
$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";
|
||
}
|
||
|
||
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 Surge Module: " . $e->getMessage() . "\n";
|
||
}
|
||
|
||
// ==========================================
|
||
// 3. وحدة استهداف الركاب الخاملين (Smart Retention) - كما هي
|
||
// ==========================================
|
||
echo "3. Running Smart Retention Module...\n";
|
||
try {
|
||
$sql = "SELECT source, COUNT(*) as opens
|
||
FROM passenger_opening_locations
|
||
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";
|
||
} catch (Exception $e) {
|
||
echo " ❌ Error in Smart Retention: " . $e->getMessage() . "\n";
|
||
}
|
||
|
||
// ==========================================
|
||
// 4. Export AI Hotzones to Redis for Captains
|
||
// ==========================================
|
||
echo "4. Exporting AI Hotzones to Redis...\n";
|
||
try {
|
||
$sql = "SELECT latitude, longitude, competitor_name AS top_competitor, avg_ppk AS avg_price
|
||
FROM competitor_surge_zones
|
||
WHERE detected_at >= DATE_SUB(NOW(), INTERVAL 6 HOUR)
|
||
AND surge_multiplier > 1.1";
|
||
|
||
$stmt = $con->query($sql);
|
||
$zones = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||
|
||
if (!empty($zones)) {
|
||
$redisData = [
|
||
'status' => 'success',
|
||
'data' => $zones
|
||
];
|
||
$redis->setex('siro:cache:ai:hotzones', 3600, json_encode($redisData));
|
||
echo " 🗺️ Exported " . count($zones) . " hot zones to Redis (siro:cache:ai:hotzones).\n";
|
||
} else {
|
||
// Clear if no surge to avoid stale data
|
||
$redis->del('siro:cache:ai:hotzones');
|
||
echo " ℹ️ No active hot zones to export.\n";
|
||
}
|
||
} catch (Exception $e) {
|
||
echo " ❌ Error in Hotzones Export: " . $e->getMessage() . "\n";
|
||
}
|
||
|
||
echo "AI Engine v2 finished successfully.\n";
|