P1: drivers + trips(★state machine) + matching(Redis GEO) + tariff engine + maps + realtime + seed
- drivers: apply/approve/status/location (+Redis GEO presence) - trips: request→accept→status transitions + trip_events audit + offers via socket - matching: Redis GEO nearby drivers - tariff: JSON-rule engine (time_or_distance/window/rounding) + quote endpoint - maps: straight-line routing seam (انطلق later) - realtime: Socket.IO gateway (JWT auth, tenant rooms) - seed: demo tenant siro + default tariff on boot - migration InitP1 (drivers/tariffs/trips/trip_events) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
import { TariffDefinition, TariffWindow } from './entities/tariff.entity';
|
||||
|
||||
export interface QuoteInput {
|
||||
distanceKm: number;
|
||||
durationMin: number;
|
||||
waitingMin?: number;
|
||||
at?: Date; // لحظة الحساب (لاختيار النافذة الزمنية)
|
||||
}
|
||||
|
||||
export interface QuoteBreakdown {
|
||||
window: string;
|
||||
flag: number;
|
||||
distance: number;
|
||||
time: number;
|
||||
waiting: number;
|
||||
bookingFee: number;
|
||||
subtotal: number;
|
||||
surgeMultiplier: number;
|
||||
total: number;
|
||||
currency: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* محرك التعرفة النقي (قابل للاختبار بوحدات) — نفس المدخل = نفس المخرج.
|
||||
* يدعم أوضاع docs/04: time_and_distance و time_or_distance (عتبة سرعة).
|
||||
*/
|
||||
export class TariffEngine {
|
||||
static quote(def: TariffDefinition, input: QuoteInput): QuoteBreakdown {
|
||||
const at = input.at ?? new Date();
|
||||
const win = TariffEngine.pickWindow(def.windows, at);
|
||||
const distanceKm = Math.max(0, input.distanceKm);
|
||||
const durationMin = Math.max(0, input.durationMin);
|
||||
const waitingMin = Math.max(0, input.waitingMin ?? 0);
|
||||
|
||||
let distanceCharge = 0;
|
||||
let timeCharge = 0;
|
||||
|
||||
if (def.mode === 'time_or_distance') {
|
||||
// العداد المنظَّم: تحت العتبة يُحسب بالدقيقة (زحمة)، فوقها بالكيلومتر.
|
||||
const avgSpeed = durationMin > 0 ? (distanceKm / durationMin) * 60 : 999;
|
||||
const threshold = def.speed_threshold_kmh ?? 18;
|
||||
if (avgSpeed < threshold) {
|
||||
timeCharge = durationMin * win.per_min;
|
||||
} else {
|
||||
distanceCharge = distanceKm * win.per_km;
|
||||
}
|
||||
} else {
|
||||
// time_and_distance (و fixed_quote يُحسب مرة ويثبّت)
|
||||
distanceCharge = distanceKm * win.per_km;
|
||||
timeCharge = durationMin * win.per_min;
|
||||
}
|
||||
|
||||
const waitingCharge = waitingMin * (win.per_min_waiting ?? 0);
|
||||
const bookingFee = def.booking_fee ?? 0;
|
||||
|
||||
let subtotal =
|
||||
win.flag + distanceCharge + timeCharge + waitingCharge + bookingFee;
|
||||
|
||||
const surgeMultiplier =
|
||||
def.surge?.enabled && def.surge.multiplier ? def.surge.multiplier : 1;
|
||||
let total = subtotal * surgeMultiplier;
|
||||
|
||||
if (def.min_fare && total < def.min_fare) total = def.min_fare;
|
||||
total = TariffEngine.round(total, def.rounding);
|
||||
|
||||
return {
|
||||
window: win.name,
|
||||
flag: win.flag,
|
||||
distance: TariffEngine.n(distanceCharge),
|
||||
time: TariffEngine.n(timeCharge),
|
||||
waiting: TariffEngine.n(waitingCharge),
|
||||
bookingFee,
|
||||
subtotal: TariffEngine.n(subtotal),
|
||||
surgeMultiplier,
|
||||
total: TariffEngine.n(total),
|
||||
currency: def.currency,
|
||||
};
|
||||
}
|
||||
|
||||
private static pickWindow(windows: TariffWindow[], at: Date): TariffWindow {
|
||||
const mins = at.getUTCHours() * 60 + at.getUTCMinutes();
|
||||
for (const w of windows) {
|
||||
const [fromH, fromM] = w.from.split(':').map(Number);
|
||||
const [toH, toM] = w.to.split(':').map(Number);
|
||||
const from = fromH * 60 + fromM;
|
||||
const to = toH * 60 + toM;
|
||||
// نافذة تعبر منتصف الليل (مثل 22:00 → 06:00)
|
||||
if (from <= to ? mins >= from && mins < to : mins >= from || mins < to) {
|
||||
return w;
|
||||
}
|
||||
}
|
||||
return windows[0];
|
||||
}
|
||||
|
||||
private static round(
|
||||
value: number,
|
||||
rounding?: { increment: number; mode?: 'nearest' | 'up' | 'down' },
|
||||
): number {
|
||||
if (!rounding || !rounding.increment) return TariffEngine.n(value);
|
||||
const q = value / rounding.increment;
|
||||
const r =
|
||||
rounding.mode === 'up'
|
||||
? Math.ceil(q)
|
||||
: rounding.mode === 'down'
|
||||
? Math.floor(q)
|
||||
: Math.round(q);
|
||||
return TariffEngine.n(r * rounding.increment);
|
||||
}
|
||||
|
||||
private static n(v: number): number {
|
||||
return Number(v.toFixed(3));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user