Files
edr-platform/integration/src/bulk-b2-per-item-floor.it.ts
2026-08-03 10:51:43 +00:00

142 lines
4.3 KiB
TypeScript

/**
* BULK B2 — break-bulk PER_ITEM wagon math on the CW4 fleet (70 T capacity).
*
* Cargo from seed-bulk-items.sql:
* E2E_IMP_AUTO automobiles — items-per-wagon floor of 4
* E2E_IMP_MACHINE machinery — no floor, tonnage-only fallback
*
* Three verdicts of the wagon-demand rule, end to end:
* BA1 16 autos @2.5 T (40 T) → the floor binds: 4 wagons (tonnage said 1)
* BA2 12 machines @20 T (240 T) → tonnage binds: 3/wagon → 4 wagons
* BA3 216 autos (540 T) → exactly 54 wagons: FULL from one booking, no split
*/
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { closeDb, gateway } from "./client";
import {
bookBulkItemsReady,
bookingRow,
closeBookingWindow,
completeDocReview,
createSchedule,
departureAt,
eatDayStr,
endPaymentPhase,
ensureCorridorRoute,
extendPayWindow,
expectWagonType,
forceWindowOpen,
payViaGateway,
pollAllocations,
pollBookingStatus,
pollWindow,
releaseUnpaidHolds,
resetCorridorDay,
seedTenantContracts,
} from "./flows";
const DEPARTURE = departureAt(41);
const BOOKING_DAY = eatDayStr(DEPARTURE);
const FULL_DEPARTURE = departureAt(42);
const FULL_DAY = eatDayStr(FULL_DEPARTURE);
const STAMP = String(Date.now());
describe("bulk b2: PER_ITEM floor vs tonnage wagon math", () => {
const booking = new Map<string, string>();
let contracts: Map<string, string>;
let scheduleId: string;
beforeAll(async () => {
await gateway.reset();
await releaseUnpaidHolds();
await ensureCorridorRoute();
await resetCorridorDay(DEPARTURE);
await resetCorridorDay(FULL_DEPARTURE);
contracts = await seedTenantContracts(
STAMP,
["BA1", "BA2", "BA3"].map((suffix) => ({ suffix, freight: "BULK" as const })),
);
scheduleId = (
await createSchedule({
departure: DEPARTURE,
kind: "bulk",
locoPair: ["LOCO-IMP-15", "LOCO-IMP-16"],
})
).id;
await forceWindowOpen(scheduleId, 60);
booking.set(
"BA1",
await bookBulkItemsReady({
contractId: contracts.get("BA1")!,
cargoCode: "E2E_IMP_AUTO",
items: 16,
tons: 40,
scheduledDate: BOOKING_DAY,
}),
);
booking.set(
"BA2",
await bookBulkItemsReady({
contractId: contracts.get("BA2")!,
cargoCode: "E2E_IMP_MACHINE",
items: 12,
tons: 240,
scheduledDate: BOOKING_DAY,
}),
);
}, 1_800_000);
afterAll(closeDb);
it("the 4-per-wagon floor binds for 16 autos → 4 CW4 wagons, not 1", async () => {
await closeBookingWindow(scheduleId);
await completeDocReview(scheduleId);
for (const suffix of ["BA1", "BA2"]) {
await pollBookingStatus(booking.get(suffix)!, ["SELECTED_FOR_BATCH", "AWAITING_PAYMENT"]);
}
await extendPayWindow(scheduleId, [booking.get("BA1")!, booking.get("BA2")!]);
await payViaGateway(booking.get("BA1")!);
await pollAllocations(booking.get("BA1")!, 4);
await expectWagonType(booking.get("BA1")!, "CW4", 4);
});
it("machinery has no floor — 12 items @20 T take 4 wagons on tonnage alone", async () => {
await payViaGateway(booking.get("BA2")!);
await pollAllocations(booking.get("BA2")!, 4);
await expectWagonType(booking.get("BA2")!, "CW4", 4);
});
it("216 autos = exactly 54 wagons: FULL from one break-bulk booking, no split", async () => {
const fullSchedule = await createSchedule({
departure: FULL_DEPARTURE,
kind: "bulk",
locoPair: ["LOCO-IMP-25", "LOCO-IMP-26"],
});
await forceWindowOpen(fullSchedule.id, 45);
const ba3 = await bookBulkItemsReady({
contractId: contracts.get("BA3")!,
cargoCode: "E2E_IMP_AUTO",
items: 216,
tons: 540,
scheduledDate: FULL_DAY,
});
await closeBookingWindow(fullSchedule.id);
await completeDocReview(fullSchedule.id);
await pollBookingStatus(ba3, ["SELECTED_FOR_BATCH", "AWAITING_PAYMENT"]);
await payViaGateway(ba3);
await pollAllocations(ba3, 54);
expect((await bookingRow(ba3)).is_split, "BA3 rides whole, not split").not.toBe(true);
await endPaymentPhase(fullSchedule.id);
await pollWindow(
fullSchedule.id,
(s) => s.booking_window_status === "FULL" && s.window_phase === "DONE",
"FULL + DONE",
);
}, 900_000);
});