/** * BULK EXPORT edge matrix (reversed corridor): * a) a booking on a day with no open window is rejected * b) mid-route boarding (DIRE_DAWA → port) shares the train with a KALITY * through-booking * c) directional FULL: the border edges are committed, so the window flips * FULL while the home leg still has free wagons * d) a dateless DOMESTIC ride-along boards the FULL train's free home leg — * its pay window is clamped to the export close, it pays and links, and * the window stays FULL * e) a same-day sibling export train keeps its own independent window */ import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { closeDb, db, gateway, poll } from "./client"; import { EXP_DEST, EXP_ORIGIN, acceptIntercityOnto, acceptOperation, allocatedWagons, bookBulk, bookBulkReady, expectDayRefused, bookingFor, bookingRow, clearIntercityBooking, createSchedule, departureAt, eatDayStr, ensureExportRoute, extendPayWindow, forceWindowOpen, payViaGateway, pollAllocations, pollBookingStatus, pollWindow, releaseUnpaidHolds, resetCorridorDay, scheduleRow, seedTenantContracts, } from "./flows"; const DEPARTURE = departureAt(35); const BOOKING_DAY = eatDayStr(DEPARTURE); const NO_WINDOW_DAY = eatDayStr(departureAt(39)); // no schedule exists there const STAMP = String(Date.now()); describe("bulk export matrix: sub-corridor, directional FULL, ride-along, own windows", () => { let contracts: Map; let scheduleId: string; beforeAll(async () => { await gateway.reset(); await releaseUnpaidHolds(); await ensureExportRoute(); await resetCorridorDay(DEPARTURE, EXP_ORIGIN, EXP_DEST); contracts = await seedTenantContracts(STAMP, [ { suffix: "YM1", freight: "BULK", direction: "EXPORT" }, { suffix: "YMNW", freight: "BULK", direction: "EXPORT" }, { suffix: "YMSUB", freight: "BULK", direction: "EXPORT", originCode: "DIRE_DAWA", destCode: EXP_DEST }, { suffix: "YMIC", freight: "BULK", direction: "DOMESTIC", originCode: EXP_ORIGIN, destCode: "MOJO" }, ]); scheduleId = ( await createSchedule({ departure: DEPARTURE, kind: "bulk", locoPair: ["LOCO-EXP-11", "LOCO-EXP-12"], originCode: EXP_ORIGIN, destCode: EXP_DEST, }) ).id; await forceWindowOpen(scheduleId, 90); }, 1_800_000); afterAll(closeDb); it("rejects a booking on a day with no open window", async () => { const res = await expectDayRefused({ contractId: contracts.get("YMNW")!, tons: 140, scheduledDate: NO_WINDOW_DAY, }); expect(res.status).toBeGreaterThanOrEqual(400); expect(JSON.stringify(res.body)).toMatch(/booking window|no departures/i); }); it("a through booking and a mid-route boarding commit the border edge", async () => { const through = await bookBulkReady({ contractId: contracts.get("YM1")!, tons: 2800, scheduledDate: BOOKING_DAY, mode: "export", }); const sub = await bookBulkReady({ contractId: contracts.get("YMSUB")!, tons: 980, scheduledDate: BOOKING_DAY, mode: "export", }); await extendPayWindow(scheduleId, [through, sub]); await payViaGateway(through); await pollAllocations(through, 40); await payViaGateway(sub); await pollAllocations(sub, 14); expect((await bookingRow(through)).train_schedule_id).toBe(scheduleId); expect((await bookingRow(sub)).train_schedule_id).toBe(scheduleId); // 40 + 14 = 54 wagons committed on the border edge (…→ DJIB_PORT), yet the // home leg (KALITY → DIRE_DAWA) still has 14 free — and the window stays // OPEN on that free leg. NOTE: the older Cypress twin asserts FULL here; // the live engine is leg-granular instead, which is why the ride-along // below can still board. Assert the occupancy invariant, not the flag. expect(await allocatedWagons(scheduleId), "border edge committed at 54").toBe(54); expect( (await scheduleRow(scheduleId)).booking_window_status, "window stays open on the free home leg", ).toBe("OPEN"); }, 1_800_000); it("a ride-along boards the train's free home leg — clamped, paid, linked", async () => { const res = await bookBulk({ contractId: contracts.get("YMIC")!, tons: 140 }); expect(res.status, JSON.stringify(res.body)).toBeLessThanOrEqual(201); const ic = (await bookingFor(contracts.get("YMIC")!)).id; await clearIntercityBooking(ic); await acceptIntercityOnto(scheduleId, ic); await pollBookingStatus(ic, ["SELECTED_FOR_BATCH", "AWAITING_PAYMENT"]); const schedule = await scheduleRow(scheduleId); expect( new Date((await bookingRow(ic)).payment_deadline!).getTime(), "ride-along deadline clamped to the export close", ).toBeLessThanOrEqual(new Date(String(schedule.window_closes_at)).getTime()); await payViaGateway(ic); // A paid ride-along is unpinned back to the pool; staff place it again. await acceptIntercityOnto(scheduleId, ic); await poll( "ride-along linked to the export 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 }, ); // The ride-along rides the home leg, so the border edge is untouched. expect(await allocatedWagons(scheduleId), "border edge still 54").toBe(54); }, 900_000); it("a same-day sibling export train keeps its own window", async () => { const sibling = new Date(DEPARTURE.getTime() + 90 * 60_000); await createSchedule({ departure: sibling, kind: "bulk", locoPair: ["LOCO-EXP-3", "LOCO-EXP-4"], originCode: EXP_ORIGIN, destCode: EXP_DEST, }); const anchor = await scheduleRow(scheduleId); const [row] = await db<{ id: string; window_phase: string; window_closes_at: string }>( `SELECT ts.id, ts.window_phase, ts.window_closes_at FROM freight.train_schedules ts JOIN freight.yards o ON o.id = ts.origin_station_id AND o.code = $1 JOIN freight.yards d ON d.id = ts.destination_station_id AND d.code = $2 WHERE ts.deleted_at IS NULL AND ts.id <> $3 AND abs(extract(epoch FROM (ts.scheduled_departure_date - $4::timestamptz))) < 7200 ORDER BY ts.created_at DESC LIMIT 1`, [EXP_ORIGIN, EXP_DEST, scheduleId, DEPARTURE.toISOString()], ); expect(row, "sibling export schedule").toBeTruthy(); expect(row.window_phase, "own fresh window").toBe("PRE_WINDOW"); expect( new Date(row.window_closes_at).getTime(), "own close, anchored to its own departure", ).not.toBe(new Date(String(anchor.window_closes_at)).getTime()); }); }); // `acceptOperation` is imported for symmetry with the import matrix; the export // ride-along is accepted onto the train instead. void acceptOperation;