mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
Refactor booking process to use bookAndClear utility
- Replaced instances of bookContainers with bookAndClear across multiple test files to streamline booking and acceptance process. - Updated import statements to include bookAndClear where necessary. - Removed redundant acceptOperation calls after booking, as bookAndClear handles this internally. - Adjusted comments and documentation to reflect changes in booking logic. - Modified forceReservationExpiry function to ensure payment deadlines are set correctly, preventing issues with booking promotions.
This commit is contained in:
@@ -24,8 +24,15 @@ export default defineConfig({
|
||||
screenshotOnRunFailure: true,
|
||||
viewportWidth: 1440,
|
||||
viewportHeight: 900,
|
||||
defaultCommandTimeout: 10000,
|
||||
requestTimeout: 15000,
|
||||
// Generous across the board: these journeys drive the batch engine, whose
|
||||
// window transitions are settled by a 10s server tick, and a single step
|
||||
// can wait on several of them. Two minutes is long enough that a real
|
||||
// timeout means something is genuinely stuck rather than merely slow.
|
||||
defaultCommandTimeout: 120000,
|
||||
requestTimeout: 120000,
|
||||
responseTimeout: 120000,
|
||||
pageLoadTimeout: 120000,
|
||||
taskTimeout: 120000,
|
||||
retries: { runMode: 1, openMode: 0 },
|
||||
env: {
|
||||
apiUrl: process.env.CYPRESS_API_URL ?? "http://localhost:3101",
|
||||
|
||||
@@ -28,7 +28,11 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
acceptExport,
|
||||
acceptOperation,
|
||||
apiPost,
|
||||
bookContainers,
|
||||
clearToOperationRequestPending,
|
||||
closeBookingWindow,
|
||||
db,
|
||||
dbSchedule,
|
||||
@@ -64,15 +68,22 @@ export function escapeRegExp(value: string): string {
|
||||
/**
|
||||
* A Date as the unzoned "YYYY-MM-DDTHH:mm" wall-clock string that an
|
||||
* `<input type="datetime-local">` accepts (the create-schedule form's Departure
|
||||
* date field). The corridor departures are pinned to 12:00 EAT = 09:00 UTC, so
|
||||
* the wall-clock the operator types is the EAT one — the browser running the
|
||||
* test is UTC in CI and EAT on a local machine, and typing a UTC-rendered
|
||||
* string on an EAT machine would file the departure three hours early and land
|
||||
* it on the wrong booking day.
|
||||
* date field).
|
||||
*
|
||||
* The value MUST be in the BROWSER's local zone, not EAT. The input carries no
|
||||
* offset, so whatever is typed is read as local time and converted on submit —
|
||||
* pre-shifting to EAT on a UTC browser files the departure three hours late,
|
||||
* which put it outside dbSchedule's ±1h lookup window and made a successfully
|
||||
* created schedule look like it had never been created at all.
|
||||
*
|
||||
* Built from the local getters rather than toISOString for exactly that reason.
|
||||
*/
|
||||
export function localDateTime(d: Date): string {
|
||||
const eat = new Date(d.getTime() + 3 * 3_600_000);
|
||||
return eat.toISOString().slice(0, 16);
|
||||
const pad = (n: number) => String(n).padStart(2, "0");
|
||||
return (
|
||||
`${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}` +
|
||||
`T${pad(d.getHours())}:${pad(d.getMinutes())}`
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -142,8 +153,8 @@ export function configureAndOpenSchedule(opts: {
|
||||
|
||||
cy.loginBackoffice(opsStaff);
|
||||
cy.visit("/dashboard/operations/train-scheduling-v2");
|
||||
cy.contains("button", "New schedule", { timeout: 30000 }).click();
|
||||
cy.contains("Create train schedule", { timeout: 20000 }).should("be.visible");
|
||||
cy.contains("button", "New schedule", { timeout: 120000 }).click();
|
||||
cy.contains("Create train schedule", { timeout: 120000 }).should("be.visible");
|
||||
|
||||
// Route options are composed by formatRouteLabel, which renders yard LABELS
|
||||
// ("Djibouti Port"), never codes — so resolve the label for this corridor.
|
||||
@@ -160,7 +171,7 @@ export function configureAndOpenSchedule(opts: {
|
||||
// Option text is composed: "TRN-G1-1 — E2E Group-1 … · 53 wagons".
|
||||
cy.mantineSelect("Train", new RegExp(escapeRegExp(trainCode)));
|
||||
cy.get(".mantine-Modal-content").contains("button", "Create").click();
|
||||
cy.get(".mantine-Modal-content", { timeout: 30000 }).should("not.exist");
|
||||
cy.get(".mantine-Modal-content", { timeout: 120000 }).should("not.exist");
|
||||
|
||||
expectCapacity(opts.departure, opts.wagons ?? G1_WAGONS);
|
||||
withSchedule(opts.departure, (s) => forceWindowOpen(s.id, opts.closesInMinutes ?? 45));
|
||||
@@ -179,8 +190,8 @@ export function closeWindowAndRunBatch(departure: Date) {
|
||||
|
||||
cy.loginBackoffice(opsStaff);
|
||||
withSchedule(departure, (s) => cy.visit(`/dashboard/operations/batch-board/${s.id}`));
|
||||
cy.contains("Doc review", { timeout: 30000 }).should("exist");
|
||||
cy.contains("button", "Doc review complete — run batch", { timeout: 20000 }).click();
|
||||
cy.contains("Doc review", { timeout: 120000 }).should("exist");
|
||||
cy.contains("button", "Doc review complete — run batch", { timeout: 120000 }).click();
|
||||
|
||||
withSchedule(departure, (s) =>
|
||||
pollDb<ScheduleRow>(
|
||||
@@ -212,12 +223,12 @@ export function expectBoard(
|
||||
) {
|
||||
cy.loginBackoffice(opsStaff);
|
||||
withSchedule(departure, (s) => cy.visit(`/dashboard/operations/batch-board/${s.id}`));
|
||||
cy.contains(/Priority Tracking/, { timeout: 30000 }).click();
|
||||
cy.contains("Priority ranking", { timeout: 20000 }).should("be.visible");
|
||||
cy.contains(/Priority Tracking/, { timeout: 120000 }).click();
|
||||
cy.contains("Priority ranking", { timeout: 120000 }).should("be.visible");
|
||||
|
||||
if (opts.inBatch !== undefined) {
|
||||
cy.contains(new RegExp(`In the batch\\s*\\(${opts.inBatch}\\)`), {
|
||||
timeout: 20000,
|
||||
timeout: 120000,
|
||||
}).should("exist");
|
||||
}
|
||||
if (opts.waiting !== undefined) {
|
||||
@@ -309,6 +320,71 @@ export function expectVerdict(
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// booking → clearance gate → operations queue
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Book containers and walk the booking all the way to the operations pool.
|
||||
*
|
||||
* EVERY contract booking is now born in the clearance gate — see
|
||||
* contract-booking.service.ts:211, "EVERY contract booking clears per booking
|
||||
* now — both contract kinds, both paths, intercity included". A booking is
|
||||
* created in AWAITING_DOCUMENTS regardless of whether customs clearance is
|
||||
* enabled, so calling `acceptOperation` straight after `bookContainers` always
|
||||
* 409s with:
|
||||
*
|
||||
* Cannot perform this action on status "AWAITING_DOCUMENTS".
|
||||
* Allowed: OPERATION_REQUEST_PENDING
|
||||
*
|
||||
* The gate is: upload a document → GL approves it → finalize → the customer
|
||||
* proceeds with the shipment day. `clearToOperationRequestPending` runs that
|
||||
* whole chain (the e2e seed configures no required documents, so one ad-hoc
|
||||
* doc satisfies the 100%-approved rule).
|
||||
*
|
||||
* Use this instead of bookContainers + acceptOperation anywhere a booking has
|
||||
* to reach the day pool.
|
||||
*/
|
||||
export function bookAndClear(opts: {
|
||||
suffix: string;
|
||||
runStamp: string;
|
||||
isoSeed: number;
|
||||
twenty?: number;
|
||||
forty?: number;
|
||||
scheduledDate: string;
|
||||
vgmTons?: number;
|
||||
/** EXPORT reserves on accept (FCFS) rather than entering the batch pool. */
|
||||
mode?: "import" | "export";
|
||||
}) {
|
||||
bookContainers({
|
||||
suffix: opts.suffix,
|
||||
runStamp: opts.runStamp,
|
||||
isoSeed: opts.isoSeed,
|
||||
twenty: opts.twenty,
|
||||
forty: opts.forty,
|
||||
scheduledDate: opts.scheduledDate,
|
||||
vgmTons: opts.vgmTons,
|
||||
});
|
||||
clearToOperationRequestPending(opts.suffix, opts.scheduledDate);
|
||||
if (opts.mode === "export") acceptExport(opts.suffix);
|
||||
else acceptOperation(opts.suffix);
|
||||
}
|
||||
|
||||
/**
|
||||
* The clearance half alone, for a booking created some other way — e.g. the
|
||||
* portal form (`bookContainersVisually`), which leaves the booking sitting in
|
||||
* the same AWAITING_DOCUMENTS gate.
|
||||
*/
|
||||
export function clearAndAccept(opts: {
|
||||
suffix: string;
|
||||
scheduledDate: string;
|
||||
mode?: "import" | "export";
|
||||
}) {
|
||||
clearToOperationRequestPending(opts.suffix, opts.scheduledDate);
|
||||
if (opts.mode === "export") acceptExport(opts.suffix);
|
||||
else acceptOperation(opts.suffix);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// booking through the real portal form
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -349,52 +425,93 @@ export function bookContainersVisually(opts: {
|
||||
expect(twenty % 2, "20ft quantity must be even").to.eq(0);
|
||||
|
||||
cy.visitPortal(`/contracts/${opts.contractId}/bookings/new`);
|
||||
cy.contains("New Shipment Booking", { timeout: 30000 }).should("be.visible");
|
||||
cy.contains("New Shipment Booking", { timeout: 120000 }).should("be.visible");
|
||||
|
||||
if (twenty) fillSizeQuantity("20ft", String(twenty));
|
||||
if (forty) fillSizeQuantity("40ft", String(forty));
|
||||
// BOTH size cards must be given a quantity, including the unused one.
|
||||
//
|
||||
// The form renders a ContainerLineEditor per size in the contract's cargo
|
||||
// scope, and an untouched editor keeps one blank unit row. The zod schema
|
||||
// requires a valid ISO number AND a VGM on EVERY unit row
|
||||
// (new-shipment-form/schema.ts:39-50), so that blank row fails validation and
|
||||
// handleSubmit aborts SILENTLY — no modal, no toast, no request. Typing 0
|
||||
// truncates the card's units to none (syncUnits: `next.length = max(0, qty)`)
|
||||
// and takes it out of validation.
|
||||
fillSizeQuantity("20ft", String(twenty));
|
||||
fillSizeQuantity("40ft", String(forty));
|
||||
|
||||
// One ISO row per container, then the VGM on each.
|
||||
//
|
||||
// Scoped PER SIZE CARD, not globally: the form renders a ContainerLineEditor
|
||||
// for every size in the contract's cargo scope, and an editor left at
|
||||
// quantity 0 still renders one blank unit row. A global
|
||||
// `input[placeholder*="MSCU"]` therefore counts the other card's row too —
|
||||
// "Found 7, expected 6" — and the numbers land in the wrong card.
|
||||
const prefix = opts.isoPrefix ?? "MSCU";
|
||||
cy.get('input[placeholder*="MSCU"]', { timeout: 20000 }).should("have.length", total);
|
||||
for (let i = 0; i < total; i += 1) {
|
||||
let unit = 0;
|
||||
const fillUnits = (size: "20ft" | "40ft", count: number) => {
|
||||
if (!count) return;
|
||||
cy.contains(`${size} containers`, { timeout: 120000 })
|
||||
.closest("div.rounded-xl")
|
||||
.within(() => {
|
||||
cy.get('input[placeholder*="MSCU"]', { timeout: 120000 }).should(
|
||||
"have.length",
|
||||
count,
|
||||
);
|
||||
for (let i = 0; i < count; i += 1) {
|
||||
const iso = `${prefix}${String(1_000_000 + unit + i).slice(0, 7)}`;
|
||||
cy.get('input[placeholder*="MSCU"]')
|
||||
.eq(i)
|
||||
.clear({ force: true })
|
||||
.type(`${prefix}${String(1_000_000 + i).slice(0, 7)}`, { force: true });
|
||||
.type(iso, { force: true });
|
||||
}
|
||||
cy.get('input[placeholder*="24.5"]').each(($input) => {
|
||||
cy.wrap($input)
|
||||
.clear({ force: true })
|
||||
.type(String(opts.vgmTons ?? 10), { force: true });
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
unit += count;
|
||||
});
|
||||
};
|
||||
fillUnits("20ft", twenty);
|
||||
fillUnits("40ft", forty);
|
||||
|
||||
pickShipmentDay(opts.shipmentDay);
|
||||
cy.fillCargoDescription();
|
||||
|
||||
cy.contains("button", "Review price & book").should("not.be.disabled").click();
|
||||
cy.contains("Confirm shipment price", { timeout: 30000 }).should("be.visible");
|
||||
cy.contains("Confirm shipment price", { timeout: 120000 }).should("be.visible");
|
||||
cy.contains("button", "Confirm & book").click();
|
||||
cy.location("pathname", { timeout: 30000 }).should("match", /^\/bookings\/.+/);
|
||||
cy.location("pathname", { timeout: 120000 }).should("match", /^\/bookings\/.+/);
|
||||
}
|
||||
|
||||
/** One container-size line's quantity, addressed by its heading (see above). */
|
||||
/**
|
||||
* One container-size line's quantity, addressed by its heading rather than by
|
||||
* position — the cards render in CONTRACT-SCOPE order, not 20ft-then-40ft.
|
||||
*
|
||||
* A no-op when the contract does not scope this size, so callers can always
|
||||
* set both (see the note about blank unit rows in bookContainersVisually).
|
||||
*/
|
||||
function fillSizeQuantity(size: "20ft" | "40ft", value: string) {
|
||||
cy.contains(`${size} containers`, { timeout: 20000 })
|
||||
cy.get("body").then(($body) => {
|
||||
if (!$body.text().includes(`${size} containers`)) return;
|
||||
cy.contains(`${size} containers`, { timeout: 120000 })
|
||||
.closest("div.rounded-xl")
|
||||
.find('input[type="number"]')
|
||||
.first()
|
||||
.clear({ force: true })
|
||||
.type(value, { force: true });
|
||||
});
|
||||
}
|
||||
|
||||
/** Pick a day on the Schedule card's inline, cargo-aware calendar. */
|
||||
function pickShipmentDay(day: Date) {
|
||||
cy.contains(/available day/, { timeout: 30000 }).should("exist");
|
||||
cy.contains(/available day/, { timeout: 120000 }).should("exist");
|
||||
// Day cells are plain buttons in a grid; only bookable days are enabled
|
||||
// (out-of-month duplicates and unscheduled days stay disabled).
|
||||
const eatDay = new Date(day.getTime() + 3 * 3_600_000).getUTCDate();
|
||||
cy.get("button:not(:disabled)", { timeout: 20000 })
|
||||
cy.get("button:not(:disabled)", { timeout: 120000 })
|
||||
.contains(new RegExp(`^${eatDay}$`))
|
||||
.click({ force: true });
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ import {
|
||||
} from "./import-utils";
|
||||
import {
|
||||
G1_WAGONS,
|
||||
bookAndClear,
|
||||
closeWindowAndRunBatch,
|
||||
configureAndOpenSchedule,
|
||||
expectVerdict,
|
||||
@@ -275,7 +276,7 @@ describe("G10·S40b: a re-priced booking parks with no capacity footprint", { re
|
||||
|
||||
let isoSeed = 27_100;
|
||||
ORDER.forEach((suffix) => {
|
||||
bookContainers({
|
||||
bookAndClear({
|
||||
suffix,
|
||||
runStamp: stamp,
|
||||
isoSeed,
|
||||
@@ -283,7 +284,6 @@ describe("G10·S40b: a re-priced booking parks with no capacity footprint", { re
|
||||
scheduledDate: BOOKING_DAY,
|
||||
});
|
||||
isoSeed += SHAPES[suffix].forty;
|
||||
acceptOperation(suffix);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* GROUP 1 · S1 — a no-pay expiry frees exactly the space the waiting list needs.
|
||||
*
|
||||
* A 3×40FT = 3 wagons
|
||||
* B 20×20FT + 10×40FT = 20 wagons (customs clearance)
|
||||
* B 20×20FT + 10×40FT = 20 wagons
|
||||
* C 30×40FT = 30 wagons
|
||||
* ─────────
|
||||
* 53 = the whole train → RESERVED FULL
|
||||
@@ -67,7 +67,9 @@ import {
|
||||
import {
|
||||
G1_TRAIN,
|
||||
G1_WAGONS,
|
||||
bookAndClear,
|
||||
bookContainersVisually,
|
||||
clearAndAccept,
|
||||
closeWindowAndRunBatch,
|
||||
configureAndOpenSchedule,
|
||||
expectBoard,
|
||||
@@ -90,11 +92,11 @@ const stampedRef = (suffix: string) => `CTR-IMP-${stamp}-${suffix}`;
|
||||
* genuinely inside the batch and its expiry genuinely frees space.
|
||||
*/
|
||||
const SHAPES = {
|
||||
A: { twenty: 0, forty: 3, wagons: 3, customs: false },
|
||||
B: { twenty: 20, forty: 10, wagons: 20, customs: true },
|
||||
C: { twenty: 0, forty: 30, wagons: 30, customs: false },
|
||||
A: { twenty: 0, forty: 3, wagons: 3 },
|
||||
B: { twenty: 20, forty: 10, wagons: 20 },
|
||||
C: { twenty: 0, forty: 30, wagons: 30 },
|
||||
// D is booked through the UI, not from this table — see the portal step.
|
||||
D: { twenty: 6, forty: 0, wagons: 3, customs: false },
|
||||
D: { twenty: 6, forty: 0, wagons: 3 },
|
||||
} as const;
|
||||
|
||||
const IN_BATCH = ["A", "B", "C"] as const;
|
||||
@@ -103,12 +105,12 @@ describe("G1·S1: expiry frees exactly the waiting list's space", { retries: 0 }
|
||||
before(() => {
|
||||
cy.task("db:seedFile", "seed-import-corridor.sql");
|
||||
cy.task("db:seedFile", "seed-g1-train.sql");
|
||||
// All four self-clear. A customs contract routes its booking into
|
||||
// AWAITING_DOCUMENTS and a whole clearance gate before ops can accept it
|
||||
// (see clearGeneralBooking in import-utils) — orthogonal to this
|
||||
// scenario, which is about expiry and waiting-list promotion.
|
||||
(["A", "B", "C", "D"] as const).forEach((suffix) =>
|
||||
seedImportContract({
|
||||
suffix,
|
||||
reference: stampedRef(suffix),
|
||||
customs: SHAPES[suffix].customs,
|
||||
}),
|
||||
seedImportContract({ suffix, reference: stampedRef(suffix) }),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -142,14 +144,18 @@ describe("G1·S1: expiry frees exactly the waiting list's space", { retries: 0 }
|
||||
cy.visit(`/dashboard/train-builder/${rows[0].id}`);
|
||||
},
|
||||
);
|
||||
cy.contains(`Train ${G1_TRAIN}`, { timeout: 30000 }).should("exist");
|
||||
cy.contains("Wagon order", { timeout: 20000 }).should("exist");
|
||||
// Stat strip: "Wagons" → 53. Scoped to the stat, since bare "53" would
|
||||
// also match wagon numbers in the consist list below it.
|
||||
cy.contains("Wagons")
|
||||
.parent()
|
||||
.contains(String(G1_WAGONS), { timeout: 20000 })
|
||||
.should("exist");
|
||||
cy.contains(`Train ${G1_TRAIN}`, { timeout: 120000 }).should("exist");
|
||||
cy.contains("Wagon order", { timeout: 120000 }).should("exist");
|
||||
// Stat strip: the "Wagons" KPI cell reads 53.
|
||||
//
|
||||
// Scope to the KpiStrip cell (`div.flex-1`, KpiStrip.tsx) rather than
|
||||
// matching "Wagons" anywhere: the left sidebar has a NavLink of the same
|
||||
// name, cy.contains returns the FIRST match, and its parent never contains
|
||||
// the count — which is exactly how this first failed.
|
||||
cy.contains("div.flex-1", "Wagons", { timeout: 120000 }).should(
|
||||
"contain.text",
|
||||
String(G1_WAGONS),
|
||||
);
|
||||
});
|
||||
|
||||
it("operations schedules that train on the corridor and opens the window", () => {
|
||||
@@ -166,7 +172,7 @@ describe("G1·S1: expiry frees exactly the waiting list's space", { retries: 0 }
|
||||
let isoSeed = 7100;
|
||||
IN_BATCH.forEach((suffix) => {
|
||||
const shape = SHAPES[suffix];
|
||||
bookContainers({
|
||||
bookAndClear({
|
||||
suffix,
|
||||
runStamp: stamp,
|
||||
isoSeed,
|
||||
@@ -175,7 +181,6 @@ describe("G1·S1: expiry frees exactly the waiting list's space", { retries: 0 }
|
||||
scheduledDate: BOOKING_DAY,
|
||||
});
|
||||
isoSeed += shape.twenty + shape.forty;
|
||||
acceptOperation(suffix);
|
||||
});
|
||||
// Booking order = priority order, so A is inside the batch and its expiry
|
||||
// is what frees space (see the SHAPES comment).
|
||||
@@ -193,8 +198,7 @@ describe("G1·S1: expiry frees exactly the waiting list's space", { retries: 0 }
|
||||
isoPrefix: "DDDU",
|
||||
});
|
||||
});
|
||||
pollBookingStatus("D", "OPERATION_REQUEST_PENDING", 10);
|
||||
acceptOperation("D");
|
||||
clearAndAccept({ suffix: "D", scheduledDate: BOOKING_DAY });
|
||||
setPriority("D", 4);
|
||||
});
|
||||
|
||||
|
||||
@@ -43,7 +43,9 @@ import {
|
||||
} from "./import-utils";
|
||||
import {
|
||||
G1_WAGONS,
|
||||
bookAndClear,
|
||||
bookContainersVisually,
|
||||
clearAndAccept,
|
||||
closeWindowAndRunBatch,
|
||||
configureAndOpenSchedule,
|
||||
expectBoard,
|
||||
@@ -101,7 +103,7 @@ describe("G1·S2: four bookings pay and fill the train exactly", { retries: 0 },
|
||||
let isoSeed = 8100;
|
||||
VIA_API.forEach((suffix) => {
|
||||
const shape = SHAPES[suffix];
|
||||
bookContainers({
|
||||
bookAndClear({
|
||||
suffix,
|
||||
runStamp: stamp,
|
||||
isoSeed,
|
||||
@@ -110,7 +112,6 @@ describe("G1·S2: four bookings pay and fill the train exactly", { retries: 0 },
|
||||
scheduledDate: BOOKING_DAY,
|
||||
});
|
||||
isoSeed += shape.containers;
|
||||
acceptOperation(suffix);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -124,8 +125,7 @@ describe("G1·S2: four bookings pay and fill the train exactly", { retries: 0 },
|
||||
isoPrefix: "SEXU",
|
||||
});
|
||||
});
|
||||
pollBookingStatus("D", "OPERATION_REQUEST_PENDING", 10);
|
||||
acceptOperation("D");
|
||||
clearAndAccept({ suffix: "D", scheduledDate: BOOKING_DAY });
|
||||
});
|
||||
|
||||
it("the batch reserves all four — they fit exactly, so nobody is offered a split", () => {
|
||||
|
||||
@@ -39,7 +39,9 @@ import {
|
||||
} from "./import-utils";
|
||||
import {
|
||||
G1_WAGONS,
|
||||
bookAndClear,
|
||||
bookContainersVisually,
|
||||
clearAndAccept,
|
||||
closeWindowAndRunBatch,
|
||||
configureAndOpenSchedule,
|
||||
expectBoard,
|
||||
@@ -93,7 +95,7 @@ describe("G1·S3: an under-filled train keeps its day open", { retries: 0 }, ()
|
||||
let isoSeed = 8600;
|
||||
(["A", "B"] as const).forEach((suffix) => {
|
||||
const shape = SHAPES[suffix];
|
||||
bookContainers({
|
||||
bookAndClear({
|
||||
suffix,
|
||||
runStamp: stamp,
|
||||
isoSeed,
|
||||
@@ -102,7 +104,6 @@ describe("G1·S3: an under-filled train keeps its day open", { retries: 0 }, ()
|
||||
scheduledDate: BOOKING_DAY,
|
||||
});
|
||||
isoSeed += shape.twenty + shape.forty;
|
||||
acceptOperation(suffix);
|
||||
});
|
||||
|
||||
cy.loginPortal(customer);
|
||||
@@ -114,8 +115,7 @@ describe("G1·S3: an under-filled train keeps its day open", { retries: 0 }, ()
|
||||
isoPrefix: "CSQU",
|
||||
});
|
||||
});
|
||||
pollBookingStatus("C", "OPERATION_REQUEST_PENDING", 10);
|
||||
acceptOperation("C");
|
||||
clearAndAccept({ suffix: "C", scheduledDate: BOOKING_DAY });
|
||||
});
|
||||
|
||||
it("the batch reserves all three whole — there is room to spare, so no splits", () => {
|
||||
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
} from "./import-utils";
|
||||
import {
|
||||
G1_WAGONS,
|
||||
bookAndClear,
|
||||
closeWindowAndRunBatch,
|
||||
configureAndOpenSchedule,
|
||||
expectBoard,
|
||||
@@ -109,7 +110,7 @@ describe("G1·S4: a split closes the last 3-wagon gap", { retries: 0 }, () => {
|
||||
let isoSeed = 9100;
|
||||
ORDER.forEach((suffix) => {
|
||||
const shape = SHAPES[suffix];
|
||||
bookContainers({
|
||||
bookAndClear({
|
||||
suffix,
|
||||
runStamp: stamp,
|
||||
isoSeed,
|
||||
@@ -118,7 +119,6 @@ describe("G1·S4: a split closes the last 3-wagon gap", { retries: 0 }, () => {
|
||||
scheduledDate: BOOKING_DAY,
|
||||
});
|
||||
isoSeed += shape.twenty + shape.forty;
|
||||
acceptOperation(suffix);
|
||||
});
|
||||
// Priority decides who gets a whole seat and who gets the offer.
|
||||
ORDER.forEach((suffix, i) => setPriority(suffix, i + 1));
|
||||
|
||||
@@ -43,6 +43,7 @@ import {
|
||||
} from "./import-utils";
|
||||
import {
|
||||
G1_WAGONS,
|
||||
bookAndClear,
|
||||
closeWindowAndRunBatch,
|
||||
configureAndOpenSchedule,
|
||||
expectBoard,
|
||||
@@ -104,7 +105,7 @@ describe("G1·S5: expiry cascades into a second promotion", { retries: 0 }, () =
|
||||
let isoSeed = 9600;
|
||||
ORDER.forEach((suffix) => {
|
||||
const shape = SHAPES[suffix];
|
||||
bookContainers({
|
||||
bookAndClear({
|
||||
suffix,
|
||||
runStamp: stamp,
|
||||
isoSeed,
|
||||
@@ -113,7 +114,6 @@ describe("G1·S5: expiry cascades into a second promotion", { retries: 0 }, () =
|
||||
scheduledDate: BOOKING_DAY,
|
||||
});
|
||||
isoSeed += shape.twenty + shape.forty;
|
||||
acceptOperation(suffix);
|
||||
});
|
||||
ORDER.forEach((suffix, i) => setPriority(suffix, i + 1));
|
||||
});
|
||||
|
||||
@@ -56,6 +56,7 @@ import {
|
||||
} from "./import-utils";
|
||||
import {
|
||||
G1_WAGONS,
|
||||
bookAndClear,
|
||||
closeWindowAndRunBatch,
|
||||
configureAndOpenSchedule,
|
||||
expectBoard,
|
||||
@@ -105,7 +106,7 @@ describe("G1·S6: an ignored split offer expires the booking whole", { retries:
|
||||
let isoSeed = 10_100;
|
||||
ORDER.forEach((suffix) => {
|
||||
const shape = SHAPES[suffix];
|
||||
bookContainers({
|
||||
bookAndClear({
|
||||
suffix,
|
||||
runStamp: stamp,
|
||||
isoSeed,
|
||||
@@ -114,7 +115,6 @@ describe("G1·S6: an ignored split offer expires the booking whole", { retries:
|
||||
scheduledDate: BOOKING_DAY,
|
||||
});
|
||||
isoSeed += shape.twenty + shape.forty;
|
||||
acceptOperation(suffix);
|
||||
});
|
||||
ORDER.forEach((suffix, i) => setPriority(suffix, i + 1));
|
||||
|
||||
@@ -195,7 +195,7 @@ describe("G1·S7: a government booking preempts commercial", { retries: 0 }, ()
|
||||
let isoSeed = 10_600;
|
||||
ORDER.forEach((suffix) => {
|
||||
const shape = SHAPES[suffix];
|
||||
bookContainers({
|
||||
bookAndClear({
|
||||
suffix,
|
||||
runStamp: stamp,
|
||||
isoSeed,
|
||||
@@ -203,7 +203,6 @@ describe("G1·S7: a government booking preempts commercial", { retries: 0 }, ()
|
||||
scheduledDate: BOOKING_DAY,
|
||||
});
|
||||
isoSeed += shape.forty;
|
||||
acceptOperation(suffix);
|
||||
});
|
||||
// GA outranks GB, so GB is the one preemption should take.
|
||||
ORDER.forEach((suffix, i) => setPriority(suffix, i + 1));
|
||||
@@ -369,7 +368,7 @@ describe("G1·S8: priority tiers order the batch", { retries: 0 }, () => {
|
||||
|
||||
let isoSeed = 11_100;
|
||||
ORDER.forEach((suffix) => {
|
||||
bookContainers({
|
||||
bookAndClear({
|
||||
suffix,
|
||||
runStamp: stamp,
|
||||
isoSeed,
|
||||
@@ -377,7 +376,6 @@ describe("G1·S8: priority tiers order the batch", { retries: 0 }, () => {
|
||||
scheduledDate: BOOKING_DAY,
|
||||
});
|
||||
isoSeed += SHAPES[suffix].forty;
|
||||
acceptOperation(suffix);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@ import {
|
||||
} from "./import-utils";
|
||||
import {
|
||||
G1_WAGONS,
|
||||
bookAndClear,
|
||||
closeWindowAndRunBatch,
|
||||
configureAndOpenSchedule,
|
||||
expectSplitOffer,
|
||||
@@ -124,7 +125,7 @@ describe("G2·S9: weight refuses a booking while slots sit empty", { retries: 0
|
||||
it("three heavy bookings arrive — 45 wagons of demand for 53 slots", () => {
|
||||
let isoSeed = 12_100;
|
||||
ORDER.forEach((suffix) => {
|
||||
bookContainers({
|
||||
bookAndClear({
|
||||
suffix,
|
||||
runStamp: stamp,
|
||||
isoSeed,
|
||||
@@ -133,7 +134,6 @@ describe("G2·S9: weight refuses a booking while slots sit empty", { retries: 0
|
||||
vgmTons: HEAVY_VGM,
|
||||
});
|
||||
isoSeed += SHAPES[suffix].twenty;
|
||||
acceptOperation(suffix);
|
||||
});
|
||||
ORDER.forEach((suffix, i) => setPriority(suffix, i + 1));
|
||||
});
|
||||
@@ -205,7 +205,7 @@ describe("G2·S10: the overage tolerance admits C whole", { retries: 0 }, () =>
|
||||
|
||||
let isoSeed = 12_600;
|
||||
ORDER.forEach((suffix) => {
|
||||
bookContainers({
|
||||
bookAndClear({
|
||||
suffix,
|
||||
runStamp: stamp,
|
||||
isoSeed,
|
||||
@@ -214,7 +214,6 @@ describe("G2·S10: the overage tolerance admits C whole", { retries: 0 }, () =>
|
||||
vgmTons: HEAVY_VGM,
|
||||
});
|
||||
isoSeed += SHAPES[suffix].twenty;
|
||||
acceptOperation(suffix);
|
||||
});
|
||||
ORDER.forEach((suffix, i) => setPriority(suffix, i + 1));
|
||||
|
||||
@@ -295,7 +294,7 @@ describe("G2·S11: a split is sized against base weight only", { retries: 0 }, (
|
||||
|
||||
let isoSeed = 13_100;
|
||||
ORDER.forEach((suffix) => {
|
||||
bookContainers({
|
||||
bookAndClear({
|
||||
suffix,
|
||||
runStamp: stamp,
|
||||
isoSeed,
|
||||
@@ -304,7 +303,6 @@ describe("G2·S11: a split is sized against base weight only", { retries: 0 }, (
|
||||
vgmTons: HEAVY_VGM,
|
||||
});
|
||||
isoSeed += SHAPES[suffix].twenty;
|
||||
acceptOperation(suffix);
|
||||
});
|
||||
ORDER.forEach((suffix, i) => setPriority(suffix, i + 1));
|
||||
closeWindowAndRunBatch(DEPARTURE);
|
||||
@@ -384,7 +382,7 @@ describe("G2·S12: with light cargo the slots bind first", { retries: 0 }, () =>
|
||||
|
||||
let isoSeed = 13_600;
|
||||
ORDER.forEach((suffix) => {
|
||||
bookContainers({
|
||||
bookAndClear({
|
||||
suffix,
|
||||
runStamp: stamp,
|
||||
isoSeed,
|
||||
@@ -393,7 +391,6 @@ describe("G2·S12: with light cargo the slots bind first", { retries: 0 }, () =>
|
||||
vgmTons: LIGHT_VGM,
|
||||
});
|
||||
isoSeed += SHAPES[suffix].twenty;
|
||||
acceptOperation(suffix);
|
||||
});
|
||||
|
||||
closeWindowAndRunBatch(DEPARTURE);
|
||||
|
||||
@@ -53,6 +53,7 @@ import {
|
||||
seedImportContract,
|
||||
withBooking,
|
||||
} from "./import-utils";
|
||||
import { bookAndClear } from "./g1-utils";
|
||||
|
||||
const stamp = String(Date.now());
|
||||
const stampedRef = (suffix: string) => `CTR-IMP-${stamp}-${suffix}`;
|
||||
@@ -182,14 +183,12 @@ describe("G3·S13: unpaid export holds occupy the train", { retries: 0 }, () =>
|
||||
// Export: ACCEPT is the reservation (FCFS), not a batch entry.
|
||||
acceptExport("EA");
|
||||
|
||||
bookContainers({
|
||||
bookAndClear({
|
||||
suffix: "EB",
|
||||
runStamp: stamp,
|
||||
isoSeed: 14_200,
|
||||
forty: SHAPES.EB.forty,
|
||||
scheduledDate: BOOKING_DAY,
|
||||
});
|
||||
acceptExport("EB");
|
||||
scheduledDate: BOOKING_DAY, mode: "export"});
|
||||
|
||||
// Neither has paid. Both nevertheless hold their wagons: reserved =
|
||||
// SELECTED_FOR_BATCH / AWAITING_PAYMENT is subtracted from capacity
|
||||
@@ -273,14 +272,12 @@ describe("G3·S14: a booking that cannot fit whole is SKIPPED, not split", { ret
|
||||
});
|
||||
|
||||
it("A boards; B is refused WHOLE — no partial offer is ever raised", () => {
|
||||
bookContainers({
|
||||
bookAndClear({
|
||||
suffix: "FA",
|
||||
runStamp: stamp,
|
||||
isoSeed: 14_600,
|
||||
forty: SHAPES.FA.forty,
|
||||
scheduledDate: BOOKING_DAY,
|
||||
});
|
||||
acceptExport("FA");
|
||||
scheduledDate: BOOKING_DAY, mode: "export"});
|
||||
|
||||
// B asks for 25 with 24 free. Export cannot part-load, so the request is
|
||||
// refused at requestOperation with a sized message
|
||||
@@ -310,14 +307,12 @@ describe("G3·S14: a booking that cannot fit whole is SKIPPED, not split", { ret
|
||||
|
||||
it("C and D board behind B — the train fills to 54 without it", () => {
|
||||
(["FC", "FD"] as const).forEach((suffix, i) => {
|
||||
bookContainers({
|
||||
bookAndClear({
|
||||
suffix,
|
||||
runStamp: stamp,
|
||||
isoSeed: 14_800 + i * 100,
|
||||
forty: SHAPES[suffix].forty,
|
||||
scheduledDate: BOOKING_DAY,
|
||||
});
|
||||
acceptExport(suffix);
|
||||
scheduledDate: BOOKING_DAY, mode: "export"});
|
||||
});
|
||||
|
||||
(["FA", "FC", "FD"] as const).forEach((suffix) => {
|
||||
@@ -570,14 +565,12 @@ describe("G3·S16: an expired hold frees space and fits flips back", { retries:
|
||||
});
|
||||
|
||||
(["HA", "HB"] as const).forEach((suffix, i) => {
|
||||
bookContainers({
|
||||
bookAndClear({
|
||||
suffix,
|
||||
runStamp: stamp,
|
||||
isoSeed: 15_100 + i * 100,
|
||||
forty: SHAPES[suffix].forty,
|
||||
scheduledDate: BOOKING_DAY,
|
||||
});
|
||||
acceptExport(suffix);
|
||||
scheduledDate: BOOKING_DAY, mode: "export"});
|
||||
});
|
||||
|
||||
bookContainers({
|
||||
@@ -695,14 +688,12 @@ describe("G3·S18: a booking larger than the train is refused outright", { retri
|
||||
|
||||
it("split into two 30-wagon bookings, both board — the documented workaround", () => {
|
||||
(["OB1", "OB2"] as const).forEach((suffix, i) => {
|
||||
bookContainers({
|
||||
bookAndClear({
|
||||
suffix,
|
||||
runStamp: stamp,
|
||||
isoSeed: 15_800 + i * 100,
|
||||
forty: 27,
|
||||
scheduledDate: BOOKING_DAY,
|
||||
});
|
||||
acceptExport(suffix);
|
||||
scheduledDate: BOOKING_DAY, mode: "export"});
|
||||
markPaid(suffix);
|
||||
pollAllocations(suffix, 27);
|
||||
});
|
||||
|
||||
@@ -54,6 +54,7 @@ import {
|
||||
import {
|
||||
G1_TRAIN_2,
|
||||
G1_WAGONS,
|
||||
bookAndClear,
|
||||
closeWindowAndRunBatch,
|
||||
configureAndOpenSchedule,
|
||||
expectVerdict,
|
||||
@@ -142,7 +143,7 @@ describe("G4·S19: an import booking splits across two trains", { retries: 0 },
|
||||
it("three bookings arrive, MA first in priority", () => {
|
||||
let isoSeed = 17_100;
|
||||
ORDER.forEach((suffix) => {
|
||||
bookContainers({
|
||||
bookAndClear({
|
||||
suffix,
|
||||
runStamp: stamp,
|
||||
isoSeed,
|
||||
@@ -150,7 +151,6 @@ describe("G4·S19: an import booking splits across two trains", { retries: 0 },
|
||||
scheduledDate: BOOKING_DAY,
|
||||
});
|
||||
isoSeed += SHAPES[suffix].forty;
|
||||
acceptOperation(suffix);
|
||||
});
|
||||
ORDER.forEach((suffix, i) => setPriority(suffix, i + 1));
|
||||
});
|
||||
@@ -229,7 +229,7 @@ describe("G4·S20: the batch prefers a whole placement over a forced split", { r
|
||||
|
||||
let isoSeed = 17_600;
|
||||
ORDER.forEach((suffix) => {
|
||||
bookContainers({
|
||||
bookAndClear({
|
||||
suffix,
|
||||
runStamp: stamp,
|
||||
isoSeed,
|
||||
@@ -237,7 +237,6 @@ describe("G4·S20: the batch prefers a whole placement over a forced split", { r
|
||||
scheduledDate: BOOKING_DAY,
|
||||
});
|
||||
isoSeed += SHAPES[suffix].forty;
|
||||
acceptOperation(suffix);
|
||||
});
|
||||
ORDER.forEach((suffix, i) => setPriority(suffix, i + 1));
|
||||
closeWindowAndRunBatch(T1);
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
import {
|
||||
G1_TRAIN_2,
|
||||
G1_WAGONS,
|
||||
bookAndClear,
|
||||
closeWindowAndRunBatch,
|
||||
configureAndOpenSchedule,
|
||||
expectBoard,
|
||||
@@ -81,7 +82,7 @@ describe("G5·S22: an expired booking moves to the next day intact", { retries:
|
||||
|
||||
let isoSeed = 18_100;
|
||||
ORDER.forEach((suffix) => {
|
||||
bookContainers({
|
||||
bookAndClear({
|
||||
suffix,
|
||||
runStamp: stamp,
|
||||
isoSeed,
|
||||
@@ -89,7 +90,6 @@ describe("G5·S22: an expired booking moves to the next day intact", { retries:
|
||||
scheduledDate: BOOKING_DAY_1,
|
||||
});
|
||||
isoSeed += SHAPES[suffix].forty;
|
||||
acceptOperation(suffix);
|
||||
});
|
||||
ORDER.forEach((suffix, i) => setPriority(suffix, i + 1));
|
||||
closeWindowAndRunBatch(DAY_1);
|
||||
@@ -135,14 +135,13 @@ describe("G5·S22: an expired booking moves to the next day intact", { retries:
|
||||
|
||||
it("day 2 carries RC plus a new 40-wagon customer — FULL at 53/53", () => {
|
||||
seedImportContract({ suffix: "RD", reference: stampedRef("RD") });
|
||||
bookContainers({
|
||||
bookAndClear({
|
||||
suffix: "RD",
|
||||
runStamp: stamp,
|
||||
isoSeed: 18_500,
|
||||
forty: 40,
|
||||
scheduledDate: BOOKING_DAY_2,
|
||||
});
|
||||
acceptOperation("RD");
|
||||
|
||||
closeWindowAndRunBatch(DAY_2);
|
||||
(["RC", "RD"] as const).forEach((suffix) => {
|
||||
@@ -189,30 +188,27 @@ describe("G5·S23: a split's paid part survives the remainder expiring", { retri
|
||||
resetCorridorDay(DAY_1);
|
||||
configureAndOpenSchedule({ departure: DAY_1 });
|
||||
|
||||
bookContainers({
|
||||
bookAndClear({
|
||||
suffix: "SA",
|
||||
runStamp: stamp,
|
||||
isoSeed: 18_800,
|
||||
forty: SHAPES.SA.forty,
|
||||
scheduledDate: BOOKING_DAY_1,
|
||||
});
|
||||
acceptOperation("SA");
|
||||
bookContainers({
|
||||
bookAndClear({
|
||||
suffix: "SB",
|
||||
runStamp: stamp,
|
||||
isoSeed: 18_900,
|
||||
twenty: SHAPES.SB.twenty,
|
||||
scheduledDate: BOOKING_DAY_1,
|
||||
});
|
||||
acceptOperation("SB");
|
||||
bookContainers({
|
||||
bookAndClear({
|
||||
suffix: "SC",
|
||||
runStamp: stamp,
|
||||
isoSeed: 19_000,
|
||||
twenty: SHAPES.SC.twenty,
|
||||
scheduledDate: BOOKING_DAY_1,
|
||||
});
|
||||
acceptOperation("SC");
|
||||
(["SA", "SB", "SC"] as const).forEach((suffix, i) => setPriority(suffix, i + 1));
|
||||
|
||||
closeWindowAndRunBatch(DAY_1);
|
||||
@@ -316,7 +312,7 @@ describe("G5·S24: the waiting list is walked, never skipped silently", { retrie
|
||||
|
||||
let isoSeed = 19_500;
|
||||
ORDER.forEach((suffix) => {
|
||||
bookContainers({
|
||||
bookAndClear({
|
||||
suffix,
|
||||
runStamp: stamp,
|
||||
isoSeed,
|
||||
@@ -324,7 +320,6 @@ describe("G5·S24: the waiting list is walked, never skipped silently", { retrie
|
||||
scheduledDate: BOOKING_DAY,
|
||||
});
|
||||
isoSeed += SHAPES[suffix].forty;
|
||||
acceptOperation(suffix);
|
||||
});
|
||||
ORDER.forEach((suffix, i) => setPriority(suffix, i + 1));
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ import {
|
||||
type ScheduleRow,
|
||||
} from "./import-utils";
|
||||
import {
|
||||
bookAndClear,
|
||||
closeWindowAndRunBatch,
|
||||
configureAndOpenSchedule,
|
||||
} from "./g1-utils";
|
||||
@@ -125,7 +126,7 @@ describe("G6·S25: three bookings alight at three stations", { retries: 0 }, ()
|
||||
|
||||
let isoSeed = 20_100;
|
||||
ORDER.forEach((suffix) => {
|
||||
bookContainers({
|
||||
bookAndClear({
|
||||
suffix,
|
||||
runStamp: stamp,
|
||||
isoSeed,
|
||||
@@ -133,7 +134,6 @@ describe("G6·S25: three bookings alight at three stations", { retries: 0 }, ()
|
||||
scheduledDate: BOOKING_DAY,
|
||||
});
|
||||
isoSeed += SHAPES[suffix].forty;
|
||||
acceptOperation(suffix);
|
||||
});
|
||||
|
||||
closeWindowAndRunBatch(DEPARTURE);
|
||||
@@ -302,14 +302,13 @@ describe("G6·S27: a paid booking with no schedule is not on rails yet", { retri
|
||||
resetCorridorDay(DEPARTURE);
|
||||
configureAndOpenSchedule({ departure: DEPARTURE });
|
||||
|
||||
bookContainers({
|
||||
bookAndClear({
|
||||
suffix: "TR",
|
||||
runStamp: stamp,
|
||||
isoSeed: 21_100,
|
||||
forty: 5,
|
||||
scheduledDate: BOOKING_DAY,
|
||||
});
|
||||
acceptOperation("TR");
|
||||
|
||||
// Accepted into the pool but not yet through the batch: no train.
|
||||
withBooking("TR", (b) => {
|
||||
@@ -368,14 +367,13 @@ describe("G6·S28: an early alighter is ARRIVED while the train runs on", { retr
|
||||
configureAndOpenSchedule({ departure: DEPARTURE });
|
||||
|
||||
(["EA1", "EA2"] as const).forEach((suffix, i) => {
|
||||
bookContainers({
|
||||
bookAndClear({
|
||||
suffix,
|
||||
runStamp: stamp,
|
||||
isoSeed: 21_400 + i * 100,
|
||||
forty: 10,
|
||||
scheduledDate: BOOKING_DAY,
|
||||
});
|
||||
acceptOperation(suffix);
|
||||
});
|
||||
closeWindowAndRunBatch(DEPARTURE);
|
||||
(["EA1", "EA2"] as const).forEach((suffix) => {
|
||||
@@ -445,14 +443,13 @@ describe("G6·S29: out-of-order checkpoint handling", { retries: 0 }, () => {
|
||||
ensureCorridorRoute();
|
||||
resetCorridorDay(DEPARTURE);
|
||||
configureAndOpenSchedule({ departure: DEPARTURE });
|
||||
bookContainers({
|
||||
bookAndClear({
|
||||
suffix: "OO",
|
||||
runStamp: stamp,
|
||||
isoSeed: 21_800,
|
||||
forty: 6,
|
||||
scheduledDate: BOOKING_DAY,
|
||||
});
|
||||
acceptOperation("OO");
|
||||
closeWindowAndRunBatch(DEPARTURE);
|
||||
markPaid("OO");
|
||||
pollAllocations("OO", 6);
|
||||
|
||||
@@ -38,6 +38,7 @@ import {
|
||||
import {
|
||||
G1_TRAIN_2,
|
||||
G1_WAGONS,
|
||||
bookAndClear,
|
||||
closeWindowAndRunBatch,
|
||||
configureAndOpenSchedule,
|
||||
expectVerdict,
|
||||
@@ -92,7 +93,7 @@ describe("G7·S30: a cancelled train freezes a snapshot of what it was", { retri
|
||||
|
||||
let isoSeed = 22_100;
|
||||
ORDER.forEach((suffix) => {
|
||||
bookContainers({
|
||||
bookAndClear({
|
||||
suffix,
|
||||
runStamp: stamp,
|
||||
isoSeed,
|
||||
@@ -100,7 +101,6 @@ describe("G7·S30: a cancelled train freezes a snapshot of what it was", { retri
|
||||
scheduledDate: BOOKING_DAY_1,
|
||||
});
|
||||
isoSeed += SHAPES[suffix].forty;
|
||||
acceptOperation(suffix);
|
||||
});
|
||||
closeWindowAndRunBatch(DAY_1);
|
||||
ORDER.forEach((suffix) => {
|
||||
@@ -220,14 +220,13 @@ describe("G7·S31: a wagon shortage is filled by transfer request", { retries: 0
|
||||
configureAndOpenSchedule({ departure: DEPARTURE });
|
||||
|
||||
(["YA", "YB"] as const).forEach((suffix, i) => {
|
||||
bookContainers({
|
||||
bookAndClear({
|
||||
suffix,
|
||||
runStamp: stamp,
|
||||
isoSeed: 22_600 + i * 100,
|
||||
forty: 10,
|
||||
scheduledDate: BOOKING_DAY,
|
||||
});
|
||||
acceptOperation(suffix);
|
||||
});
|
||||
closeWindowAndRunBatch(DEPARTURE);
|
||||
(["YA", "YB"] as const).forEach((suffix) => {
|
||||
@@ -316,14 +315,13 @@ describe("G7·S32: a failed wagon is replaced and the train stays full", { retri
|
||||
ensureCorridorRoute();
|
||||
resetCorridorDay(DEPARTURE);
|
||||
configureAndOpenSchedule({ departure: DEPARTURE });
|
||||
bookContainers({
|
||||
bookAndClear({
|
||||
suffix: "MW",
|
||||
runStamp: stamp,
|
||||
isoSeed: 23_100,
|
||||
forty: 12,
|
||||
scheduledDate: BOOKING_DAY,
|
||||
});
|
||||
acceptOperation("MW");
|
||||
closeWindowAndRunBatch(DEPARTURE);
|
||||
markPaid("MW");
|
||||
pollAllocations("MW", 12);
|
||||
@@ -403,14 +401,13 @@ describe("G7·S33: an under-filled train dispatches anyway", { retries: 0 }, ()
|
||||
configureAndOpenSchedule({ departure: DEPARTURE });
|
||||
|
||||
(["UA", "UB"] as const).forEach((suffix, i) => {
|
||||
bookContainers({
|
||||
bookAndClear({
|
||||
suffix,
|
||||
runStamp: stamp,
|
||||
isoSeed: 23_400 + i * 100,
|
||||
forty: SHAPES[suffix].forty,
|
||||
scheduledDate: BOOKING_DAY,
|
||||
});
|
||||
acceptOperation(suffix);
|
||||
});
|
||||
closeWindowAndRunBatch(DEPARTURE);
|
||||
(["UA", "UB"] as const).forEach((suffix) => {
|
||||
|
||||
@@ -48,7 +48,11 @@ import {
|
||||
superAdmin,
|
||||
withBooking,
|
||||
} from "./import-utils";
|
||||
import { closeWindowAndRunBatch, configureAndOpenSchedule } from "./g1-utils";
|
||||
import {
|
||||
bookAndClear,
|
||||
closeWindowAndRunBatch,
|
||||
configureAndOpenSchedule,
|
||||
} from "./g1-utils";
|
||||
|
||||
const stamp = String(Date.now());
|
||||
const stampedRef = (suffix: string) => `CTR-IMP-${stamp}-${suffix}`;
|
||||
@@ -74,14 +78,13 @@ function arrangeCustomsBooking(opts: {
|
||||
ensureCorridorRoute();
|
||||
resetCorridorDay(opts.departure);
|
||||
configureAndOpenSchedule({ departure: opts.departure });
|
||||
bookContainers({
|
||||
bookAndClear({
|
||||
suffix: opts.suffix,
|
||||
runStamp: stamp,
|
||||
isoSeed: opts.isoSeed,
|
||||
forty: opts.forty ?? 6,
|
||||
scheduledDate: bookingDay,
|
||||
});
|
||||
acceptOperation(opts.suffix);
|
||||
closeWindowAndRunBatch(opts.departure);
|
||||
markPaid(opts.suffix);
|
||||
pollAllocations(opts.suffix, opts.forty ?? 6);
|
||||
|
||||
@@ -40,7 +40,11 @@ import {
|
||||
withBooking,
|
||||
withSchedule,
|
||||
} from "./import-utils";
|
||||
import { closeWindowAndRunBatch, configureAndOpenSchedule } from "./g1-utils";
|
||||
import {
|
||||
bookAndClear,
|
||||
closeWindowAndRunBatch,
|
||||
configureAndOpenSchedule,
|
||||
} from "./g1-utils";
|
||||
|
||||
const stamp = String(Date.now());
|
||||
const stampedRef = (suffix: string) => `CTR-IMP-${stamp}-${suffix}`;
|
||||
@@ -114,7 +118,7 @@ describe("G9·S38: customers collect with their own trucks", { retries: 0 }, ()
|
||||
|
||||
let isoSeed = 26_100;
|
||||
ORDER.forEach((suffix) => {
|
||||
bookContainers({
|
||||
bookAndClear({
|
||||
suffix,
|
||||
runStamp: stamp,
|
||||
isoSeed,
|
||||
@@ -122,7 +126,6 @@ describe("G9·S38: customers collect with their own trucks", { retries: 0 }, ()
|
||||
scheduledDate: BOOKING_DAY,
|
||||
});
|
||||
isoSeed += SHAPES[suffix].twenty;
|
||||
acceptOperation(suffix);
|
||||
});
|
||||
closeWindowAndRunBatch(DEPARTURE);
|
||||
ORDER.forEach((suffix) => {
|
||||
@@ -273,7 +276,7 @@ describe("G9·S39: last-mile, self-haul and yard pickup on one train", { retries
|
||||
|
||||
let isoSeed = 26_600;
|
||||
ORDER.forEach((suffix) => {
|
||||
bookContainers({
|
||||
bookAndClear({
|
||||
suffix,
|
||||
runStamp: stamp,
|
||||
isoSeed,
|
||||
@@ -281,7 +284,6 @@ describe("G9·S39: last-mile, self-haul and yard pickup on one train", { retries
|
||||
scheduledDate: BOOKING_DAY,
|
||||
});
|
||||
isoSeed += SHAPES[suffix].twenty;
|
||||
acceptOperation(suffix);
|
||||
});
|
||||
closeWindowAndRunBatch(DEPARTURE);
|
||||
ORDER.forEach((suffix) => {
|
||||
|
||||
@@ -725,7 +725,16 @@ export function settleViaGateway(suffix: string) {
|
||||
export function forceReservationExpiry(suffix: string) {
|
||||
withBooking(suffix, (b) =>
|
||||
db(
|
||||
`UPDATE freight.bookings SET payment_deadline = now() - interval '1 second'
|
||||
// A full HOUR into the past, not one second.
|
||||
//
|
||||
// The sweep races the top-up it triggers: promoting a waiting booking
|
||||
// calls extendPaymentPhaseForTopUp (booking-batch.service.ts:2795),
|
||||
// which pushes paymentPhaseEndsAt out — and a deadline only just behind
|
||||
// `now()` can end up on the wrong side of the moved boundary, leaving
|
||||
// the reservation un-flipped while its wagons have already been handed
|
||||
// to the waiting list. An hour is unambiguously overdue under any
|
||||
// extension the top-up applies.
|
||||
`UPDATE freight.bookings SET payment_deadline = now() - interval '1 hour'
|
||||
WHERE id = $1`,
|
||||
[b.id],
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user