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