import { TariffDefinition } from './entities/tariff.entity'; /** * تعرفة الانطلاق الافتراضية لكل بلد — **مصدر الحقيقة الوحيد**. * * يستهلكها اثنان: `SeedService` (فيأخذها كل مستأجر جديد تلقائياً) ومولّد * الـSQL (`scripts/generate-tariff-sql.ts`) لقواعد قائمة. نسختان من هذه * الأرقام كانتا ستتباعدان بصمت، والتسعير أسوأ مكان يقع فيه هذا. * * هذه تعرفة **بداية** فقط؛ يُفترض أن يحدّثها كرون استخبارات الأسعار بنسخة * أعلى (`version + 1`) لا أن يكتب فوقها. * * قرارات تحكم الملف (وسبب كلٍّ منها): * * 1. **`city = 'default'`** لا اسم البلد — كل الكود يبحث بهذه القيمة * (`trips.service`: `dto.city ?? 'default'`). النسخة السابقة كتبت * `'jordan'/'egypt'/'syria'` فما طابقت شيئاً: لا تعرفة ← الراكب بلا * سعر ← `settleFare` تأخذ `quoted_fare ?? 0` ← **رحلة مجانية وعمولة * صفر**، بسطر تحذير واحد في اللوج. البلد من `country_pack` للمستأجر. * * 2. **النوافذ بالتوقيت المحلي** عبر `timezone` — المحرّك كان يقرأ بـUTC، * فذروة 16:00–20:00 تشتغل 19:00–23:00 في عمّان. * * 3. **فتحة العدّاد (`flag`) حقيقية** لا صفر — الفتحة تغطي تكلفة بدء * الرحلة، وإسقاطها يجعل الرحلة القصيرة خاسرة على السائق. * * 4. **الحدّ الأدنى بقرار المالك**: 1.10 دينار · 20 جنيهاً · 150 ليرة. * النسخة السابقة اشتقّته `perKm × 2` فأنتجت 0.368 ديناراً — ثلث الحقيقي. * * 5. **الفئات مشتقّة بمعاملات** لا أرقاماً مستقلّة — الأرقام الخام السابقة * جعلت الفان أرخص من الاقتصادي في مصر وسوريا (22 مقابل 29.5 جنيهاً)، * أي أن كل راكب عاقل يطلب أغلى فئة على المشغّل. */ export const SERVICE_CLASSES = { ECONOMY: 'economy', COMFORT: 'comfort', LADY: 'lady', ELECTRIC: 'electric', VAN: 'van', DELIVERY: 'delivery', VIP: 'vip', FIXED: 'fixed', SAVER: 'saver', } as const; export type ServiceClass = (typeof SERVICE_CLASSES)[keyof typeof SERVICE_CLASSES]; /** * معاملات الفئات نسبةً إلى الاقتصادي. الترتيب هنا **هو** منطق المنتج: * أوفر < اقتصادي < كهربائي < ليدي < مريح < فان < VIP. */ export const CLASS_MULTIPLIERS: Record = { [SERVICE_CLASSES.SAVER]: 0.85, [SERVICE_CLASSES.DELIVERY]: 0.9, [SERVICE_CLASSES.ECONOMY]: 1.0, [SERVICE_CLASSES.FIXED]: 1.0, [SERVICE_CLASSES.ELECTRIC]: 1.05, [SERVICE_CLASSES.LADY]: 1.1, [SERVICE_CLASSES.COMFORT]: 1.25, [SERVICE_CLASSES.VAN]: 1.45, [SERVICE_CLASSES.VIP]: 1.75, }; /** * الأساس الاقتصادي بالدينار — مأخوذ من التعرفة العاملة في `SeedService` * لا من أرقام النظام القديم. */ const JOD_BASE = { flag: 0.39, perKm: 0.28, perMin: 0.06, waitingPerMin: 0.48, bookingFee: 0.25, }; export interface CountryTariff { countryPack: string; currency: string; timezone: string; minFare: number; /** معامل التحويل من الدينار — مشتقّ من الحدّ الأدنى لا من سعر صرف يومي. */ scale: number; commissionPercent: number; rounding: { increment: number; mode: 'nearest' | 'up' | 'down' }; /** سعر الخط الثابت الافتراضي (فئة `fixed`) — يضبطه المشغّل لكل خط. */ fixedFare: number; } /** * `scale` مشتقّ من الحدّ الأدنى الذي أقرّه المالك (20 ÷ 1.10 لمصر، * 150 ÷ 1.10 لسوريا): مرساة واحدة تعني أن كل الأرقام تتحرك معاً حين * يتغير الحدّ الأدنى، بدل جدولين ينحرف أحدهما عن الآخر. */ export const COUNTRY_TARIFFS: CountryTariff[] = [ { countryPack: 'jo', currency: 'JOD', timezone: 'Asia/Amman', minFare: 1.1, scale: 1, commissionPercent: 14.0, rounding: { increment: 0.05, mode: 'nearest' }, fixedFare: 2.5, }, { countryPack: 'eg', currency: 'EGP', timezone: 'Africa/Cairo', minFare: 20, scale: 20 / 1.1, commissionPercent: 12.0, rounding: { increment: 1, mode: 'nearest' }, fixedFare: 45, }, { countryPack: 'sy', currency: 'SYP', timezone: 'Asia/Damascus', minFare: 150, scale: 150 / 1.1, commissionPercent: 11.0, rounding: { increment: 5, mode: 'nearest' }, fixedFare: 350, }, ]; /** النوافذ الزمنية — بالتوقيت المحلي، وتغطي اليوم كاملاً بلا ثغرة. */ export const WINDOWS = [ { name: 'morning_peak', from: '07:00', to: '09:30', multiplier: 1.25 }, { name: 'normal_day', from: '09:30', to: '16:00', multiplier: 1.0 }, { name: 'evening_peak', from: '16:00', to: '19:30', multiplier: 1.25 }, { name: 'normal_evening', from: '19:30', to: '23:00', multiplier: 1.0 }, { name: 'late_night', from: '23:00', to: '07:00', multiplier: 1.15 }, ]; /** تدوير لخانة معقولة بحجم العملة — 3 كسور بالدينار، صحيح بالليرة. */ function money(value: number, currency: string): number { const decimals = currency === 'JOD' ? 3 : currency === 'EGP' ? 2 : 0; return Number(value.toFixed(decimals)); } export function countryTariff(countryPack: string): CountryTariff { return ( COUNTRY_TARIFFS.find((c) => c.countryPack === countryPack) ?? COUNTRY_TARIFFS[0] // الأردن افتراضاً — نفس افتراض `country_pack` في الكيان ); } /** يبني تعريف تعرفة كامل لـ(بلد × فئة خدمة). */ export function buildDefinition( country: CountryTariff, serviceClass: string, ): TariffDefinition { const classMultiplier = CLASS_MULTIPLIERS[serviceClass] ?? 1; const rate = (base: number, windowMultiplier: number) => money(base * country.scale * classMultiplier * windowMultiplier, country.currency); const isFixed = serviceClass === SERVICE_CLASSES.FIXED; const definition: TariffDefinition = { currency: country.currency, // المنطقة الزمنية شرط صحة النوافذ — بدونها يقرأ المحرّك بـUTC. timezone: country.timezone, mode: isFixed ? 'fixed_quote' : 'time_and_distance', rounding: country.rounding, windows: WINDOWS.map((w) => ({ name: w.name, from: w.from, to: w.to, flag: rate(JOD_BASE.flag, w.multiplier), per_km: rate(JOD_BASE.perKm, w.multiplier), per_min: rate(JOD_BASE.perMin, w.multiplier), // الانتظار لا يتبع معامل الفئة: دقيقة انتظار السائق واحدة مهما كانت // فئة السيارة — الفرق في السيارة لا في الوقت. per_min_waiting: money( JOD_BASE.waitingPerMin * country.scale * w.multiplier, country.currency, ), })), booking_fee: money(JOD_BASE.bookingFee * country.scale, country.currency), min_fare: country.minFare, free_waiting_min: 5, surge: { enabled: false }, commission: { percent: country.commissionPercent }, }; if (isFixed) { definition.fixed_fare = country.fixedFare; // السعر الثابت لا حدّ أدنى له ولا رسم حجز: المعلَن هو المتفَق عليه، // ورفعه بحدّ أدنى يعني إعلان سعر وتحصيل غيره. definition.min_fare = 0; definition.booking_fee = 0; } return definition; }