الأساس: common/cache — كاش-جانبي فوق Redis. مبدأ صارم: فشل Redis لا يُسقط الطلب (يُسجَّل ويُرجَع للقاعدة) — الكاش تحسين أداء لا مصدر حقيقة. - G1: لغة المستخدم في Redis (كانت استعلاماً قبل كل إشعار)؛ تُكتب عند PATCH /users/me فيصير الإصابة دائمة - G2/G3: توكنات FCM في Redis — يُبطَل عند register وعند اكتشاف توكن ميت. البث الجماعي صار بلا استعلامات قاعدة - G4: كاش tenant resolve/config + إبطال عند الإنشاء - G5: كاش التعرفة و ride-types + إبطال صريح عند أي إنشاء - G6: التقييم يتراكم في Redis، والقاعدة تُكتب مرة واحدة يومياً لكل طرف (حجز الكتابة ذرّي بـLua). التأخير مقصود ليبعد الاحتكاك بين الطرفين. أُضيف ratings.target_user_id و users.rating — تقييم السائق للراكب كان يُخزَّن بلا هدف فلا يُجمَّع أبداً - كاش الخرائط (route/reverse/geocode) بإحداثيات مقرَّبة 4 خانات (~11م): نداء انطلق كان يهيمن على p50 (2.9s). لا يُخزَّن الرجوع لخط مستقيم ولا استجابة فاشلة تصحيح: ادّعيت سابقاً أن tenants.resolve يعمل على كل request — خطأ. tenant_id يأتي من الـJWT؛ resolve يُستدعى عند الدخول و3 كنترولرات فقط. الحمل الأثقل فعلياً هو رفع موقع السائق (المجموعة H). هجرة: RatingsBothParties. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
191 lines
22 KiB
Markdown
191 lines
22 KiB
Markdown
# 17 — Backlog الباك إند (مراجعة المالك + مقارنة سيرو)
|
||
|
||
> مصدره: مراجعة المالك (2026-07-17) + قراءة مباشرة لجداول سيرو (`schema_primary.sql`, `schema_ride.sql`).
|
||
> الترتيب حسب الأثر. نمشي **مجموعة مجموعة**.
|
||
|
||
---
|
||
|
||
## المجموعة A — الزمن الحقيقي والإشعارات (الأعلى أولوية) — ✅ منفَّذة
|
||
> شكوى المالك الأساسية: «تسأل القاعدة كثيراً، أقرب للـ polling»، والإشعارات ناقصة.
|
||
|
||
| # | البند | التفصيل | الحالة |
|
||
|---|-------|---------|--------|
|
||
| A1 | **FCM على كل حالات الرحلة** | حالياً push عند `assigned` فقط. المطلوب: FCM **+** WebSocket على **كل** انتقال. ضروري للخلفية (background). | ✅ `notifyParties` تُنادى من كل انتقال + `priority: high` + حذف التوكنات الميتة |
|
||
| A2 | **ترجمة الإشعارات** | الإشعارات تُرسل إنجليزي والهاتف عربي → نصوص الإشعارات في **ملفات ترجمة** وتُرسل حسب لغة المستخدم. | ✅ `common/i18n` (ar/en) + عمود `users.language` |
|
||
| A3 | **تقليل استعلامات القاعدة** | حالة الرحلة الجارية + المواقع في **Redis**؛ القاعدة للحقيقة الدائمة فقط. (الآن كل انتقال يقرأ/يكتب عدة مرات + يقرأ السائق ثانيةً). | ✅ `TripStateService` (hash لكل رحلة نشطة، TTL 6س) — الانتقال صار UPDATE شرطي + قراءة واحدة |
|
||
| A4 | **Race condition عند القبول** | سائقان يقبلان بنفس اللحظة → **قبول ذرّي** (Redis SETNX / UPDATE شرطي `WHERE status='searching'`). أول قبول يفوز، والثاني يُرفض بوضوح. | ✅ CAS بـLua في Redis + `UPDATE … WHERE status='searching'` كحَكَم نهائي |
|
||
| A5 | **إلغاء العرض عند القبول** | فور القبول: بث WebSocket **+ FCM** لبقية السائقين المعروض عليهم → «الرحلة لم تعد متاحة» فتختفي من شاشتهم/الـ overlay. | ✅ مجموعة عروض في Redis + `trip:offer_taken` + FCM |
|
||
| A6 | **الرحلات المتاحة (available rides)** | قائمة طلبات متاحة يسحبها السائق (بديل/مكمّل للعرض المباشر). | ✅ `GET /trips/available` |
|
||
| A7 | **overlay أندرويد** | معلومات الرحلة للقبول/الرفض فوق التطبيقات — يحتاج FCM data-message + payload كامل. | ✅ `dataOnly` + payload كامل (نقاط، مسافة، أجرة، فئة، دفع) |
|
||
|
||
**مؤجَّل من A:** سجل الأحداث (`trip_events`) ما زال يُكتب متزامناً داخل الطلب — نقله إلى BullMQ يبقى تحسيناً مفتوحاً (`worker.ts` لا يزال هيكلاً فارغاً).
|
||
|
||
---
|
||
|
||
## المجموعة B — اكتمال نموذج الرحلة
|
||
> مقارنة بجدول `ride` في سيرو + قواعد التشغيل.
|
||
|
||
| # | البند | التفصيل |
|
||
|---|-------|---------|
|
||
| B1 | **حالة `started` وفصل الوصول عن البدء** | السائق وصل ≠ الرحلة بدأت. الراكب قد لا يرد/يلغي. نحتاج تمييزاً صريحاً. |
|
||
| B2 | **عدّاد انتظار 5 دقائق (قانون)** | يبدأ عند وصول السائق (بعد **تأكيد المسافة** من فلاتر). عند انتهائه: خيار للراكب — يبقى السائق منتظراً، أو **تعويض** فترة الانتظار. |
|
||
| B3 | **احتساب مشوار الوصول للراكب** | المسافة/الزمن من موقع السائق حتى الراكب تُحتسب بالتعرفة (دقائق + مسافة). |
|
||
| B4 | **نقاط توقف (stops)** | نقطتا توقف ضمن الرحلة (كما في سيرو/شير). |
|
||
| B5 | **حجز مسبق/جدولة** | جداول الرحلة (`date`/`time`/`endtime` في سيرو). |
|
||
| B6 | **فصل السعر** | `price_for_driver` مقابل `price_for_passenger` (العمولة) — **ناقص عندنا**. |
|
||
| B7 | **طوابع زمنية دقيقة** | `DriverIsGoingToPassenger` · `rideTimeStart` · `rideTimeFinish` (سيرو) — عندنا `assigned_at`/`completed_at` فقط. |
|
||
| B8 | **مطابقة الوجهة** | `is_destination_match` + خصم الراكب. |
|
||
| B9 | **تعديل التعرفة من لوحة الأدمن** | جداول تعرفة قابلة للتحرير (موجودة كـ jsonb — نحتاج واجهة/نقاط CRUD). |
|
||
| B10 | **كتالوج أنواع الرحلات العالمي** | `ride_types` فكرته سليمة (الأدمن/المستأجر يضيف أنواعه). المطلوب: كتالوج جاهز بما هو شائع عالمياً كخيارات جاهزة للاختيار (عندنا 6 فقط الآن). |
|
||
| B11 | **تعرفة بالوزن** | بُعد تسعير إضافي بالوزن (للشحن/التوصيل) بجانب المسافة والزمن. الاستعلام عن التعرفة من Redis خط أول (G5). |
|
||
|
||
---
|
||
|
||
## المجموعة C — بيانات السائق والمركبة (من سيرو)
|
||
|
||
| # | البند | التفصيل |
|
||
|---|-------|---------|
|
||
| C1 | **CarRegistration كامل** | `vin` · `car_plate` · `make` · `model` · `year` · `expiration_date` · **`color` + `color_hex`** (لتلوين السيارة في فلاتر) · `owner` · `fuel` · `isDefault` · `vehicle_category_id` · `fuel_type_id` · `status`. |
|
||
| C2 | **حقول السائق** | `gender` · `national_number` (فريد) · `name_arabic` · `first/last_name` · `birthdate` · `license_type/categories/issue/expiry` · `address` · `accountBank`/`bankCode` · `employmentType` · `maritalStatus` · **`rejected_reason`** (سبب رفض خدمة العملاء). |
|
||
| C3 | **`ai_data` + `user_input`** | تخزين مخرجات Gemini الخام **و** ما أدخله السائق — للمقارنة والتدقيق (نمط سيرو). |
|
||
| C4 | **صور السيارة ×2** | صورتان للمركبة (لا واحدة). |
|
||
| C5 | **فيديو/liveness للوجه** | تأكيد حيّ للوجه (غير السيلفي الثابت). |
|
||
|
||
---
|
||
|
||
## المجموعة D — الأمان والمصادقة
|
||
|
||
| # | البند | التفصيل |
|
||
|---|-------|---------|
|
||
| D1 | **تطبيع أرقام الهاتف (JO/EG/SY)** | خصوصاً **مصر**: الناس تكتب `01…` بدل `1…` وتظن المفتاح `2` لا `20`. نحتاج normalize احترافي لكل دولة. |
|
||
| D2 | **بصمة الجهاز (device fingerprint)** | تُرسل من فلاتر مع **كل** request وتُربط بالجلسة (نمط سيرو: التوكن المسروق لا يعمل على جهاز آخر). |
|
||
| D3 | **HMAC للعمليات الحساسة** | خصوصاً **المدفوعات** — توقيع الطلب. |
|
||
|
||
---
|
||
|
||
## المجموعة E — كشف الاحتيال (من `driver_ride_scam`)
|
||
|
||
| # | البند | التفصيل |
|
||
|---|-------|---------|
|
||
| E1 | **تسجيل زر الاتصال** | `isDriverCallPassenger` لكل رحلة (سيرو). |
|
||
| E2 | **ربط الاتصال بالإلغاء** | اتصال ثم إلغاء = مؤشر اتفاق خارج التطبيق. **3 إلغاءات/يوم → إنذار**؛ التكرار → إجراء. |
|
||
|
||
---
|
||
|
||
## المجموعة F — الدردشة
|
||
| F1 | إشعار الرسالة يُرسل **فقط** إذا لم تكن صفحة الدردشة مفتوحة (المعالجة الأساسية في فلاتر؛ الباك إند يرسل دائماً ويترك القرار للعميل أو عبر presence). |
|
||
|
||
---
|
||
|
||
## المجموعة G — Redis خط أول والقاعدة خط احتياط — ✅ منفَّذة
|
||
> مراجعة المالك (2026-07-17، الجولة الثانية): «كل ما بدي أبعث notification أستعلم من القاعدة — هذا ثقيل. الريدز خط أول، القاعدة نقطة الاحتياط».
|
||
> القاعدة: البيانات **الساخنة والمتكررة** تُقرأ من Redis؛ القاعدة تُقرأ مرة واحدة عند البرود (cache miss) ثم تُكتب في Redis.
|
||
> الأساس: `common/cache/cache.service.ts` — كاش-جانبي مع مبدأ **فشل Redis لا يُسقط الطلب** (يُسجَّل ويُرجَع للقاعدة).
|
||
|
||
| # | البند | التفصيل | الحالة |
|
||
|---|-------|---------|--------|
|
||
| G1 | **لغة المستخدم في Redis** | فلاتر يفحص لغة الجهاز عند الفتح ويرفعها عبر `PATCH /users/me` → تُكتب في Redis مباشرة → الإشعار يقرأ من هناك. | ✅ `user:lang:{tenant}:{user}` (TTL يوم) + كتابة عند التحديث |
|
||
| G2 | **توكنات الأجهزة (FCM) في Redis** | كان استعلام `device_tokens` قبل كل إرسال. | ✅ `user:fcm:{tenant}:{user}`؛ يُبطَل عند `register` وعند اكتشاف توكن ميت |
|
||
| G3 | **البث الجماعي بلا استعلام لكل مستخدم** | — | ✅ التوكنات واللغة من الكاش، فالبث الجماعي صار بلا استعلامات قاعدة أصلاً |
|
||
| G4 | **`tenant` في Redis** | كاش `resolve` (slug↔UUID) + `config` مع إبطال عند الإنشاء/التعديل. | ✅ `tenant:{slugOrId}` (TTL ساعة) |
|
||
| G5 | **التعرفة و ride-types في Redis** | كاش مع **إبطال صريح** عند إنشاء تعرفة/نوع جديد. | ✅ `tariff:{t}:{city}:{class}` و `ridetypes:{t}` (TTL 15د) |
|
||
| G6 | **التقييم بتراكم يومي** | مجموع/عدد في Redis، والقاعدة تُكتب **مرة واحدة يومياً** لكل طرف (حجز الكتابة ذرّي بـLua). التأخير مقصود ليبعد الاحتكاك. **للطرفين معاً**. | ✅ + أُضيف `ratings.target_user_id` و`users.rating` — تقييم السائق للراكب كان يُخزَّن **بلا هدف فلا يُجمَّع أبداً** |
|
||
| — | **كاش الخرائط** | route/reverse/geocode في Redis بإحداثيات مقرَّبة 4 خانات (~11م). | ✅ TTL يوم. لا يُخزَّن الرجوع لخط مستقيم ولا استجابة فاشلة |
|
||
| G7 | **سعة Redis** | رصد مساحة أكبر أو Redis منفصل عند الحاجة (خصوصاً dispatch + المواقع). حالياً DB 3 مشترك — راجع docs/14. | ⏳ قرار تشغيلي — يُراجَع بعد المجموعة H |
|
||
|
||
**تصحيح لادّعاء سابق في هذا المستند:** كُتب أن `TenantsService.resolve` يعمل على كل request وأنه «أعلى نسبة قراءات في النظام». **هذا خطأ** — الـmiddleware يضع نص الهيدر في السياق فقط، و`tenant_id` يأتي جاهزاً من داخل الـJWT. `resolve` يُستدعى عند الدخول و3 كنترولرات فقط. كاشه مفيد لكن أثره أصغر بكثير مما ادّعيت. **الحمل الحقيقي الأثقل هو رفع موقع السائق** (استعلام + كتابة قاعدة كل نبضة) — وهو المجموعة H.
|
||
|
||
---
|
||
|
||
## المجموعة H — المواقع والتتبع (من `loction_server` في سيرو)
|
||
> ملاحظة المالك: «لحد الآن مش شايف السائق أو الراكب يرفع موقعه، ولا جداول location».
|
||
> عندنا الآن: `driver:location` عبر WebSocket → Redis GEO + **حفظ صف السائق في Postgres على كل نبضة** (`drivers.updateLocation` يعمل `repo.save`) — هذا أثقل حتى من سيرو.
|
||
|
||
**ما يفعله سيرو (مقروء من الكود):**
|
||
- `driver_socket.php` — سوكيت مخصص للمواقع، **لا يلمس القاعدة إطلاقاً**؛ Redis فقط عبر **pipeline كل 500ms** (`REDIS_BATCH_INTERVAL`).
|
||
- عتبات لتقليل الكتابة: `MIN_MOVE_METERS=10` (GEOADD فقط عند تحرّك >10م)، `HMSET_SPEED_DELTA=1.0`، `HMSET_HEADING_DELTA=5`، `FORWARD_MIN_METERS=15` / `FORWARD_MAX_SECONDS=3` للبث للراكب.
|
||
- **فهرسان منفصلان**: `geo:drivers:available` و`geo:drivers:busy` (عندنا فهرس واحد لكل فئة خدمة، بلا تمييز مشغول/متاح).
|
||
- `driver:profile:{id}` hash في Redis (heading/speed/status) — المطابقة تقرأ منه بلا قاعدة.
|
||
- عروض الرحلة: `setex` للعرض + `sadd` لمجموعة المعروض عليهم + `expire` — **نفس نمطنا في A5** ✅.
|
||
|
||
| # | البند | التفصيل |
|
||
|---|-------|---------|
|
||
| H1 | **إيقاف كتابة الموقع على القاعدة لكل نبضة** | `drivers.updateLocation` يكتب Postgres كل ثانية/ثلاث — يُنقل إلى Redis فقط. |
|
||
| H2 | **عتبات + batching** | تبنّي عتبات سيرو (10م/1.0 سرعة/5 اتجاه) + pipeline كل 500ms. |
|
||
| H3 | **جدول `car_locations` مكافئ** | صف واحد لكل سائق (آخر موقع) — يُكتب دورياً من worker لا من الطلب. عند سيرو: `ON DUPLICATE KEY UPDATE` + عمود `point` SRID 4326 عبر trigger (عندنا PostGIS متاح). |
|
||
| H4 | **جدول `car_tracks` (المسار)** | سجل نقاط تاريخي للتتبع/النزاعات — إدراج مجمّع (batch insert) من worker. |
|
||
| H5 | **فهرس available/busy** | تمييز السائق المشغول عن المتاح في فهرس GEO. |
|
||
| H6 | **`driver_behavior`** | max_speed · avg_speed · hard_brakes · total_distance · behavior_score لكل رحلة. |
|
||
| H7 | **`driver_daily_work` / `driver_daily_summary`** | ساعات عمل السائق (total_seconds باليوم + last_point_at + last_status). |
|
||
| H8 | **ربط المواقع بالمطابقة والـdispatch** | المطابقة تقرأ `driver:profile` من Redis؛ لوحة dispatch ترى الأسطول حياً. |
|
||
| — | **Geofence** | مؤجَّل بقرار المالك («خليها لوقتها») — موجود في سيرو (`get_location_area_links`, `LocationIntelligenceEngine`). |
|
||
|
||
---
|
||
|
||
## المجموعة I — المدفوعات والمحفظة (من `payment_server` في سيرو) 🔴 أمني
|
||
> طلب المالك: تدقيق أمني على المدفوعات، خصوصاً **الـpayout**: OTP عبر نبيه + بصمة (وجه/إصبع) في فلاتر + HMAC.
|
||
|
||
**بنية سيرو (مقروءة من `WalletDB.sql` + `sms_webhook/`):**
|
||
- **جدول لكل طريقة دفع** (كما قال المالك): `cliq_invoices` · `ecash_transactions` (+`_driver`) · `invoices_shamcash` (+`_passenger`) · `mtn_invoices` · `invoices_sms` (+`_passenger`) · `kazan`.
|
||
- **محافظ منفصلة**: `driverWallet` · `passengerWallet` · `siroWallet` (محفظة الشركة) — نمط **دفتر قيود append-only**، الرصيد = `SUM(amount)`.
|
||
- **نمط ذكي جداً**: `raw_sms_log` + `process_with_gemini.php` — رسالة SMS من مزوّد الدفع تُرفع خاماً، **Gemini يقرأها** ويستخرج المبلغ/المرجع، ثم `finalize_wallet_payment`. يحلّ غياب الـAPI الرسمي في سوريا.
|
||
- `payment_tokens` (+`_passenger`) للتتبع/عدم التكرار · `admin_audit_log` · `paymentsLogSyria(Driver)`.
|
||
|
||
**🔴 ثغرات وجدتها في كود سيرو — لا تُنقل كما هي:**
|
||
| الثغرة | التفصيل |
|
||
|--------|---------|
|
||
| **IDOR في `request_payout.php`** | `driverId` و`phone` يُؤخذان من **الطلب** لا من الـJWT → سائق يطلب سحب رصيد سائق آخر إلى هاتفه. **يجب** اشتقاق الهوية من التوكن حصراً. |
|
||
| **لا خصم عند الطلب** | الطلب يفحص الرصيد ثم يُدرج سجلاً فقط بلا حجز → **طلبات متعددة متزامنة تمرّ كلها** (double-spend). |
|
||
| **عمولة متناقضة** | `request_payout` يفحص `balance >= amount + 3500` (العمولة فوق المبلغ)، و`finalize_payout` يحسب `netAmount = amount - 3500` ويخصم الصافي فقط (العمولة داخله) → **تسريب مال**. والرقم 3500 مكرر حرفياً في الملفين. |
|
||
| **`finalizePayout` بلا معاملة** | 5 عمليات كتابة بلا `beginTransaction`/`rollBack` → فشل في المنتصف = خصم بلا تسجيل عمولة (حالة نصفية). |
|
||
| **`UPDATE payments SET isGiven=TRUE WHERE driverID=… AND isGiven=FALSE`** | يعلّم **كل** الدفعات المعلّقة كمدفوعة بغضّ النظر عن مبلغ السحب. |
|
||
| **`driverWallet.amount` = `varchar(10)`** | المال مخزَّن كنص (وحساب `SUM` على varchar). |
|
||
| **لا OTP على الـpayout** | `phone_verification` موجود لكنه مستخدم في **التسجيل/الدخول فقط** — لا شيء يحمي السحب. (ملاحظة المالك صحيحة.) |
|
||
| **جدولان متداخلان** | `payout_requests` و`driver_withdrawal_requests` لنفس المفهوم. |
|
||
|
||
**🔴 ثغرة في كودنا نحن (`wallet.service.ts`):**
|
||
`credit`/`debit` تعمل read-modify-write على عمود `balance` بلا قفل ولا معاملة → **سباق حقيقي**: خصمان متزامنان يقرآن نفس الرصيد ويكتبان فوق بعض = مال مفقود/مخلوق. سيرو هنا **أفضل منّا** (دفتر قيود + `FOR UPDATE`).
|
||
|
||
| # | البند | التفصيل |
|
||
|---|-------|---------|
|
||
| I1 | ✅ **إصلاح سباق المحفظة** | **منفَّذ ومُثبَت على السيرفر** (commit 9d6b752): `UPDATE … SET balance = balance ± :delta WHERE tenant_id … AND balance >= :amount RETURNING *` — عبارة واحدة ذرّية، والقيد+الرصيد في معاملة واحدة. أُضيف `wallet_txns.balance_after` وقيد `CHECK (balance >= 0)` كشبكة أمان. إنشاء المحفظة عبر `ON CONFLICT DO NOTHING`.<br>**نتيجة `wallet-race-test.mjs 100 5` على Postgres حقيقي:** 100 خصم متزامن → نجح 50 بالضبط، رُفض 50، الرصيد 250→0، المخصوم = 250 (لا مال ضائع ولا مخلوق)، الزمن 1492ms. ✅ |
|
||
| I2 | **محفظة لكل طرف + محفظة المنصة** | راكب · سائق · المستأجر/المنصة (مكافئ `siroWallet`) — أساس العمولة (B6). |
|
||
| I3 | **جدول لكل طريقة دفع** | شام كاش · كليك · إي كاش · بيموب · MTN · فوري … لكل واحدة جدولها + محوّل (adapter) موحّد. حالياً عندنا `tripz_pay_payments` عام. |
|
||
| I4 | **OTP على الـpayout عبر نبيه** | إرسال كود + تحقّق قبل تنفيذ السحب (لا يوجد في سيرو). |
|
||
| I5 | **بصمة (وجه/إصبع) في فلاتر** | تأكيد حيوي قبل السحب — يُربط بالطلب (device fingerprint من D2). |
|
||
| I6 | **HMAC على العمليات المالية** | توقيع الطلب (D3) — إلزامي على topup/payout. |
|
||
| I7 | **سجل تدقيق مالي** | مكافئ `admin_audit_log` — من فعل ماذا ومتى على كل عملية. |
|
||
| I8 | **webhook SMS + Gemini** | تبنّي نمط سيرو للأسواق بلا API رسمي (سوريا): `raw_sms_log` + استخراج بالـAI + تسوية. |
|
||
|
||
---
|
||
|
||
## قرار: هل يخاطب فلاتر خرائط انطلق مباشرة؟
|
||
**القرار المتّخذ (2026-07-17): تقسيم حسب نوع النداء — لا «كله مباشر» ولا «كله عبر السيرفر».**
|
||
|
||
| النوع | المسار | السبب |
|
||
|------|--------|-------|
|
||
| **البلاطات (tiles)** | فلاتر → انطلق **مباشرة** | حجم كبير ومتكرر، لا يحمل أسراراً، ويُخزَّن مؤقتاً على الجهاز. تمريره عبر سيرفرنا = تضخيم عرض النطاق بلا فائدة. |
|
||
| **geocode / reverse / route / places** | فلاتر → **سيرفرنا** → انطلق | يحمي مفتاح API (مفتاح داخل التطبيق = مسروق)، يتيح كاش Redis، حصص لكل مستأجر، وتبديل المزوّد بلا تحديث التطبيق. |
|
||
|
||
**السرعة ليست مقايضة هنا — بالعكس:** اختبار التحميل عندنا أظهر أن p50 قفز إلى **2.9 ثانية** والسبب المهيمن هو نداء HTTP الخارجي إلى انطلق. كاش النتائج في Redis (نفس العنوان يُطلب آلاف المرات) يجعل المسار عبر سيرفرنا **أسرع** من النداء المباشر، لا أبطأ. الإضافة الصافية للسيرفر ~10–30ms مقابل توفير ~2.9s على كل إصابة كاش.
|
||
→ **البند المطلوب: كاش Redis لنتائج geocode/route/places** (يُضاف للمجموعة G).
|
||
|
||
---
|
||
|
||
## مؤجَّل عمداً (قرار المالك)
|
||
المفاوض الذكي · تدرّج السائق · خصم العمولة — **آخر شيء** (جديدة حتى على سيرو).
|
||
**Geofence** — مؤجَّل («لوقتها»)، موجود في سيرو للاستئناس.
|
||
|
||
---
|
||
|
||
## ترتيب التنفيذ المقترح
|
||
1. ~~**A** (الزمن الحقيقي + FCM + Redis + race)~~ ✅ **منفَّذة ومُثبَتة على السيرفر**.
|
||
2. ~~**I1** (سباق المحفظة)~~ ✅ **منفَّذة ومُثبَتة بتزامن حقيقي**.
|
||
3. ~~**G** (Redis خط أول: لغة/توكنات/tenant/تعرفة/تقييم/كاش الخرائط)~~ ✅ **منفَّذة**.
|
||
4. **H** (المواقع: إيقاف الكتابة لكل نبضة + batching + tracks) — **التالي، وهو الحمل الأثقل فعلياً**.
|
||
5. **B** (نموذج الرحلة: started/انتظار/فصل السعر/الطوابع/stops).
|
||
6. **I** الباقي (المدفوعات: جداول لكل طريقة + OTP/بصمة/HMAC للسحب).
|
||
7. **C** (بيانات المركبة والسائق + ai_data).
|
||
8. **D** (تطبيع الهاتف + بصمة الجهاز + HMAC).
|
||
9. **E** (الاحتيال) ثم **F**.
|