Update: 2026-07-09 03:41:48

This commit is contained in:
Hamza-Ayed
2026-07-09 03:41:48 +03:00
parent 7dff7b6973
commit 70718946f5
10 changed files with 295 additions and 639 deletions
+9 -15
View File
@@ -66,26 +66,20 @@ if ($avgPricePerKm <= 0) {
} }
// Calculate the competitor's total price based on distance and average market per-km rate // Calculate the competitor's total price based on distance and average market per-km rate
// 🔥 لا يوجد أي تعديل صناعي على سعر المنافس هنا — الرقم المعروض للراكب
// يجب أن يعكس بيانات السوق الحقيقية فقط، حتى لو لم نكن أرخص فعلياً في هذه الرحلة.
$competitorTotalPrice = round($distance * $avgPricePerKm, 2); $competitorTotalPrice = round($distance * $avgPricePerKm, 2);
// Make sure competitor is slightly higher than us for psychological effect
// if their raw math somehow ended up lower due to straight per-km multiplication
if ($siroPrice > 0 && $competitorTotalPrice <= $siroPrice) {
// Force a dynamic difference based on country
$multiplier = ($countryCode === 'JO') ? 1.15 : 1.10;
$competitorTotalPrice = round($siroPrice * $multiplier, 2);
}
// Calculate savings
$savingsPct = 0;
if ($competitorTotalPrice > 0 && $siroPrice < $competitorTotalPrice) {
$savingsPct = (($competitorTotalPrice - $siroPrice) / $competitorTotalPrice) * 100;
}
// Format the labels // Format the labels
$compNameAr = 'التطبيقات الأخرى'; $compNameAr = 'التطبيقات الأخرى';
$savingsLabel = "أوفر بـ " . number_format($savingsPct, 1) . "% من $compNameAr ⚡"; // نعرض شارة "أوفر" فقط إذا كنا أرخص فعلياً حسب البيانات الحقيقية — لا تلاعب بالأرقام
$savingsPct = 0;
$savingsLabel = null;
if ($competitorTotalPrice > 0 && $siroPrice > 0 && $siroPrice < $competitorTotalPrice) {
$savingsPct = (($competitorTotalPrice - $siroPrice) / $competitorTotalPrice) * 100;
$savingsLabel = "أوفر بـ " . number_format($savingsPct, 1) . "% من $compNameAr ⚡";
}
$siroCommissionRate = 0.14; // Default 14% commission $siroCommissionRate = 0.14; // Default 14% commission
if ($countryCode === 'JO') $siroCommissionRate = 0.14; if ($countryCode === 'JO') $siroCommissionRate = 0.14;
+13 -1
View File
@@ -119,7 +119,18 @@ try {
// --- Location Server Redis --- // --- Location Server Redis ---
$redisLocation = new Redis(); $redisLocation = new Redis();
$locHost = getenv('REDIS_LOCATION_HOST') ?: $redisHost; // 🔥 [Fix Silent Fallback] إذا لم تُضبط REDIS_LOCATION_HOST صراحة، نسقط
// على Redis الرئيسي — وهذا يجعل استعلامات كثافة السائقين (geo:drivers:*)
// ترجع فارغة بصمت لأن تلك المفاتيح تُكتب فقط على Redis الخاص بلوكيشن
// سيرفر. نسجّل تحذيراً واضحاً حتى لا يمر هذا دون ملاحظة في اللوجز.
$locHostConfigured = getenv('REDIS_LOCATION_HOST');
if (!$locHostConfigured) {
error_log('[REDIS] ⚠️ REDIS_LOCATION_HOST is not set — $redisLocation is falling back to the MAIN redis host (' . $redisHost . '). ' .
'geo:drivers:available / driver:profile:* / driver:public:* keys live only on the location-server Redis, ' .
'so driver-density lookups (getSpeed.php, heatmap_live.php, pricing/get.php) will silently return empty results ' .
'unless REDIS_LOCATION_HOST/PORT/PASSWORD are configured correctly in .env.');
}
$locHost = $locHostConfigured ?: $redisHost;
$locPort = (int)(getenv('REDIS_LOCATION_PORT') ?: $redisPort); $locPort = (int)(getenv('REDIS_LOCATION_PORT') ?: $redisPort);
$locPass = getenv('REDIS_LOCATION_PASSWORD') ?: $redisPass; $locPass = getenv('REDIS_LOCATION_PASSWORD') ?: $redisPass;
@@ -127,6 +138,7 @@ try {
if ($locPass) $redisLocation->auth($locPass); if ($locPass) $redisLocation->auth($locPass);
// No prefix for location server // No prefix for location server
} else { } else {
error_log("[REDIS] ⚠️ Failed to connect \$redisLocation to $locHost:$locPort — driver-density features will be degraded.");
$redisLocation = null; $redisLocation = null;
} }
} }
-553
View File
@@ -1,553 +0,0 @@
<?php
/**
* driver_socket.php
* ==================
* WebSocket Server للسائقين — بورت 2020
* Internal HTTP Server — بورت 2021
*
* 🚀 Level 2 Architecture (Production Ready):
* - Event Buffering (Batching)
* - Redis Pipelines (تقليل الـ I/O والـ Latency بشكل كبير)
* - Memory State Cache للسائقين
* - جميع طرق HTTP (Dispatch, Market, Force Disconnect...) موجودة بالكامل
*/
use Workerman\Worker;
use Workerman\Timer;
use Workerman\Http\Client as AsyncHttp;
use PHPSocketIO\SocketIO;
use Predis\Client as RedisClient;
require_once __DIR__ . '/vendor/autoload.php';
// ============================================================
// ⚙️ إعدادات عامة
// ============================================================
ini_set('memory_limit', '512M');
date_default_timezone_set('Asia/Amman');
// ── Tunables (إعدادات الأداء) ──────────────────────────────────
const MIN_MOVE_METERS = 10.0; // GEOADD فقط إذا تحرك أكثر من 10 متر
const HMSET_SPEED_DELTA = 1.0; // فرق السرعة المطلوب لتحديث Redis
const HMSET_HEADING_DELTA = 5.0; // فرق الاتجاه المطلوب لتحديث Redis
const EXPIRE_REFRESH_SECONDS = 300; // 5 دقائق لتجديد الـ TTL
const FORWARD_MIN_METERS = 15.0; // HTTP forward للراكب
const FORWARD_MAX_SECONDS = 3; // أقصى مدة للـ Forward
const REDIS_BATCH_INTERVAL = 0.5; // تنفيذ مجمّع (Batch) كل نصف ثانية (500ms)
// ─────────────────────────────────────────────────────────────
function logMsg(string $msg): void {
echo '[' . date('Y-m-d H:i:s') . '] ' . $msg . PHP_EOL;
}
function loadEnvironment(string $filePath): void {
if (!file_exists($filePath)) {
logMsg("⚠️ .env not found: $filePath");
return;
}
foreach (file($filePath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) {
if (str_starts_with(trim($line), '#') || !str_contains($line, '=')) continue;
[$name, $value] = explode('=', $line, 2);
putenv(trim($name) . '=' . trim($value, "\"'"));
}
logMsg('✅ Environment loaded.');
}
$siteUser = get_current_user();
$homeDir = "/home/$siteUser";
if (!is_dir($homeDir)) {
$homeDir = '/home/location'; // Fallback to original location
}
loadEnvironment(getenv('ENV_FILE_PATH') ?: ($homeDir . '/.env'));
// ============================================================
// 🔐 مفاتيح الأمان
// ============================================================
$INTERNAL_KEY = trim((string) @file_get_contents(getenv('INTERNAL_SOCKET_KEY_PATH') ?: ($homeDir . '/.internal_socket_key')));
$redisPass = trim((string) @file_get_contents(getenv('REDIS_PASS_KEY_PATH') ?: ($homeDir . '/.reds_pass_key')));
if (empty($INTERNAL_KEY)) logMsg('❌ CRITICAL: Internal key missing!');
if (empty($redisPass)) logMsg('❌ CRITICAL: Redis password missing!');
// ============================================================
// 🗄️ Redis Singleton
// ============================================================
$redis = null;
function getRedis(): ?RedisClient {
global $redis, $redisPass;
if ($redis !== null) {
try {
$redis->ping();
return $redis;
} catch (\Exception $e) {
logMsg('⚠️ Redis ping failed, reconnecting...');
$redis = null;
}
}
try {
$client = new RedisClient([
'scheme' => 'tcp',
'host' => '127.0.0.1',
'port' => 6379,
'password' => $redisPass,
'read_write_timeout' => 0,
]);
$client->connect();
$redis = $client;
return $redis;
} catch (\Exception $e) {
logMsg('❌ Redis Error: ' . $e->getMessage());
return null;
}
}
// ============================================================
// 📐 Haversine Distance (متر)
// ============================================================
function haversineDistance(float $lat1, float $lng1, float $lat2, float $lng2): float {
$R = 6371000;
$dLat = deg2rad($lat2 - $lat1);
$dLng = deg2rad($lng2 - $lng1);
$a = sin($dLat / 2) ** 2
+ cos(deg2rad($lat1)) * cos(deg2rad($lat2)) * sin($dLng / 2) ** 2;
return $R * 2 * atan2(sqrt($a), sqrt(1 - $a));
}
// ============================================================
// 📡 Forward موقع السائق → سيرفر الراكب (ASYNC)
// ============================================================
function forwardLocationToPassengerSocket(
string $driverId,
string $passengerId,
array $payload,
string $internalKey,
array &$fwdThrottle
): void {
if (empty($passengerId)) return;
$now = time();
$last = $fwdThrottle[$driverId] ?? null;
if ($last !== null) {
$timeDiff = $now - $last['ts'];
$dist = haversineDistance(
$last['lat'], $last['lng'],
(float)$payload['lat'], (float)$payload['lng']
);
if ($dist < FORWARD_MIN_METERS && $timeDiff < FORWARD_MAX_SECONDS) return;
}
$fwdThrottle[$driverId] = [
'ts' => $now,
'lat' => (float)$payload['lat'],
'lng' => (float)$payload['lng'],
];
$http = new AsyncHttp();
$http->request(
'http://127.0.0.1:3031',
[
'method' => 'POST',
'data' => http_build_query([
'action' => 'update_driver_location',
'passenger_id' => $passengerId,
'payload' => json_encode($payload),
]),
'headers' => [
'Content-Type' => 'application/x-www-form-urlencoded',
'x-internal-key' => $internalKey,
'Connection' => 'close',
],
'timeout' => 3,
],
null,
fn(\Exception $e) => logMsg('⚠️ Forward failed: ' . $e->getMessage())
);
}
// ============================================================
// 📲 FCM (ASYNC)
// ============================================================
function sendFCM_Async(string $token, string $title, string $body, array $rideData): void {
if (empty($token)) return;
$http = new AsyncHttp();
$http->request(
'https://api.intaleq.xyz/siro/ride/firebase/send_fcm.php',
[
'method' => 'POST',
'data' => json_encode([
'target' => $token,
'title' => $title,
'body' => $body,
'isTopic' => false,
'category' => 'Order',
'tone' => 'start',
'passengerList' => json_encode($rideData),
]),
'headers' => ['Content-Type' => 'application/json; charset=UTF-8'],
'timeout' => 5,
],
null,
fn(\Exception $e) => logMsg('⚠️ FCM failed: ' . $e->getMessage())
);
}
// ============================================================
// 🧠 Memory State & Event Buffer
// ============================================================
$connectedDrivers = [];
$active_orders_drivers = [];
$driverState = [];
$fwdThrottle = [];
$eventBuffer = []; // 🚀 Level 2: مصفوفة تجميع الأحداث لـ Redis
// ============================================================
// 🚀 Socket.IO — بورت 2020
// ============================================================
$io = new SocketIO(2020);
// ============================================================
// A. Internal HTTP Server & Redis Batch Processor (Worker Start)
// ============================================================
$io->on('workerStart', function () use ($io, $INTERNAL_KEY) {
// 🚀 1. Redis Pipeline Batch Processor (Level 2)
// يعمل كل نصف ثانية، يجمع كل الأوامر ويرسلها لـ Redis دفعة واحدة
Timer::add(REDIS_BATCH_INTERVAL, function() {
global $eventBuffer;
if (empty($eventBuffer)) return;
$redis = getRedis();
if (!$redis) return;
try {
$pipe = $redis->pipeline();
$processedCount = 0;
foreach ($eventBuffer as $driverId => $ops) {
$profileKey = "driver:profile:$driverId";
$processedCount++;
if (isset($ops['hmset'])) {
$pipe->hmset($profileKey, $ops['hmset']);
}
if (isset($ops['expire'])) {
$pipe->expire($profileKey, $ops['expire']);
}
if (isset($ops['status_change'])) {
$oldStatus = $ops['status_change']['old'];
$newStatus = $ops['status_change']['new'];
if ($oldStatus === 'on') $pipe->zrem('geo:drivers:busy', $driverId);
if ($oldStatus === 'off') $pipe->zrem('geo:drivers:available', $driverId);
if ($newStatus === 'close' || $newStatus === 'blocked') {
$pipe->zrem('geo:drivers:available', $driverId);
$pipe->zrem('geo:drivers:busy', $driverId);
}
}
if (isset($ops['geoadd'])) {
$st = $ops['geoadd']['status'];
$lng = $ops['geoadd']['lng'];
$lat = $ops['geoadd']['lat'];
if ($st === 'off') {
$pipe->geoadd('geo:drivers:available', $lng, $lat, $driverId);
} elseif ($st === 'on') {
$pipe->geoadd('geo:drivers:busy', $lng, $lat, $driverId);
}
}
}
$pipe->execute();
$eventBuffer = []; // إفراغ المصفوفة بعد التنفيذ الناجح
// logMsg("⚡ Processed Redis Batch: $processedCount drivers updated in 1 network call.");
} catch (\Exception $e) {
logMsg("⚠️ Redis Pipeline Error: " . $e->getMessage());
}
});
// 🌐 2. Internal HTTP Server — بورت 2021
$innerHttp = new Worker('http://0.0.0.0:2021');
$innerHttp->onMessage = function ($connection, $request) use ($io, $INTERNAL_KEY) {
global $active_orders_drivers, $connectedDrivers;
$headers = $request->header();
if (($headers['x-internal-key'] ?? '') !== $INTERNAL_KEY) {
$connection->send('Unauthorized');
return;
}
$post = $request->post();
$action = trim($post['action'] ?? '');
$redis = getRedis();
// ── 1. Dispatch Order ────────────────────────────────
if ($action === 'dispatch_order') {
$rideId = $post['ride_id'] ?? null;
$drivers = json_decode($post['drivers_ids'] ?? '[]', true);
$payload = $post['payload'] ?? [];
if (is_array($payload)) $payload = array_values($payload);
if ($rideId && !empty($drivers)) {
$active_orders_drivers[$rideId] = $drivers;
logMsg("🚀 Dispatch Ride #$rideId → " . count($drivers) . ' drivers.');
}
foreach ($drivers as $driverId) {
if (!isset($connectedDrivers[$driverId])) continue;
$io->to('driver_' . $driverId)->emit('new_ride_request', $payload);
$platform = $connectedDrivers[$driverId]['platform'] ?? 'android';
$token = $connectedDrivers[$driverId]['token'] ?? '';
if (!empty($token)) {
sendFCM_Async($token, 'طلب جديد', 'لديك رحلة جديدة قريبة منك', $payload);
}
}
$connection->send('Dispatched');
// ── 2. Market New Ride ────────────────────────────────
} elseif ($action === 'market_new_ride') {
$payload = $post['payload'] ?? [];
$rideId = $payload['id'] ?? null;
$lat = (float)($payload['start_lat'] ?? 0);
$lng = (float)($payload['start_lng'] ?? 0);
if (!$redis || !$rideId || $lat == 0 || $lng == 0) {
$connection->send('Error: Redis unavailable or invalid coords');
return;
}
$redis->geoadd('geo:rides:waiting', $lng, $lat, $rideId);
$nearbyDrivers = $redis->georadius('geo:drivers:available', $lng, $lat, 50, 'km');
$count = 0;
foreach ($nearbyDrivers as $driverId) {
if (isset($connectedDrivers[$driverId])) {
$io->to('driver_' . $driverId)->emit('market_new_ride', $payload);
$count++;
}
}
logMsg("📢 Market Ride #$rideId → $count drivers.");
$connection->send("Broadcasted to $count drivers");
// ── 3. Get Nearby Ride IDs ────────────────────────────
} elseif ($action === 'get_nearby_ride_ids') {
$lat = (float)($post['lat'] ?? 0);
$lng = (float)($post['lng'] ?? 0);
$radius = (float)($post['radius'] ?? 9);
if (!$redis) { $connection->send(json_encode([])); return; }
$results = $redis->georadius(
'geo:rides:waiting', $lng, $lat, $radius, 'km',
['WITHDIST' => true, 'SORT' => 'ASC', 'COUNT' => 40]
);
$connection->send(json_encode($results));
// ── 4. Ride Taken ─────────────────────────────────────
} elseif ($action === 'ride_taken_event') {
$rideId = $post['ride_id'] ?? null;
$winnerDriverId = $post['taken_by_driver_id'] ?? null;
if (!$rideId) { $connection->send('Error: Missing ride_id'); return; }
if ($redis) $redis->zrem('geo:rides:waiting', $rideId);
$io->emit('ride_taken', [
'ride_id' => $rideId,
'taken_by_driver_id' => $winnerDriverId,
]);
unset($active_orders_drivers[$rideId]);
logMsg("✅ Ride #$rideId taken by #$winnerDriverId.");
$connection->send('OK');
// ── 5. Force Disconnect ───────────────────────────────
} elseif ($action === 'force_disconnect') {
$driverId = $post['driver_id'] ?? null;
if ($driverId && isset($connectedDrivers[$driverId])) {
$connectedDrivers[$driverId]['conn']->disconnect();
unset($connectedDrivers[$driverId]);
if ($redis) {
$redis->zrem('geo:drivers:available', $driverId);
$redis->zrem('geo:drivers:busy', $driverId);
}
logMsg("🚫 Driver #$driverId force-disconnected.");
$connection->send('Disconnected');
} else {
$connection->send('Driver not connected');
}
} else {
$connection->send('Unknown action');
}
};
$innerHttp->listen();
});
// ============================================================
// B. WebSocket Events للسائقين
// ============================================================
$io->on('connection', function ($socket) use ($INTERNAL_KEY) {
global $connectedDrivers, $driverState, $fwdThrottle, $eventBuffer;
$query = $socket->handshake['query'] ?? [];
$driverId = $query['driver_id'] ?? null;
$platform = $query['platform'] ?? 'android';
$token = $query['token'] ?? '';
if (!$driverId) {
$socket->disconnect();
return;
}
$socket->join('driver_' . $driverId);
$connectedDrivers[$driverId] = [
'conn' => $socket,
'platform' => $platform,
'token' => $token,
];
if (!isset($driverState[$driverId])) {
$driverState[$driverId] = [
'lat' => 0.0,
'lng' => 0.0,
'speed' => -999.0,
'heading' => -999.0,
'status' => '',
'expire_ts' => 0,
];
}
logMsg("✅ Driver Connected: #$driverId ($platform)");
$socket->on('ping_alive', function () {
// Socket.IO handles pong automatically
});
$socket->on('update_location', function ($data)
use ($driverId, $INTERNAL_KEY, &$driverState, &$fwdThrottle, &$eventBuffer)
{
global $connectedDrivers;
$data = (array) $data;
$lat = isset($data['lat']) ? (float)$data['lat'] : null;
$lng = isset($data['lng']) ? (float)$data['lng'] : null;
$heading = (float)($data['heading'] ?? 0);
$speed = (float)($data['speed'] ?? 0);
$status = (string)($data['status'] ?? 'off');
$distance = (float)($data['distance'] ?? 0);
$passengerId = (string)($data['passenger_id'] ?? '');
$rideId = $data['ride_id'] ?? null;
if ($lat === null || $lng === null) return;
$state = &$driverState[$driverId];
$now = time();
// 1. Forward للراكب (ASYNC + throttle)
if (!empty($passengerId)) {
forwardLocationToPassengerSocket(
$driverId, $passengerId,
[
'latitude' => $lat,
'longitude' => $lng,
'heading' => $heading,
'speed' => $speed,
'ride_id' => $rideId,
'driver_id' => $driverId,
],
$INTERNAL_KEY, $fwdThrottle
);
}
// 2. حساب ماذا تغيّر لتجنب ضغط Redis
$movedMeters = ($state['lat'] == 0.0 && $state['lng'] == 0.0)
? 999.0
: haversineDistance($state['lat'], $state['lng'], $lat, $lng);
$didMove = $movedMeters >= MIN_MOVE_METERS;
$speedMs = $speed / 3.6;
$speedChanged = abs($speedMs - $state['speed']) >= HMSET_SPEED_DELTA;
$headingChanged = abs($heading - $state['heading']) >= HMSET_HEADING_DELTA;
$statusChanged = ($status !== $state['status']);
$needHmset = $speedChanged || $headingChanged || $statusChanged;
$needGeoadd = $didMove;
$needExpireRefresh = ($now - $state['expire_ts']) >= EXPIRE_REFRESH_SECONDS;
if (!$needHmset && (!$needGeoadd && !$statusChanged) && !$needExpireRefresh) {
return; // لم يتغير شيء مهم، تجاهل تماماً (0 عمليات Redis)
}
// 🚀 3. Buffering Event بدل الإرسال المباشر لـ Redis (Level 2 Magic)
if (!isset($eventBuffer[$driverId])) {
$eventBuffer[$driverId] = [];
}
if ($needHmset) {
$eventBuffer[$driverId]['hmset'] = [
'id' => $driverId, 'heading' => $heading, 'speed' => $speed, 'status' => $status, 'updated_at' => $now
];
$state['speed'] = $speedMs;
$state['heading'] = $heading;
}
if ($needExpireRefresh || $needHmset) {
$eventBuffer[$driverId]['expire'] = 900;
$state['expire_ts'] = $now;
}
if ($statusChanged) {
$eventBuffer[$driverId]['status_change'] = [
'old' => $state['status'],
'new' => $status
];
$state['status'] = $status;
// Auto disconnect if blocked
if ($status === 'blocked') {
if (isset($connectedDrivers[$driverId])) {
$connectedDrivers[$driverId]['conn']->disconnect();
unset($connectedDrivers[$driverId]);
}
}
}
if ($needGeoadd || $statusChanged) {
$eventBuffer[$driverId]['geoadd'] = [
'status' => $status,
'lng' => $lng,
'lat' => $lat
];
if ($needGeoadd) {
$state['lat'] = $lat;
$state['lng'] = $lng;
}
}
});
$socket->on('disconnect', function () use ($driverId) {
global $connectedDrivers, $driverState, $fwdThrottle;
unset($connectedDrivers[$driverId]);
unset($driverState[$driverId]);
unset($fwdThrottle[$driverId]);
logMsg("❌ Driver Disconnected: #$driverId");
});
});
Worker::runAll();
+4 -1
View File
@@ -79,7 +79,10 @@ function sendToLocationServer($action, $data) {
function findBestDrivers($con, $lat, $lng, $carType, $endLat = null, $endLng = null) { function findBestDrivers($con, $lat, $lng, $carType, $endLat = null, $endLng = null) {
// 1. الاتصال بـ Redis لجلب الأقرب // 1. الاتصال بـ Redis لجلب الأقرب
$locationServerUrl = "https://location.intaleq.xyz/api_get_nearby.php"; // 🔥 [Fix Hardcoded URL] كان مثبتاً على رابط الإنتاج مباشرة بخلاف كل استدعاء
// آخر لسيرفر اللوكيشن في هذا الملف (يعتمد على env)، فأي بيئة غير إنتاج كانت
// تضرب سيرفر الإنتاج الحقيقي بالخطأ.
$locationServerUrl = getenv('LOCATION_API_URL') ?: "https://location.intaleq.xyz/api_get_nearby.php";
$INTERNAL_KEY = function_exists('getInternalSocketKey') ? getInternalSocketKey() : ''; $INTERNAL_KEY = function_exists('getInternalSocketKey') ? getInternalSocketKey() : '';
$postData = ['lat' => $lat, 'lng' => $lng, 'radius' => 5, 'limit' => 100]; $postData = ['lat' => $lat, 'lng' => $lng, 'radius' => 5, 'limit' => 100];
+24 -23
View File
@@ -77,31 +77,32 @@ try {
} }
// ================================================================= // =================================================================
// 2. إشعار السائق (Socket + FCM) // 2. إشعار السائق/السائقين (Socket + FCM)
// ================================================================= // =================================================================
// 🔥 يُرسل دائماً بغض النظر عن driver_id — إذا كانت الرحلة لم تُقبل بعد
// (driver_id = 0)، لوكيشن سيرفر يستخدم ride:offered_drivers:{rideId}
// من Redis لإشعار كل السائقين الذين وصلهم عرض هذه الرحلة أصلاً.
$socketUrl = getenv('LOCATION_SERVER_URL') ?: 'http://location.intaleq.xyz:2021';
$internalKeyPath = getenv('INTERNAL_SOCKET_KEY_PATH') ?: '';
$internalKey = ($internalKeyPath && file_exists($internalKeyPath)) ? trim(file_get_contents($internalKeyPath)) : (getenv('INTERNAL_SOCKET_KEY') ?: '');
$ch = curl_init($socketUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
'action' => 'cancel_ride',
'driver_id' => $driverId,
'ride_id' => $rideId,
'reason' => $reason
]));
if (!empty($internalKey)) curl_setopt($ch, CURLOPT_HTTPHEADER, ["x-internal-key: $internalKey"]);
curl_setopt($ch, CURLOPT_TIMEOUT_MS, 500);
curl_setopt($ch, CURLOPT_NOSIGNAL, 1);
@curl_exec($ch);
curl_close($ch);
if ($driverId > 0) { if ($driverId > 0) {
// FCM للسائق الذي قَبِل الرحلة فعلاً (باستخدام الدالة الجديدة مع فك التشفير)
// أ) Socket (إشعار السائق في التطبيق فوراً)
$socketUrl = getenv('LOCATION_SERVER_URL') ?: 'http://location.intaleq.xyz:2021';
$internalKeyPath = getenv('INTERNAL_SOCKET_KEY_PATH') ?: '';
$internalKey = ($internalKeyPath && file_exists($internalKeyPath)) ? trim(file_get_contents($internalKeyPath)) : (getenv('INTERNAL_SOCKET_KEY') ?: '');
$ch = curl_init($socketUrl);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query([
'action' => 'cancel_ride',
'driver_id' => $driverId,
'ride_id' => $rideId,
'reason' => $reason
]));
if (!empty($internalKey)) curl_setopt($ch, CURLOPT_HTTPHEADER, ["x-internal-key: $internalKey"]);
curl_setopt($ch, CURLOPT_TIMEOUT_MS, 500);
curl_setopt($ch, CURLOPT_NOSIGNAL, 1);
@curl_exec($ch);
curl_close($ch);
// ب) FCM (باستخدام الدالة الجديدة مع فك التشفير)
$driverToken = filterRequest("driver_token"); $driverToken = filterRequest("driver_token");
if (empty($driverToken)) { if (empty($driverToken)) {
+70 -39
View File
@@ -1,53 +1,85 @@
<?php <?php
// cron_cleanup_waiting.php // cron_ride_timeout.php
// مسار الاتصال بقاعدة البيانات // 🔥 أُعيدت كتابته بالكامل: كان يعتمد على جدول waitingRides الذي لم يعد
require_once __DIR__ . '/../../get_connect.php'; // يُملأ من تدفق add_ride.php الحقيقي (addWaitingRide.php لا يُستدعى من هناك)،
// فكانت الرحلات العالقة بحالة 'waiting' لا تُصفَّر أبداً. الآن يعمل مباشرة
// على جدول ride باستخدام أعمدة date/time المكتوبة فعلياً عند الإنشاء.
require_once __DIR__ . '/../../core/bootstrap.php';
require_once __DIR__ . '/../../functions.php';
// تسجيل في اللوج لبداية العملية
error_log("⏰ [Cleanup Cron] Started cleaning old waiting rides..."); error_log("⏰ [Cleanup Cron] Started cleaning old waiting rides...");
$minutesLimit = 15;
try { try {
// المدة المسموحة بالدقائق $con = Database::get('main');
$minutesLimit = 15;
// ========================================================= // =========================================================
// الخطوة 1: تحديث الحالة في الجدول الرئيسي (الأرشيف) // الخطوة 1: جلب الرحلات العالقة (status='waiting' منذ أكثر من 15 دقيقة)
// ========================================================= // =========================================================
// نقوم بتحديث حالة الرحلات التي ستُحذف ليعلم الراكب أنها انتهت (Time Out) $sqlSelect = "SELECT id, passenger_id FROM ride
// بدلاً من أن تبقى 'waiting' للأبد في سجلات الراكب WHERE status = 'waiting'
AND TIMESTAMP(`date`, `time`) < DATE_SUB(NOW(), INTERVAL $minutesLimit MINUTE)";
$sqlUpdate = "UPDATE ride $stmtSelect = $con->prepare($sqlSelect);
SET status = 'timeout' $stmtSelect->execute();
WHERE id IN ( $staleRides = $stmtSelect->fetchAll(PDO::FETCH_ASSOC);
SELECT id FROM waitingRides
WHERE created_at < DATE_SUB(NOW(), INTERVAL $minutesLimit MINUTE)
) AND status = 'waiting'"; // نتأكد أننا نحدث ما هو معلق فقط
$stmtUpdate = $con->prepare($sqlUpdate); $updatedCount = 0;
$stmtUpdate->execute();
$updatedCount = $stmtUpdate->rowCount(); if (!empty($staleRides)) {
$sqlUpdate = "UPDATE ride SET status = 'timeout', updated_at = NOW() WHERE id = ? AND status = 'waiting'";
$stmtUpdate = $con->prepare($sqlUpdate);
// نسخة أرشيفية (best-effort)
$con_ride = null;
try {
$con_ride = Database::get('ride');
} catch (Exception $e) {
error_log("[cron_ride_timeout] Secondary ride DB unavailable: " . $e->getMessage());
}
$stmtUpdate2 = $con_ride ? $con_ride->prepare(
"UPDATE ride SET status = 'timeout', updated_at = NOW() WHERE id = ? AND status = 'waiting'"
) : null;
foreach ($staleRides as $ride) {
$rideId = $ride['id'];
$stmtUpdate->execute([$rideId]);
$updatedCount += $stmtUpdate->rowCount();
if ($stmtUpdate2) {
try {
$stmtUpdate2->execute([$rideId]);
} catch (PDOException $e) {
error_log("[cron_ride_timeout] Secondary DB update failed for #$rideId: " . $e->getMessage());
}
}
// 🆕 تنظيف Redis + إعلام أي سائقين ما زالوا يرون هذا الطلب على أنه ملغى
sendToLocationServer('cancel_ride', [
'ride_id' => $rideId,
'driver_id' => 0,
'reason' => 'timeout',
]);
}
}
// ========================================================= // =========================================================
// الخطوة 2: الحذف من جدول الانتظار (تنظيف Hot Data) // الخطوة 2: تنظيف جدول waitingRides القديم (إن وُجدت صفوف فيه من مسار قديم)
// ========================================================= // =========================================================
$deletedCount = 0;
$sqlDelete = "DELETE FROM waitingRides try {
WHERE created_at < DATE_SUB(NOW(), INTERVAL $minutesLimit MINUTE)"; $sqlDelete = "DELETE FROM waitingRides WHERE created_at < DATE_SUB(NOW(), INTERVAL $minutesLimit MINUTE)";
$stmtDelete = $con->prepare($sqlDelete);
$stmtDelete->execute();
$deletedCount = $stmtDelete->rowCount();
} catch (PDOException $e) {
error_log("[cron_ride_timeout] waitingRides cleanup skipped: " . $e->getMessage());
}
$stmtDelete = $con->prepare($sqlDelete); if ($updatedCount > 0 || $deletedCount > 0) {
$stmtDelete->execute(); $msg = "✅ [Cleanup Cron] Success: Timed out $updatedCount ride(s) in ride table, and cleaned $deletedCount legacy waitingRides row(s).";
$deletedCount = $stmtDelete->rowCount();
// =========================================================
// الخطوة 3: (اختياري) تنظيف الريدز
// =========================================================
// بما أنك تستخدم Redis، المفترض أن تحذفها منه أيضاً.
// لكن بما أن الريدز يعتمد على TTL (Expire) أو سيتم تحديثه عند الطلب القادم،
// فالحذف من الـ MySQL يكفي لأن getRideWaiting سيفحص MySQL ولن يجدها.
// تقرير العملية
if ($deletedCount > 0) {
$msg = "✅ [Cleanup Cron] Success: Timed out $updatedCount rides in Main DB, and Deleted $deletedCount rides from Waiting DB.";
error_log($msg); error_log($msg);
echo json_encode(["status" => "success", "message" => $msg]); echo json_encode(["status" => "success", "message" => $msg]);
} else { } else {
@@ -59,7 +91,6 @@ try {
} catch (PDOException $e) { } catch (PDOException $e) {
$errorMsg = "❌ [Cleanup Cron] Error: " . $e->getMessage(); $errorMsg = "❌ [Cleanup Cron] Error: " . $e->getMessage();
error_log($errorMsg); error_log($errorMsg);
error_log("[cron_ride_timeout] Error: " . $e->getMessage());
echo json_encode(["status" => "failure", "message" => "An internal error occurred."]); echo json_encode(["status" => "failure", "message" => "An internal error occurred."]);
} }
?> ?>
@@ -0,0 +1,89 @@
<div dir="rtl" align="right">
<h1>استراتيجيات النمو والتسويق (Growth & Marketing Strategies)</h1>
<h2>1. إعادة تأطير المشكلة: السيولة والثقة</h2>
المشكلة الحقيقية في التطبيقات الجديدة ليست "الوعي بالعلامة التجارية" ولا كثرة المنشورات الإعلانية، بل هي <b>السيولة والثقة</b>:
<br>
- <b>الركاب:</b> يحتاجون لسيارة قريبة ومتاحة فوراً. السعر الأرخص لا قيمة له إذا لم يجد الراكب سيارة عند فتح التطبيق.
<br>
- <b>السائقون:</b> يحتاجون لطلبات مستمرة. إذا لم يجدوا طلبات، سيطفئون التطبيق ويعودون للمنافسين.
<br>
لذلك، الهدف ليس الانتشار الجغرافي الواسع في البداية، بل <b>الكثافة العالية في منطقة محددة ووقت محدد</b>.
<hr>
<h2>2. القاعدة الذهبية: كثافة لا انتشار</h2>
بدلاً من محاولة تغطية مدينة كاملة بميزانية محدودة:
<br>
- <b>السيطرة المحلية:</b> اختر منطقة واحدة (مثل محيط جامعة، أو حي مزدحم) وركز كل السائقين والعروض فيها حتى يصبح وقت الوصول أقل من 4 دقائق.
<br>
- الكثافة العالية تؤدي إلى: وصول سريع ← تجربة ممتازة للراكب ← حديث الناس الإيجابي بين بعضهم ← نمو مجاني.
<br>
- التوسع يتم تدريجياً لحي مجاور بعد السيطرة التامة على الحي الأول. الانتشار المبكر قاتل للميزانيات المحدودة.
<hr>
<h2>3. السائقون أولاً: توفير العرض هو الأساس</h2>
الركاب يأتون تلقائياً إذا توفرت السيارات. يجب التركيز على:
<br>
- <b>الاستغلال العالي:</b> كلما زادت رحلات السائق في الساعة الواحدة، يمكنك خفض السعر للراكب مع زيادة دخل السائق الإجمالي في نفس الوقت.
<br>
- <b>تلميح الأرباح:</b> إبراز الأرباح الإضافية التي سيجنيها السائق في كل رحلة مقارنة بالتطبيقات الأخرى بوضوح.
<br>
- <b>رسالة التوظيف:</b> التركيز على "العمولة الأقل" والدفع السريع والمباشر.
<br>
- <b>الدخل المضمون:</b> توفير أوقات دخل مضمون خلال الساعات الميتة لإبقاء السائقين متصلين ومتاحين.
<br>
- <b>مجتمع السائقين:</b> إنشاء مجموعات حقيقية للسائقين (واتساب/تيليجرام) للاستماع لهم وحل مشاكلهم بسرعة، وتفعيل نظام إحالة السائقين لجلب زملائهم.
<br>
- <b>أيام بدون عمولة:</b> تخصيص أهدأ يوم في الأسبوع ليكون بدون عمولة تماماً لتشجيع توفر السيارات بكثافة.
<hr>
<h2>4. الركاب: حلقات النمو الفعالة</h2>
- <b>إحالة ثنائية الجانب:</b> مكافأة كلا الطرفين (الداعي والمدعو) برصيد رحلات، وتفعيل المكافأة فقط <b>بعد اكتمال أول رحلة</b> لتجنب التلاعب والغش. البنية التقنية لذلك جاهزة لديك.
<br>
- <b>الاحتفاظ بالعملاء (الولاء):</b> خصم كبير على أول رحلة، ثم إشعار تذكير للرحلة الثانية، وتفعيل نظام التلعيب والولاء للمحافظة على الراكب.
<br>
- <b>استهداف المناسبات:</b> التركيز على حملات دقيقة بأوقات مدروسة (رمضان وقت الإفطار، الأعياد، بداية الفصل الجامعي). هذه الحملات مردودها أعلى بكثير من الإعلان العام.
<br>
- <b>شراكات محلية:</b> التعاون مع المقاهي والمطاعم والجامعات، واعتبارها "نقاط التقاط" مدعومة بخصومات، بالإضافة للتعاون مع المؤثرين الصغار محلياً.
<hr>
<h2>5. الثقة كسلاح ضد المنافسين القدامى</h2>
لا تنافس الشركات العملاقة على "السمعة التاريخية"، بل ركز على <b>نقاط ضعفها</b>:
<br>
- <b>السعر الثابت والشفاف:</b> تقديم وعْد للعملاء بأنه "لا مفاجآت في السعر" (استقرار الأسعار)، على عكس تقلبات الأسعار والقفزات المزعجة عند المنافسين.
<br>
- <b>الأمان المطلق:</b> إبراز ميزات هامة للثقة، خاصة للنساء (مشاركة الرحلة، زر الطوارئ، خيار تفضيل سائقة، سائقون موثقون).
<br>
- <b>الشفافية المؤسسية:</b> فيديوهات حقيقية من مؤسس التطبيق يشرح فيها بشفافية كيف يتم تسعير الرحلات ولماذا التطبيق أرخص ومربح للسائق. الصدق هو أفضل وأقوى محتوى مقنع.
<br>
- <b>الدفع النقدي السلس:</b> تسهيل التعامل بالكاش بدون تعقيدات، حيث يعتبر الكاش الملك والمفضل في العديد من أسواقنا.
<hr>
<h2>6. استراتيجية وسائل التواصل الاجتماعي السليمة</h2>
- <b>المحتوى الحقيقي:</b> قصص النجاح الواقعية وردود وتجارب الركاب الفعليين هي الذهب الحقيقي الذي يجب الاستثمار فيه.
<br>
- <b>الابتعاد عن التفاعلات الوهمية والدراما المفبركة:</b>
<br>&nbsp;&nbsp;1. المحتوى المزور يعرض حسابات التطبيق للحظر الجماعي من قبل المنصات.
<br>&nbsp;&nbsp;2. إذا تم كشفها، سيكون الضرر على الثقة كارثياً ولا يمكن إصلاحه بسهولة، وسيستغلها المنافسون ضدك.
<br>&nbsp;&nbsp;3. الإعجابات الوهمية لا تزيد من "السيولة" ولا تضع سيارة حقيقية قريبة من الراكب.
<br>
- <b>البديل البناء:</b> توجيه جهود الأتمتة لإدارة مجتمع حقيقية، الرد السريع على الاستفسارات، وتسليط الضوء على آراء المستخدمين الحقيقية الموثقة بإذنهم.
<hr>
<h2>7. إشعار تحديث الأسعار الذكي للإدارة</h2>
فكرة ممتازة تشغيلياً، ويجب أن تكون مبنية على "أحداث هامة" وليس إشعارات مستمرة لكي لا يتم تجاهلها:
<br>
- لا يتم الإرسال مع كل تحديث بسيط (ضجيج).
<br>
- <b>متى يتم الإرسال؟</b> فقط عند تجاوز التغير نسبة مئوية معينة، أو عند تفعيل نافذة تسعير موسمي أو اكتشاف ذروة جديدة.
<br>
- <b>محتوى الإشعار:</b> الدولة/المنطقة، المحرك المسؤول عن التغيير (اكتشاف ذروة / موسمي / تعديل ثبات)، السعر قبل وبعد، والسبب.
<br>
- <b>الخلاصة اليومية:</b> تقديم إشعار تجميعي بنهاية اليوم للإدارة يلخص أهم تحركات السوق.
</div>
+43
View File
@@ -440,6 +440,49 @@ $io->on('workerStart', function () use ($io, $INTERNAL_KEY) {
logMsg("✅ Ride #$rideId taken by #$winnerDriverId."); logMsg("✅ Ride #$rideId taken by #$winnerDriverId.");
$connection->send('OK'); $connection->send('OK');
// ── 4b. Ride Cancelled (بالراكب أو بالسائق) ─────────────
} elseif ($action === 'cancel_ride') {
$rideId = $post['ride_id'] ?? null;
$driverId = $post['driver_id'] ?? null;
$reason = $post['reason'] ?? '';
if (!$rideId) { $connection->send('Error: Missing ride_id'); return; }
// كل السائقين الذين وصلهم عرض هذه الرحلة (سواء قَبِلها أحد أم لا)
$offeredDrivers = [];
if ($redis) {
$redis->zrem('geo:rides:waiting', $rideId);
$offeredSetKey = "ride:offered_drivers:$rideId";
$offeredDrivers = $redis->smembers($offeredSetKey);
foreach ($offeredDrivers as $dId) {
$redis->zrem("driver:pending_orders:$dId", $rideId);
}
$redis->del($offeredSetKey);
$redis->del("ride:offer:$rideId");
}
// نوحّد قائمة من يجب إشعارهم: السائق الحالي (إن وُجد) + كل من عُرضت عليهم الرحلة
$notifyDrivers = $offeredDrivers;
if ($driverId) $notifyDrivers[] = (string)$driverId;
$notifyDrivers = array_unique(array_filter($notifyDrivers, fn($d) => $d && $d !== '0'));
$notified = 0;
foreach ($notifyDrivers as $dId) {
if (isset($connectedDrivers[$dId])) {
$io->to('driver_' . $dId)->emit('ride_cancelled', [
'ride_id' => $rideId,
'reason' => $reason,
]);
$notified++;
}
}
unset($active_orders_drivers[$rideId]);
logMsg("🚫 Ride #$rideId cancelled — notified $notified/" . count($notifyDrivers) . " driver(s).");
$connection->send('OK');
// ── 7. Update Ride State (Redis Cache فقط — بدون Forward) // ── 7. Update Ride State (Redis Cache فقط — بدون Forward)
} elseif ($action === 'update_ride_state') { } elseif ($action === 'update_ride_state') {
$rideId = $post['ride_id'] ?? null; $rideId = $post['ride_id'] ?? null;
@@ -2145,6 +2145,10 @@ class MapDriverController extends GetxController
// تحديث فقط الجزء الخاص بمعلومات الراكب // تحديث فقط الجزء الخاص بمعلومات الراكب
update(); update();
// 🔥 [Fix Polyline] مسح المسار الأصفر (سائق→راكب) قبل رسم الأزرق (راكب→وجهة)
// نفس علة إعادة استخدام نفس المعرف بلون جديد الموجودة في انتقال بدء الرحلة أعلاه
clearPolyline();
// رسم المسار للوجهة // رسم المسار للوجهة
getRoute( getRoute(
origin: latLngPassengerLocation, origin: latLngPassengerLocation,
@@ -63,6 +63,9 @@ class RideLifecycleController extends GetxController {
// --- Missing variables from monolithic controller --- // --- Missing variables from monolithic controller ---
String currentRideId = ''; String currentRideId = '';
bool isDrawingRoute = false; bool isDrawingRoute = false;
// 🔥 [Fix Race] يمنع استجابة HTTP قديمة (لوجهة سابقة) من الكتابة فوق
// نتيجة أحدث عند تبديل الوجهة بسرعة داخل getDirectionMap.
int _routeRequestGeneration = 0;
bool isAnotherOreder = false; bool isAnotherOreder = false;
bool isWhatsAppOrder = false; bool isWhatsAppOrder = false;
LatLng startLocation = const LatLng(32, 35); LatLng startLocation = const LatLng(32, 35);
@@ -670,6 +673,7 @@ class RideLifecycleController extends GetxController {
if (rideStatusFromStartApp['data'] == null) { if (rideStatusFromStartApp['data'] == null) {
_isReviewProcessed = false; _isReviewProcessed = false;
restCounter();
currentRideState.value = RideState.noRide; currentRideState.value = RideState.noRide;
startMasterTimer(); startMasterTimer();
return; return;
@@ -700,6 +704,9 @@ class RideLifecycleController extends GetxController {
currentRideState.value = RideState.noRide; currentRideState.value = RideState.noRide;
startMasterTimer(); startMasterTimer();
} else { } else {
// 🔥 [Fix Polyline] لا حاجة للتقييم — لازم نمسح مسار الرحلة المنتهية
// قبل العودة لحالة noRide، وإلا يبقى ظاهراً على الخريطة الخاملة
restCounter();
currentRideState.value = RideState.noRide; currentRideState.value = RideState.noRide;
startMasterTimer(); startMasterTimer();
} }
@@ -3508,7 +3515,15 @@ class RideLifecycleController extends GetxController {
} }
Future<void> getDirectionMap(String origin, String destination, Future<void> getDirectionMap(String origin, String destination,
[List<String> waypoints = const [], int attemptCount = 0]) async { [List<String> waypoints = const [],
int attemptCount = 0,
int? gen]) async {
// 🔥 [Fix Race] كل طلب جديد (attemptCount == 0) يفتح "جيلاً" جديداً؛
// إعادة المحاولات (retries) تتبع نفس الجيل. أي استجابة تصل بعد أن بدأ
// جيل أحدث (تبديل وجهة سريع) تُهمَل ولا تكتب فوق الحالة الحالية.
final int myGeneration =
attemptCount == 0 ? (++_routeRequestGeneration) : (gen ?? _routeRequestGeneration);
if (attemptCount == 0) { if (attemptCount == 0) {
isDrawingRoute = true; isDrawingRoute = true;
update(); update();
@@ -3563,7 +3578,8 @@ class RideLifecycleController extends GetxController {
if (!isRequestValid) { if (!isRequestValid) {
if (attemptCount < 2) { if (attemptCount < 2) {
await _retryProcess(origin, destination, waypoints, attemptCount); await _retryProcess(
origin, destination, waypoints, attemptCount, myGeneration);
return; return;
} }
_handleFatalError( _handleFatalError(
@@ -3571,6 +3587,13 @@ class RideLifecycleController extends GetxController {
return; return;
} }
// 🔥 [Fix Race] وجهة أحدث بدأت طلبها الخاص أثناء انتظار هذا الرد — تجاهل
if (myGeneration != _routeRequestGeneration) {
Log.print(
'🚫 Stale route response ignored (gen $myGeneration != current $_routeRequestGeneration)');
return;
}
double apiDistanceMeters; double apiDistanceMeters;
String pointsString; String pointsString;
dynamic routeData; dynamic routeData;
@@ -3593,8 +3616,8 @@ class RideLifecycleController extends GetxController {
if (attemptCount < 2) { if (attemptCount < 2) {
Log.print("🔄 Retrying request (Attempt ${attemptCount + 2})..."); Log.print("🔄 Retrying request (Attempt ${attemptCount + 2})...");
await Future.delayed(const Duration(seconds: 1)); await Future.delayed(const Duration(seconds: 1));
await getDirectionMap( await getDirectionMap(origin, destination, waypoints,
origin, destination, waypoints, attemptCount + 1); attemptCount + 1, myGeneration);
return; return;
} else { } else {
Log.print("❌ All retries failed. Calculating Route is impossible."); Log.print("❌ All retries failed. Calculating Route is impossible.");
@@ -3628,6 +3651,14 @@ class RideLifecycleController extends GetxController {
return; return;
} }
// 🔥 [Fix Race] فحص ثانٍ بعد فك التشفير في isolate منفصل (compute) —
// قد يكون طلب أحدث بدأ وانتهى خلال هذه الفترة
if (myGeneration != _routeRequestGeneration) {
Log.print(
'🚫 Stale route response ignored after decode (gen $myGeneration != current $_routeRequestGeneration)');
return;
}
mapEngine.polylineCoordinates.clear(); mapEngine.polylineCoordinates.clear();
mapEngine.polylineCoordinates.addAll(decodedPoints); mapEngine.polylineCoordinates.addAll(decodedPoints);
@@ -3758,7 +3789,8 @@ class RideLifecycleController extends GetxController {
Log.print('🚨 STACKTRACE: $stackTrace'); Log.print('🚨 STACKTRACE: $stackTrace');
if (attemptCount < 2) { if (attemptCount < 2) {
await _retryProcess(origin, destination, waypoints, attemptCount); await _retryProcess(
origin, destination, waypoints, attemptCount, myGeneration);
} else { } else {
_handleFatalError("Connection Error".tr, _handleFatalError("Connection Error".tr,
"Please check your internet and try again.".tr); "Please check your internet and try again.".tr);
@@ -3767,11 +3799,11 @@ class RideLifecycleController extends GetxController {
} }
Future<void> _retryProcess(String origin, String dest, List<String> waypoints, Future<void> _retryProcess(String origin, String dest, List<String> waypoints,
int currentAttempt) async { int currentAttempt, [int? gen]) async {
Log.print( Log.print(
"🔄 Exception or Error caught. Retrying in 1s... (Attempt ${currentAttempt + 1})"); "🔄 Exception or Error caught. Retrying in 1s... (Attempt ${currentAttempt + 1})");
await Future.delayed(const Duration(seconds: 1)); await Future.delayed(const Duration(seconds: 1));
getDirectionMap(origin, dest, waypoints, currentAttempt + 1); getDirectionMap(origin, dest, waypoints, currentAttempt + 1, gen);
} }
bool _isUsingFallback = false; bool _isUsingFallback = false;