mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 17:38:12 +00:00
142 lines
4.7 KiB
TypeScript
142 lines
4.7 KiB
TypeScript
/**
|
||
* 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);
|
||
});
|
||
});
|