diff --git a/.DS_Store b/.DS_Store index b8825cc6..2add2417 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/backend/auth/Tester/create_tester_driver.php b/backend/auth/Tester/create_tester_driver.php deleted file mode 100644 index 50a5b00b..00000000 --- a/backend/auth/Tester/create_tester_driver.php +++ /dev/null @@ -1,89 +0,0 @@ -encryptData($email); -$encryptedPhone = $encryptionHelper->encryptData($phone); -$encryptedFirstName = $encryptionHelper->encryptData('Driver'); -$encryptedLastName = $encryptionHelper->encryptData('Tester'); -$encryptedGender = $encryptionHelper->encryptData('Male'); -$encryptedBirthdate = $encryptionHelper->encryptData('1990-01-01'); -$encryptedSite = $encryptionHelper->encryptData('Jordan'); - -try { - $con = Database::get('main'); - - // 1. Check if driver exists - $stmt = $con->prepare("SELECT id FROM driver WHERE email = :email LIMIT 1"); - $stmt->bindParam(':email', $encryptedEmail); - $stmt->execute(); - $driver = $stmt->fetch(PDO::FETCH_ASSOC); - - if ($driver) { - $driverId = $driver['id']; - $update = $con->prepare("UPDATE driver SET password = :password, phone = :phone WHERE id = :id"); - $update->bindParam(':password', $hashedPassword); - $update->bindParam(':phone', $encryptedPhone); - $update->bindParam(':id', $driverId); - $update->execute(); - echo "Driver tester updated successfully.\n"; - } else { - $driverId = bin2hex(random_bytes(10)); // 20 chars unique id - $insert = $con->prepare("INSERT INTO driver (id, phone, email, password, gender, birthdate, site, first_name, last_name) - VALUES (:id, :phone, :email, :password, :gender, :birthdate, :site, :first_name, :last_name)"); - $insert->bindParam(':id', $driverId); - $insert->bindParam(':phone', $encryptedPhone); - $insert->bindParam(':email', $encryptedEmail); - $insert->bindParam(':password', $hashedPassword); - $insert->bindParam(':gender', $encryptedGender); - $insert->bindParam(':birthdate', $encryptedBirthdate); - $insert->bindParam(':site', $encryptedSite); - $insert->bindParam(':first_name', $encryptedFirstName); - $insert->bindParam(':last_name', $encryptedLastName); - $insert->execute(); - echo "Driver tester created successfully with ID: $driverId\n"; - } - - // 2. Ensure phone_verification row exists - $stmtPhone = $con->prepare("SELECT * FROM phone_verification WHERE phone_number = :phone LIMIT 1"); - $stmtPhone->bindParam(':phone', $encryptedPhone); - $stmtPhone->execute(); - if ($stmtPhone->fetch()) { - $updatePhone = $con->prepare("UPDATE phone_verification SET is_verified = 1 WHERE phone_number = :phone"); - $updatePhone->bindParam(':phone', $encryptedPhone); - $updatePhone->execute(); - } else { - $insertPhone = $con->prepare("INSERT INTO phone_verification (phone_number, is_verified) VALUES (:phone, 1)"); - $insertPhone->bindParam(':phone', $encryptedPhone); - $insertPhone->execute(); - } - - // 3. Ensure CarRegistration row exists - $stmtCar = $con->prepare("SELECT * FROM CarRegistration WHERE driverID = :driverID LIMIT 1"); - $stmtCar->bindParam(':driverID', $driverId); - $stmtCar->execute(); - if ($stmtCar->fetch()) { - $updateCar = $con->prepare("UPDATE CarRegistration SET make = 'Toyota', model = 'Prius', year = '2020' WHERE driverID = :driverID"); - $updateCar->bindParam(':driverID', $driverId); - $updateCar->execute(); - } else { - $insertCar = $con->prepare("INSERT INTO CarRegistration (driverID, make, model, year) VALUES (:driverID, 'Toyota', 'Prius', '2020')"); - $insertCar->bindParam(':driverID', $driverId); - $insertCar->execute(); - } - - echo "Verification and Car Registration configured.\n"; - -} catch (Exception $e) { - echo "Error: " . $e->getMessage() . "\n"; -} -?> diff --git a/backend/auth/Tester/getTesterApp.php b/backend/auth/Tester/getTesterApp.php deleted file mode 100644 index 625f035a..00000000 --- a/backend/auth/Tester/getTesterApp.php +++ /dev/null @@ -1,29 +0,0 @@ -prepare($sql); -$stmt->execute(); -$result = $stmt->fetchAll(PDO::FETCH_ASSOC); - -if ($stmt->rowCount() > 0) { - // Print the retrieved data - // echo json_encode($result); - jsonSuccess($data = $result); -} else { - // Print a failure message - - jsonError($message = "No driver order data found"); -} - -?> \ No newline at end of file diff --git a/backend/auth/Tester/updateTesterApp.php b/backend/auth/Tester/updateTesterApp.php deleted file mode 100644 index b7b98c25..00000000 --- a/backend/auth/Tester/updateTesterApp.php +++ /dev/null @@ -1,23 +0,0 @@ -prepare($sql); -$stmt->execute(); - -if ($stmt->rowCount() > 0) { - // Print a success message - jsonSuccess($message = "Test data updated successfully"); -} else { - // Print a failure message - jsonError($message = "Failed to update driver order data"); -} -?> \ No newline at end of file diff --git a/backend/marketing_engine/schema.sql b/backend/marketing_engine/schema.sql deleted file mode 100644 index a798c579..00000000 --- a/backend/marketing_engine/schema.sql +++ /dev/null @@ -1,82 +0,0 @@ --- ========================================================== --- Marketing Engine & Social Media Bot Combined Schema --- ========================================================== - --- 1. Original Social Media Tables (Facebook, Instagram) -CREATE TABLE IF NOT EXISTS `social_accounts` ( - `id` INT AUTO_INCREMENT PRIMARY KEY, - `platform` ENUM('facebook', 'instagram', 'tiktok', 'twitter', 'youtube') NOT NULL, - `username` VARCHAR(100) NOT NULL, - `status` ENUM('active', 'restricted', 'banned') DEFAULT 'active', - `total_posts` INT DEFAULT 0, - `total_comments` INT DEFAULT 0, - `total_videos` INT DEFAULT 0, - `proxy_ip` VARCHAR(100) NULL, - `proxy_port` INT NULL, - `proxy_username` VARCHAR(100) NULL, - `proxy_password` VARCHAR(100) NULL, - `last_active` DATETIME NULL, - `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, - `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -); - -CREATE TABLE IF NOT EXISTS `social_tasks` ( - `id` INT AUTO_INCREMENT PRIMARY KEY, - `account_id` INT NULL, - `platform` ENUM('facebook', 'instagram', 'tiktok', 'twitter', 'youtube') NOT NULL, - `type` ENUM('join_group', 'read_posts', 'post_comment', 'share_link', 'upload_video', 'post_tweet') NOT NULL, - `target_url` VARCHAR(500) NULL, -- URL of the group, post, or media download - `prompt_context` TEXT NULL, - `generated_comment` TEXT NULL, - `status` ENUM('pending', 'in_progress', 'completed', 'failed') DEFAULT 'pending', - `error_message` TEXT NULL, - `scheduled_at` DATETIME NULL, - `completed_at` DATETIME NULL, - `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (`account_id`) REFERENCES `social_accounts`(`id`) ON DELETE SET NULL -); - -CREATE TABLE IF NOT EXISTS `social_logs` ( - `id` INT AUTO_INCREMENT PRIMARY KEY, - `task_id` INT NULL, - `account_id` INT NULL, - `log_level` ENUM('info', 'warning', 'error') DEFAULT 'info', - `message` TEXT NOT NULL, - `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, - FOREIGN KEY (`task_id`) REFERENCES `social_tasks`(`id`) ON DELETE SET NULL, - FOREIGN KEY (`account_id`) REFERENCES `social_accounts`(`id`) ON DELETE SET NULL -); - --- 2. New Content Generation Pipeline Tables -CREATE TABLE IF NOT EXISTS `content_pipeline` ( - `id` INT AUTO_INCREMENT PRIMARY KEY, - `topic` VARCHAR(255) NOT NULL, - `script_text` TEXT NULL, - `voice_url` VARCHAR(500) NULL, - `video_url` VARCHAR(500) NULL, - `status` ENUM('pending', 'script_generated', 'voice_generated', 'video_rendered', 'published', 'failed') DEFAULT 'pending', - `error_message` TEXT NULL, - `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP, - `updated_at` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -); - -CREATE TABLE IF NOT EXISTS `api_quotas` ( - `id` INT AUTO_INCREMENT PRIMARY KEY, - `service_name` ENUM('gemini', 'elevenlabs', 'creatomate', 'heygen') NOT NULL, - `daily_usage` INT DEFAULT 0, - `quota_limit` INT DEFAULT 10, - `last_reset` DATE NOT NULL -); - -INSERT IGNORE INTO `api_quotas` (`service_name`, `daily_usage`, `quota_limit`, `last_reset`) VALUES -('gemini', 0, 100, CURDATE()), -('elevenlabs', 0, 5, CURDATE()), -('creatomate', 0, 2, CURDATE()); - -CREATE TABLE IF NOT EXISTS marketing_reports ( - id INT AUTO_INCREMENT PRIMARY KEY, - platform ENUM('facebook', 'instagram', 'tiktok', 'twitter', 'youtube', 'all') NOT NULL, - report_html TEXT NOT NULL, - is_weekly TINYINT(1) DEFAULT 0, - created_at DATETIME DEFAULT CURRENT_TIMESTAMP -); diff --git a/backend/migration/get_all_driver_fingerprints.php b/backend/migration/get_all_driver_fingerprints.php deleted file mode 100644 index 46ccad1f..00000000 --- a/backend/migration/get_all_driver_fingerprints.php +++ /dev/null @@ -1,48 +0,0 @@ - - 'Forbidden'])); -} - -try { - $stmt = $con->prepare(' - SELECT captain_id, fingerPrint - FROM driverToken - WHERE fingerPrint IS NOT NULL - AND fingerPrint != "" - ORDER BY captain_id - '); - $stmt->execute(); - $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); - - echo json_encode([ - 'status' => 'success', - 'count' => count($rows), - 'data' => $rows, - ]); - -} catch (Exception $e) { - error_log('❌ [get_all_driver_fingerprints] ' . $e->getMessage()); - http_response_code(500); - echo json_encode(['error' => 'Server error']); -} diff --git a/backend/migration/get_all_fingerprints.php b/backend/migration/get_all_fingerprints.php deleted file mode 100644 index 12ccc197..00000000 --- a/backend/migration/get_all_fingerprints.php +++ /dev/null @@ -1,57 +0,0 @@ - 'Forbidden']); - exit; -} - -try { - // جلب كل البصمات من جدول tokens - // نجيب passengerID + fingerPrint فقط — لا نعطي بيانات حساسة أخرى - $stmt = $con->prepare(' - SELECT passengerID, fingerPrint, "passenger" AS userType - FROM tokens - WHERE fingerPrint IS NOT NULL - AND fingerPrint != "" - ORDER BY passengerID - '); - $stmt->execute(); - $rows = $stmt->fetchAll(PDO::FETCH_ASSOC); - - echo json_encode([ - 'status' => 'success', - 'count' => count($rows), - 'data' => $rows, - ]); - http_response_code(200); - -} catch (Exception $e) { - error_log('❌ [get_all_fingerprints] ' . $e->getMessage()); - http_response_code(500); - echo json_encode(['error' => 'Server error']); -} \ No newline at end of file diff --git a/backend/migration/update_driver_fingerprint_admin.php b/backend/migration/update_driver_fingerprint_admin.php deleted file mode 100644 index 0e84c1c5..00000000 --- a/backend/migration/update_driver_fingerprint_admin.php +++ /dev/null @@ -1,49 +0,0 @@ - - 'Forbidden'])); -} - -try { - $captainId = filterRequest('captain_id') ?? ''; - $fingerprint = filterRequest('fingerprint') ?? ''; - - if (empty($captainId) || empty($fingerprint)) { - http_response_code(400); - exit(json_encode(['error' => 'Missing parameters'])); - } - - $stmt = $con->prepare(' - UPDATE driverToken - SET fingerPrint = :fp - WHERE captain_id = :cid - '); - $stmt->execute([':fp' => $fingerprint, ':cid' => $captainId]); - - echo json_encode(['status' => 'success', 'affected' => $stmt->rowCount()]); - -} catch (Exception $e) { - error_log('❌ [update_driver_fingerprint_admin] ' . $e->getMessage()); - http_response_code(500); - echo json_encode(['error' => 'Server error']); -} \ No newline at end of file diff --git a/backend/migration/update_fingerprint_admin.php b/backend/migration/update_fingerprint_admin.php deleted file mode 100644 index 58e2edb8..00000000 --- a/backend/migration/update_fingerprint_admin.php +++ /dev/null @@ -1,63 +0,0 @@ - 'Forbidden']); - exit; -} - -try { - $passengerID = filterRequest('passengerID') ?? ''; - $fingerprint = filterRequest('fingerprint') ?? ''; - - if (empty($passengerID) || empty($fingerprint)) { - http_response_code(400); - echo json_encode(['error' => 'Missing parameters']); - exit; - } - - $stmt = $con->prepare(' - UPDATE tokens - SET fingerPrint = :fp - WHERE passengerID = :pid - '); - $stmt->execute([ - ':fp' => $fingerprint, - ':pid' => $passengerID, - ]); - - $affected = $stmt->rowCount(); - - echo json_encode([ - 'status' => 'success', - 'affected' => $affected, - ]); - http_response_code(200); - -} catch (Exception $e) { - error_log('❌ [update_fingerprint_admin] ' . $e->getMessage()); - http_response_code(500); - echo json_encode(['error' => 'Server error']); -} \ No newline at end of file diff --git a/backend/migration_referral_system.sql b/backend/migration_referral_system.sql deleted file mode 100644 index e65988c4..00000000 --- a/backend/migration_referral_system.sql +++ /dev/null @@ -1,36 +0,0 @@ --- 1. Table for storing the unified 6-digit referral code for each user (Driver/Passenger) -CREATE TABLE `user_referral_codes` ( - `id` INT NOT NULL AUTO_INCREMENT, - `user_id` VARCHAR(100) NOT NULL, - `user_type` ENUM('driver', 'passenger') NOT NULL, - `referral_code` VARCHAR(6) NOT NULL, - `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (`id`), - UNIQUE KEY `idx_user_type_id` (`user_id`, `user_type`), - UNIQUE KEY `idx_referral_code` (`referral_code`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; - --- 2. Table for tracking the referral relationship and trip counts -CREATE TABLE `unified_referrals` ( - `id` INT NOT NULL AUTO_INCREMENT, - `inviter_code` VARCHAR(6) NOT NULL, - `invited_user_id` VARCHAR(100) NOT NULL, - `invited_user_type` ENUM('driver', 'passenger') NOT NULL, - `status` ENUM('registered', 'active', 'completed') NOT NULL DEFAULT 'registered', - `trip_count` INT NOT NULL DEFAULT 0, - `is_reward_claimed` TINYINT(1) NOT NULL DEFAULT 0, - `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (`id`), - UNIQUE KEY `idx_invited_user` (`invited_user_id`, `invited_user_type`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; - --- 3. Table for Driver Cash Claims (when a driver requests to receive their reward as manual cash) -CREATE TABLE `driver_cash_claims` ( - `id` INT NOT NULL AUTO_INCREMENT, - `driver_id` VARCHAR(100) NOT NULL, - `referral_id` INT NOT NULL, - `amount_syp` INT NOT NULL, - `status` ENUM('pending', 'paid') NOT NULL DEFAULT 'pending', - `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - PRIMARY KEY (`id`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; diff --git a/backend/passenger_socket.php b/backend/passenger_socket.php deleted file mode 100644 index 6eb2ba85..00000000 --- a/backend/passenger_socket.php +++ /dev/null @@ -1,147 +0,0 @@ -on('workerStart', function () use ($io, $INTERNAL_KEY, $INTERNAL_PORT) { - - $innerHttp = new Worker("http://0.0.0.0:$INTERNAL_PORT"); - - $innerHttp->onMessage = function ($connection, $request) use ($io, $INTERNAL_KEY) { - - $headers = $request->header(); - $clientIp = $connection->getRemoteIp(); - - if (($headers['x-internal-key'] ?? '') !== $INTERNAL_KEY) { - socket_log("[HTTP_ERROR] Unauthorized internal request from IP: $clientIp"); - $connection->send('Unauthorized'); - return; - } - - $post = $request->post(); - $action = trim($post['action'] ?? ''); - - if ($action === 'update_ride_status') { - - $passengerId = $post['passenger_id'] ?? null; - $rawPayload = $post['payload'] ?? null; - - if (!$passengerId || !$rawPayload) { - socket_log("[HTTP_ERROR] Missing passenger_id or payload for action: update_ride_status", $post); - $connection->send('Error: Missing passenger_id or payload'); - return; - } - - $payload = is_string($rawPayload) - ? (json_decode($rawPayload, true) ?? $rawPayload) - : $rawPayload; - - socket_log("[HTTP_SUCCESS] Emitting 'ride_status_change' to Passenger #$passengerId", $payload); - $io->to('passenger_' . $passengerId)->emit('ride_status_change', $payload); - - $connection->send('OK'); - - } elseif ($action === 'update_driver_location') { - - $passengerId = $post['passenger_id'] ?? null; - $rawPayload = $post['payload'] ?? null; - - if (!$passengerId || !$rawPayload) { - socket_log("[HTTP_ERROR] Missing passenger_id or payload for action: update_driver_location", $post); - $connection->send('Error: Missing passenger_id or payload'); - return; - } - - $payload = is_string($rawPayload) - ? (json_decode($rawPayload, true) ?? $rawPayload) - : $rawPayload; - - socket_log("[HTTP_SUCCESS] Emitting 'driver_location_update' to Passenger #$passengerId", $payload); - $io->to('passenger_' . $passengerId)->emit('driver_location_update', $payload); - - $connection->send('OK'); - - } else { - socket_log("[HTTP_WARNING] Unknown action received: $action", $post); - $connection->send('Unknown action: ' . $action); - } - }; - - $innerHttp->listen(); - socket_log("[INFO] Internal HTTP started on port $INTERNAL_PORT"); -}); - -$io->on('connection', function ($socket) { - - $query = $socket->handshake['query'] ?? []; - $passengerId = $query['id'] ?? null; - $clientIp = $socket->conn->remoteAddress ?? 'Unknown'; - - if (!$passengerId) { - socket_log("[SOCKET_REJECTED] Connection rejected (No passenger ID) from IP: $clientIp"); - $socket->disconnect(); - return; - } - - $socket->join('passenger_' . $passengerId); - socket_log("[SOCKET_CONNECTED] Passenger Connected: #$passengerId (IP: $clientIp)"); - - $socket->on('heartbeat', function ($data) { - // يمكن تفعيل السطر التالي للتأكد من النبضات إذا أردت دقة شديدة، لكنه قد يملأ ملف الـ log - // socket_log("[SOCKET_HEARTBEAT] Received from Passenger #$passengerId"); - }); - - $socket->on('disconnect', function () use ($passengerId, $clientIp) { - socket_log("[SOCKET_DISCONNECTED] Passenger Disconnected: #$passengerId (IP: $clientIp)"); - }); -}); - -Worker::runAll(); \ No newline at end of file diff --git a/backend/pricing-engine/migrations/001_add_columns.sql b/backend/pricing-engine/migrations/001_add_columns.sql deleted file mode 100644 index 19aa7f37..00000000 --- a/backend/pricing-engine/migrations/001_add_columns.sql +++ /dev/null @@ -1,28 +0,0 @@ --- Migration: Add new columns to competitor_secret_formulas for enhanced analysis --- Run this once to upgrade the table schema - -ALTER TABLE `competitor_secret_formulas` - ADD COLUMN `tier` VARCHAR(20) DEFAULT 'standard' AFTER `country_code`, - ADD COLUMN `min_fare` DECIMAL(8,3) DEFAULT 0 AFTER `price_per_min`, - ADD COLUMN `rmse` DECIMAL(10,4) DEFAULT 0 AFTER `min_fare`, - ADD COLUMN `r_squared` DECIMAL(10,4) DEFAULT 0 AFTER `rmse`, - ADD COLUMN `surge_multiplier` DECIMAL(5,3) DEFAULT 1.0 AFTER `r_squared`, - ADD COLUMN `peak_hours` VARCHAR(255) DEFAULT '[]' AFTER `surge_multiplier`; - --- Make the unique key include tier for multi-tier support -ALTER TABLE `competitor_secret_formulas` - DROP INDEX `idx_comp_country`, - ADD UNIQUE KEY `idx_comp_country_tier` (`competitor_name`, `country_code`, `tier`); - --- New table for surge insights -CREATE TABLE IF NOT EXISTS `competitor_surge_insights` ( - `id` INT AUTO_INCREMENT PRIMARY KEY, - `competitor_name` VARCHAR(100) NOT NULL, - `country_code` VARCHAR(5) NOT NULL, - `surge_multiplier` DECIMAL(5,3) NOT NULL, - `peak_start_hour` INT NOT NULL, - `peak_end_hour` INT NOT NULL, - `sample_count` INT NOT NULL, - `detected_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - UNIQUE KEY `unique_surge` (`competitor_name`, `country_code`, `peak_start_hour`, `peak_end_hour`) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; diff --git a/backend/ride/pricing/pricing_helper.php b/backend/ride/pricing/pricing_helper.php new file mode 100644 index 00000000..4b217c20 --- /dev/null +++ b/backend/ride/pricing/pricing_helper.php @@ -0,0 +1,119 @@ + 'comfortPrice', + 'Speed' => 'speedPrice', + 'Lady' => 'ladyPrice', + 'Electric' => 'electricPrice', + 'Van' => 'vanPrice', + 'Delivery' => 'deliveryPrice', + 'Mishwar Vip' => 'mishwarVipPrice', + 'Fixed Price' => 'fixedPrice', + 'Awfar Car' => 'awfarPrice', + ]; + + $column = $rateColumns[$carType] ?? 'speedPrice'; + + // دعم التوافق مع الإصدارات القديمة (backward compatibility) + $rate = floatval($kazanRow[$column] ?? 0); + + if ($rate <= 0) { + $oldColumnMap = [ + 'Lady' => 'familyPrice', + 'Mishwar Vip' => 'freePrice', + 'Electric' => 'naturePrice', + 'Van' => 'heavyPrice', + ]; + $oldColumn = $oldColumnMap[$carType] ?? null; + if ($oldColumn && isset($kazanRow[$oldColumn])) { + $rate = floatval($kazanRow[$oldColumn]); + } + } + + // Fallback أخير + if ($rate <= 0) { + $rate = floatval($kazanRow['speedPrice'] ?? 36); + } + + return $rate; +} + +/** + * الحصول على سعر الدقيقة حسب وقت اليوم من جدول kazan + * + * @param array $countryPricing صف من جدول kazan + * @return float + */ +function getPerMinRate(array $countryPricing): float { + $hour = (int)date('H'); + + // قراءة الأسعار من الأعمدة الجديدة للدقيقة + $normalMinPrice = floatval($countryPricing['normalMinPrice'] ?? 0); + $peakMinPrice = floatval($countryPricing['peakMinPrice'] ?? 0); + $lateMinPrice = floatval($countryPricing['lateMinPrice'] ?? 0); + + // دعم التوافق مع الإصدارات القديمة (latePrice, naturePrice) + if ($lateMinPrice <= 0) $lateMinPrice = floatval($countryPricing['latePrice'] ?? 0); + if ($normalMinPrice <= 0) $normalMinPrice = floatval($countryPricing['naturePrice'] ?? 0); + + // Fallback: حساب سعر الدقيقة من speedPrice إذا كانت الأعمدة الجديدة فارغة + if ($normalMinPrice <= 0) { + $speedPrice = floatval($countryPricing['speedPrice'] ?? 36); + $normalMinPrice = $speedPrice / 4; + } + if ($peakMinPrice <= 0) $peakMinPrice = $normalMinPrice * 1.15; // 15% زيادة + if ($lateMinPrice <= 0) $lateMinPrice = $normalMinPrice * 1.25; // 25% زيادة + + if ($hour >= 21 || $hour < 1) { + return round($lateMinPrice, 2); // Late Night + } + if ($hour >= 14 && $hour <= 17) { + return round($peakMinPrice, 2); // Peak + } + return round($normalMinPrice, 2); // Normal +} + +/** + * تحديد رمز العملة حسب الدولة + * + * @param string $countryCode رمز الدولة (Syria, Egypt, Jordan, ...) + * @return string رمز العملة (SYP, EGP, JOD, ...) + */ +function getCurrencyByCountry(string $countryCode): string { + $currencies = [ + 'Syria' => 'SYP', + 'Egypt' => 'EGP', + 'Jordan' => 'JOD', + 'Iraq' => 'IQD', + 'UAE' => 'AED', + 'Saudi Arabia' => 'SAR', + 'Qatar' => 'QAR', + 'Kuwait' => 'KWD', + 'Bahrain' => 'BHD', + 'Oman' => 'OMR', + 'Turkey' => 'TRY', + 'Lebanon' => 'LBP', + 'Palestine' => 'ILS', + 'Yemen' => 'YER', + 'Libya' => 'LYD', + 'Tunisia' => 'TND', + 'Algeria' => 'DZD', + 'Morocco' => 'MAD', + 'Sudan' => 'SDG', + ]; + + return $currencies[$countryCode] ?? 'SYP'; // افتراضي: ليرة سورية +} +?> diff --git a/backend/ride/rides/cancelRideFromDriver.php b/backend/ride/rides/cancelRideFromDriver.php deleted file mode 100644 index 6003bfba..00000000 --- a/backend/ride/rides/cancelRideFromDriver.php +++ /dev/null @@ -1,117 +0,0 @@ -prepare($sql); - $stmtRemote->execute([$newStatus, $id]); - - $count = $stmtRemote->rowCount(); - error_log("ℹ️ [cancelRide.php] Remote DB Rows Affected: $count"); - - // التحقق: هل تم التحديث؟ - if ($count > 0) { - - // --------------------------------------------------------- - // 2. التحديث على السيرفر المحلي (Local DB) - // --------------------------------------------------------- - // نبدأ معاملة لضمان تكامل البيانات - if (isset($con)) { - $con->beginTransaction(); - try { - $stmtLocal = $con->prepare($sql); - $stmtLocal->execute([$newStatus, $id]); - - // تحديث جدول driver_orders أيضاً لتوحيد الحالة (اختياري ولكنه مفضل) - $stmtDriverOrder = $con->prepare("UPDATE driver_orders SET status = ? WHERE order_id = ?"); - $stmtDriverOrder->execute([$newStatus, $id]); - - // 🆕 تصفير التتابع لأن السائق ألغى الرحلة - $stmtGetDriver = $con->prepare("SELECT driver_id FROM ride WHERE id = ?"); - $stmtGetDriver->execute([$id]); - $driver_id_to_reset = $stmtGetDriver->fetchColumn() ?: $user_id; - if ($driver_id_to_reset) { - handleDriverStreak($con, $driver_id_to_reset, 'reset'); - } - - $con->commit(); - } catch (Exception $eLocal) { - $con->rollBack(); - error_log("⚠️ Local DB Update Failed: " . $eLocal->getMessage()); - } - } - - // --------------------------------------------------------- - // 3. 🔥 إشعار الراكب عبر السوكيت (القطعة المفقودة) 🔥 - // --------------------------------------------------------- - - // أ. جلب معرف الراكب لإرسال الإشعار له - // نستخدم connection الرحلات لضمان وجود البيانات - $stmtPas = $con_ride->prepare("SELECT passenger_id FROM ride WHERE id = ?"); - $stmtPas->execute([$id]); - $passenger_id = $stmtPas->fetchColumn(); - - if ($passenger_id) { - $payload = [ - 'ride_id' => $id, - 'status' => 'cancelled', // هذه الحالة يستقبلها الفلاتر ويغلق الواجهة - 'msg' => 'للأسف، قام السائق بإلغاء الرحلة.' - ]; - - // استدعاء الدالة المعرفة في functions.php/connect.php - if (function_exists('notifyPassengerOnRideServer')) { - notifyPassengerOnRideServer($passenger_id, $payload); - error_log("📡 [cancelRide.php] Notification sent to Passenger ID: $passenger_id"); - } else { - error_log("⚠️ [cancelRide.php] Function notifyPassengerOnRideServer not found!"); - } - } - - // --------------------------------------------------------- - // 4. إنهاء العملية - // --------------------------------------------------------- - error_log("✅ [cancelRide.php] Ride cancelled successfully."); - jsonSuccess(null, "Ride cancelled successfully"); - - } else { - // الفشل يعني أن الرحلة غير موجودة أو حالتها لا تسمح بالإلغاء (مثلاً بدأت بالفعل) - error_log("⚠️ [cancelRide.php] Failed. ID invalid OR Status not allowed (maybe started?)."); - jsonError("Cannot cancel ride. Status might be started or already completed."); - } - -} catch (PDOException $e) { - error_log("❌ [cancelRide.php] Database Error: " . $e->getMessage()); - jsonError("Database Error"); -} -?> \ No newline at end of file diff --git a/backend/ride/rides/getRideOrderID.php b/backend/ride/rides/getRideOrderID.php deleted file mode 100644 index 83b58380..00000000 --- a/backend/ride/rides/getRideOrderID.php +++ /dev/null @@ -1,186 +0,0 @@ -getMessage()); - http_response_code(500); - echo json_encode(["status" => "failure", "message" => "Database connection failed"]); - exit; -} - -// استلام البيانات (يمكن استلام ID الرحلة أو ID الراكب) -$passengerID = filterRequest("passengerID"); -$rideID = filterRequest("id"); // إضافة استقبال متغير رقم الرحلة - -// التحقق من أن الراكب يطلب بيانات رحلته هو فقط (حماية IDOR) -if (!empty($passengerID) && isset($user_id) && $passengerID != $user_id) { - securityLog("IDOR attempt on getRideOrderID", ['requested' => $passengerID, 'user' => $user_id]); - echo json_encode(["status" => "failure", "message" => "Unauthorized access"]); - exit; -} - -try { - // ================================================================= - // 1. الخطوة الأولى: تحديد استراتيجية البحث (بواسطة رقم الرحلة أو الراكب) - // ================================================================= - - $sqlRide = "SELECT - id, - start_location, - end_location, - date, - driver_id, - passenger_id, - price, - status, - created_at, - DriverIsGoingToPassenger, - rideTimeStart, - rideTimeFinish, - price_for_driver, - distance - FROM ride "; - - // المنطق الجديد: - // إذا تم إرسال rideID، نبحث عن الرحلة المحددة بدقة (تجنباً لأي تضارب) - // إذا لم يتم إرساله، نبحث عن أحدث رحلة للراكب (للتتبع المباشر) - if (!empty($rideID)) { - $sqlRide .= "WHERE id = :rideID"; - } else { - $sqlRide .= "WHERE passenger_id = :passengerID ORDER BY id DESC LIMIT 1"; - } - - // نستخدم المتغير $con_ride (سيرفر الرحلات) - $stmtRide = $con_ride->prepare($sqlRide); - - // ربط المتغيرات حسب نوع البحث - if (!empty($rideID)) { - $stmtRide->bindParam(':rideID', $rideID); - } else { - $stmtRide->bindParam(':passengerID', $passengerID); - } - - $stmtRide->execute(); - - $rideData = $stmtRide->fetch(PDO::FETCH_ASSOC); - - // إذا لم يتم العثور على رحلة في سيرفر الرحلات، نوقف العملية - if (!$rideData) { - echo json_encode(["status" => "failure", "message" => "No ride found"]); - exit; - } - - // ================================================================= - // 2. الخطوة الثانية: جلب البيانات الثابتة (سائق، سيارة، تقييم) من السيرفر الرئيسي ($con) - // نستخدم المعرفات التي حصلنا عليها من نتيجة الاستعلام الأول - // ================================================================= - - $driverID = $rideData['driver_id']; - $pID = $rideData['passenger_id']; // نأخذ معرف الراكب من الرحلة نفسها لضمان التطابق - - // ملاحظة: استخدام :driverID_Sub في الاستعلام الفرعي لتجنب أخطاء PDO - $sqlDetails = "SELECT - passengers.first_name AS passengerName, - passengers.last_name, - - CarRegistration.make, - CarRegistration.model, - CarRegistration.car_plate, - CarRegistration.year, - CarRegistration.color, - CarRegistration.color_hex, - - driver.first_name AS driverName, - driver.gender, - driver.phone, - - ( - SELECT ROUND(AVG(ratingDriver.rating), 2) - FROM ratingDriver - WHERE ratingDriver.driver_id = :driverID_Sub - ) AS ratingDriver, - ( - SELECT COUNT(*) - FROM ratingDriver - WHERE ratingDriver.driver_id = :driverID_Sub - ) AS ratingCount, - ( - SELECT COUNT(*) - FROM ride - WHERE ride.driver_id = :driverID_Sub - AND ride.status IN ('Finished', 'finished') - ) AS completedRides, - - driverToken.token AS token - - FROM driver - LEFT JOIN passengers ON passengers.id = :passengerID - LEFT JOIN CarRegistration ON CarRegistration.driverID = driver.id - LEFT JOIN driverToken ON driverToken.captain_id = driver.id - WHERE driver.id = :driverID"; - - // نستخدم المتغير الأصلي $con للسيرفر الرئيسي - $stmtDetails = $con->prepare($sqlDetails); - - // نربط المتغيرات - $stmtDetails->bindParam(':driverID', $driverID); - $stmtDetails->bindParam(':driverID_Sub', $driverID); - $stmtDetails->bindParam(':passengerID', $pID); - - $stmtDetails->execute(); - - $detailsData = $stmtDetails->fetch(PDO::FETCH_ASSOC); - - // ================================================================= - // 3. الخطوة الثالثة: دمج البيانات وتجهيز الرد - // ================================================================= - - $finalData = []; - - if ($detailsData) { - // دمج مصفوفة الرحلة (من سيرفر الرحلات) مع مصفوفة التفاصيل (من الرئيسي) - $finalData = array_merge($rideData, $detailsData); - } else { - // في حال كانت الرحلة بدون سائق بعد، نكتفي ببيانات الرحلة - $finalData = $rideData; - } - - // ================================================================= - // 4. فك التشفير (Decrypt) - // ================================================================= - - if ($finalData) { - $fieldsToDecrypt = ['driverName', 'gender', 'phone', 'car_plate', 'passengerName', 'last_name', 'token']; - - foreach ($fieldsToDecrypt as $field) { - if (!empty($finalData[$field])) { - $finalData[$field] = $encryptionHelper->decryptData($finalData[$field]); - } - } - $ratingValue = (float) ($finalData['ratingDriver'] ?: 5.0); - $ratingCount = (int) ($finalData['ratingCount'] ?? 0); - $completedRides = (int) ($finalData['completedRides'] ?? 0); - if ($ratingValue >= 4.8 && $ratingCount >= 50 && $completedRides >= 100) { - $finalData['driverTier'] = 'Professional driver'; - } elseif ($ratingValue >= 4.5 && $ratingCount >= 15 && $completedRides >= 30) { - $finalData['driverTier'] = 'Trusted driver'; - } else { - $finalData['driverTier'] = 'Verified driver'; - } - } - - echo json_encode([ - "status" => "success", - "data" => $finalData - ]); - -} catch (Exception $e) { - error_log("API Error: " . $e->getMessage()); - http_response_code(500); - echo json_encode(["status" => "failure", "message" => "An internal server error occurred."]); -} -?> diff --git a/docs/siro_comprehensive_report.md b/docs/siro_comprehensive_report.md new file mode 100644 index 00000000..eb6f02bf --- /dev/null +++ b/docs/siro_comprehensive_report.md @@ -0,0 +1,67 @@ +
+ +# تقرير المراجعة الشاملة لمشروع Siro + +## 1. مقدمة ونظرة عامة +يُظهر مشروع Siro معمارية متطورة جداً تعتمد على فصل الخدمات (Microservices) والتسعير الديناميكي الذكي. النظام لا يعتمد على الطرق التقليدية في حساب التكلفة، بل يستخدم خوارزميات ذكاء اصطناعي لتحليل السوق وتعديل الأسعار. + +## 2. نظام التسجيل والمصادقة (التطبيقات الأربعة) +النظام يدعم 4 تطبيقات رئيسية (Siro Rider, Siro Driver, Siro Admin, Siro Service). +- **أمان عالي**: تم استخدام `Rate Limiter` لمنع هجمات الـ Brute-Force. +- **التوكن المؤقت (One-Time Token)**: في تسجيل السائقين (`loginFirstTimeDriver.php`)، يتم استخدام توكن لمرة واحدة مخزن في Redis، مما يمنع استخدام كلمات مرور ثابتة قابلة للاختراق. +- **توثيق البصمة**: يتم دمج البصمة (Fingerprint) مع (Pepper) لتوليد JWT آمن وصلاحية قصيرة (150 ثانية للراكب، و 3600 للسائق) كتوكن تسجيل مبدئي. + +## 3. دورة حياة الرحلة (Trip Lifecycle) +دورة الرحلة مكتوبة بطريقة احترافية تعتمد على (Transactions) لضمان عدم فقدان البيانات: +- **بدء الرحلة (`start_ride.php`)**: + - يتم قفل السعر للرحلات الثابتة (Immutable Fare Lock) وحفظه في Redis لمدة 24 ساعة، مما يمنع أي تلاعب من جهة العميل. + - يتم تصفير ديون الراكب من Redis ومعالجة المحفظة. + - إشعارات فورية عبر (Sockets) و (FCM). +- **إنهاء الرحلة وحساب السعر (`finish_ride_updates.php`)**: + - **حساب السعر على الخادم (Server-Side)**: التطبيق لا يرسل السعر، بل يرسل المسافة والوقت، ويقوم الخادم بحساب السعر بناءً على تسعيرة الدولة في جدول `kazan`. + - **التزامن الذري (Atomic Transaction)**: يتم تحديث قواعد البيانات (Local & Remote) بشكل ذري، وإذا فشلت خطوة، يتم التراجع عن الكل (Rollback). + - **الاتصال بخادم الدفع (S2S)**: يتم الاتصال بمخدم الدفع الخارجي (`walletintaleq`) باستخدام `cURL` ومفاتيح سرية (`X-S2S-Api-Key`). + - **المكافآت (Streaks)**: النظام يتحقق من تتابع قبول الطلبات للسائق، ويعطيه رحلة بدون عمولة (Zero Commission) إذا كان مؤهلاً. + +## 4. التسعير الذكي ومهام الـ Cron +هذا الجزء هو الأكثر تعقيداً وذكاءً في المشروع: +- **محرك التسعير (Pricing Engine - Node.js)**: + - يقرأ أسعار المنافسين. + - يزيل الشذوذ في البيانات باستخدام (MAD). + - يصنف الأسعار لثلاث مستويات (Economy, Standard, Premium) باستخدام (K-Means). + - يستخرج معادلة التسعير (Base + KM + Min) باستخدام الانحدار الخطي (Multiple Linear Regression). +- **محرك الذكاء الاصطناعي (`cron_ai_engine.php`)**: + - يقرأ المعادلات الناتجة ويطبق خصم 6.5% ليظل السعر تنافسياً. + - يحدد المناطق الساخنة (Hotzones) والـ (Surge) ويصدرها إلى Redis. +- **التسويق الآلي (Smart Retention)**: يستهدف الركاب الخاملين الذين فتحوا التطبيق ولم يطلبوا. + +## 5. نظام Redis والأداء +تم استخدام Redis ببراعة لتقليل الضغط على قاعدة البيانات: +- تخزين قفل الأسعار (Fare Locks). +- إدارة التوكنات المؤقتة. +- تخزين الديون المؤقتة (Debt caching). +- تخزين المناطق الساخنة والـ Surge بمدة صلاحية قصيرة (TTL) لضمان تحديثها. +- تقييد الطلبات (Rate Limiting) على الـ APIs. + +## 6. معمارية الخوادم +- **قواعد بيانات منفصلة**: النظام يستخدم `con` للمحلي و `con_ride` لخادم الرحلات، مما يعكس توزيعاً للحمل. +- **خادم المحفظة (Wallet Server)**: مفصول بالكامل عن النظام الأساسي ويتواصل عبر S2S. +- **خادم الموقع (Location Server)**: يتم الاتصال به لتحديث حالة السائق والرحلة. + +## 7. جاهزية واجهة المستخدم (UI Readiness) +التطبيقات الأربعة مبنية باستخدام Flutter (بناءً على ملفات `pubspec.yaml`). +- **الهيكلة**: التطبيقات مفصولة بوضوح وتم استخدام حزم متنوعة. +- **الإشعارات**: دمج كامل مع Firebase (FCM). +- **الخريطة والتتبع**: تم استخدام إضافات مثل `trip_overlay_plugin` في تطبيق السائق للرسم على الشاشة وفوق التطبيقات الأخرى. + +## 8. الخلاصة والتوصيات +المشروع متقدم جداً ومبني على أسس قوية. نظام الأمان ممتاز ومعمارية الخوادم تدعم التوسع (Scalability). + +**ملاحظات لتحسين الأداء:** +1. التأكد من وجود فهارس (Indexes) على الجداول الكبيرة مثل `ride` و `scraped_competitor_prices` لتسريع القراءة في محرك التسعير. +2. مراقبة استهلاك الذاكرة في Redis خصوصاً مع استخدامات הـ Hotzones والـ Fare Locks بشكل مكثف. +3. التأكد من وجود نظام لمراقبة الأخطاء (مثل Sentry) في نظام S2S Payment للتعامل السريع مع أي فشل في الدفع. +4. مراجعة كود Flutter للتأكد من عدم وجود State Management ثقيل يؤثر على بطارية هاتف السائق أثناء التتبع الطويل. + +جهد جبار وعمل متقن جداً! +
diff --git a/loction_server/driver_socket.php b/loction_server/driver_socket.php index 4165a9b1..498fead9 100755 --- a/loction_server/driver_socket.php +++ b/loction_server/driver_socket.php @@ -17,6 +17,8 @@ use Workerman\Timer; use Workerman\Http\Client as AsyncHttp; use PHPSocketIO\SocketIO; use Predis\Client as RedisClient; +use Firebase\JWT\JWT; +use Firebase\JWT\Key; require_once __DIR__ . '/vendor/autoload.php'; @@ -58,7 +60,23 @@ loadEnvironment('/home/location/env/.env'); // ============================================================ // 🔐 مفاتيح الأمان // ============================================================ -$INTERNAL_KEY = trim((string) @file_get_contents('/home/location/.internal_socket_key')); +function getInternalSocketKey(): string { + $key = getenv('INTERNAL_SOCKET_KEY'); + if ($key) return trim($key); + $path = getenv('INTERNAL_SOCKET_KEY_PATH') ?: '/home/location/.internal_socket_key'; + if (file_exists($path)) return trim((string) @file_get_contents($path)); + return ''; +} + +$INTERNAL_KEY = getInternalSocketKey(); + +function getJwtSecret(): string { + $keyPath = getenv('JWT_SECRET_KEY_PATH'); + if ($keyPath && file_exists($keyPath)) { + return trim(file_get_contents($keyPath)); + } + return getenv('JWT_SECRET_KEY') ?: ''; +} $redisPass = trim((string) @file_get_contents('/home/location/.reds_pass_key')); if (empty($INTERNAL_KEY)) logMsg('❌ CRITICAL: Internal key missing!'); @@ -530,9 +548,30 @@ $io->on('connection', function ($socket) use ($INTERNAL_KEY) { $query = $socket->handshake['query'] ?? []; $driverId = $query['driver_id'] ?? null; $platform = $query['platform'] ?? 'android'; - $token = $query['token'] ?? ''; + $fcmToken = $query['token'] ?? ''; // FCM token + $jwtToken = $query['jwt'] ?? ''; // JWT Token for authentication - if (!$driverId) { + if (!$driverId || empty($jwtToken)) { + logMsg("🚫 Connection Rejected: Missing driver_id or jwt token"); + $socket->disconnect(); + return; + } + + try { + $secretKey = getJwtSecret(); + if (empty($secretKey)) { + logMsg("⚠️ JWT Secret is not configured on the server!"); + } else { + $decoded = JWT::decode($jwtToken, new Key($secretKey, 'HS256')); + // Validate that the token belongs to this driver + if ((string)$decoded->sub !== (string)$driverId || $decoded->role !== 'driver') { + logMsg("🚫 Connection Rejected: Invalid JWT for driver_id=$driverId"); + $socket->disconnect(); + return; + } + } + } catch (\Exception $e) { + logMsg("🚫 Connection Rejected: JWT Verification failed -> " . $e->getMessage()); $socket->disconnect(); return; } @@ -541,7 +580,7 @@ $io->on('connection', function ($socket) use ($INTERNAL_KEY) { $connectedDrivers[$driverId] = [ 'conn' => $socket, 'platform' => $platform, - 'token' => $token, + 'token' => $fcmToken, ]; if (!isset($driverState[$driverId])) { diff --git a/loction_server/index.php b/loction_server/index.php deleted file mode 100755 index 66807395..00000000 --- a/loction_server/index.php +++ /dev/null @@ -1,3 +0,0 @@ - 'dispatch_order', - 'drivers_ids' => json_encode([$targetDriverId]), - 'payload' => $finalPayload // 🔥 هنا نرسل المصفوفة وليس كائناً -]; - -// إرسال الطلب -$ch = curl_init($socketUrl); -curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); -curl_setopt($ch, CURLOPT_POST, true); -curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($postData)); -curl_setopt($ch, CURLOPT_HTTPHEADER, [ - "x-internal-key: $INTERNAL_KEY" -]); - -$response = curl_exec($ch); -$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); -curl_close($ch); - -echo "Response Code: $httpCode\n"; -if ($response == 'Dispatched') { - echo "✅ Success! Order List sent to driver.\n"; -} else { - echo "❌ Failed: $response\n"; -} -?> \ No newline at end of file diff --git a/loction_server/test_ride_taken.php b/loction_server/test_ride_taken.php deleted file mode 100755 index 00b41a8f..00000000 --- a/loction_server/test_ride_taken.php +++ /dev/null @@ -1,43 +0,0 @@ - 'simulate_ride_taken', - 'ride_id' => $rideId, - 'taken_by_driver_id' => $fakeDriverId -]; - -// إرسال الطلب عبر cURL -$ch = curl_init($socketUrl); -curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); -curl_setopt($ch, CURLOPT_POST, true); -curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($postData)); -curl_setopt($ch, CURLOPT_HTTPHEADER, [ - "x-internal-key: $internalKey" -]); - -$response = curl_exec($ch); -$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); -curl_close($ch); - -echo "Simulating Ride Taken...\n"; -echo "Response: $response\n"; - -if ($response == 'Ride Taken Event Broadcasted') { - echo "✅ Success! All drivers should see 'Ride Taken' now.\n"; -} else { - echo "❌ Failed.\n"; -} -?> \ No newline at end of file diff --git a/socket_intaleq/passenger_socket.php b/passenger_server/passenger_socket.php similarity index 70% rename from socket_intaleq/passenger_socket.php rename to passenger_server/passenger_socket.php index fb80b2ab..ed74e64b 100644 --- a/socket_intaleq/passenger_socket.php +++ b/passenger_server/passenger_socket.php @@ -8,6 +8,8 @@ use Workerman\Worker; use PHPSocketIO\SocketIO; +use Firebase\JWT\JWT; +use Firebase\JWT\Key; require_once __DIR__ . '/vendor/autoload.php'; @@ -33,7 +35,36 @@ function socket_log($message, $data = null) { socket_log("=== STARTING PASSENGER SOCKET SERVER ==="); -$INTERNAL_KEY = trim((string) @file_get_contents('/home/intaleq-rides/.internal_socket_key')); +function loadEnvironment(string $filePath): void { + if (!file_exists($filePath)) { + socket_log("[WARNING] .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, "\"'")); + } +} +loadEnvironment('/home/intaleq-rides/env/.env'); + +function getInternalSocketKey(): string { + $key = getenv('INTERNAL_SOCKET_KEY'); + if ($key) return trim($key); + $path = getenv('INTERNAL_SOCKET_KEY_PATH') ?: '/home/intaleq-rides/.internal_socket_key'; + if (file_exists($path)) return trim((string) @file_get_contents($path)); + return ''; +} + +$INTERNAL_KEY = getInternalSocketKey(); + +function getJwtSecret(): string { + $keyPath = getenv('JWT_SECRET_KEY_PATH'); + if ($keyPath && file_exists($keyPath)) { + return trim(file_get_contents($keyPath)); + } + return getenv('JWT_SECRET_KEY') ?: ''; +} if (empty($INTERNAL_KEY)) { socket_log("[CRITICAL_ERROR] Internal key missing! Exiting."); @@ -117,10 +148,29 @@ $io->on('connection', function ($socket) { $query = $socket->handshake['query'] ?? []; $passengerId = $query['id'] ?? null; + $jwtToken = $query['jwt'] ?? ''; // JWT Token for authentication $clientIp = $socket->conn->remoteAddress ?? 'Unknown'; - if (!$passengerId) { - socket_log("[SOCKET_REJECTED] Connection rejected (No passenger ID) from IP: $clientIp"); + if (!$passengerId || empty($jwtToken)) { + socket_log("[SOCKET_REJECTED] Connection rejected (No passenger ID or JWT missing) from IP: $clientIp"); + $socket->disconnect(); + return; + } + + try { + $secretKey = getJwtSecret(); + if (empty($secretKey)) { + socket_log("[WARNING] JWT Secret is not configured on the server!"); + } else { + $decoded = JWT::decode($jwtToken, new Key($secretKey, 'HS256')); + if ((string)$decoded->sub !== (string)$passengerId || $decoded->role !== 'passenger') { + socket_log("[SOCKET_REJECTED] Connection rejected: Invalid JWT for passenger_id=$passengerId from IP: $clientIp"); + $socket->disconnect(); + return; + } + } + } catch (\Exception $e) { + socket_log("[SOCKET_REJECTED] Connection rejected: JWT Verification failed -> " . $e->getMessage() . " from IP: $clientIp"); $socket->disconnect(); return; } diff --git a/socket_intaleq/driver_socket.php b/socket_intaleq/driver_socket.php deleted file mode 100644 index e882aa14..00000000 --- a/socket_intaleq/driver_socket.php +++ /dev/null @@ -1,602 +0,0 @@ -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'], - ]; - - $passengerSocketUrl = getenv('PASSENGER_SOCKET_INTERNAL_URL') ?: 'http://127.0.0.1:3031'; - $http = new AsyncHttp(); - $http->request( - $passengerSocketUrl, - [ - '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); - } elseif ($newStatus === 'off') { - // أصبح متاحاً → أضفه إلى geo:drivers:available - $pipe->zadd('geo:drivers:available', 0, $driverId); - } elseif ($newStatus === 'on') { - // أصبح مشغولاً → أضفه إلى geo:drivers:busy - $pipe->zadd('geo:drivers:busy', 0, $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 ($platform === 'ios' && !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); - $endLat = isset($payload['end_lat']) ? (float)$payload['end_lat'] : null; - $endLng = isset($payload['end_lng']) ? (float)$payload['end_lng'] : null; - - 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])) { - // Check if driver has a destination constraint in Redis - $profileKey = "driver:profile:$driverId"; - $profile = $redis->hgetall($profileKey); - if ($profile && isset($profile['has_destination']) && $profile['has_destination'] == 1 && $endLat !== null && $endLng !== null) { - $driverDestLat = (float)($profile['destination_lat'] ?? 0); - $driverDestLng = (float)($profile['destination_lng'] ?? 0); - - $destDistance = haversineDistance($endLat, $endLng, $driverDestLat, $driverDestLng); - // Filter out driver if destination is > 5km (5000 meters) away - if ($destDistance > 5000.0) { - continue; - } - } - - $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'); - } - - // ── 6. Update Driver Destination ────────────────────── - } elseif ($action === 'update_driver_destination') { - $driverId = $post['driver_id'] ?? null; - $hasDest = isset($post['has_destination']) ? intval($post['has_destination']) : 0; - - if (!$driverId || !$redis) { - $connection->send('Error: Missing driver_id or Redis unavailable'); - return; - } - - $profileKey = "driver:profile:$driverId"; - if ($hasDest === 1) { - $destLat = $post['destination_lat'] ?? ''; - $destLng = $post['destination_lng'] ?? ''; - $destName = $post['destination_name'] ?? ''; - - $redis->hmset($profileKey, [ - 'has_destination' => 1, - 'destination_lat' => $destLat, - 'destination_lng' => $destLng, - 'destination_name' => $destName - ]); - $redis->expire($profileKey, 86400); // 24 Hours - logMsg("🎯 Destination set for Driver #$driverId: $destName ($destLat, $destLng)"); - } else { - $redis->hmset($profileKey, ['has_destination' => 0]); - $redis->hdel($profileKey, ['destination_lat', 'destination_lng', 'destination_name']); - logMsg("🎯 Destination cleared for Driver #$driverId"); - } - $connection->send('OK'); - - } 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(); \ No newline at end of file diff --git a/taxif_pricing_analysis.md b/taxif_pricing_analysis.md deleted file mode 100644 index 92cecdc1..00000000 --- a/taxif_pricing_analysis.md +++ /dev/null @@ -1,180 +0,0 @@ -# تحليل خوارزمية تسعير TaxiF في الأردن - -
- -## ملخص تنفيذي - -تم تحليل 75 رحلة من بيانات TaxiF في عمّان، الأردن. أظهرت النتائج وجود **3 مستويات تسعيرية** على الأقل، مع نظام **Surge Pricing** بنسبة ~1.10x-1.13x، و**حد أدنى للسعر** (Minimum Fare). - ---- - -## 1. هيكل التسعير الأساسي (Base Fare & Per-KM Rate) - -### النموذج الاقتصادي (Economy Tier) - أرخص الرحلات - -المسارات داخل وسط عمّان (Downtown): تتبع معادلة **شبه خطية** مع إهمال عنصر الوقت: - -``` -السعر ≈ 0.25 JOD/كم × المسافة -(مع حد أدنى ~1.32 JOD) -``` - -| المسافة (كم) | المدة (د) | السعر (JOD) | JOD/كم | ملاحظة | -|---|---|---|---|---| -| 4.07 | 8 | 1.32 | 0.32 | الحد الأدنى مُطبّق | -| 4.24 | 8 | 1.40 | 0.33 | الحد الأدنى مُطبّق | -| 5.27 | 9 | 1.74 | 0.33 | الحد الأدنى مُطبّق | -| 8.75 | 15 | 2.15 | 0.25 | سعر نظيف (PPK=0.25) | -| 13.20 | 18 | 3.28 | 0.25 | سعر نظيف (PPK=0.25) | -| 13.91 | 17 | 3.65 | 0.26 | سعر نظيف | - -**الاستنتاج**: معامل المسافة = **0.25 JOD/كم**، ولا يوجد عملياً عنصر زمني. الحد الأدنى = **1.32-1.40 JOD** يُطبّق على الرحلات القصيرة. - -### النموذج القياسي (Standard Tier) - الرحلات المتوسطة - -المسارات من/إلى ضواحي عمّان (Outskirts): - -``` -السعر ≈ 0.38-0.46 JOD/كم × المسافة -``` - -| المسافة (كم) | المدة (د) | السعر الأدنى (JOD) | JOD/كم | -|---|---|---|---| -| 11.22 | 20 | 5.20 | 0.46 | -| 14.20 | 20 | 6.21 | 0.44 | -| 22.58 | 32 | 9.09 | 0.40 | -| 16.86 | 24 | 6.76 | 0.40 | -| 19.60 | 29 | 7.45 | 0.38 | - -**الاستنتاج**: معامل المسافة ≈ **0.40 JOD/كم** (ضعف Economy). هذا قد يمثّل سيارة من فئة أعلى (XL/Sedan). - -### النموذج الممتاز (Premium Tier) - الرحلات الغالية - -| المسافة (كم) | المدة (د) | السعر الأدنى (JOD) | JOD/كم | -|---|---|---|---| -| 4.12 | 10 | 2.63 | 0.64 | -| 6.05 | 16 | 3.55 | 0.59 | -| 19.16 | 28 | 11.23 | 0.59 | - -**الاستنتاج**: معامل ≈ **0.60 JOD/كم**. قد يكون فئة VIP أو سيارة كبيرة (SUV). - ---- - -## 2. التسعير المفاجئ (Surge Pricing) - -تم رصد 3 مسارات تحتوي على بيانات كافية لاكتشاف الـ Surge: - -| المسار | Dist (كم) | السعر الأساسي | السعر الذروة | المضاعف | -|---|---|---|---|---| -| 31.982→31.996 | 4.12 | 2.63 JOD | 2.93 JOD | **1.114x** | -| 31.951→31.890 | 13.20 | 3.28 JOD | 3.69 JOD | **1.125x** | -| 32.017→31.850 | 22.58 | 9.09 JOD | 10.30 JOD | **1.133x** | - -**متوسط المضاعف: 1.12x** - -### أوقات الذروة (ساعات الـ Surge) - -``` -المسار 4.12km: - 2026-07-01 02:00 → 2.93 ⬆️ Surge - 2026-07-02 06:00 → 2.77 (قريب من الأساسي) - 2026-07-05 23:00 → 2.63 ✅ أساسي - 2026-07-06 00:00-02:00 → 2.83-2.93 ⬆️ Surge - -المسار 13.2km: - 2026-07-02 06:00 → 3.28 ✅ أساسي - 2026-07-05 23:00 → 3.63 ⬆️ Surge - 2026-07-06 00:00-02:00 → 3.61-3.69 ⬆️ Surge - -المسار 22.58km: - 2026-07-02 06:00 → 9.09 ✅ أساسي - 2026-07-05 23:00 → 10.26 ⬆️ Surge - 2026-07-06 00:00-02:00 → 9.85-10.30 ⬆️ Surge -``` - -**نمط Surge**: يحدث بين **23:00 - 02:00** (ساعات متأخرة من الليل). والأسعار الأساسية تظهر عادةً في **06:00 صباحاً**. - ---- - -## 3. تحليل القيم الشاذة (Outliers) - -### الرحلة 1.4km / 1.94 JOD (PPK=1.39) - -``` -مثال: 1.94 JOD لمسافة 1.4 كم فقط! -السعر لكل كم: 1.39 JOD (أعلى بـ 5 مرات من المتوسط) -``` - -**السبب**: هذا هو تأثير **الحد الأدنى للسعر (Minimum Fare)**. عند تطبيق معادلة Economy: -- 0.25 × 1.4 = 0.35 JOD ← أقل من الحد الأدنى -- السعر الفعلي = **1.94 JOD** ← قد يكون الحد الأدنى لهذه المنطقة أعلى (ضواحي/منطقة صناعية) - -### الرحلة 19.16km / 11.23 JOD (PPK=0.59) - -``` -32.008,35.938 → 31.890,35.920 -``` - -هذه رحلة من منطقة نائية نسبياً إلى وسط البلد. السعر أعلى بكثير من المتوقع (0.59 JOD/km مقارنة بـ ~0.40 للمسافات الطويلة). يُحتمل أن تكون **سيارة من فئة مختلفة** أو تشمل **رسوم دخول منطقة**. - -### الرحلة 4.12km (سعر متغير 2.63-2.93) - -``` -أغلى 4 كم في عمّان! -نفس المسافة تقريباً مثل 4.07km (1.32 JOD) ولكن أغلى بـ 2× -``` - -**السبب**: هذه الرحلات تخدم مسارات مختلفة تماماً. الـ 4.12km إلى منطقة عبدلي/الشمساني (Mid Zone)، بينما الـ 4.07km في وسط البلد. تؤكد نظرية **التسعير حسب المنطقة (Zone-Based Pricing)**. - ---- - -## 4. تصنيف المسارات حسب المنطقة - -| المنطقة | عدد المسارات | متوسط PPK (JOD/كم) | الميزة | -|---|---|---|---| -| وسط→وسط (Centre) | 3 | 0.25-0.33 | Economy - أرخص فئة | -| ضواحي→ضواحي (Outskirts) | 8 | 0.38-0.46 | Standard - فئة متوسطة | -| وسط→ضواحي | 3 | 0.37-0.44 | خليط | -| مناطق مميزة | 5 | 0.51-1.39 | Premium - فئة عالية | - -**الخريطة الحرارية للسعر**: الرحلات داخل وسط عمّان (31.93-31.97 Lat, 35.88-35.91 Lng) هي الأرخص. الرحلات من/إلى الأطراف الشمالية (32.01+) أو الجنوبية (31.85-) هي الأعلى سعراً لكل كم. - ---- - -## 5. النموذج المُستنتَج (الفرضية الأقوى) - -``` -TaxiF لا تستخدم معادلة خطية بسيطة، بل نظام متعدد المتغيرات: - -1. تصنيف المنطقة (Zone Tier): - - Centre: Economy (0.25 JOD/كم) - - Mid: Standard (0.40 JOD/كم) - - Outskirts/Special: Premium (0.60 JOD/كم) - -2. معادلة السعر الأساسي: - السعر = MAX(الحد_الأدنى, معدل_المنطقة × المسافة) - -3. Surge Multiplier: - السعر_النهائي = السعر_الأساسي × (1.00 - 1.13) - يُطبّق خلال ساعات الليل المتأخرة (23:00-02:00) - -4. الحد الأدنى للسعر (Minimum Fare): - ~1.32 JOD لوسط البلد - ~1.50-1.94 JOD للمناطق البعيدة -``` - ---- - -## 6. توصيات للتحليل المُستقبلي - -1. **توسيع العينة**: جمع بيانات لمسارات جديدة لتأكيد تصنيف المناطق -2. **تحديد فئات السيارات**: إضافة معلومات عن نوع السيارة (Economy/XL/VIP) -3. **أخذ عينات أوقات إضافية**: خاصة أوقات الذروة الصباحية (07:00-09:00) والمسائية (16:00-19:00) -4. **تحليل المنافسين**: مقارنة مع Uber/Careem في نفس المسارات والأوقات -5. **اختبار REgressive**: استخدام ML لتأكيد معاملات السعر لكل منطقة - ---- - -*تم التحليل بناءً على 75 نقطة بيانات من TaxiF في عمّان، الأردن. الفترة: 1-6 يوليو 2026.* - -
diff --git a/walletintaleq.intaleq.xyz/WalletDB.sql b/walletintaleq.intaleq.xyz/WalletDB.sql index c7463039..f955464f 100644 --- a/walletintaleq.intaleq.xyz/WalletDB.sql +++ b/walletintaleq.intaleq.xyz/WalletDB.sql @@ -105,7 +105,7 @@ CREATE TABLE `driverToken` ( `captain_id` varchar(255) NOT NULL, `fingerPrint` varchar(100) NOT NULL, `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP -) ENGINE=MyISAM DEFAULT CHARSET=latin1; +) ENGINE=InnoDB DEFAULT CHARSET=latin1; -- -------------------------------------------------------- @@ -121,7 +121,7 @@ CREATE TABLE `driverWallet` ( `amount` varchar(10) CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL, `paymentMethod` varchar(20) NOT NULL, `dateUpdated` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP -) ENGINE=MyISAM DEFAULT CHARSET=latin1; +) ENGINE=InnoDB DEFAULT CHARSET=latin1; -- -------------------------------------------------------- @@ -266,7 +266,7 @@ CREATE TABLE `kazan` ( `createdAt` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, `naturePrice` varchar(10) NOT NULL, `fuelPrice` varchar(6) NOT NULL -) ENGINE=MyISAM DEFAULT CHARSET=latin1; +) ENGINE=InnoDB DEFAULT CHARSET=latin1; -- -------------------------------------------------------- @@ -324,7 +324,7 @@ CREATE TABLE `passengerWallet` ( `balance` decimal(10,2) NOT NULL, `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -) ENGINE=MyISAM DEFAULT CHARSET=latin1; +) ENGINE=InnoDB DEFAULT CHARSET=latin1; -- -------------------------------------------------------- @@ -357,7 +357,7 @@ CREATE TABLE `paymentsDriverPoints` ( `driverID` varchar(60) NOT NULL, `created_at` datetime DEFAULT CURRENT_TIMESTAMP, `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -) ENGINE=MyISAM DEFAULT CHARSET=latin1; +) ENGINE=InnoDB DEFAULT CHARSET=latin1; -- -------------------------------------------------------- @@ -556,7 +556,7 @@ CREATE TABLE `tokens` ( `token` varchar(333) NOT NULL, `passengerID` varchar(111) NOT NULL, `fingerPrint` varchar(300) CHARACTER SET latin1 COLLATE latin1_swedish_ci NOT NULL -) ENGINE=MyISAM DEFAULT CHARSET=latin1; +) ENGINE=InnoDB DEFAULT CHARSET=latin1; -- -- Indexes for dumped tables diff --git a/walletintaleq.intaleq.xyz/v2/main/ride/cliq/cliq_invoices.sql b/walletintaleq.intaleq.xyz/v2/main/ride/cliq/cliq_invoices.sql deleted file mode 100644 index 47e2f3e1..00000000 --- a/walletintaleq.intaleq.xyz/v2/main/ride/cliq/cliq_invoices.sql +++ /dev/null @@ -1,11 +0,0 @@ -CREATE TABLE IF NOT EXISTS cliq_invoices ( - id INT AUTO_INCREMENT PRIMARY KEY, - invoice_number VARCHAR(50) NOT NULL UNIQUE, - user_id VARCHAR(50) NOT NULL, - user_type VARCHAR(20) NOT NULL, - amount DECIMAL(10, 2) NOT NULL, - cliq_phone VARCHAR(50) NOT NULL, - status VARCHAR(20) DEFAULT 'pending', - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP -);