Files
edr-platform/docs/qa/edr-freight-qa-test-plan.md
2026-07-09 15:37:06 +00:00

23 KiB
Raw Permalink Blame History

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-<DIR>-<YYYYMMDD>-<8>; PDF opens; customer SMS sent
Import load list / marshalling Import train, before unload Schedule has bookings assigned Summary endpoint and printable PDF agree; portrait/landscape both render
Export marshalling / load list Export train, after loading Items LOADED onto wagons Wagon, booking, container rows are complete
Gate pass Djibouti-side operations Granted per schedule gatepass_granted_at is set; export Djibouti unload reads this same field
Interchange document Automatically, after a successful Djibouti unload At least one item unloaded Document number returned in the unload response; visible in Interchange Documents
Handover Self-haul: on truck arrival. EDR: at delivery. See §04 / §05 Self-haul is booking-level, one per booking; EDR is one per delivering truck
Exit paper / release doc Truck leaving Handover fully signed and no unpaid warehouse fee Weights on the paper match the gate weighing

⚠️ Cross-module quirk worth confirming with the team. The export Djibouti unload checks the gate-pass flag stored on the import Djibouti operations record (import_djibouti_operations.gatepass_granted_at). It works, but it is surprising. If an export unload silently skips every item, check that field first.


07 · Fee rules

Four rule types. Two are day-based with free days and tiers; double handling is a flat rate multiplied by a basis; truck detention is hour-graced and vehicle-scoped.

Rule type Charged on Key fields Test cases
STORAGE_FEE Days in storage free days, tiers Within free days → zero. One day past → tier 1. Cross a tier boundary → correct tier rate.
DEMURRAGE_FEE Days beyond free time free days, tiers Same boundary tests. Confirm it blocks exit paper and delivery while ISSUED.
DOUBLE_HANDLING_FEE Flat rate × quantity basis: PER_CONTAINER | PER_TON | PER_ITEM Container booking → PER_CONTAINER uses container count. Bulk → PER_TON uses tonnage. Break-bulk machinery → PER_ITEM uses item count. Import only. Free days and tiers must not apply.
TRUCK_DETENTION_FEE Per truck, per day free_hours grace, tiers, vehicle type scope Return inside the grace window → zero. Just past grace → day 1 at tier 1. Multi-day → tier escalation. A vehicle type outside the rule's scope → no charge. Import only.

Fee behaviour to verify on every rule

  • Preview before invoice. The preview amount must equal the issued invoice total.
  • Notification on issue. Issuing a warehouse fee invoice sends the customer an in-app INVOICE_ISSUED notification and an SMS, deep-linked to pay.
  • Clearance gate. While a warehouse-source invoice is ISSUED or PARTIALLY_PAID, exit paper, terminal release, and Approve delivery are all blocked.
  • Payable-but-uninvoiced. If fees are payable and no invoice exists yet, release is still blocked with "Generate and fully pay…". Confirm the operator can generate it from that state.
  • Fully paid → release proceeds; a receipt PDF is available.
  • Edit a rule (rate, free days, tiers, grace hours) and confirm the next preview reflects it.

08 · Allocation rules

Where an item is stored is decided by the first matching rule, in priority order. Test the precedence, not just one rule.

Match criteria (any may be null = wildcard) Targets
freight_type, trade_direction, cargo_type_code, container_status, requires_inspection, ordered by priority target_facility_code, target_warehouse_code, target_yard_code (required), target_zone_code, storage_type

Precedence, highest first

# Source of the location How to trigger Note recorded
1 Operator selection Store modal → pick warehouse + yard + zone "Stored at operator-selected location"
2 Allocation rule Leave the Store modal blank; a matching rule exists "Stored by allocation rule <name>"
3 Capacity-balanced fallback Leave blank; no rule matches "Stored by capacity-balanced allocation"
  • Two matching rules → the lower priority number wins.
  • Yard dropdowns are filtered by freight type — container bookings offer container yards only.
  • Only ACTIVE warehouses, yards and zones are selectable.
  • Storing beyond a zone's capacity is rejected; capacities are compared in tonnes.
  • Move an item to another warehouse/yard/zone → capacity released at source, taken at destination.
  • Edit a rule, an existing warehouse, a yard, and a zone — all four must be editable.

09 · Negative & guard cases

Every row here is intended behaviour. The test passes when the action is refused with the stated message. Anything that succeeds is the bug.

Area Attempt Expected refusal
Booking Enter a container number that is not 4 letters + 7 digits (e.g. MSKU10105185, 3456789). "Enter a valid ISO container number". Lowercase is auto-uppercased; input capped at 11 characters.
Booking Enter the same container number twice in one shipment. "Duplicate container number in this shipment."
Receive Receive a booking that is not PAID. Skipped: "Booking not PAID".
Receive Receive the same booking twice. Skipped: "Already received".
Receive Export receive with no truck entrance captured. Rejected before any inventory is created.
Truck assign Put two 40ft containers on one truck. "A 40ft container fills the truck — assign only 1 container to this truck".
Truck assign Put three containers on one truck. "A truck carries at most 2 containers".
Truck assign Assign more trucks than the booking has containers. "Cannot assign more trucks than containers…".
Truck assign Assign a container from another booking, or one already on another truck. "…is not one of this booking's containers" / "…already loaded onto another truck".
Truck assign Edit a truck after it has arrived. Refused — edits are allowed only until arrival.
Loading Load containers onto a truck that has not arrived. "Record the truck arrival before loading…". The truck picker lists only arrived, not-departed trucks.
Loading Load onto a truck that has already departed. "This truck has already left — its load is locked".
Stages Customer assigns containers to a truck, then check the container list. Stage is ASSIGNED, never LOADED. Exit Paper is not offered.
Truck leaving Enter a gross weight where gross tare ≠ the selected containers' cargo weight. "Weight mismatch…". Exit paper and gate clearance blocked, client and server.
Truck leaving Save leaving with no containers selected. "Select the containers loaded on this truck".
Exit paper Generate before the handover is signed. "Handover must be signed…". In the container list the button is grey and instead sends the customer a signature request.
Exit paper Generate with an ISSUED demurrage/storage invoice. "…must be fully paid before terminal release".
Approve delivery Approve without a saved signature. "Please save your signature…" and the portal routes to the signature page.
Approve delivery Approve before warehouse inspection has passed. "Delivery can be approved after warehouse inspection has passed".
Approve delivery Approve when a truck is assigned but has not arrived. "Customer truck arrival must be recorded before delivery approval".
Deliver Deliver before a release order was issued, or before the self-haul truck has left. "A release order must be issued…" / "Deliver is available only after the customer truck has left".
Djibouti unload Unload an export train with no gate pass granted. Items skipped; no interchange document generated.
Warehouse Store into an INACTIVE warehouse/yard/zone, or beyond capacity. Not selectable / "No active warehouse yard/zone is available" / capacity error.

10 · Notifications

All customer notifications land in the same portal inbox. Verify the message, the deep link, and — where noted — the SMS and email.

Notification Fires when Channels Deep link
Shipment dispatched Train schedule dispatched, per booking In-app, SMS, email Booking
Shipment arrived Train schedule arrived, per booking In-app, SMS, email Booking
Assign a truck for pickup Export: on warehouse receive. Import: on inspection pass → ready for pickup. Only if self-haul and no truck assigned. In-app, SMS, email Booking → assign truck
Handover — signature needed Self-haul truck arrives (handover generated), and re-sent when an operator requests a signature from the Exit Paper button In-app, SMS, email Booking → approve delivery
Warehouse fee due Storage / demurrage invoice issued In-app, SMS Booking → pay
Wagon allocated / payment window Scheduling In-app Booking

🚫 Do not chase a missing in-app notification before checking this. Recipients are resolved from the company's linked portal users. If a company has none, notify() logs "0 recipients — skipped" and stores nothing — the notification simply never appears, with no error. SMS and email still go out, because they address the company's phone and email directly. On a fresh environment this is the usual explanation.


11 · Known open issues

Do not raise duplicates for these. Each is already identified.

Status Issue Impact on testing
🔴 Open Export receive returns 500 on the deployed environment (POST /warehouse-inventory/receive-bulk). Blocks flows E1 and E2 at step .2. Awaiting the response body / server log to diagnose. Likely schema drift, not the SQL.
🟠 Fix pending deploy Export receive was extremely slow. The owner SMS was awaited inside the DB transaction, and the SMS client had no HTTP timeout. Fixed on branch: SMS now has a timeout, and notifications are sent after commit. Re-test receive latency once deployed.
🟠 Data Drivers table has no unique constraints on licence number, email, or phone, despite the entity declaring them unique. Duplicate drivers can be created. Do not rely on uniqueness in test assertions.
🔵 Behaviour Handover generated before truck arrival. If an operator triggers a signature request from the Exit Paper button while a truck is assigned but not arrived, Approve delivery refuses with "truck arrival must be recorded". Only reachable off the normal path. Follow flow I1 in order and it will not occur.