mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 04:50:54 +00:00
remove reopen delay minutes from global rules and update related types
- Removed the field from and related components. - Updated to reflect the removal of the reopen delay input field. - Modified to include new train number fields: and . - Added interface to manage active schedules with trade direction. - Introduced interface to track wagon shortages in bookings. - Updated logic to ensure consistent UI state representation. - Created migrations to drop the column and add and columns to the table. - Added tests for the new booking window display logic and wagon planning functionality.
This commit is contained in:
@@ -0,0 +1,264 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { bookingWindowUiState } from "@edr/ui-common";
|
||||
import type {
|
||||
BookingWindowStateInput,
|
||||
BookingWindowUiState,
|
||||
} from "@edr/ui-common";
|
||||
|
||||
/**
|
||||
* Scenario table for the shared badge/countdown state. This is the logic that
|
||||
* previously let a full export train show an "Upcoming" badge above a live
|
||||
* "Window closes in …" countdown — every row asserts badge kind, countdown
|
||||
* target, and bookability TOGETHER, so they can never disagree again.
|
||||
*/
|
||||
|
||||
const OPENS = "2026-07-26T05:00:00.000Z";
|
||||
const CLOSES = "2026-07-27T05:00:00.000Z";
|
||||
const DOC_ENDS = "2026-07-24T08:30:00.000Z";
|
||||
const PAY_ENDS = "2026-07-24T09:30:00.000Z";
|
||||
|
||||
/** A full row with every timestamp present; scenarios override what they test. */
|
||||
function row(over: Partial<BookingWindowStateInput>): BookingWindowStateInput {
|
||||
return {
|
||||
windowPhase: "OPEN",
|
||||
bookingWindowStatus: "OPEN",
|
||||
windowOpensAt: OPENS,
|
||||
windowClosesAt: CLOSES,
|
||||
docReviewEndsAt: DOC_ENDS,
|
||||
paymentPhaseEndsAt: PAY_ENDS,
|
||||
...over,
|
||||
};
|
||||
}
|
||||
|
||||
interface Scenario {
|
||||
name: string;
|
||||
input: BookingWindowStateInput;
|
||||
expected: BookingWindowUiState;
|
||||
}
|
||||
|
||||
const scenarios: Scenario[] = [
|
||||
// ---- export FCFS lifecycle -------------------------------------------------
|
||||
{
|
||||
name: "export announced, before lead window (PRE_WINDOW/CLOSED)",
|
||||
input: row({ windowPhase: "PRE_WINDOW", bookingWindowStatus: "CLOSED" }),
|
||||
expected: { kind: "PRE_WINDOW", countdownTo: OPENS, isBookable: false },
|
||||
},
|
||||
{
|
||||
name: "export window open, space left (OPEN/OPEN)",
|
||||
input: row({}),
|
||||
expected: { kind: "OPEN", countdownTo: CLOSES, isBookable: true },
|
||||
},
|
||||
{
|
||||
name: "export filled mid-window (OPEN/FULL) — the reported bug",
|
||||
input: row({ bookingWindowStatus: "FULL" }),
|
||||
expected: { kind: "FULL", countdownTo: null, isBookable: false },
|
||||
},
|
||||
{
|
||||
name: "export space freed after an expiry cleared FULL (OPEN/OPEN again)",
|
||||
input: row({}),
|
||||
expected: { kind: "OPEN", countdownTo: CLOSES, isBookable: true },
|
||||
},
|
||||
{
|
||||
name: "export window over (DONE/CLOSED)",
|
||||
input: row({ windowPhase: "DONE", bookingWindowStatus: "CLOSED" }),
|
||||
expected: { kind: "CLOSED", countdownTo: null, isBookable: false },
|
||||
},
|
||||
{
|
||||
name: "export departed while full (DONE/FULL)",
|
||||
input: row({ windowPhase: "DONE", bookingWindowStatus: "FULL" }),
|
||||
expected: { kind: "CLOSED", countdownTo: null, isBookable: false },
|
||||
},
|
||||
|
||||
// ---- import daily cycle ----------------------------------------------------
|
||||
{
|
||||
name: "import before booking day (PRE_WINDOW/CLOSED)",
|
||||
input: row({ windowPhase: "PRE_WINDOW", bookingWindowStatus: "CLOSED" }),
|
||||
expected: { kind: "PRE_WINDOW", countdownTo: OPENS, isBookable: false },
|
||||
},
|
||||
{
|
||||
name: "import window open (OPEN/OPEN)",
|
||||
input: row({}),
|
||||
expected: { kind: "OPEN", countdownTo: CLOSES, isBookable: true },
|
||||
},
|
||||
{
|
||||
name: "import window closed, staff reviewing docs (DOC_REVIEW/CLOSED)",
|
||||
input: row({ windowPhase: "DOC_REVIEW", bookingWindowStatus: "CLOSED" }),
|
||||
expected: { kind: "DOC_REVIEW", countdownTo: DOC_ENDS, isBookable: false },
|
||||
},
|
||||
{
|
||||
name: "import payment phase, selected customers paying (PAYMENT/CLOSED)",
|
||||
input: row({ windowPhase: "PAYMENT", bookingWindowStatus: "CLOSED" }),
|
||||
expected: { kind: "PAYMENT", countdownTo: PAY_ENDS, isBookable: false },
|
||||
},
|
||||
{
|
||||
name: "import batch tentatively filled the train (PAYMENT/FULL) — phase wins, unpaid may still free space",
|
||||
input: row({ windowPhase: "PAYMENT", bookingWindowStatus: "FULL" }),
|
||||
expected: { kind: "PAYMENT", countdownTo: PAY_ENDS, isBookable: false },
|
||||
},
|
||||
{
|
||||
name: "import doc review while flag already FULL (DOC_REVIEW/FULL) — phase wins",
|
||||
input: row({ windowPhase: "DOC_REVIEW", bookingWindowStatus: "FULL" }),
|
||||
expected: { kind: "DOC_REVIEW", countdownTo: DOC_ENDS, isBookable: false },
|
||||
},
|
||||
{
|
||||
name: "import reopen cycle scheduled (PRE_WINDOW/CLOSED, cycle 2)",
|
||||
input: row({ windowPhase: "PRE_WINDOW", bookingWindowStatus: "CLOSED" }),
|
||||
expected: { kind: "PRE_WINDOW", countdownTo: OPENS, isBookable: false },
|
||||
},
|
||||
{
|
||||
name: "import reopen refused while train still FULL (PRE_WINDOW/FULL)",
|
||||
input: row({ windowPhase: "PRE_WINDOW", bookingWindowStatus: "FULL" }),
|
||||
expected: { kind: "FULL", countdownTo: null, isBookable: false },
|
||||
},
|
||||
{
|
||||
name: "import train full and finalized (DONE/FULL)",
|
||||
input: row({ windowPhase: "DONE", bookingWindowStatus: "FULL" }),
|
||||
expected: { kind: "CLOSED", countdownTo: null, isBookable: false },
|
||||
},
|
||||
{
|
||||
name: "import no cycle fits before departure (DONE/CLOSED)",
|
||||
input: row({ windowPhase: "DONE", bookingWindowStatus: "CLOSED" }),
|
||||
expected: { kind: "CLOSED", countdownTo: null, isBookable: false },
|
||||
},
|
||||
{
|
||||
name: "legacy closed-for-the-day row (CLOSED_FOR_DAY/CLOSED)",
|
||||
input: row({ windowPhase: "CLOSED_FOR_DAY", bookingWindowStatus: "CLOSED" }),
|
||||
expected: { kind: "CLOSED", countdownTo: null, isBookable: false },
|
||||
},
|
||||
{
|
||||
name: "legacy closed-for-the-day row while full (CLOSED_FOR_DAY/FULL)",
|
||||
input: row({ windowPhase: "CLOSED_FOR_DAY", bookingWindowStatus: "FULL" }),
|
||||
expected: { kind: "CLOSED", countdownTo: null, isBookable: false },
|
||||
},
|
||||
|
||||
// ---- desync / stale rows ---------------------------------------------------
|
||||
{
|
||||
name: "phase OPEN but desk flag CLOSED (desync) — closed, no countdown",
|
||||
input: row({ bookingWindowStatus: "CLOSED" }),
|
||||
expected: { kind: "CLOSED", countdownTo: null, isBookable: false },
|
||||
},
|
||||
{
|
||||
name: "dispatched train stuck at OPEN/CLOSED (tick skips non-scheduled rows)",
|
||||
input: row({ bookingWindowStatus: "CLOSED" }),
|
||||
expected: { kind: "CLOSED", countdownTo: null, isBookable: false },
|
||||
},
|
||||
{
|
||||
name: "FULL flag with no phase at all (legacy pre-window-engine row)",
|
||||
input: row({ windowPhase: null, bookingWindowStatus: "FULL" }),
|
||||
expected: { kind: "FULL", countdownTo: null, isBookable: false },
|
||||
},
|
||||
{
|
||||
name: "legacy row, no phase, desk open (null/OPEN) — not phase-driven, shows closed",
|
||||
input: row({ windowPhase: null }),
|
||||
expected: { kind: "CLOSED", countdownTo: null, isBookable: false },
|
||||
},
|
||||
{
|
||||
name: "unknown future phase value — safe fallback to closed",
|
||||
input: row({ windowPhase: "SOMETHING_NEW" }),
|
||||
expected: { kind: "CLOSED", countdownTo: null, isBookable: false },
|
||||
},
|
||||
|
||||
// ---- missing timestamps (no countdown, badge still right) -------------------
|
||||
{
|
||||
name: "PRE_WINDOW without an opens-at timestamp",
|
||||
input: row({
|
||||
windowPhase: "PRE_WINDOW",
|
||||
bookingWindowStatus: "CLOSED",
|
||||
windowOpensAt: null,
|
||||
}),
|
||||
expected: { kind: "PRE_WINDOW", countdownTo: null, isBookable: false },
|
||||
},
|
||||
{
|
||||
name: "OPEN without a closes-at timestamp",
|
||||
input: row({ windowClosesAt: null }),
|
||||
expected: { kind: "OPEN", countdownTo: null, isBookable: true },
|
||||
},
|
||||
{
|
||||
name: "DOC_REVIEW without an ends-at timestamp",
|
||||
input: row({
|
||||
windowPhase: "DOC_REVIEW",
|
||||
bookingWindowStatus: "CLOSED",
|
||||
docReviewEndsAt: null,
|
||||
}),
|
||||
expected: { kind: "DOC_REVIEW", countdownTo: null, isBookable: false },
|
||||
},
|
||||
{
|
||||
name: "PAYMENT without an ends-at timestamp",
|
||||
input: row({
|
||||
windowPhase: "PAYMENT",
|
||||
bookingWindowStatus: "CLOSED",
|
||||
paymentPhaseEndsAt: null,
|
||||
}),
|
||||
expected: { kind: "PAYMENT", countdownTo: null, isBookable: false },
|
||||
},
|
||||
{
|
||||
name: "row with every field null",
|
||||
input: {
|
||||
windowPhase: null,
|
||||
bookingWindowStatus: null,
|
||||
windowOpensAt: null,
|
||||
windowClosesAt: null,
|
||||
docReviewEndsAt: null,
|
||||
paymentPhaseEndsAt: null,
|
||||
},
|
||||
expected: { kind: "CLOSED", countdownTo: null, isBookable: false },
|
||||
},
|
||||
{
|
||||
name: "row with every field undefined (structural minimum)",
|
||||
input: {},
|
||||
expected: { kind: "CLOSED", countdownTo: null, isBookable: false },
|
||||
},
|
||||
|
||||
// ---- countdown targets track the right deadline per phase -------------------
|
||||
{
|
||||
name: "PRE_WINDOW counts to opens-at, not closes-at",
|
||||
input: row({ windowPhase: "PRE_WINDOW", bookingWindowStatus: "CLOSED" }),
|
||||
expected: { kind: "PRE_WINDOW", countdownTo: OPENS, isBookable: false },
|
||||
},
|
||||
{
|
||||
name: "OPEN counts to closes-at, not doc review",
|
||||
input: row({}),
|
||||
expected: { kind: "OPEN", countdownTo: CLOSES, isBookable: true },
|
||||
},
|
||||
{
|
||||
name: "DOC_REVIEW counts to review end, not payment end",
|
||||
input: row({ windowPhase: "DOC_REVIEW", bookingWindowStatus: "CLOSED" }),
|
||||
expected: { kind: "DOC_REVIEW", countdownTo: DOC_ENDS, isBookable: false },
|
||||
},
|
||||
{
|
||||
name: "PAYMENT counts to payment end, not window close",
|
||||
input: row({ windowPhase: "PAYMENT", bookingWindowStatus: "CLOSED" }),
|
||||
expected: { kind: "PAYMENT", countdownTo: PAY_ENDS, isBookable: false },
|
||||
},
|
||||
];
|
||||
|
||||
describe("bookingWindowUiState", () => {
|
||||
it.each(scenarios)("$name", ({ input, expected }) => {
|
||||
expect(bookingWindowUiState(input)).toEqual(expected);
|
||||
});
|
||||
|
||||
it("never yields a countdown on a non-bookable FULL state, whatever else is set", () => {
|
||||
for (const phase of ["OPEN", "PRE_WINDOW", null, "ANYTHING"]) {
|
||||
const state = bookingWindowUiState(row({ windowPhase: phase, bookingWindowStatus: "FULL" }));
|
||||
expect(state.kind).toBe("FULL");
|
||||
expect(state.countdownTo).toBeNull();
|
||||
expect(state.isBookable).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
it("is bookable ONLY when phase and desk flag are both OPEN", () => {
|
||||
const combos: Array<[string | null, string | null]> = [];
|
||||
for (const phase of ["PRE_WINDOW", "OPEN", "DOC_REVIEW", "PAYMENT", "DONE", "CLOSED_FOR_DAY", null]) {
|
||||
for (const status of ["OPEN", "CLOSED", "FULL", null]) {
|
||||
combos.push([phase, status]);
|
||||
}
|
||||
}
|
||||
for (const [phase, status] of combos) {
|
||||
const state = bookingWindowUiState(
|
||||
row({ windowPhase: phase, bookingWindowStatus: status }),
|
||||
);
|
||||
expect(state.isBookable).toBe(phase === "OPEN" && status === "OPEN");
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user