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