/** * GROUP 1 · S5 — one expiry cascades into a second promotion. * * In the batch: A 10w + B 20w + C 23w = 53/53 * Waiting: D 8w, E 5w (priority order D before E) * * A never pays and EXPIRES → 10 wagons freed. ONE settle then serves BOTH * waiting bookings in the same pass: D is reserved whole (8w) and E, which no * longer fits in the 2 wagons left, is OFFERED a partial of exactly those 2. * That is the assertion the scenario exists for — fillFromWaitingList loops * until a pass places nothing, so a single-pass top-up would leave E untouched * until the next window cycle. * * D then expires too, freeing 8 more wagons — and E's offer is NOT resized. * FINDING (the same one bulk-b1 pins): a refill never supersedes an open * partial offer, so E pays its stale 2-wagon offer and ships 2 of its 5 wagons * while 8 sit idle. The train settles at B 20 + C 23 + E 2 = 45/53. * * Every expiry must also leave an audit trail — a terminal EXPIRED booking with * its invoice closed out, never a silent disappearance. An open invoice on an * expired seat is money the customer could still pay for a train they are no * longer on. */ 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, extendPayWindow, forceReservationExpiry, forceWindowOpen, livePayableInvoices, payViaGateway, pollAllocations, pollBookingStatus, pollCycleConcluded, pollPartialOffer, releaseUnpaidHolds, resetCorridorDay, scheduleRow, seedTenantContracts, setPriority, } from "./flows"; const DEPARTURE = departureAt(54); const BOOKING_DAY = eatDayStr(DEPARTURE); const STAMP = String(Date.now()); const SHAPES = { A: { twenty: 20, forty: 0, wagons: 10 }, B: { twenty: 40, forty: 0, wagons: 20 }, C: { twenty: 0, forty: 23, wagons: 23 }, D: { twenty: 16, forty: 0, wagons: 8 }, E: { twenty: 10, forty: 0, wagons: 5 }, } as const; const ORDER = ["A", "B", "C", "D", "E"] as const; const IN_BATCH = ["A", "B", "C"] as const; const WAITING = ["D", "E"] as const; /** What A's expiry leaves loose once D takes its 8 — E's offer is sized to it. */ const E_OFFER = SHAPES.A.wagons - SHAPES.D.wagons; // 2 const RIDING = SHAPES.B.wagons + SHAPES.C.wagons + E_OFFER; // 45 describe("g1 s5: expiry cascades into a second promotion", () => { 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 = 24_000; for (const [i, suffix] of ORDER.entries()) { 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; await setPriority(booking.get(suffix)!, i + 1); } }, 1_800_000); afterAll(closeDb); it("A+B+C fill the train; D and E queue behind them, each fitting the hole above", () => { for (const suffix of ORDER) { expect(containerWagons(SHAPES[suffix].twenty, SHAPES[suffix].forty), `${suffix} wagons`).toBe( SHAPES[suffix].wagons, ); } expect( IN_BATCH.reduce((sum, s) => sum + SHAPES[s].wagons, 0), "A+B+C fill the train exactly", ).toBe(G1_WAGONS); expect(SHAPES.D.wagons, "D fits inside A's 10").toBeLessThan(SHAPES.A.wagons); expect(SHAPES.E.wagons, "E fits inside D's 8").toBeLessThan(SHAPES.D.wagons); }); it("the batch reserves A, B and C; D and E wait in priority order", async () => { await closeBookingWindow(scheduleId); await completeDocReview(scheduleId); for (const suffix of IN_BATCH) { await pollBookingStatus(booking.get(suffix)!, ["SELECTED_FOR_BATCH", "AWAITING_PAYMENT"]); } for (const suffix of WAITING) { const row = await bookingRow(booking.get(suffix)!); expect(row.status, `${suffix} waitlisted`).toBe("FULLY_EXECUTED"); expect(row.train_schedule_id, `${suffix} holds no seat`).toBeNull(); } }); it("B and C pay; A expires and ONE pass serves both waiting bookings", async () => { await extendPayWindow(scheduleId, [booking.get("B")!, booking.get("C")!]); await payViaGateway(booking.get("B")!); await pollAllocations(booking.get("B")!, SHAPES.B.wagons); await payViaGateway(booking.get("C")!); await pollAllocations(booking.get("C")!, SHAPES.C.wagons); await forceReservationExpiry(booking.get("A")!); // D fits A's hole whole… await pollBookingStatus(booking.get("D")!, ["SELECTED_FOR_BATCH", "AWAITING_PAYMENT"], 40); // …and the same pass keeps going: E no longer fits in the 2 wagons left, so // it is offered exactly those rather than being left for the next cycle. const offer = await pollPartialOffer(booking.get("E")!); expect(Number(offer.offered_wagons), "E offered the 2 loose wagons").toBe(E_OFFER); }); it("D expires too — but E's stale 2-wagon offer is never resized", async () => { await forceReservationExpiry(booking.get("D")!); // FINDING (bulk-b1 pins the same gap): the refill re-selects a booking that // already holds an OFFER without re-sizing it, so the 8 wagons D just freed // stay unsold and E ships 2 of the 5 it asked for. Assert what the engine // really does, so a fix to the offer path fails loudly here. await pollBookingStatus(booking.get("E")!, ["SELECTED_FOR_BATCH", "AWAITING_PAYMENT"], 40); expect((await bookingRow(booking.get("E")!)).payment_deadline, "E got a pay window").toBeTruthy(); await extendPayWindow(scheduleId, [booking.get("E")!]); await payViaGateway(booking.get("E")!); await pollAllocations(booking.get("E")!, E_OFFER); expect((await bookingRow(booking.get("E")!)).is_split, "E rode split, 2 of 5").toBe(true); }); it("both expiries are terminal and auditable — nothing vanished silently", async () => { for (const suffix of ["A", "D"] as const) { const row = await bookingRow(booking.get(suffix)!); expect(row.status, `${suffix} terminal EXPIRED`).toBe("EXPIRED"); expect(row.train_schedule_id, `${suffix} holds no seat`).toBeNull(); expect( await livePayableInvoices(booking.get(suffix)!), `${suffix} has no live payable invoice`, ).toBe(0); } }); it("the train settles at 45/53 — eight wagons unsold, short of FULL", async () => { await endPaymentPhase(scheduleId); await pollCycleConcluded(scheduleId); expect(await allocatedWagons(scheduleId), "45 wagons allocated").toBe(RIDING); expect( (await scheduleRow(scheduleId)).booking_window_status, "window not FULL at 45/53", ).not.toBe("FULL"); }); });