# EDR Freight — Operations QA Test Plan End-to-end test flows from booking through warehouse, rail, and delivery — import and export, with and without first/last mile, self-haul and EDR haulage. Every status, guard, and endpoint below is taken from the code, not assumed. | | | |---|---| | **Branch** | `Truckdetantion` | | **Scope** | Warehouse · Fees · Allocation · First mile · Last mile | | **Depth** | Tester steps + technical refs | --- ## 00 · Test data setup Nothing below passes without this. Set it up once per environment and confirm each line before opening a single flow. - [ ] **Warehouse tree.** At least one `ACTIVE` warehouse with a yard and a zone. Capacities are in **tonnes**, not kg. - [ ] **Company has a linked portal user.** Critical — in-app notifications resolve recipients from the company's portal users. With none linked, `notify()` logs `0 recipients — skipped` and stores nothing. SMS/email still fire. - [ ] **Customer saved signature.** Required for Approve delivery; without it the API returns *"Please save your signature before approving delivery"*. - [ ] **Allocation rules** covering the freight type and trade direction under test (see §08), or accept the capacity-balanced fallback. - [ ] **Fee rules** — at least one each of `STORAGE_FEE`, `DEMURRAGE_FEE`, `DOUBLE_HANDLING_FEE`, `TRUCK_DETENTION_FEE` (see §07). - [ ] **Drivers and vehicles** registered; a train schedule with wagons for the route under test. - [ ] **Booking reaches `PAID`.** Receive-to-warehouse skips any booking that is not PAID. - [ ] **Container numbers are ISO 6346** — 4 letters + 7 digits, uppercase (`ABCU1234567`). Enforced at booking input and at every reference point. > **Direction is derived, not declared.** Receive-to-warehouse computes trade direction from the > *origin and destination yard countries*, not the booking's stored `trade_direction`. A booking > whose route says IMPORT will be skipped from an EXPORT receive with *"Booking route is IMPORT, > not EXPORT"*. Set up yards accordingly. --- ## 01 · Lifecycle reference The three state machines a tester needs to read a failure. Anything not listed as an allowed transition is rejected by `assertTransition`. ### Warehouse inventory transitions | From | Allowed next | Notes | |---|---|---| | `UNLOADED` | `STORED`, `READY_FOR_PICKUP` | Import landing state after train unload | | `RECEIVED` | `STORED`, `READY_FOR_PICKUP` | Export landing state after truck receive | | `STORED` | `RESERVED`, `READY_FOR_LOADING` | Reserve is retired from the UI; `STORED → READY_FOR_LOADING` is the live path | | `READY_FOR_LOADING` | `LOADED` | Onto a wagon | | `LOADED` | `DISPATCHED` | | | `DISPATCHED` | `UNLOADED_AT_DJIBOUTI_PORT` | Export only, at Djibouti | | `READY_FOR_PICKUP` | `DELIVERED`, `STORED`, `DISPATCHED` | Import; may be put back into storage | ### Container item stages ``` PENDING → RECEIVED → GRN → ASSIGNED → LOADED → LEFT → DELIVERED ``` **ASSIGNED** means the customer picked which containers ride which truck — planning only. **LOADED** requires the operator to actually load them, and only after the truck has arrived (`loaded_at` is stamped then). Assignment alone must never show LOADED. ### First mile & last mile | Leg | Statuses, in order | Gate it controls | |---|---|---| | First mile | `PAYMENT_PENDING → READY_TO_TRANSIT → IN_TRANSIT → RECEIVED_TO_PORT` | Export receive is blocked until `RECEIVED_TO_PORT` | | Last mile | `PAYMENT_PENDING → READY_TO_TRANSIT → IN_TRANSIT → DELIVERED` | Truck-detention window: `arrivedAt` (reached destination) → `deliveredAt` (vehicle returned) | --- ## 02 · Export — without first mile Customer brings the cargo to the facility themselves. The happy path from a paid booking to cargo unloaded at Djibouti port with an interchange document. | Step | Tester action | Expected result | Technical ref | |---|---|---|---| | E1.1 | Create an export booking (route origin ET → destination DJ), pay it. | Booking reaches `PAID`. | Container numbers must be ISO 6346 | | E1.2 | Export Operations → **Receive for Loading**. Select the booking, capture the truck entrance (plate, driver, weights), pick warehouse/yard/zone. | Inventory created; **GRN issued** as `GRN-EXPORT-YYYYMMDD-XXXXXXXX`. Customer gets a receive SMS. | `POST /warehouse-inventory/receive-bulk` | | E1.3 | Store the item — auto-allocate, or pick warehouse/yard/zone in the Store modal. | Status `STORED`; note records "allocation rule", "capacity-balanced", or "operator-selected". | Capacity decremented in tonnes | | E1.4 | Inspect: mark selected items as inspected, outcome **PASSED**. | Export items advance straight to `READY_FOR_LOADING`. | Reserve step is retired | | E1.5 | Ready To Load tab → load onto the allocated wagon. | Status `LOADED`; a warehouse loading record exists. | Requires an allocated wagon | | E1.6 | Download the **export marshalling / load list** PDF. | PDF lists the train's wagons, bookings, containers. | train-scheduling controller | | E1.7 | Dispatch Queue → dispatch. | Status `DISPATCHED`. Customer receives **"Shipment dispatched"** naming origin → destination. | Per booking on the schedule | | E1.8 | Move the schedule to arrived at the Djibouti-side port. | Train appears in the Djibouti unloading queue. Customer receives **"Shipment arrived"**. | status `ARRIVED` / `ARRIVED_AT_DJIBOUTI` | | E1.9 | Grant the gate pass for the train, then **Unload at Djibouti**. | Items become `UNLOADED_AT_DJIBOUTI_PORT` and an **interchange document** is generated. | Unload checks `gatepass_granted_at` | | **E1.G** | Try to unload at Djibouti *before* granting the gate pass. | 🚫 **Blocked.** Items skipped with a gate-pass reason; no interchange document. | See §06 | --- ## 03 · Export — with first mile EDR collects the cargo from the customer's premises. Identical to §02 from the store step onward; the difference is entirely in the gate before receive. | Step | Tester action | Expected result | Technical ref | |---|---|---|---| | E2.1 | Create the export booking with a **first-mile pickup address** (or a service type that includes first mile). | Booking is flagged `hasFirstMile`. | Derived from address *or* `service_types.includes_first_mile` | | E2.2 | Create the first-mile request; assign a vehicle and driver. | Driver receives an SMS naming the vehicle, booking, pickup and destination. | First Mile page | | E2.3 | Walk the leg: `READY_TO_TRANSIT → IN_TRANSIT → RECEIVED_TO_PORT`. | Each transition persists. | | | E2.4 | Now run **Receive for Loading**. | Booking is received; truck entrance pre-filled from the first-mile vehicle and driver. | Then continue at E1.3 | | **E2.G1** | Attempt receive with **no first-mile request created**. | 🚫 Skipped: *"First-mile request not created"*. | | | **E2.G2** | Attempt receive while first-mile status is `IN_TRANSIT`. | 🚫 Skipped: *"First-mile truck has not arrived"*. | Only `RECEIVED_TO_PORT` passes | --- ## 04 · Import — self-haul (customer collects) The longest flow, and the one with the most guards. The customer assigns their own trucks, signs a booking-level handover, and collects. Test this one first — it exercises truck arrival, loading, weighing, handover, exit paper, and fees. ``` train arrives → unload → inspect → ready for pickup → assign truck → truck arrival → sign handover → load → truck leaving → exit paper → deliver ``` | Step | Tester action | Expected result | Technical ref | |---|---|---|---| | I1.1 | Import booking (route DJ → ET), paid, **no last-mile address** and a service type that excludes last mile. | Booking is self-haul. | Drives `usesCustomerTruck` | | I1.2 | Import Operations → **Arrival Queue**. Open the arrived train, assign warehouse/yard/zone per booking, **Auto Unload Arrived Bookings**. | Inventory created at `UNLOADED`. Counter shows `n/total unloaded`. | Train must be `ARRIVED` | | I1.3 | Inspect the item, outcome **PASSED**. | Item advances to `READY_FOR_PICKUP`. Customer receives **"Assign a truck for pickup"** (in-app + SMS + email). | Fires only when self-haul *and* no truck assigned | | I1.4 | **Portal:** customer assigns truck(s), entering ISO container numbers per truck. | Containers move to stage `ASSIGNED` and show their planned truck. Booking becomes `TRUCK_ASSIGNED`. | 20ft → max 2/truck; 40ft → 1/truck; trucks ≤ containers | | I1.5 | Backoffice row menu → **Truck Arrival**. Select the assigned truck, record gate-in time and **tare** weight (tonnes). | Truck stamped arrived. A **SELF_HAUL handover** is generated (booking-level) and the customer is notified to sign, on all three channels. | Truck Arrival disabled until a truck is assigned | | I1.6 | **Portal:** customer opens the booking → **Approve delivery**. | The handover PDF opens for review; approving applies their saved signature and returns the signed PDF. | Signs all unsigned handovers on the booking | | I1.7 | Open the container list → select the assigned containers → **Load onto truck** (pick the arrived truck). | Containers move to `LOADED`; `loaded_at` stamped. | Only arrived, not-departed trucks are listed | | I1.8 | Row menu → **Truck Leaving**. Select the containers on the truck, record gate-out time and **gross** weight. | Net is computed from the selected containers' cargo weight and must equal `gross − tare`. Release document issued. | Weight match enforced client- *and* server-side | | I1.9 | Generate the **exit paper** for the truck. | PDF lists the truck, driver, and its containers. Containers move to `LEFT`. | Requires signed handover + cleared fees | | I1.10 | **Deliver** the inventory, recording the receiver name. | Status `DELIVERED`; handovers stamped delivered. | Requires handover signed *and* truck departed | > 🚫 **The single most likely bug you will hit.** Exit paper returns `400` when the handover is not > fully signed, or when a warehouse fee invoice is `ISSUED` / `PARTIALLY_PAID`. In the container > list, the Exit Paper button turns grey and clicking it **sends the customer a signature request** > instead of erroring. That is correct behaviour — verify the message, don't file it as a bug. --- ## 05 · Import — EDR last mile EDR delivers to the customer's door. No customer truck, no portal Approve delivery, and the handover is *per delivering truck* — not per booking. This is where truck detention accrues. | Step | Tester action | Expected result | Technical ref | |---|---|---|---| | I2.1 | Import booking with a **last-mile delivery address** (or service type including last mile). | `hasLastMile` is true; no "assign a truck" notification is sent. | EDR haulage — customer assigns nothing | | I2.2 | Unload from the arrived train, inspect PASSED. | Item becomes `READY_FOR_PICKUP` and the last-mile leg is accepted automatically. | | | I2.3 | Last Mile page: assign vehicle + driver. | Leg reaches `READY_TO_TRANSIT`. Row shows Assigned. | Driver notified by SMS | | I2.4 | **Truck Arrival** from the Last Mile row: gate-in, tare weight. | Weighing saved; the assigned last-mile truck is pre-selected. | Re-opening must show the saved details | | I2.5 | **Truck Leaving**: gate-out, gross weight. | Release document issued; leg moves to `IN_TRANSIT`. `arrivedAt` starts the detention clock. | Detention window opens | | I2.6 | Deliver at the customer's door, recording the receiver name. | Item `DELIVERED`. An **EDR_LAST_MILE handover is generated per delivering truck**, resolved from the container's allocated vehicle. | Not booking-level | | I2.7 | Return the vehicle → mark the leg `DELIVERED`. | `deliveredAt` stamped; detention clock stops. | | | I2.8 | Preview, then generate the **truck detention invoice**. | Charged per truck per day beyond the grace hours, at the matching tier. See §07. | `POST /last-mile/:id/generate-truck-detention-invoice` | | **I2.G** | Open the booking in the portal. | 🚫 **Approve delivery must NOT appear.** The portal flag counts only `SELF_HAUL` handovers; EDR handovers are signed by the receiver at the door. | Regression check | --- ## 06 · Marshalling, gate pass, interchange Documents are generated, not uploaded. Each has a precondition; test the precondition, not just the PDF. | Document | When | Precondition | Verify | |---|---|---|---| | **GRN** | On receive to warehouse | Booking `PAID`; route direction matches; export needs a truck entrance | Number format `GRN-