/** * BULK IMPORT — the CW4 train fills from THREE wheat bookings; three more sit * in the waiting pool of the same window. The selected trio pays through the * gateway and allocates; when the cycle concludes FULL the waiting three * expire with the day — they never ride and never pay. * * 70 T per CW4 wagon, 54-wagon consist: * selected: BWA 1 400 T = 20 w, BWB 1 400 T = 20 w, BWC 980 T = 14 w → Σ 54 * waiting: BW1 / BW2 / BW3 700 T = 10 w each */ import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { closeDb, gateway } from "./client"; import { allocatedWagons, bookBulkReady, bookingRow, closeBookingWindow, completeDocReview, createSchedule, departureAt, eatDayStr, endPaymentPhase, extendPayWindow, ensureCorridorRoute, forceWindowOpen, payViaGateway, pollAllocations, pollBookingStatus, pollWindow, releaseUnpaidHolds, resetCorridorDay, seedTenantContracts, setPriority, } from "./flows"; const DEPARTURE = departureAt(21); const BOOKING_DAY = eatDayStr(DEPARTURE); const STAMP = String(Date.now()); const SELECTED = [ { suffix: "BWA", tons: 1400, wagons: 20 }, { suffix: "BWB", tons: 1400, wagons: 20 }, { suffix: "BWC", tons: 980, wagons: 14 }, ]; const WAITING = [ { suffix: "BW1", tons: 700, wagons: 10 }, { suffix: "BW2", tons: 700, wagons: 10 }, { suffix: "BW3", tons: 700, wagons: 10 }, ]; const ALL = [...SELECTED, ...WAITING]; describe("bulk import: three bookings fill the train, three wait and expire", () => { const booking = new Map(); let scheduleId: string; beforeAll(async () => { await gateway.reset(); await releaseUnpaidHolds(); await ensureCorridorRoute(); await resetCorridorDay(DEPARTURE); const contracts = await seedTenantContracts( STAMP, ALL.map((b) => ({ suffix: b.suffix, freight: "BULK" as const })), ); scheduleId = ( await createSchedule({ departure: DEPARTURE, kind: "bulk", locoPair: ["LOCO-IMP-17", "LOCO-IMP-18"], }) ).id; await forceWindowOpen(scheduleId, 45); for (const [i, b] of ALL.entries()) { const id = await bookBulkReady({ contractId: contracts.get(b.suffix)!, tons: b.tons, scheduledDate: BOOKING_DAY, }); booking.set(b.suffix, id); // Priority order = the order above: the exact-fill trio picks first. await setPriority(id, i + 1); } }, 1_800_000); afterAll(closeDb); it("the batch selects exactly the three that fill 54 wagons; the rest keep waiting", async () => { await closeBookingWindow(scheduleId); await completeDocReview(scheduleId); for (const b of SELECTED) { await pollBookingStatus(booking.get(b.suffix)!, ["SELECTED_FOR_BATCH", "AWAITING_PAYMENT"]); } for (const b of WAITING) { const row = await bookingRow(booking.get(b.suffix)!); expect(row.status, `${b.suffix} still waiting`).toBe("FULLY_EXECUTED"); expect(row.payment_deadline, `${b.suffix} has no pay deadline`).toBeNull(); } }); it("the three selected pay through the gateway and allocate — 54/54", async () => { await extendPayWindow(scheduleId, SELECTED.map((b) => booking.get(b.suffix)!)); for (const b of SELECTED) { await payViaGateway(booking.get(b.suffix)!); await pollAllocations(booking.get(b.suffix)!, b.wagons); } expect(await allocatedWagons(scheduleId), "54 wagons allocated").toBe(54); }); it("the cycle concludes FULL — the three waiting bookings expire with the day", async () => { await endPaymentPhase(scheduleId); await pollWindow( scheduleId, (s) => s.booking_window_status === "FULL" && s.window_phase === "DONE", "FULL + DONE", ); for (const b of WAITING) await pollBookingStatus(booking.get(b.suffix)!, "EXPIRED", 40); for (const b of SELECTED) { const row = await bookingRow(booking.get(b.suffix)!); expect(row.status, `${b.suffix} stays PAID`).toBe("PAID"); } }); });