diff --git a/backend/scripts/provision-test.mjs b/backend/scripts/provision-test.mjs new file mode 100644 index 0000000..ef46c9d --- /dev/null +++ b/backend/scripts/provision-test.mjs @@ -0,0 +1,86 @@ +// اختبار طبقة التزويد (docs/22 — N): السوبر-أدمن ينشئ مستأجراً كاملاً ويقرأ +// مانيفست تطبيقه. يتطلّب PLATFORM_SECRET (نفس سرّ السيرفر). +// +// التشغيل: +// docker run --rm --network tripz-net -e BASE=http://tripz-api:4010/api \ +// -e SECRET=$PLATFORM_SECRET -v /home/tripz-llc/backend/scripts:/s \ +// node:22-alpine node /s/provision-test.mjs + +const BASE = process.env.BASE || 'http://localhost:4010/api'; +const SECRET = process.env.SECRET || process.env.PLATFORM_SECRET || ''; + +let passed = 0, failed = 0; +const check = (n, c, d) => { c ? (passed++, console.log(` ✅ ${n}`)) : (failed++, console.log(` ❌ ${n}${d ? ` — ${d}` : ''}`)); }; + +async function api(method, path, body, secret = SECRET) { + const res = await fetch(`${BASE}${path}`, { + method, + headers: { 'Content-Type': 'application/json', ...(secret ? { 'x-platform-secret': secret } : {}) }, + body: body ? JSON.stringify(body) : undefined, + }); + const text = await res.text().catch(() => ''); + let json = {}; try { json = text ? JSON.parse(text) : {}; } catch {} + return { status: res.status, ok: res.ok, body: json }; +} + +async function main() { + console.log(`\n🧪 اختبار تزويد السوبر-أدمن على ${BASE}\n`); + if (!SECRET) { console.log(' ❌ لا PLATFORM_SECRET — مرّره عبر -e SECRET=…'); process.exit(1); } + + // 1) الحارس: بلا سرّ يُرفض + const noSecret = await api('GET', '/admin/tenants', null, ''); + check('نقاط السوبر-أدمن محميّة (بلا سرّ = 401)', noSecret.status === 401, `status=${noSecret.status}`); + + const wrong = await api('GET', '/admin/tenants', null, 'wrong-secret'); + check('سرّ خاطئ يُرفض', wrong.status === 401); + + // 2) الكتالوج + const cat = await api('GET', '/admin/features'); + check('كتالوج الميزات ووسائل الدفع متاح', Array.isArray(cat.body.features) && Array.isArray(cat.body.payment_methods)); + + // 3) تزويد مستأجر كامل + const slug = `test${Date.now().toString().slice(-8)}`; + const prov = await api('POST', '/admin/tenants/provision', { + name: 'مستأجر اختبار', slug, countryPack: 'eg', plan: 'brand', + features: { bots: true }, paymentMethods: ['cash', 'paymob'], + branding: { app_name: 'تطبيق الاختبار', bundle_id_android: 'com.test.rider', bundle_id_ios: 'com.test.rider' }, + }); + check('تزويد مستأجر جديد نجح', prov.ok, JSON.stringify(prov.body).slice(0, 120)); + const tenantId = prov.body.id; + + // 4) وسائل الدفع صُفّيت للكتالوج + const summary = await api('GET', `/admin/tenants/${tenantId}/summary`); + check('وسائل الدفع محفوظة', Array.isArray(summary.body.payment_methods) && + summary.body.payment_methods.includes('paymob'), JSON.stringify(summary.body.payment_methods)); + + // 5) الاستحقاقات: الميزة المشتراة مفعّلة + الافتراض منع + const ent = await api('GET', `/admin/tenants/${tenantId}/entitlements`); + check('ميزة bots المشتراة مفعّلة', ent.body.features?.bots === true); + check('ميزة غير مشتراة (transit) محجوبة افتراضاً', ent.body.features?.transit === false); + + // 6) المانيفست جاهز لسكربت البناء + const manifest = await api('GET', `/admin/tenants/${tenantId}/app-manifest`); + check('المانيفست يحمل bundle ID', manifest.body.bundle_id_android === 'com.test.rider'); + check('المانيفست يحمل اسم التطبيق', manifest.body.app_name === 'تطبيق الاختبار'); + check('المانيفست يحمل الدولة والدفع', manifest.body.country_pack === 'eg' && + manifest.body.payment_methods.includes('paymob')); + + // 7) slug مكرّر يُرفض + const dup = await api('POST', '/admin/tenants/provision', { name: 'x', slug }); + check('slug مكرّر يُرفض', dup.status === 400); + + // 8) تعديل وسائل الدفع (تصفية المخترع) + const setPay = await api('PATCH', `/admin/tenants/${tenantId}/payment-methods`, { + methods: ['cash', 'made_up_gateway', 'cliq'], + }); + check('وسيلة مخترعة تُصفّى، الصحيحة تبقى', + setPay.body.settings?.payment_methods?.includes('cliq') && + !setPay.body.settings?.payment_methods?.includes('made_up_gateway')); + + console.log('\n' + '═'.repeat(48)); + console.log(`النتيجة: ${passed} نجح · ${failed} فشل`); + console.log('═'.repeat(48) + '\n'); + process.exit(failed === 0 ? 0 : 1); +} + +main().catch((e) => { console.error('💥', e.message); process.exit(1); }); diff --git a/backend/src/common/entitlements/payment-methods.ts b/backend/src/common/entitlements/payment-methods.ts new file mode 100644 index 0000000..d954739 --- /dev/null +++ b/backend/src/common/entitlements/payment-methods.ts @@ -0,0 +1,29 @@ +/** + * كتالوج بوابات الدفع المتاحة (docs/22 — N) والدول التي تخدمها. + * السوبر-أدمن يفعّل ما يناسب دولة المستأجر عند التزويد. + */ +export const PAYMENT_METHODS = [ + { code: 'cash', name_ar: 'نقداً', countries: ['jo', 'sy', 'eg'] }, + { code: 'wallet', name_ar: 'المحفظة', countries: ['jo', 'sy', 'eg'] }, + { code: 'cliq', name_ar: 'كليك', countries: ['jo'] }, + { code: 'zaincash', name_ar: 'زين كاش', countries: ['jo'] }, + { code: 'paymob', name_ar: 'باي موب', countries: ['eg'] }, + { code: 'mtn', name_ar: 'MTN', countries: ['sy'] }, + { code: 'syriatel', name_ar: 'سيرياتيل كاش', countries: ['sy'] }, + { code: 'shamcash', name_ar: 'شام كاش', countries: ['sy'] }, +] as const; + +export type PaymentMethodCode = (typeof PAYMENT_METHODS)[number]['code']; + +/** الوسائل الافتراضية لدولة (نقد + محفظة + بوابات الدولة) — نقطة بداية التزويد. */ +export function defaultPaymentMethods(countryPack: string): string[] { + return PAYMENT_METHODS.filter((m) => (m.countries as readonly string[]).includes(countryPack)).map( + (m) => m.code, + ); +} + +/** يتحقق أن الوسائل المطلوبة كلها في الكتالوج (لا وسيلة مخترعة). */ +export function validPaymentMethods(codes: string[]): string[] { + const known = new Set(PAYMENT_METHODS.map((m) => m.code)); + return (codes ?? []).filter((c) => known.has(c)); +} diff --git a/backend/src/modules/tenants/tenants.controller.ts b/backend/src/modules/tenants/tenants.controller.ts index 7a929ac..c3c9bae 100644 --- a/backend/src/modules/tenants/tenants.controller.ts +++ b/backend/src/modules/tenants/tenants.controller.ts @@ -1,14 +1,31 @@ -import { Body, Controller, Get, Param, Patch, Post, UseGuards } from '@nestjs/common'; +import { + Body, + Controller, + Get, + Param, + Patch, + Post, + Res, + UploadedFile, + UseGuards, + UseInterceptors, +} from '@nestjs/common'; +import { FileInterceptor } from '@nestjs/platform-express'; import { ApiTags, ApiSecurity } from '@nestjs/swagger'; -import { TenantsService } from './tenants.service'; +import { TenantsService, ProvisionDto } from './tenants.service'; import { Tenant } from '../../database/entities/tenant.entity'; import { FEATURES } from '../../common/entitlements/features'; +import { PAYMENT_METHODS } from '../../common/entitlements/payment-methods'; import { PlatformGuard } from '../../common/platform/platform.guard'; +import { StorageService } from '../../common/storage/storage.service'; @ApiTags('tenants') @Controller() export class TenantsController { - constructor(private readonly tenants: TenantsService) {} + constructor( + private readonly tenants: TenantsService, + private readonly storage: StorageService, + ) {} /** * إعداد المستأجر عند إقلاع التطبيق. @@ -23,6 +40,27 @@ export class TenantsController { return this.tenants.config(slug); } + /** + * لوغو المستأجر — **عام** (يظهر في splash التطبيق ولوحة السوبر-أدمن). + * يخدم أصل الهوية البصرية **فقط** عبر `branding.logo_key` — لا يخدم مجلد + * التخزين كاملاً (فيه صور وثائق الهوية الحساسة). لذلك نقطة مخصّصة لا + * `useStaticAssets` على كل `/storage`. + */ + @Get('tenant/logo/:slug') + async logoOf(@Param('slug') slug: string, @Res() res: any) { + const t = await this.tenants.findBySlug(slug); + const key = t?.branding?.logo_key; + if (!key) return res.status(404).json({ message: 'no logo' }); + try { + const buf = await this.storage.read(key); + res.setHeader('Content-Type', key.endsWith('.png') ? 'image/png' : 'image/jpeg'); + res.setHeader('Cache-Control', 'public, max-age=3600'); + return res.send(buf); + } catch { + return res.status(404).json({ message: 'logo not found' }); + } + } + // ---- سوبر-أدمن: مالك المنصة يدير كل المستأجرين ---- // كلها خلف PlatformGuard (`x-platform-secret`). حرجٌ أن تكون خارج أدوار // المستأجر: بلا هذا الحارس يستطيع أدمن أي مستأجر ترقية اشتراكه بنفسه — @@ -35,12 +73,12 @@ export class TenantsController { return this.tenants.findAll(); } - /** كتالوج الميزات القابلة للبيع — تعرضه لوحة السوبر-أدمن. */ + /** كتالوج الميزات ووسائل الدفع — تعرضهما لوحة السوبر-أدمن عند التزويد. */ @ApiSecurity('x-platform-secret') @UseGuards(PlatformGuard) @Get('admin/features') features() { - return { features: FEATURES }; + return { features: FEATURES, payment_methods: PAYMENT_METHODS }; } @ApiSecurity('x-platform-secret') @@ -50,6 +88,56 @@ export class TenantsController { return this.tenants.create(body); } + /** تزويد مستأجر كامل بضربة واحدة (docs/22 — N1). */ + @ApiSecurity('x-platform-secret') + @UseGuards(PlatformGuard) + @Post('admin/tenants/provision') + provision(@Body() body: ProvisionDto) { + return this.tenants.provision(body); + } + + /** ملخّص كامل لمستأجر — للوحة السوبر-أدمن. */ + @ApiSecurity('x-platform-secret') + @UseGuards(PlatformGuard) + @Get('admin/tenants/:id/summary') + summary(@Param('id') id: string) { + return this.tenants.summary(id); + } + + /** مانيفست توليد التطبيق — يستهلكه سكربت البناء (docs/22 — N3). */ + @ApiSecurity('x-platform-secret') + @UseGuards(PlatformGuard) + @Get('admin/tenants/:id/app-manifest') + appManifest(@Param('id') id: string) { + return this.tenants.appManifest(id); + } + + /** الهوية البصرية (اسم التطبيق · bundle IDs · ألوان). */ + @ApiSecurity('x-platform-secret') + @UseGuards(PlatformGuard) + @Patch('admin/tenants/:id/branding') + branding(@Param('id') id: string, @Body() body: Record) { + return this.tenants.setBranding(id, body); + } + + /** وسائل الدفع المفعّلة للمستأجر. */ + @ApiSecurity('x-platform-secret') + @UseGuards(PlatformGuard) + @Patch('admin/tenants/:id/payment-methods') + paymentMethods(@Param('id') id: string, @Body('methods') methods: string[]) { + return this.tenants.setPaymentMethods(id, methods); + } + + /** رفع لوغو المستأجر (docs/22 — N2) → يُخزَّن ويُربط في الهوية البصرية. */ + @ApiSecurity('x-platform-secret') + @UseGuards(PlatformGuard) + @Post('admin/tenants/:id/logo') + @UseInterceptors(FileInterceptor('file')) + async logo(@Param('id') id: string, @UploadedFile() file: any) { + const key = await this.storage.save(id, 'branding', file.buffer, file.originalname); + return this.tenants.setBranding(id, { logo_key: key }); + } + /** الاستحقاقات الفعّالة كما يراها الحارس — للتشخيص ولعرضها في اللوحة. */ @ApiSecurity('x-platform-secret') @UseGuards(PlatformGuard) diff --git a/backend/src/modules/tenants/tenants.service.ts b/backend/src/modules/tenants/tenants.service.ts index 351e39a..383c9ff 100644 --- a/backend/src/modules/tenants/tenants.service.ts +++ b/backend/src/modules/tenants/tenants.service.ts @@ -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; + paymentMethods?: string[]; + branding?: Record; // 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 { + 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): Promise { + 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 { + 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); diff --git a/dashboards/superadmin-web/index.html b/dashboards/superadmin-web/index.html new file mode 100644 index 0000000..c191881 --- /dev/null +++ b/dashboards/superadmin-web/index.html @@ -0,0 +1,204 @@ + + + + + + Tripz — لوحة السوبر-أدمن + + + + +
+

🚕 Tripz — لوحة السوبر-أدمن

+ + + +
+
+ +
+

المستأجرون

+ + + +
الاسمslugالدولةالباقةالحالة
…
+
+ + +
+

تزويد مستأجر جديد

+
+
+
+
+
+
+ +
+
+ +
+
+ +
+
+
+
+
+
+
+
+
+ +
+ + + +

سرّ المنصة

+

أدخل PLATFORM_SECRET للوصول. يُحفظ في هذه الجلسة فقط.

+ +
+
+ + + + diff --git a/docs/15-deploy-flow.md b/docs/15-deploy-flow.md index 85d6f1e..c7e8769 100644 --- a/docs/15-deploy-flow.md +++ b/docs/15-deploy-flow.md @@ -86,3 +86,11 @@ docker run --rm --network tripz-net -e BASE=http://tripz-api:4010/api \ -v /home/tripz-llc/backend/scripts:/s node:22-alpine node /s/e2e-test.mjs # يتطلّب OTP_DEV_MODE=true. ``` + +``` +# تزويد السوبر-أدمن (docs/22 — N): يثبت إنشاء مستأجر + المانيفست + الحارس +docker run --rm --network tripz-net -e BASE=http://tripz-api:4010/api \ + -e SECRET=$PLATFORM_SECRET -v /home/tripz-llc/backend/scripts:/s \ + node:22-alpine node /s/provision-test.mjs +# (بدّل $PLATFORM_SECRET بقيمته الفعلية من .env) +``` diff --git a/docs/22-full-product-roadmap.md b/docs/22-full-product-roadmap.md index f661c09..14ada85 100644 --- a/docs/22-full-product-roadmap.md +++ b/docs/22-full-product-roadmap.md @@ -75,14 +75,16 @@ | M3 | **السياج الجغرافي** (من `Admin/geofence`): مناطق مسموح/ممنوع · تسعير حسب المنطقة · حظر دخول. | | M4 | **تأمين السائق** (`driver_assurance`). | -### المجموعة N — طبقة الـSaaS: السوبر-أدمن وتوليد التطبيق ⭐ (الهدف الأساسي) -| # | البند | -|---|-------| -| N1 | **لوحة سوبر-أدمن (ويب)**: إنشاء مستأجر · اسم/لوغو/دولة/دفع/باقة/ميزات · تعديل الاشتراك · مراقبة الأسطول. (الباك إند K جاهز؛ نبني الواجهة.) | -| N2 | **رفع اللوغو + توليد الأيقونات/splash** تلقائياً لكل مستأجر. | -| N3 | **سكربت توليد التطبيق الواحد**: يضبط bundle ID · يبدّل الأيقونات/الـsplash من اللوغو · يحقن FCM · **يربط إضافات Kotlin/C++ الأصيلة** (overlay · method channels · NDK · root) المبنية على bundle ID · يبني · **يسجّل ويدفع عبر Shorebird**. | -| N4 | **لوحة أدمن المستأجر (ويب)**: dispatch · سائقون · رحلات · تعرفة · تقارير · مراجعة وثائق. | -| N5 | **لوحة خدمة العملاء (ويب)**: بحث مستخدم · شكاوى · تدخّل. | +### المجموعة N — طبقة الـSaaS: السوبر-أدمن وتوليد التطبيق ⭐ (الهدف الأساسي) — 🔵 قيد التنفيذ +| # | البند | الحالة | +|---|-------|--------| +| N1 | **لوحة سوبر-أدمن (ويب)**: إنشاء مستأجر · اسم/لوغو/دولة/دفع/باقة/ميزات · مراقبة. | 🔵 الباك إند ✅ (`provision` · `summary` · `app-manifest` · `payment-methods` · `branding` خلف PlatformGuard؛ كتالوج `payment-methods.ts`) + واجهة ويب مكتفية ذاتياً (`dashboards/superadmin-web/index.html`). الباقي: لوحة أغنى (تعديل/تعطيل/GMV). | +| N2 | **رفع اللوغو + توليد الأيقونات/splash**. | 🔵 الباك إند ✅ (`POST /admin/tenants/:id/logo` + `GET /tenant/logo/:slug` **عام لأصل الهوية فقط** — لا يخدم مجلد التخزين كاملاً حتى لا تُسرَّب صور الوثائق). توليد الأيقونات نفسه في N3. | +| N3 | **سكربت توليد التطبيق الواحد**: bundle ID · أيقونات/splash من اللوغو · FCM · **إضافات Kotlin/C++ الأصيلة** (overlay · method channels · NDK) · بناء · **Shorebird**. | 🔵 `scripts/generate-tenant-app.sh` — يجلب المانيفست ويضبط الهيكل؛ خطوات فلاتر موسومة [Q] تُوصَل عند بناء المجموعة Q (المشروع غير موجود بعد). | +| N4 | **لوحة أدمن المستأجر (ويب)**: dispatch · سائقون · رحلات · تعرفة · تقارير · مراجعة وثائق. | ⏳ التالي | +| N5 | **لوحة خدمة العملاء (ويب)**: بحث مستخدم · شكاوى · تدخّل. | ⏳ | + +**اختبار**: `backend/scripts/provision-test.mjs` (يتطلّب `PLATFORM_SECRET`) — يثبت الحارس، التزويد، تصفية الدفع، الاستحقاقات، والمانيفست. ### المجموعة O — التحليلات والمواصلات والتسويق | # | البند | diff --git a/scripts/generate-tenant-app.sh b/scripts/generate-tenant-app.sh new file mode 100755 index 0000000..333c629 --- /dev/null +++ b/scripts/generate-tenant-app.sh @@ -0,0 +1,78 @@ +#!/usr/bin/env bash +# ========================================================================== +# توليد تطبيق مستأجر واحد من مانيفست الباك إند (docs/22 — N3). +# +# سكربت واحد يأخذ slug المستأجر → يجلب مانيفسته → يضبط bundle ID والأيقونات +# والـsplash من اللوغو، يحقن FCM، يربط إضافات Kotlin/C++ الأصيلة (overlay · +# method channels · NDK) المبنية على bundle ID، يبني، ويسجّل/يدفع عبر Shorebird. +# +# الاستعمال: +# PLATFORM_SECRET=xxx API=https://tripz-api.intaleqapp.com/api \ +# ./scripts/generate-tenant-app.sh +# +# ⚠️ الخطوات الموسومة [Q] تعتمد على مشروع فلاتر (المجموعة Q) الذي لم يُبنَ بعد. +# البنية والعقد (المانيفست) جاهزان الآن؛ تُوصَل هذه الخطوات عند وصول Q. +# ========================================================================== +set -euo pipefail + +SLUG="${1:?الاستعمال: generate-tenant-app.sh }" +APP_KIND="${2:-rider}" # rider | driver +API="${API:?اضبط API=https://tripz-api.intaleqapp.com/api}" +SECRET="${PLATFORM_SECRET:?اضبط PLATFORM_SECRET}" + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +APP_DIR="$ROOT/apps/$APP_KIND" # [Q] مشروع فلاتر لكل نوع +WORK="$ROOT/.build/$SLUG-$APP_KIND" + +echo "▶ توليد تطبيق [$APP_KIND] للمستأجر [$SLUG]" + +# ── 1. جلب المانيفست من الباك إند (جاهز الآن) ────────────────────────── +echo " • جلب المانيفست…" +# نحصل على tenant id من القائمة ثم المانيفست +TENANT_ID="$(curl -fsSL -H "x-platform-secret: $SECRET" "$API/admin/tenants" \ + | node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{const t=JSON.parse(s).find(x=>x.slug==='$SLUG');if(!t){console.error('مستأجر غير موجود: $SLUG');process.exit(1)}process.stdout.write(t.id)})")" + +MANIFEST="$(curl -fsSL -H "x-platform-secret: $SECRET" "$API/admin/tenants/$TENANT_ID/app-manifest")" +echo "$MANIFEST" | node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{const m=JSON.parse(s);console.log(' الاسم:',m.app_name,'| bundleAndroid:',m.bundle_id_android,'| دولة:',m.country_pack)})" + +val() { echo "$MANIFEST" | node -e "let s='';process.stdin.on('data',d=>s+=d).on('end',()=>{process.stdout.write(String(JSON.parse(s).$1 ?? ''))})"; } +APP_NAME="$(val app_name)" +BID_ANDROID="$(val bundle_id_android)" +BID_IOS="$(val bundle_id_ios)" +COUNTRY="$(val country_pack)" + +[ -z "$BID_ANDROID" ] && { echo " ✗ bundle_id_android غير مضبوط في المانيفست — اضبطه من لوحة السوبر-أدمن أولاً"; exit 1; } + +# ── 2. تحضير نسخة عمل من مشروع فلاتر ─────────────────────────────────── +if [ ! -d "$APP_DIR" ]; then + echo " ⚠️ [Q] مشروع فلاتر $APP_DIR غير موجود بعد — تخطّي خطوات البناء." + echo " المانيفست جاهز؛ تُوصَل خطوات فلاتر عند بناء المجموعة Q." + exit 0 +fi +rm -rf "$WORK"; mkdir -p "$(dirname "$WORK")"; cp -r "$APP_DIR" "$WORK" + +# ── 3. ضبط bundle ID (أندرويد + iOS) ─────────────────────────────────── +echo " • ضبط bundle ID…" +# [Q] applicationId في build.gradle، و PRODUCT_BUNDLE_IDENTIFIER في iOS. +# الإضافات الأصيلة (overlay/NDK/method-channel) مبنية على bundle ID — +# تُحدَّث مراجعها هنا. راجع apps/*/android و apps/*/ios. +# sed -i "s/applicationId \".*\"/applicationId \"$BID_ANDROID\"/" "$WORK/android/app/build.gradle" + +# ── 4. الأيقونات و splash من اللوغو ──────────────────────────────────── +echo " • توليد الأيقونات و splash من اللوغو…" +curl -fsSL "$API/tenant/logo/$SLUG" -o "$WORK/assets/logo.png" 2>/dev/null || echo " (لا لوغو مرفوع — يُستعمل الافتراضي)" +# [Q] flutter_launcher_icons + flutter_native_splash من assets/logo.png + +# ── 5. حقن FCM + إعداد المستأجر ──────────────────────────────────────── +echo " • حقن إعداد المستأجر ($COUNTRY)…" +# [Q] نسخ google-services.json / GoogleService-Info.plist الخاص بالمستأجر. +# BASE_URL و TENANT عبر --dart-define عند البناء. + +# ── 6. البناء + Shorebird ────────────────────────────────────────────── +echo " • البناء وتسجيل Shorebird…" +# [Q] أول مرة: shorebird init (يسجّل app_id لهذا bundle ID في حساب Shorebird). +# التحديثات: shorebird release / shorebird patch. +# ( cd "$WORK" && shorebird release android \ +# --dart-define=BASE_URL=${API} --dart-define=TENANT=$SLUG ) + +echo "✅ اكتمل توليد [$APP_KIND] لـ[$SLUG] (الخطوات الموسومة [Q] معلّقة حتى مشروع فلاتر)."