feat: N1 — خدمة اللوحات + فرض تعليق المستأجر + النظرة الشاملة
- اللوحات الثلاث تُخدم من نفس أصل الـAPI (/panel/{superadmin,admin,service})
عبر useStaticAssets + bind mount، فترث TLS القائم بلا دومين ولا CORS.
- تعليق المستأجر كان زخرفة: tenant.status يُكتب ويُعرض بلا أي فرض. الآن
يُفرض في JwtStrategy (نقطة واحدة) + AuthService.resolveTenant، مع إبطال
الكاش ليسري فوراً، وتمرير عند تعذّر القراءة حتى لا تسقط المنصة كلها.
- GET /admin/overview: رحلات · GMV · إيراد المنصة لكل مستأجر باستعلام
واحد مجمَّع (لا N+1).
- توثيق قرار طبقات التطبيق (const flags + tree-shaking · أصيل موحّد
لـShorebird · طبقتا الإعداد) في docs/22 §1.5.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
5cad6adbde
commit
3ab74a13a8
@@ -1,6 +1,9 @@
|
||||
// اختبار طبقة التزويد (docs/22 — N): السوبر-أدمن ينشئ مستأجراً كاملاً ويقرأ
|
||||
// مانيفست تطبيقه. يتطلّب PLATFORM_SECRET (نفس سرّ السيرفر).
|
||||
//
|
||||
// ⚠️ يتطلّب أيضاً OTP_DEV_MODE=true: قسم التعليق (10) ينادي send-otp مرّتين،
|
||||
// وبلا وضع التطوير تصير كل دورة اختبار رسالتَي واتساب مدفوعتين فعلياً.
|
||||
//
|
||||
// التشغيل:
|
||||
// docker run --rm --network tripz-net -e BASE=http://tripz-api:4010/api \
|
||||
// -e SECRET=$PLATFORM_SECRET -v /home/tripz-llc/backend/scripts:/s \
|
||||
@@ -77,6 +80,44 @@ async function main() {
|
||||
setPay.body.settings?.payment_methods?.includes('cliq') &&
|
||||
!setPay.body.settings?.payment_methods?.includes('made_up_gateway'));
|
||||
|
||||
// 9) النظرة الشاملة (docs/22 — N1b)
|
||||
const ov = await api('GET', '/admin/overview?days=30');
|
||||
check('النظرة الشاملة تعمل', ov.ok && typeof ov.body.totals?.tenants === 'number');
|
||||
check('المستأجر الجديد يظهر فيها', (ov.body.tenants || []).some((t) => t.slug === slug));
|
||||
check('أرقام المال أعداد لا نصوص', typeof (ov.body.totals?.gmv) === 'number' &&
|
||||
typeof (ov.body.totals?.revenue) === 'number');
|
||||
const badWindow = await api('GET', '/admin/overview?days=abc');
|
||||
check('نافذة غير صالحة تسقط للافتراضي لا تنكسر', badWindow.ok && badWindow.body.window_days === 30);
|
||||
|
||||
// 10) التعليق يسري فعلياً (docs/22 — N1a) — الحقل كان زخرفة بلا فرض
|
||||
const badStatus = await api('PATCH', `/admin/tenants/${tenantId}/status`, { status: 'whatever' });
|
||||
check('حالة مخترعة تُرفض', badStatus.status === 400);
|
||||
|
||||
const susp = await api('PATCH', `/admin/tenants/${tenantId}/status`, { status: 'suspended' });
|
||||
check('تعليق المستأجر نجح', susp.ok && susp.body.status === 'suspended');
|
||||
|
||||
// الإثبات الحقيقي: الدخول نفسه يُقطع — لا مجرّد تغيّر الحقل.
|
||||
const otpWhileSuspended = await fetch(`${BASE}/auth/send-otp`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'x-tenant-id': slug },
|
||||
body: JSON.stringify({ phone: '01000000000' }),
|
||||
});
|
||||
check('المستأجر المعلَّق لا يستطيع حتى طلب OTP (401)', otpWhileSuspended.status === 401,
|
||||
`status=${otpWhileSuspended.status}`);
|
||||
|
||||
const back = await api('PATCH', `/admin/tenants/${tenantId}/status`, { status: 'active' });
|
||||
check('إعادة التفعيل تعمل', back.ok && back.body.status === 'active');
|
||||
|
||||
const otpAfter = await fetch(`${BASE}/auth/send-otp`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json', 'x-tenant-id': slug },
|
||||
body: JSON.stringify({ phone: '01000000000' }),
|
||||
});
|
||||
// نتحقّق من 2xx تحديداً لا من «ليس 401»: لو صادف حدّ الطلبات (429) لمرّ
|
||||
// الاختبار وهو لا يثبت شيئاً عن إبطال الكاش.
|
||||
check('بعد إعادة التفعيل يعود الدخول فوراً (الكاش أُبطل)',
|
||||
otpAfter.status >= 200 && otpAfter.status < 300, `status=${otpAfter.status}`);
|
||||
|
||||
console.log('\n' + '═'.repeat(48));
|
||||
console.log(`النتيجة: ${passed} نجح · ${failed} فشل`);
|
||||
console.log('═'.repeat(48) + '\n');
|
||||
|
||||
Reference in New Issue
Block a user