Files
tripz-llc/backend/src/modules/tariff/default-tariffs.ts
T

198 lines
7.8 KiB
TypeScript
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.
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<string, number> = {
[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;
}