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
@@ -3,14 +3,20 @@ import { ApiBearerAuth, ApiTags } from '@nestjs/swagger';
import { PaymentsService } from './payments.service';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { CurrentUser, AuthUser } from '../auth/decorators/current-user.decorator';
import { FeatureGuard, RequiresFeature } from '../../common/entitlements/feature.guard';
/**
* الحراسة هنا **لكل نقطة** لا على مستوى الصنف: الـwebhook يناديه مزوّد الدفع
* بلا JWT، فحارس الميزة على الصنف كان سيمنعه (بلا مستأجر = منع).
*/
@ApiTags('payments')
@Controller('payments')
export class PaymentsController {
constructor(private readonly payments: PaymentsService) {}
@ApiBearerAuth()
@UseGuards(JwtAuthGuard)
@UseGuards(JwtAuthGuard, FeatureGuard)
@RequiresFeature('payments')
@Post('charge')
charge(@CurrentUser() user: AuthUser, @Body() body: any) {
return this.payments.charge(user.tenantId, {