Files
tripz-llc/docs/17-backend-backlog.md
T
Hamza-AyedandClaude Opus 4.8 2edd8f6916 docs: نموذج الرصيد التشغيلي (18) والاستحقاقات (19) + تصحيحان على B
18 — الرصيد التشغيلي: السائق يشحن سلفاً، الراكب يدفع له الأجرة كاملة،
والعمولة تُخصم من الرصيد لا من الأجرة. مقارنة صريحة: هذا نموذج inDrive/
Yango/سيرو لا نموذج أوبر — وهو المعيار الفعلي في أسواق الكاش المستهدفة.
ثمنه ثلاثة التزامات: الحجب عند نفاد الرصيد (بدونه ينهار)، باقات لكل عملة،
ومعالجة حاجز دخول السائق الجديد.

19 — الاستحقاقات: علم الميزة في التطبيق قرار عرض لا حدّ أمني. الهندسة
العكسية تكشف شاشة تنادي نقطة ترجع 403. الحدّ الحقيقي FeatureGuard على
السيرفر + tenant_id من JWT موقَّع. الحقيقة التي تُقال صراحةً: ما يعمل كلياً
على الجهاز لا يُحمى، ووضع السيادة لا يُحمى تقنياً أصلاً (المستأجر يملك
السيرفر) — فليشمل سعره كل شيء بدل وهم حماية.

تصحيحان على المجموعة B (كلاهما بُني على افتراض خاطئ مني):
- B3: مشوار الوصول تعويضُ عدم حضور عند الإلغاء بعد 5 دقائق انتظار،
  لا بند في كل أجرة كما بنيته
- B6: price_for_driver = price_for_passenger — العمولة لا تُقتطع من الأجرة

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 13:32:32 +03:00

246 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`.<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).
---
---
## المجموعة 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**.