mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-27 14:20:58 +00:00
170 lines
5.8 KiB
TypeScript
170 lines
5.8 KiB
TypeScript
/**
|
|
* BULK EXPORT — six wheat bookings fill the 54-wagon CW4 train on the reversed
|
|
* corridor KALITY → MOJO → E2E_AWASH → DIRE_DAWA → NAGAD → DJIB_PORT, inside
|
|
* the ONE FCFS export window, then the full life of the train to Djibouti Port
|
|
* and the export customs tail.
|
|
*
|
|
* Export is FCFS: the staff accept IS the reservation — there is no batch — and
|
|
* a pay deadline may never outlive the window close. Both are asserted here.
|
|
*
|
|
* 70 T per CW4 wagon — Σ = 54 wagons / 3 780 T:
|
|
* XBF1 customs USD 560 T = 8 w · XBF2 customs ETB 420 T = 6 w
|
|
* XBF3 self ETB 420 T = 6 w · XBF4 self ETB 420 T = 6 w
|
|
* XBF5 customs USD 1 540 T = 22 w · XBF6 self USD 420 T = 6 w
|
|
*/
|
|
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
import { closeDb, gateway } from "./client";
|
|
import {
|
|
EXP_DEST,
|
|
EXP_ORIGIN,
|
|
allocatedWagons,
|
|
bookBulkReady,
|
|
bookingRow,
|
|
createSchedule,
|
|
departureAt,
|
|
dispatchSchedule,
|
|
eatDayStr,
|
|
ensureExportRoute,
|
|
extendPayWindow,
|
|
expectMilestoneDone,
|
|
finalizeSchedule,
|
|
forceWindowOpen,
|
|
gatePassGranted,
|
|
invoiceForBooking,
|
|
milestoneCount,
|
|
payViaGateway,
|
|
pollAllocations,
|
|
pollBookingStatus,
|
|
pollWindow,
|
|
releaseUnpaidHolds,
|
|
resetCorridorDay,
|
|
runCorridor,
|
|
scheduleRow,
|
|
seedTenantContracts,
|
|
closeT1,
|
|
completeMilestone,
|
|
uploadTransportDocument,
|
|
} from "./flows";
|
|
|
|
const DEPARTURE = departureAt(30);
|
|
const BOOKING_DAY = eatDayStr(DEPARTURE);
|
|
const STAMP = String(Date.now());
|
|
|
|
const BOOKINGS = [
|
|
{ suffix: "XBF1", customs: true, currency: "USD" as const, tons: 560, wagons: 8 },
|
|
{ suffix: "XBF2", customs: true, currency: "ETB" as const, tons: 420, wagons: 6 },
|
|
{ suffix: "XBF3", customs: false, currency: "ETB" as const, tons: 420, wagons: 6 },
|
|
{ suffix: "XBF4", customs: false, currency: "ETB" as const, tons: 420, wagons: 6 },
|
|
{ suffix: "XBF5", customs: true, currency: "USD" as const, tons: 1540, wagons: 22 },
|
|
{ suffix: "XBF6", customs: false, currency: "USD" as const, tons: 420, wagons: 6 },
|
|
];
|
|
const CUSTOMS = BOOKINGS.filter((b) => b.customs);
|
|
const SELF_CLEAR = BOOKINGS.filter((b) => !b.customs);
|
|
|
|
describe("bulk export: six wheat bookings fill the 54-wagon CW4 train (FCFS)", () => {
|
|
const booking = new Map<string, string>();
|
|
let scheduleId: string;
|
|
|
|
beforeAll(async () => {
|
|
await gateway.reset();
|
|
await releaseUnpaidHolds();
|
|
await ensureExportRoute();
|
|
await resetCorridorDay(DEPARTURE, EXP_ORIGIN, EXP_DEST);
|
|
|
|
const contracts = await seedTenantContracts(
|
|
STAMP,
|
|
BOOKINGS.map((b) => ({
|
|
suffix: b.suffix,
|
|
currency: b.currency,
|
|
customs: b.customs,
|
|
freight: "BULK" as const,
|
|
direction: "EXPORT" as const,
|
|
})),
|
|
);
|
|
scheduleId = (
|
|
await createSchedule({
|
|
departure: DEPARTURE,
|
|
kind: "bulk",
|
|
locoPair: ["LOCO-EXP-1", "LOCO-EXP-2"],
|
|
originCode: EXP_ORIGIN,
|
|
destCode: EXP_DEST,
|
|
})
|
|
).id;
|
|
await forceWindowOpen(scheduleId, 60);
|
|
|
|
for (const b of BOOKINGS) {
|
|
booking.set(
|
|
b.suffix,
|
|
await bookBulkReady({
|
|
contractId: contracts.get(b.suffix)!,
|
|
tons: b.tons,
|
|
scheduledDate: BOOKING_DAY,
|
|
mode: "export",
|
|
customs: b.customs,
|
|
}),
|
|
);
|
|
}
|
|
}, 1_800_000);
|
|
|
|
afterAll(closeDb);
|
|
|
|
it("each accept reserved immediately, with a deadline clamped to the window close", async () => {
|
|
const schedule = await scheduleRow(scheduleId);
|
|
const closesAt = new Date(String(schedule.window_closes_at)).getTime();
|
|
for (const b of BOOKINGS) {
|
|
const row = await bookingRow(booking.get(b.suffix)!);
|
|
expect(row.payment_deadline, `${b.suffix} pay deadline`).toBeTruthy();
|
|
expect(
|
|
new Date(row.payment_deadline!).getTime(),
|
|
`${b.suffix} deadline never outlives the window close`,
|
|
).toBeLessThanOrEqual(closesAt);
|
|
const invoice = await invoiceForBooking(booking.get(b.suffix)!);
|
|
expect(invoice.currency, `${b.suffix} invoice currency`).toBe(b.currency);
|
|
}
|
|
});
|
|
|
|
it("all six pay — 54/54 allocated, the export window flips FULL, staff finalize", async () => {
|
|
await extendPayWindow(scheduleId, [...booking.values()]);
|
|
for (const b of BOOKINGS) {
|
|
await payViaGateway(booking.get(b.suffix)!);
|
|
await pollAllocations(booking.get(b.suffix)!, b.wagons);
|
|
}
|
|
await pollWindow(scheduleId, (s) => s.booking_window_status === "FULL", "export window FULL");
|
|
expect(await allocatedWagons(scheduleId), "54 wagons allocated").toBe(54);
|
|
|
|
await finalizeSchedule(scheduleId);
|
|
await pollWindow(scheduleId, (s) => s.status === "SCHEDULED", "SCHEDULED");
|
|
});
|
|
|
|
it("gate pass + transport documents, then the train runs the corridor to the port", async () => {
|
|
await gatePassGranted(scheduleId);
|
|
for (const b of CUSTOMS) {
|
|
const res = await uploadTransportDocument(booking.get(b.suffix)!);
|
|
expect(res.status, `${b.suffix} transport document`).toBeLessThanOrEqual(201);
|
|
}
|
|
|
|
await dispatchSchedule(scheduleId);
|
|
await pollWindow(scheduleId, (s) => s.status === "DISPATCHED", "DISPATCHED");
|
|
await runCorridor(scheduleId);
|
|
for (const b of BOOKINGS) await pollBookingStatus(booking.get(b.suffix)!, "ARRIVED", 20);
|
|
});
|
|
|
|
it("GL Djibouti closes the export tail on every customs booking", async () => {
|
|
for (const b of CUSTOMS) {
|
|
const id = booking.get(b.suffix)!;
|
|
await closeT1(id);
|
|
await completeMilestone(id, "OFFLOADED");
|
|
await expectMilestoneDone(id, "T1_CLOSED");
|
|
await expectMilestoneDone(id, "OFFLOADED");
|
|
}
|
|
});
|
|
|
|
it("the self-clearing bookings arrived clean — no customs tail", async () => {
|
|
for (const b of SELF_CLEAR) {
|
|
const id = booking.get(b.suffix)!;
|
|
expect((await bookingRow(id)).status, `${b.suffix} final status`).toBe("ARRIVED");
|
|
expect(await milestoneCount(id, "T1_CLOSED"), `${b.suffix} has no T1 tail`).toBe(0);
|
|
}
|
|
});
|
|
});
|