/** * EXPORT critical-scenario matrix (reversed corridor, D+7): * * 1. sub-corridor export: a DIRE_DAWA → DJIB_PORT booking boards mid-route * and shares the train with a KALITY through-booking * 2. directional FULL: through 40w + sub-corridor 14w commit every wagon on * the border edges → the export window flips FULL while the KALITY→MOJO * home leg still has 14 free wagons * 3. intercity ride-along on the FULL export train's free home leg * (KALITY → MOJO, DOMESTIC, dateless) — accepted, pay deadline clamped * to the EXPORT window close, paid + linked; the window stays FULL * 4. same-day sibling export train keeps its OWN window — export is * excluded from the import route-day group rule * 5. duplicate ISO container number across two bookings of the same * route-day is rejected * * Sequential steps — retries off. */ import { acceptExport, apiPost, bookContainers, createImportSchedule, db, departureAt, eatDayStr, ensureExportRoute, EXP_DEST, EXP_ORIGIN, forceWindowOpen, isoNumber, markPaid, opsStaff, pollAllocations, pollBookingStatus, pollDb, resetCorridorDay, seedImportContract, withBooking, withExportSchedule, type ScheduleRow, } from "./import-utils"; const DEPARTURE = departureAt(7); const BOOKING_DAY = eatDayStr(DEPARTURE); const stamp = String(Date.now()); const stampedRef = (suffix: string) => `CTR-IMP-${stamp}-${suffix}`; describe("export matrix: sub-corridor, directional FULL, intercity on FULL train, own windows", { retries: 0 }, () => { before(() => { cy.task("db:seedFile", "seed-import-corridor.sql"); seedImportContract({ suffix: "EM1", reference: stampedRef("EM1"), direction: "EXPORT", originCode: EXP_ORIGIN, destCode: EXP_DEST, }); seedImportContract({ suffix: "EMSUB", reference: stampedRef("EMSUB"), direction: "EXPORT", originCode: "DIRE_DAWA", destCode: EXP_DEST, }); seedImportContract({ suffix: "EMIC", reference: stampedRef("EMIC"), direction: "DOMESTIC", originCode: EXP_ORIGIN, destCode: "MOJO", }); seedImportContract({ suffix: "EMDUP", reference: stampedRef("EMDUP"), direction: "EXPORT", originCode: EXP_ORIGIN, destCode: EXP_DEST, }); }); it("operations prepares the export corridor and the D+7 train with an open window", () => { ensureExportRoute(); resetCorridorDay(DEPARTURE, EXP_DEST, EXP_ORIGIN); createImportSchedule({ departure: DEPARTURE, locoPair: ["LOCO-EXP-11", "LOCO-EXP-12"], originCode: EXP_ORIGIN, destCode: EXP_DEST, }); withExportSchedule(DEPARTURE, (s) => forceWindowOpen(s.id, 90)); }); it("a KALITY through-booking (40w) and a DIRE_DAWA sub-corridor booking (14w) share the train", () => { bookContainers({ suffix: "EM1", runStamp: stamp, isoSeed: 8600, twenty: 80, // 40 wagons, full corridor scheduledDate: BOOKING_DAY, }); acceptExport("EM1"); // While the window is still OPEN (14 wagons free): a booking reusing one // of EM1's ISO container numbers on the same route-day is rejected. db<{ id: string }>( `SELECT id FROM freight.contracts WHERE reference LIKE 'CTR-IMP-%-' || $1 ORDER BY created_at DESC LIMIT 1`, ["EMDUP"], ).then(({ rows }) => { apiPost( "user@gmail.com", `/api/contracts/${rows[0].id}/bookings`, { scheduledDate: BOOKING_DAY, containers: [ { containerSize: "20ft", quantity: 2, units: [ { containerNumber: isoNumber(stamp, 8600), vgmTons: 10 }, { containerNumber: isoNumber(stamp, 9990), vgmTons: 10 }, ], }, ], }, false, ).then((res) => { expect(res.status, "cross-booking ISO clash rejected").to.be.within(400, 422); expect(JSON.stringify(res.body).toLowerCase()).to.include("container"); }); }); bookContainers({ suffix: "EMSUB", runStamp: stamp, isoSeed: 8700, twenty: 28, // 14 wagons, boards mid-route at DIRE_DAWA scheduledDate: BOOKING_DAY, }); acceptExport("EMSUB"); markPaid("EM1"); pollAllocations("EM1", 40); markPaid("EMSUB"); pollAllocations("EMSUB", 14); withExportSchedule(DEPARTURE, (s) => { withBooking("EM1", (b) => expect(b.train_schedule_id).to.eq(s.id)); withBooking("EMSUB", (b) => expect(b.train_schedule_id).to.eq(s.id)); }); }); it("the border edges are committed — the export window flips FULL (home leg still empty)", () => { withExportSchedule(DEPARTURE, (s) => { pollDb( "directional FULL", `SELECT booking_window_status FROM freight.train_schedules WHERE id = $1`, [s.id], (row) => row?.booking_window_status === "FULL", ); }); }); it("intercity ride-along boards the FULL train's free home leg — deadline clamped to the export close", () => { bookContainers({ suffix: "EMIC", runStamp: stamp, isoSeed: 8800, twenty: 4, // 2 wagons KALITY → MOJO, dateless }); withBooking("EMIC", (b) => { apiPost(opsStaff, `/api/bookings/${b.id}/operation/review`, { decision: "ACCEPT" }) .its("status") .should("be.oneOf", [200, 201]); }); pollBookingStatus("EMIC", "FULLY_EXECUTED", 10); withExportSchedule(DEPARTURE, (s) => { withBooking("EMIC", (b) => { apiPost(opsStaff, `/api/train-scheduling/schedules/${s.id}/intercity/accept`, { bookingIds: [b.id], }) .its("status") .should("be.oneOf", [200, 201]); }); }); pollBookingStatus("EMIC", ["SELECTED_FOR_BATCH", "AWAITING_PAYMENT"]); withExportSchedule(DEPARTURE, (s) => { withBooking("EMIC", (b) => { // Export parity: the ride-along's pay window never outlives the close. expect(new Date(b.payment_deadline!).getTime()).to.be.at.most( new Date(s.window_closes_at!).getTime(), ); }); }); markPaid("EMIC"); withExportSchedule(DEPARTURE, (s) => { withBooking("EMIC", (b) => { expect(b.train_schedule_id, "linked to the export train").to.eq(s.id); }); // The ride-along never reopens the export window. expect(s.booking_window_status, "window stays FULL").to.eq("FULL"); }); }); it("a same-day sibling export train keeps its OWN window — no route-day group for export", () => { const sibling = new Date(DEPARTURE.getTime() + 90 * 60_000); createImportSchedule({ departure: sibling, locoPair: ["LOCO-EXP-1", "LOCO-EXP-2"], originCode: EXP_ORIGIN, destCode: EXP_DEST, }); withExportSchedule(DEPARTURE, (anchor) => { 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, anchor.id, DEPARTURE.toISOString()], ).then(({ rows }) => { expect(rows, "sibling export schedule").to.have.length(1); // The anchor's window was forced/consumed (FULL); a joiner under the // import group rule would copy its live state. Export siblings don't: // this one starts its own PRE_WINDOW timeline anchored to its OWN // departure. expect(rows[0].window_phase, "own fresh window").to.eq("PRE_WINDOW"); expect( new Date(rows[0].window_closes_at).getTime(), "own close, clamped to its own departure", ).to.not.eq(new Date(anchor.window_closes_at!).getTime()); }); }); }); }); export {};