From 2edd8f69165c462805112ed1b2645b8deb35a898 Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Fri, 17 Jul 2026 13:32:32 +0300 Subject: [PATCH] =?UTF-8?q?docs:=20=D9=86=D9=85=D9=88=D8=B0=D8=AC=20=D8=A7?= =?UTF-8?q?=D9=84=D8=B1=D8=B5=D9=8A=D8=AF=20=D8=A7=D9=84=D8=AA=D8=B4=D8=BA?= =?UTF-8?q?=D9=8A=D9=84=D9=8A=20(18)=20=D9=88=D8=A7=D9=84=D8=A7=D8=B3?= =?UTF-8?q?=D8=AA=D8=AD=D9=82=D8=A7=D9=82=D8=A7=D8=AA=20(19)=20+=20=D8=AA?= =?UTF-8?q?=D8=B5=D8=AD=D9=8A=D8=AD=D8=A7=D9=86=20=D8=B9=D9=84=D9=89=20B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docs/17-backend-backlog.md | 54 ++++++++++++- docs/18-driver-credit-commission.md | 116 ++++++++++++++++++++++++++++ docs/19-entitlements-licensing.md | 111 ++++++++++++++++++++++++++ 3 files changed, 278 insertions(+), 3 deletions(-) create mode 100644 docs/18-driver-credit-commission.md create mode 100644 docs/19-entitlements-licensing.md diff --git a/docs/17-backend-backlog.md b/docs/17-backend-backlog.md index 1a8d3ea..d5264f3 100644 --- a/docs/17-backend-backlog.md +++ b/docs/17-backend-backlog.md @@ -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** — مؤجَّل («لوقتها»)، موجود في سيرو للاستئناس. diff --git a/docs/18-driver-credit-commission.md b/docs/18-driver-credit-commission.md new file mode 100644 index 0000000..e82fa9c --- /dev/null +++ b/docs/18-driver-credit-commission.md @@ -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) diff --git a/docs/19-entitlements-licensing.md b/docs/19-entitlements-licensing.md new file mode 100644 index 0000000..13b3a06 --- /dev/null +++ b/docs/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)