feat: المجموعة N (طبقة الـSaaS) — تزويد المستأجر + مانيفست التطبيق + لوحة سوبر-أدمن
الهدف الأساسي: السوبر-أدمن ينشئ مستأجراً كاملاً (اسم/لوغو/دولة/دفع/باقة/ ميزات/bundle IDs) ويولّد تطبيقه. الباك إند (خلف PlatformGuard): - POST /admin/tenants/provision — تزويد بضربة واحدة - GET /admin/tenants/:id/summary + /app-manifest (يستهلكه سكربت البناء) - PATCH .../branding + .../payment-methods - POST /admin/tenants/:id/logo (رفع) + GET /tenant/logo/:slug عام **يخدم أصل الهوية فقط لا مجلد التخزين كاملاً** — وإلا سُرّبت صور وثائق الهوية الحساسة بمفاتيحها - كتالوج payment-methods.ts (cash/wallet/cliq/zaincash/paymob/mtn/ syriatel/shamcash حسب الدولة) + تصفية المخترع - GET /admin/features يرجع الميزات ووسائل الدفع للكتالوج الواجهة: dashboards/superadmin-web/index.html — SPA مكتفية ذاتياً (vanilla JS، بلا خطوة بناء)، سرّ المنصة في sessionStorage، RTL. تُخدَم كملف ثابت. سكربت التوليد: scripts/generate-tenant-app.sh — يجلب المانيفست ويضبط الهيكل؛ خطوات فلاتر موسومة [Q] تُوصَل عند بناء مشروع فلاتر. اختبار: provision-test.mjs (يثبت الحارس + التزويد + تصفية الدفع + الاستحقاقات + المانيفست). لا هجرة — settings/branding jsonb موجودان. القرار: اللوحات ويب (لا فلاتر) — تحديث فوري، سطح مكتب، معيار الصناعة. البدء بـN (طبقة الـSaaS) قرار المالك. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
4fc79181ff
commit
c93be38622
@@ -1,9 +1,21 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { Tenant, TenantPlan } from '../../database/entities/tenant.entity';
|
||||
import { CacheService, CacheKeys, TTL } from '../../common/cache/cache.service';
|
||||
import { EntitlementsService } from '../../common/entitlements/entitlements.service';
|
||||
import { defaultPaymentMethods, validPaymentMethods } from '../../common/entitlements/payment-methods';
|
||||
|
||||
/** ما يدخله السوبر-أدمن لتزويد مستأجر جديد (docs/22 — N1). */
|
||||
export interface ProvisionDto {
|
||||
name: string;
|
||||
slug: string;
|
||||
countryPack?: string;
|
||||
plan?: TenantPlan;
|
||||
features?: Record<string, any>;
|
||||
paymentMethods?: string[];
|
||||
branding?: Record<string, any>; // app_name · bundle ids · colors
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class TenantsService {
|
||||
@@ -76,6 +88,99 @@ export class TenantsService {
|
||||
return saved;
|
||||
}
|
||||
|
||||
/**
|
||||
* تزويد مستأجر جديد بضربة واحدة (docs/22 — N1): اسم · slug · دولة · باقة ·
|
||||
* ميزات · وسائل دفع · هوية بصرية. البديل عن `create` العاري.
|
||||
*/
|
||||
async provision(dto: ProvisionDto): Promise<Tenant> {
|
||||
if (!dto.name || !dto.slug) throw new BadRequestException('name and slug are required');
|
||||
if (await this.findBySlug(dto.slug)) throw new BadRequestException('slug already taken');
|
||||
|
||||
const countryPack = dto.countryPack ?? 'jo';
|
||||
// وسائل الدفع: ما طلبه السوبر-أدمن (مصفّى للكتالوج) أو افتراضات الدولة.
|
||||
const payments = dto.paymentMethods
|
||||
? validPaymentMethods(dto.paymentMethods)
|
||||
: defaultPaymentMethods(countryPack);
|
||||
|
||||
const tenant = this.repo.create({
|
||||
name: dto.name,
|
||||
slug: dto.slug,
|
||||
countryPack,
|
||||
plan: dto.plan ?? 'launch',
|
||||
features: dto.features ?? {},
|
||||
settings: { payment_methods: payments },
|
||||
branding: dto.branding ?? {},
|
||||
status: 'active',
|
||||
});
|
||||
const saved = await this.repo.save(tenant);
|
||||
await this.invalidate(saved);
|
||||
return saved;
|
||||
}
|
||||
|
||||
/** الهوية البصرية (docs/22 — N2): app_name · bundle ids · ألوان · مفتاح اللوغو. */
|
||||
async setBranding(tenantId: string, branding: Record<string, any>): Promise<Tenant> {
|
||||
const t = await this.repo.findOne({ where: { id: tenantId } });
|
||||
if (!t) throw new NotFoundException('Tenant not found');
|
||||
t.branding = { ...(t.branding ?? {}), ...branding };
|
||||
const saved = await this.repo.save(t);
|
||||
await this.invalidate(saved);
|
||||
return saved;
|
||||
}
|
||||
|
||||
/** وسائل الدفع المفعّلة (docs/22 — N1). تُصفّى للكتالوج. */
|
||||
async setPaymentMethods(tenantId: string, methods: string[]): Promise<Tenant> {
|
||||
const t = await this.repo.findOne({ where: { id: tenantId } });
|
||||
if (!t) throw new NotFoundException('Tenant not found');
|
||||
t.settings = { ...(t.settings ?? {}), payment_methods: validPaymentMethods(methods) };
|
||||
const saved = await this.repo.save(t);
|
||||
await this.invalidate(saved);
|
||||
return saved;
|
||||
}
|
||||
|
||||
/**
|
||||
* مانيفست توليد التطبيق (docs/22 — N3): كل ما يحتاجه سكربت البناء لإنتاج
|
||||
* تطبيق المستأجر — الاسم · bundle IDs · اللوغو · الألوان · الميزات · الدفع.
|
||||
*/
|
||||
async appManifest(tenantId: string) {
|
||||
const t = await this.repo.findOne({ where: { id: tenantId } });
|
||||
if (!t) throw new NotFoundException('Tenant not found');
|
||||
const ent = await this.entitlements.forTenant(t.id);
|
||||
const b = t.branding ?? {};
|
||||
return {
|
||||
slug: t.slug,
|
||||
name: t.name,
|
||||
app_name: b.app_name ?? t.name,
|
||||
bundle_id_android: b.bundle_id_android ?? null,
|
||||
bundle_id_ios: b.bundle_id_ios ?? null,
|
||||
logo_url: b.logo_key ? `/storage/${b.logo_key}` : null,
|
||||
colors: b.colors ?? {},
|
||||
country_pack: t.countryPack,
|
||||
payment_methods: t.settings?.payment_methods ?? [],
|
||||
features: ent?.features ?? {},
|
||||
limits: ent?.limits ?? {},
|
||||
};
|
||||
}
|
||||
|
||||
/** ملخّص كامل للوحة السوبر-أدمن (docs/22 — N1). */
|
||||
async summary(tenantId: string) {
|
||||
const t = await this.repo.findOne({ where: { id: tenantId } });
|
||||
if (!t) throw new NotFoundException('Tenant not found');
|
||||
const ent = await this.entitlements.forTenant(t.id);
|
||||
return {
|
||||
id: t.id,
|
||||
slug: t.slug,
|
||||
name: t.name,
|
||||
countryPack: t.countryPack,
|
||||
plan: t.plan,
|
||||
status: t.status,
|
||||
branding: t.branding ?? {},
|
||||
payment_methods: t.settings?.payment_methods ?? [],
|
||||
features: ent?.features ?? {},
|
||||
limits: ent?.limits ?? {},
|
||||
createdAt: t.createdAt,
|
||||
};
|
||||
}
|
||||
|
||||
/** الاستحقاقات الفعّالة — نفس ما يقرأه `FeatureGuard` (docs/19 — K3). */
|
||||
entitlementsOf(tenantId: string) {
|
||||
return this.entitlements.forTenant(tenantId);
|
||||
|
||||
Reference in New Issue
Block a user