feat: استيراد كود سيرو إلى تريبز (سيرو @ecfe7568) — بلا تعديل

قرار المالك 2026-07-27: باك إند سيرو PHP هو المعتمد، وتطبيقاته المجرّبة
ميدانياً تحل محل إعادة البناء المؤرشفة. سيرو نفسه لم يُمسّ.

الخريطة:
  backend · payment_server · loction_server · ride_server ·
  passenger_server · docker · dashboard · stress_test  → الجذر
  siro_rider  → apps/rider          siro_driver  → apps/driver
  siro_admin  → dashboards/admin    siro_service → dashboards/service
  android_bot → apps/android_bot    socialBot    → apps/socialBot

نُسخ المتعقَّب في git سيرو فقط عبر `git archive` (3,198 ملفاً / ~169 م.ب)
لا `cp -r` — فاستُثنيت مخلفات البناء تلقائياً. بلا أي تعديل محتوى عمداً:
كل ما يلي يصير فرقاً مقروءاً مقابل المصدر.

لم يُستورد وسببه: siromove.com (الموقع التسويقي يبقى marketing/ في تريبز،
سيرو فيه 8 ملفات) · docs و planning (تريبز له docs/ الخاص) · deploy.sh
(ليس نشراً على سيرفر بل `git add . && git push origin --all` — فخّ في
مستودع آخر) · transit_dashboard (بانتظار قرار مصير backend-transit و
dashboards/transit-web).

⚠️ لا يبني بعد — ثلاثة نواقص متوقعة ومقصودة:
1. `.env` و `lib/env/env.g.dart` غير متعقَّبين في سيرو (أسرار لكل مستأجر):
   كل تطبيق فلاتر يحتاج .env خاصاً ثم توليد env.g.dart بـ build_runner.
2. إعدادات Firebase (9 ملفات google-services.json و GoogleService-Info.plist)
   يستبعدها .gitignore تريبز — ولكل مستأجر مشروع Firebase خاص أصلاً.
3. apps/driver في سيرو يشير إلى `../../Intaleq/packages/get` خارج المستودع →
   يجب ضمّ الحزم داخله أسوة بـ apps/rider.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hamza-Ayed
2026-07-27 05:14:13 +03:00
co-authored by Claude Opus 5
parent 9909d9b4c1
commit 4d8414c96b
3198 changed files with 766859 additions and 0 deletions
@@ -0,0 +1,64 @@
<?php
/**
* ai_price_prediction.php
* يتوقع أوقات الذروة القادمة (Surge Prediction) بناءً على تحليل الشواذ السابقة
*/
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');
if (!$countryCode) {
jsonError("Missing required parameter: country_code");
exit;
}
// 1. Analyze the most common hours for competitor surges in the last 14 days
$sql = "SELECT HOUR(created_at) as surge_hour, COUNT(*) as frequency
FROM price_anomalies
WHERE country_code = :country
AND anomaly_type = 'opportunity'
AND created_at >= DATE_SUB(NOW(), INTERVAL 14 DAY)
GROUP BY surge_hour
ORDER BY frequency DESC
LIMIT 3";
$stmt = $con->prepare($sql);
$stmt->execute([':country' => strtoupper($countryCode)]);
$peakHours = $stmt->fetchAll(PDO::FETCH_ASSOC);
// 2. Prepare prediction message
$predictionMessage = "لا تتوفر بيانات كافية حالياً لبناء نموذج توقع דقيق.";
$predictedHours = [];
if (count($peakHours) > 0) {
$hoursStr = [];
foreach ($peakHours as $h) {
$predictedHours[] = (int)$h['surge_hour'];
$time = sprintf("%02d:00", $h['surge_hour']);
$hoursStr[] = $time;
}
$predictionMessage = "بناءً على خوارزميات التوقع وتحليل 14 يوماً من البيانات السابقة، يتوقع النظام حدوث ذروة عالية لدى المنافسين في الأوقات التالية اليوم: " . implode('، ', $hoursStr) . ". يُنصح بتجهيز كباتن سيرو مسبقاً في هذه الأوقات.";
}
// Optional: Could send $predictionMessage to Gemini for more conversational output.
// For performance, we return the deterministic heuristic here.
jsonSuccess([
'status' => 'success',
'predicted_surge_hours' => $predictedHours,
'ai_analysis_message' => $predictionMessage,
'confidence_score' => count($peakHours) > 0 ? 85 : 0
]);
} catch (Exception $e) {
error_log("[ai_price_prediction] Error: " . $e->getMessage());
jsonError("Failed to run AI prediction");
}
@@ -0,0 +1,76 @@
<?php
// ============================================================
// Admin/marketing/get_campaigns_log.php
// API Endpoint to fetch marketing campaign delivery logs for Admin dashboard
// ============================================================
require_once __DIR__ . '/../../connect.php';
// 1. Authorize Admin/Super Admin
if ($role !== 'admin' && $role !== 'super_admin') {
http_response_code(403);
echo json_encode(['status' => 'failure', 'message' => 'Unauthorized access. Admin role required.']);
exit;
}
try {
$limit = filterRequest('limit', 'int') ?? 50;
$countryCode = filterRequest('country_code');
$sql = "SELECT l.*, p.first_name, p.last_name
FROM marketing_campaigns_log l
LEFT JOIN passengers p ON p.id = l.passenger_id";
$params = [];
if ($countryCode) {
$sql .= " WHERE l.country_code = :country";
$params[':country'] = strtoupper($countryCode);
}
$sql .= " ORDER BY l.sent_at DESC LIMIT :limit";
$stmt = $con->prepare($sql);
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
foreach ($params as $key => $val) {
$stmt->bindValue($key, $val);
}
$stmt->execute();
$logs = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Decrypt names since they are encrypted in the passengers table
foreach ($logs as &$log) {
if (!empty($log['first_name'])) {
$decName = $encryptionHelper->decryptData($log['first_name']);
if ($decName) $log['first_name'] = $decName;
}
if (!empty($log['last_name'])) {
$decName = $encryptionHelper->decryptData($log['last_name']);
if ($decName) $log['last_name'] = $decName;
}
}
unset($log);
// Aggregate statistics for Dashboard charts
$sqlStats = "SELECT message_type, COUNT(*) as count
FROM marketing_campaigns_log";
if ($countryCode) {
$sqlStats .= " WHERE country_code = :country";
$sqlStats .= " GROUP BY message_type";
$stmtStats = $con->prepare($sqlStats);
$stmtStats->execute([':country' => strtoupper($countryCode)]);
} else {
$sqlStats .= " GROUP BY message_type";
$stmtStats = $con->prepare($sqlStats);
$stmtStats->execute();
}
$stats = $stmtStats->fetchAll(PDO::FETCH_ASSOC);
jsonSuccess([
'logs' => $logs,
'stats' => $stats
]);
} catch (Exception $e) {
error_log("[get_campaigns_log.php] Error: " . $e->getMessage());
jsonError("Failed to fetch campaigns log: " . $e->getMessage());
}
@@ -0,0 +1,62 @@
<?php
// ============================================================
// Admin/marketing/get_market_anomalies.php
// API Endpoint for Admin App (Flutter) to fetch price anomalies
// ============================================================
require_once __DIR__ . '/../../connect.php';
// 1. Authorize role
if ($role !== 'admin' && $role !== 'super_admin') {
http_response_code(403);
echo json_encode(['status' => 'failure', 'message' => 'Unauthorized access. Admin role required.']);
exit;
}
// 2. Fetch anomalies
try {
$limit = filterRequest('limit', 'int') ?? 50;
$countryCode = filterRequest('country_code');
$sql = "SELECT * FROM price_anomalies";
$params = [];
if ($countryCode) {
$sql .= " WHERE country_code = :country";
$params[':country'] = strtoupper($countryCode);
}
$sql .= " ORDER BY created_at DESC LIMIT :limit";
$stmt = $con->prepare($sql);
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
foreach ($params as $key => $val) {
$stmt->bindValue($key, $val);
}
$stmt->execute();
$anomalies = $stmt->fetchAll(PDO::FETCH_ASSOC);
// Fetch some recent competitor prices for context
$sqlPrices = "SELECT * FROM scraped_competitor_prices";
$paramsPrices = [];
if ($countryCode) {
$sqlPrices .= " WHERE country_code = :country";
$paramsPrices[':country'] = strtoupper($countryCode);
}
$sqlPrices .= " ORDER BY created_at DESC LIMIT 20";
$stmtPrices = $con->prepare($sqlPrices);
foreach ($paramsPrices as $key => $val) {
$stmtPrices->bindValue($key, $val);
}
$stmtPrices->execute();
$recentPrices = $stmtPrices->fetchAll(PDO::FETCH_ASSOC);
jsonSuccess([
'anomalies' => $anomalies,
'recent_prices' => $recentPrices
]);
} catch (Exception $e) {
error_log("[get_market_anomalies.php] Error: " . $e->getMessage());
jsonError("Failed to fetch market anomalies: " . $e->getMessage());
}
@@ -0,0 +1,55 @@
<?php
/**
* get_market_share_analytics.php
* جلب بيانات الحصة السوقية التاريخية لعرضها كرسوم بيانية للإدارة
*/
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');
if (!$countryCode) {
jsonError("Missing required parameter: country_code");
exit;
}
// Fetch up to 12 weeks of historical market health reports
$sql = "SELECT report_date, average_pci, market_share_percent, total_anomalies, total_surge_opportunities
FROM market_health_reports
WHERE country_code = :country
ORDER BY report_date ASC
LIMIT 12";
$stmt = $con->prepare($sql);
$stmt->execute([':country' => strtoupper($countryCode)]);
$reports = $stmt->fetchAll(PDO::FETCH_ASSOC);
// If no reports exist yet, we can simulate or return empty.
// For now, we return exactly what is in the DB.
$chartData = [];
foreach ($reports as $row) {
$chartData[] = [
'date' => $row['report_date'],
'pci' => (float)$row['average_pci'],
'market_share' => (float)$row['market_share_percent'],
'anomalies' => (int)$row['total_anomalies'],
'surges' => (int)$row['total_surge_opportunities']
];
}
jsonSuccess([
'status' => 'success',
'historical_data' => $chartData
]);
} catch (Exception $e) {
error_log("[get_market_share_analytics] Error: " . $e->getMessage());
jsonError("Failed to fetch analytics");
}
@@ -0,0 +1,77 @@
<?php
require_once __DIR__ . '/../../connect.php';
if ($role !== 'admin' && $role !== 'super_admin') {
http_response_code(403);
echo json_encode(['status' => 'failure', 'message' => 'Unauthorized access.']);
exit;
}
try {
$countryCode = filterRequest('country_code');
// 1. Hourly competitor price averages (last 24h)
$compSql = "SELECT
DATE_FORMAT(created_at, '%Y-%m-%d %H:00:00') AS hour_bucket,
AVG(price_per_km) AS avg_price_per_km,
COUNT(*) AS sample_count
FROM scraped_competitor_prices
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)";
$compParams = [];
if ($countryCode) {
$compSql .= " AND country_code = :country";
$compParams[':country'] = strtoupper($countryCode);
}
$compSql .= " GROUP BY hour_bucket ORDER BY hour_bucket ASC LIMIT 24";
$stmt = $con->prepare($compSql);
foreach ($compParams as $k => $v) {
$stmt->bindValue($k, $v);
}
$stmt->execute();
$hourlyData = $stmt->fetchAll(PDO::FETCH_ASSOC);
// 2. PCI by region — group competitor prices by ~0.02° grid cells
$pciSql = "SELECT
ROUND(start_lat * 50, 0) / 50 AS lat_group,
ROUND(start_lng * 50, 0) / 50 AS lng_group,
competitor_name,
AVG(price_per_km) AS avg_price_per_km,
COUNT(*) AS samples
FROM scraped_competitor_prices
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)";
$pciParams = [];
if ($countryCode) {
$pciSql .= " AND country_code = :country2";
$pciParams[':country2'] = strtoupper($countryCode);
}
$pciSql .= " GROUP BY lat_group, lng_group, competitor_name
ORDER BY samples DESC LIMIT 20";
$stmtPci = $con->prepare($pciSql);
foreach ($pciParams as $k => $v) {
$stmtPci->bindValue($k, $v);
}
$stmtPci->execute();
$pciData = $stmtPci->fetchAll(PDO::FETCH_ASSOC);
// 3. Siro base prices by category (from kazan table)
$siroSql = "SELECT speedPrice, comfortPrice, awfarPrice, ladyPrice, electricPrice, vanPrice
FROM kazan WHERE country = :country3 LIMIT 1";
$countryNameMap = ['SY' => 'Syria', 'JO' => 'Jordan', 'EG' => 'Egypt', 'IQ' => 'Iraq'];
$siroCountry = $countryNameMap[strtoupper($countryCode ?: 'SY')] ?? 'Syria';
$stmtSiro = $con->prepare($siroSql);
$stmtSiro->execute([':country3' => $siroCountry]);
$siroPrices = $stmtSiro->fetch(PDO::FETCH_ASSOC);
jsonSuccess([
'hourly_competitor_prices' => $hourlyData,
'pci_regions' => $pciData,
'siro_base_prices' => $siroPrices ?: [],
]);
} catch (Exception $e) {
error_log("[get_price_comparison.php] Error: " . $e->getMessage());
jsonError("Failed to fetch price comparison: " . $e->getMessage());
}
@@ -0,0 +1,87 @@
<?php
/**
* get_price_gap_heatmap.php
* يجلب بيانات الخريطة الحرارية (Price Gap Heatmap) لعرضها في تطبيق Flutter
*/
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');
if (!$countryCode) {
jsonError("Missing required parameter: country_code");
exit;
}
// Determine current Siro speed price
$sqlKazan = "SELECT speedPrice FROM kazan WHERE country = :country LIMIT 1";
$stmtKazan = $con->prepare($sqlKazan);
$countryNameMap = ['SY' => 'Syria', 'JO' => 'Jordan', 'EG' => 'Egypt', 'IQ' => 'Iraq'];
$stmtKazan->execute([':country' => $countryNameMap[strtoupper($countryCode)] ?? 'Syria']);
$kazanRow = $stmtKazan->fetch(PDO::FETCH_ASSOC);
$currentSpeedPrice = $kazanRow ? (float)$kazanRow['speedPrice'] : 0;
if ($currentSpeedPrice <= 0) {
jsonError("Siro base price not configured for this country.");
exit;
}
// Aggregate competitor data by geographical grid (approx 1.5km x 1.5km)
$sql = "SELECT
ROUND(start_lat * 74, 0) / 74 AS lat_group,
ROUND(start_lng * 74, 0) / 74 AS lng_group,
AVG(price_per_km) as avg_competitor_price_per_km,
COUNT(*) as trip_count
FROM scraped_competitor_prices
WHERE country_code = :country
AND price_per_km > 0
AND created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)
GROUP BY lat_group, lng_group
HAVING trip_count >= 3"; // Require at least 3 trips for a reliable heatmap point
$stmt = $con->prepare($sql);
$stmt->execute([':country' => strtoupper($countryCode)]);
$grids = $stmt->fetchAll(PDO::FETCH_ASSOC);
$heatmapData = [];
foreach ($grids as $grid) {
$compPricePerKm = (float)$grid['avg_competitor_price_per_km'];
if ($compPricePerKm <= 0) continue;
// Calculate PCI for this specific grid
// PCI < 1 means we are cheaper. PCI > 1 means we are more expensive.
$pci = round($currentSpeedPrice / $compPricePerKm, 2);
// Calculate the "weight" for the heatmap renderer
// E.g. -1 (We are 100% cheaper) to +1 (We are 100% more expensive)
$weight = round($pci - 1.0, 2);
// Clamp between -1 and 1
$weight = max(-1.0, min(1.0, $weight));
$heatmapData[] = [
'lat' => (float)$grid['lat_group'],
'lng' => (float)$grid['lng_group'],
'pci' => $pci,
'weight' => $weight, // Negative = Green (Cheaper), Positive = Red (More expensive)
'sample_size' => (int)$grid['trip_count']
];
}
jsonSuccess([
'total_heatmap_points' => count($heatmapData),
'current_siro_price_per_km' => $currentSpeedPrice,
'heatmap_data' => $heatmapData
]);
} catch (Exception $e) {
error_log("[get_price_gap_heatmap] Error: " . $e->getMessage());
jsonError("Failed to generate heatmap data");
}
@@ -0,0 +1,57 @@
<?php
// ============================================================
// get_pricing_stability_log.php
// شاشة مراجعة محرك الثبات (Shadow Mode) — يعرض سجل التصنيفات
// والإجراءات المقترحة بدون ما يكون أي منها مطبّق فعلياً على kazan
// ============================================================
require_once __DIR__ . '/../../connect.php';
if ($role !== 'admin' && $role !== 'super_admin') {
http_response_code(403);
echo json_encode(['status' => 'failure', 'message' => 'Unauthorized access. Admin role required.']);
exit;
}
try {
$countryCode = filterRequest('country_code');
$limit = filterRequest('limit', 'int') ?? 100;
$sql = "SELECT * FROM pricing_stability_log";
$params = [];
if ($countryCode) {
$sql .= " WHERE country_code = :country";
$params[':country'] = strtoupper($countryCode);
}
$sql .= " ORDER BY evaluated_at DESC LIMIT :limit";
$stmt = $con->prepare($sql);
$stmt->bindValue(':limit', $limit, PDO::PARAM_INT);
foreach ($params as $key => $val) {
$stmt->bindValue($key, $val);
}
$stmt->execute();
$log = $stmt->fetchAll(PDO::FETCH_ASSOC);
// ملخص سريع لآخر تصنيف لكل دولة
$stmtLatest = $con->query("
SELECT l1.* FROM pricing_stability_log l1
INNER JOIN (
SELECT country_code, MAX(evaluated_at) AS max_time
FROM pricing_stability_log
GROUP BY country_code
) l2 ON l1.country_code = l2.country_code AND l1.evaluated_at = l2.max_time
");
$latestPerCountry = $stmtLatest->fetchAll(PDO::FETCH_ASSOC);
jsonSuccess([
'log' => $log,
'latest_per_country' => $latestPerCountry,
]);
} catch (Exception $e) {
error_log("[get_pricing_stability_log.php] Error: " . $e->getMessage());
jsonError("Failed to fetch pricing stability log: " . $e->getMessage());
}
+55
View File
@@ -0,0 +1,55 @@
<?php
require_once __DIR__ . '/../../connect.php';
if ($role !== 'admin' && $role !== 'super_admin') {
http_response_code(403);
echo json_encode(['status' => 'failure', 'message' => 'Unauthorized access. Admin role required.']);
exit;
}
try {
$countryCode = filterRequest('country_code');
$countSql = "SELECT COUNT(*) FROM marketing_campaigns_log";
$params = [];
if ($countryCode) {
$countSql .= " WHERE country_code = :country";
$params[':country'] = strtoupper($countryCode);
}
$stmt = $con->prepare($countSql);
foreach ($params as $key => $val) {
$stmt->bindValue($key, $val);
}
$stmt->execute();
$campaignCount = (int)$stmt->fetchColumn();
$estTokensPerCampaign = 3250;
$estCostPerCampaign = 0.00048;
$totalTokens = $campaignCount * $estTokensPerCampaign;
$estimatedCost = $campaignCount * $estCostPerCampaign;
$anomalySql = "SELECT COUNT(*) FROM price_anomalies";
$anomalyParams = [];
if ($countryCode) {
$anomalySql .= " WHERE country_code = :country2";
$anomalyParams[':country2'] = strtoupper($countryCode);
}
$stmtAnomaly = $con->prepare($anomalySql);
foreach ($anomalyParams as $key => $val) {
$stmtAnomaly->bindValue($key, $val);
}
$stmtAnomaly->execute();
$anomalyCount = (int)$stmtAnomaly->fetchColumn();
jsonSuccess([
'api_requests_count' => $campaignCount,
'total_tokens_used' => $totalTokens,
'estimated_cost_usd' => round($estimatedCost, 6),
'campaigns_count' => $campaignCount,
'anomalies_count' => $anomalyCount,
]);
} catch (Exception $e) {
error_log("[get_telemetry.php] Error: " . $e->getMessage());
jsonError("Failed to fetch telemetry: " . $e->getMessage());
}
@@ -0,0 +1,155 @@
<?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.start_lat * 74, 0) / 74 AS lat_group,
ROUND(cp.start_lng * 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 scraped_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");
}
@@ -0,0 +1,265 @@
<?php
// ============================================================
// Admin/marketing/trigger_campaign.php
// API Endpoint to trigger Gemini AI campaign generation and dispatch
// ============================================================
require_once __DIR__ . '/../../connect.php';
require_once __DIR__ . '/../../core/Services/SiroGeminiService.php';
// 1. Authorize Admin/Super Admin
if ($role !== 'admin' && $role !== 'super_admin') {
http_response_code(403);
echo json_encode(['status' => 'failure', 'message' => 'Unauthorized access. Admin role required.']);
exit;
}
// 2. Filter inputs
$countryCode = filterRequest('country_code') ?? 'SY';
$regionName = filterRequest('region_name');
if (empty($regionName)) {
if ($countryCode === 'JO') $regionName = 'Amman';
elseif ($countryCode === 'EG') $regionName = 'Cairo';
elseif ($countryCode === 'IQ') $regionName = 'Baghdad';
else $regionName = 'Damascus';
}
$siroBasePrice = filterRequest('siro_base_price', 'float') ?? 10000.0;
try {
// 3. Fetch recent competitor prices for this region to supply context to Gemini
$sqlPrices = "SELECT competitor_name, price_amount AS total_price, (price_amount / price_per_km) AS distance_km
FROM scraped_competitor_prices
WHERE country_code = :country AND price_per_km > 0
ORDER BY created_at DESC LIMIT 10";
$stmtPrices = $con->prepare($sqlPrices);
$stmtPrices->execute([':country' => strtoupper($countryCode)]);
$competitorPrices = $stmtPrices->fetchAll(PDO::FETCH_ASSOC);
if (empty($competitorPrices)) {
// Fallback mock context if no competitor pricing has been scraped yet
$competitorPrices = [
['competitor_name' => 'yallago', 'total_price' => 12000, 'distance_km' => 5],
['competitor_name' => 'zaken', 'total_price' => 11500, 'distance_km' => 5]
];
}
// 4. Initialize Gemini AI service and run market analysis
$geminiService = new SiroGeminiService();
$aiCampaign = $geminiService->analyzeMarketAndDraftCampaign(
$competitorPrices,
$siroBasePrice,
$regionName,
$countryCode
);
if (!$aiCampaign) {
jsonError("Failed to generate campaign via Gemini AI service.");
}
// Check if campaign is recommended
$opportunityDetected = $aiCampaign['opportunity_detected'] ?? false;
if (!$opportunityDetected) {
jsonSuccess([
'campaign_created' => false,
'reason' => 'Gemini AI determined no marketing opportunity is present based on current pricing structures.',
'ai_analysis' => $aiCampaign
]);
}
$promoCode = $aiCampaign['promo_code'] ?? 'SIROGO10';
$discountVal = $aiCampaign['discount_percentage'] ?? 10;
$pushTitle = $aiCampaign['push_title'] ?? 'خصومات مميزة من سيرو!';
$pushBody = $aiCampaign['push_body'] ?? 'وفر أكثر على رحلتك القادمة معنا.';
$smsBody = $aiCampaign['sms_body'] ?? 'اشتقنا لك! عد إلينا ووفر أكثر مع الرمز الترويجي الخاص بك.';
// 5. Target Passengers in the specified country
// Since phone numbers are encrypted, we fetch all passengers, decrypt, and filter by country prefix.
$sqlTarget = "SELECT id AS passenger_id, phone FROM passengers";
$stmtTarget = $con->prepare($sqlTarget);
$stmtTarget->execute();
$allPassengers = $stmtTarget->fetchAll(PDO::FETCH_ASSOC);
$targets = [];
$debugCounts = ['JO' => 0, 'SY' => 0, 'EG' => 0, 'IQ' => 0, 'UNKNOWN' => 0, 'DECRYPT_FAIL' => 0];
foreach ($allPassengers as $p) {
$decryptedPhone = $encryptionHelper->decryptData($p['phone']);
if (!$decryptedPhone) {
$debugCounts['DECRYPT_FAIL']++;
continue;
}
$cleanPhone = preg_replace('/[^0-9]/', '', $decryptedPhone);
$pCountry = 'UNKNOWN';
if (strpos($cleanPhone, '962') === 0 || strpos($cleanPhone, '07') === 0) $pCountry = 'JO';
elseif (strpos($cleanPhone, '963') === 0 || (strpos($cleanPhone, '09') === 0 && strlen($cleanPhone) == 10)) $pCountry = 'SY';
elseif (strpos($cleanPhone, '20') === 0 || (strpos($cleanPhone, '01') === 0 && strlen($cleanPhone) == 11)) $pCountry = 'EG';
elseif (strpos($cleanPhone, '964') === 0) $pCountry = 'IQ';
$debugCounts[$pCountry]++;
if ($pCountry === strtoupper($countryCode)) {
$targets[] = ['passenger_id' => $p['passenger_id'], 'decrypted_phone' => $decryptedPhone];
}
}
$sentFcm = 0;
$sentSms = 0;
$sentWhatsApp = 0;
$dispatchedPassengers = [];
$fcmErrors = [];
// 5.5 وضع المعاينة: يُرجع ما ستفعله الحملة (النص، الكود، حجم الجمهور)
// دون إنشاء كود ترويجي ودون إرسال أي إشعار. الحملة تُنشئ خصماً حقيقياً
// وتصل كل ركاب الدولة، فوجود معاينة قبل الإطلاق ضروري.
if (filterRequest('dry_run') === '1') {
jsonSuccess([
'dry_run' => true,
'campaign_created' => false,
'promo_code' => $promoCode,
'discount_percent' => $discountVal,
'region' => $regionName,
'country_code' => strtoupper($countryCode),
'audience_size' => count($targets),
'ai_analysis' => $aiCampaign,
], 'Preview only — no promo code was created and no notification was sent.');
}
// 6. Save broadcast promo for this campaign (Option 1 - promos table adjustment)
$sqlPromo = "INSERT INTO promos
(promo_code, amount, description, passengerID, source, validity_start_date, validity_end_date)
VALUES (:code, :amount, :desc, 'all', 'ai_generated', CURDATE(), DATE_ADD(CURDATE(), INTERVAL 7 DAY))";
$stmtPromo = $con->prepare($sqlPromo);
$stmtPromo->execute([
':code' => $promoCode,
':amount' => (string)$discountVal,
':desc' => "AI Dynamic Promo: $promoCode ($discountVal%)"
]);
foreach ($targets as $target) {
$passengerId = $target['passenger_id'];
// Enforce anti-spam: check if passenger received any SMS/WhatsApp campaign in the last 24 hours
$sqlSpamCheck = "SELECT COUNT(*) FROM marketing_campaigns_log
WHERE passenger_id = :pid
AND message_type IN ('sms', 'whatsapp')
AND sent_at > DATE_SUB(NOW(), INTERVAL 24 HOUR)";
$stmtSpam = $con->prepare($sqlSpamCheck);
$stmtSpam->execute([':pid' => $passengerId]);
$spamCount = intval($stmtSpam->fetchColumn());
// Check if passenger has active FCM token
$sqlToken = "SELECT token FROM tokens WHERE passengerID = :pid ORDER BY id DESC LIMIT 1";
$stmtToken = $con->prepare($sqlToken);
$stmtToken->execute([':pid' => $passengerId]);
$fcmToken = $stmtToken->fetchColumn();
$pushSent = false;
if ($fcmToken) {
$decryptedToken = $encryptionHelper->decryptData($fcmToken);
if ($decryptedToken) {
// Send FCM Push Notification (Free channel - no anti-spam restriction needed)
$fcmData = [
'type' => 'marketing_campaign',
'promo_code' => $promoCode,
'discount' => (string)$discountVal
];
$fcmResult = sendFcmNotification(
$decryptedToken,
$pushTitle,
$pushBody,
$fcmData,
'Marketing',
'notification'
);
if ($fcmResult['status'] === 'success') {
$sentFcm++;
// Log campaign dispatch
$logStmt = $con->prepare("INSERT INTO marketing_campaigns_log (passenger_id, message_type, country_code, region_name, triggered_by) VALUES (?, 'push', ?, ?, 'autopilot')");
$logStmt->execute([$passengerId, $countryCode, $regionName]);
$dispatchedPassengers[] = $passengerId;
$pushSent = true;
} else {
$fcmErrors[] = ['passenger_id' => $passengerId, 'error' => $fcmResult];
}
} else {
$fcmErrors[] = ['passenger_id' => $passengerId, 'error' => 'Token decryption failed'];
}
} else {
$fcmErrors[] = ['passenger_id' => $passengerId, 'error' => 'No token in DB'];
}
if (!$pushSent) {
// Fallback: Churned user (No token) OR Push failed -> Send WhatsApp or SMS
// Check anti-spam first to prevent unnecessary marketing cost
if ($spamCount === 0) {
// Fetch and decrypt passenger phone number
$sqlUser = "SELECT phone FROM passengers WHERE id = :pid LIMIT 1";
$stmtUser = $con->prepare($sqlUser);
$stmtUser->execute([':pid' => $passengerId]);
$encPhone = $stmtUser->fetchColumn();
if ($encPhone) {
$decryptedPhone = $encryptionHelper->decryptData($encPhone);
if ($decryptedPhone) {
// Send WhatsApp (or fallback to SMS simulation)
$waResult = sendWhatsAppFromServer($decryptedPhone, $smsBody);
if ($waResult && ($waResult['status'] ?? '') === 'success') {
$sentWhatsApp++;
$logStmt = $con->prepare("INSERT INTO marketing_campaigns_log (passenger_id, message_type, country_code, region_name, triggered_by) VALUES (?, 'whatsapp', ?, ?, 'autopilot')");
$logStmt->execute([$passengerId, $countryCode, $regionName]);
$dispatchedPassengers[] = $passengerId;
} else {
// Fallback to SMS simulation
$sentSms++;
$logStmt = $con->prepare("INSERT INTO marketing_campaigns_log (passenger_id, message_type, country_code, region_name, triggered_by) VALUES (?, 'sms', ?, ?, 'autopilot')");
$logStmt->execute([$passengerId, $countryCode, $regionName]);
$dispatchedPassengers[] = $passengerId;
}
}
}
}
}
}
// Log the audit event for Admin action
logAudit(
$con,
$user_id ?? 'admin_system',
'trigger_marketing_campaign',
'promos',
$promoCode,
['promo_code' => $promoCode, 'targets_count' => count($dispatchedPassengers)]
);
jsonSuccess([
'campaign_created' => true,
'promo_code' => $promoCode,
'discount_percentage' => $discountVal,
'push_notification' => [
'title' => $pushTitle,
'body' => $pushBody,
'sent_count' => $sentFcm
],
'whatsapp_sms' => [
'body' => $smsBody,
'whatsapp_sent_count' => $sentWhatsApp,
'sms_sent_count' => $sentSms
],
'total_dispatched' => count($dispatchedPassengers),
'debug_info' => [
'requested_country' => $countryCode,
'total_passengers_in_db' => count($allPassengers),
'matched_targets' => count($targets),
'distribution' => $debugCounts,
'fcm_errors' => $fcmErrors ?? []
]
]);
} catch (Exception $e) {
error_log("[trigger_campaign.php] Error: " . $e->getMessage());
jsonError("Failed to trigger marketing campaign: " . $e->getMessage());
}
@@ -0,0 +1,110 @@
<?php
/**
* what_if_simulator.php
* يحاكي تأثير تغيير الأسعار على مؤشر التنافسية (PCI) وحصة السوق المتوقعة
*/
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');
$proposedSpeedPrice = (float)filterRequest('speed_price');
if (!$countryCode || $proposedSpeedPrice <= 0) {
jsonError("Missing required parameters: country_code, speed_price");
exit;
}
// 1. Fetch recent competitor trips (last 7 days, limit 500 for fast simulation)
$sql = "SELECT (price_amount / price_per_km) AS distance_km, price_amount AS total_price, competitor_name
FROM scraped_competitor_prices
WHERE country_code = :country
AND price_per_km > 0
AND created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)
ORDER BY created_at DESC
LIMIT 500";
$stmt = $con->prepare($sql);
$stmt->execute([':country' => strtoupper($countryCode)]);
$trips = $stmt->fetchAll(PDO::FETCH_ASSOC);
if (empty($trips)) {
jsonError("No competitor data available for simulation in this country.");
exit;
}
// 2. Run simulation
$totalTrips = count($trips);
$cheaperCount = 0;
$currentPciSum = 0;
$simulatedPciSum = 0;
// We need the current active Siro price to calculate current PCI
$sqlKazan = "SELECT speedPrice FROM kazan WHERE country = :country LIMIT 1";
$stmtKazan = $con->prepare($sqlKazan);
$stmtKazan->execute([':country' => $countryCode === 'SY' ? 'Syria' : ($countryCode === 'JO' ? 'Jordan' : 'Egypt')]);
$kazanRow = $stmtKazan->fetch(PDO::FETCH_ASSOC);
$currentSpeedPrice = $kazanRow ? (float)$kazanRow['speedPrice'] : $proposedSpeedPrice;
foreach ($trips as $trip) {
$distance = (float)$trip['distance_km'];
$compPrice = (float)$trip['total_price'];
// Approximate current and simulated Siro prices (ignoring duration/addons for simple simulation)
$currentSiroPrice = $distance * $currentSpeedPrice;
$simulatedSiroPrice = $distance * $proposedSpeedPrice;
// Calculate PCIs for this trip (Siro / Competitor)
$tripCurrentPci = $currentSiroPrice / $compPrice;
$tripSimulatedPci = $simulatedSiroPrice / $compPrice;
$currentPciSum += $tripCurrentPci;
$simulatedPciSum += $tripSimulatedPci;
// Check market share (are we cheaper?)
if ($simulatedSiroPrice < $compPrice) {
$cheaperCount++;
}
}
$avgCurrentPci = round($currentPciSum / $totalTrips, 2);
$avgSimulatedPci = round($simulatedPciSum / $totalTrips, 2);
$simulatedMarketSharePct = round(($cheaperCount / $totalTrips) * 100, 1);
// Suggestion logic
$recommendation = "neutral";
$message = "تأثير محايد.";
if ($avgSimulatedPci > 1.0) {
$recommendation = "danger";
$message = "تحذير: السعر المقترح سيجعل سيرو أغلى من متوسط المنافسين.";
} elseif ($avgSimulatedPci < 0.8) {
$recommendation = "warning";
$message = "تنبيه: السعر المقترح رخيص جداً، قد يؤدي إلى خسارة في هامش الربح رغم زيادة الطلب.";
} elseif ($avgSimulatedPci >= 0.9 && $avgSimulatedPci <= 0.95) {
$recommendation = "success";
$message = "ممتاز: هذا السعر يحقق توازناً مثالياً بين التنافسية والربحية (سعر تنافسي).";
}
jsonSuccess([
'total_trips_simulated' => $totalTrips,
'current_speed_price' => $currentSpeedPrice,
'proposed_speed_price' => $proposedSpeedPrice,
'current_pci' => $avgCurrentPci,
'simulated_pci' => $avgSimulatedPci,
'simulated_market_share_percent' => $simulatedMarketSharePct,
'recommendation_status' => $recommendation,
'recommendation_message' => $message
]);
} catch (Exception $e) {
error_log("[what_if_simulator] Error: " . $e->getMessage());
jsonError("Simulation failed");
}
@@ -0,0 +1,90 @@
<?php
/**
* winback_hotspot_targets.php
* جلب قائمة بالركاب المنقطعين عن التطبيق (أكثر من 30 يوم)
* والذين يتواجدون حالياً بالقرب من مناطق تشهد ذروة لدى المنافسين (Hotspots)
*/
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');
if (!$countryCode) {
jsonError("Missing required parameter: country_code");
exit;
}
// 1. Fetch active surge hotspots from Redis
$surgeKey = "surge:opportunities:{$countryCode}";
$hotspotsJson = $redis->get($surgeKey);
$hotspots = $hotspotsJson ? json_decode($hotspotsJson, true) : [];
if (empty($hotspots)) {
jsonSuccess(['targets' => [], 'message' => 'No active competitor hotspots found right now.']);
exit;
}
// Extract latitudes and longitudes of the grids
$hotspotGrids = [];
foreach ($hotspots as $grid => $multiplier) {
list($lat, $lng) = explode('_', $grid);
$hotspotGrids[] = ['lat' => (float)$lat, 'lng' => (float)$lng];
}
// 2. Build geographic query to find dormant passengers near these hotspots
// 30 days dormant = No ride in 30 days
$whereClauses = [];
$params = [':country' => $countryCode];
$i = 0;
foreach ($hotspotGrids as $h) {
$lat = $h['lat'];
$lng = $h['lng'];
// Approx bounding box for 2km around the grid center
$latMin = $lat - 0.018;
$latMax = $lat + 0.018;
$lngMin = $lng - 0.018;
$lngMax = $lng + 0.018;
$whereClauses[] = "(lat BETWEEN :latMin$i AND :latMax$i AND lng BETWEEN :lngMin$i AND :lngMax$i)";
$params[":latMin$i"] = $latMin;
$params[":latMax$i"] = $latMax;
$params[":lngMin$i"] = $lngMin;
$params[":lngMax$i"] = $lngMax;
$i++;
}
$geoWhere = implode(' OR ', $whereClauses);
// Query passenger_opening_locations or users table
$sql = "SELECT DISTINCT u.users_id, u.users_name, u.users_phone, p.lat, p.lng
FROM users u
JOIN passenger_opening_locations p ON u.users_id = p.passenger_id
WHERE u.country_code = :country
AND u.users_type = 1
AND u.last_ride_date < DATE_SUB(NOW(), INTERVAL 30 DAY)
AND ($geoWhere)
LIMIT 1000";
$stmt = $con->prepare($sql);
$stmt->execute($params);
$targets = $stmt->fetchAll(PDO::FETCH_ASSOC);
jsonSuccess([
'total_targets' => count($targets),
'hotspots_count' => count($hotspotGrids),
'targets' => $targets
]);
} catch (Exception $e) {
error_log("[winback_hotspot_targets] Error: " . $e->getMessage());
jsonError("Failed to fetch targets");
}