mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 17:08:18 +00:00
223 lines
7.5 KiB
TypeScript
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 {};
|