/** * Tunables for the demand-batching booking → allocation flow. * Times run in EAT so window boundaries match the local operating clock. * * Cadence and pay-window durations moved to the train_scheduling_global_rules * table (TrainSchedulingService.getWindowConfig) — the window engine * (BookingWindowService) drives all timing off that config. */ export const BATCH_TIMEZONE = "Africa/Addis_Ababa"; /** How long before the pay deadline the one reminder notification goes out. */ export const PAYMENT_REMINDER_LEAD_MS = 10 * 60_000; /** Drain tail applied to every pay window when FREIGHT_PAYMENT_DRAIN_MINUTES is unset. */ export const DEFAULT_PAYMENT_DRAIN_MINUTES = 7; /** * Drain tail on every pay window, in ms. Read per call so the env var can be * changed without a rebuild (and so tests can set it). */ export function paymentDrainMs(): number { // An empty/blank value is UNSET, not zero — a bare `FREIGHT_PAYMENT_DRAIN_MINUTES=` // left in a .env must not silently disable the drain (Number('') is 0). const raw = process.env.FREIGHT_PAYMENT_DRAIN_MINUTES?.trim(); const minutes = raw ? Number(raw) : NaN; return ( (Number.isFinite(minutes) && minutes >= 0 ? minutes : DEFAULT_PAYMENT_DRAIN_MINUTES) * 60_000 ); } /** * ISO timestamp of the end of a pay window's drain tail, for client display * (the "payment processing" countdown). Null in ⇒ null out. */ export function paymentDrainEndsAtIso( deadline: Date | string | null | undefined, ): string | null { if (deadline == null) return null; const ms = new Date(deadline).getTime(); if (!Number.isFinite(ms)) return null; return new Date(ms + paymentDrainMs()).toISOString(); } /** * A pay window AND its drain tail have closed. * * Settlement is asynchronous (provider confirm → payment-api → outbox relay), so * a payment made in the last seconds of the window lands after `paymentDeadline`. * The drain defers the WHOLE expiry pipeline — wagons stay held, the waiting list * is not promoted, the window cycle does not conclude — so that settlement still * has a live booking to land on. A payment that arrives even later is not lost * either: it settles the expired invoice and revives the booking (see * BillingService.settleByPaymentId + BookingInvoiceService.advanceBookingOnPayment). * * No deadline ⇒ never lapsed; callers decide what an unknown deadline means. */ export function payWindowLapsed( deadline: Date | null | undefined, now: number, drainMs: number = paymentDrainMs(), ): boolean { return deadline != null && deadline.getTime() + drainMs <= now; } /** Fallback wagons-per-booking when a booking has no computed `wagonsRequired`. */ export const DEFAULT_WAGONS_PER_BOOKING = 1; /** * Fallback per-wagon length (m) for the batch length budget when global rules don't yet * define maxTrainLength / maxWagons to derive it from. Used only to estimate train length * against the locomotive's max train length. */ export const DEFAULT_WAGON_LENGTH_METERS = 14; /** Default NW5 flat wagon length for container bookings (m). */ export const DEFAULT_CONTAINER_WAGON_LENGTH_METERS = 14; /** Default CW3 covered wagon length for bulk bookings (m). */ export const DEFAULT_BULK_WAGON_LENGTH_METERS = 14; /** * Fallback tare weights (T) matching the length fallbacks above. The locomotive * pull limit is a GROSS limit, so a booking's weight budget must include the * empty weight of every wagon it occupies — not just its cargo. */ export const DEFAULT_CONTAINER_WAGON_TARE_TONS = 22.4; /** Default CW3 gondola tare for bulk bookings (T). */ export const DEFAULT_BULK_WAGON_TARE_TONS = 23.4; /** * Fallback rated payloads (T) matching the tare fallbacks above. A bulk booking's * wagon count is its cargo divided by this, so a zero here would make the count * infinite — callers must floor it at a positive number. */ export const DEFAULT_CONTAINER_WAGON_CAPACITY_TONS = 70; /** Default CW3 gondola rated payload for bulk bookings (T). */ export const DEFAULT_BULK_WAGON_CAPACITY_TONS = 60;