156 lines
5.8 KiB
PHP
156 lines
5.8 KiB
PHP
<?php
|
||
/**
|
||
* surge_opportunity_index.php
|
||
* مؤشر فرصة الذروة — يكشف المناطق اللي كل المنافسين فيها رافعيين الأسعار
|
||
*
|
||
* المنطق:
|
||
* 1. لكل منطقة grid (~1.5km)، لكل منافس
|
||
* 2. baseline = متوسط price_per_km آخر 7 أيام (بدون آخر 6 ساعات)
|
||
* 3. current = متوسط price_per_km آخر ساعتين
|
||
* 4. إذا current > baseline × 1.2 → المنافس في surge
|
||
* 5. إذا كل المنافسين النشطين في zone في surge → فرصة ذروة ✅
|
||
*/
|
||
|
||
require_once __DIR__ . '/../../connect.php';
|
||
|
||
if ($role !== 'admin' && $role !== 'super_admin') {
|
||
http_response_code(403);
|
||
echo json_encode(['status' => 'failure', 'message' => 'Unauthorized']);
|
||
exit;
|
||
}
|
||
|
||
try {
|
||
$countryCode = filterRequest('country_code');
|
||
|
||
$where = '';
|
||
$params = [];
|
||
if ($countryCode) {
|
||
$where = 'AND cp.country_code = :country';
|
||
$params[':country'] = strtoupper($countryCode);
|
||
}
|
||
|
||
// 1. حساب الـ baseline (آخر 7 أيام، بدون آخر 6 ساعات)
|
||
// و current (آخر ساعتين) لكل منافس في كل خلية grid
|
||
$sql = "SELECT
|
||
ROUND(cp.from_latitude * 74, 0) / 74 AS lat_group,
|
||
ROUND(cp.from_longitude * 74, 0) / 74 AS lng_group,
|
||
cp.competitor_name,
|
||
cp.country_code,
|
||
AVG(CASE WHEN cp.created_at < DATE_SUB(NOW(), INTERVAL 6 HOUR)
|
||
THEN cp.price_per_km END) AS baseline_avg,
|
||
AVG(CASE WHEN cp.created_at >= DATE_SUB(NOW(), INTERVAL 2 HOUR)
|
||
THEN cp.price_per_km END) AS current_avg,
|
||
COUNT(*) AS total_samples,
|
||
SUM(CASE WHEN cp.created_at >= DATE_SUB(NOW(), INTERVAL 2 HOUR) THEN 1 ELSE 0 END) AS recent_samples
|
||
FROM competitor_prices cp
|
||
WHERE cp.created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)
|
||
AND cp.price_per_km > 0
|
||
$where
|
||
GROUP BY lat_group, lng_group, cp.competitor_name, cp.country_code
|
||
HAVING recent_samples >= 2
|
||
ORDER BY lat_group, lng_group, cp.competitor_name";
|
||
|
||
$stmt = $con->prepare($sql);
|
||
if ($countryCode) {
|
||
foreach ($params as $k => $v) {
|
||
$stmt->bindValue($k, $v);
|
||
}
|
||
}
|
||
$stmt->execute();
|
||
$rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||
|
||
// 2. تجميع البيانات لكل zone
|
||
$zones = [];
|
||
foreach ($rows as $row) {
|
||
$zoneKey = $row['lat_group'] . '_' . $row['lng_group'];
|
||
|
||
$baseline = (float)$row['baseline_avg'];
|
||
$current = (float)$row['current_avg'];
|
||
|
||
$surgeRatio = ($baseline > 0) ? round($current / $baseline, 2) : 1.0;
|
||
$isSurging = $baseline > 0 && $surgeRatio >= 1.2;
|
||
|
||
if (!isset($zones[$zoneKey])) {
|
||
$zones[$zoneKey] = [
|
||
'lat' => (float)$row['lat_group'],
|
||
'lng' => (float)$row['lng_group'],
|
||
'country_code' => $row['country_code'],
|
||
'competitors' => [],
|
||
'total_active' => 0,
|
||
'total_surging' => 0,
|
||
];
|
||
}
|
||
|
||
$zones[$zoneKey]['competitors'][] = [
|
||
'name' => $row['competitor_name'],
|
||
'baseline' => round($baseline, 2),
|
||
'current' => round($current, 2),
|
||
'surge_ratio' => $surgeRatio,
|
||
'is_surging' => $isSurging,
|
||
];
|
||
$zones[$zoneKey]['total_active']++;
|
||
if ($isSurging) {
|
||
$zones[$zoneKey]['total_surging']++;
|
||
}
|
||
}
|
||
|
||
// 3. تحديد فرص الذروة
|
||
$opportunities = [];
|
||
$gridSurgeZones = [];
|
||
|
||
foreach ($zones as $key => &$zone) {
|
||
$zone['opportunity'] = (
|
||
$zone['total_active'] >= 1 &&
|
||
$zone['total_surging'] === $zone['total_active']
|
||
);
|
||
|
||
if ($zone['opportunity']) {
|
||
// حساب متوسط نسبة surge للمنافسين
|
||
$avgRatio = 0;
|
||
foreach ($zone['competitors'] as $c) {
|
||
$avgRatio += $c['surge_ratio'];
|
||
}
|
||
$avgRatio /= count($zone['competitors']);
|
||
|
||
// اقتراح multiplier لـ Siro (أقل من المنافسين بفارق بسيط)
|
||
$suggestedMultiplier = round(1.0 + ($avgRatio - 1.0) * 0.6, 2);
|
||
if ($suggestedMultiplier < 1.0) $suggestedMultiplier = 1.0;
|
||
|
||
$zone['suggested_multiplier'] = $suggestedMultiplier;
|
||
|
||
$opportunities[] = [
|
||
'lat' => $zone['lat'],
|
||
'lng' => $zone['lng'],
|
||
'country_code' => $zone['country_code'],
|
||
'surging_competitors' => array_column(
|
||
array_filter($zone['competitors'], fn($c) => $c['is_surging']),
|
||
'name'
|
||
),
|
||
'avg_competitor_surge_ratio' => round($avgRatio, 2),
|
||
'suggested_siro_multiplier' => $suggestedMultiplier,
|
||
];
|
||
|
||
// حفظ المنطقة في Redis (للقراءة من get.php بعدين)
|
||
$gridSurgeZones[$key] = $suggestedMultiplier;
|
||
}
|
||
}
|
||
unset($zone);
|
||
|
||
// 4. تخزين فرص الذروة في Redis بصلاحية 10 دقائق
|
||
if (!empty($gridSurgeZones) && isset($redis) && $redis !== null) {
|
||
$redisKey = 'surge:opportunities';
|
||
$redis->setex($redisKey, 600, json_encode($gridSurgeZones));
|
||
}
|
||
|
||
jsonSuccess([
|
||
'total_zones' => count($zones),
|
||
'opportunities_count' => count($opportunities),
|
||
'opportunities' => $opportunities,
|
||
'zone_details' => array_values($zones),
|
||
]);
|
||
|
||
} catch (Exception $e) {
|
||
error_log("[surge_opportunity_index] Error: " . $e->getMessage());
|
||
jsonError("Failed to calculate surge opportunity index");
|
||
}
|