mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 15:18:11 +00:00
143 lines
4.7 KiB
TypeScript
143 lines
4.7 KiB
TypeScript
/**
|
||
* GROUP 1 · S2 — four bookings pay and fill the train to the slot.
|
||
*
|
||
* A 3×40ft = 3 wagons · 3 containers
|
||
* B 20×20ft + 10×40ft = 20 wagons · 30 containers
|
||
* C 25×40ft = 25 wagons · 25 containers
|
||
* D 10×20ft = 5 wagons · 10 containers
|
||
* ─────────
|
||
* 53/53 → FULL
|
||
*
|
||
* Everyone is selected, everyone pays, nothing splits and nobody waits. What
|
||
* this really guards is the ALLOCATION rather than the arithmetic: the 53
|
||
* wagons carry 68 containers and every one must land on exactly one slot with
|
||
* its number on it. A booking that took wagons but never mapped its units would
|
||
* still read 53/53 on the board — hence the per-container assertion at the end.
|
||
*/
|
||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||
import { closeDb, gateway } from "./client";
|
||
import {
|
||
G1_WAGONS,
|
||
allocatedWagons,
|
||
bookContainersReady,
|
||
bookingRow,
|
||
closeBookingWindow,
|
||
completeDocReview,
|
||
containerWagons,
|
||
createBuiltTrainSchedule,
|
||
departureAt,
|
||
eatDayStr,
|
||
endPaymentPhase,
|
||
ensureCorridorRoute,
|
||
expectContainersPlaced,
|
||
expectNoPartialOffer,
|
||
extendPayWindow,
|
||
forceWindowOpen,
|
||
linkedBookings,
|
||
payViaGateway,
|
||
pollAllocations,
|
||
pollBookingStatus,
|
||
pollWindow,
|
||
releaseUnpaidHolds,
|
||
resetCorridorDay,
|
||
seedTenantContracts,
|
||
} from "./flows";
|
||
|
||
const DEPARTURE = departureAt(51);
|
||
const BOOKING_DAY = eatDayStr(DEPARTURE);
|
||
const STAMP = String(Date.now());
|
||
|
||
const SHAPES = {
|
||
A: { twenty: 0, forty: 3, wagons: 3, containers: 3 },
|
||
B: { twenty: 20, forty: 10, wagons: 20, containers: 30 },
|
||
C: { twenty: 0, forty: 25, wagons: 25, containers: 25 },
|
||
D: { twenty: 10, forty: 0, wagons: 5, containers: 10 },
|
||
} as const;
|
||
const ORDER = ["A", "B", "C", "D"] as const;
|
||
const TOTAL_CONTAINERS = ORDER.reduce((sum, s) => sum + SHAPES[s].containers, 0); // 68
|
||
|
||
describe("g1 s2: four bookings pay and fill the train exactly", () => {
|
||
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 = 21_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.containers;
|
||
}
|
||
}, 1_800_000);
|
||
|
||
afterAll(closeDb);
|
||
|
||
it("the four bookings add up to exactly one trainload", () => {
|
||
for (const suffix of ORDER) {
|
||
expect(containerWagons(SHAPES[suffix].twenty, SHAPES[suffix].forty), `${suffix} wagons`).toBe(
|
||
SHAPES[suffix].wagons,
|
||
);
|
||
}
|
||
expect(
|
||
ORDER.reduce((sum, s) => sum + SHAPES[s].wagons, 0),
|
||
"A+B+C+D fill the train exactly",
|
||
).toBe(G1_WAGONS);
|
||
});
|
||
|
||
it("the batch reserves all four whole — an exact fit offers nobody 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 four pay through the gateway and are allocated onto the train", async () => {
|
||
await extendPayWindow(scheduleId, [...booking.values()]);
|
||
for (const suffix of ORDER) {
|
||
await payViaGateway(booking.get(suffix)!);
|
||
await pollAllocations(booking.get(suffix)!, SHAPES[suffix].wagons);
|
||
}
|
||
});
|
||
|
||
it("the train is FULL at 53/53 and every one of the 68 containers has a slot", async () => {
|
||
await endPaymentPhase(scheduleId);
|
||
await pollWindow(
|
||
scheduleId,
|
||
(s) => s.booking_window_status === "FULL" && s.window_phase === "DONE",
|
||
"FULL + DONE",
|
||
);
|
||
expect(await allocatedWagons(scheduleId), "53 wagons allocated").toBe(G1_WAGONS);
|
||
expect(await linkedBookings(scheduleId), "4 bookings linked").toBe(4);
|
||
for (const suffix of ORDER) {
|
||
expect((await bookingRow(booking.get(suffix)!)).status, `${suffix} rides`).toBe("PAID");
|
||
}
|
||
|
||
// Allocation is per CONTAINER, not just per wagon: 53 filled slots with
|
||
// only some units mapped would still read as a full train.
|
||
await expectContainersPlaced(scheduleId, TOTAL_CONTAINERS);
|
||
});
|
||
});
|