Files
tripz-llc/backend/src/modules/tariff/tariff-seed.spec.ts
T

174 lines
6.8 KiB
TypeScript

import { TariffEngine } from './tariff.engine';
import { TariffDefinition } from './entities/tariff.entity';
// نستورد مصدر الحقيقة نفسه — الذي يزرعه التطبيق ويولّد منه الـSQL — لا
// نسخة موازية منه تتباعد عنه بصمت.
import {
buildDefinition,
COUNTRY_TARIFFS as countries,
SERVICE_CLASSES as ServiceClasses,
WINDOWS,
countryTariff as byPack,
} from './default-tariffs';
const def = (pack: string, cls: string): TariffDefinition =>
buildDefinition(byPack(pack), cls);
/** ظهيرة يوم عادي في كل منطقة — نافذة `normal_day`. */
const NOON_AMMAN = new Date('2026-07-19T09:00:00Z'); // 12:00 بتوقيت عمّان (+3)
describe('تعرفة الانطلاق — الحدود الدنيا بقرار المالك', () => {
it('الحدّ الأدنى: 1.10 دينار · 20 جنيهاً · 150 ليرة', () => {
expect(def('jo', ServiceClasses.ECONOMY).min_fare).toBe(1.1);
expect(def('eg', ServiceClasses.ECONOMY).min_fare).toBe(20);
expect(def('sy', ServiceClasses.ECONOMY).min_fare).toBe(150);
});
it('رحلة قصيرة جداً تُرفع للحدّ الأدنى لا تُحاسَب بالعدّاد', () => {
for (const [pack, floor] of [['jo', 1.1], ['eg', 20], ['sy', 150]] as const) {
const q = TariffEngine.quote(def(pack, ServiceClasses.ECONOMY), {
distanceKm: 0.3,
durationMin: 2,
at: NOON_AMMAN,
});
expect(q.total).toBeGreaterThanOrEqual(floor);
}
});
it('فتحة العدّاد موجودة في كل نافذة ولكل بلد — لا صفر', () => {
for (const country of countries) {
for (const w of def(country.countryPack, ServiceClasses.ECONOMY).windows) {
// الفتحة صفراً كانت تجعل الرحلة القصيرة خاسرة على السائق.
expect(w.flag).toBeGreaterThan(0);
}
}
});
});
describe('تعرفة الانطلاق — ترتيب الفئات', () => {
// الخلل الذي كسر النسخة السابقة: الفان أرخص من الاقتصادي في مصر وسوريا،
// فيصير الخيار العقلاني لكل راكب هو أغلى فئة على المشغّل.
const ordered = [
ServiceClasses.SAVER,
ServiceClasses.ECONOMY,
ServiceClasses.ELECTRIC,
ServiceClasses.LADY,
ServiceClasses.COMFORT,
ServiceClasses.VAN,
ServiceClasses.VIP,
];
for (const country of countries) {
it(`${country.countryPack}: السعر يتصاعد مع الفئة بلا انعكاس`, () => {
const fares = ordered.map(
(cls) =>
TariffEngine.quote(def(country.countryPack, cls), {
distanceKm: 5,
durationMin: 15,
at: NOON_AMMAN,
}).total,
);
for (let i = 1; i < fares.length; i++) {
expect(fares[i]).toBeGreaterThan(fares[i - 1]);
}
});
}
});
describe('النوافذ الزمنية — بالتوقيت المحلي لا UTC', () => {
it('تغطي اليوم كاملاً بلا ثغرة ولا تداخل', () => {
const toMin = (s: string) => Number(s.slice(0, 2)) * 60 + Number(s.slice(3));
for (let i = 0; i < WINDOWS.length; i++) {
const next = WINDOWS[(i + 1) % WINDOWS.length];
// نهاية كل نافذة = بداية التالية بالضبط؛ أي فجوة تعني دقائق بلا سعر.
expect(toMin(WINDOWS[i].to)).toBe(toMin(next.from));
}
});
it('**الذروة المسائية تقع مساءً في عمّان لا بعد منتصف الليل**', () => {
const d = def('jo', ServiceClasses.ECONOMY);
// 17:00 بتوقيت عمّان = 14:00 UTC. بالحساب القديم (UTC) كانت الساعة
// تُقرأ 14:00 فتقع في `normal_day` — أي ذروة لا تُحصَّل أبداً.
const q = TariffEngine.quote(d, {
distanceKm: 5,
durationMin: 15,
at: new Date('2026-07-19T14:00:00Z'),
});
expect(q.window).toBe('evening_peak');
});
it('الظهيرة نافذة عادية، والفجر نافذة ليلية — في كل بلد بمنطقته', () => {
expect(
TariffEngine.quote(def('jo', ServiceClasses.ECONOMY), {
distanceKm: 1,
durationMin: 1,
at: new Date('2026-07-19T09:00:00Z'), // 12:00 عمّان
}).window,
).toBe('normal_day');
expect(
TariffEngine.quote(def('eg', ServiceClasses.ECONOMY), {
distanceKm: 1,
durationMin: 1,
at: new Date('2026-07-19T00:30:00Z'), // 02:30 القاهرة
}).window,
).toBe('late_night');
});
it('الذروة أغلى من النافذة العادية لنفس الرحلة', () => {
const d = def('jo', ServiceClasses.ECONOMY);
const normal = TariffEngine.quote(d, {
distanceKm: 8,
durationMin: 20,
at: new Date('2026-07-19T09:00:00Z'),
}).total;
const peak = TariffEngine.quote(d, {
distanceKm: 8,
durationMin: 20,
at: new Date('2026-07-19T14:00:00Z'),
}).total;
expect(peak).toBeGreaterThan(normal);
});
});
describe('السعر الثابت (fixed_quote)', () => {
it('**لا يتغير بالمسافة ولا بالزمن** — هذا معنى «ثابت»', () => {
const d = def('eg', ServiceClasses.FIXED);
const short = TariffEngine.quote(d, { distanceKm: 2, durationMin: 5, at: NOON_AMMAN });
const long = TariffEngine.quote(d, { distanceKm: 40, durationMin: 90, at: NOON_AMMAN });
// سابقاً كان `fixed_quote` يسقط للفرع المتري فيُحسب بالعدّاد رغم اسمه.
expect(short.total).toBe(long.total);
expect(short.total).toBe(byPack('eg').fixedFare);
});
it('الانتظار وحده يُضاف — وقت السائق تكلفة حقيقية مهما ثبت الخط', () => {
const d = def('eg', ServiceClasses.FIXED);
const withWait = TariffEngine.quote(d, {
distanceKm: 10,
durationMin: 20,
waitingMin: 10,
at: NOON_AMMAN,
});
expect(withWait.total).toBeGreaterThan(byPack('eg').fixedFare);
expect(withWait.distance).toBe(0);
expect(withWait.time).toBe(0);
});
it('لا حدّ أدنى يرفع السعر المعلن — المعلَن هو المتفَق عليه', () => {
const d = def('sy', ServiceClasses.FIXED);
expect(d.min_fare).toBe(0);
expect(
TariffEngine.quote(d, { distanceKm: 0.5, durationMin: 1, at: NOON_AMMAN }).total,
).toBe(byPack('sy').fixedFare);
});
});
describe('العمولة', () => {
it('تُحسب بنسبة البلد على أجرة الراكب', () => {
const d = def('jo', ServiceClasses.ECONOMY);
const q = TariffEngine.quote(d, { distanceKm: 10, durationMin: 25, at: NOON_AMMAN });
const c = TariffEngine.commission(d, q.total);
expect(c.rate).toBe(14);
expect(c.amount).toBeCloseTo(q.total * 0.14, 2);
});
});