/** * BULK IMPORT edge matrix: * a) a wheat booking on a day with no open window is rejected at submission * b) a sub-corridor booking (NAGAD → MOJO) rides the through-train next to a * DJIB_PORT → KALITY booking — the batch is corridor-aware * c) bulk intercity ride-along (MOJO → KALITY, DOMESTIC, dateless): staff * assign it onto the import train's free leg, the pay window opens, it * pays and links * d) whole-train giant: 4 000 T (58 wagons' worth) alone on a 54-wagon train * → partial offer of the FULL consist (3 780 T); the gateway settlement * applies the split and the train is FULL from ONE booking; the 220 T * outstanding must be rebooked EXACTLY on a later train */ import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { closeDb, db, gateway, poll } from "./client"; import { acceptIntercityOnto, acceptOperation, bookBulk, bookBulkReady, expectDayRefused, bookingFor, bookingRow, clearIntercityBooking, closeBookingWindow, completeDocReview, createSchedule, departureAt, eatDayStr, endPaymentPhase, ensureCorridorRoute, extendPayWindow, forceWindowOpen, payViaGateway, pollAllocations, pollBookingStatus, pollPartialOffer, pollWindow, releaseUnpaidHolds, resetCorridorDay, seedTenantContracts, } from "./flows"; const DEPARTURE = departureAt(25); const BOOKING_DAY = eatDayStr(DEPARTURE); const GIANT_DEPARTURE = departureAt(26); const GIANT_DAY = eatDayStr(GIANT_DEPARTURE); const REMAINDER_DEPARTURE = departureAt(27); const REMAINDER_DAY = eatDayStr(REMAINDER_DEPARTURE); const NO_WINDOW_DAY = eatDayStr(departureAt(29)); // no schedule exists there const STAMP = String(Date.now()); describe("bulk import matrix: gates, sub-corridor, ride-along, whole-train giant", () => { let contracts: Map; let scheduleId: string; beforeAll(async () => { await gateway.reset(); await releaseUnpaidHolds(); await ensureCorridorRoute(); await resetCorridorDay(DEPARTURE); await resetCorridorDay(GIANT_DEPARTURE); await resetCorridorDay(REMAINDER_DEPARTURE); contracts = await seedTenantContracts(STAMP, [ { suffix: "BM1", freight: "BULK" }, { suffix: "BMNW", freight: "BULK" }, { suffix: "BMSUB", freight: "BULK", originCode: "NAGAD", destCode: "MOJO" }, { suffix: "BMIC", freight: "BULK", direction: "DOMESTIC", originCode: "MOJO", destCode: "KALITY" }, { suffix: "BMG", freight: "BULK" }, ]); scheduleId = ( await createSchedule({ departure: DEPARTURE, kind: "bulk", locoPair: ["LOCO-IMP-25", "LOCO-IMP-26"], }) ).id; await forceWindowOpen(scheduleId, 60); }, 1_800_000); afterAll(closeDb); it("refuses a shipment day with no open window", async () => { const res = await expectDayRefused({ contractId: contracts.get("BMNW")!, tons: 140, scheduledDate: NO_WINDOW_DAY, }); expect(res.status).toBeGreaterThanOrEqual(400); // Wording differs by gate: the day probe answers "No departures available // on the selected day", the route probe "the import booking window …". expect(JSON.stringify(res.body)).toMatch(/booking window|no departures/i); }); it("a through booking and a NAGAD→MOJO sub-corridor booking share the train", async () => { const bm1 = await bookBulkReady({ contractId: contracts.get("BM1")!, tons: 1400, scheduledDate: BOOKING_DAY, }); const sub = await bookBulkReady({ contractId: contracts.get("BMSUB")!, tons: 700, scheduledDate: BOOKING_DAY, }); await closeBookingWindow(scheduleId); await completeDocReview(scheduleId); for (const id of [bm1, sub]) { await pollBookingStatus(id, ["SELECTED_FOR_BATCH", "AWAITING_PAYMENT"]); } await extendPayWindow(scheduleId, [bm1, sub]); await payViaGateway(bm1); await pollAllocations(bm1, 20); await payViaGateway(sub); await pollAllocations(sub, 10); expect((await bookingRow(bm1)).train_schedule_id).toBe(scheduleId); expect((await bookingRow(sub)).train_schedule_id).toBe(scheduleId); }); it("a dateless DOMESTIC ride-along boards the import train's free leg", async () => { // 140 T = 2 wagons MOJO → KALITY, no shipment day of its own. const res = await bookBulk({ contractId: contracts.get("BMIC")!, tons: 140 }); expect(res.status, JSON.stringify(res.body)).toBeLessThanOrEqual(201); const ic = (await bookingFor(contracts.get("BMIC")!)).id; await clearIntercityBooking(ic); await acceptIntercityOnto(scheduleId, ic); await pollBookingStatus(ic, ["SELECTED_FOR_BATCH", "AWAITING_PAYMENT"]); expect((await bookingRow(ic)).payment_deadline, "ride-along pay window opened").toBeTruthy(); await payViaGateway(ic); // Settlement UNPINS a paid ride-along back to the ride-along pool — staff // then place it on the train they want (acceptIntercity's `paid` branch, // booking-batch.service.ts). The Cypress twin never sees this: its staff // mark-paid shortcut leaves the reservation pinned. expect( (await bookingRow(ic)).train_schedule_id, "paid ride-along returns to the pool", ).toBeNull(); await acceptIntercityOnto(scheduleId, ic); await poll( "ride-along linked to the import train", `SELECT train_schedule_id FROM freight.bookings WHERE id = $1`, [ic], (row) => (row as { train_schedule_id?: string })?.train_schedule_id === scheduleId, { attempts: 20 }, ); const [link] = await db<{ n: string }>( `SELECT count(*)::text AS n FROM freight.train_schedule_bookings WHERE booking_id = $1 AND train_schedule_id = $2 AND deleted_at IS NULL`, [ic, scheduleId], ); expect(Number(link.n), "link row").toBe(1); }); it("a 4 000 T giant alone gets a FULL-consist partial offer and fills the train", async () => { const giantSchedule = await createSchedule({ departure: GIANT_DEPARTURE, kind: "bulk", locoPair: ["LOCO-IMP-27", "LOCO-IMP-28"], }); await forceWindowOpen(giantSchedule.id, 45); const bmg = await bookBulkReady({ contractId: contracts.get("BMG")!, tons: 4000, scheduledDate: GIANT_DAY, }); await closeBookingWindow(giantSchedule.id); await completeDocReview(giantSchedule.id); await pollBookingStatus(bmg, ["SELECTED_FOR_BATCH", "AWAITING_PAYMENT"]); const offer = await pollPartialOffer(bmg); expect(Number(offer.offered_wagons), "offered the whole consist").toBe(54); await payViaGateway(bmg); await pollAllocations(bmg, 54); expect((await bookingRow(bmg)).is_split, "BMG is split").toBe(true); await endPaymentPhase(giantSchedule.id); await pollWindow( giantSchedule.id, (s) => s.booking_window_status === "FULL" && s.window_phase === "DONE", "FULL from one booking", ); }); it("the giant's 220 T outstanding must be rebooked EXACTLY on a later train", async () => { const remainder = await createSchedule({ departure: REMAINDER_DEPARTURE, kind: "bulk", locoPair: ["LOCO-IMP-13", "LOCO-IMP-14"], }); await forceWindowOpen(remainder.id, 45); const wrong = await bookBulk({ contractId: contracts.get("BMG")!, tons: 100, scheduledDate: REMAINDER_DAY, }); expect(wrong.status).toBeGreaterThanOrEqual(400); expect(JSON.stringify(wrong.body)).toMatch(/must take the whole/i); const exact = await bookBulk({ contractId: contracts.get("BMG")!, tons: 220, scheduledDate: REMAINDER_DAY, }); expect(exact.status, JSON.stringify(exact.body)).toBeLessThanOrEqual(201); await acceptOperationIfPossible(contracts.get("BMG")!); }); }); /** The remainder booking only needs to exist; walking its gate is out of scope. */ async function acceptOperationIfPossible(contractId: string) { const booking = await bookingFor(contractId); if (booking.status === "OPERATION_REQUEST_PENDING") await acceptOperation(booking.id); }