feat: حارس السوبر-أدمن + سياسة الدين لكل مستأجر

PlatformGuard (x-platform-secret، مقارنة ثابتة الزمن) يحرس admin/tenants*
و admin/features و platform/*. بدونه كان أدمن أي مستأجر يرقّي اشتراكه بنفسه
— أي أن المجموعة K كلها كانت زينة. سرّ غير مضبوط = منع الجميع لا فتح الباب
(الإعداد الناقص خطأ تشغيلي شائع، وفتح الباب عنده أسوأ من إغلاقه).
admin/users/:id/role تبقى لأدمن المستأجر: نطاقها من التوكن، وليست نقطة منصة.

سياسة الدين لكل مستأجر (قرار المالك): كل مشغّل يحدّد كم ديناً يسمح به،
و0 خيار صريح = لا دين إطلاقاً.
- tenants.settings jsonb — سياسات تشغيل يحكمها المستأجر، منفصلة عن features
  (استحقاقات يحكمها السوبر-أدمن): «كيف تشتغل» مقابل «ماذا اشتريت»
- credit-policy.ts: debt_allowance + signup_bonus لكل عملة. الافتراضات
  صُحّحت لقيم المالك: 2 JOD · 200 SYP · 200 EGP (كانت 5/500/500)
- تُخزَّن موجبةً («كم ديناً أسمح؟») لا كأرضية سالبة — أوضح للمشغّل و0 بلا
  لبس في الإشارة
- PATCH /credit/policy لأدمن المستأجر؛ updateSettings يدمج عميقاً فلا يمحو
  سياسة أخرى، ويُبطل الكاش وإلا سرت السياسة القديمة حتى ساعة
- إعداد مكسور يسقط للافتراض لا لدين بلا حدّ. والصفر لا يُبتلع (فخّ ||)

هجرة: TenantSettings.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Hamza-Ayed
2026-07-17 15:26:41 +03:00
co-authored by Claude Opus 4.8
parent aae01fc4ee
commit 91e1692457
16 changed files with 388 additions and 60 deletions
@@ -1,8 +1,9 @@
import { Body, Controller, Get, Param, Patch, Post } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { Body, Controller, Get, Param, Patch, Post, UseGuards } from '@nestjs/common';
import { ApiTags, ApiSecurity } from '@nestjs/swagger';
import { TenantsService } from './tenants.service';
import { Tenant } from '../../database/entities/tenant.entity';
import { FEATURES } from '../../common/entitlements/features';
import { PlatformGuard } from '../../common/platform/platform.guard';
@ApiTags('tenants')
@Controller()
@@ -22,25 +23,36 @@ export class TenantsController {
return this.tenants.config(slug);
}
// ---- سوبر-أدمن (docs/19 — K3). يُحمى بحارس دور المنصة لاحقاً ----
// ---- سوبر-أدمن: مالك المنصة يدير كل المستأجرين ----
// كلها خلف PlatformGuard (`x-platform-secret`). حرجٌ أن تكون خارج أدوار
// المستأجر: بلا هذا الحارس يستطيع أدمن أي مستأجر ترقية اشتراكه بنفسه —
// أي أن كل المجموعة K كانت ستصير زينة.
@ApiSecurity('x-platform-secret')
@UseGuards(PlatformGuard)
@Get('admin/tenants')
list() {
return this.tenants.findAll();
}
/** كتالوج الميزات القابلة للبيع — تعرضه لوحة السوبر-أدمن. */
@ApiSecurity('x-platform-secret')
@UseGuards(PlatformGuard)
@Get('admin/features')
features() {
return { features: FEATURES };
}
@ApiSecurity('x-platform-secret')
@UseGuards(PlatformGuard)
@Post('admin/tenants')
create(@Body() body: Partial<Tenant>) {
return this.tenants.create(body);
}
/** الاستحقاقات الفعّالة كما يراها الحارس — للتشخيص ولعرضها في اللوحة. */
@ApiSecurity('x-platform-secret')
@UseGuards(PlatformGuard)
@Get('admin/tenants/:id/entitlements')
entitlements(@Param('id') id: string) {
return this.tenants.entitlementsOf(id);
@@ -50,6 +62,8 @@ export class TenantsController {
* تغيير الاشتراك و/أو الميزات المشتراة منفردة.
* يُبطل الكاش فوراً فيسري خلال ثوانٍ **بلا تحديث تطبيق**.
*/
@ApiSecurity('x-platform-secret')
@UseGuards(PlatformGuard)
@Patch('admin/tenants/:id/subscription')
updateSubscription(
@Param('id') id: string,
@@ -54,6 +54,28 @@ export class TenantsService {
await this.entitlements.invalidate(tenant.id);
}
/**
* سياسات تشغيل المستأجر — يضبطها **المستأجر نفسه** لا السوبر-أدمن.
* الدمج عميق مفتاحاً بمفتاح: ضبط سياسة الدين يجب ألّا يمحو باقي السياسات.
*/
async updateSettings(
tenantId: string,
patch: Record<string, Record<string, any>>,
): Promise<Tenant> {
const t = await this.repo.findOne({ where: { id: tenantId } });
if (!t) throw new NotFoundException('Tenant not found');
const merged: Record<string, any> = { ...(t.settings ?? {}) };
for (const [section, value] of Object.entries(patch)) {
merged[section] = { ...(merged[section] ?? {}), ...value };
}
t.settings = merged;
const saved = await this.repo.save(t);
await this.invalidate(saved); // وإلا قرأت الخدمة السياسة القديمة حتى ساعة
return saved;
}
/** الاستحقاقات الفعّالة — نفس ما يقرأه `FeatureGuard` (docs/19 — K3). */
entitlementsOf(tenantId: string) {
return this.entitlements.forTenant(tenantId);