mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 14:48:18 +00:00
feat: integration tests
This commit is contained in:
224
integration/src/bulk-import-matrix.it.ts
Normal file
224
integration/src/bulk-import-matrix.it.ts
Normal file
@@ -0,0 +1,224 @@
|
||||
/**
|
||||
* BULK IMPORT edge matrix:
|
||||
* a) a wheat booking on a day with no open window is rejected at submission
|
||||
* b) a sub-corridor booking (NAGAD → MOJO) rides the through-train next to a
|
||||
* DJIB_PORT → KALITY booking — the batch is corridor-aware
|
||||
* c) bulk intercity ride-along (MOJO → KALITY, DOMESTIC, dateless): staff
|
||||
* assign it onto the import train's free leg, the pay window opens, it
|
||||
* pays and links
|
||||
* d) whole-train giant: 4 000 T (58 wagons' worth) alone on a 54-wagon train
|
||||
* → partial offer of the FULL consist (3 780 T); the gateway settlement
|
||||
* applies the split and the train is FULL from ONE booking; the 220 T
|
||||
* outstanding must be rebooked EXACTLY on a later train
|
||||
*/
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import { closeDb, db, gateway, poll } from "./client";
|
||||
import {
|
||||
acceptIntercityOnto,
|
||||
acceptOperation,
|
||||
bookBulk,
|
||||
bookBulkReady,
|
||||
expectDayRefused,
|
||||
bookingFor,
|
||||
bookingRow,
|
||||
clearIntercityBooking,
|
||||
closeBookingWindow,
|
||||
completeDocReview,
|
||||
createSchedule,
|
||||
departureAt,
|
||||
eatDayStr,
|
||||
endPaymentPhase,
|
||||
ensureCorridorRoute,
|
||||
extendPayWindow,
|
||||
forceWindowOpen,
|
||||
payViaGateway,
|
||||
pollAllocations,
|
||||
pollBookingStatus,
|
||||
pollPartialOffer,
|
||||
pollWindow,
|
||||
releaseUnpaidHolds,
|
||||
resetCorridorDay,
|
||||
seedTenantContracts,
|
||||
} from "./flows";
|
||||
|
||||
const DEPARTURE = departureAt(25);
|
||||
const BOOKING_DAY = eatDayStr(DEPARTURE);
|
||||
const GIANT_DEPARTURE = departureAt(26);
|
||||
const GIANT_DAY = eatDayStr(GIANT_DEPARTURE);
|
||||
const REMAINDER_DEPARTURE = departureAt(27);
|
||||
const REMAINDER_DAY = eatDayStr(REMAINDER_DEPARTURE);
|
||||
const NO_WINDOW_DAY = eatDayStr(departureAt(29)); // no schedule exists there
|
||||
const STAMP = String(Date.now());
|
||||
|
||||
describe("bulk import matrix: gates, sub-corridor, ride-along, whole-train giant", () => {
|
||||
let contracts: Map<string, string>;
|
||||
let scheduleId: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
await gateway.reset();
|
||||
await releaseUnpaidHolds();
|
||||
await ensureCorridorRoute();
|
||||
await resetCorridorDay(DEPARTURE);
|
||||
await resetCorridorDay(GIANT_DEPARTURE);
|
||||
await resetCorridorDay(REMAINDER_DEPARTURE);
|
||||
|
||||
contracts = await seedTenantContracts(STAMP, [
|
||||
{ suffix: "BM1", freight: "BULK" },
|
||||
{ suffix: "BMNW", freight: "BULK" },
|
||||
{ suffix: "BMSUB", freight: "BULK", originCode: "NAGAD", destCode: "MOJO" },
|
||||
{ suffix: "BMIC", freight: "BULK", direction: "DOMESTIC", originCode: "MOJO", destCode: "KALITY" },
|
||||
{ suffix: "BMG", freight: "BULK" },
|
||||
]);
|
||||
|
||||
scheduleId = (
|
||||
await createSchedule({
|
||||
departure: DEPARTURE,
|
||||
kind: "bulk",
|
||||
locoPair: ["LOCO-IMP-25", "LOCO-IMP-26"],
|
||||
})
|
||||
).id;
|
||||
await forceWindowOpen(scheduleId, 60);
|
||||
}, 1_800_000);
|
||||
|
||||
afterAll(closeDb);
|
||||
|
||||
it("refuses a shipment day with no open window", async () => {
|
||||
const res = await expectDayRefused({
|
||||
contractId: contracts.get("BMNW")!,
|
||||
tons: 140,
|
||||
scheduledDate: NO_WINDOW_DAY,
|
||||
});
|
||||
expect(res.status).toBeGreaterThanOrEqual(400);
|
||||
// Wording differs by gate: the day probe answers "No departures available
|
||||
// on the selected day", the route probe "the import booking window …".
|
||||
expect(JSON.stringify(res.body)).toMatch(/booking window|no departures/i);
|
||||
});
|
||||
|
||||
it("a through booking and a NAGAD→MOJO sub-corridor booking share the train", async () => {
|
||||
const bm1 = await bookBulkReady({
|
||||
contractId: contracts.get("BM1")!,
|
||||
tons: 1400,
|
||||
scheduledDate: BOOKING_DAY,
|
||||
});
|
||||
const sub = await bookBulkReady({
|
||||
contractId: contracts.get("BMSUB")!,
|
||||
tons: 700,
|
||||
scheduledDate: BOOKING_DAY,
|
||||
});
|
||||
|
||||
await closeBookingWindow(scheduleId);
|
||||
await completeDocReview(scheduleId);
|
||||
for (const id of [bm1, sub]) {
|
||||
await pollBookingStatus(id, ["SELECTED_FOR_BATCH", "AWAITING_PAYMENT"]);
|
||||
}
|
||||
|
||||
await extendPayWindow(scheduleId, [bm1, sub]);
|
||||
await payViaGateway(bm1);
|
||||
await pollAllocations(bm1, 20);
|
||||
await payViaGateway(sub);
|
||||
await pollAllocations(sub, 10);
|
||||
|
||||
expect((await bookingRow(bm1)).train_schedule_id).toBe(scheduleId);
|
||||
expect((await bookingRow(sub)).train_schedule_id).toBe(scheduleId);
|
||||
});
|
||||
|
||||
it("a dateless DOMESTIC ride-along boards the import train's free leg", async () => {
|
||||
// 140 T = 2 wagons MOJO → KALITY, no shipment day of its own.
|
||||
const res = await bookBulk({ contractId: contracts.get("BMIC")!, tons: 140 });
|
||||
expect(res.status, JSON.stringify(res.body)).toBeLessThanOrEqual(201);
|
||||
const ic = (await bookingFor(contracts.get("BMIC")!)).id;
|
||||
await clearIntercityBooking(ic);
|
||||
|
||||
await acceptIntercityOnto(scheduleId, ic);
|
||||
await pollBookingStatus(ic, ["SELECTED_FOR_BATCH", "AWAITING_PAYMENT"]);
|
||||
expect((await bookingRow(ic)).payment_deadline, "ride-along pay window opened").toBeTruthy();
|
||||
|
||||
await payViaGateway(ic);
|
||||
// Settlement UNPINS a paid ride-along back to the ride-along pool — staff
|
||||
// then place it on the train they want (acceptIntercity's `paid` branch,
|
||||
// booking-batch.service.ts). The Cypress twin never sees this: its staff
|
||||
// mark-paid shortcut leaves the reservation pinned.
|
||||
expect(
|
||||
(await bookingRow(ic)).train_schedule_id,
|
||||
"paid ride-along returns to the pool",
|
||||
).toBeNull();
|
||||
|
||||
await acceptIntercityOnto(scheduleId, ic);
|
||||
await poll(
|
||||
"ride-along linked to the import 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 },
|
||||
);
|
||||
const [link] = await db<{ n: string }>(
|
||||
`SELECT count(*)::text AS n FROM freight.train_schedule_bookings
|
||||
WHERE booking_id = $1 AND train_schedule_id = $2 AND deleted_at IS NULL`,
|
||||
[ic, scheduleId],
|
||||
);
|
||||
expect(Number(link.n), "link row").toBe(1);
|
||||
});
|
||||
|
||||
it("a 4 000 T giant alone gets a FULL-consist partial offer and fills the train", async () => {
|
||||
const giantSchedule = await createSchedule({
|
||||
departure: GIANT_DEPARTURE,
|
||||
kind: "bulk",
|
||||
locoPair: ["LOCO-IMP-27", "LOCO-IMP-28"],
|
||||
});
|
||||
await forceWindowOpen(giantSchedule.id, 45);
|
||||
|
||||
const bmg = await bookBulkReady({
|
||||
contractId: contracts.get("BMG")!,
|
||||
tons: 4000,
|
||||
scheduledDate: GIANT_DAY,
|
||||
});
|
||||
await closeBookingWindow(giantSchedule.id);
|
||||
await completeDocReview(giantSchedule.id);
|
||||
await pollBookingStatus(bmg, ["SELECTED_FOR_BATCH", "AWAITING_PAYMENT"]);
|
||||
|
||||
const offer = await pollPartialOffer(bmg);
|
||||
expect(Number(offer.offered_wagons), "offered the whole consist").toBe(54);
|
||||
|
||||
await payViaGateway(bmg);
|
||||
await pollAllocations(bmg, 54);
|
||||
expect((await bookingRow(bmg)).is_split, "BMG is split").toBe(true);
|
||||
|
||||
await endPaymentPhase(giantSchedule.id);
|
||||
await pollWindow(
|
||||
giantSchedule.id,
|
||||
(s) => s.booking_window_status === "FULL" && s.window_phase === "DONE",
|
||||
"FULL from one booking",
|
||||
);
|
||||
});
|
||||
|
||||
it("the giant's 220 T outstanding must be rebooked EXACTLY on a later train", async () => {
|
||||
const remainder = await createSchedule({
|
||||
departure: REMAINDER_DEPARTURE,
|
||||
kind: "bulk",
|
||||
locoPair: ["LOCO-IMP-13", "LOCO-IMP-14"],
|
||||
});
|
||||
await forceWindowOpen(remainder.id, 45);
|
||||
|
||||
const wrong = await bookBulk({
|
||||
contractId: contracts.get("BMG")!,
|
||||
tons: 100,
|
||||
scheduledDate: REMAINDER_DAY,
|
||||
});
|
||||
expect(wrong.status).toBeGreaterThanOrEqual(400);
|
||||
expect(JSON.stringify(wrong.body)).toMatch(/must take the whole/i);
|
||||
|
||||
const exact = await bookBulk({
|
||||
contractId: contracts.get("BMG")!,
|
||||
tons: 220,
|
||||
scheduledDate: REMAINDER_DAY,
|
||||
});
|
||||
expect(exact.status, JSON.stringify(exact.body)).toBeLessThanOrEqual(201);
|
||||
await acceptOperationIfPossible(contracts.get("BMG")!);
|
||||
});
|
||||
});
|
||||
|
||||
/** The remainder booking only needs to exist; walking its gate is out of scope. */
|
||||
async function acceptOperationIfPossible(contractId: string) {
|
||||
const booking = await bookingFor(contractId);
|
||||
if (booking.status === "OPERATION_REQUEST_PENDING") await acceptOperation(booking.id);
|
||||
}
|
||||
Reference in New Issue
Block a user