174 lines
6.8 KiB
TypeScript
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);
|
|
});
|
|
});
|