Files
tripz-llc/backend/scripts/provision-test.mjs
T
Hamza-AyedandClaude Opus 4.8 c93be38622 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>
2026-07-18 03:39:02 +03:00

87 lines
4.6 KiB
JavaScript

// اختبار طبقة التزويد (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); });