Update: 2026-07-09 01:55:25

This commit is contained in:
Hamza-Ayed
2026-07-09 01:55:25 +03:00
parent ac0c343f18
commit 7dff7b6973
12 changed files with 374 additions and 28 deletions
+68
View File
@@ -10,6 +10,7 @@
require_once __DIR__ . '/../core/bootstrap.php'; require_once __DIR__ . '/../core/bootstrap.php';
require_once __DIR__ . '/../functions.php'; require_once __DIR__ . '/../functions.php';
require_once __DIR__ . '/../ride/pricing/pricing_helper.php';
try { try {
$con = Database::get('main'); $con = Database::get('main');
@@ -20,6 +21,46 @@ try {
echo "Starting Siro AI Engine v2 (Powered by Pricing Engine)...\n"; echo "Starting Siro AI Engine v2 (Powered by Pricing Engine)...\n";
// ==========================================
// 0. جدول افتراضات عمولة المنافسين (لميزة "أرباحك أعلى" للسائق)
// ==========================================
$con->exec("
CREATE TABLE IF NOT EXISTS `competitor_commission_assumptions` (
`country_code` VARCHAR(5) NOT NULL,
`competitor_name` VARCHAR(64) NOT NULL,
`commission_pct` DECIMAL(5,2) NOT NULL,
`updated_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`country_code`, `competitor_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
");
// زرع أولي بأرقام حقيقية زوّدنا فيها — INSERT IGNORE عشان ما نبهدل أي تعديل يدوي لاحق من الأدمن
$commissionSeed = [
// الأردن — كل التطبيقات المذكورة أعطتنا نفس النسبة 23%
['JO', 'com.careem.ae', 23.0],
['JO', 'com.ubercab', 23.0],
['JO', 'com.jeeny.app', 23.0],
['JO', 'com.taxif.passenger', 23.0],
['JO', 'gogo', 23.0],
['JO', 'petra_ride', 23.0],
// سوريا
['SY', 'yallago', 20.0],
['SY', 'zaken', 17.0],
['SY', 'tufaddal', 15.0],
// مصر — استخدمنا الطرف الأدنى من كل مجال أعطانا إياه المستخدم (تحفظاً)
['EG', 'uber', 25.0],
['EG', 'careem', 20.0],
['EG', 'didi', 15.0],
['EG', 'indrive', 14.0],
];
$seedStmt = $con->prepare("
INSERT IGNORE INTO competitor_commission_assumptions (country_code, competitor_name, commission_pct)
VALUES (:cc, :name, :pct)
");
foreach ($commissionSeed as [$cc, $name, $pct]) {
$seedStmt->execute([':cc' => $cc, ':name' => $name, ':pct' => $pct]);
}
// ========================================== // ==========================================
// 1. التسعير الديناميكي بناءً على المعادلات الإحصائية // 1. التسعير الديناميكي بناءً على المعادلات الإحصائية
// ========================================== // ==========================================
@@ -156,6 +197,33 @@ try {
]); ]);
echo " ✅ Updated $countryName pricing in kazan table.\n"; echo " ✅ Updated $countryName pricing in kazan table.\n";
// ── تخزين معاملات المنافس الحالي بـ Redis لميزة "أرباحك أعلى" للسائق ──
// get.php / add_ride.php يقرأوا هذا المفتاح فقط (بدون أي استعلام DB
// وقت إنشاء الرحلة) عبر estimateDriverEarningsAdvantage()
try {
$commStmt = $con->prepare("
SELECT commission_pct FROM competitor_commission_assumptions
WHERE country_code = :cc AND competitor_name = :name LIMIT 1
");
$commStmt->execute([':cc' => $country, ':name' => $bestFormula['competitor_name']]);
$commissionPct = (float)($commStmt->fetchColumn() ?: 20.0);
$driverComparisonData = [
'competitor_name' => $bestFormula['competitor_name'],
'base_fare' => $bestBaseFare,
'km_rate' => $bestKmRate,
'min_rate' => $bestMinRate,
'commission_pct' => $commissionPct,
'currency' => getCurrencyByCountry($countryName),
'updated_at' => date('Y-m-d H:i:s'),
];
// TTL أطول من فترة الجدولة (3 ساعات) بهامش أمان
$redis->setex("pricing:driver_comparison:{$country}", 14400, json_encode($driverComparisonData));
echo " 💰 Cached driver-comparison data for $countryName (commission assumed: {$commissionPct}%)\n";
} catch (Exception $e) {
echo " ⚠️ Failed to cache driver-comparison data: " . $e->getMessage() . "\n";
}
} }
} }
} catch (Exception $e) { } catch (Exception $e) {
+4 -4
View File
@@ -349,7 +349,7 @@ function notifyPassengerOnRideServer($passenger_id, $payload) {
// ============================================================================ // ============================================================================
// دالة توزيع الطلب (Dispatch Function) - النسخة المصححة // دالة توزيع الطلب (Dispatch Function) - النسخة المصححة
// ============================================================================ // ============================================================================
function dispatchRideToDrivers($driversData, $rideId, $payloadTemplate, $startNameLoc, $encryptionHelper) { function dispatchRideToDrivers($driversData, $rideId, $payloadTemplate, $startNameLoc, $encryptionHelper, $extraFcmData = []) {
$countDrivers = count($driversData); $countDrivers = count($driversData);
error_log("🚀 [DISPATCH_START] RideID: $rideId | Drivers Count: $countDrivers"); error_log("🚀 [DISPATCH_START] RideID: $rideId | Drivers Count: $countDrivers");
@@ -377,11 +377,11 @@ function dispatchRideToDrivers($driversData, $rideId, $payloadTemplate, $startNa
// 3. إرسال FCM // 3. إرسال FCM
if (!empty($driverToken)) { if (!empty($driverToken)) {
$fcmData = [ $fcmData = array_merge([
'DriverList' => $payloadForDriver, 'DriverList' => $payloadForDriver,
'order_id' => (string)$rideId 'order_id' => (string)$rideId
]; ], $extraFcmData);
// 🆕 Destination Match Indicator // 🆕 Destination Match Indicator
$title = "طلب جديد 🔔"; $title = "طلب جديد 🔔";
if (!empty($driver['is_destination_match'])) { if (!empty($driver['is_destination_match'])) {
+50
View File
@@ -116,4 +116,54 @@ function getCurrencyByCountry(string $countryCode): string {
return $currencies[$countryCode] ?? 'SYP'; // افتراضي: ليرة سورية return $currencies[$countryCode] ?? 'SYP'; // افتراضي: ليرة سورية
} }
/**
* يقدّر مقدار زيادة أجرة السائق عندنا مقارنة بأقرب منافس لنفس الرحلة.
* يعتمد على معاملات المنافس المخزّنة بـ Redis (يحدّثها cron_ai_engine.php
* كل 3 ساعات على مفتاح pricing:driver_comparison:{country_code}) — لا يوجد
* أي استعلام مباشر لقاعدة البيانات هنا لتفادي أي تأخير وقت إنشاء الرحلة.
*
* @return array|null ['extra' => float, 'currency' => string, 'competitor_name' => string]
* أو null إذا ما في بيانات كافية أو الفرق مش مجدي يُعرض
*/
function estimateDriverEarningsAdvantage(
string $countryCode,
float $distanceKm,
float $durationMin,
float $ourDriverPrice,
$redis
): ?array {
if (!$redis || $ourDriverPrice <= 0) return null;
try {
$json = $redis->get("pricing:driver_comparison:{$countryCode}");
} catch (Exception $e) {
return null;
}
if (!$json) return null;
$data = json_decode($json, true);
if (!is_array($data) || empty($data['competitor_name'])) return null;
$competitorFare = (float)($data['base_fare'] ?? 0)
+ (float)($data['km_rate'] ?? 0) * $distanceKm
+ (float)($data['min_rate'] ?? 0) * $durationMin;
if ($competitorFare <= 0) return null;
$commissionPct = (float)($data['commission_pct'] ?? 20.0);
$competitorDriverPayout = $competitorFare * (1 - $commissionPct / 100);
$extra = round($ourDriverPrice - $competitorDriverPayout, 2);
// حد أدنى نسبي (3% من أجرة سائق المنافس) حتى ما نعرض فرق تافه أو غير موثوق
$minMeaningful = $competitorDriverPayout * 0.03;
if ($extra <= $minMeaningful) return null;
return [
'extra' => $extra,
'currency' => $data['currency'] ?? 'SYP',
'competitor_name' => $data['competitor_name'],
];
}
?> ?>
+47 -13
View File
@@ -16,23 +16,23 @@ try {
// ================================================================================= // =================================================================================
// 🛠️ دالة مساعدة: إرسال الرحلة لسوق السائقين (Marketplace Broadcast) // 🛠️ دالة مساعدة: إرسال الرحلة لسوق السائقين (Marketplace Broadcast)
// ================================================================================= // =================================================================================
function broadcastRideToMarket($rideId, $lat, $lng, $payloadData) { function broadcastRideToMarket($rideId, $lat, $lng, $payloadData, $extraMarketData = []) {
$url = getenv('LOCATION_SOCKET_URL'); $url = getenv('LOCATION_SOCKET_URL');
$INTERNAL_KEY = function_exists('getInternalSocketKey') ? getInternalSocketKey() : ''; $INTERNAL_KEY = function_exists('getInternalSocketKey') ? getInternalSocketKey() : '';
$marketPayload = [ $marketPayload = array_merge([
'id' => (string)$rideId, 'id' => (string)$rideId,
'start_lat' => $lat, 'start_lat' => $lat,
'start_lng' => $lng, 'start_lng' => $lng,
'end_lat' => $payloadData[3], 'end_lat' => $payloadData[3],
'end_lng' => $payloadData[4], 'end_lng' => $payloadData[4],
'price' => $payloadData[2], 'price' => $payloadData[2],
'carType' => $payloadData[31], 'carType' => $payloadData[31],
'startName' => $payloadData[29], 'startName' => $payloadData[29],
'endName' => $payloadData[30], 'endName' => $payloadData[30],
'distance' => $payloadData[11], 'distance' => $payloadData[11],
'duration' => $payloadData[15], 'duration' => $payloadData[15],
'passengerRate' => $payloadData[33], 'passengerRate' => $payloadData[33],
]; ], $extraMarketData);
$postData = [ $postData = [
'action' => 'market_new_ride', 'action' => 'market_new_ride',
@@ -254,6 +254,36 @@ try {
// ═══════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════
$kazan = (float) $price - (float) $price_for_driver; $kazan = (float) $price - (float) $price_for_driver;
$passengerFp = isset($_SERVER['HTTP_X_DEVICE_FP']) ? $_SERVER['HTTP_X_DEVICE_FP'] : ''; $passengerFp = isset($_SERVER['HTTP_X_DEVICE_FP']) ? $_SERVER['HTTP_X_DEVICE_FP'] : '';
// ── 🆕 "أرباحك أعلى" — مقارنة أجرة السائق عندنا مع أقرب منافس ─────
require_once __DIR__ . '/../pricing/pricing_helper.php';
$extraDispatchData = [];
try {
$rideCountryCode = 'JO';
$stmtCountry = $con->prepare("
SELECT country_code FROM passenger_opening_locations
WHERE passenger_id = :pid
ORDER BY created_at DESC LIMIT 1
");
$stmtCountry->execute([':pid' => $passenger_id]);
$foundCountry = $stmtCountry->fetchColumn();
if ($foundCountry) $rideCountryCode = strtoupper($foundCountry);
$durationMinForEarnings = is_numeric($duration_text) ? (float)$duration_text : 0.0;
$earnings = estimateDriverEarningsAdvantage(
$rideCountryCode,
(float)$distance,
$durationMinForEarnings,
(float)$price_for_driver,
$redis ?? null
);
if ($earnings) {
$extraDispatchData['driver_earnings_extra'] = (string)$earnings['extra'];
$extraDispatchData['driver_earnings_currency'] = $earnings['currency'];
}
} catch (Exception $e) {
error_log("[add_ride] Driver earnings estimate failed: " . $e->getMessage());
}
$payload = [ $payload = [
(string) $startLat, (string) $startLat,
(string) $startLng, (string) $startLng,
@@ -290,6 +320,10 @@ try {
number_format($kazan, 2, '.', ''), number_format($kazan, 2, '.', ''),
(string) $passenger_rating, (string) $passenger_rating,
(string) $is_prime, // 👑 Index 34: Prime Status (string) $is_prime, // 👑 Index 34: Prime Status
// 🆕 Index 35/36: "أرباحك أعلى" — تُقرأ من نافذة الـ Overlay (order_over_lay.dart)
// اللي بتوصلها البيانات كـ List مش كـ Map مسمّى مثل FCM
isset($extraDispatchData['driver_earnings_extra']) ? $extraDispatchData['driver_earnings_extra'] : '',
isset($extraDispatchData['driver_earnings_currency']) ? $extraDispatchData['driver_earnings_currency'] : '',
]; ];
// Direct dispatch للسائقين القريبين // Direct dispatch للسائقين القريبين
@@ -369,14 +403,14 @@ try {
} }
if (!empty($driversData)) { if (!empty($driversData)) {
dispatchRideToDrivers($driversData, $insertedId, $payload, $start_name_loc, $encryptionHelper); dispatchRideToDrivers($driversData, $insertedId, $payload, $start_name_loc, $encryptionHelper, $extraDispatchData);
error_log("[add_ride] Dispatched RideID=$insertedId to " . count($driversData) . " drivers."); error_log("[add_ride] Dispatched RideID=$insertedId to " . count($driversData) . " drivers.");
} else { } else {
error_log("[add_ride] No direct drivers found for RideID=$insertedId — market only."); error_log("[add_ride] No direct drivers found for RideID=$insertedId — market only.");
} }
// Broadcast للـ marketplace دائماً // Broadcast للـ marketplace دائماً
broadcastRideToMarket($insertedId, $startLat, $startLng, $payload); broadcastRideToMarket($insertedId, $startLat, $startLng, $payload, $extraDispatchData);
// رد النجاح للتطبيق // رد النجاح للتطبيق
printSuccess($insertedId); printSuccess($insertedId);
+99
View File
@@ -0,0 +1,99 @@
<div dir="rtl" align="right">
<h1>تقرير نظام التسعير الشامل</h1>
<h2>1. دورة حياة التسعير: كيف تتم عملية التسعير من البداية للنهاية؟</h2>
نظام التسعير هو نظام ديناميكي وذكي يعتمد على تحليل السوق باستمرار لضمان التنافسية والربحية. تمر العملية بالمراحل التالية:
<br><br>
<b>المرحلة الأولى: جمع البيانات الحية</b><br>
تقوم سكريبتات الجافاسكريبت بالعمل على مدار الساعة لجلب أسعار المنافسين من السوق وحالة الطلب في مناطق مختلفة، ويتم تغذية قاعدة البيانات بهذه الأرقام الأولية.
<br><br>
<b>المرحلة الثانية: تحليل البيانات بالذكاء الاصطناعي</b><br>
يعمل السكريبت المخصص للذكاء الاصطناعي على تحليل البيانات الواردة وفك تشفير معادلات التسعير الخاصة بالمنافسين (سعر الكيلومتر، سعر البداية) ويقوم بتخزين هذه المعادلات.
<br><br>
<b>المرحلة الثالثة: محرك الثبات والاستقرار</b><br>
وهو السكريبت الجديد، ومهمته الأساسية هي منع التذبذب العشوائي لأسعارنا.
<br>
- يقوم بمقارنة السعر الحالي للمنافس مع متوسط السعر في آخر 7 أيام.
<br>
- إذا قام المنافس بتنزيل السعر فجأة، يعتبره النظام عرضاً ترويجياً مؤقتاً ولا يغير سعرنا الأساسي، بل يقترح استخدام أكواد خصم.
<br>
- إذا استمر انخفاض أو ارتفاع السعر لفترة طويلة، يقرر النظام أن هذا تغيير حقيقي في السوق، ويقترح تعديل سعرنا بحد أقصى لضمان الاستقرار.
<br>
- (ملاحظة: هذا المحرك يعمل حالياً في وضع المراقبة ولا يقوم بتغيير الأسعار تلقائياً بل يسجل الاقتراحات).
<br><br>
<b>المرحلة الرابعة: التسعير الموسمي</b><br>
يتدخل سكريبت التسعير الموسمي في أوقات معينة (مثل أوقات الإفطار في رمضان، الأعياد، أو حالات الطقس السيئة) ليقوم برفع السعر تدريجياً وبنسبة مدروسة ليتناسب مع الطلب المتزايد.
<br><br>
<b>المرحلة الخامسة: اقتناص الذروة وتوجيه السائقين</b><br>
- <b>اكتشاف الذروة:</b> يقوم سكريبت الذروة بتحليل الخريطة واكتشاف المناطق التي قام فيها المنافسون برفع أسعارهم فجأة.
<br>
- <b>تحفيز السائقين:</b> بمجرد اكتشاف منطقة ذروة، يتدخل سكريبت العمولة ليقوم بتخفيض عمولة التطبيق على السائقين في تلك المنطقة المحددة، مما يشجع السائقين على التوجه فوراً لهذه المنطقة لتلبية الطلب العالي وضرب المنافسين.
<hr>
<h2>2. تصحيح وقت وظيفة الجدولة الأخيرة</h2>
لقد قمت بإضافة الوظيفة الخاصة بمحرك الثبات والاستقرار في لوحة التحكم بالتوقيت التالي:
</div>
```text
0 0 * * 0
```
<div dir="rtl" align="right">
(وهذا يعني أنه سيعمل مرة واحدة فقط في الأسبوع، يوم الأحد منتصف الليل).
<br><br>
<b>التصحيح المطلوب:</b><br>
بناءً على تصميم الكود، هذا السكريبت يجب أن يراقب الأسعار باستمرار بالتوازي مع محرك الذكاء الاصطناعي. المطور كتب في الكود ملاحظة صريحة أن الجدولة المقترحة هي كل 3 ساعات.
<br><br>
لذلك، يجب عليك حذفه وإضافته مرة أخرى بالتوقيت التالي:
<br>
<b>الدقائق:</b> 0<br>
<b>الساعات:</b> */3<br>
<b>باقي الحقول:</b> *<br>
ليصبح الأمر كالتالي:
</div>
```text
0 */3 * * * /usr/bin/php8.4 /home/intaleqapp-jordan-siro/htdocs/jordan-siro.intaleqapp.com/backend/bot/cron_pricing_stability_engine.php
```
<div dir="rtl" align="right">
<hr>
<h2>3. شرح جدول الوظائف المجدولة بالكامل وتوقيتاتها</h2>
فيما يلي جدول يوضح وظيفة كل ملف والتوقيت الذي يعمل به:
<br><br>
</div>
| التوقيت | الوظيفة |
|---|---|
| `0 */3 * * *` | `cron_ai_engine.php`: تحليل أسعار المنافسين بالذكاء الاصطناعي لاستنتاج معادلاتهم. |
| `0 */3 * * *` | `cron_pricing_stability_engine.php`: (الوقت المقترح) يراقب استقرار الأسعار ويقترح التعديلات دون تذبذب. |
| `0 * * * *` | `cron_seasonal_pricing.php`: يطبق زيادات الأسعار في المناسبات كل ساعة. |
| `*/10 * * * *` | `cron_surge_opportunity.php`: يكتشف مناطق الذروة النشطة بناءً على أسعار المنافسين الحية كل 10 دقائق. |
| `*/5 * * * *` | `cron_kazan_adjuster.php`: يعدّل عمولة التطبيق ويخفضها للسائقين في مناطق الذروة كل 5 دقائق. |
| `*/16 * * * *` | `cron_generate_heatmap_cache.php`: يولد خريطة حرارية لأماكن الطلب للسائقين كل 16 دقيقة. |
| `0 */1 * * *` | `cron_predictive_demand.php`: يتنبأ بأماكن الطلب المستقبلية بناءً على البيانات التاريخية كل ساعة. |
| `0 * * * *` | `cron_empty_results_to_db.php`: ينظف ويسجل نتائج البحث الفارغة لتحليلها لاحقاً كل ساعة. |
| `0 10 * * *` | `cron_passenger_reengagement.php`: يعيد استهداف الركاب الخاملين برسائل أو عروض يومياً الساعة 10 صباحاً. |
| `0 * * * *` | `cron_auto_marketing_pusher.php`: يرسل حملات التسويق الآلية للمستخدمين كل ساعة. |
| `0 0 * * 0` | `cron_weekly_health_report.php`: يرسل تقرير أسبوعي عن صحة وعمل النظام كل أحد في منتصف الليل. |
| `0 */3 * * *` | `telegram_scraper.php`: أداة تسويقية لاستخراج بيانات من المنصات الأخرى كل 3 ساعات. |
| `0 10 * * 0` | `cron_weekly_report.php`: يرسل تقرير أسبوعي إداري أو تسويقي كل أحد الساعة 10 صباحاً. |
| `0 */4 * * *` | `cron_insert_task.php`: يضيف مهام مجدولة للنظام كل 4 ساعات. |
| `*/15 * * * *` | `generate_10_tasks_cli.php`: يولد مهام مصغرة أو وهمية لأغراض الاختبار كل 15 دقيقة. |
<div dir="rtl" align="right">
<hr>
<h2>4. مهام الجافاسكريبت المستمرة</h2>
هذه المهام لا تعمل بنظام الجدولة المتقطعة، بل هي عمليات تعمل دائماً ولا تتوقف أبداً وتدار بواسطة أداة مدير العمليات:
<br><br>
1. <b>التتبع الشامل</b>: مهمته التتبع الشامل والمتواصل لأسعار المنافسين في جميع المناطق طوال الوقت.
<br>
2. <b>اكتشاف الذروة السريع</b>: متخصص في المراقبة السريعة جداً للمناطق لاكتشاف أي ارتفاع مفاجئ فور حدوثه لإرسال البيانات إلى السكريبتات الأخرى.
<br>
3. <b>التقارير الحية</b>: يقوم بمعالجة البيانات وتصدير التقارير الحية للوحة التحكم أولاً بأول.
<br><br>
هذه السكريبتات الثلاثة هي التي تجمع المواد الخام التي تقوم السكريبتات المذكورة في الأعلى بتحليلها واستخدامها لتحديد الأسعار النهائية للتطبيق.
</div>
@@ -159,6 +159,9 @@ class FirebaseMessagesController extends GetxController {
'DriverList': myList, 'DriverList': myList,
'body': body, 'body': body,
'is_destination_match': message.data['is_destination_match'] ?? '0', 'is_destination_match': message.data['is_destination_match'] ?? '0',
// 🆕 "أرباحك أعلى" — يُرسَل فقط عند وجود فرق إيجابي حقيقي
'driver_earnings_extra': message.data['driver_earnings_extra'],
'driver_earnings_currency': message.data['driver_earnings_currency'],
}); });
} }
break; break;
@@ -411,6 +411,9 @@ class LocationController extends GetxController with WidgetsBindingObserver {
'DriverList': driverList, 'DriverList': driverList,
'body': 'New Trip Request via Socket ⚡', 'body': 'New Trip Request via Socket ⚡',
'is_destination_match': rideData['is_destination_match'] ?? '0', 'is_destination_match': rideData['is_destination_match'] ?? '0',
// 🆕 "أرباحك أعلى" — نفس الحقل الجاي من FCM، متوفر أيضاً عبر السوكيت
'driver_earnings_extra': rideData['driver_earnings_extra'],
'driver_earnings_currency': rideData['driver_earnings_currency'],
}); });
} else { } else {
Log.print( Log.print(
@@ -65,6 +65,10 @@ class OrderRequestController extends GetxController
String tripPrice = "--"; String tripPrice = "--";
bool isDestinationMatch = false; // 🆕 Destination Match flag bool isDestinationMatch = false; // 🆕 Destination Match flag
// 🆕 Driver Earnings Advantage — "أرباحك أعلى" badge
double? driverEarningsExtra;
String driverEarningsCurrency = 'ل.س';
String timeToPassenger = "Calculating...".tr; String timeToPassenger = "Calculating...".tr;
String distanceToPassenger = "--"; String distanceToPassenger = "--";
@@ -165,6 +169,7 @@ class OrderRequestController extends GetxController
args['is_destination_match'] == true) { args['is_destination_match'] == true) {
isDestinationMatch = true; isDestinationMatch = true;
} }
_parseEarningsData(args);
} }
// ب) هل هي قادمة من Socket بالمفاتيح الرقمية ("0", "1", ...)؟ // ب) هل هي قادمة من Socket بالمفاتيح الرقمية ("0", "1", ...)؟
else { else {
@@ -175,6 +180,7 @@ class OrderRequestController extends GetxController
args['is_destination_match'] == true) { args['is_destination_match'] == true) {
isDestinationMatch = true; isDestinationMatch = true;
} }
_parseEarningsData(args);
} }
} }
} }
@@ -189,6 +195,21 @@ class OrderRequestController extends GetxController
"📍 Parsed Coordinates: Pax($latPassenger, $lngPassenger) -> Dest($latDestination, $lngDestination)"); "📍 Parsed Coordinates: Pax($latPassenger, $lngPassenger) -> Dest($latDestination, $lngDestination)");
} }
/// 🆕 يقرأ حقل "أرباحك أعلى" من بيانات الطلب (FCM أو Socket) إن وُجد
void _parseEarningsData(Map args) {
final rawExtra = args['driver_earnings_extra'];
if (rawExtra != null) {
final parsed = double.tryParse(rawExtra.toString());
if (parsed != null && parsed > 0) {
driverEarningsExtra = parsed;
}
}
final rawCurrency = args['driver_earnings_currency'];
if (rawCurrency != null && rawCurrency.toString().isNotEmpty) {
driverEarningsCurrency = rawCurrency.toString();
}
}
/// 🔥 دالة ذكية تجلب القيمة سواء كانت البيانات في List أو Map /// 🔥 دالة ذكية تجلب القيمة سواء كانت البيانات في List أو Map
dynamic _getValueAt(int index) { dynamic _getValueAt(int index) {
// الأولوية للقائمة // الأولوية للقائمة
@@ -24,6 +24,10 @@ class OrderData {
final String? rawStartCoordinates; final String? rawStartCoordinates;
final String? rawEndCoordinates; final String? rawEndCoordinates;
// 🆕 "أرباحك أعلى" — الفرق الإيجابي بأجرة السائق مقارنة بالمنافس (إن وُجد)
final double? driverEarningsExtra;
final String? driverEarningsCurrency;
OrderData({ OrderData({
required this.customerName, required this.customerName,
required this.customerToken, required this.customerToken,
@@ -40,6 +44,8 @@ class OrderData {
required this.passengerRate, required this.passengerRate,
this.rawStartCoordinates, this.rawStartCoordinates,
this.rawEndCoordinates, this.rawEndCoordinates,
this.driverEarningsExtra,
this.driverEarningsCurrency,
}); });
// --- NEW: Factory constructor to create an instance from a Map --- // --- NEW: Factory constructor to create an instance from a Map ---
@@ -74,6 +80,8 @@ class OrderData {
// For nullable strings, direct access is fine as it returns null if the key doesn't exist // For nullable strings, direct access is fine as it returns null if the key doesn't exist
rawStartCoordinates: map['rawStartCoordinates'], rawStartCoordinates: map['rawStartCoordinates'],
rawEndCoordinates: map['rawEndCoordinates'], rawEndCoordinates: map['rawEndCoordinates'],
driverEarningsExtra: (map['driverEarningsExtra'] as num?)?.toDouble(),
driverEarningsCurrency: map['driverEarningsCurrency']?.toString(),
); );
} }
@@ -118,6 +126,15 @@ class OrderData {
rawStartCoordinates: list.isNotEmpty ? list[0].toString() : null, rawStartCoordinates: list.isNotEmpty ? list[0].toString() : null,
rawEndCoordinates: list.length > 1 ? list[1].toString() : null, rawEndCoordinates: list.length > 1 ? list[1].toString() : null,
// 🆕 Index 35/36: "أرباحك أعلى"
driverEarningsExtra: list.length > 35 && list[35].toString().isNotEmpty
? double.tryParse(list[35].toString())
: null,
driverEarningsCurrency:
list.length > 36 && list[36].toString().isNotEmpty
? list[36].toString()
: null,
); );
} }
@@ -182,6 +199,8 @@ class OrderData {
'passengerRate': passengerRate, 'passengerRate': passengerRate,
'rawStartCoordinates': rawStartCoordinates, 'rawStartCoordinates': rawStartCoordinates,
'rawEndCoordinates': rawEndCoordinates, 'rawEndCoordinates': rawEndCoordinates,
'driverEarningsExtra': driverEarningsExtra,
'driverEarningsCurrency': driverEarningsCurrency,
}; };
} }
} }
@@ -529,6 +529,40 @@ class _OrderOverlayState extends State<OrderOverlay>
), ),
], ],
), ),
// 🆕 "أرباحك أعلى" — يظهر فقط إذا في فرق إيجابي حقيقي من الباك اند
if (order.driverEarningsExtra != null &&
order.driverEarningsExtra! > 0) ...[
const SizedBox(height: 10),
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
decoration: BoxDecoration(
color: AppColors.priceHighlight.withOpacity(0.12),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: AppColors.priceHighlight.withOpacity(0.35),
width: 1),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.trending_up_rounded,
color: AppColors.priceHighlight, size: 16),
const SizedBox(width: 6),
Text(
"أرباحك أعلى بـ ${order.driverEarningsExtra!.toStringAsFixed(2)} ${order.driverEarningsCurrency ?? ''} من التطبيقات الثانية"
.tr,
style: const TextStyle(
color: AppColors.priceHighlight,
fontSize: 11.5,
fontWeight: FontWeight.w700,
),
textAlign: TextAlign.center,
),
],
),
),
],
const SizedBox(height: 12), const SizedBox(height: 12),
Divider(color: AppColors.gray.withOpacity(0.2), thickness: 1), Divider(color: AppColors.gray.withOpacity(0.2), thickness: 1),
const SizedBox(height: 12), const SizedBox(height: 12),
@@ -238,14 +238,16 @@ class OrderRequestPage extends StatelessWidget {
), ),
// ── Earnings Compare Badge ────────────────── // ── Earnings Compare Badge ──────────────────
// يتم جلبه من API مشابه أو يمكن ربطه ببيانات الكنترولر لاحقاً. // مصدر البيانات: driver_earnings_extra/currency القادمة من add_ride.php
// نتركه متاحاً للتحديث عبر الكنترولر (controller.driverBadgeText, controller.extraEarnings). // (عبر FCM أو Socket) — controller.driverEarningsExtra يبقى null إذا
// سنستخدم قيم افتراضية للتأكد من الريندر (UI render) الآن // ما في فرق إيجابي حقيقي، فالويدجت نفسه بيختفي تلقائياً.
// سيتم تفعيله من الباك إند
const SizedBox(height: 5), const SizedBox(height: 5),
const DriverEarningsBadge( DriverEarningsBadge(
earningsLabel: "أرباحك أعلى هنا مقارنة بـ المنافسين", earningsLabel: controller.driverEarningsExtra != null
extraAmount: 2.50, // القيمة هنا ستُسحب من الـ Socket / API قريباً ? "أرباحك أعلى هنا مقارنة بـ المنافسين"
: null,
extraAmount: controller.driverEarningsExtra,
currency: controller.driverEarningsCurrency,
), ),
const SizedBox(height: 15), const SizedBox(height: 15),
@@ -110,6 +110,13 @@ class RideAvailableCard extends StatelessWidget {
const RideAvailableCard({Key? key, required this.rideInfo}) : super(key: key); const RideAvailableCard({Key? key, required this.rideInfo}) : super(key: key);
/// يحوّل driver_earnings_extra (نص من الـ Socket) إلى double، أو null إن لم يوجد
double? get _earningsExtra {
final raw = rideInfo['driver_earnings_extra'];
if (raw == null) return null;
return double.tryParse(raw.toString());
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Card( return Card(
@@ -128,10 +135,16 @@ class RideAvailableCard extends StatelessWidget {
const Divider(height: 24, thickness: 0.5), const Divider(height: 24, thickness: 0.5),
_buildRideDetails(), _buildRideDetails(),
const SizedBox(height: 12), const SizedBox(height: 12),
// ── إضافة شارة الأرباح هنا ── // ── شارة الأرباح — مصدرها rideInfo['driver_earnings_extra'] القادم
const DriverEarningsBadge( // من add_ride.php (broadcastRideToMarket عبر Socket). ملاحظة: الرحلات
earningsLabel: "أرباحك أعلى هنا مقارنة بـ المنافسين", // المجلوبة من الجلب الأولي (getRideWaiting.php) ما زالت لا تحمل هذا
extraAmount: 2.50, // القيمة هنا ستُسحب من الـ Socket / API قريباً // الحقل، فالشارة بتختفي تلقائياً لحد ما تُحدَّث تلك النقطة لاحقاً.
DriverEarningsBadge(
earningsLabel: _earningsExtra != null
? "أرباحك أعلى هنا مقارنة بـ المنافسين"
: null,
extraAmount: _earningsExtra,
currency: rideInfo['driver_earnings_currency']?.toString() ?? 'ل.س',
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
_buildAcceptButton(), _buildAcceptButton(),