Files
edr-platform/integration/src/g1-s6-s8-offers-government-tiers.it.ts
2026-08-03 10:51:43 +00:00

421 lines
17 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* GROUP 1 · S6S8 — who gets the last wagons on the 53-wagon built train.
*
* S6 a split offer nobody takes: the offer lapses, the booking expires
* WHOLE, and the wagons it was offered go unsold.
* S7 a government booking jumps the queue — by PREEMPTION, not by ranking:
* it displaces the lowest-priority commercial reservation and rides
* unpaid, carrying the +50 000 bonus.
* S8 commercial priority tiers decide who is offered the remainder:
* USD payer > customs service > plain, with no per-booking priority set.
*
* TWO NOTES ON HOW THE PRODUCT REALLY WORKS
*
* S7 — the +50 000 bonus (GOVERNMENT_PRIORITY_BONUS) keys off
* `bookings.is_government`, not a government-institution lookup, and
* government does not merely outrank: `preemptForGovernment` EXPIRES the
* lowest-priority commercial booking whose leg overlaps and allocates in its
* place. Government bookings are created with POST /api/bookings against a
* kind='government' company and promoted with /government-expedite — never
* through the contract wizard.
*
* FINDING — preemption cannot reach a train whose window already reads FULL:
* `isFillable` rejects FULL outright, before any budget or victim is
* considered, and `refreshWindowStatus` re-derives FULL from live capacity, so
* a genuinely full train stays skipped. The scenario therefore books 52 of 53
* slots: committed, one slot short, which is the closest reachable state to
* "a full train" and still exercises the displacement.
*
* S8 — the retired USD_PAYER / RAIL_AND_FORWARDING priority RULES are gone
* (ReplacePriorityRulesWithPriorityConfigs). The live model is
* `priority_configs`, typed WAGON | CURRENCY | CUSTOMS and scored by
* wagon-count band. The scenario's intent — tiered ordering, lowest tier gets
* the split — is preserved against that mechanism. The CUSTOMS band only
* applies when the booking's SERVICE TYPE bundles customs, which is why S8's
* customs tenant is sold RAIL_CUSTOMS (seed-customs-service-type.sql).
*/
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { closeDb, gateway } from "./client";
import {
G1_WAGONS,
allocatedWagons,
bookContainersReady,
bookingRow,
closeBookingWindow,
completeDocReview,
containerCount,
containerWagons,
createBuiltTrainSchedule,
createGovernmentBooking,
createPriorityConfig,
departureAt,
dropPriorityConfig,
eatDayStr,
endPaymentPhase,
ensureCorridorRoute,
expectNoPartialOffer,
extendPayWindow,
forceOfferLapse,
forceWindowOpen,
governmentExpedite,
linkedBookings,
livePayableInvoices,
payViaGateway,
pinToSchedule,
pollAllocations,
pollBookingStatus,
pollCycleConcluded,
pollPartialOffer,
pollWindow,
releaseGovernmentBookings,
releaseUnpaidHolds,
resetCorridorDay,
scheduleRow,
seedTenantContracts,
setPriority,
triggerBatchRun,
} from "./flows";
const STAMP = String(Date.now());
// ───────────────────────────────────────────────────────────────────────────
// S6 — a split offer nobody takes
// ───────────────────────────────────────────────────────────────────────────
describe("g1 s6: an ignored split offer expires the booking whole", () => {
const DEPARTURE = departureAt(55);
const BOOKING_DAY = eatDayStr(DEPARTURE);
const SHAPES = {
SA: { twenty: 0, forty: 30, wagons: 30 },
SB: { twenty: 40, forty: 0, wagons: 20 },
SC: { twenty: 20, forty: 0, wagons: 10 },
} as const;
const ORDER = ["SA", "SB", "SC"] as const;
const GAP = G1_WAGONS - SHAPES.SA.wagons - SHAPES.SB.wagons; // 3
const RIDING = SHAPES.SA.wagons + SHAPES.SB.wagons; // 50
const booking = new Map<string, string>();
let scheduleId: string;
beforeAll(async () => {
await gateway.reset();
await releaseUnpaidHolds();
await ensureCorridorRoute();
await resetCorridorDay(DEPARTURE);
const contracts = await seedTenantContracts(
STAMP,
ORDER.map((suffix) => ({ suffix, freight: "CONTAINER" as const })),
);
scheduleId = (await createBuiltTrainSchedule({ departure: DEPARTURE })).id;
await forceWindowOpen(scheduleId, 60);
let isoSeed = 25_000;
for (const [i, suffix] of ORDER.entries()) {
const shape = SHAPES[suffix];
booking.set(
suffix,
await bookContainersReady({
contractId: contracts.get(suffix)!,
runStamp: STAMP,
isoSeed,
twenty: shape.twenty,
forty: shape.forty,
scheduledDate: BOOKING_DAY,
}),
);
isoSeed += shape.twenty + shape.forty;
await setPriority(booking.get(suffix)!, i + 1);
}
}, 1_800_000);
it("SA and SB take 50 wagons; SC is offered the last 3", async () => {
expect(containerWagons(SHAPES.SC.twenty, 0), "SC needs 10 wagons").toBe(SHAPES.SC.wagons);
await closeBookingWindow(scheduleId);
await completeDocReview(scheduleId);
for (const suffix of ["SA", "SB"] as const) {
await pollBookingStatus(booking.get(suffix)!, ["SELECTED_FOR_BATCH", "AWAITING_PAYMENT"]);
await expectNoPartialOffer(booking.get(suffix)!, suffix);
}
const offer = await pollPartialOffer(booking.get("SC")!);
expect(Number(offer.offered_wagons), "SC offered the 3-wagon gap").toBe(GAP);
});
it("SC ignores the offer for the whole window — it EXPIRES whole", async () => {
await extendPayWindow(scheduleId, [booking.get("SA")!, booking.get("SB")!]);
await payViaGateway(booking.get("SA")!);
await pollAllocations(booking.get("SA")!, SHAPES.SA.wagons);
await payViaGateway(booking.get("SB")!);
await pollAllocations(booking.get("SB")!, SHAPES.SB.wagons);
// The offer dies with the booking's pay deadline; the tick settles it.
await forceOfferLapse(booking.get("SC")!);
// "Whole" is the load-bearing word: an ignored PARTIAL must not leave the
// booking silently reduced to the 3 wagons it was offered — the customer
// still owns all 20 containers and can rebook them intact.
const sc = await bookingRow(booking.get("SC")!);
expect(sc.is_split, "SC was never split").not.toBe(true);
expect(await containerCount(booking.get("SC")!), "SC's 20 containers intact").toBe(
SHAPES.SC.twenty,
);
});
it("the train departs NOT FULL at 50/53 — the 3 offered wagons went unsold", async () => {
await endPaymentPhase(scheduleId);
await pollCycleConcluded(scheduleId);
expect(await allocatedWagons(scheduleId), "50 wagons allocated").toBe(RIDING);
expect(
(await scheduleRow(scheduleId)).booking_window_status,
"window not FULL at 50/53",
).not.toBe("FULL");
expect(GAP, "3 wagons wasted").toBe(3);
});
});
// ───────────────────────────────────────────────────────────────────────────
// S7 — government preempts the lowest-priority commercial reservation
// ───────────────────────────────────────────────────────────────────────────
describe("g1 s7: a government booking preempts commercial", () => {
const DEPARTURE = departureAt(56);
const BOOKING_DAY = eatDayStr(DEPARTURE);
/** GA outranks GB, so GB is the one preemption must take. */
const SHAPES = {
GA: { forty: 25, wagons: 25 },
GB: { forty: 27, wagons: 27 },
} as const;
const ORDER = ["GA", "GB"] as const;
/** More than the single free slot: the government booking cannot fit as-is. */
const GOV_WAGONS = 15;
const booking = new Map<string, string>();
let scheduleId: string;
let govBookingId: string;
beforeAll(async () => {
await gateway.reset();
await releaseUnpaidHolds();
await releaseGovernmentBookings();
await ensureCorridorRoute();
await resetCorridorDay(DEPARTURE);
const contracts = await seedTenantContracts(
STAMP,
ORDER.map((suffix) => ({ suffix, freight: "CONTAINER" as const })),
);
scheduleId = (await createBuiltTrainSchedule({ departure: DEPARTURE })).id;
await forceWindowOpen(scheduleId, 60);
let isoSeed = 26_000;
for (const [i, suffix] of ORDER.entries()) {
booking.set(
suffix,
await bookContainersReady({
contractId: contracts.get(suffix)!,
runStamp: STAMP,
isoSeed,
forty: SHAPES[suffix].forty,
scheduledDate: BOOKING_DAY,
}),
);
isoSeed += SHAPES[suffix].forty;
await setPriority(booking.get(suffix)!, i + 1);
}
}, 1_800_000);
it("GA pays and GB holds a reservation — 52 of 53 slots committed", async () => {
await closeBookingWindow(scheduleId);
await completeDocReview(scheduleId);
for (const suffix of ORDER) {
await pollBookingStatus(booking.get(suffix)!, ["SELECTED_FOR_BATCH", "AWAITING_PAYMENT"]);
}
await extendPayWindow(scheduleId, [booking.get("GA")!]);
await payViaGateway(booking.get("GA")!);
await pollAllocations(booking.get("GA")!, SHAPES.GA.wagons);
// GB stays UNPAID on purpose — it is the reservation the government
// booking must displace — but its hold is widened so it does not lapse on
// its own while the government booking is being created.
await extendPayWindow(scheduleId, [booking.get("GB")!]);
expect((await bookingRow(booking.get("GB")!)).status, "GB still reserved").toMatch(
/SELECTED_FOR_BATCH|AWAITING_PAYMENT/,
);
expect(SHAPES.GA.wagons + SHAPES.GB.wagons, "52 of 53 committed").toBe(G1_WAGONS - 1);
});
it("a government booking is created and expedited — PAID without paying", async () => {
govBookingId = await createGovernmentBooking({ forty: GOV_WAGONS });
// Idempotent: create() already expedites, the endpoint is the retry path.
await governmentExpedite(govBookingId);
const gov = await bookingRow(govBookingId);
expect(gov.status, "PAID after expedite").toBe("PAID");
expect(gov.is_government, "flagged government").toBe(true);
});
it("it carries the +50 000 bonus, far above any commercial score", async () => {
const govScore = Number((await bookingRow(govBookingId)).priority_score);
expect(govScore, "government bonus applied").toBeGreaterThanOrEqual(50_000);
expect(
Number((await bookingRow(booking.get("GA")!)).priority_score),
"top commercial still far below government",
).toBeLessThan(govScore);
});
it("the fill displaces GB — the lower-priority reservation — and GA is untouched", async () => {
// Staff pin, then run the batch: fillSchedule's pool is keyed on
// `booking.train_schedule_id`, and the customer-facing pin is unavailable
// because the window closed at doc review.
await pinToSchedule(govBookingId, scheduleId);
await triggerBatchRun(scheduleId);
await pollBookingStatus(booking.get("GB")!, "EXPIRED", 30);
const gb = await bookingRow(booking.get("GB")!);
expect(gb.scheduling_status, "GB back to ELIGIBLE").toBe("ELIGIBLE");
expect(gb.payment_deadline, "GB pay window cleared").toBeNull();
expect(await livePayableInvoices(booking.get("GB")!), "GB invoice closed out").toBe(0);
const ga = await bookingRow(booking.get("GA")!);
expect(ga.status, "GA survives untouched").toBe("PAID");
expect(ga.train_schedule_id, "GA still on this train").toBe(scheduleId);
});
it("the government booking rides on the freed wagons — allocated, never invoiced", async () => {
await pollAllocations(govBookingId, GOV_WAGONS);
const gov = await bookingRow(govBookingId);
expect(gov.scheduling_status, "government SCHEDULED").toBe("SCHEDULED");
expect(await livePayableInvoices(govBookingId), "government rides unpaid").toBe(0);
expect(await linkedBookings(scheduleId), "GA + government hold the seats").toBe(2);
expect(await allocatedWagons(scheduleId), "25 commercial + 15 government").toBe(
SHAPES.GA.wagons + GOV_WAGONS,
);
});
});
// ───────────────────────────────────────────────────────────────────────────
// S8 — commercial priority tiers order the batch
// ───────────────────────────────────────────────────────────────────────────
describe("g1 s8: priority tiers order the batch", () => {
const DEPARTURE = departureAt(57);
const BOOKING_DAY = eatDayStr(DEPARTURE);
/** Three equal bookings — only the TIER differs, so ordering is the only
* thing that can decide who is left with the remainder. */
const SHAPES = {
PA: { forty: 20, wagons: 20, tier: "USD payer" },
PB: { forty: 20, wagons: 20, tier: "customs service" },
PC: { forty: 20, wagons: 20, tier: "plain" },
} as const;
const ORDER = ["PA", "PB", "PC"] as const;
/** 60 wagons of demand for 53 slots → the third gets a 13-wagon offer. */
const GAP = G1_WAGONS - SHAPES.PA.wagons - SHAPES.PB.wagons; // 13
const booking = new Map<string, string>();
let scheduleId: string;
let usdConfigId: string;
beforeAll(async () => {
await gateway.reset();
await releaseUnpaidHolds();
await ensureCorridorRoute();
await resetCorridorDay(DEPARTURE);
// The USD tier. The CUSTOMS bands (7 / 15 points) ship with the corridor
// fixture, so only the currency band has to be added — and it is dropped in
// afterAll, because an active band re-ranks every later file's pool.
usdConfigId = await createPriorityConfig({
type: "CURRENCY",
label: `IT USD payer ${STAMP.slice(-5)}`,
currency: "USD",
minWagonCount: 1,
maxWagonCount: 53,
scorePoints: 35,
});
const contracts = await seedTenantContracts(STAMP, [
{ suffix: "PA", freight: "CONTAINER" as const, currency: "USD" as const },
{
suffix: "PB",
freight: "CONTAINER" as const,
customs: true,
serviceTypeCode: "RAIL_CUSTOMS",
},
{ suffix: "PC", freight: "CONTAINER" as const },
]);
scheduleId = (await createBuiltTrainSchedule({ departure: DEPARTURE })).id;
await forceWindowOpen(scheduleId, 60);
let isoSeed = 27_000;
for (const suffix of ORDER) {
booking.set(
suffix,
await bookContainersReady({
contractId: contracts.get(suffix)!,
runStamp: STAMP,
isoSeed,
forty: SHAPES[suffix].forty,
scheduledDate: BOOKING_DAY,
customs: suffix === "PB",
}),
);
isoSeed += SHAPES[suffix].forty;
}
}, 1_800_000);
afterAll(async () => {
await dropPriorityConfig(usdConfigId);
await closeDb();
});
it("the engine scores USD above customs above plain — no manual priority set", async () => {
// Deliberately no setPriority anywhere in this file: the point is that the
// rule engine's own bands produce the order. Pinning the scores by hand
// would test setPriority, not the tiers.
const scores = new Map<string, number>();
for (const suffix of ORDER) {
scores.set(suffix, Number((await bookingRow(booking.get(suffix)!)).priority_score));
}
expect(scores.get("PA")!, "USD tier outranks the customs tier").toBeGreaterThan(
scores.get("PB")!,
);
expect(scores.get("PB")!, "customs tier outranks plain").toBeGreaterThan(scores.get("PC")!);
});
it("the two top tiers board whole; the lowest is offered the 13-wagon remainder", async () => {
expect(GAP, "13-wagon remainder after the top two").toBe(13);
await closeBookingWindow(scheduleId);
await completeDocReview(scheduleId);
for (const suffix of ["PA", "PB"] as const) {
await pollBookingStatus(booking.get(suffix)!, ["SELECTED_FOR_BATCH", "AWAITING_PAYMENT"]);
await expectNoPartialOffer(booking.get(suffix)!, suffix);
}
const offer = await pollPartialOffer(booking.get("PC")!);
expect(Number(offer.offered_wagons), "PC offered the exact remainder").toBe(GAP);
});
it("all three settle — PC ships 13 of its 20 wagons and the train is FULL", async () => {
await extendPayWindow(scheduleId, [...booking.values()]);
await payViaGateway(booking.get("PA")!);
await pollAllocations(booking.get("PA")!, SHAPES.PA.wagons);
await payViaGateway(booking.get("PB")!);
await pollAllocations(booking.get("PB")!, SHAPES.PB.wagons);
await payViaGateway(booking.get("PC")!);
await pollAllocations(booking.get("PC")!, GAP);
expect((await bookingRow(booking.get("PC")!)).is_split, "PC is the split one").toBe(true);
expect(await containerCount(booking.get("PC")!), "PC shrank to 13 containers").toBe(GAP);
await endPaymentPhase(scheduleId);
await pollWindow(
scheduleId,
(s) => s.booking_window_status === "FULL" && s.window_phase === "DONE",
"FULL + DONE",
);
expect(await allocatedWagons(scheduleId), "53 wagons allocated").toBe(G1_WAGONS);
});
});