import { DEFAULT_PAYMENT_DRAIN_MINUTES, paymentDrainEndsAtIso, paymentDrainMs, payWindowLapsed, } from "./booking-batch.constants"; /** * The drain tail is what keeps a pay window's LAST payment from being thrown * away: settlement is asynchronous (provider confirm → payment-api → outbox * relay), so a payment made in the window's final seconds lands after * `paymentDeadline`. Nothing may expire, no wagons may be resold and no window * cycle may conclude until the tail has passed. */ describe("payWindowLapsed — pay-window drain tail", () => { const deadline = new Date("2026-08-02T22:04:41Z"); const at = (offsetMs: number) => deadline.getTime() + offsetMs; const MIN = 60_000; afterEach(() => { delete process.env.FREIGHT_PAYMENT_DRAIN_MINUTES; }); it("is not lapsed before the deadline", () => { expect(payWindowLapsed(deadline, at(-1 * MIN))).toBe(false); }); it("is not lapsed inside the drain tail", () => { // The reproduced finding: settled ~7 minutes late. With the default 5-minute // tail the booking is still live at 4 minutes. expect(payWindowLapsed(deadline, at(4 * MIN))).toBe(false); expect(payWindowLapsed(deadline, at(DEFAULT_PAYMENT_DRAIN_MINUTES * MIN - 1))).toBe( false, ); }); it("is lapsed once the tail passes", () => { expect(payWindowLapsed(deadline, at(DEFAULT_PAYMENT_DRAIN_MINUTES * MIN))).toBe( true, ); expect(payWindowLapsed(deadline, at(7 * MIN))).toBe(true); }); it("never lapses without a deadline — callers decide what unknown means", () => { expect(payWindowLapsed(null, at(60 * MIN))).toBe(false); expect(payWindowLapsed(undefined, at(60 * MIN))).toBe(false); }); it("honours FREIGHT_PAYMENT_DRAIN_MINUTES", () => { process.env.FREIGHT_PAYMENT_DRAIN_MINUTES = "20"; expect(paymentDrainMs()).toBe(20 * MIN); expect(payWindowLapsed(deadline, at(10 * MIN))).toBe(false); expect(payWindowLapsed(deadline, at(20 * MIN))).toBe(true); }); it("allows an explicit zero drain (old deadline-is-the-line behaviour)", () => { process.env.FREIGHT_PAYMENT_DRAIN_MINUTES = "0"; expect(paymentDrainMs()).toBe(0); expect(payWindowLapsed(deadline, at(0))).toBe(true); }); it("falls back to the default on garbage or negative values", () => { for (const bad of ["", "abc", "-3"]) { process.env.FREIGHT_PAYMENT_DRAIN_MINUTES = bad; expect(paymentDrainMs()).toBe(DEFAULT_PAYMENT_DRAIN_MINUTES * MIN); } }); it("paymentDrainEndsAtIso reports deadline + drain, null/garbage-safe", () => { expect(paymentDrainEndsAtIso(deadline)).toBe( new Date(at(DEFAULT_PAYMENT_DRAIN_MINUTES * MIN)).toISOString(), ); expect(paymentDrainEndsAtIso(deadline.toISOString())).toBe( new Date(at(DEFAULT_PAYMENT_DRAIN_MINUTES * MIN)).toISOString(), ); expect(paymentDrainEndsAtIso(null)).toBeNull(); expect(paymentDrainEndsAtIso(undefined)).toBeNull(); expect(paymentDrainEndsAtIso("not-a-date")).toBeNull(); }); });