feat: المجموعة K — استحقاقات الاشتراك مفروضة على السيرفر

القاعدة (docs/19): علم الميزة في التطبيق قرار عرض لا حدّ أمني. من يفكّك
التطبيق ويفعّل الأعلام يرى الشاشة ثم يصطدم بـ403 من FeatureGuard.

- K1: FeatureGuard + @RequiresFeature — tenant_id من التوكن الموقَّع لا من
  ترويسة. الوحدة عالمية عمداً: إجبار كل وحدة على استيرادها = نقطة منسيّة
  يوماً ما، والمنسيّة ميزة مجانية للجميع
- K2: كتالوج الميزات + افتراضات الباقات. الافتراض هو المنع (قائمة بيضاء):
  ميزة جديدة تبقى محجوبة حتى تُمنح صراحةً
- K3: GET /admin/features · GET /admin/tenants/:id/entitlements ·
  PATCH /admin/tenants/:id/subscription — يدمج features لا يستبدلها
  (استبدالها كان سيمحو المشتريات السابقة) ويُبطل الكاش فوراً
- K4: drivers_max عند apply — عند الإنشاء فقط، فسائق قائم لا يُطرد بتغيير باقة
- K5: /tenant/config يرجع الاستحقاقات المحسوبة لا features الخام (الخام
  تجاوزات فقط فكان سيُظهر ميزات الباقة مطفأة)
- K6: اختبار أن الترويسة لا تزوّر المستأجر + أن الافتراض منع لا سماح
- K7: السيادة = كل الميزات — المستأجر يملك السيرفر فالحجب هناك وهم لا حماية

محروس: dispatch · chat · payments/charge. المدفوعات لكل نقطة لا للصنف —
الـwebhook بلا JWT فحارس الصنف كان سيمنعه.

seed: المستأجر التجريبي siro صار sovereign (قرار المالك: أول مشترك بالباقة
الكاملة) — وأيضاً يمنع حجب dispatch وحدّ 500 سائق من إسقاط سكربتات التحقق.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hamza-Ayed
2026-07-17 14:24:50 +03:00
co-authored by Claude Opus 4.8
parent d9b0faab2d
commit aae01fc4ee
15 changed files with 459 additions and 20 deletions
+16 -10
View File
@@ -214,19 +214,25 @@
---
## المجموعة K — الاستحقاقات ومنع تفعيل الميزات بلا اشتراك
## المجموعة 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 | قرار وضع السيادة: كل الميزات مشمولة + بند تعاقدي (لا وهم حماية تقنية) |
| # | البند | الحالة |
|---|-------|--------|
| K1 | `FeatureGuard` + `@RequiresFeature()` — الاستحقاقات من Redis والقاعدة احتياط. الوحدة **عالمية** عمداً: إجبار كل وحدة على استيرادها يعني نقطة منسيّة يوماً ما، والمنسيّة ميزةٌ مجانية للجميع | ✅ |
| K2 | `common/entitlements/features.ts` — الكتالوج + افتراضات كل باقة. **الافتراض هو المنع**: قائمة بيضاء، وميزة جديدة تبقى محجوبة حتى تُمنح | ✅ |
| K3 | `GET /admin/features` · `GET /admin/tenants/:id/entitlements` · `PATCH /admin/tenants/:id/subscription` (يدمج `features` لا يستبدلها، ويُبطل الكاش فوراً) | ✅ |
| K4 | `drivers_max` يُفرض عند `drivers.apply` — عند الإنشاء فقط، فسائق قائم لا يُطرد بتغيير باقة | ✅ (`cities_max` معرَّف بلا نقطة إنفاذ بعد) |
| K5 | `GET /tenant/config` يرجع **الاستحقاقات المحسوبة** لا `features` الخام (الخام تجاوزات فقط، فكانت ستُظهر ميزات الباقة مطفأة) — موثّقة كعرض لا حماية | ✅ |
| K6 | `feature.guard.spec` + `features.spec` — منها اختبار أن الترويسة لا تزوّر المستأجر، وأن الحارس يقرأ التوكن وحده | ✅ |
| K7 | السيادة = كل الميزات بلا حدود، قراراً مقصوداً (لا وهم حماية تقنية) | ✅ |
**نقاط محروسة الآن**: `dispatch` · `chat` · `payments/charge`. المدفوعات محروسة **لكل نقطة** لا على مستوى الصنف — الـwebhook يناديه المزوّد بلا JWT فكان حارس الصنف سيمنعه.
**تغيير مقصود على الـseed**: المستأجر التجريبي `siro` صار `sovereign` بدل `brand`. سببان: يطابق قرار المالك («سيرو أول مشترك، له الباقة الكاملة»)، ويمنع حجب `dispatch` وحدّ `drivers_max: 500` من إسقاط سكربتات التحقق (كل تشغيلة تحميل تُنشئ عشرات السائقين وهم يتراكمون).
**ما تبقّى للمجموعة K**: حارس دور المنصة على نقاط `admin/*` (مفتوحة حالياً — بند قائم قبل K أصلاً)، ونقطة إنفاذ لـ`cities_max`.
---