Files
edr-platform/integration/src/g1-s3-underfill-day-open.it.ts
2026-08-03 10:51:43 +00:00

142 lines
4.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* GROUP 1 · S3 — an under-filled train keeps its day open.
*
* A 12×20ft = 6 wagons
* B 10×40ft = 10 wagons
* C 24×20ft = 12 wagons
* ─────────
* 28/53 → 25 slots still free
*
* Everyone pays, nobody splits, nobody waits. The assertion is the NEGATIVE
* one: the window must NOT be marked FULL, because the day has to stay on offer
* to customers who have not booked yet. A train that closed its day at 28/53
* would silently refuse 25 wagons of business — so the claim is checked the way
* a customer experiences it, through the portal's own day-availability query.
*/
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { closeDb, gateway } from "./client";
import {
G1_WAGONS,
allocatedWagons,
bookContainersReady,
bookingRow,
closeBookingWindow,
completeDocReview,
containerWagons,
createBuiltTrainSchedule,
dayAvailability,
departureAt,
eatDayStr,
endPaymentPhase,
ensureCorridorRoute,
expectNoPartialOffer,
extendPayWindow,
forceWindowOpen,
payViaGateway,
pollAllocations,
pollBookingStatus,
pollCycleConcluded,
releaseUnpaidHolds,
resetCorridorDay,
scheduleRow,
seedTenantContracts,
} from "./flows";
const DEPARTURE = departureAt(52);
const BOOKING_DAY = eatDayStr(DEPARTURE);
const STAMP = String(Date.now());
const SHAPES = {
A: { twenty: 12, forty: 0, wagons: 6 },
B: { twenty: 0, forty: 10, wagons: 10 },
C: { twenty: 24, forty: 0, wagons: 12 },
} as const;
const ORDER = ["A", "B", "C"] as const;
const BOOKED_WAGONS = ORDER.reduce((sum, s) => sum + SHAPES[s].wagons, 0); // 28
const FREE_WAGONS = G1_WAGONS - BOOKED_WAGONS; // 25
describe("g1 s3: an under-filled train keeps its day open", () => {
const booking = new Map<string, string>();
let scheduleId: string;
beforeAll(async () => {
await gateway.reset();
await releaseUnpaidHolds();
await ensureCorridorRoute();
await resetCorridorDay(DEPARTURE);
const contracts = await seedTenantContracts(
STAMP,
ORDER.map((suffix) => ({ suffix, freight: "CONTAINER" as const })),
);
scheduleId = (await createBuiltTrainSchedule({ departure: DEPARTURE })).id;
await forceWindowOpen(scheduleId, 60);
let isoSeed = 22_000;
for (const suffix of ORDER) {
const shape = SHAPES[suffix];
booking.set(
suffix,
await bookContainersReady({
contractId: contracts.get(suffix)!,
runStamp: STAMP,
isoSeed,
twenty: shape.twenty,
forty: shape.forty,
scheduledDate: BOOKING_DAY,
}),
);
isoSeed += shape.twenty + shape.forty;
}
}, 1_800_000);
afterAll(closeDb);
it("the three bookings leave 25 of the 53 slots free", () => {
for (const suffix of ORDER) {
expect(containerWagons(SHAPES[suffix].twenty, SHAPES[suffix].forty), `${suffix} wagons`).toBe(
SHAPES[suffix].wagons,
);
}
expect(BOOKED_WAGONS, "A+B+C = 28 wagons").toBe(28);
expect(FREE_WAGONS, "25 slots unused").toBe(25);
});
it("the batch reserves all three whole — with room to spare nobody is offered a split", async () => {
await closeBookingWindow(scheduleId);
await completeDocReview(scheduleId);
for (const suffix of ORDER) {
await pollBookingStatus(booking.get(suffix)!, ["SELECTED_FOR_BATCH", "AWAITING_PAYMENT"]);
await expectNoPartialOffer(booking.get(suffix)!, suffix);
}
});
it("all three pay — 28 of 53 wagons used and the window is NOT marked FULL", async () => {
await extendPayWindow(scheduleId, [...booking.values()]);
for (const suffix of ORDER) {
await payViaGateway(booking.get(suffix)!);
await pollAllocations(booking.get(suffix)!, SHAPES[suffix].wagons);
}
await endPaymentPhase(scheduleId);
await pollCycleConcluded(scheduleId);
expect(await allocatedWagons(scheduleId), "28 wagons allocated").toBe(BOOKED_WAGONS);
expect(
(await scheduleRow(scheduleId)).booking_window_status,
"window not FULL at 28/53",
).not.toBe("FULL");
for (const suffix of ORDER) {
expect((await bookingRow(booking.get(suffix)!)).status, `${suffix} rides`).toBe("PAID");
}
});
it("the day is still on offer to customers, with 25 free wagons", async () => {
// The customer-facing consequence, asked the way the portal asks it. A
// train that under-filled but stopped offering its day is the actual bug
// this scenario guards, and `freeWagons` is where it would show.
const day = await dayAvailability(booking.get("A")!, BOOKING_DAY);
expect(day.trainsForDay, "the day still runs a train").toBe(true);
expect(Number(day.freeWagons), "25 wagons still on offer").toBe(FREE_WAGONS);
});
});