Files
edr-platform/apps/edr-freight-api/src/modules/train-scheduling/pay-window-drain.spec.ts
2026-08-06 23:48:06 +00:00

81 lines
3.0 KiB
TypeScript

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();
});
});