mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 00:52:50 +00:00
142 lines
4.3 KiB
TypeScript
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);
|
|
});
|