198 lines
7.8 KiB
TypeScript
198 lines
7.8 KiB
TypeScript
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;
|
||
}
|