Files
edr-platform/e2e/freight/BULK_SCENARIOS.md
Marshal 53d8655dc3 Enhance booking and signature functionalities
- Updated MySignaturePage title to Signature
2026-08-01 22:03:18 +00:00

8.6 KiB
Raw Permalink Blame History

Bulk scenario catalog — BS1BS40

Real-world bulk (PER_TON) and break-bulk (PER_ITEM) scenarios on the e2e corridor DJIB_PORT → NAGAD → DIRE_DAWA → E2E_AWASH → MOJO → KALITY and its export inverse. Companion to SCENARIO_ENGINE_NOTES.md (containers, S1S40).

Fleet facts (dev + e2e DB, verified 2026-08-01):

Wagon Capacity Length Tare
CW4 (wheat, autos, machinery) 70 T 13.976 m 24.8 T
PW2 (grains) 70 T 17.066 m 25.2 T

Standard bulk train: 54 CW4 wagons / 3 780 T cargo. Per-item math (train-capacity.util.ts): items per wagon = min(floor(capacity ÷ per-item tons), configured floor from cargo_types.items_per_wagon_map); items never split across wagons; bulk partial offers are whole wagons at full rated payload only (sizePartialOfferWagons fullWagonsOnly).

E2E cargo codes: E2E_IMP_WHEAT (PER_TON, CW4), E2E_IMP_GRAINS (PER_TON, PW2), E2E_IMP_AUTO (PER_ITEM, CW4, floor 4/wagon — seed-bulk-items.sql), E2E_IMP_MACHINE (PER_ITEM, CW4, no floor → tonnage-only).

Coverage column: spec that runs it, or doc-only (same engine path already proven by the named spec — a bulk twin adds no new engine coverage), or gap (engine contradicts the expectation — ticket, not a test).

bulk_b1_*, bulk_b2_*, bulk_b3_* are authored but not yet run — first execution may need assertion tuning.


A. Fill · payment · waitlist (import wheat, PER_TON)

# Scenario Expected Coverage
BS1 Six wheat bookings (560+420+420+420+1540+420 T = 54 wagons) fill the CW4 train in the first window; mixed USD/ETB, customs/self All selected, all pay, 54/54 allocated, window FULL, schedule finalized bulk_import_full_train
BS2 Staff priority ordering: 1 960 T + 1 400 T + 700 T (58 w > 54); staff put the 700 T relief cargo first Priority order wins: 700 T + 1 960 T reserved whole, 1 400 T gets a whole-wagon offer for the leftover 16 w bulk_b1_priority_expiry_refill
BS3 Reserved giant misses the 1 h pay window EXPIRED; its 28 wagons return; refill round promotes the offered booking WHOLE (offer superseded) bulk_b1_priority_expiry_refill
BS4 Waitlisted wheat promoted on expiry Waiting-list booking selected in the freed space, pays, rides bulk_import_waiting_expiry
BS5 Partial offer accepted → split is_split = true, offered wagons allocated, remainder must rebook bulk_import_split_promote
BS6 Window reopens after under-fill Second cycle opens; late bookings enter cycle 2 bulk_import_window_reopen
BS7 Booking on a day with no open window Rejected at creation ("booking window") bulk_critical_matrix
BS8 Currency per booking: USD and ETB invoices on one train Each invoice carries its booking's currency bulk_import_full_train

B. Capacity axes · wagon types · giants (PER_TON)

# Scenario Expected Coverage
BS9 4 000 T giant alone on a 54-wagon train Full-consist offer 3 780 T, gateway settle applies split, train FULL from one booking bulk_critical_matrix
BS10 Giant's 220 T remainder rebooks 100 T attempt rejected ("must take the whole"); exactly 220 T accepted bulk_critical_matrix
BS11 Wheat rides CW4 only, grains ride PW2 only expectWagonType CW4 for wheat / PW2 for grains on their trains export_ledger_day + bulk_b1 (CW4 assert)
BS12 PW2 length tax: 17.066 m wagons on a 760 m board 44 slots by length vs CW4's 54 — same tonnage needs a longer consist doc-only (export_ledger_day runs the 37-wagon PW2 board)
BS13 Gross weight = tare + cargo (PW2: 37 × 95.2 = 3 522 T ≈ the 3 500 T pull limit) Weight axis binds before slots; overbooking by tare fraction impossible unit-tested in train-capacity.util + export_ledger_day
BS14 Tolerance spent only on a whole booking, never sizing a split Split offers budget against base caps doc-only (booking-batch.service.ts:4109; container twin g2)
BS15 Two bulk bookings, one over-weight last wagon Batch trims to whole wagons at full payload — no part-loaded squeeze into leftover pull weight doc-only (sizePartialOfferWagons fullWagonsOnly)
BS16 Sub-corridor 700 T (NAGAD→MOJO) shares the through-train with a 1 400 T DJIB_PORT→KALITY booking Both allocated to the same schedule bulk_critical_matrix

C. Break-bulk PER_ITEM (autos, machinery — NEW)

# Scenario Expected Coverage
BS17 16 automobiles @ 2.5 T (40 T). Tonnage alone says 1 wagon; floor says 4/wagon 4 wagons allocated — physical floor binds, rated capacity rides empty bulk_b2_per_item_floor
BS18 12 machines @ 20 T (240 T), no configured floor floor(70/20) = 3 per wagon → 4 wagons — tonnage fallback binds bulk_b2_per_item_floor
BS19 216 automobiles (540 T) — exactly 54 wagons Whole booking fits, no split; train FULL from one break-bulk booking bulk_b2_per_item_floor
BS20 Giant: 240 automobiles (60 wagons) on a 54-wagon train Whole-wagon offer of 216 autos; is_split = true; 54/54; FULL bulk_b3_per_item_giant_quantities
BS21 Giant's remainder: 24 autos outstanding 10-auto attempt rejected ("must take the whole"); exactly 24 accepted bulk_b3_per_item_giant_quantities
BS22 hazardousQuantity 12 on a 10-item line Engine CLAMPS to 10, returns 201 — no rejection (same gap as container S40). Spec asserts the clamp so the gap is visible bulk_b3_per_item_giant_quantities
BS23 reeferQuantity 3 on an 8-item line Stored on the booking (bulk_reefer_quantity = 3), reefer surcharge applies bulk_b3_per_item_giant_quantities
BS24 One item heavier than a whole wagon (80 T machine on a 70 T CW4) Engine still charges 1 wagon per item (ponytail: note in bulkItemWagonsRequired) — creation-time rejection does NOT exist gap — ticket, not a test

D. Export bulk (KALITY → DJIB_PORT)

# Scenario Expected Coverage
BS25 Sesame/grain export fills the PW2 board Whole-booking placement, board FULL bulk_export_full_train
BS26 Export FCFS: space taken by earlier holds Later booking sees reduced space bulk_export_fcfs_space
BS27 Export pay-or-lose: hold lapses at deadline Space returns, next customer takes it bulk_export_pay_or_lose
BS28 Export whole-or-nothing (no split) Oversized booking 409s with a sized message bulk_export_matrix
BS29 Export matrix: currencies × customs Per-combination invoice + clearance behavior bulk_export_matrix
BS30 "Export never splits" is flag-dependent Assert FREIGHT_EXPORT_SPLIT !== "true" or the suite is vacuous noted in SCENARIO_ENGINE_NOTES.md
BS31 Export day ledger: who booked/rode/expired/refused Ledger report written per day export_ledger_day

E. Corridor ops · intercity · disruptions

# Scenario Expected Coverage
BS32 Bulk intercity ride-along (MOJO→KALITY, DOMESTIC, dateless) accepted onto the import train's free leg Pay window opens on accept; paid + linked bulk_critical_matrix
BS33 Checkpoint-by-checkpoint corridor run; bookings ARRIVED at terminal Statuses walk IN_TRANSIT → ARRIVED; ≥54 wagon-movement ledger rows bulk_import_full_train
BS34 Mid-corridor auto-unload: booking destined MOJO on a KALITY train Checkpoint at MOJO auto-unloads it (booking-journey.service.ts:261) container twin g6_corridor — same engine path
BS35 Schedule cancelled after bulk allocation Frozen wagon_allocation_snapshot, bookings re-pool still PAID container twin g7_disruptions — same engine path
BS36 Paid bulk booking, yard short of CW4 → transfer request WAITING_FOR_WAGON → PARTIALLY_FULFILLED → FULFILLED → placed fleet_wagon_transfer

F. Customs tail · validation

# Scenario Expected Coverage
BS37 Customs bookings: gatepass → T1 → dispatch → T1 close → risk → second duty → release → final invoice Full milestone tail COMPLETED in order bulk_import_full_train
BS38 Self-clearance bookings arrive with NO customs tail Zero T1_CLOSED milestones bulk_import_full_train
BS39 importReleaseGranted not gated on second duty Release grantable with SECOND_DUTY_PAID pending — known gap SCENARIO_ENGINE_NOTES.md §S36
BS40 Out-of-order checkpoint relocates rolling stock silently No sequence guard in recordCheckpointknown bug SCENARIO_ENGINE_NOTES.md §S29