Files
edr-platform/e2e/freight/cypress/e2e/flows/bulk_critical_matrix.cy.ts
2026-07-31 10:33:27 +00:00

223 lines
7.5 KiB
TypeScript

/**
* BULK IMPORT critical-scenario matrix — bulk-specific corridor edge cases:
*
* 1. gate: a wheat booking on a day with no open window is rejected
* 2. sub-corridor bulk (NAGAD → MOJO, 700 T) rides the through-train next
* to a DJIB_PORT → KALITY 1 400 T booking — corridor-aware batch
* 3. bulk intercity ride-along (MOJO → KALITY, DOMESTIC, 140 T): dateless
* booking, staff accept onto the import train's free leg, pay window
* opens, paid + linked
* 4. WHOLE-TRAIN giant: a 4 000 T booking (58 wagons worth) alone on a
* 54-wagon train → partial offer of the FULL consist (3 780 T), gateway
* settle applies the split, the train is FULL from ONE booking, and the
* 220 T outstanding must be rebooked EXACTLY on a later train.
*
* Sequential steps — retries off.
*/
import {
acceptOperation,
apiPost,
bookBulk,
clearIntercityToFullyExecuted,
clearToOperationRequestPending,
closeBookingWindow,
completeDocReview,
createImportSchedule,
db,
departureAt,
eatDayStr,
endPaymentPhase,
ensureCorridorRoute,
forceWindowOpen,
markPaid,
opsStaff,
pollAllocations,
pollBookingStatus,
pollDb,
resetCorridorDay,
seedImportContract,
settleViaGateway,
withBooking,
withSchedule,
type ScheduleRow,
} from "./import-utils";
const DEPARTURE = departureAt(15);
const BOOKING_DAY = eatDayStr(DEPARTURE);
const GIANT_DEPARTURE = departureAt(16);
const GIANT_DAY = eatDayStr(GIANT_DEPARTURE);
const REMAINDER_DEPARTURE = departureAt(17);
const REMAINDER_DAY = eatDayStr(REMAINDER_DEPARTURE);
const NO_WINDOW_DAY = eatDayStr(departureAt(19)); // no schedule exists there
const stamp = String(Date.now());
const stampedRef = (suffix: string) => `CTR-IMP-${stamp}-${suffix}`;
describe("bulk critical matrix: gates, sub-corridor, bulk intercity, whole-train giant", { retries: 0 }, () => {
before(() => {
cy.task("db:seedFile", "seed-import-corridor.sql");
seedImportContract({ suffix: "BM1", reference: stampedRef("BM1"), freight: "BULK" });
seedImportContract({
suffix: "BMSUB",
reference: stampedRef("BMSUB"),
freight: "BULK",
originCode: "NAGAD",
destCode: "MOJO",
});
seedImportContract({
suffix: "BMIC",
reference: stampedRef("BMIC"),
freight: "BULK",
direction: "DOMESTIC",
originCode: "MOJO",
destCode: "KALITY",
});
seedImportContract({ suffix: "BMG", reference: stampedRef("BMG"), freight: "BULK" });
});
it("operations prepares the corridor and the D+15 bulk train with an open window", () => {
ensureCorridorRoute();
resetCorridorDay(DEPARTURE);
resetCorridorDay(GIANT_DEPARTURE);
resetCorridorDay(REMAINDER_DEPARTURE);
createImportSchedule({
departure: DEPARTURE,
locoPair: ["LOCO-IMP-25", "LOCO-IMP-26"],
kind: "bulk",
});
withSchedule(DEPARTURE, (s) => forceWindowOpen(s.id, 60));
});
it("gate: a wheat booking on a day with no open window is rejected", () => {
bookBulk({
suffix: "BM1",
tons: 140,
scheduledDate: NO_WINDOW_DAY,
expectFailure: "booking window",
});
});
it("a through-corridor 1 400 T booking and a NAGAD→MOJO 700 T booking share the train", () => {
bookBulk({ suffix: "BM1", tons: 1400, scheduledDate: BOOKING_DAY });
clearToOperationRequestPending("BM1", BOOKING_DAY);
acceptOperation("BM1");
bookBulk({ suffix: "BMSUB", tons: 700, scheduledDate: BOOKING_DAY });
clearToOperationRequestPending("BMSUB", BOOKING_DAY);
acceptOperation("BMSUB");
withSchedule(DEPARTURE, (s) => {
closeBookingWindow(s.id);
completeDocReview(s.id);
});
["BM1", "BMSUB"].forEach((suffix) =>
pollBookingStatus(suffix, ["SELECTED_FOR_BATCH", "AWAITING_PAYMENT"]),
);
markPaid("BM1");
pollAllocations("BM1", 20);
markPaid("BMSUB");
pollAllocations("BMSUB", 10);
withSchedule(DEPARTURE, (s) => {
withBooking("BM1", (b) => expect(b.train_schedule_id).to.eq(s.id));
withBooking("BMSUB", (b) => expect(b.train_schedule_id).to.eq(s.id));
});
});
it("bulk intercity ride-along: dateless DOMESTIC wheat accepted onto the import train's free leg", () => {
bookBulk({ suffix: "BMIC", tons: 140 }); // 2 wagons MOJO → KALITY, dateless
// DOMESTIC bookings skip the shipment-day step entirely — finalize alone
// lands FULLY_EXECUTED; staff assign it to a passing train below.
clearIntercityToFullyExecuted("BMIC");
withSchedule(DEPARTURE, (s) => {
withBooking("BMIC", (b) => {
apiPost(opsStaff, `/api/train-scheduling/schedules/${s.id}/intercity/accept`, {
bookingIds: [b.id],
})
.its("status")
.should("be.oneOf", [200, 201]);
});
});
pollBookingStatus("BMIC", ["SELECTED_FOR_BATCH", "AWAITING_PAYMENT"]);
withBooking("BMIC", (b) => {
expect(b.payment_deadline, "ride-along pay window opened").to.be.a("string");
});
markPaid("BMIC");
withSchedule(DEPARTURE, (s) => {
withBooking("BMIC", (b) => {
expect(b.train_schedule_id, "linked to the import train").to.eq(s.id);
db<{ n: string }>(
`SELECT count(*) AS n FROM freight.train_schedule_bookings
WHERE booking_id = $1 AND train_schedule_id = $2 AND deleted_at IS NULL`,
[b.id, s.id],
).then(({ rows }) => expect(Number(rows[0].n), "link row").to.eq(1));
});
});
});
it("whole-train giant: 4 000 T alone gets a FULL-consist partial offer (3 780 T) and fills the train", () => {
createImportSchedule({
departure: GIANT_DEPARTURE,
locoPair: ["LOCO-IMP-27", "LOCO-IMP-28"],
kind: "bulk",
});
withSchedule(GIANT_DEPARTURE, (s) => forceWindowOpen(s.id, 45));
bookBulk({ suffix: "BMG", tons: 4000, scheduledDate: GIANT_DAY });
clearToOperationRequestPending("BMG", GIANT_DAY);
acceptOperation("BMG");
withSchedule(GIANT_DEPARTURE, (s) => {
closeBookingWindow(s.id);
completeDocReview(s.id);
});
pollBookingStatus("BMG", ["SELECTED_FOR_BATCH", "AWAITING_PAYMENT"]);
withBooking("BMG", (b) => {
pollDb<{ status: string }>(
"BMG open partial offer",
`SELECT status FROM freight.booking_batch_offers
WHERE booking_id = $1 AND deleted_at IS NULL
ORDER BY created_at DESC LIMIT 1`,
[b.id],
(row) => row?.status === "OFFERED",
10,
);
});
settleViaGateway("BMG");
pollAllocations("BMG", 54);
withBooking("BMG", (b) => {
expect(b.is_split, "BMG is split").to.eq(true);
});
withSchedule(GIANT_DEPARTURE, (s) => {
endPaymentPhase(s.id);
pollDb<ScheduleRow>(
"giant train FULL + DONE from one booking",
`SELECT window_phase, booking_window_status FROM freight.train_schedules WHERE id = $1`,
[s.id],
(row) => row?.booking_window_status === "FULL" && row?.window_phase === "DONE",
);
});
});
it("the giant's 220 T outstanding must be rebooked EXACTLY on a later train", () => {
createImportSchedule({
departure: REMAINDER_DEPARTURE,
locoPair: ["LOCO-IMP-13", "LOCO-IMP-14"],
kind: "bulk",
});
withSchedule(REMAINDER_DEPARTURE, (s) => forceWindowOpen(s.id, 45));
bookBulk({
suffix: "BMG",
tons: 100,
scheduledDate: REMAINDER_DAY,
expectFailure: "must take the whole",
});
bookBulk({ suffix: "BMG", tons: 220, scheduledDate: REMAINDER_DAY });
clearToOperationRequestPending("BMG", REMAINDER_DAY);
});
});
export {};