# 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.