diff --git a/backend/Admin/marketing/ai_price_prediction.php b/backend/Admin/marketing/ai_price_prediction.php
new file mode 100644
index 0000000..8545d3a
--- /dev/null
+++ b/backend/Admin/marketing/ai_price_prediction.php
@@ -0,0 +1,64 @@
+ 'failure', 'message' => 'Unauthorized']);
+ exit;
+}
+
+try {
+ $countryCode = resolveAdminCountry(filterRequest('country_code'), $role, $admin_country ?? null);
+
+ 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");
+}
diff --git a/backend/Admin/marketing/get_market_share_analytics.php b/backend/Admin/marketing/get_market_share_analytics.php
new file mode 100644
index 0000000..8b09adb
--- /dev/null
+++ b/backend/Admin/marketing/get_market_share_analytics.php
@@ -0,0 +1,55 @@
+ 'failure', 'message' => 'Unauthorized']);
+ exit;
+}
+
+try {
+ $countryCode = resolveAdminCountry(filterRequest('country_code'), $role, $admin_country ?? null);
+
+ 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");
+}
diff --git a/backend/Admin/marketing/get_price_gap_heatmap.php b/backend/Admin/marketing/get_price_gap_heatmap.php
new file mode 100644
index 0000000..822ca78
--- /dev/null
+++ b/backend/Admin/marketing/get_price_gap_heatmap.php
@@ -0,0 +1,87 @@
+ 'failure', 'message' => 'Unauthorized']);
+ exit;
+}
+
+try {
+ $countryCode = resolveAdminCountry(filterRequest('country_code'), $role, $admin_country ?? null);
+
+ 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(from_latitude * 74, 0) / 74 AS lat_group,
+ ROUND(from_longitude * 74, 0) / 74 AS lng_group,
+ AVG(price_per_km) as avg_competitor_price_per_km,
+ COUNT(*) as trip_count
+ FROM competitor_prices
+ WHERE country_code = :country
+ AND distance_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");
+}
diff --git a/backend/Admin/marketing/surge_opportunity_index.php b/backend/Admin/marketing/surge_opportunity_index.php
new file mode 100644
index 0000000..733e4e7
--- /dev/null
+++ b/backend/Admin/marketing/surge_opportunity_index.php
@@ -0,0 +1,155 @@
+ 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");
+}
diff --git a/backend/Admin/marketing/what_if_simulator.php b/backend/Admin/marketing/what_if_simulator.php
new file mode 100644
index 0000000..ba680dc
--- /dev/null
+++ b/backend/Admin/marketing/what_if_simulator.php
@@ -0,0 +1,110 @@
+ 'failure', 'message' => 'Unauthorized']);
+ exit;
+}
+
+try {
+ $countryCode = resolveAdminCountry(filterRequest('country_code'), $role, $admin_country ?? null);
+ $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 distance_km, total_price, competitor_name
+ FROM competitor_prices
+ WHERE country_code = :country
+ AND distance_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");
+}
diff --git a/backend/Admin/marketing/winback_hotspot_targets.php b/backend/Admin/marketing/winback_hotspot_targets.php
new file mode 100644
index 0000000..5cda153
--- /dev/null
+++ b/backend/Admin/marketing/winback_hotspot_targets.php
@@ -0,0 +1,90 @@
+ 'failure', 'message' => 'Unauthorized']);
+ exit;
+}
+
+try {
+ $countryCode = resolveAdminCountry(filterRequest('country_code'), $role, $admin_country ?? null);
+
+ 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");
+}
diff --git a/backend/bot/cron_kazan_adjuster.php b/backend/bot/cron_kazan_adjuster.php
new file mode 100644
index 0000000..b8c74ef
--- /dev/null
+++ b/backend/bot/cron_kazan_adjuster.php
@@ -0,0 +1,59 @@
+getMessage() . "\n");
+}
+
+$countries = ['SY', 'JO', 'EG', 'IQ'];
+
+foreach ($countries as $country) {
+ $surgeKey = "surge:opportunities:{$country}";
+ $hotspotsJson = $redis->get($surgeKey);
+ $hotspots = $hotspotsJson ? json_decode($hotspotsJson, true) : [];
+
+ // In a real scenario, Kazan configs might be stored in a `kazan_commissions` table
+ // or applied dynamically at the ride request time.
+ // Here we will save the "active discounted Kazan multiplier" in Redis so the ride logic can apply it instantly.
+
+ $kazanDiscountConfig = [];
+
+ if (!empty($hotspots)) {
+ foreach ($hotspots as $grid => $multiplier) {
+ // If competitor surge is high enough, we lower our Kazan by a factor
+ // Standard Kazan is let's say 15%.
+ if ($multiplier > 1.2) {
+ // Apply a 30% reduction to Kazan Commission for this grid
+ $kazanDiscountConfig[$grid] = 0.70; // 70% of standard commission
+ } elseif ($multiplier > 1.05) {
+ // Apply a 15% reduction
+ $kazanDiscountConfig[$grid] = 0.85;
+ }
+ }
+ }
+
+ $kazanKey = "surge:kazan_discounts:{$country}";
+
+ if (empty($kazanDiscountConfig)) {
+ $redis->del($kazanKey);
+ echo "[$country] No active Kazan discounts.\n";
+ } else {
+ $redis->setex($kazanKey, 1200, json_encode($kazanDiscountConfig));
+ echo "[$country] Updated Kazan discounts for " . count($kazanDiscountConfig) . " active hotspots.\n";
+ }
+}
+
+echo "Done.\n";
diff --git a/backend/bot/cron_seasonal_pricing.php b/backend/bot/cron_seasonal_pricing.php
new file mode 100644
index 0000000..c8ea63a
--- /dev/null
+++ b/backend/bot/cron_seasonal_pricing.php
@@ -0,0 +1,76 @@
+getMessage() . "\n");
+}
+
+$countries = ['SY' => 'Asia/Damascus', 'JO' => 'Asia/Amman', 'EG' => 'Africa/Cairo', 'IQ' => 'Asia/Baghdad'];
+
+// Simulated Calendar of Events
+$seasons = [
+ 'ramadan_iftar' => [
+ 'is_active' => true,
+ 'start_hour' => 18,
+ 'end_hour' => 20,
+ 'modifier' => 1.25 // 25% surge during Iftar
+ ],
+ 'eid' => [
+ 'is_active' => false,
+ 'start_hour' => 0,
+ 'end_hour' => 24,
+ 'modifier' => 1.15
+ ],
+ 'severe_weather' => [
+ 'is_active' => false, // Can be toggled manually or via Weather API
+ 'start_hour' => 0,
+ 'end_hour' => 24,
+ 'modifier' => 1.30
+ ]
+];
+
+foreach ($countries as $code => $timezone) {
+ date_default_timezone_set($timezone);
+ $currentHour = (int)date('H');
+
+ $activeMultiplier = 1.0;
+ $activeReason = null;
+
+ foreach ($seasons as $seasonName => $rules) {
+ if ($rules['is_active'] && $currentHour >= $rules['start_hour'] && $currentHour < $rules['end_hour']) {
+ $activeMultiplier = max($activeMultiplier, $rules['modifier']);
+ $activeReason = $seasonName;
+ }
+ }
+
+ $seasonalKey = "surge:seasonal:{$code}";
+
+ if ($activeMultiplier > 1.0) {
+ $data = [
+ 'multiplier' => $activeMultiplier,
+ 'reason' => $activeReason,
+ 'timestamp' => date('Y-m-d H:i:s')
+ ];
+ $redis->setex($seasonalKey, 3600, json_encode($data)); // Expires in 1 hour
+ echo "[$code] Applied Seasonal Pricing: $activeMultiplier x ($activeReason)\n";
+ } else {
+ $redis->del($seasonalKey);
+ echo "[$code] No active seasonal pricing.\n";
+ }
+
+ // --- Point 16: ETA Benchmarking Placeholder ---
+ // If we had a cron that pulled Google Distance Matrix ETAs vs Competitor ETAs, we'd log it here.
+ // echo "[$code] ETA Benchmark recorded.\n";
+}
+
+echo "Done.\n";
diff --git a/backend/bot/cron_surge_opportunity.php b/backend/bot/cron_surge_opportunity.php
new file mode 100644
index 0000000..335eb3b
--- /dev/null
+++ b/backend/bot/cron_surge_opportunity.php
@@ -0,0 +1,144 @@
+getMessage() . "\n");
+}
+
+echo "[".date('Y-m-d H:i:s')."] Starting cron_surge_opportunity...\n";
+
+try {
+ // 1. حساب الـ baseline و current
+ $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
+ 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);
+ $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']) {
+ $avgRatio = 0;
+ foreach ($zone['competitors'] as $c) {
+ $avgRatio += $c['surge_ratio'];
+ }
+ $avgRatio /= count($zone['competitors']);
+
+ $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'],
+ 'avg_competitor_surge_ratio' => round($avgRatio, 2),
+ 'suggested_siro_multiplier' => $suggestedMultiplier,
+ ];
+
+ // حفظ المنطقة مع المضاعف المقترح في Redis (للقراءة من get.php بعدين)
+ $gridSurgeZones[$key] = $suggestedMultiplier;
+ }
+ }
+ unset($zone);
+
+ // 4. تخزين فرص الذروة في Redis بصلاحية 10 دقائق (600 ثانية)
+ if (!empty($gridSurgeZones) && isset($redis) && $redis !== null) {
+ $redisKey = 'surge:opportunities';
+ $redis->setex($redisKey, 600, json_encode($gridSurgeZones));
+ echo "[".date('Y-m-d H:i:s')."] Successfully stored ".count($gridSurgeZones)." surge zones in Redis.\n";
+ } else {
+ if (!isset($redis) || $redis === null) {
+ echo "[".date('Y-m-d H:i:s')."] Error: Redis connection not available.\n";
+ } else {
+ // مسح الكي في حال لم يعد هناك أي ذروة
+ $redis->del('surge:opportunities');
+ echo "[".date('Y-m-d H:i:s')."] No active surge opportunities found. Cleared Redis key.\n";
+ }
+ }
+
+ echo "[".date('Y-m-d H:i:s')."] cron_surge_opportunity completed successfully.\n";
+
+} catch (Exception $e) {
+ echo "[".date('Y-m-d H:i:s')."] Error: " . $e->getMessage() . "\n";
+}
diff --git a/backend/bot/cron_weekly_health_report.php b/backend/bot/cron_weekly_health_report.php
new file mode 100644
index 0000000..0befb9f
--- /dev/null
+++ b/backend/bot/cron_weekly_health_report.php
@@ -0,0 +1,111 @@
+getMessage() . "\n");
+}
+
+echo "[".date('Y-m-d H:i:s')."] Starting Weekly Market Health Report...\n";
+
+try {
+ $countries = ['SY', 'JO', 'EG', 'IQ']; // Countries we operate in or monitor
+
+ foreach ($countries as $countryCode) {
+
+ // 1. Calculate Average PCI and Market Share
+ $sql = "SELECT distance_km, total_price
+ FROM competitor_prices
+ WHERE country_code = :country
+ AND distance_km > 0
+ AND created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)";
+ $stmt = $con->prepare($sql);
+ $stmt->execute([':country' => $countryCode]);
+ $trips = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+ if (empty($trips)) {
+ continue; // Skip if no data for this country
+ }
+
+ $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[$countryCode] ?? 'Syria']);
+ $kazanRow = $stmtKazan->fetch(PDO::FETCH_ASSOC);
+ $currentSpeedPrice = $kazanRow ? (float)$kazanRow['speedPrice'] : 0;
+
+ $pciSum = 0;
+ $cheaperCount = 0;
+ $totalTrips = count($trips);
+
+ foreach ($trips as $trip) {
+ $compPrice = (float)$trip['total_price'];
+ $siroPrice = (float)$trip['distance_km'] * $currentSpeedPrice;
+
+ if ($compPrice > 0) {
+ $pciSum += ($siroPrice / $compPrice);
+ if ($siroPrice < $compPrice) {
+ $cheaperCount++;
+ }
+ }
+ }
+
+ $averagePci = round($pciSum / $totalTrips, 2);
+ $marketSharePct = round(($cheaperCount / $totalTrips) * 100, 2);
+
+ // 2. Count Anomalies (Last 7 Days)
+ $sqlAnomalies = "SELECT COUNT(*) as count FROM price_anomalies
+ WHERE country_code = :country
+ AND created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)";
+ $stmtAnomalies = $con->prepare($sqlAnomalies);
+ $stmtAnomalies->execute([':country' => $countryCode]);
+ $anomaliesCount = (int)$stmtAnomalies->fetchColumn();
+
+ // 3. Automated Campaigns / Surge Tracking (If applicable in logs)
+ $sqlCampaigns = "SELECT COUNT(*) as count FROM marketing_campaigns_log
+ WHERE JSON_EXTRACT(target_criteria, '$.country_code') = :country
+ AND sent_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)";
+ $stmtCampaigns = $con->prepare($sqlCampaigns);
+ $stmtCampaigns->execute([':country' => $countryCode]);
+ $campaignsCount = (int)$stmtCampaigns->fetchColumn();
+
+ // 4. Save to Database
+ $reportData = [
+ 'total_competitor_samples' => $totalTrips,
+ 'automated_campaigns_sent' => $campaignsCount,
+ 'current_speed_price' => $currentSpeedPrice
+ ];
+
+ $sqlInsert = "INSERT INTO market_health_reports
+ (report_date, country_code, average_pci, market_share_percent, total_anomalies, total_surge_opportunities, report_data_json)
+ VALUES (CURDATE(), :country, :pci, :market_share, :anomalies, :surge, :report_data)";
+ $stmtInsert = $con->prepare($sqlInsert);
+ $stmtInsert->execute([
+ ':country' => $countryCode,
+ ':pci' => $averagePci,
+ ':market_share' => $marketSharePct,
+ ':anomalies' => $anomaliesCount,
+ ':surge' => 0, // Placeholder for actual surge count if tracked in DB
+ ':report_data' => json_encode($reportData)
+ ]);
+
+ echo "[".date('Y-m-d H:i:s')."] Report generated for $countryCode. PCI: $averagePci, Share: $marketSharePct%\n";
+ }
+
+ echo "[".date('Y-m-d H:i:s')."] cron_weekly_health_report completed successfully.\n";
+
+} catch (Exception $e) {
+ echo "[".date('Y-m-d H:i:s')."] Error: " . $e->getMessage() . "\n";
+}
diff --git a/backend/ride/heatmap/get_surge_heatmap.php b/backend/ride/heatmap/get_surge_heatmap.php
new file mode 100644
index 0000000..dad9f26
--- /dev/null
+++ b/backend/ride/heatmap/get_surge_heatmap.php
@@ -0,0 +1,52 @@
+ 'failure', 'message' => 'Unauthorized']);
+ exit;
+}
+
+try {
+ $countryCode = filterRequest('country_code');
+ if (!$countryCode) {
+ $countryCode = 'SY'; // Fallback
+ }
+
+ $surgeKey = "surge:opportunities:{$countryCode}";
+ $hotspotsJson = $redis->get($surgeKey);
+ $hotspots = $hotspotsJson ? json_decode($hotspotsJson, true) : [];
+
+ $driverHeatmap = [];
+
+ if (!empty($hotspots)) {
+ foreach ($hotspots as $grid => $multiplier) {
+ // Only show grids with a meaningful multiplier > 1.05
+ if ($multiplier > 1.05) {
+ list($lat, $lng) = explode('_', $grid);
+ $driverHeatmap[] = [
+ 'lat' => (float)$lat,
+ 'lng' => (float)$lng,
+ 'surge_multiplier' => (float)$multiplier,
+ 'radius_meters' => 1500 // 1.5km grid box
+ ];
+ }
+ }
+ }
+
+ jsonSuccess([
+ 'status' => 'success',
+ 'message' => 'Surge heatmap data fetched',
+ 'hotspots' => $driverHeatmap
+ ]);
+
+} catch (Exception $e) {
+ error_log("[get_surge_heatmap] Error: " . $e->getMessage());
+ jsonError("Failed to fetch heatmap");
+}
diff --git a/backend/ride/pricing/auto_adapt.php b/backend/ride/pricing/auto_adapt.php
new file mode 100644
index 0000000..2c62622
--- /dev/null
+++ b/backend/ride/pricing/auto_adapt.php
@@ -0,0 +1,122 @@
+getMessage());
+ exit(1);
+}
+
+$countries = [
+ 'Syria' => 'SY',
+ 'Jordan' => 'JO',
+ 'Egypt' => 'EG',
+ 'Iraq' => 'IQ'
+];
+
+$undercutFactor = 0.92;
+$minFloorRatio = 0.85;
+$maxCeilRatio = 1.15;
+
+$kmColumns = [
+ 'speedPrice', 'comfortPrice', 'ladyPrice', 'electricPrice',
+ 'vanPrice', 'deliveryPrice', 'mishwarVipPrice', 'fixedPrice', 'awfarPrice'
+];
+
+$logEntries = [];
+
+foreach ($countries as $country => $cc) {
+ // 1. متوسط سعر الكيلو لكل منافس (آخر 24 ساعة)
+ $sql = "SELECT competitor_name, AVG(price_per_km) AS avg_ppm
+ FROM competitor_prices
+ WHERE country_code = :cc
+ AND created_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)
+ AND price_per_km > 0
+ GROUP BY competitor_name
+ ORDER BY avg_ppm ASC";
+ $stmt = $con->prepare($sql);
+ $stmt->execute([':cc' => $cc]);
+ $competitorAvgs = $stmt->fetchAll(PDO::FETCH_ASSOC);
+
+ if (empty($competitorAvgs)) {
+ error_log("[AutoAdapt] $country — لا توجد بيانات منافسين");
+ continue;
+ }
+
+ $lowestAvg = (float)$competitorAvgs[0]['avg_ppm'];
+
+ // 2. الأسعار الحالية من kazan
+ $kazanSql = "SELECT * FROM kazan WHERE country = :country LIMIT 1";
+ $stmtK = $con->prepare($kazanSql);
+ $stmtK->execute([':country' => $country]);
+ $kazanRow = $stmtK->fetch(PDO::FETCH_ASSOC);
+
+ if (!$kazanRow) {
+ error_log("[AutoAdapt] $country — لا يوجد صف في kazan");
+ continue;
+ }
+
+ $currentSpeed = (float)$kazanRow['speedPrice'];
+ $targetSpeed = $lowestAvg * $undercutFactor;
+
+ $minAllowed = $currentSpeed * $minFloorRatio;
+ $maxAllowed = $currentSpeed * $maxCeilRatio;
+
+ if ($targetSpeed < $minAllowed) {
+ $targetSpeed = $minAllowed;
+ } elseif ($targetSpeed > $maxAllowed) {
+ $targetSpeed = $maxAllowed;
+ }
+
+ $ratio = $targetSpeed / $currentSpeed;
+
+ $updates = [];
+ $params = [':country' => $country];
+
+ foreach ($kmColumns as $col) {
+ $oldVal = (float)($kazanRow[$col] ?? 0);
+ if ($oldVal > 0) {
+ $newVal = round($oldVal * $ratio, 2);
+ $updates[] = "`$col` = :$col";
+ $params[":$col"] = (string)$newVal;
+ }
+ }
+
+ if (empty($updates)) {
+ continue;
+ }
+
+ $updateSql = "UPDATE kazan SET " . implode(', ', $updates) . " WHERE country = :country";
+ $stmtU = $con->prepare($updateSql);
+ $stmtU->execute($params);
+
+ $logEntries[] = [
+ 'country' => $country,
+ 'code' => $cc,
+ 'old_speed' => $currentSpeed,
+ 'new_speed' => $targetSpeed,
+ 'lowest_competitor' => $lowestAvg,
+ 'ratio' => round($ratio, 4),
+ ];
+
+ error_log("[AutoAdapt] ✅ $country ($cc): speedPrice $currentSpeed → $targetSpeed (ratio: $ratio)");
+}
+
+if (php_sapi_name() === 'cli') {
+ echo json_encode(['status' => 'success', 'updates' => $logEntries], JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE);
+} else {
+ echo json_encode(['status' => 'success', 'updates' => $logEntries]);
+}
diff --git a/backend/ride/pricing/get.php b/backend/ride/pricing/get.php
index 8c2c527..98f38bb 100644
--- a/backend/ride/pricing/get.php
+++ b/backend/ride/pricing/get.php
@@ -124,6 +124,16 @@ function calculateDynamicPrice($country, $minFare, $distance, $duration, $kazanR
$demandCount = (int)$redis->get("demand:grid:" . $grid_id);
$availableDrivers = 0;
+ // Check competitor surge opportunities
+ $competitorSurgeMultiplier = 1.0;
+ $surgeOpsJson = $redis->get("surge:opportunities");
+ if ($surgeOpsJson) {
+ $surgeOps = json_decode($surgeOpsJson, true);
+ if (is_array($surgeOps) && isset($surgeOps[$grid_id])) {
+ $competitorSurgeMultiplier = (float)$surgeOps[$grid_id];
+ }
+ }
+
// Driver locations are handled by Location Redis (no prefix)
try {
if (isset($redisLocation) && $redisLocation !== null) {
@@ -139,6 +149,11 @@ 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;
+ }
} catch (Exception $e) {}
}
diff --git a/backend/schema_primary.sql b/backend/schema_primary.sql
index 69c4f0f..7fba399 100644
--- a/backend/schema_primary.sql
+++ b/backend/schema_primary.sql
@@ -1925,3 +1925,20 @@ CREATE TABLE IF NOT EXISTS `driver_destinations` (
PRIMARY KEY (`id`),
KEY `idx_driver_date` (`driver_id`, `usage_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+
+--
+-- Table structure for table `market_health_reports`
+--
+CREATE TABLE IF NOT EXISTS `market_health_reports` (
+ `id` int NOT NULL AUTO_INCREMENT,
+ `report_date` date NOT NULL,
+ `country_code` varchar(5) NOT NULL,
+ `average_pci` decimal(5,2) NOT NULL,
+ `market_share_percent` decimal(5,2) NOT NULL,
+ `total_anomalies` int NOT NULL DEFAULT 0,
+ `total_surge_opportunities` int NOT NULL DEFAULT 0,
+ `report_data_json` json DEFAULT NULL,
+ `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (`id`),
+ KEY `idx_country_date` (`country_code`, `report_date`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
diff --git a/knowledge/full_system_simulation_ar.html b/knowledge/full_system_simulation_ar.html
new file mode 100644
index 0000000..91675af
--- /dev/null
+++ b/knowledge/full_system_simulation_ar.html
@@ -0,0 +1,1055 @@
+
+
+
+
+
+محاكاة نظام Siro الكاملة - اليوم 2026-06-21
+
+
+
+
+
+
🚀 محاكاة نظام Siro الكاملة
+
جميع الإضافات والتغييرات التي تم العمل عليها بتاريخ 2026-06-21
+
+
+
+
+
+
+
2
+
لغات (Dart/Kotlin)
+
+
+
+
+
+
+
+
+ ⏰
+ المرحلة الأولى: Cron Job يولد مهام تسعير المنافسين
+ backend/bot/generate_price_tasks.php
+
+
+ كل 15 دقيقة
+ |
+ المناطق: 10 مناطق في دمشق
+ |
+ المنافسون: YallaGo • Zaken • Tfadal
+
+
+
+
1
+
+
الاتصال بقاعدة البيانات و Redis
+
يتم تحميل boostrap.php لإنشاء اتصال MySQL و Redis
+
+
+
+
+
2
+
+
إنشاء جدول competitor_prices إن لم يكن موجوداً
+
+CREATE TABLE IF NOT EXISTS competitor_prices (
+ id INT AUTO_INCREMENT PRIMARY KEY,
+ competitor_name VARCHAR(50),
+ from_latitude VARCHAR(30), from_longitude VARCHAR(30),
+ to_latitude VARCHAR(30), to_longitude VARCHAR(30),
+ distance_km DECIMAL(8,2), total_price DECIMAL(10,2),
+ price_per_km DECIMAL(8,2), country_code VARCHAR(5) DEFAULT 'SY',
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
+);
+
+
+
+
+
+
3
+
+
توليد نقاط عشوائية لـ 10 مناطق رئيسية في دمشق
+
+ ساحة الأمويين
+ المزة
+ المالكي
+ كفرسوسة
+ الميدان
+ باب توما
+ ركن الدين
+ دمر
+ برامكة
+ المهاجرين
+
+
لكل منطقة: نقطة بداية عشوائية ضمن 2 كم + رحلة قصيرة (2-5 كم) + رحلة طويلة (10-15 كم)
+
+
+
+
+
4
+
+
إدراج المهام في Redis Queue
+
استخدام LPUSH إلى queue:bot:tasks
+
+// 10 مناطق × رحلتين × 3 منافسين = 60 مهمة كل 15 دقيقة
+$redis->lpush('queue:bot:tasks', json_encode($taskData));
+echo "Successfully generated and queued $tasksCreated pricing tasks.";
+
+
+
+
+
+
+
+
+
+
+ 🤖
+ المرحلة الثانية: Android Bot يسحب المهام وينفذها
+ android_bot/
+
+
+
+ آلية السحب: Polling كل 15 ثانية
+ |
+ الأجهزة المسجلة: SHAM_CASH_BOT_01 • PRICE_SCRAPER_BOT_01
+
+
+
+
+
+
1
+
+
WorkerClient.kt — التوقيع HMAC-SHA256
+
+fun generateSignature(deviceId: String, ts: Long): String {
+ val message = "$deviceId$ts"
+ val algorithm = "HmacSHA256"
+ val mac = Mac.getInstance(algorithm)
+ val secretKeySpec = SecretKeySpec(SECRET_KEY.toByteArray(), algorithm)
+ mac.init(secretKeySpec)
+ val hashBytes = mac.doFinal(message.toByteArray())
+ return hashBytes.joinToString("") { "%02x".format(it) }
+}
+
+
+
+
+
2
+
+
إرسال GET إلى standalone_worker.php
+
+GET /standalone_worker.php?device_id=ANDROID_ID&ts=1718970000&sig=abc123...
+→ Response (JSON):
+{
+ "status": "success",
+ "has_task": true,
+ "task": {
+ "task_id": "prc_667b8e2f",
+ "type": "price_check",
+ "app": "com.zakinn.app",
+ "payload": {
+ "start_lat": 33.5138, "start_lng": 36.2765,
+ "end_lat": 33.5350, "end_lng": 36.2950
+ }
+ }
+}
+
+
+
+
+
+
+
3
+
+
ScraperAccessibilityService.kt — نقر آلي
+
آلة حالة متكاملة لتطبيقات المنافسين:
+
+IDLE → LAUNCHING_APP → SEARCHING_START → SEARCHING_END → READING_PRICE → SUBMITTING
+
+
+ YallaGo البحث عن "Where to" أو "أين تريد الذهاب" والنقر عليه
+
+ Zakinn استخدام findAccessibilityNodeInfosByViewId للـ TextInput
+
+ Tfadal البحث بـ 7 لغات مختلفة (AR/EN/TR) عن حقول الإدخال
+
+
+
+
+
4
+
+
قراءة السعر وإرسال النتيجة
+
+// قراءة السعر من النصوص التي تحتوي على SYP, ل.س, AED, SP
+searchPriceByCurrency(node) → "ل.س 12,500"
+→ submitPriceToServer → POST /standalone_worker.php
+{
+ "device_id": "...", "ts": 1718970000, "sig": "...",
+ "task_id": "prc_667b8e2f", "type": "price_check",
+ "status": "success",
+ "result_data": {
+ "app": "com.zakinn.app",
+ "distance_km": 4.8,
+ "price": 12500,
+ "start_lat": 33.5138, "start_lng": 36.2765,
+ "end_lat": 33.5350, "end_lng": 36.2950
+ }
+}
+
+
+
+
+
+
+
+
+
+
+
+
+ ⚙️
+ المرحلة الثالثة: Worker يستقبل النتائج ويخزنها
+ backend/bot/worker.php
+
+
+
+
1
+
+
استقبال POST مع التحقق من HMAC-SHA256
+
// التحقق من التوقيع + منع Replay Attacks (صلاحية 5 دقائق)
+if (abs(time() - $ts) > 300) → رفض الطلب
+$expected_sig = hash_hmac('sha256', $device_id . $ts, $SECRET_KEY);
+hash_equals($expected_sig, $sig) → تحقق آمن من التوقيع
+
+
+
+
2
+
+
تخزين السعر في MySQL + Redis
+
+// MySQL: حفظ البيانات للتقارير اللاحقة
+$stmt = $con->prepare("INSERT INTO competitor_prices (...) VALUES (...?)");
+$stmt->execute([$app_name, $start_lat, $start_lng, $end_lat, $end_lng, $distance_km, $price, $pricePerKm, $country_code]);
+
+// Redis: حفظ آخر 50 سعر لكل تطبيق للتسعير الديناميكي
+$redis->lpush("competitor:price_history:$app_name", $pricePerKm);
+$redis->ltrim("competitor:price_history:$app_name", 0, 49);
+
+
+
+
+
+
+
+
+
+
+ 📊
+ المرحلة الرابعة: Standalone Worker Dashboard (واجهة تحكم)
+ standalone_worker.php
+
+
+
1
+
+
واجهة مدير متكاملة (HTML/CSS)
+
نظام ملفات JSON (tasks.json / results.json) — بدون حاجة لقاعدة بيانات
+
+ قائمة المهام المعلقة
+ سجل النتائج المنجزة
+ نموذج إضافة مهمة يدوية
+
+
+
+
+
2
+
+
عينة من البيانات المعروضة
+
+ | # | التطبيق | المسار | السعر | الحالة |
+ | 1 | YallaGo | المزة → مطار دمشق | 12,500 SYP | ✅ نجاح |
+ | 2 | Zakinn | ساحة الأمويين → دمر | 8,200 SYP | ✅ نجاح |
+ | 3 | Tfadal | المالكي → كفرسوسة | — | ❌ فشل |
+ | 4 | YallaGo | برامكة → المهاجرين | 15,000 SYP | ✅ نجاح |
+ | 5 | Zakinn | الميدان → باب توما | 6,750 SYP | ✅ نجاح |
+
+
+
+
+
+
+
+
+
+
+ 🧠
+ المرحلة الخامسة: نظام التسويق الذكي بالذكاء الاصطناعي (Gemini AI)
+ SiroGeminiService.php + trigger_campaign.php
+
+
+
+
1
+
+
المدير يضغط "إطلاق حملة استعادة ذكية فوراً"
+
من siro_admin → Marketing Page
+
+// Flutter Controller: marketing_controller.dart
+Future triggerAICampaign() async {
+ var res = await CRUD().post(link: AppLink.triggerCampaign, payload: params);
+ // → POST /Admin/marketing/trigger_campaign.php
+}
+
+
+
+
+
+
2
+
+
PHP يسحب آخر أسعار المنافسين ويرسلها إلى Gemini AI
+
+$sqlPrices = "SELECT competitor_name, total_price, distance_km
+ FROM competitor_prices WHERE country_code = :country
+ ORDER BY created_at DESC LIMIT 10";
+// البيانات تسحب من جدول competitor_prices الذي ملأه البوت!
+
+$geminiService = new SiroGeminiService();
+$aiCampaign = $geminiService->analyzeMarketAndDraftCampaign(
+ $competitorPrices, $siroBasePrice, $regionName, $countryCode
+);
+
+
+
+
+
+
3
+
+
Gemini AI يحلل ويكتب حملة تسويقية كاملة
+
+// SiroGeminiService.php — إرسال prompt إلى Gemini API
+$prompt = "
+أنت خبير تسويق ذكي لتطبيق Siro...
+1. أسعار المنافسين: " . json_encode($competitorPrices) . "
+2. سعر Siro الأساسي: 10000 SYP
+الخرج المطلوب JSON:
+{
+ \"opportunity_detected\": true,
+ \"recommended_price\": 8500,
+ \"discount_percentage\": 15,
+ \"promo_code\": \"SIRODM15\",
+ \"push_title\": \"🔥 وفر 15% على رحلتك!\",
+ \"push_body\": \"خصم خاص لسكان دمشق! استخدم كود SIRODM15\",
+ \"sms_body\": \"اشتقنا لك يا ${passenger_name}! عد إلينا ووفر 15% مع كود SIRODM15\"
+}"
+
+
+
+
+
+
4
+
+
إرسال الإشعارات والحملات الترويجية
+
+ FCM Push للمستخدمين النشطين (بدون حدود سبام)
+
+ WhatsApp للمستخدمين المنقطعين (مع anti-spam 24 ساعة)
+
+ SMS كخيار احتياطي إذا فشل WhatsApp
+
+
+// trigger_campaign.php
+foreach ($targets as $target) {
+ if ($fcmToken) {
+ sendFcmNotification($fcmToken, $pushTitle, $pushBody, $fcmData);
+ $sentFcm++;
+ } else {
+ if ($spamCount === 0) { // anti-spam
+ sendWhatsAppFromServer($decryptedPhone, $smsBody);
+ $sentWhatsApp++;
+ }
+ }
+}
+
+
+
+
+
+
5
+
+
تسجيل الحملة في سجل التدقيق
+
+// Audit Log
+logAudit($con, $user_id, 'trigger_marketing_campaign', 'promos', $promoCode, [
+ 'promo_code' => $promoCode,
+ 'targets_count' => count($dispatchedPassengers)
+]);
+
+// Response للمدير:
+{
+ "campaign_created": true,
+ "promo_code": "SIRODM15",
+ "push_notification": { "sent_count": 34 },
+ "whatsapp_sms": { "whatsapp_sent_count": 12, "sms_sent_count": 5 },
+ "total_dispatched": 51
+}
+
+
+
+
+
+
+
+
+
+
+ 💰
+ المرحلة السادسة: محرك التسعير الديناميكي يستخدم بيانات المنافسين
+ backend/ride/pricing/get.php
+
+
+
+
1
+
+
حساب السعر مع الـ Surge Multiplier من Redis
+
+// قراءة الطلب (demand) من Redis الرئيسي
+$demandCount = (int)$redis->get("demand:grid:" . $grid_id);
+
+// قراءة السائقين المتاحين من Redis الموقع
+$drivers = $redisLocation->georadius('geo:drivers:available', $lng, $lat, 0.75, 'km');
+
+if ($demandCount > 0 && $availableDrivers > 0) {
+ $surgeRatio = $demandCount / $availableDrivers;
+ if ($surgeRatio > 1.2) {
+ $surgeMultiplier = 1.0 + ($surgeRatio - 1.2) * 0.5; // Capped at 3.0
+ }
+}
+
+
+
+
+
+
2
+
+
تطبيق التسعير التنافسي (Competitor Undercut — أقل بـ 8%)
+
+// البحث عن أسعار منافسين لنفس المنطقة الجغرافية
+SELECT total_price, distance_km FROM competitor_prices
+WHERE country_code = 'SY'
+ AND (from_latitude + 0.0) BETWEEN :min_flat AND :max_flat
+ AND created_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)
+
+// إذا وجدنا تطابق → نخفض سعرنا ليكون أقل من المنافس بـ 8%
+if ($competitorTarget !== null) {
+ $undercutPrice = $competitorTarget * 0.92; // أقل بــ 8% 🎯
+ if ($price > $targetAdjustedPrice) {
+ $price = $targetAdjustedPrice; // تطبيق السعر التنافسي
+ }
+}
+
+
+
+
+
+
3
+
+
تطبيق الخصم (Promo Code) + الديون السابقة + التوقيع المشفر
+
+// التحقق من صلاحية الكود الترويجي
+$sqlPromo = "SELECT amount FROM promos WHERE promo_code = :code AND ...";
+$discount = (float) $promoData['amount'];
+
+// إضافة الديون السابقة
+$redisDebt = $redisInstance->get("passenger_debt_" . $passenger_id);
+$finalPrice += (float) $redisDebt;
+
+// توقيع السعر بشكل مشفر لمنع التلاعب
+$priceToken = $encryptionHelper->encryptData(json_encode([
+ 'passenger_id' => $passenger_id,
+ 'distance' => $distance, 'duration' => $duration,
+ 'expires' => time() + 420, // 7 دقائق فقط
+ 'prices' => $pricesRaw
+]));
+
+
+
+
+
+
+
+
+
+
+ 🔌
+ المرحلة السابعة: WebSocket — الاتصال المباشر مع السائقين
+ socket_intaleq/driver_socket.php
+
+
+
+ 🚀 المستوى الثاني (Level 2 Architecture)
+ |
+ بورت 2020 WebSocket
+ |
+ بورت 2021 HTTP Internal
+
+
+
+
1
+
+
اتصال السائق بالـ Socket
+
+// siro_driver → background_service.dart → Socket.IO
+socket = IO.io(AppLink.locationSocketUrl, OptionBuilder()
+ .setQuery({'driver_id': driverId, 'token': token}));
+
+// server side: driver_socket.php
+$socket->join('driver_' . $driverId);
+$connectedDrivers[$driverId] = ['conn' => $socket, 'platform' => $platform, 'token' => $token];
+
+
+
+
+
+
2
+
+
تحديث الموقع مع Redis Pipeline Buffering (Level 2)
+
+// السائق يرسل موقعه ← يتم تجميع الأحداث كل 500ms
+$socket->on('update_location', function($data) use (&$eventBuffer) {
+ // حساب التغيير الفعال (تجنب عمليات Redis غير الضرورية)
+ if (!$didMove && !$speedChanged && !$headingChanged && !$statusChanged) return;
+
+ // التخزين المؤقت → Redis Pipeline
+ $eventBuffer[$driverId] = [
+ 'hmset' => ['heading' => $heading, 'speed' => $speed, 'status' => $status],
+ 'geoadd' => ['status' => $status, 'lng' => $lng, 'lat' => $lat],
+ 'status_change' => ['old' => $oldStatus, 'new' => $newStatus]
+ ];
+});
+
+// Timer كل 500ms: تنفيذ Pipeline
+Timer::add(0.5, function() {
+ $pipe = $redis->pipeline();
+ foreach ($eventBuffer as $driverId => $ops) { /* تنفيذ مجمع */ }
+ $pipe->execute();
+ $eventBuffer = [];
+});
+
+
+ تحسين أداء بدلاً من 3 عمليات Redis لكل تحديث → عملية واحدة كل 500ms لكل السائقين
+
+
+
+
+
+
3
+
+
إرسال الطلبات الجديدة (Dispatch + Market)
+
+// HTTP Internal → dispatch_order
+$io->to('driver_' . $driverId)->emit('new_ride_request', $payload);
+if ($platform === 'ios') sendFCM_Async($token, 'طلب جديد', 'لديك رحلة جديدة');
+
+// Market New Ride
+$redis->geoadd('geo:rides:waiting', $lng, $lat, $rideId);
+$nearbyDrivers = $redis->georadius('geo:drivers:available', $lng, $lat, 50, 'km');
+foreach ($nearbyDrivers as $driverId) {
+ $io->to('driver_' . $driverId)->emit('market_new_ride', $payload);
+}
+
+
+
+
+
+
4
+
+
تحديث موقع السائق إلى الراكب (Forward to Passenger Socket)
+
+// Throttle: إرسال الموقع للراكب فقط إذا تحرك >15 متر أو مر >3 ثواني
+function forwardLocationToPassengerSocket(...) {
+ if ($dist < FORWARD_MIN_METERS && $timeDiff < FORWARD_MAX_SECONDS) return;
+
+ $http = new AsyncHttp();
+ $http->request($passengerSocketUrl, ['method' => 'POST', 'data' => $payload,
+ 'headers' => ['x-internal-key' => $internalKey]]);
+}
+
+
+
+
+
+
+
+
+
+
+ 📍
+ المرحلة الثامنة: تطبيق السائق — الوجهة المقترحة والخلفية
+ siro_driver/
+
+
+
+
+
+
1
+
+
DestinationController — إدارة الوجهة
+
+// حفظ الوجهة (حد أقصى مرتين في اليوم)
+Future saveDestination(LatLng position, String name) async {
+ final response = await CRUD().post(link: AppLink.saveDriverDestination, payload: {
+ 'action': 'set',
+ 'destination_lat': position.latitude.toString(),
+ 'destination_lng': position.longitude.toString(),
+ 'destination_name': name,
+ });
+ // تحقق من رسالة "الحد الأقصى لتعديل الوجهة"
+ if (msg.contains("الحد الأقصى") || msg.contains("limit")) {
+ mySnackbarWarning('You have reached the daily limit');
+ }
+}
+
+// جلب الوجهة الحالية + مسحها
+fetchActiveDestination() / clearDestination()
+
+
+
+
+
+
+
2
+
+
Background Service — استقبال الطلبات في الخلفية
+
+// Socket في الخلفية ← عند وصول new_ride_request
+socket.on('new_ride_request', (data) async {
+ if (isAppInForeground || overlayActive) return; // لا نكرر
+
+ // أندرويد: نعرض Overlay فوق أي تطبيق
+ await FlutterOverlayWindow.showOverlay(
+ overlayTitle: "طلب جديد 🚖",
+ overlayContent: "لديك طلب رحلة جديد!",
+ flag: OverlayFlag.focusPointer,
+ );
+
+ // iOS: نعرض Local Notification
+ flutterLocalNotificationsPlugin.show(1002, "طلب رحلة جديد 🚖", ...);
+});
+
+
+
+
+
+
+
+
+
+
+
+
+ 📱
+ المرحلة التاسعة: Admin App — لوحة التسويق الذكي
+ siro_admin/
+
+
+
+
1
+
+
MarketingController — التحكم بالأسواق والحملات
+
يدير كل من:
+ Anomalies
+ Campaigns Log
+ Telemetry
+ Price Comparison
+ What-If Simulator
+
+
+// دوال رئيسية:
+fetchAnomalies() // شواذ الأسعار
+fetchCampaignsLog() // سجل الحملات
+fetchTelemetry() // استهلاك API والتكاليف
+fetchPriceComparison() // مقارنة الأسعار + PCI
+fetchPriceGapHeatmap() // خارطة الفجوات السعرية
+fetchMarketShareAnalytics() // تطور الحصة السوقية
+fetchAiPricePrediction() // توقعات AI
+fetchWinbackTargets() // أهداف الاستعادة
+triggerAICampaign() // إطلاق حملة AI
+runWhatIfSimulation(price) // محاكي تغيير الأسعار
+
+
+
+
+
+
2
+
+
واجهة MarketingPage متكاملة بـ 3 تبويبات
+
+ التبويب 1: شواذ الأسعار والمنافسين
+ - بطاقة التحكم بالـ AI (Gemini)
+ - توقعات الذكاء الاصطناعي (Siro AI Prediction)
+ - محاكي تغيير الأسعار الذكي (What-If Simulator)
+ - رسم بياني لتطور الحصة السوقية (آخر 12 أسبوع)
+ - رسم بياني لمقارنة تقلبات الأسعار اللحظية
+ - خارطة الفجوات ومؤشر التنافسية السعري (PCI)
+ - أهداف استعادة العملاء (Win-Back)
+
+ التبويب 2: سجل الحملات المنجزة
+ - عرض الحملات المرسلة (Push / WhatsApp / SMS)
+
+ التبويب 3: إعدادات الأتمتة والتحكم
+ - مراقب استهلاك الرموز API Telemetry
+ - تفعيل/تعطيل الـ Autopilot
+ - تعديل System Prompt للذكاء الاصطناعي
+
+
+
+
+
+
3
+
+
What-If Simulator — محاكي السيناريوهات
+
+// مثلاً: المدير يقترح سعر 8500 لكل كم
+POST /Admin/marketing/what_if_simulator.php?speed_price=8500&country_code=SY
+→ Response:
+{
+ "simulated_pci": 0.89,
+ "market_share_percent": 72.5,
+ "recommendation_status": "success",
+ "recommendation_message": "✅ السعر المقترح 8500 SYP/كم سيجعلنا أرخص من جميع المنافسين!"
+}
+
+
+
+
+
+
+
+
+
+
+ 🗣️
+ المرحلة العاشرة: تطبيق الراكب — الملاحة والـ TTS
+ siro_rider/
+
+
+
+
1
+
+
NavigationView + NavigationController — الملاحة
+
واجهة ملاحة كاملة مع: معلومات المسار، وقت الوصول، المسافة المتبقية
+
+
+
+
2
+
+
TTS (Text-to-Speech) Controller
+
تحويل إرشادات الملاحة إلى صوت بالعربية — إعلانات صوتية للمنعطفات
+
+// siro_rider/lib/controller/functions/tts.dart
+class TextToSpeechController extends GetxController {
+ Future speakText(String text) async {
+ await flutterTts.setLanguage("ar-SA");
+ await flutterTts.setSpeechRate(0.5);
+ await flutterTts.speak(text);
+ }
+}
+
+
+
+
+
+
+
+
+
+
+ 🔁
+ مخطط تدفق البيانات الكامل — دورة متكاملة
+
+
+
+ ⏰ Cron Job (15 دقيقة)
+ ↓ يولد
+ 📋 60 مهمة تسعير → Redis Queue
+ ↓ يسحب
+ 🤖 Android Bot (Accessibility Service)
+ ↓ يفتح التطبيق وينقر آلياً ويقرأ
+ 📱 YallaGo • Zakinn • Tfadal
+ ↓ يرسل النتائج
+ 📊 Worker → MySQL (competitor_prices) + Redis
+ ↓ يستخدم في
+ 💰 محرك التسعير الديناميكي (Undercut 8%)
+ ↓ ويستخدم في
+ 🧠 Gemini AI Campaign Generator
+ ↓ يرسل
+ 📣 FCM Push + WhatsApp + SMS → الركاب
+ ↓ وأيضاً
+ 📊 Admin Dashboard يعرض التحليلات
+
+
+
+
+
+
+
+
+ 📁
+ هيكلية الملفات المضافة والمعدلة اليوم
+
+
+
+
📱 siro_driver/
+
+ ├── lib/constant/links.dart
+ ├── lib/controller/functions/background_service.dart
+ ├── lib/controller/functions/location_controller.dart
+ ├── lib/controller/home/captin/
+ │ ├── destination_controller.dart
+ │ ├── home_captain_controller.dart
+ │ └── order_request_controller.dart
+ ├── lib/controller/local/ar_eg.dart
+ ├── lib/controller/local/ar_jo.dart
+ ├── lib/controller/local/ar_sy.dart
+ ├── lib/controller/local/en.dart
+ ├── lib/views/home/Captin/home_captain/
+ │ ├── home_captin.dart
+ │ └── widget/destination_bottom_sheet.dart
+ └── lib/views/home/Captin/mapDriverWidgets/passenger_info_window.dart
+
+
+
+
⚙️ backend/
+
+ ├── bot/
+ │ ├── generate_price_tasks.php (Cron Job)
+ │ ├── worker.php (Redis Worker Endpoint)
+ │ └── standalone_worker.php (Dashboard)
+ ├── core/Services/SiroGeminiService.php
+ ├── Admin/marketing/
+ │ ├── trigger_campaign.php
+ │ ├── get_price_comparison.php
+ │ ├── get_market_anomalies.php
+ │ ├── get_telemetry.php
+ │ └── get_campaigns_log.php
+ ├── ride/pricing/get.php
+ ├── ride/location/save_driver_destination.php
+ └── ride/rides/add_ride.php, acceptRide.php, cancel*.php
+
+
🤖 android_bot/
+
+ ├── service/ScraperAccessibilityService.kt
+ ├── service/AppLauncher.kt
+ ├── network/WorkerClient.kt
+ └── MainActivity.kt
+
+
+
+
+
+
+
+
+
+
+ ✅
+ الخلاصة — ماذا أنجزنا اليوم؟
+
+
+ مكتمل Android Bot — نقر آلي ذكي (Accessibility Service) مع آلة حالة متكاملة لـ 3 تطبيقات منافسة
+ مكتمل Cron Job — توليد 60 مهمة تسعير كل 15 دقيقة لـ 10 مناطق في دمشق
+ مكتمل Worker Endpoint — استقبال نتائج البوت مع HMAC Authentication + MySQL/Redis
+ مكتمل Gemini AI — خدمة تحليل أسعار وصياغة حملات تسويقية بالعربية (لهجات محلية)
+ مكتمل Marketing Campaign — إطلاق حملات عبر FCM + WhatsApp + SMS مع Anti-Spam
+ مكتمل Pricing Engine — تسعير ديناميكي مع Undercut 8% عن المنافسين + Surge Pricing
+ مكتمل WebSocket Level 2 — Redis Pipeline Buffering كل 500ms + Forward للموقع
+ مكتمل Destination System — وجهة السائق مع حد يومي + خلفية مع Overlay للطلبات
+ مكتمل Admin Dashboard — لوحة تسويق ذكية مع PCI, Heatmap, What-If Simulator, Telemetry
+ مكتمل Standalone Dashboard — واجهة تحكم كاملة للبوت بدون حاجة Redis/MySQL
+
+
+
+
+
+
diff --git a/siro_admin/lib/constant/links.dart b/siro_admin/lib/constant/links.dart
index ce89d85..3a47b36 100644
--- a/siro_admin/lib/constant/links.dart
+++ b/siro_admin/lib/constant/links.dart
@@ -345,6 +345,11 @@ import 'box_name.dart';class AppLink {
static String getCampaignsLog = "$server/Admin/marketing/get_campaigns_log.php";
static String getTelemetry = "$server/Admin/marketing/get_telemetry.php";
static String getPriceComparison = "$server/Admin/marketing/get_price_comparison.php";
+ static String whatIfSimulator = "$server/Admin/marketing/what_if_simulator.php";
+ static String getPriceGapHeatmap = "$server/Admin/marketing/get_price_gap_heatmap.php";
+ static String winbackHotspotTargets = "$server/Admin/marketing/winback_hotspot_targets.php";
+ static String getMarketShareAnalytics = "$server/Admin/marketing/get_market_share_analytics.php";
+ static String aiPricePrediction = "$server/Admin/marketing/ai_price_prediction.php";
static String saveDriverDestination = "$server/ride/location/save_driver_destination.php";
static String paymentServerV2 = 'https://walletintaleq.intaleq.xyz/v2/main';
static String realtimeDashboardV2 = "$server/Admin/v2/realtime_dashboard.php";
diff --git a/siro_admin/lib/controller/admin/marketing_controller.dart b/siro_admin/lib/controller/admin/marketing_controller.dart
index f9fc92c..34ffd7e 100644
--- a/siro_admin/lib/controller/admin/marketing_controller.dart
+++ b/siro_admin/lib/controller/admin/marketing_controller.dart
@@ -28,6 +28,10 @@ class MarketingController extends GetxController {
fetchAnomalies();
fetchCampaignsLog();
fetchPriceComparison();
+ fetchPriceGapHeatmap();
+ fetchMarketShareAnalytics();
+ fetchAiPricePrediction();
+ fetchWinbackTargets();
}
// --- Autopilot Status Toggle ---
@@ -187,4 +191,158 @@ class MarketingController extends GetxController {
update();
}
}
+
+ // --- What-If Simulator State ---
+ double? simulatedPci;
+ double? simulatedMarketShare;
+ String? simulatorRecommendationStatus;
+ String? simulatorRecommendationMessage;
+
+ Future runWhatIfSimulation(String proposedSpeedPrice) async {
+ isLoading = true;
+ update();
+ try {
+ Map params = {
+ 'speed_price': proposedSpeedPrice
+ };
+ if (selectedCountry != 'All') {
+ params['country_code'] = selectedCountry;
+ } else {
+ params['country_code'] = 'SY'; // Fallback
+ }
+
+ var res = await CRUD().post(
+ link: AppLink.whatIfSimulator,
+ payload: params,
+ );
+ if (res is Map && res['status'] == 'success') {
+ final data = res['message'];
+ if (data is Map) {
+ simulatedPci = (data['simulated_pci'] ?? 0.0).toDouble();
+ simulatedMarketShare = (data['simulated_market_share_percent'] ?? 0.0).toDouble();
+ simulatorRecommendationStatus = data['recommendation_status'];
+ simulatorRecommendationMessage = data['recommendation_message'];
+ }
+ } else {
+ Get.snackbar("Simulation Error", res['message'] ?? "Failed to run simulation");
+ }
+ } catch (e) {
+ Get.snackbar("Error", "Network error during simulation");
+ } finally {
+ isLoading = false;
+ update();
+ }
+ }
+
+ // --- Heatmap State ---
+ List heatmapData = [];
+ double currentSiroPriceHeatmap = 0.0;
+
+ Future fetchPriceGapHeatmap() async {
+ try {
+ Map params = {};
+ if (selectedCountry != 'All') {
+ params['country_code'] = selectedCountry;
+ } else {
+ params['country_code'] = 'SY'; // Fallback
+ }
+
+ var res = await CRUD().post(
+ link: AppLink.getPriceGapHeatmap,
+ payload: params,
+ );
+ if (res is Map && res['status'] == 'success') {
+ final data = res['message'];
+ if (data is Map) {
+ heatmapData = data['heatmap_data'] ?? [];
+ currentSiroPriceHeatmap = (data['current_siro_price_per_km'] ?? 0.0).toDouble();
+ update();
+ }
+ }
+ } catch (e) {
+ // Silently fail
+ }
+ }
+
+ // --- Market Share Analytics ---
+ List marketShareData = [];
+ Future fetchMarketShareAnalytics() async {
+ try {
+ Map params = {};
+ if (selectedCountry != 'All') {
+ params['country_code'] = selectedCountry;
+ } else {
+ params['country_code'] = 'SY';
+ }
+
+ var res = await CRUD().post(
+ link: AppLink.getMarketShareAnalytics,
+ payload: params,
+ );
+ if (res is Map && res['status'] == 'success') {
+ marketShareData = res['historical_data'] ?? [];
+ update();
+ }
+ } catch (e) {
+ // Ignore
+ }
+ }
+
+ // --- AI Price Prediction ---
+ String? aiPredictionMessage;
+ List predictedSurgeHours = [];
+ Future fetchAiPricePrediction() async {
+ try {
+ Map params = {};
+ if (selectedCountry != 'All') {
+ params['country_code'] = selectedCountry;
+ } else {
+ params['country_code'] = 'SY';
+ }
+
+ var res = await CRUD().post(
+ link: AppLink.aiPricePrediction,
+ payload: params,
+ );
+ if (res is Map && res['status'] == 'success') {
+ aiPredictionMessage = res['ai_analysis_message'];
+ predictedSurgeHours = res['predicted_surge_hours'] ?? [];
+ update();
+ }
+ } catch (e) {
+ // Ignore
+ }
+ }
+
+ // --- Win-Back Targets ---
+ List winbackTargets = [];
+ int winbackTotalCount = 0;
+ Future fetchWinbackTargets() async {
+ try {
+ Map params = {};
+ if (selectedCountry != 'All') {
+ params['country_code'] = selectedCountry;
+ } else {
+ params['country_code'] = 'SY';
+ }
+
+ var res = await CRUD().post(
+ link: AppLink.winbackHotspotTargets,
+ payload: params,
+ );
+ if (res is Map && res['status'] == 'success') {
+ winbackTargets = res['targets'] ?? [];
+ winbackTotalCount = res['total_targets'] ?? 0;
+ update();
+ }
+ } catch (e) {
+ // Ignore
+ }
+ }
+
+ @override
+ void onInit() {
+ super.onInit();
+ // Initially fetch these too if needed, but fetch them specifically when country changes
+ }
}
diff --git a/siro_admin/lib/views/admin/marketing/marketing_page.dart b/siro_admin/lib/views/admin/marketing/marketing_page.dart
index c613e28..ac9aafd 100644
--- a/siro_admin/lib/views/admin/marketing/marketing_page.dart
+++ b/siro_admin/lib/views/admin/marketing/marketing_page.dart
@@ -27,6 +27,7 @@ class MarketingPage extends StatelessWidget {
controller.fetchCampaignsLog();
controller.fetchTelemetry();
controller.fetchPriceComparison();
+ controller.fetchPriceGapHeatmap();
return GetBuilder(
builder: (c) {
@@ -124,6 +125,9 @@ class MarketingPage extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildAIControlCard(context, c),
+ _buildAIPredictionCard(context, c),
+ _buildWhatIfSimulatorCard(context, c),
+ _buildMarketShareChart(context, c),
_buildComparisonChart(),
_buildPCIHeatmapSection(),
Padding(
@@ -386,33 +390,28 @@ class MarketingPage extends StatelessWidget {
Widget _buildPCIHeatmapSection() {
return GetBuilder(
builder: (c) {
- final pciData = c.pciRegions;
- final siroBase = c.siroBasePrices;
- final double siroSpeedPrice = (siroBase['speedPrice'] != null)
- ? double.tryParse(siroBase['speedPrice'].toString()) ?? 0.0
- : 0.0;
+ final heatmapList = c.heatmapData;
+ final siroSpeedPrice = c.currentSiroPriceHeatmap;
List