Update: 2026-07-02 05:27:04

This commit is contained in:
Hamza-Ayed
2026-07-02 05:27:05 +03:00
parent d2ce4bdb16
commit 05d047d871
25 changed files with 1625 additions and 85 deletions
+18
View File
@@ -89,6 +89,24 @@ FEMALE_GENDER_HASH=<CHANGE_ME_FEMALE_HASH>
FIREBASE_PROJECT_ID=siro-project
FIREBASE_API_KEY=<CHANGE_ME_FIREBASE_KEY>
# =============================================================================
# Payment Gateway Configuration
# =============================================================================
PAYMENT_GATEWAY_URL=https://api.paymentprovider.com
PAYMENT_GATEWAY_KEY=<CHANGE_ME_PAYMENT_KEY>
PAYMENT_GATEWAY_SECRET=<CHANGE_ME_PAYMENT_SECRET>
PAYMENT_WEBHOOK_SECRET=<CHANGE_ME_WEBHOOK_SECRET>
# =============================================================================
# Siro Commissions per Country
# =============================================================================
# Set the commission percentage Siro takes from drivers in each country
# (0.15 = 15%, 0.12 = 12%, 0.10 = 10%)
SIRO_COMMISSION_JO=0.15
SIRO_COMMISSION_SY=0.12
SIRO_COMMISSION_EG=0.10
SIRO_COMMISSION_IQ=0.10
# =============================================================================
# SMS Configuration (for OTP)
# =============================================================================
+70 -22
View File
@@ -1,29 +1,77 @@
<?php
header('Content-Type: application/json; charset=utf-8');
require_once __DIR__ . '/../../connect.php';
/**
* get_heatmap.php
* ───────────────
* تقرأ بيانات الخريطة الحرارية المجمعة من Redis
* البيانات مقسمة حسب الدولة (عبر Bounding Boxes في الـ Cron)
*/
// Optional filter: days
$days = filterRequest('days') ?? 7;
header('Content-Type: application/json; charset=utf-8');
require_once __DIR__ . '/../../connect.php'; // Includes functions.php which has filterRequest()
$days = (int)(filterRequest('days') ?? 7);
$source = filterRequest('source') ?? 'all';
$countryCode = strtoupper(filterRequest('country_code') ?? 'all');
try {
$sql = "SELECT latitude, longitude, source, created_at
FROM passenger_opening_locations
WHERE created_at >= DATE_SUB(NOW(), INTERVAL :days DAY)
ORDER BY created_at DESC LIMIT 5000"; // Limit to prevent massive payloads
$stmt = $con->prepare($sql);
$stmt->bindValue(':days', (int) $days, PDO::PARAM_INT);
$stmt->execute();
$locations = $stmt->fetchAll(PDO::FETCH_ASSOC);
echo json_encode([
"status" => "success",
"data" => $locations
]);
$redis = getRedisConnection();
$cacheJson = $redis->get('siro:cache:heatmap:data');
} catch (Exception $e) {
error_log("Error fetching heatmap data: " . $e->getMessage());
echo json_encode(["status" => "error", "message" => "Server error"]);
echo json_encode(['status' => 'error', 'message' => 'Redis connection failed']);
exit;
}
if (!$cacheJson) {
echo json_encode(['status' => 'error', 'message' => 'Cache not generated yet']);
exit;
}
$cacheData = json_decode($cacheJson, true);
if (!$cacheData || !isset($cacheData['data'])) {
echo json_encode(['status' => 'error', 'message' => 'Invalid cache data']);
exit;
}
$limitDate = date('Y-m-d', strtotime("-$days days"));
$filteredLocations = [];
$stats = ['geofence' => 0, 'app_usage' => 0, 'silent_push' => 0];
$dataByCountry = $cacheData['data'];
// تحديد الدول التي سنسحب منها
$countriesToSearch = ($countryCode === 'ALL') ? array_keys($dataByCountry) : [$countryCode];
foreach ($countriesToSearch as $cc) {
if (!isset($dataByCountry[$cc])) continue;
foreach ($dataByCountry[$cc] as $loc) {
// فلتر الأيام
if ($loc['date'] < $limitDate) continue;
// فلتر المصدر
if ($source !== 'all' && $loc['source'] !== $source) continue;
$filteredLocations[] = [
'latitude' => $loc['lat'],
'longitude' => $loc['lng'],
'source' => $loc['source']
];
if (isset($stats[$loc['source']])) {
$stats[$loc['source']]++;
}
}
}
echo json_encode([
'status' => 'success',
'data' => $filteredLocations,
'total' => count($filteredLocations),
'stats' => $stats,
'source' => 'redis_cache'
], JSON_UNESCAPED_UNICODE);
// تم إزالة دالة filterRequest من هنا لتجنب خطأ Redeclaration لأنها معرفة في functions.php
?>
+120
View File
@@ -0,0 +1,120 @@
<?php
/**
* get_competitor_context.php
* ──────────────────────────
* واجهة فائقة السرعة (Ultra-Fast API)
* تقرأ البيانات مباشرة من الـ Redis المولد عبر الـ Cron Job.
* زمن الاستجابة: O(1).
*/
header('Content-Type: application/json; charset=utf-8');
require_once __DIR__ . '/../../connect.php'; // Web-safe
$lat = filterRequest('lat');
$lng = filterRequest('lng');
$countryCode = filterRequest('country_code');
if (!$lat || !$lng || !$countryCode) {
echo json_encode(["status" => "error", "message" => "Missing parameters"]);
exit;
}
$lat = (float)$lat;
$lng = (float)$lng;
$countryCode = strtoupper($countryCode);
try {
$redis = getRedisConnection();
$cacheJson = $redis->get('siro:cache:pricing:grids');
} catch (Exception $e) {
echo json_encode(["status" => "error", "message" => "Redis connection failed"]);
exit;
}
if (!$cacheJson) {
echo json_encode(["status" => "success", "data" => null, "message" => "Cache not generated yet"]);
exit;
}
$cacheData = json_decode($cacheJson, true);
if (!$cacheData || !isset($cacheData['grids'])) {
echo json_encode(["status" => "success", "data" => null, "message" => "Invalid cache data"]);
exit;
}
// محاولة إيجاد الشبكة (Grid) للراكب
$gridSize = 0.025;
$gLat = round($lat / $gridSize) * $gridSize;
$gLng = round($lng / $gridSize) * $gridSize;
$gridKey = "{$countryCode}_" . number_format($gLat, 3) . "_" . number_format($gLng, 3);
$grids = $cacheData['grids'];
if (isset($grids[$gridKey])) {
$compData = $grids[$gridKey];
} else {
// إذا لم نجد، نستخدم الـ Fallback للدولة
$fallbackKey = "{$countryCode}_FALLBACK";
if (isset($grids[$fallbackKey])) {
$compData = $grids[$fallbackKey];
} else {
echo json_encode(["status" => "success", "data" => null, "message" => "No data for this region"]);
exit;
}
}
$avgPrice = $compData['avg_price'];
$topComp = $compData['top_competitor'] ?? 'Other';
// جلب سعر Siro للمقارنة السريعة
$sqlKazan = "SELECT (price_km + start_price) AS siro_base
FROM kazan
WHERE country_code = :cc AND type = 'speed'
LIMIT 1";
$stmtKazan = $con->prepare($sqlKazan);
$stmtKazan->execute([':cc' => $countryCode]);
$kazanRow = $stmtKazan->fetch(PDO::FETCH_ASSOC);
$siroPrice = $kazanRow ? (float)$kazanRow['siro_base'] : $avgPrice * 0.9;
$savingsPct = 0;
if ($avgPrice > 0 && $siroPrice < $avgPrice) {
$savingsPct = (($avgPrice - $siroPrice) / $avgPrice) * 100;
}
$passengerMessage = null;
$driverMessage = null;
$extraEarnings = 0;
if ($savingsPct > 2) {
$compNameAr = match(strtolower($topComp)) {
'careem' => 'كريم',
'uber' => 'أوبر',
'yallago' => 'يلا غو',
'jenny' => 'جيني',
default => 'التطبيقات الأخرى'
};
$passengerMessage = "أوفر بـ " . number_format($savingsPct, 1) . "% من $compNameAr ⚡";
// قراءة نسبة عمولة سيرو ديناميكياً من الإنفيرومنت، والنسبة الافتراضية 10% إذا لم تكن موجودة
$siroCommissionRate = (float)(getenv('SIRO_COMMISSION_' . $countryCode) ?: 0.10);
$extraEarnings = $siroPrice * $siroCommissionRate;
$driverMessage = "رحلة مربحة! تكسب أكثر مقارنة بـ $compNameAr 💰";
}
echo json_encode([
"status" => "success",
"data" => [
"competitor_avg_price" => $avgPrice,
"top_competitor" => $topComp,
"savings_percent" => $savingsPct,
"passenger_badge_text" => $passengerMessage,
"driver_badge_text" => $driverMessage,
"driver_extra_earnings" => round($extraEarnings, 2),
"source" => "redis_cache"
]
]);
?>
+30
View File
@@ -0,0 +1,30 @@
<?php
/**
* get_hotzones.php
* ───────────────
* واجهة فائقة السرعة (Ultra-Fast API) مخصصة لتطبيق السائق (Flutter).
* تقرأ المناطق الساخنة (Hot Zones) التي حددها الذكاء الاصطناعي من الـ Redis مباشرة.
* زمن الاستجابة: O(1).
*/
header('Content-Type: application/json; charset=utf-8');
require_once __DIR__ . '/../../connect.php';
$countryCode = strtoupper(filterRequest('country_code') ?? 'JO');
try {
$redis = getRedisConnection();
$hotZonesJson = $redis->get('siro:cache:ai:hotzones');
} catch (Exception $e) {
echo json_encode(["status" => "error", "message" => "Redis connection failed"]);
exit;
}
if (!$hotZonesJson) {
echo json_encode(["status" => "success", "data" => [], "message" => "No hot zones available"]);
exit;
}
// الـ JSON القادم من Redis تم تصميمه بالفعل بالشكل النهائي المطلوب للتطبيق
echo $hotZonesJson;
?>
@@ -58,9 +58,9 @@ try {
$encryptedBirthdate = $encryptionHelper->encryptData('1990-01-01');
$encryptedSite = $encryptionHelper->encryptData('Jordan');
// Insert passenger
$insert = $con->prepare("INSERT INTO passengers (id, phone, email, password, gender, birthdate, site, first_name, last_name)
VALUES (:id, :phone, :email, :password, :gender, :birthdate, :site, :first_name, :last_name)");
// Insert passenger with verified = 1 so app doesn't reject
$insert = $con->prepare("INSERT INTO passengers (id, phone, email, password, gender, birthdate, site, first_name, last_name, is_test, verified)
VALUES (:id, :phone, :email, :password, :gender, :birthdate, :site, :first_name, :last_name, 1, 1)");
$insert->execute([
':id' => $passengerId,
':phone' => $encryptedPhone,
@@ -131,6 +131,11 @@ try {
if(isset($data['employmentType'])) $data['employmentType'] = $encryptionHelper->decryptData($data['employmentType']);
if(isset($data['maritalStatus'])) $data['maritalStatus'] = $encryptionHelper->decryptData($data['maritalStatus']);
// Force verified = 1 for the test user so the Rider app doesn't reject the login
if (isset($data['is_test']) && $data['is_test'] == 1) {
$data['verified'] = 1;
}
// توليد الـ JWT بصلاحية (tester) لتميزهم عن المستخدمين الفعليين
$jwtService = new JwtService($redis);
$jwt = $jwtService->generateAccessToken($data['id'], 'tester', $audience, $fingerprint);
+176
View File
@@ -0,0 +1,176 @@
<?php
/**
* ai_formula_solver.php
* مكتشف خوارزميات المنافسين (AI Competitor Formula Solver)
* يستخدم الانحدار الخطي المتعدد (Multiple Linear Regression) لاكتشاف
* أجرة فتح العداد، وسعر الكيلومتر، وسعر الدقيقة لكل تطبيق منافس.
*/
require_once __DIR__ . '/../core/bootstrap.php';
require_once __DIR__ . '/../functions.php';
try {
$con = Database::get('main');
} catch (Exception $e) {
die("Database connection failed: " . $e->getMessage() . "\n");
}
echo "Starting AI Formula Discovery Engine...\n";
// نجلب التطبيقات التي لديها بيانات (مسافة ووقت وسعر)
$sqlApps = "SELECT DISTINCT competitor_name, country_code
FROM scraped_competitor_prices
WHERE distance_km > 0 AND duration_min > 0 AND price_amount > 0";
$stmtApps = $con->query($sqlApps);
$apps = $stmtApps->fetchAll(PDO::FETCH_ASSOC);
if (empty($apps)) {
echo "No sufficient data (Distance/Duration) found to perform regression.\n";
exit;
}
// دالة لحل نظام معادلات خطية (Gaussian Elimination)
function solveLinearSystem($A, $B) {
$n = count($A);
for ($i = 0; $i < $n; $i++) {
// Search for maximum in this column
$maxEl = abs($A[$i][$i]);
$maxRow = $i;
for ($k = $i + 1; $k < $n; $k++) {
if (abs($A[$k][$i]) > $maxEl) {
$maxEl = abs($A[$k][$i]);
$maxRow = $k;
}
}
// Swap maximum row with current row
for ($k = $i; $k < $n; $k++) {
$tmp = $A[$maxRow][$k];
$A[$maxRow][$k] = $A[$i][$k];
$A[$i][$k] = $tmp;
}
$tmp = $B[$maxRow];
$B[$maxRow] = $B[$i];
$B[$i] = $tmp;
// Make all rows below this one 0 in current column
for ($k = $i + 1; $k < $n; $k++) {
if ($A[$i][$i] == 0) continue;
$c = -$A[$k][$i] / $A[$i][$i];
for ($j = $i; $j < $n; $j++) {
if ($i == $j) {
$A[$k][$j] = 0;
} else {
$A[$k][$j] += $c * $A[$i][$j];
}
}
$B[$k] += $c * $B[$i];
}
}
// Solve equation Ax=b for an upper triangular matrix A
$x = array_fill(0, $n, 0);
for ($i = $n - 1; $i >= 0; $i--) {
if ($A[$i][$i] == 0) continue;
$x[$i] = $B[$i] / $A[$i][$i];
for ($k = $i - 1; $k >= 0; $k--) {
$B[$k] -= $A[$k][$i] * $x[$i];
}
}
return $x;
}
foreach ($apps as $app) {
$competitor = $app['competitor_name'];
$countryCode = $app['country_code'];
echo "Analyzing: $competitor ($countryCode)...\n";
// سحب أحدث 5000 رحلة لتكوين نموذج رياضي دقيق
$sqlData = "SELECT distance_km, duration_min, price_amount
FROM scraped_competitor_prices
WHERE competitor_name = :comp
AND country_code = :country
AND distance_km > 0 AND duration_min > 0 AND price_amount > 0
ORDER BY id DESC LIMIT 5000";
$stmtData = $con->prepare($sqlData);
$stmtData->execute([':comp' => $competitor, ':country' => $countryCode]);
$samples = $stmtData->fetchAll(PDO::FETCH_ASSOC);
$N = count($samples);
if ($N < 10) {
echo " -> Not enough samples ($N). Skipping.\n";
continue;
}
// بناء مصفوفات Least Squares (X^T X) * Beta = (X^T Y)
// Beta = [Base_Fare, KM_Price, Min_Price]
$sum_x1 = 0; $sum_x2 = 0; $sum_y = 0;
$sum_x1_sq = 0; $sum_x2_sq = 0; $sum_x1_x2 = 0;
$sum_x1_y = 0; $sum_x2_y = 0;
foreach ($samples as $s) {
$x1 = (float)$s['distance_km'];
$x2 = (float)$s['duration_min'];
$y = (float)$s['price_amount'];
$sum_x1 += $x1;
$sum_x2 += $x2;
$sum_y += $y;
$sum_x1_sq += ($x1 * $x1);
$sum_x2_sq += ($x2 * $x2);
$sum_x1_x2 += ($x1 * $x2);
$sum_x1_y += ($x1 * $y);
$sum_x2_y += ($x2 * $y);
}
$matrixA = [
[$N, $sum_x1, $sum_x2],
[$sum_x1, $sum_x1_sq, $sum_x1_x2],
[$sum_x2, $sum_x1_x2, $sum_x2_sq]
];
$matrixB = [
$sum_y,
$sum_x1_y,
$sum_x2_y
];
// حل المصفوفة
try {
$beta = solveLinearSystem($matrixA, $matrixB);
$baseFare = round(max(0, $beta[0]), 3); // Base fare cannot be negative
$kmPrice = round(max(0, $beta[1]), 3);
$minPrice = round(max(0, $beta[2]), 3);
echo " -> [DISCOVERED] Base Fare: $baseFare, KM: $kmPrice, Min: $minPrice\n";
// حفظ في جدول المعادلات السرية
$sqlUpsert = "INSERT INTO competitor_secret_formulas
(competitor_name, country_code, base_fare, price_per_km, price_per_min, sample_size)
VALUES (:comp, :country, :base, :km, :min, :size)
ON DUPLICATE KEY UPDATE
base_fare = :base, price_per_km = :km, price_per_min = :min, sample_size = :size, last_updated = NOW()";
$stmtUp = $con->prepare($sqlUpsert);
$stmtUp->execute([
':comp' => $competitor,
':country' => $countryCode,
':base' => $baseFare,
':km' => $kmPrice,
':min' => $minPrice,
':size' => $N
]);
echo " -> Saved successfully.\n";
} catch (Exception $e) {
echo " -> Error solving matrix: " . $e->getMessage() . "\n";
}
}
echo "Done.\n";
?>
+172
View File
@@ -0,0 +1,172 @@
<?php
/**
* cron_ai_engine.php
* المحرك الرئيسي للذكاء الاصطناعي (AI Engine)
* يتم تشغيله كـ Cron Job كل 30 دقيقة أو ساعة لتقليل الضغط على السيرفر.
*
* يدمج 3 وحدات (Modules) ذكية:
* 1. AI Pricing (Total Price Math): تعديل جدول kazan ليكون السعر الإجمالي أرخص بـ 6.5% من المنافس الأقوى بدقة.
* 2. AI Dispatch: تحديد مناطق الذروة وتوجيه السائقين إليها.
* 3. AI Retention: اصطياد الركاب الخاملين.
*/
require_once __DIR__ . '/../core/bootstrap.php';
require_once __DIR__ . '/../functions.php';
try {
$con = Database::get('main');
$redis = getRedisConnection();
} catch (Exception $e) {
die("Connection failed: " . $e->getMessage() . "\n");
}
echo "Starting Siro AI Engine...\n";
// نسبة الخصم المستهدفة (6.5% من إجمالي سعر الرحلة)
$targetMargin = 0.065;
// ==========================================
// 1. وحدة التسعير الديناميكي بناءً على السعر الإجمالي
// ==========================================
echo "1. Running Smart Pricing Module (Total Price Formula)...\n";
try {
$sql = "SELECT country_code,
AVG(price_per_km) as avg_price_km,
MIN(price_per_km) as min_price_km
FROM scraped_competitor_prices
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 3 HOUR)
AND price_per_km > 0
GROUP BY country_code";
$stmt = $con->query($sql);
$competitorRates = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($competitorRates as $rate) {
$country = $rate['country_code'];
$countryNameMap = ['JO' => 'Jordan', 'SY' => 'Syria', 'EG' => 'Egypt', 'IQ' => 'Iraq'];
$countryName = $countryNameMap[$country] ?? null;
if ($countryName) {
$avgKmPrice = (float)$rate['avg_price_km'];
$minKmPrice = (float)$rate['min_price_km'];
// 1. حساب السعر الفعّال للكيلومتر بناءً على المتوسط والأرخص
$effectiveCompetitorPrice = round($avgKmPrice * (1 - $targetMargin), 2);
if ($effectiveCompetitorPrice > $minKmPrice) {
$effectiveCompetitorPrice = round(($effectiveCompetitorPrice + $minKmPrice) / 2, 2);
}
// 2. الهندسة العكسية للسعر الإجمالي (Reverse Engineering)
// بما أن سيرو يضيف سعر الدقيقة (والتي تعادل دقيقتين لكل كيلومتر تقريباً)، فإن التكلفة الإضافية للدقائق ترفع السعر الإجمالي بمقدار 1.5x
// لضمان أن يكون السعر النهائي أقل بـ 6%، نقسم الناتج على 1.5 ليمتص تكلفة الدقائق.
$calculatedSpeedPrice = round($effectiveCompetitorPrice / 1.5, 3);
// 3. تسعير الفئات المتعددة
$newSpeedPrice = $calculatedSpeedPrice;
$newComfortPrice = round($newSpeedPrice * 1.30, 3);
$newLadyPrice = round($newSpeedPrice * 1.10, 3);
$newElectricPrice = round($newSpeedPrice * 1.20, 3);
$newVanPrice = round($newSpeedPrice * 1.50, 3);
$newDeliveryPrice = round($newSpeedPrice * 0.90, 3);
$newMishwarVipPrice = round($newSpeedPrice * 1.40, 3);
$newFixedPrice = $newSpeedPrice;
$newAwfarPrice = round($newSpeedPrice * 0.85, 3);
// أسعار الدقائق
$newNormalMin = round($newSpeedPrice / 4, 3);
$newPeakMin = round($newNormalMin * 1.15, 3);
$newLateMin = round($newNormalMin * 1.25, 3);
$updateSql = "UPDATE kazan
SET speedPrice = :speedPrice,
comfortPrice = :comfortPrice,
ladyPrice = :ladyPrice,
electricPrice = :electricPrice,
vanPrice = :vanPrice,
deliveryPrice = :deliveryPrice,
mishwarVipPrice = :mishwarVipPrice,
fixedPrice = :fixedPrice,
awfarPrice = :awfarPrice,
normalMinPrice = :normalMin,
peakMinPrice = :peakMin,
lateMinPrice = :lateMin
WHERE country = :countryName";
$upStmt = $con->prepare($updateSql);
$upStmt->execute([
':speedPrice' => $newSpeedPrice,
':comfortPrice' => $newComfortPrice,
':ladyPrice' => $newLadyPrice,
':electricPrice' => $newElectricPrice,
':vanPrice' => $newVanPrice,
':deliveryPrice' => $newDeliveryPrice,
':mishwarVipPrice' => $newMishwarVipPrice,
':fixedPrice' => $newFixedPrice,
':awfarPrice' => $newAwfarPrice,
':normalMin' => $newNormalMin,
':peakMin' => $newPeakMin,
':lateMin' => $newLateMin,
':countryName' => $countryName
]);
echo " -> Updated $countryName (Total Price Math applied): Speed=$newSpeedPrice JOD/KM, NormalMin=$newNormalMin JOD/MIN\n";
}
}
} catch (Exception $e) {
echo " Error in Pricing Module: " . $e->getMessage() . "\n";
}
// ==========================================
// 2. وحدة توجيه السائقين (Demand Predictor)
// ==========================================
echo "2. Running Demand Predictor Module...\n";
try {
$cacheJson = $redis->get('siro:cache:pricing:grids');
$hotZones = [];
if ($cacheJson) {
$grids = json_decode($cacheJson, true)['grids'] ?? [];
foreach ($grids as $key => $data) {
if (strpos($key, 'FALLBACK') !== false) continue;
if ($data['avg_price'] > 0) {
$parts = explode('_', $key);
if (count($parts) == 3) {
$hotZones[] = [
'latitude' => (float)$parts[1],
'longitude' => (float)$parts[2],
'avg_price' => $data['avg_price'],
'top_competitor' => $data['top_competitor'],
'timestamp' => time()
];
}
}
}
$redis->set('siro:cache:ai:hotzones', json_encode(['status' => 'success', 'data' => $hotZones], JSON_UNESCAPED_UNICODE));
echo " -> Saved " . count($hotZones) . " Hot Zones to Redis for Driver Map Guidance.\n";
}
} catch (Exception $e) {
echo " Error in Demand Predictor: " . $e->getMessage() . "\n";
}
// ==========================================
// 3. وحدة استهداف الركاب الخاملين (Smart Retention)
// ==========================================
echo "3. Running Smart Retention Module...\n";
try {
$sql = "SELECT source, COUNT(*) as opens
FROM passenger_opening_locations
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 3 HOUR)
GROUP BY source
HAVING opens >= 3";
$stmt = $con->query($sql);
$idleRiders = $stmt->fetchAll(PDO::FETCH_ASSOC);
$notifiedCount = count($idleRiders);
echo " -> Identified $notifiedCount idle riders requiring push notifications.\n";
} catch (Exception $e) {
echo " Error in Smart Retention: " . $e->getMessage() . "\n";
}
echo "AI Engine finished successfully.\n";
?>
+22 -6
View File
@@ -35,7 +35,7 @@ if (empty($data) || !is_array($data)) {
}
$insertedCount = 0;
$stmt = $con->prepare("INSERT INTO scraped_competitor_prices (task_id, app_name, competitor_name, start_location, end_location, start_lat, start_lng, end_lat, end_lng, price_amount, price_per_km, currency, country_code) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
$stmt = $con->prepare("INSERT INTO scraped_competitor_prices (task_id, app_name, competitor_name, start_location, end_location, start_lat, start_lng, end_lat, end_lng, price_amount, price_per_km, distance_km, duration_min, currency, country_code) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)");
foreach ($data as $row) {
if (isset($row['status']) && $row['status'] !== 'success') {
@@ -46,6 +46,7 @@ foreach ($data as $row) {
$resultData = $row['result_data'] ?? [];
$appName = $resultData['app'] ?? $row['app'] ?? 'Unknown';
$competitorName = $appName; // Assuming app_name is competitor_name for now
$startLoc = $resultData['start_location'] ?? $row['start_location'] ?? '';
if (empty($startLoc) && !empty($resultData['start_lat'])) {
@@ -84,19 +85,34 @@ foreach ($data as $row) {
}
$distanceKm = (float)($resultData['distance_km'] ?? 1);
if ($distanceKm <= 0) $distanceKm = 1;
if ($distanceKm <= 0) $distanceKm = 1; // Prevent division by zero
$pricePerKm = $amount / $distanceKm;
$durationMin = isset($resultData['duration_min']) ? (int)$resultData['duration_min'] : null;
$startLat = $resultData['start_lat'] ?? null;
$startLng = $resultData['start_lng'] ?? null;
$endLat = $resultData['end_lat'] ?? null;
$endLng = $resultData['end_lng'] ?? null;
$countryCode = 'JO'; // Default for now, as scraping is in Jordan
$countryCode = $row['country_code'] ?? 'JO'; // Default
if ($stmt->execute([
$taskId, $appName, $appName, $startLoc, $endLoc,
$startLat, $startLng, $endLat, $endLng,
$amount, $pricePerKm, $currency, $countryCode
$taskId,
$appName,
$competitorName,
$startLoc,
$endLoc,
$startLat,
$startLng,
$endLat,
$endLng,
$amount,
$pricePerKm,
$distanceKm,
$durationMin,
$currency,
$countryCode
])) {
$insertedCount++;
} else {
+103
View File
@@ -0,0 +1,103 @@
<?php
/**
* cron_generate_heatmap_cache.php
* يجمع بيانات الخريطة الحرارية ويخزنها في Redis
*/
require_once __DIR__ . '/../core/bootstrap.php';
require_once __DIR__ . '/../functions.php';
try {
$con = Database::get('main');
$redis = getRedisConnection();
} catch (Exception $e) {
die("Connection failed: " . $e->getMessage() . "\n");
}
echo "Starting Heatmap Cache Generation (Redis)...\n";
// مربعات المدن الكبرى لتمثيل الدول (لتجنب حساب المضلعات المعقدة)
// الأردن (عمان والزرقاء)
// سوريا (دمشق)
// مصر (القاهرة والإسكندرية)
// العراق (بغداد)
$cityBounds = [
'JO' => [ // Amman & Zarqa rough bounding box
'lat' => [31.80, 32.20],
'lng' => [35.80, 36.20]
],
'SY' => [ // Damascus
'lat' => [33.40, 33.60],
'lng' => [36.20, 36.40]
],
'EG' => [ // Cairo & Alexandria
'lat' => [29.80, 31.30],
'lng' => [29.80, 31.50]
],
'IQ' => [ // Baghdad
'lat' => [33.10, 33.50],
'lng' => [44.20, 44.60]
]
];
try {
$sql = "SELECT latitude, longitude, source, created_at
FROM passenger_opening_locations
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY)
ORDER BY created_at DESC
LIMIT 20000";
$stmt = $con->query($sql);
$locations = $stmt->fetchAll(PDO::FETCH_ASSOC);
$stats = ['geofence' => 0, 'app_usage' => 0, 'silent_push' => 0];
// تقسيم البيانات حسب الدولة (لتسهيل قراءتها من الـ API)
$countryData = [
'JO' => [], 'SY' => [], 'EG' => [], 'IQ' => [], 'OTHER' => []
];
foreach ($locations as $loc) {
$lat = (float)$loc['latitude'];
$lng = (float)$loc['longitude'];
if ($lat == 0 || $lng == 0) continue;
$src = $loc['source'] ?? 'app_usage';
$date = substr($loc['created_at'], 0, 10);
$assignedCountry = 'OTHER';
// البحث عن المربع الذي يقع فيه الإحداثي
foreach ($cityBounds as $cc => $b) {
if ($lat >= $b['lat'][0] && $lat <= $b['lat'][1] &&
$lng >= $b['lng'][0] && $lng <= $b['lng'][1]) {
$assignedCountry = $cc;
break;
}
}
if (isset($stats[$src])) $stats[$src]++;
$countryData[$assignedCountry][] = [
'lat' => $lat,
'lng' => $lng,
'source' => $src,
'date' => $date
];
}
$redisData = [
'last_updated' => date('Y-m-d H:i:s'),
'total' => count($locations),
'stats' => $stats,
'data' => $countryData // مقسمة وجاهزة
];
$redis->set('siro:cache:heatmap:data', json_encode($redisData, JSON_UNESCAPED_UNICODE));
echo "Heatmap Cache Generated Successfully. Points: " . count($locations) . "\n";
} catch (Exception $e) {
error_log("Error generating heatmap cache: " . $e->getMessage());
echo "Error: " . $e->getMessage();
}
?>
+119
View File
@@ -0,0 +1,119 @@
<?php
/**
* cron_generate_pricing_cache.php
* يجمع أسعار المنافسين لكل مربع جغرافي (2.5km) ويحفظ النتيجة في Redis
* يتم تشغيله كـ Cron Job (CLI) لتخفيف الضغط تماماً عن الاستعلام المباشر للركاب.
*/
require_once __DIR__ . '/../core/bootstrap.php';
require_once __DIR__ . '/../functions.php';
try {
$con = Database::get('main');
$redis = getRedisConnection();
} catch (Exception $e) {
die("Connection failed: " . $e->getMessage() . "\n");
}
echo "Starting Pricing Cache Generation (Redis)...\n";
try {
// 1. جلب متوسط الأسعار من المنافسين في آخر ساعة للشبكة
$sql = "SELECT country_code,
ROUND(latitude / 0.025) * 0.025 AS grid_lat,
ROUND(longitude / 0.025) * 0.025 AS grid_lng,
competitor_name,
AVG(price) as avg_price,
COUNT(*) as requests_count
FROM competitor_prices
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 1 HOUR)
GROUP BY country_code, grid_lat, grid_lng, competitor_name";
$stmt = $con->query($sql);
$results = $stmt->fetchAll(PDO::FETCH_ASSOC);
// بناء مصفوفة الشبكات
$grids = [];
foreach ($results as $row) {
$cc = strtoupper($row['country_code']);
$gLat = number_format((float)$row['grid_lat'], 3);
$gLng = number_format((float)$row['grid_lng'], 3);
$key = "{$cc}_{$gLat}_{$gLng}";
if (!isset($grids[$key])) {
$grids[$key] = [
'competitors' => [],
'total_price' => 0,
'total_competitors' => 0
];
}
$grids[$key]['competitors'][$row['competitor_name']] = (float)$row['avg_price'];
$grids[$key]['total_price'] += (float)$row['avg_price'];
$grids[$key]['total_competitors']++;
}
$processedGrids = [];
$fallbackData = []; // لمتوسط البلد بالكامل كبديل
foreach ($grids as $key => $data) {
if ($data['total_competitors'] == 0) continue;
$overallAvg = $data['total_price'] / $data['total_competitors'];
// إيجاد المنافس الأرخص في هذا المربع
$cheapestComp = '';
$cheapestPrice = 999999;
foreach ($data['competitors'] as $name => $price) {
if ($price < $cheapestPrice) {
$cheapestPrice = $price;
$cheapestComp = $name;
}
}
$gridInfo = [
'avg_price' => round($overallAvg, 2),
'top_competitor' => $cheapestComp,
'cheapest_price' => round($cheapestPrice, 2)
];
$processedGrids[$key] = $gridInfo;
// حفظ المتوسط للـ Fallback
$cc = explode('_', $key)[0];
if (!isset($fallbackData[$cc])) {
$fallbackData[$cc] = ['sum' => 0, 'count' => 0, 'cheapest_comp' => $cheapestComp];
}
$fallbackData[$cc]['sum'] += $overallAvg;
$fallbackData[$cc]['count']++;
}
// إضافة Fallback لكل دولة (في حال الراكب كان في مربع فارغ)
foreach ($fallbackData as $cc => $d) {
if ($d['count'] > 0) {
$processedGrids["{$cc}_FALLBACK"] = [
'avg_price' => round($d['sum'] / $d['count'], 2),
'top_competitor' => $d['cheapest_comp'],
'cheapest_price' => round($d['sum'] / $d['count'], 2)
];
}
}
// 2. الحفظ في Redis
// نحفظ المصفوفة بالكامل كـ JSON String داخل مفتاح رئيسي واحد للسرعة العالية في القراءة
// مفتاح: siro:cache:pricing:grids
$redisData = [
'last_updated' => date('Y-m-d H:i:s'),
'grids' => $processedGrids
];
$redis->set('siro:cache:pricing:grids', json_encode($redisData, JSON_UNESCAPED_UNICODE));
echo "Pricing Cache Generated Successfully in Redis. Grids: " . count($processedGrids) . "\n";
} catch (Exception $e) {
error_log("Error generating pricing cache (Redis): " . $e->getMessage());
echo "Error: " . $e->getMessage();
}
?>
+28
View File
@@ -30,6 +30,8 @@ CREATE TABLE IF NOT EXISTS `scraped_competitor_prices` (
`end_lng` decimal(10,7) DEFAULT NULL,
`price_amount` decimal(8,2) NOT NULL,
`price_per_km` decimal(8,2) NOT NULL,
`distance_km` decimal(8,2) DEFAULT NULL,
`duration_min` int DEFAULT NULL,
`currency` varchar(10) NOT NULL DEFAULT 'JOD',
`country_code` varchar(10) NOT NULL DEFAULT 'JO',
`scraped_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
@@ -42,6 +44,32 @@ CREATE TABLE IF NOT EXISTS `scraped_competitor_prices` (
";
$con->exec($sql);
// [AI Formula Discovery] Create table for reverse-engineered formulas
$sqlFormula = "
CREATE TABLE IF NOT EXISTS `competitor_secret_formulas` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`competitor_name` varchar(100) NOT NULL,
`country_code` varchar(10) NOT NULL,
`base_fare` decimal(8,3) NOT NULL,
`price_per_km` decimal(8,3) NOT NULL,
`price_per_min` decimal(8,3) NOT NULL,
`confidence_score` decimal(5,2) DEFAULT 0,
`sample_size` int DEFAULT 0,
`last_updated` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY `idx_comp_country` (`competitor_name`, `country_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
";
$con->exec($sqlFormula);
// Auto-patch existing table if upgrading
try {
$con->exec("ALTER TABLE `scraped_competitor_prices` ADD COLUMN `distance_km` decimal(8,2) DEFAULT NULL AFTER `price_per_km`");
$con->exec("ALTER TABLE `scraped_competitor_prices` ADD COLUMN `duration_min` int DEFAULT NULL AFTER `distance_km`");
} catch (Exception $e) {
// Columns might already exist, ignore error
}
// 2. Ten Key Regions in Damascus (Syria) and Amman (Jordan)
$countriesConfig = [
'SY' => [
+57 -1
View File
@@ -72,6 +72,62 @@ class SiroGeminiService {
}
";
return $this->callGemini($prompt, $model);
}
/**
* يُولّد إشعاراً مخصصاً لحدث دخول الجيوفينس بناءً على بيانات أسعار حقيقية.
*
* @param string $zoneName اسم منطقة السياج الجغرافي
* @param string $countryCode رمز الدولة (SY, JO, EG, IQ)
* @param float $savingsPct نسبة التوفير (مثال: 8.5)
* @param string $topCompetitor اسم أبرز منافس في المنطقة
* @param string $model
* @return array|null
*/
public function generateGeofenceMessage(
string $zoneName,
string $countryCode,
float $savingsPct,
string $topCompetitor = 'كريم',
string $model = 'gemini-flash-lite-latest'
): ?array {
if (!$this->apiKey) return null;
$dialect = match (strtoupper($countryCode)) {
'SY' => 'السورية الشامية',
'JO' => 'الأردنية',
'EG' => 'المصرية العامية',
'IQ' => 'العراقية',
default => 'العربية الفصحى'
};
$savingsFormatted = number_format($savingsPct, 1);
$prompt = "
أنت كاتب إشعارات تسويقية ذكية لتطبيق Siro لخدمات نقل الركاب.
المستخدم الآن موجود بالقرب من '$zoneName'.
سعر سيرو أقل بـ $savingsFormatted% من $topCompetitor وبقية التطبيقات في هذه المنطقة.
المطلوب: اكتب إشعاراً push قصيراً جداً (عنوان + جسم) باللهجة $dialect.
- العنوان: لا يتجاوز 5 كلمات، مثير للاهتمام
- الجسم: لا يتجاوز 12 كلمة، يذكر التوفير الفعلي ويحفّز على الطلب الآن
- اجعله طبيعياً كأنه يكتبه شخص حقيقي وليس روبوت
الخرج (JSON فقط، بدون أي شرح):
{
\"push_title\": \"...\",
\"push_body\": \"...\"
}
";
return $this->callGemini($prompt, $model);
}
/**
* Helper: يُرسل prompt لـ Gemini ويُعيد JSON مُفكَّك
*/
private function callGemini(string $prompt, string $model): ?array {
$apiUrl = $this->baseUrl . $model . ":generateContent?key=" . $this->apiKey;
$payload = [
@@ -95,7 +151,7 @@ class SiroGeminiService {
]);
$response = curl_exec($ch);
$curlErr = curl_error($ch);
$curlErr = curl_error($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
+25
View File
@@ -66,6 +66,31 @@ foreach ($keys as $key) {
}
}
// === MERGE AI HOT ZONES ===
// ندمج مناطق الذروة الخاصة بالمنافسين (التي استخرجها الذكاء الاصطناعي) لتظهر باللون الأحمر (High)
try {
$aiHotZonesJson = $redis->get('siro:cache:ai:hotzones');
if ($aiHotZonesJson) {
$aiZonesData = json_decode($aiHotZonesJson, true);
if (isset($aiZonesData['status']) && $aiZonesData['status'] == 'success' && isset($aiZonesData['data'])) {
foreach ($aiZonesData['data'] as $zone) {
// نضيفها للمصفوفة ليقوم تطبيق الفلاتر برسمها كمربعات حمراء تلقائياً
$heatmap_data[] = [
"lat" => (float)$zone['latitude'],
"lng" => (float)$zone['longitude'],
"count" => 99, // رقم كبير لإجبار التطبيق على تلوينها بالأحمر (High)
"intensity" => "high",
"is_ai_surge" => true,
"competitor" => $zone['top_competitor'] ?? '',
"price" => $zone['avg_price'] ?? 0
];
}
}
}
} catch (Exception $e) {
error_log("[heatmap_live.php] AI Hotzones merge error: " . $e->getMessage());
}
// Output the JSON array as expected by home_captain_controller.dart
header('Content-Type: application/json');
echo json_encode($heatmap_data);