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); }); });