# 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` وفصل الوصول عن البدء** | — | ✅ **كان منفَّذاً أصلاً**: `driver_arrived` و`in_progress` حالتان منفصلتان في آلة الحالة منذ P1. الناقص كان الطوابع والانتظار (B2/B7) لا الحالة نفسها. | | B2 | **عدّاد انتظار 5 دقائق (قانون)** | يبدأ عند `driver_arrived`، ويُحتسب عند `in_progress`. الدقائق المجانية `free_waiting_min` (5 افتراضاً) قابلة للضبط لكل تعرفة، والزائد بسعر `per_min_waiting` للنافذة الفعّالة. | ✅ `waiting_min` + `waiting_charge` | | B3 | **احتساب مشوار الوصول للراكب** | يُقاس من موقع السائق **الحيّ لحظة القبول** حتى نقطة الالتقاط. | 🟡 **القياس ✅ والقاعدة ❌** — راجع التصحيح أدناه | | B6 | **فصل السعر** | `price_for_passenger` · `price_for_driver` · `commission_amount` · `commission_rate`. العمولة تُعرَّف في التعرفة: `commission: { percent, flat, min }`، ولا تتجاوز الأجرة أبداً. | ✅ + **تسوية المحفظة صُحّحت**: كانت تُضيف للسائق كامل أجرة الراكب — أي أن العمولة كانت تضيع | | B7 | **طوابع زمنية دقيقة** | `driver_going_at` (`DriverIsGoingToPassenger`) · `arrived_at` · `started_at` (`rideTimeStart`) · `completed_at` (`rideTimeFinish`). | ✅ + كشف «الإنهاء السريع» صار يقيس من **بدء** الرحلة لا إسنادها | | B4 | **نقاط توقف (stops)** | نقطتا توقف ضمن الرحلة (كما في سيرو/شير). | ⏳ الشريحة التالية | | B5 | **حجز مسبق/جدولة** | جداول الرحلة (`date`/`time`/`endtime` في سيرو). | ⏳ الشريحة التالية | | B8 | **مطابقة الوجهة** | `is_destination_match` + خصم الراكب. | ⏳ الشريحة التالية | | B9 | **تعديل التعرفة من لوحة الأدمن** | جداول تعرفة قابلة للتحرير (موجودة كـ jsonb — نحتاج واجهة/نقاط CRUD). | ⏳ | | B10 | **كتالوج أنواع الرحلات العالمي** | `ride_types` فكرته سليمة (الأدمن/المستأجر يضيف أنواعه). المطلوب: كتالوج جاهز بما هو شائع عالمياً كخيارات جاهزة للاختيار (عندنا 6 فقط الآن). | ⏳ | | B11 | **تعرفة بالوزن** | بُعد تسعير إضافي بالوزن (للشحن/التوصيل) بجانب المسافة والزمن. | ⏳ | ### 🔴 تصحيحان لازمان على الشريحة الأولى (توضيح المالك 2026-07-17) بنيتُ B3 وB6 على افتراضات خاطئة. الصواب: **1. مشوار الوصول ليس بنداً في كل أجرة — بل تعويض عن عدم حضور الراكب.** > «مشوار الوصول يُحسب إذا انتظر السائق خمس دقائق وأُلغيت الرحلة بعد الخمس دقائق.» فالقاعدة: يُحتسب **فقط** عند الإلغاء من حالة `driver_arrived` بعد انقضاء الدقائق المجانية. بنيتُه كمفتاح تعرفة يُضاف لكل رحلة (`pickup_leg.charge`) — **خطأ**. المطلوب: يحلّ محلّ رسم الإلغاء الثابت في تلك الحالة، ويذهب **للسائق** تعويضاً. **2. العمولة لا تُقتطع من أجرة السائق إطلاقاً** — راجع **[18-driver-credit-commission](18-driver-credit-commission.md)**. `price_for_driver = price_for_passenger` (السائق يقبض كاملاً)، والعمولة تُخصم من **رصيد تشغيلي مدفوع سلفاً**. ما نُفِّذ (`price_for_driver = fare − commission`) نموذجُ أوبر، لا نموذجنا. **الأجرة النهائية بعد التصحيح** = السعر المقفول + رسم الانتظار الزائد. ومشوار الوصول يظهر عند الإلغاء فقط. --- ## المجموعة 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 فقط. الكتابة الدائمة صارت من الـworker | | H2 | **عتبات + batching** | عتبات سيرو: 10م حركة · 1.0 سرعة · 5° اتجاه. سائق واقف = `EXPIRE` فقط (بلا كتابة ولا نقطة مسار ولا بثّ). | ✅ + البثّ للراكب صار عند الحركة ذات الدلالة فقط | | H3 | **لقطة «آخر موقع» لكل سائق** | صف واحد لكل سائق يكتبه الـworker دورياً. **قرار: لم نُنشئ جدولاً منفصلاً** — `drivers` أصلاً صف واحد لكل سائق؛ أُضيفت `heading`/`speed`/`loc_status`/`loc_updated_at` بجانب `last_lat/last_lng`. | ✅ | | H4 | **`driver_tracks` (المسار)** | نقاط تاريخية للتتبع/النزاعات — تتراكم في قائمة Redis ويُدرجها الـworker **بعبارة واحدة**. | ✅ جدول `tripz_driver_tracks` | | H5 | **فهرس available/busy** | `geo:drivers:{tenant}:{class}:{available\|busy}` — البحث يمسح المتاحين فقط. السائق يصير busy عند القبول ويعود available عند الإنهاء/الإلغاء. | ✅ | | H8 | **ربط المواقع بالمطابقة** | المطابقة والاحتيال و«الرحلات المتاحة» صاروا يقرؤون الموقع الحيّ من Redis. | ✅ | | H6 | **`driver_behavior`** | max_speed · avg_speed · hard_brakes · total_distance · behavior_score لكل رحلة. | ⏳ **مؤجَّل** — تحليلات فوق المسار، تُبنى من `driver_tracks` لاحقاً | | H7 | **`driver_daily_work` / `driver_daily_summary`** | ساعات عمل السائق (total_seconds باليوم + last_point_at + last_status). | ⏳ **مؤجَّل** | | — | **Geofence** | مؤجَّل بقرار المالك («خليها لوقتها») — موجود في سيرو (`get_location_area_links`, `LocationIntelligenceEngine`). | ⏳ | **الـworker صار حقيقياً**: كان هيكلاً فارغاً (`setInterval` بلا عمل). الآن `NestFactory.createApplicationContext(WorkerModule)` — قاعدة + Redis + المواقع، يفرّغ كل 5s (`LOC_FLUSH_INTERVAL_MS`)، بقفل يمنع تراكب الدورات، وتفريغة أخيرة عند SIGTERM حتى لا تضيع اللقطات عند النشر. **الأثر**: سائق متصل كان يكلّف استعلاماً + كتابة قاعدة لكل نبضة (كل 1–3 ثوانٍ). الآن: نبضة السائق الواقف = أمر `EXPIRE` واحد، والمتحرّك = كتابة Redis + كتابة قاعدة واحدة كل 5 ثوانٍ **مهما بلغ عدد نبضاته**. --- ## المجموعة 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`.
**نتيجة `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). --- --- ## المجموعة J — الرصيد التشغيلي وعمولة السائق 🔴 يسبق I2 > **المستند الكامل: [18-driver-credit-commission](18-driver-credit-commission.md).** > السائق يشحن رصيداً تشغيلياً سلفاً · الراكب يدفع له الأجرة كاملة · العمولة تُخصم من الرصيد لا من الأجرة. > نموذج معتمد عالمياً في أسواق الكاش (inDrive · Yango · سيرو) — وليس نموذج أوبر. | # | البند | |---|-------| | J1 | جدول `driver_credit` + دفتر قيود — خصم ذرّي داخل معاملة (نمط I1 حرفياً) | | J2 | خصم العمولة عند `completed` من الرصيد التشغيلي (لا من الأجرة) | | J3 | **تصحيح B6**: `price_for_driver = price_for_passenger` | | J4 | **الحجب عند نفاد الرصيد** — بدونه ينهار النموذج (سائق برصيد صفر يعمل مجاناً للأبد) | | J5 | باقات شحن لكل عملة + الحافز كقيد منفصل | | J6 | رصيد ترحيبي / فترة سماح للسائق الجديد (حاجز الدخول) | | J7 | أرضية سالبة محدودة لسباق «أنهى ورصيده لا يكفي» | | J8 | نقاط: رصيدي · تاريخ الخصومات · الشحن | > **أثره على I2**: محفظة المنصة لم تعد تستقبل عمولة من كل رحلة — بل تستقبل **مدفوعات الشحن**. هذا يبسّط I2 كثيراً. --- ## المجموعة K — الاستحقاقات ومنع تفعيل الميزات بلا اشتراك > **المستند الكامل: [19-entitlements-licensing](19-entitlements-licensing.md).** > القاعدة: **علم الميزة في التطبيق قرار عرض لا حدّ أمني.** الحدّ الحقيقي حارس على السيرفر. | # | البند | |---|-------| | K1 | `FeatureGuard` + `@RequiresFeature()` — الاستحقاقات من Redis والقاعدة احتياط | | K2 | كتالوج الميزات + افتراضات لكل باقة | | K3 | نقاط سوبر-أدمن: إنشاء مستأجر باشتراك · تعديل الاستحقاقات · إبطال الكاش | | K4 | فرض الحدود العددية (`drivers_max`, `cities_max`) على السيرفر | | K5 | `GET /tenant/config` — `features` للعرض فقط، موثّقة صراحةً أنها ليست حدّاً أمنياً | | K6 | اختبار عزل في CI: مستأجر بلا ميزة يأخذ 403 على كل نقاطها | | K7 | قرار وضع السيادة: كل الميزات مشمولة + بند تعاقدي (لا وهم حماية تقنية) | --- ## مؤجَّل عمداً (قرار المالك) المفاوض الذكي · تدرّج السائق · خصم العمولة — **آخر شيء** (جديدة حتى على سيرو). **Geofence** — مؤجَّل («لوقتها»)، موجود في سيرو للاستئناس. --- ## ترتيب التنفيذ المقترح 1. ~~**A** (الزمن الحقيقي + FCM + Redis + race)~~ ✅ **منفَّذة ومُثبَتة على السيرفر**. 2. ~~**I1** (سباق المحفظة)~~ ✅ **منفَّذة ومُثبَتة بتزامن حقيقي**. 3. ~~**G** (Redis خط أول: لغة/توكنات/tenant/تعرفة/تقييم/كاش الخرائط)~~ ✅ **منفَّذة**. 4. ~~**H** (المواقع: إيقاف الكتابة لكل نبضة + batching + tracks)~~ ✅ **منفَّذة** (عدا H6/H7 — تحليلات مؤجَّلة). 5. **B** — 🔵 الشريحة الأولى ✅ (الانتظار B2 · مشوار الوصول B3 · فصل السعر B6 · الطوابع B7). الباقي: B4 stops · B5 جدولة · B8 مطابقة الوجهة · B9 · B10 · B11. 6. **I** الباقي (المدفوعات: جداول لكل طريقة + OTP/بصمة/HMAC للسحب). 7. **C** (بيانات المركبة والسائق + ai_data). 8. **D** (تطبيع الهاتف + بصمة الجهاز + HMAC). 9. **E** (الاحتيال) ثم **F**.