mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-09 02:28:18 +00:00
Enhance booking and signature functionalities
- Updated MySignaturePage title to Signature
This commit is contained in:
61
e2e/freight/cypress/e2e/flows/bulk-items-utils.ts
Normal file
61
e2e/freight/cypress/e2e/flows/bulk-items-utils.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
/**
|
||||
* Shared helper for the PER_ITEM break-bulk specs (bulk_b2 / bulk_b3).
|
||||
* Cargo types come from fixtures/seed-bulk-items.sql.
|
||||
*/
|
||||
|
||||
import { apiPost, customer, db } from "./import-utils";
|
||||
|
||||
/** Book a PER_ITEM break-bulk line under the suffix's seeded contract. */
|
||||
export function bookBulkItems(opts: {
|
||||
suffix: string;
|
||||
cargoCode: "E2E_IMP_AUTO" | "E2E_IMP_MACHINE";
|
||||
items: number;
|
||||
tons: number;
|
||||
scheduledDate?: string;
|
||||
hazardousQuantity?: number;
|
||||
reeferQuantity?: number;
|
||||
expectFailure?: string | RegExp;
|
||||
}) {
|
||||
db<{ id: string; cargo_type_id: string }>(
|
||||
`SELECT ct.id,
|
||||
(SELECT t.id FROM freight.cargo_types t WHERE t.code = $2) AS cargo_type_id
|
||||
FROM freight.contracts ct
|
||||
WHERE ct.reference LIKE 'CTR-IMP-%-' || $1
|
||||
ORDER BY ct.created_at DESC LIMIT 1`,
|
||||
[opts.suffix, opts.cargoCode],
|
||||
).then(({ rows }) => {
|
||||
expect(rows, `seeded contract *-${opts.suffix}`).to.have.length(1);
|
||||
expect(rows[0].cargo_type_id, `${opts.cargoCode} seeded`).to.be.a("string");
|
||||
apiPost(
|
||||
customer,
|
||||
`/api/contracts/${rows[0].id}/bookings`,
|
||||
{
|
||||
...(opts.scheduledDate ? { scheduledDate: opts.scheduledDate } : {}),
|
||||
bulkLines: [
|
||||
{
|
||||
cargoTypeId: rows[0].cargo_type_id,
|
||||
itemCount: opts.items,
|
||||
cargoWeightTons: opts.tons,
|
||||
...(opts.hazardousQuantity != null
|
||||
? { hazardousQuantity: opts.hazardousQuantity }
|
||||
: {}),
|
||||
...(opts.reeferQuantity != null ? { reeferQuantity: opts.reeferQuantity } : {}),
|
||||
},
|
||||
],
|
||||
cargoFreeText: `E2E break-bulk ${opts.cargoCode}`,
|
||||
},
|
||||
!opts.expectFailure,
|
||||
).then((res) => {
|
||||
if (opts.expectFailure) {
|
||||
expect(res.status, `${opts.suffix} booking rejected`).to.be.within(400, 422);
|
||||
if (opts.expectFailure instanceof RegExp) {
|
||||
expect(JSON.stringify(res.body)).to.match(opts.expectFailure);
|
||||
} else {
|
||||
expect(JSON.stringify(res.body)).to.include(opts.expectFailure);
|
||||
}
|
||||
} else {
|
||||
expect(res.status, `${opts.suffix} booking created`).to.be.oneOf([200, 201]);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* BULK B1 — priority ordering + expiry refill (BS2/BS3 in BULK_SCENARIOS.md).
|
||||
*
|
||||
* Day D+28, 54-wagon CW4 wheat train, three bookings that cannot all fit:
|
||||
*
|
||||
* BP1 1 960 T = 28 wagons (commercial giant)
|
||||
* BP2 1 400 T = 20 wagons (commercial)
|
||||
* BP3 700 T = 10 wagons (relief cargo — staff put it FIRST)
|
||||
*
|
||||
* 58 wagons chase 54. With BP3 forced to the top of the order the batch
|
||||
* reserves BP3 + BP1 whole (38 w) and leaves BP2 a whole-wagon offer for the
|
||||
* remaining 16. Then BP1 misses its pay window: its 28 wagons return and the
|
||||
* refill round must promote BP2 WHOLE — the 16-wagon offer is superseded.
|
||||
*
|
||||
* ⚠ Authored, not yet run — see BULK_SCENARIOS.md.
|
||||
*/
|
||||
|
||||
import {
|
||||
acceptOperation,
|
||||
bookBulk,
|
||||
clearToOperationRequestPending,
|
||||
closeBookingWindow,
|
||||
completeDocReview,
|
||||
createImportSchedule,
|
||||
departureAt,
|
||||
eatDayStr,
|
||||
ensureCorridorRoute,
|
||||
expectWagonType,
|
||||
forceReservationExpiry,
|
||||
forceWindowOpen,
|
||||
markPaid,
|
||||
pollAllocations,
|
||||
pollBookingStatus,
|
||||
pollDb,
|
||||
resetCorridorDay,
|
||||
seedImportContract,
|
||||
setPriority,
|
||||
withBooking,
|
||||
withSchedule,
|
||||
} from "./import-utils";
|
||||
|
||||
const DEPARTURE = departureAt(28);
|
||||
const BOOKING_DAY = eatDayStr(DEPARTURE);
|
||||
|
||||
const stamp = String(Date.now());
|
||||
const stampedRef = (suffix: string) => `CTR-IMP-${stamp}-${suffix}`;
|
||||
|
||||
describe("bulk b1: staff priority decides who rides; expiry refill promotes the offered booking whole", { retries: 0 }, () => {
|
||||
before(() => {
|
||||
cy.task("db:seedFile", "seed-import-corridor.sql");
|
||||
(["BP1", "BP2", "BP3"] as const).forEach((suffix) =>
|
||||
seedImportContract({ suffix, reference: stampedRef(suffix), freight: "BULK" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("operations prepares the D+28 wheat train with an open window", () => {
|
||||
ensureCorridorRoute();
|
||||
resetCorridorDay(DEPARTURE);
|
||||
createImportSchedule({
|
||||
departure: DEPARTURE,
|
||||
locoPair: ["LOCO-IMP-15", "LOCO-IMP-16"],
|
||||
kind: "bulk",
|
||||
});
|
||||
withSchedule(DEPARTURE, (s) => forceWindowOpen(s.id, 60));
|
||||
});
|
||||
|
||||
it("three wheat bookings (28+20+10 wagons) enter the window; staff rank the relief cargo first", () => {
|
||||
bookBulk({ suffix: "BP1", tons: 1960, scheduledDate: BOOKING_DAY });
|
||||
clearToOperationRequestPending("BP1", BOOKING_DAY);
|
||||
acceptOperation("BP1");
|
||||
bookBulk({ suffix: "BP2", tons: 1400, scheduledDate: BOOKING_DAY });
|
||||
clearToOperationRequestPending("BP2", BOOKING_DAY);
|
||||
acceptOperation("BP2");
|
||||
bookBulk({ suffix: "BP3", tons: 700, scheduledDate: BOOKING_DAY });
|
||||
clearToOperationRequestPending("BP3", BOOKING_DAY);
|
||||
acceptOperation("BP3");
|
||||
|
||||
setPriority("BP3", 1);
|
||||
setPriority("BP1", 2);
|
||||
setPriority("BP2", 3);
|
||||
});
|
||||
|
||||
it("batch reserves BP3 + BP1 whole; BP2 gets a whole-wagon offer for the 16-wagon leftover", () => {
|
||||
withSchedule(DEPARTURE, (s) => {
|
||||
closeBookingWindow(s.id);
|
||||
completeDocReview(s.id);
|
||||
});
|
||||
pollBookingStatus("BP3", ["SELECTED_FOR_BATCH", "AWAITING_PAYMENT"]);
|
||||
pollBookingStatus("BP1", ["SELECTED_FOR_BATCH", "AWAITING_PAYMENT"]);
|
||||
withBooking("BP2", (b) => {
|
||||
pollDb<{ status: string; offered_wagons: string }>(
|
||||
"BP2 open partial offer",
|
||||
`SELECT status, offered_wagons FROM freight.booking_batch_offers
|
||||
WHERE booking_id = $1 AND deleted_at IS NULL
|
||||
ORDER BY created_at DESC LIMIT 1`,
|
||||
[b.id],
|
||||
(row) => row?.status === "OFFERED" && Number(row?.offered_wagons) === 16,
|
||||
15,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("BP3 pays and rides CW4; BP1 misses the pay window and EXPIRES", () => {
|
||||
markPaid("BP3");
|
||||
pollAllocations("BP3", 10);
|
||||
expectWagonType("BP3", "CW4", 10);
|
||||
|
||||
forceReservationExpiry("BP1");
|
||||
pollBookingStatus("BP1", "EXPIRED", 20);
|
||||
});
|
||||
|
||||
it("refill round promotes BP2 WHOLE into the freed 28 wagons — the 16-wagon offer is superseded", () => {
|
||||
pollBookingStatus("BP2", ["SELECTED_FOR_BATCH", "AWAITING_PAYMENT"], 30);
|
||||
markPaid("BP2");
|
||||
pollAllocations("BP2", 20);
|
||||
withBooking("BP2", (b) => {
|
||||
expect(b.is_split, "BP2 rides whole, not split").to.not.eq(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
export {};
|
||||
147
e2e/freight/cypress/e2e/flows/bulk_b2_per_item_floor.cy.ts
Normal file
147
e2e/freight/cypress/e2e/flows/bulk_b2_per_item_floor.cy.ts
Normal file
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* BULK B2 — break-bulk PER_ITEM wagon math (BS17–BS19 in BULK_SCENARIOS.md).
|
||||
*
|
||||
* Cargo from seed-bulk-items.sql, riding the CW4 fleet (70 T / 24.8 T tare):
|
||||
*
|
||||
* E2E_IMP_AUTO automobiles, items_per_wagon_map floor = 4 per CW4
|
||||
* E2E_IMP_MACHINE machinery, NO floor → tonnage-only fallback
|
||||
*
|
||||
* Three verdicts of bulkItemWagonsRequired, end to end:
|
||||
* BA1 16 autos @2.5 T (40 T) → floor binds: 4 wagons (tonnage said 1)
|
||||
* BA2 12 machines @20 T (240 T) → tonnage binds: floor(70/20)=3/wagon → 4 wagons
|
||||
* BA3 216 autos (540 T) → exactly 54 wagons — FULL from one booking
|
||||
*
|
||||
* ⚠ Authored, not yet run — see BULK_SCENARIOS.md.
|
||||
*/
|
||||
|
||||
import { bookBulkItems } from "./bulk-items-utils";
|
||||
import {
|
||||
acceptOperation,
|
||||
clearToOperationRequestPending,
|
||||
closeBookingWindow,
|
||||
completeDocReview,
|
||||
createImportSchedule,
|
||||
departureAt,
|
||||
eatDayStr,
|
||||
endPaymentPhase,
|
||||
ensureCorridorRoute,
|
||||
expectWagonType,
|
||||
forceWindowOpen,
|
||||
markPaid,
|
||||
pollAllocations,
|
||||
pollBookingStatus,
|
||||
pollDb,
|
||||
resetCorridorDay,
|
||||
seedImportContract,
|
||||
withBooking,
|
||||
withSchedule,
|
||||
type ScheduleRow,
|
||||
} from "./import-utils";
|
||||
|
||||
const DEPARTURE = departureAt(30);
|
||||
const BOOKING_DAY = eatDayStr(DEPARTURE);
|
||||
const FULL_DEPARTURE = departureAt(31);
|
||||
const FULL_DAY = eatDayStr(FULL_DEPARTURE);
|
||||
|
||||
const stamp = String(Date.now());
|
||||
const stampedRef = (suffix: string) => `CTR-IMP-${stamp}-${suffix}`;
|
||||
|
||||
describe("bulk b2: PER_ITEM floor vs tonnage wagon math on the CW4 fleet", { retries: 0 }, () => {
|
||||
before(() => {
|
||||
cy.task("db:seedFile", "seed-import-corridor.sql");
|
||||
cy.task("db:seedFile", "seed-bulk-items.sql");
|
||||
(["BA1", "BA2", "BA3"] as const).forEach((suffix) =>
|
||||
seedImportContract({ suffix, reference: stampedRef(suffix), freight: "BULK" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("operations prepares the D+30 break-bulk train with an open window", () => {
|
||||
ensureCorridorRoute();
|
||||
resetCorridorDay(DEPARTURE);
|
||||
resetCorridorDay(FULL_DEPARTURE);
|
||||
createImportSchedule({
|
||||
departure: DEPARTURE,
|
||||
locoPair: ["LOCO-IMP-15", "LOCO-IMP-16"],
|
||||
kind: "bulk",
|
||||
});
|
||||
withSchedule(DEPARTURE, (s) => forceWindowOpen(s.id, 60));
|
||||
});
|
||||
|
||||
it("BS17 — 16 autos (40 T): the 4-per-wagon floor binds → 4 wagons, not 1", () => {
|
||||
bookBulkItems({
|
||||
suffix: "BA1",
|
||||
cargoCode: "E2E_IMP_AUTO",
|
||||
items: 16,
|
||||
tons: 40,
|
||||
scheduledDate: BOOKING_DAY,
|
||||
});
|
||||
clearToOperationRequestPending("BA1", BOOKING_DAY);
|
||||
acceptOperation("BA1");
|
||||
});
|
||||
|
||||
it("BS18 — 12 machines @20 T: no floor, tonnage fallback → 3 per wagon → 4 wagons", () => {
|
||||
bookBulkItems({
|
||||
suffix: "BA2",
|
||||
cargoCode: "E2E_IMP_MACHINE",
|
||||
items: 12,
|
||||
tons: 240,
|
||||
scheduledDate: BOOKING_DAY,
|
||||
});
|
||||
clearToOperationRequestPending("BA2", BOOKING_DAY);
|
||||
acceptOperation("BA2");
|
||||
});
|
||||
|
||||
it("batch reserves both; payment allocates exactly 4 + 4 CW4 wagons", () => {
|
||||
withSchedule(DEPARTURE, (s) => {
|
||||
closeBookingWindow(s.id);
|
||||
completeDocReview(s.id);
|
||||
});
|
||||
(["BA1", "BA2"] as const).forEach((suffix) =>
|
||||
pollBookingStatus(suffix, ["SELECTED_FOR_BATCH", "AWAITING_PAYMENT"]),
|
||||
);
|
||||
markPaid("BA1");
|
||||
pollAllocations("BA1", 4);
|
||||
expectWagonType("BA1", "CW4", 4);
|
||||
markPaid("BA2");
|
||||
pollAllocations("BA2", 4);
|
||||
expectWagonType("BA2", "CW4", 4);
|
||||
});
|
||||
|
||||
it("BS19 — 216 autos (540 T) = exactly 54 wagons: FULL from one break-bulk booking, no split", () => {
|
||||
createImportSchedule({
|
||||
departure: FULL_DEPARTURE,
|
||||
locoPair: ["LOCO-IMP-25", "LOCO-IMP-26"],
|
||||
kind: "bulk",
|
||||
});
|
||||
withSchedule(FULL_DEPARTURE, (s) => forceWindowOpen(s.id, 45));
|
||||
|
||||
bookBulkItems({
|
||||
suffix: "BA3",
|
||||
cargoCode: "E2E_IMP_AUTO",
|
||||
items: 216,
|
||||
tons: 540,
|
||||
scheduledDate: FULL_DAY,
|
||||
});
|
||||
clearToOperationRequestPending("BA3", FULL_DAY);
|
||||
acceptOperation("BA3");
|
||||
withSchedule(FULL_DEPARTURE, (s) => {
|
||||
closeBookingWindow(s.id);
|
||||
completeDocReview(s.id);
|
||||
});
|
||||
pollBookingStatus("BA3", ["SELECTED_FOR_BATCH", "AWAITING_PAYMENT"]);
|
||||
markPaid("BA3");
|
||||
pollAllocations("BA3", 54);
|
||||
withBooking("BA3", (b) => {
|
||||
expect(b.is_split, "BA3 whole, not split").to.not.eq(true);
|
||||
});
|
||||
withSchedule(FULL_DEPARTURE, (s) => {
|
||||
endPaymentPhase(s.id);
|
||||
pollDb<ScheduleRow>(
|
||||
"full-day schedule FULL + DONE",
|
||||
`SELECT window_phase, booking_window_status FROM freight.train_schedules WHERE id = $1`,
|
||||
[s.id],
|
||||
(row) => row?.booking_window_status === "FULL" && row?.window_phase === "DONE",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,181 @@
|
||||
/**
|
||||
* BULK B3 — PER_ITEM giant split + line quantities (BS20–BS23 in
|
||||
* BULK_SCENARIOS.md).
|
||||
*
|
||||
* BG1 240 automobiles (600 T) on a 54-wagon CW4 train (floor 4/wagon
|
||||
* → needs 60 wagons). Bulk partial offers are WHOLE wagons only →
|
||||
* offer = 54 wagons / 216 autos. Gateway settle applies the split,
|
||||
* the train is FULL from one break-bulk booking, and the 24-auto
|
||||
* outstanding must be rebooked EXACTLY on a later train.
|
||||
* BQ1 hazardousQuantity 12 on a 10-item line: the engine CLAMPS to 10
|
||||
* and returns 201 (documented gap — same as container S40; this
|
||||
* spec pins the CURRENT behaviour so a future fix flips it loudly).
|
||||
* BQ2 reeferQuantity 3 on an 8-item line is stored on the booking.
|
||||
*
|
||||
* ⚠ Authored, not yet run — see BULK_SCENARIOS.md.
|
||||
*/
|
||||
|
||||
import { bookBulkItems } from "./bulk-items-utils";
|
||||
import {
|
||||
acceptOperation,
|
||||
clearToOperationRequestPending,
|
||||
closeBookingWindow,
|
||||
completeDocReview,
|
||||
createImportSchedule,
|
||||
db,
|
||||
departureAt,
|
||||
eatDayStr,
|
||||
endPaymentPhase,
|
||||
ensureCorridorRoute,
|
||||
forceWindowOpen,
|
||||
pollAllocations,
|
||||
pollBookingStatus,
|
||||
pollDb,
|
||||
resetCorridorDay,
|
||||
seedImportContract,
|
||||
settleViaGateway,
|
||||
withBooking,
|
||||
withSchedule,
|
||||
type ScheduleRow,
|
||||
} from "./import-utils";
|
||||
|
||||
const GIANT_DEPARTURE = departureAt(32);
|
||||
const GIANT_DAY = eatDayStr(GIANT_DEPARTURE);
|
||||
const REMAINDER_DEPARTURE = departureAt(33);
|
||||
const REMAINDER_DAY = eatDayStr(REMAINDER_DEPARTURE);
|
||||
|
||||
const stamp = String(Date.now());
|
||||
const stampedRef = (suffix: string) => `CTR-IMP-${stamp}-${suffix}`;
|
||||
|
||||
describe("bulk b3: per-item giant gets a whole-wagon offer; line quantities clamp/store", { retries: 0 }, () => {
|
||||
before(() => {
|
||||
cy.task("db:seedFile", "seed-import-corridor.sql");
|
||||
cy.task("db:seedFile", "seed-bulk-items.sql");
|
||||
(["BG1", "BQ1", "BQ2"] as const).forEach((suffix) =>
|
||||
seedImportContract({ suffix, reference: stampedRef(suffix), freight: "BULK" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("operations prepares the D+32 giant train and the D+33 remainder train", () => {
|
||||
ensureCorridorRoute();
|
||||
resetCorridorDay(GIANT_DEPARTURE);
|
||||
resetCorridorDay(REMAINDER_DEPARTURE);
|
||||
createImportSchedule({
|
||||
departure: GIANT_DEPARTURE,
|
||||
locoPair: ["LOCO-IMP-27", "LOCO-IMP-28"],
|
||||
kind: "bulk",
|
||||
});
|
||||
withSchedule(GIANT_DEPARTURE, (s) => forceWindowOpen(s.id, 45));
|
||||
});
|
||||
|
||||
it("BS20 — 240 autos need 60 wagons: whole-consist offer of 216 autos / 54 wagons, split applied", () => {
|
||||
bookBulkItems({
|
||||
suffix: "BG1",
|
||||
cargoCode: "E2E_IMP_AUTO",
|
||||
items: 240,
|
||||
tons: 600,
|
||||
scheduledDate: GIANT_DAY,
|
||||
});
|
||||
clearToOperationRequestPending("BG1", GIANT_DAY);
|
||||
acceptOperation("BG1");
|
||||
withSchedule(GIANT_DEPARTURE, (s) => {
|
||||
closeBookingWindow(s.id);
|
||||
completeDocReview(s.id);
|
||||
});
|
||||
pollBookingStatus("BG1", ["SELECTED_FOR_BATCH", "AWAITING_PAYMENT"]);
|
||||
withBooking("BG1", (b) => {
|
||||
pollDb<{ status: string; offered_wagons: string }>(
|
||||
"BG1 whole-wagon partial offer",
|
||||
`SELECT status, offered_wagons FROM freight.booking_batch_offers
|
||||
WHERE booking_id = $1 AND deleted_at IS NULL
|
||||
ORDER BY created_at DESC LIMIT 1`,
|
||||
[b.id],
|
||||
(row) => row?.status === "OFFERED" && Number(row?.offered_wagons) === 54,
|
||||
15,
|
||||
);
|
||||
});
|
||||
|
||||
settleViaGateway("BG1");
|
||||
pollAllocations("BG1", 54);
|
||||
withBooking("BG1", (b) => {
|
||||
expect(b.is_split, "BG1 is split").to.eq(true);
|
||||
});
|
||||
withSchedule(GIANT_DEPARTURE, (s) => {
|
||||
endPaymentPhase(s.id);
|
||||
pollDb<ScheduleRow>(
|
||||
"giant train FULL from one break-bulk booking",
|
||||
`SELECT window_phase, booking_window_status FROM freight.train_schedules WHERE id = $1`,
|
||||
[s.id],
|
||||
(row) => row?.booking_window_status === "FULL" && row?.window_phase === "DONE",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it("BS21 — the 24-auto outstanding must be rebooked EXACTLY on the later train", () => {
|
||||
createImportSchedule({
|
||||
departure: REMAINDER_DEPARTURE,
|
||||
locoPair: ["LOCO-IMP-13", "LOCO-IMP-14"],
|
||||
kind: "bulk",
|
||||
});
|
||||
withSchedule(REMAINDER_DEPARTURE, (s) => forceWindowOpen(s.id, 45));
|
||||
|
||||
bookBulkItems({
|
||||
suffix: "BG1",
|
||||
cargoCode: "E2E_IMP_AUTO",
|
||||
items: 10,
|
||||
tons: 25,
|
||||
scheduledDate: REMAINDER_DAY,
|
||||
expectFailure: "must take the whole",
|
||||
});
|
||||
bookBulkItems({
|
||||
suffix: "BG1",
|
||||
cargoCode: "E2E_IMP_AUTO",
|
||||
items: 24,
|
||||
tons: 60,
|
||||
scheduledDate: REMAINDER_DAY,
|
||||
});
|
||||
clearToOperationRequestPending("BG1", REMAINDER_DAY);
|
||||
});
|
||||
|
||||
it("BS22 — hazardousQuantity 12 on a 10-item line is CLAMPED to 10, not rejected (pins the gap)", () => {
|
||||
bookBulkItems({
|
||||
suffix: "BQ1",
|
||||
cargoCode: "E2E_IMP_AUTO",
|
||||
items: 10,
|
||||
tons: 25,
|
||||
scheduledDate: REMAINDER_DAY,
|
||||
hazardousQuantity: 12,
|
||||
});
|
||||
withBooking("BQ1", (b) => {
|
||||
db<{ bulk_hazardous_quantity: string }>(
|
||||
`SELECT bulk_hazardous_quantity FROM freight.bookings WHERE id = $1`,
|
||||
[b.id],
|
||||
).then(({ rows }) => {
|
||||
// Engine clamps to 0..quantity (bookings.repository.ts) — a future
|
||||
// fix that rejects instead will fail HERE first. See BS22.
|
||||
expect(Number(rows[0].bulk_hazardous_quantity), "clamped hazmat").to.eq(10);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("BS23 — reeferQuantity 3 on an 8-item line is stored on the booking", () => {
|
||||
bookBulkItems({
|
||||
suffix: "BQ2",
|
||||
cargoCode: "E2E_IMP_AUTO",
|
||||
items: 8,
|
||||
tons: 20,
|
||||
scheduledDate: REMAINDER_DAY,
|
||||
reeferQuantity: 3,
|
||||
});
|
||||
withBooking("BQ2", (b) => {
|
||||
db<{ bulk_reefer_quantity: string }>(
|
||||
`SELECT bulk_reefer_quantity FROM freight.bookings WHERE id = $1`,
|
||||
[b.id],
|
||||
).then(({ rows }) => {
|
||||
expect(Number(rows[0].bulk_reefer_quantity), "reefer stored").to.eq(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
export {};
|
||||
37
e2e/freight/cypress/fixtures/seed-bulk-items.sql
Normal file
37
e2e/freight/cypress/fixtures/seed-bulk-items.sql
Normal file
@@ -0,0 +1,37 @@
|
||||
-- PER_ITEM break-bulk cargo types for the bulk_b2/bulk_b3 specs.
|
||||
-- Run AFTER seed-import-corridor.sql (needs E2E_IMP_GRAINS + the CW4 fleet).
|
||||
-- Idempotent — safe on re-runs and cross-origin before() replays.
|
||||
|
||||
-- Automobiles: PER_ITEM with a configured physical floor (4 cars per CW4).
|
||||
INSERT INTO freight.cargo_types (id, code, cargo_type_name, parent_group_id, unit_of_measure, is_active)
|
||||
SELECT gen_random_uuid(), 'E2E_IMP_AUTO', 'E2E Import Automobiles', g.id, 'PER_ITEM', true
|
||||
FROM freight.cargo_types g
|
||||
WHERE g.code = 'E2E_IMP_GRAINS'
|
||||
AND NOT EXISTS (SELECT 1 FROM freight.cargo_types WHERE code = 'E2E_IMP_AUTO');
|
||||
|
||||
-- Machinery: PER_ITEM with NO items_per_wagon_map — exercises the
|
||||
-- tonnage-only fallback in bulkItemWagonsRequired.
|
||||
INSERT INTO freight.cargo_types (id, code, cargo_type_name, parent_group_id, unit_of_measure, is_active)
|
||||
SELECT gen_random_uuid(), 'E2E_IMP_MACHINE', 'E2E Import Machinery', g.id, 'PER_ITEM', true
|
||||
FROM freight.cargo_types g
|
||||
WHERE g.code = 'E2E_IMP_GRAINS'
|
||||
AND NOT EXISTS (SELECT 1 FROM freight.cargo_types WHERE code = 'E2E_IMP_MACHINE');
|
||||
|
||||
-- Both ride the corridor's CW4 bulk fleet.
|
||||
INSERT INTO freight.cargo_type_wagon_types (cargo_type_id, wagon_type_id)
|
||||
SELECT ct.id, wt.id
|
||||
FROM freight.cargo_types ct
|
||||
JOIN freight.wagon_types wt ON wt.code = 'CW4'
|
||||
WHERE ct.code IN ('E2E_IMP_AUTO', 'E2E_IMP_MACHINE')
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM freight.cargo_type_wagon_types x
|
||||
WHERE x.cargo_type_id = ct.id AND x.wagon_type_id = wt.id
|
||||
);
|
||||
|
||||
-- Physical floor: 4 automobiles fit one CW4 regardless of tonnage headroom.
|
||||
UPDATE freight.cargo_types ct
|
||||
SET items_per_wagon_map = jsonb_build_object(
|
||||
(SELECT wt.id::text FROM freight.wagon_types wt WHERE wt.code = 'CW4'), 4)
|
||||
WHERE ct.code = 'E2E_IMP_AUTO'
|
||||
AND (ct.items_per_wagon_map IS NULL
|
||||
OR NOT ct.items_per_wagon_map ? (SELECT wt.id::text FROM freight.wagon_types wt WHERE wt.code = 'CW4'));
|
||||
Reference in New Issue
Block a user