mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
102 lines
8.6 KiB
Markdown
102 lines
8.6 KiB
Markdown
# Bulk scenario catalog — BS1–BS40
|
||
|
||
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, S1–S40).
|
||
|
||
**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 |
|