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

102 lines
8.6 KiB
Markdown
Raw 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.

# 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 `recordCheckpoint`**known bug** | `SCENARIO_ENGINE_NOTES.md` §S29 |