Files
tripz-llc/backend/scripts/pricing-test.mjs
T
Hamza-AyedandClaude Opus 4.8 90c3f71430 test: add live pricing script across all service classes and countries
يطلب رحلة حقيقية لكل فئة خدمة (9) × 3 مسافات × 3 بلدان عبر الـAPI الحي،
فالمسافة والزمن يأتيان من محرّك المسارات (انطلق) لا من افتراض.

يتحقّق آلياً من أمرين كانا عطلين حقيقيين قبل المجموعة M:
- ولا طلب يرجع بلا سعر (التعرفة المفقودة كانت تنتج رحلة مجانية بعمولة صفر).
- الفان أغلى من الاقتصادي في كل بلد (الأرقام الخام السابقة عكست الترتيب).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-19 00:40:30 +03:00

234 lines
9.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// اختبار تسعير عبر API حقيقي — يطلب رحلة لكل فئة خدمة في كل بلد، ويقرأ
// السعر والمسافة والزمن اللذين رجعهما محرّك التعرفة الفعلي (لا محاكاة).
//
// ينشئ مستأجرَي اختبار لمصر وسوريا إن لم يكونا موجودين (الأردن = "siro"
// القائم أصلاً)، ثم يطلب 3 رحلات نموذجية (قصيرة/متوسطة/طويلة) لكل فئة
// خدمة في كل بلد، ويطبع جدول: البلد × الفئة × الرحلة → السعر.
//
// التشغيل (من داخل شبكة الحاوية، بعد نجاح البناء):
// docker run --rm --network tripz-net -e BASE=http://tripz-api:4010/api \
// -e PLATFORM_SECRET=<القيمة من .env> \
// -v /home/tripz-llc/backend/scripts:/s node:22-alpine node /s/pricing-test.mjs
//
// يتطلّب OTP_DEV_MODE=true (الرمز الثابت 1234) و PLATFORM_SECRET مضبوطاً.
const BASE = process.env.BASE || 'http://localhost:4010/api';
const CODE = '1234';
const PLATFORM_SECRET = process.env.PLATFORM_SECRET || '';
if (!PLATFORM_SECRET) {
console.error('❌ PLATFORM_SECRET غير مضبوط — لا يمكن إنشاء مستأجرَي الاختبار.');
process.exit(1);
}
const SERVICE_CLASSES = [
'saver', 'delivery', 'economy', 'fixed', 'electric', 'lady', 'comfort', 'van', 'vip',
];
/**
* رحلات نموذجية داخل الزرقاء/عمّان (إحداثيات قابلة لإعادة الاستعمال في
* أي بلد — الفارق بين البلدان هو التعرفة لا الجغرافيا؛ المسافة والزمن
* يأتيان من محرّك المسارات الحقيقي `MapsService.route`، لا من افتراض).
*
* النافذة الزمنية الفعلية تُحدَّد بلحظة تشغيل السكربت فعلاً — لا حقلاً في
* الطلب (`TariffEngine.quote` يستعمل `new Date()` الحقيقي). اختبار الذروة
* مقابل النافذة العادية مغطّى بوحدات على `tariff-seed.spec.ts`؛ هنا الهدف
* إثبات أن **الـAPI الحي** يرجع سعراً معقولاً بمسافة/زمن حقيقيَّين من انطلق.
*/
const RIDES = [
{
label: 'قصيرة (داخل الزرقاء)',
origin: { lat: 32.0728, lng: 36.0876 },
destination: { lat: 32.0895, lng: 36.1046 },
},
{
label: 'متوسطة (الزرقاء → عمّان)',
origin: { lat: 32.0728, lng: 36.0876 },
destination: { lat: 31.9539, lng: 35.9106 },
},
{
label: 'طويلة (الزرقاء → المطار)',
origin: { lat: 32.0728, lng: 36.0876 },
destination: { lat: 31.7226, lng: 35.9932 },
},
];
const TENANTS = [
{ slug: 'siro', countryPack: 'jo', currency: 'JOD', create: false }, // موجود أصلاً
{ slug: 'tripz-test-eg', countryPack: 'eg', currency: 'EGP', create: true, name: 'Tripz Test EG' },
{ slug: 'tripz-test-sy', countryPack: 'sy', currency: 'SYP', create: true, name: 'Tripz Test SY' },
];
async function api(method, path, { token, tenant, body, platform } = {}) {
const headers = { 'Content-Type': 'application/json' };
if (token) headers.Authorization = `Bearer ${token}`;
if (tenant) headers['x-tenant-id'] = tenant;
if (platform) headers['x-platform-secret'] = PLATFORM_SECRET;
const res = await fetch(`${BASE}${path}`, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
});
let json = null;
try { json = await res.json(); } catch {}
return { status: res.status, body: json };
}
async function ensureTenant(t) {
if (!t.create) return;
const provisioned = await api('POST', '/admin/tenants/provision', {
platform: true,
body: {
name: t.name,
slug: t.slug,
countryPack: t.countryPack,
plan: 'launch',
},
});
if (provisioned.status >= 200 && provisioned.status < 300) {
console.log(`✅ أُنشئ مستأجر ${t.slug} (${t.countryPack})`);
} else if (/already taken|already exists|موجود/i.test(JSON.stringify(provisioned.body))) {
console.log(`ℹ️ ${t.slug} موجود أصلاً`);
} else {
console.error(`❌ فشل إنشاء ${t.slug}:`, provisioned.status, JSON.stringify(provisioned.body));
}
}
/**
* رقم دولي كامل صالح لكل بلد — **بلا "+"**، تماماً كما يخزّنه
* `PhoneService.normalize` (docs/17 — D1). الطول يجب أن يطابق
* `callingCode + nationalLength` بالضبط وإلا رُفض الرقم:
* الأردن/سوريا 962/963 + 9 أرقام محلية، مصر 20 + 10 أرقام محلية.
*/
function testPhone(countryPack, seed) {
const plans = {
jo: { code: '962', national: 9, prefix: '79' },
sy: { code: '963', national: 9, prefix: '93' },
eg: { code: '20', national: 10, prefix: '10' },
};
const plan = plans[countryPack] ?? plans.jo;
const remaining = plan.national - plan.prefix.length;
const digits = String(1_000_000_000 + seed).slice(-remaining);
return `${plan.code}${plan.prefix}${digits}`;
}
async function loginRider(tenantSlug, countryPack, seed) {
const phone = testPhone(countryPack, seed);
// وضع OTP التطويري: رمز ثابت 1234 يُنشئ الحساب ويسجّل الدخول بنداء واحد
// (بلا حاجة لـ/auth/send-otp أولاً) — نفس نمط e2e-test.mjs.
const r = await api('POST', '/auth/verify-otp', { tenant: tenantSlug, body: { phone, code: CODE } });
if (!r.body?.access_token) {
console.error(` ❌ فشل تسجيل الدخول (${phone}): ${r.status} ${JSON.stringify(r.body)}`);
}
return r.body?.access_token;
}
async function quoteFor(tenantSlug, token, serviceClass, ride) {
const res = await api('POST', '/trips', {
tenant: tenantSlug,
token,
body: {
origin: ride.origin,
destination: ride.destination,
service_class: serviceClass,
payment_method: 'cash',
},
});
return res;
}
async function main() {
console.log(`\n=== تسعير التعرفة الافتراضية — API حقيقي (${BASE}) ===`);
console.log(`وقت التشغيل: ${new Date().toISOString()} (UTC) — يحدّد أي نافذة تعرفة ستُختار فعلياً.\n`);
for (const t of TENANTS) await ensureTenant(t);
const rows = [];
for (const t of TENANTS) {
console.log(`\n--- ${t.slug} (${t.countryPack} / ${t.currency}) ---`);
let seed = 500 + TENANTS.indexOf(t) * 100;
const token = await loginRider(t.slug, t.countryPack, seed++);
if (!token) {
console.error(`❌ تعذّر تسجيل دخول راكب اختبار لـ${t.slug} — تخطّي`);
continue;
}
for (const ride of RIDES) {
for (const serviceClass of SERVICE_CLASSES) {
const res = await quoteFor(t.slug, token, serviceClass, ride);
const trip = res.body?.trip;
if (!trip) {
console.error(` ❌ ${serviceClass} / ${ride.label}: ${res.status} ${JSON.stringify(res.body)}`);
continue;
}
rows.push({
tenant: t.slug,
country: t.countryPack,
currency: t.currency,
ride: ride.label,
serviceClass,
distanceKm: trip.distance_km,
durationMin: trip.duration_min,
fare: trip.quoted_fare,
tariffFound: trip.quoted_fare != null,
});
}
}
}
console.log('\n=== النتائج ===\n');
const noQuote = rows.filter((r) => !r.tariffFound);
if (noQuote.length) {
console.error(`❌ ${noQuote.length} طلب بلا سعر (تعرفة مفقودة) — هذا هو العطل الذي أصلحناه، فوجوده الآن خطأ حرج:`);
for (const r of noQuote) console.error(` ${r.tenant} / ${r.serviceClass} / ${r.ride}`);
} else {
console.log(`✅ كل الطلبات (${rows.length}) رجعت سعراً — لا رحلة مجانية.`);
}
for (const t of TENANTS) {
const forTenant = rows.filter((r) => r.tenant === t.slug);
if (!forTenant.length) continue;
console.log(`\n### ${t.slug} (${t.currency}) ###`);
for (const ride of RIDES) {
console.log(`\n ${ride.label}:`);
const forRide = forTenant.filter((r) => r.ride === ride.label);
if (forRide.length) {
console.log(` مسافة: ${forRide[0].distanceKm} كم · زمن: ${forRide[0].durationMin} دقيقة`);
}
for (const cls of SERVICE_CLASSES) {
const r = forRide.find((x) => x.serviceClass === cls);
console.log(` ${cls.padEnd(10)} ${r ? r.fare + ' ' + t.currency : '—'}`);
}
}
}
console.log(`\n=== التحقق: ترتيب الفئات لا ينعكس ===\n`);
let orderOk = true;
for (const t of TENANTS) {
for (const ride of RIDES) {
const forRide = rows.filter((r) => r.tenant === t.slug && r.ride === ride.label);
const economy = forRide.find((r) => r.serviceClass === 'economy')?.fare;
const van = forRide.find((r) => r.serviceClass === 'van')?.fare;
if (economy != null && van != null && van <= economy) {
orderOk = false;
console.error(`❌ ${t.slug} / ${ride.label}: الفان (${van}) ≤ الاقتصادي (${economy})`);
}
}
}
if (orderOk) console.log('✅ الفان أغلى من الاقتصادي في كل رحلة وكل بلد.');
console.log(failedExitCode(noQuote.length, orderOk));
}
function failedExitCode(missing, orderOk) {
const bad = missing > 0 || !orderOk;
process.exitCode = bad ? 1 : 0;
return bad ? '\n❌ فشل الاختبار.' : '\n✅ الاختبار ناجح.';
}
main().catch((e) => {
console.error('خطأ غير متوقع:', e);
process.exit(1);
});