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>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
f3fdc1bc20
commit
2edd8f6916
@@ -29,7 +29,7 @@
|
||||
|---|-------|---------|--------|
|
||||
| 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 | **احتساب مشوار الوصول للراكب** | يُقاس من موقع السائق **الحيّ لحظة القبول** حتى نقطة الالتقاط. **لا يُحتسب على الراكب ما لم يُفعِّله المالك** (`pickup_leg.charge`) — تفعيله يرفع كل أجرة، فهو قرار لا افتراض صامت. | ✅ `pickup_distance_km`/`_duration_min`/`_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)** | نقطتا توقف ضمن الرحلة (كما في سيرو/شير). | ⏳ الشريحة التالية |
|
||||
@@ -39,8 +39,18 @@
|
||||
| B10 | **كتالوج أنواع الرحلات العالمي** | `ride_types` فكرته سليمة (الأدمن/المستأجر يضيف أنواعه). المطلوب: كتالوج جاهز بما هو شائع عالمياً كخيارات جاهزة للاختيار (عندنا 6 فقط الآن). | ⏳ |
|
||||
| B11 | **تعرفة بالوزن** | بُعد تسعير إضافي بالوزن (للشحن/التوصيل) بجانب المسافة والزمن. | ⏳ |
|
||||
|
||||
**الأجرة النهائية الآن** = السعر المقفول + رسم الانتظار الزائد + مشوار الوصول (إن فُعِّل)، ثم تُفصل: الراكب يدفع `price_for_passenger`، والسائق يقبض `price_for_driver`، والفرق عمولة.
|
||||
**معلَّق على I2**: العمولة محسوبة ومسجّلة على الرحلة لكنها **لا تُقيَّد في محفظة المنصة** — لأن محفظة المنصة (مكافئ `siroWallet`) لم تُبنَ بعد.
|
||||
### 🔴 تصحيحان لازمان على الشريحة الأولى (توضيح المالك 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`) نموذجُ أوبر، لا نموذجنا.
|
||||
|
||||
**الأجرة النهائية بعد التصحيح** = السعر المقفول + رسم الانتظار الزائد. ومشوار الوصول يظهر عند الإلغاء فقط.
|
||||
|
||||
---
|
||||
|
||||
@@ -179,6 +189,44 @@
|
||||
|
||||
---
|
||||
|
||||
---
|
||||
|
||||
## المجموعة 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** — مؤجَّل («لوقتها»)، موجود في سيرو للاستئناس.
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
# 18 — الرصيد التشغيلي وعمولة السائق (نموذج الدفع المسبق)
|
||||
|
||||
> **طبقتان مختلفتان لا تُخلطان:**
|
||||
> - **[05-pricing-billing](05-pricing-billing.md)** = كيف تقبض **Tripz** من المستأجر (اشتراك + نسبة GMV).
|
||||
> - **هذا الملف** = كيف يقبض **المستأجر** (سيرو مثلاً) عمولته من **سائقيه**.
|
||||
>
|
||||
> مصدره: قرار المالك (2026-07-17) — «فلسفة التطبيق اللي بدنا إياه».
|
||||
|
||||
---
|
||||
|
||||
## 1. الفكرة في سطر واحد
|
||||
السائق **يشتري رصيداً تشغيلياً مقدَّماً**. الراكب يدفع للسائق **الأجرة كاملة** (كاش أو محفظة). المنصة **لا تلمس أجرة الرحلة إطلاقاً** — بل تخصم عمولتها من الرصيد التشغيلي المدفوع سلفاً.
|
||||
|
||||
## 2. المثال المرجعي (كلام المالك حرفياً)
|
||||
```
|
||||
السائق يشحن رصيده التشغيلي → 4.000 دينار
|
||||
الراكب يصل ويدفع للسائق (كاش/محفظة) → 4.000 دينار ← يأخذها السائق كاملة
|
||||
عمولة التطبيق 10% من 4.000 → 0.400 دينار
|
||||
تُخصم من الرصيد التشغيلي → 4.000 - 0.400 = 3.600 دينار
|
||||
```
|
||||
**الرصيد التشغيلي بعد الرحلة = 3.600 دينار.** أجرة الراكب لم تُمسّ.
|
||||
|
||||
> ملاحظة على الترقيم: الـ«4 دنانير» في المثال صدفة — رصيد الشحن وأجرة الرحلة رقمان مستقلان تماماً. لو شحن السائق 50 ديناراً وأنهى نفس الرحلة لصار رصيده 49.600.
|
||||
|
||||
## 3. هل هذا نموذج متعارف عليه عالمياً؟ — **نعم، لكنه ليس نموذج أوبر**
|
||||
هناك نموذجان سائدان، وكلاهما شرعي:
|
||||
|
||||
| | **اقتطاع من الأرباح (take-rate)** | **الرصيد المدفوع مسبقاً (prepaid credit)** ← نموذجنا |
|
||||
|---|---|---|
|
||||
| **من يقبض المال؟** | المنصة تقبض الأجرة ثم تحوّل للسائق الصافي | السائق يقبض الأجرة كاملة مباشرة |
|
||||
| **من يستعمله** | Uber · Bolt · Careem · Grab · Didi | **inDrive** · Yango/Yandex (شركاء) · Maxim · أغلب التطبيقات المحلية في الشرق الأوسط وأفريقيا وجنوب آسيا · **سيرو** |
|
||||
| **يناسب** | أسواق الدفع الإلكتروني الغالب | **أسواق الكاش الغالب** |
|
||||
| **مشكلة الكاش** | السائق يصير **مديناً** للمنصة → تحصيل ومطاردة وديون معدومة | **لا توجد** — العمولة مقبوضة سلفاً |
|
||||
| **التدفق النقدي** | متأخر | **مقدَّم** |
|
||||
| **حاجز الدخول للسائق** | صفر | يدفع قبل أن يكسب ← يحتاج معالجة |
|
||||
|
||||
**الحكم:** النموذج الذي تصفه هو **المعيار الفعلي في الأسواق التي تستهدفها** (سوريا · الأردن · مصر) حيث الكاش هو الغالب. أوبر نفسها في الأسواق كثيفة الكاش تضطر لتتبّع **رصيد سائق سالب** ثم مطاردة تحصيله — وهو بالضبط الوجع الذي يلغيه نموذجك من أصله. اختيارك سليم، وليس نسخاً أعمى عن سيرو.
|
||||
|
||||
**لكن ثمنه ثلاثة التزامات** (تفصيلها في §5): الحجب عند نفاد الرصيد، وباقات شحن لكل عملة، ومعالجة حاجز دخول السائق الجديد.
|
||||
|
||||
## 4. ما الذي يتغيّر في الكود (تصحيح لما نُفِّذ في B6)
|
||||
الشريحة الأولى من المجموعة B (commit `f3fdc1b`) نُفِّذت على **نموذج الاقتطاع**:
|
||||
```ts
|
||||
price_for_driver = price_for_passenger - commission // ❌ ليس فلسفتنا
|
||||
```
|
||||
الصحيح حسب هذا المستند:
|
||||
```ts
|
||||
price_for_driver = price_for_passenger // السائق يأخذ الأجرة كاملة
|
||||
commission_amount = split(price_for_passenger) // تُحسب وتُسجَّل على الرحلة
|
||||
→ ثم تُخصم من driver_credit (رصيد منفصل تماماً)، لا من الأجرة
|
||||
```
|
||||
**`price_for_driver` و`price_for_passenger` يبقيان مفيدين** — لكن سبب اختلافهما ليس العمولة، بل: خصم على الراكب (كوبون/عرض) بينما السائق يقبض كاملاً، أو مكافأة تفاوض (`ai_negotiated_bonus` عند سيرو).
|
||||
|
||||
## 5. الآليات المطلوبة
|
||||
|
||||
### 5.1 الرصيد التشغيلي (`driver_credit`)
|
||||
- محفظة **ثانية منفصلة** عن محفظة أرباح السائق. لا تُخلط: هذه للعمولة سلفاً، وتلك لأمواله.
|
||||
- دفتر قيود append-only + خصم ذرّي — **نفس نمط I1 حرفياً** (`UPDATE … WHERE balance >= :amount`).
|
||||
- الخصم يحدث عند `completed` (لا عند `paid`): الرحلة تمّت فالعمولة استُحقّت، بغضّ النظر عن وسيلة الدفع.
|
||||
|
||||
### 5.2 الحجب عند نفاد الرصيد — **الالتزام الأهم**
|
||||
بلا حجب، النموذج **ينهار**: سائق برصيد صفر يعمل مجاناً إلى الأبد.
|
||||
```
|
||||
driver_credit < min_balance → لا يدخل فهرس geo:drivers:available
|
||||
→ لا تُعرض عليه رحلات
|
||||
→ إشعار: «اشحن رصيدك التشغيلي»
|
||||
```
|
||||
- الفحص عند: `setOnline` وقبل `accept` وبعد كل خصم.
|
||||
- `min_balance` لكل مستأجر/عملة (قد يكون صفراً أو أعلى من أغلى عمولة متوقعة).
|
||||
- **سباق حقيقي**: رحلة تُنهى ورصيده لا يكفي العمولة → نسمح بأرضية سالبة محدودة (`credit_floor`، مثلاً −1 عمولة) ثم نحجبه فوراً. البديل (رفض الخصم) يعني عمولة ضائعة.
|
||||
|
||||
### 5.3 باقات الشحن لكل عملة
|
||||
كما في سيرو — لكل عملة باقاتها وأسعارها:
|
||||
```
|
||||
JOD: [5, 10, 25, 50]
|
||||
SYP: [50k, 100k, 250k]
|
||||
EGP: [100, 250, 500]
|
||||
```
|
||||
- الشحن عبر **[07-integrations](07-integrations.md)** وطرق الدفع لكل دولة (كليك · شام كاش · إي كاش · بيموب · فوري …).
|
||||
- باقة قد تحمل **حافزاً**: «اشحن 50 واحصل على 55» — أداة تسويق مباشرة بيد المستأجر.
|
||||
- الحافز يُسجَّل قيداً منفصلاً في الدفتر (`bonus`) لا يُخلط بالمدفوع فعلاً — وإلا فسدت المحاسبة.
|
||||
|
||||
### 5.4 حاجز دخول السائق الجديد
|
||||
النموذج يطلب من السائق أن يدفع **قبل** أن يكسب — وهذا يقتل التجنيد.
|
||||
المعالجات (قرار مالك، لا افتراض تقني):
|
||||
- **رصيد ترحيبي** (مثلاً 5 دنانير مجاناً) — الأشيع.
|
||||
- **فترة سماح** (أول 7 أيام أو أول 20 رحلة بلا خصم).
|
||||
- **أرضية سالبة أوسع للسائق الجديد** ثم تُشدَّد.
|
||||
|
||||
### 5.5 وسيلة الدفع لا تغيّر شيئاً
|
||||
| الدفع | من يقبض الأجرة | من أين العمولة |
|
||||
|---|---|---|
|
||||
| كاش | السائق مباشرة | الرصيد التشغيلي |
|
||||
| محفظة الراكب | السائق (تُحوَّل لمحفظة أرباحه) | الرصيد التشغيلي |
|
||||
|
||||
**قاعدة واحدة لا استثناء لها** — وهذا أبسط بكثير من تفريع المنطق حسب وسيلة الدفع، وهو ما قاله المالك حرفياً: «إن كانت كاش أو محفظة».
|
||||
|
||||
> **مفارقة تستحق الانتباه:** في الدفع بالمحفظة، المنصة **تلمس المال فعلاً**، فتستطيع تقنياً اقتطاع العمولة منه. لكننا **لا نفعل** — عمداً. لأن قاعدتين مختلفتين حسب وسيلة الدفع تعني منطقاً مزدوجاً، ومحاسبةً مزدوجة، وسائقاً لا يفهم لماذا اختلف دخله. الاتساق أثمن من التحسين هنا.
|
||||
|
||||
## 6. لماذا تُحسب العمولة على الرحلة رغم أنها تُخصم من مكان آخر؟
|
||||
لأن `commission_amount` على الرحلة هو **سبب** القيد في دفتر الرصيد التشغيلي. بدونه لا يستطيع السائق (ولا خدمة العملاء) الإجابة عن: «لماذا نقص رصيدي 400 فلس؟». كل خصم يشير إلى `trip_id`.
|
||||
|
||||
## 7. البنود المطلوبة (تُضاف للـ[17-backend-backlog](17-backend-backlog.md) كمجموعة J)
|
||||
| # | البند |
|
||||
|---|-------|
|
||||
| J1 | جدول `driver_credit` + دفتر قيوده — خصم ذرّي داخل معاملة (نمط I1) |
|
||||
| J2 | خصم العمولة من الرصيد التشغيلي عند `completed` (لا من الأجرة) |
|
||||
| J3 | **تصحيح B6**: `price_for_driver = price_for_passenger` (السائق يقبض كاملاً) |
|
||||
| J4 | الحجب عند نفاد الرصيد: `setOnline` + `accept` + بعد كل خصم |
|
||||
| J5 | باقات الشحن لكل عملة + الحافز كقيد منفصل |
|
||||
| J6 | رصيد ترحيبي / فترة سماح للسائق الجديد |
|
||||
| J7 | أرضية سالبة محدودة (`credit_floor`) لسباق «أنهى الرحلة ورصيده لا يكفي» |
|
||||
| J8 | شاشة/نقاط: رصيدي · تاريخ الخصومات · الشحن |
|
||||
|
||||
---
|
||||
← ذو صلة: [05-pricing-billing](05-pricing-billing.md) · [17-backend-backlog](17-backend-backlog.md) · [19-entitlements-licensing](19-entitlements-licensing.md)
|
||||
@@ -0,0 +1,111 @@
|
||||
# 19 — الاستحقاقات (Entitlements) ومنع تفعيل الميزات بلا اشتراك
|
||||
|
||||
> سؤال المالك (2026-07-17): «إذا نزّلنا التطبيق بشكل كامل، وجاء المشترك وعمل **reverse engineering** وفعّل الإضافات — كيف نحدّ منها؟»
|
||||
|
||||
---
|
||||
|
||||
## 1. الجواب في سطر واحد
|
||||
**لا تُفعَّل الميزة في التطبيق أبداً. تُفعَّل في السيرفر.**
|
||||
التطبيق لا يملك ما يُسرَق: هو يعرض واجهة فقط. الميزة الحقيقية = نقاط API تحرسها المنصة.
|
||||
|
||||
## 2. القاعدة الحاكمة
|
||||
> **علم الميزة في التطبيق (feature flag) هو قرار *عرض*، وليس حدّاً أمنياً — أبداً.**
|
||||
|
||||
`GET /tenant/config` يرجع `features` كي يعرف التطبيق **ماذا يُظهر**. لو عدّل أحدهم الاستجابة أو فكّك التطبيق وفعّل كل الأعلام، فالنتيجة:
|
||||
|
||||
```
|
||||
المهاجم يفعّل ميزة «البوتات» في التطبيق
|
||||
→ تظهر له الشاشة والأزرار ✅ (لا ضرر — بكسلات فقط)
|
||||
→ يضغط زراً → POST /bots/campaigns
|
||||
→ الحارس يقرأ tenant_id من الـJWT الموقَّع
|
||||
→ يسأل: هل اشتراك هذا المستأجر يشمل bots؟ → لا
|
||||
→ 403 Forbidden ❌ لا شيء حدث
|
||||
```
|
||||
**كسب المهاجم: شاشة فارغة.** هذا هو المطلوب بالضبط.
|
||||
|
||||
## 3. لماذا لا يستطيع تزوير `tenant_id`؟
|
||||
- `tenant_id` يأتي من **JWT موقَّع بمفتاح السيرفر**، لا من هيدر يكتبه العميل.
|
||||
- الترويسة `x-tenant-id` تُستعمل **قبل الدخول فقط** (اختيار المستأجر) — وبعد الدخول الحقيقة من التوكن.
|
||||
- تزوير التوكن يحتاج `JWT_SECRET` — وهو على السيرفر لا في التطبيق.
|
||||
|
||||
## 4. التطبيق العملي: حارس استحقاقات
|
||||
```ts
|
||||
@RequiresFeature('bots') // ← الحدّ الأمني الحقيقي
|
||||
@Post('bots/campaigns')
|
||||
create(@CurrentUser() user: AuthUser, @Body() body: any) { … }
|
||||
```
|
||||
`FeatureGuard`:
|
||||
1. يقرأ `tenant_id` من التوكن.
|
||||
2. يجلب استحقاقات المستأجر — **من Redis خط أول** (نمط G4)، والقاعدة احتياط.
|
||||
3. غير مستحقّة → `403` برسالة واضحة (`feature_not_in_plan`) ليعرضها التطبيق بلطف.
|
||||
|
||||
**كل نقطة تخصّ ميزة مدفوعة تحمل هذا الحارس. بلا استثناء واحد.**
|
||||
نقطة واحدة منسيّة = الميزة مجانية للجميع. لذلك: **الافتراض هو المنع** — قائمة بيضاء لا سوداء.
|
||||
|
||||
## 5. الحد الذي لا يُتجاوز تقنياً
|
||||
> **ما يعمل كلياً على الجهاز بلا نداء سيرفر — لا يمكن حمايته. نهائياً.**
|
||||
|
||||
لا تشويش (obfuscation) ولا تشفير ولا فحص جذر يغيّر هذه الحقيقة؛ كلها ترفع الكلفة ولا تمنع. لذلك القاعدة المعمارية:
|
||||
|
||||
> **كل ميزة ذات قيمة تجارية يجب أن تمرّ بالسيرفر — ولو لم تحتج ذلك تقنياً.**
|
||||
|
||||
مثال: «الاستخبار السوقي» لو حُسب في التطبيق من بيانات محليّة = مسروق بلا حيلة. ولو كان `POST /market-intel/report` = محميّ تماماً. **هذا قرار تصميم يُتخذ عند بناء كل ميزة، لا ترقيع بعدها.**
|
||||
|
||||
## 6. الثغرة الحقيقية: وضع السيادة (Sovereign)
|
||||
هنا الخبر الذي يجب أن يُقال صراحةً:
|
||||
|
||||
**في وضع السيادة ([06-tenant-model](06-tenant-model.md)) — المستأجر يشغّل نسختنا على خوادمه. عنده الكود والقاعدة والسيرفر. لا يوجد حارس يحرس ضدّه، لأنه هو صاحب الحارس.**
|
||||
|
||||
يستطيع تعديل `FeatureGuard` ليرجع `true` دائماً. لا حلّ تقنيّ كامل. الخيارات الواقعية:
|
||||
|
||||
| الخيار | الجدوى |
|
||||
|--------|--------|
|
||||
| **السيادة = كل الميزات، بسعرها** | ✅ **الموصى به.** لا شيء يُسرق لأن لا شيء محجوب. يطابق أصلاً كون «سيادة» أعلى الباقات ($599 + $12k إعداد). |
|
||||
| **مفتاح ترخيص موقَّع + اتصال دوري بالـControl Plane** | 🟡 يردع غير التقني، ويُنزع بتعديل الكود. مفيد للكشف والتوثيق العقدي لا للمنع. |
|
||||
| **مكوّن حرج يبقى عندنا (SaaS جزئي)** | 🟡 فعّال لكنه يناقض وعد السيادة نفسه (البيانات داخل الدولة). |
|
||||
| **العقد والقانون** | ✅ الحدّ الحقيقي في هذا الوضع. تدقيق + بند جزائي. |
|
||||
|
||||
**التوصية المعمارية:** في الوضع المشترك (انطلاقة/علامة/أسطول+) الحماية **تقنية وكاملة**. في وضع السيادة الحماية **تعاقدية**، فليشمل سعرُها كلَّ شيء ولا نتظاهر بحمايةٍ لا نملكها.
|
||||
|
||||
## 7. تدفّق السوبر-أدمن: اشتراك ← استحقاقات
|
||||
```
|
||||
سوبر-أدمن ينشئ مستأجراً
|
||||
→ يختار الباقة (launch | brand | fleet | sovereign)
|
||||
→ يختار country_pack (jo | sy | eg)
|
||||
→ الباقة تولّد استحقاقات افتراضية
|
||||
→ + إضافات مشتراة منفردة (market_intel, bots, ads, transit…)
|
||||
→ تُحفظ في tenants.features (jsonb — موجود أصلاً في الكيان)
|
||||
→ تُبطَل من كاش Redis فوراً (نمط G4)
|
||||
```
|
||||
- **الميزة تُشترى منفردة** فوق الباقة — لذا `features` ليست اشتقاقاً من `plan` بل قائمة صريحة. `plan` يعطي الافتراضات، و`features` هي الحقيقة.
|
||||
- تغيير الاشتراك = تغيير `features` + إبطال الكاش → يسري خلال ثوانٍ **بلا تحديث تطبيق**.
|
||||
|
||||
## 8. شكل الاستحقاقات
|
||||
```jsonc
|
||||
// tenants.features
|
||||
{
|
||||
"dispatch": true,
|
||||
"wallet": true,
|
||||
"bots": false, // ← لم يشترِها
|
||||
"market_intel": false,
|
||||
"transit": false,
|
||||
"ads": false,
|
||||
"limits": { "drivers_max": 500, "cities_max": 3 }
|
||||
}
|
||||
```
|
||||
- **الحدود العددية** (`limits`) تُفرض على السيرفر أيضاً — سائق رقم 501 يُرفض بـ403.
|
||||
- الافتراض الصلب: **مفتاح غير موجود = ممنوع** (لا مسموح).
|
||||
|
||||
## 9. البنود المطلوبة (مجموعة K في الـ[17-backend-backlog](17-backend-backlog.md))
|
||||
| # | البند |
|
||||
|---|-------|
|
||||
| K1 | `FeatureGuard` + `@RequiresFeature()` — استحقاقات من Redis، القاعدة احتياط |
|
||||
| K2 | كتالوج الميزات + استحقاقات افتراضية لكل باقة |
|
||||
| K3 | نقاط سوبر-أدمن: إنشاء مستأجر باشتراك، تعديل الاستحقاقات، إبطال الكاش |
|
||||
| K4 | فرض الحدود العددية (`drivers_max`, `cities_max`) على السيرفر |
|
||||
| K5 | `GET /tenant/config` يرجع `features` **للعرض فقط** — موثّق صراحةً أنه ليس حدّاً أمنياً |
|
||||
| K6 | اختبار عزل في CI: مستأجر بلا ميزة يأخذ 403 على كل نقاطها |
|
||||
| K7 | قرار وضع السيادة: كل الميزات مشمولة + بند تعاقدي (لا وهم حماية تقنية) |
|
||||
|
||||
---
|
||||
← ذو صلة: [06-tenant-model](06-tenant-model.md) · [05-pricing-billing](05-pricing-billing.md) · [18-driver-credit-commission](18-driver-credit-commission.md)
|
||||
Reference in New Issue
Block a user