feat: add drain tail to the payments

This commit is contained in:
Nathnael
2026-08-04 08:59:33 +00:00
parent a20498fd77
commit acd2cfbe8c
9 changed files with 379 additions and 22 deletions

View File

@@ -0,0 +1,67 @@
import {
DEFAULT_PAYMENT_DRAIN_MINUTES,
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);
}
});
});