# EDR Freight — One Master Flow (signup → every end)
Single comprehensive graph of the entire freight business logic: from customer signup through every
branch to every terminal state. Actor-coloured, endpoint-labelled.
**Legend**
(P) Portal (customer) · (B) Backoffice (staff) · (sys) System/auto (event, cron, service-to-service)
Rounded green = success end · Red = failure/terminal end · Diamond = decision · Hexagon = domain event.
```mermaid
flowchart TD
classDef start fill:#e0e7ff,stroke:#3730a3,color:#111
classDef port fill:#dbeafe,stroke:#2563eb,color:#111
classDef back fill:#fef3c7,stroke:#b45309,color:#111
classDef sys fill:#dcfce7,stroke:#15803d,color:#111
classDef dec fill:#f8fafc,stroke:#475569,color:#111
classDef good fill:#86efac,stroke:#166534,color:#062e14
classDef bad fill:#fecaca,stroke:#991b1b,color:#450a0a
%% ================= PHASE 1: IDENTITY & ONBOARDING =================
S0(["Customer visits portal"]):::start
S0 --> S1["Signup via IAM
GET /auth/check-availability @Public
POST /otp/send + /otp/verify (P)"]:::port
S1 --> S2{"Identity proofing
(VeriFayda)?"}:::dec
S2 -->|"Yes"| S3["POST /fayda/verification/start →
/fayda/callback → /complete
upsert iam.users (verified_by=fayda) (P)"]:::port
S2 -->|"No"| S4
S3 --> S4["POST /companies/onboarding/start
draft company (placeholder TIN, PENDING) (P)"]:::port
S4 --> S4b["Wizard: PATCH /profile, /onboarding-step,
upload license + docs
GET /onboarding/requirements (P)"]:::port
S4b --> S5["POST /companies/onboarding/complete
re-validate → company+profiles = PENDING (P)"]:::port
S5 --> S6{"Backoffice reviews profile
PATCH /company-profiles/:id/status (B)"}:::dec
S6 -->|"Reject / suspend"| S6x(["SUSPENDED / BLACKLISTED
cannot transact"]):::bad
S6 -->|"Approve"| S7["Mint reference (EX-#####),
company → ACTIVE (B)"]:::back
S7 --> S8{"Start a shipment?"}:::dec
S8 -->|"idle"| S8x(["No booking (dormant account)"]):::bad
S8 -->|"Yes"| MODE
%% ================= PHASE 2: COMMERCIAL ORIGIN =================
MODE{"Booking origin?"}:::dec
MODE -->|"One-time shipment"| B0
MODE -->|"Framework agreement"| C1
%% ---- Contract track ----
C1["Create CONTRACT DRAFT
POST /contracts (routes + cargo scope) (P)"]:::port
C1 --> C1d{"Abandon draft?"}:::dec
C1d -->|"delete"| C1x(["Contract removed (soft-delete)"]):::bad
C1d -->|"continue"| C2["generate-price → submit → SUBMITTED
freeze contract_rate_snapshots (P)"]:::port
C2 --> C2p{"Price changed?"}:::dec
C2p -->|"Yes"| C2c["confirm-submit → SUBMITTED (P)"]:::port
C2p -->|"No"| C3
C2c --> C3
C3{"Staff intake
POST /contracts/:id/staff/* (B)"}:::dec
C3 -->|"request-changes"| C3r["CHANGES_REQUESTED (B)"]:::back
C3r -->|"edit + resubmit"| C2
C3 -->|"reject"| C3x(["Contract REJECTED"]):::bad
C3 -->|"accept"| C4["→ PENDING_APPROVAL
instantiate approval steps (B)"]:::back
C4 --> C4a{"Approval chain
line → director → ceo (B)"}:::dec
C4a -->|"rejectStep"| C4x(["Contract REJECTED"]):::bad
C4a -->|"all approve"| C5["generate-contract → CONTRACT_READY (B)"]:::back
C5 --> C6["Customer sign → SIGNED_CUSTOMER
POST /contracts/:id/contract/sign (P)"]:::port
C6 --> C7{"Staff counter-sign branch"}:::dec
C7 -->|"customs on"| C7a["AWAITING_CLEARANCE_DOCUMENTS (B)"]:::back
C7 -->|"GENERAL"| C7b["CONTRACT_ACTIVE (B)"]:::back
C7 -->|"ONE_TIME"| C7c["FULLY_EXECUTED (B)"]:::back
C7a --> CPATH
C7b --> CPATH
C7c --> CPATH
C7b -.->|"renew"| C7renew(["RENEWAL_DRAFT → new cycle"]):::bad
C7b -.->|"lapse"| C7exp(["Contract EXPIRED"]):::bad
CPATH{"How are shipments booked
under the contract?"}:::dec
CPATH -->|"Path A: transport-only"| CPA["Ops review self-clearance
ops-review → ops-finalize → SELF_CLEARED (B)
then customer books direct
POST /contracts/:id/bookings (P)"]:::port
CPATH -->|"Path B: GENERAL + customs"| CPB["Customer submits BookingRequest
POST /contracts/:id/booking-requests (P)"]:::port
CPB --> CPBq{"GL queue decision (B)"}:::dec
CPBq -->|"reject / customer cancels"| CPBx(["BookingRequest REJECTED / CANCELLED"]):::bad
CPBq -->|"accept → GL creates booking"| B0u
CPA --> B0u["Booking created UNDER contract
(window + capacity draw-down check) (sys)"]:::sys
B0u --> BFLOW
%% ---- One-time booking ----
B0["Create BOOKING DRAFT
POST /bookings (reference, containers,
cargo modifiers, files) (P)"]:::port
B0 --> B0d{"Abandon draft?"}:::dec
B0d -->|"delete"| B0x(["Booking removed (soft-delete)"]):::bad
B0d -->|"continue"| BFLOW
B0 -.->|"consolidation"| BCONS(["PENDING_CONSOLIDATION
waits for partner shipment
(shares a wagon) → rejoins"]):::sys
BCONS -.-> BFLOW
%% ================= PHASE 3: PRICING & SUBMIT =================
BFLOW["Configure shipment
freight type + trade direction"]:::sys
BFLOW --> FT{"Freight type?"}:::dec
FT -->|"CONTAINER"| DIR
FT -->|"BULK"| DIR
DIR{"Trade direction?"}:::dec
DIR -->|"EXPORT"| B1
DIR -->|"IMPORT"| B1
DIR -->|"DOMESTIC"| B1
B1["POST /bookings/:id/generate-price
rule-engine: LIVE rates + surcharges
HAZARDOUS / REEFER / OVERWEIGHT /
SHIPPING_LINE / CONSOLIDATION (P)"]:::port
B1 --> B1w{"weight-limit-rules check"}:::dec
B1w -->|"VGM > maxCapacity"| B1x(["HARD BLOCK (400)
cannot submit"]):::bad
B1w -->|"over maxVgm, within cap"| B1warn["warning + OVERWEIGHT surcharge"]:::sys
B1w -->|"ok"| B2
B1warn --> B2
B2["POST /bookings/:id/submit → SUBMITTED
create booking_rate_snapshot (P)"]:::port
B2 --> B2p{"Price moved since draft?"}:::dec
B2p -->|"Yes → PRICE_CHANGED_PENDING_CONFIRM"| B2c["confirm-submit → SUBMITTED (P)"]:::port
B2p -->|"No"| GOV
B2c --> GOV
B2 -.->|"customer rejects price"| B2x(["Booking REJECTED"]):::bad
%% ================= PHASE 4: INTAKE & APPROVAL =================
GOV{"Government booking?"}:::dec
GOV -->|"Yes"| GEXP["governmentExpedite →
PAID + schedulingStatus Eligible (B)"]:::back
GOV -->|"No (commercial)"| BI{"Staff intake
POST /bookings/:id/staff/* (B)"}:::dec
BI -->|"request-changes"| BIr["CHANGES_REQUESTED (B)"]:::back
BIr -->|"edit + resubmit"| B2
BI -->|"reject"| BIx(["Booking REJECTED"]):::bad
BI -->|"accept"| BA["→ PENDING_APPROVAL
instantiate approval steps
(set validity window) (B)"]:::back
BA --> BAc{"Approval chain
LINE_STAFF → DIRECTOR → CEO (B)"}:::dec
BAc -->|"rejectStep"| BAx(["Booking REJECTED"]):::bad
BAc -->|"all approve → APPROVED"| BC1
%% ================= PHASE 5: CONTRACT DOC & SIGN =================
BC1["contract/generate → CONTRACT_READY (B)"]:::back
BC1 --> BC2["Customer sign → SIGNED_CUSTOMER
POST /bookings/:id/contract/sign (P)"]:::port
BC2 --> BC3{"Staff counter-sign:
trade direction?"}:::dec
BC3 -->|"IMPORT / EXPORT
(clearance gate, even if customs off)"| CL1
BC3 -->|"DOMESTIC"| FEXD["counter-sign → FULLY_EXECUTED
enqueue batch (skips clearance + op-request) (sys)(B)"]:::back
FEXD --> FEB
%% ================= PHASE 6: CUSTOMS CLEARANCE =================
CL1["AWAITING_DOCUMENTS → customer uploads
POST /bookings/:id/clearance/documents
→ DOCUMENTS_UNDER_REVIEW (P)"]:::port
CL1 --> CL2{"GL reviews each doc
clearance/review (B)"}:::dec
CL2 -->|"Query"| CL2q["doc queried → customer re-uploads (B)"]:::back
CL2q --> CL1
CL2 -->|"Approve all"| CL3["finalize (100% approved) → CLEARANCE_READY (B)"]:::back
CL3 --> CLph["Phased ET/DJ (as applicable):
declaration → duty advise → duty slip →
transit permit → delivery/release order →
T1 docs/close → export release (sys)(B)"]:::back
CLph --> OP1
%% ================= PHASE 7: OPERATION REQUEST =================
OP1["clearance/proceed: pick binding schedule day
→ OPERATION_REQUEST_PENDING (P)"]:::port
OP1 --> OP2{"Operations review
POST /bookings/:id/operation/review (B)"}:::dec
OP2 -->|"REQUEST_CHANGES"| OP2c["OPERATION_CHANGES_REQUESTED (B)"]:::back
OP2c --> OP1
OP2 -->|"ACCEPT"| OPM{"Operation mode?"}:::dec
OPM -->|"TRAIN (rail)"| OP3t["invoice generated → FULLY_EXECUTED
(day batch pool) (B)"]:::back
OPM -->|"ROAD (truck)"| OP3r["invoice generated →
ROAD_DISPATCH_PENDING (billed by KM) (B)"]:::back
OP3t --> FEB["batch engine offers wagons →
SELECTED_FOR_BATCH (sys)"]:::sys
%% ================= PHASE 8: INVOICE & PAYMENT =================
GEXP --> SCH
FEB --> PAY1
OP3r --> PAY1
PAY1["Invoice (source=booking, INV-YYYYMMDD-#####, due +14d)
booking invoice starts DRAFT → ISSUED at operation-accept (sys)"]:::sys
PAY1 --> PAY2["Customer pays
POST /billing/my-invoices/:id/pay →
billing.payInvoice → payment-api initiate (P)"]:::port
PAY2 --> PAYp{"Provider result
(Telebirr/CBE/EBirr/Waafi/DMoney/Card/CAC)"}:::dec
PAYp -->|"FAILED"| PAYf["invoice stays OPEN (retry)"]:::sys
PAYf --> PAY2
PAYp -->|"pay window lapses"| PAYexp(["Booking/reservation EXPIRED"]):::bad
PAYp -->|"SUCCEEDED"| PAYok["webhook → payment outbox →
POST /internal/payments/mark-paid →
settleByPaymentId → invoice PAID (sys)"]:::sys
PAYok --> EVT1{{"booking.invoice.paid event"}}:::sys
EVT1 --> PAID["Booking → PAID"]:::sys
PAYok -.->|"post-pay"| PAYref(["REFUNDED (terminal)"]):::bad
PAID --> FMQ0
EVT1 -.->|"if EXPORT + first-mile"| FM1
PAID --> SCH
%% ================= PHASE 9: SCHEDULING & ALLOCATION =================
SCH["schedulingStatus = Eligible (sys)"]:::sys
SCH --> SC2["Train schedule DRAFT
POST /train-scheduling/{container|bulk}/schedules
≥2 locomotives, derive direction (B)"]:::back
SC2 --> SC3["assign-bookings + run-allocation
(wagon_booking_allocations) (B)"]:::back
SC3 --> SC4["pin physical wagons → finalize → SCHEDULED
bookings → Scheduled (B)"]:::back
SC4 -.->|"cancel schedule"| SC4x["bookings back to Eligible (B)"]:::back
SC4x -.-> SC2
SC4 -.->|"gov preempt / maintenance"| RESCH["reschedule: retained / displaced /
readmitted (priority: gov first) (B)"]:::back
RESCH -.-> SC3
SC4 --> FMQ0
%% ================= PHASE 10: FIRST-MILE (export origin road leg) =================
FMQ0{"EXPORT + first-mile requested?"}:::dec
FMQ0 -->|"Yes"| FM1["first-mile leg auto-created
firstMile.acceptBooking (READY_TO_TRANSIT) (sys)"]:::sys
FMQ0 -->|"No"| WO1
FM1 --> FM2["setVehicles → vehicle BUSY, SMS driver,
fleet_events (B)"]:::back
FM2 --> FM3["IN_TRANSIT (needs vehicle) →
RECEIVED_TO_PORT (free vehicles) (B)"]:::back
FM3 --> FM4["first-mile invoice (FIRST_MILE fee) (B)"]:::back
FM4 --> WO1
%% ================= PHASE 11: WAREHOUSE ORIGIN (export) =================
WO1["receive / bulkReceive → RECEIVED
capacity assert, GRN, notify owner (B)"]:::back
WO1 --> WO2{"inspection outcome"}:::dec
WO2 -->|"FAILED / NEEDS_REVIEW"| WO2f["hold + re-inspect (B)"]:::back
WO2f --> WO2
WO2 -->|"PASSED"| WO3["store (allocation rule picks yard/zone) → STORED (B)"]:::back
WO3 --> WO4["reserve (booking PAID) → RESERVED (B)"]:::back
WO4 --> WO5["mark-ready-for-loading → READY_FOR_LOADING (B)"]:::back
WO5 --> WO6["load onto wagon → LOADED
(+ warehouse_loadings) (B)"]:::back
WO6 --> TR1
%% ================= PHASE 12: DISPATCH & TRANSIT =================
TR1["dispatch → DISPATCHED
assign train_number, locos ASSIGNED,
window CLOSED, unpaid reservations EXPIRED (B)"]:::back
TR1 --> TR2["record checkpoints (corridor stations) →
train_checkpoint_events (B)"]:::back
TR2 --> TRC["Customer tracking page
GET /tracking/:consignmentId (JWT) (P)"]:::port
TR2 --> TR3["arrive (final checkpoint) → ARRIVED
bookings IN_TRANSIT, locos+wagons freed,
warehouse arrival automation (B)"]:::back
TR3 --> WD1
%% ================= PHASE 13: WAREHOUSE DEST + IMPORT CUSTOMS =================
WD1["destination warehouse: auto-unload arrived
→ UNLOADED / RECEIVED (B)"]:::back
WD1 --> WD2{"inspection PASSED?"}:::dec
WD2 -->|"No"| WD2f["hold + re-inspect / incident report (B)"]:::back
WD2f --> WD2
WD2 -->|"Yes"| DIRW{"trade direction at destination"}:::dec
DIRW -->|"IMPORT"| WD3["READY_FOR_PICKUP (B)"]:::back
DIRW -->|"EXPORT (Djibouti)"| WDX["auto-unload-at-djibouti → DISPATCHED /
UNLOADED_AT_DJIBOUTI_PORT (B)"]:::back
WD3 --> IMP1["Import customs finalization:
upload docs → declaration → notify duties →
duties paid (needs slip) → assign risk →
release-permitted (all gates) (sys)(B)"]:::back
IMP1 --> LMQ
WDX --> ICD["interchange document (handover manifest)
generate-from-schedule → GENERATED →
ACKNOWLEDGED / DISPUTED (B)"]:::back
ICD --> DE1
%% ================= PHASE 14: LAST-MILE (import destination road leg) =================
LMQ{"IMPORT + last-mile requested?"}:::dec
LMQ -->|"Yes"| LM1["last-mile leg auto-created
(IMPORT inspection PASSED only) (sys)"]:::sys
LMQ -->|"No"| DE1
LM1 --> LM2["setVehicles → IN_TRANSIT → DELIVERED
(free vehicles) (B)"]:::back
LM2 --> LM3["last-mile invoice (LAST_MILE fee) (B)"]:::back
LM3 --> DE1
%% ================= PHASE 15: DELIVERY & COMPLETION =================
DE1{"Warehouse/storage fees fully PAID?"}:::dec
DE1 -->|"No"| DE1x["gate-clearance BLOCKED
findBlockingInvoice / assertClearanceAllowed (B)"]:::back
DE1x --> DE1p["Customer pays storage/demurrage
warehouse-fee-invoices/:id/pay-online (P)"]:::port
DE1p --> DE1
DE1 -->|"Yes"| DE2["release order (DO) + gate-clearance →
deliver (B)"]:::back
DE2 --> DE3["Customer approves delivery (saved signature)
POST /warehouse-inventory/bookings/:id/approve-delivery (P)"]:::port
DE3 --> DE4["inventory DELIVERED, POD to cargo,
container freed, capacity released (sys)"]:::sys
DE4 --> DONE(["Booking COMPLETED (done)
operations/complete"]):::good
%% ================= GLOBAL EXITS =================
GEXIT(["CANCELLED — POST /bookings/:id/cancel (staff-only)
ONLY from DRAFT, SUBMITTED, PRICE_CHANGED_PENDING_CONFIRM,
CHANGES_REQUESTED, PENDING_APPROVAL, CONTRACT_READY,
OPERATION_REQUEST_PENDING"]):::bad
B2 -.->|"cancel"| GEXIT
BA -.->|"cancel"| GEXIT
OP1 -.->|"cancel"| GEXIT
```
---
## Reading notes
- **Solid arrows** = the primary progression. **Dotted arrows** = optional / event-driven / exit hops
(consolidation, reschedule, cancel, the first-mile event branch).
- **Every terminal** is a rounded red or green node:
`SUSPENDED/BLACKLISTED`, `dormant`, `booking/contract removed`, `REJECTED` (customer price, staff intake,
approval step, GL booking-request), `HARD BLOCK` (VGM), `EXPIRED` (pay window / contract), `REFUNDED`,
`RENEWAL_DRAFT`, `CANCELLED`, and the single success end **`COMPLETED (done) `**.
- **Branch axes** captured: identity-proofing (Fayda / skip), origin (one-time vs contract Path A / Path B),
freight type (container / bulk), trade direction (import / export / domestic), customer type
(government expedite vs commercial approval chain), customs on/off, operation mode (rail / road),
provider outcome (success / fail-retry / expire / refund), first-mile (export), last-mile (import),
Djibouti export unload + interchange handover.
- **Actors**: (P) portal customer, (B) backoffice staff, (sys) system (events like `booking.invoice.paid`,
`warehouse.invoice.paid`, auto leg creation, payment webhook/outbox settlement).
> Endpoint-level tables, per-domain state machines, and the payment-microservice sequence live in
> [`FREIGHT_SYSTEM_FLOW.md`](./FREIGHT_SYSTEM_FLOW.md). This file is the single end-to-end picture.