mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-09 02:28:18 +00:00
Merge pull request #492 from Tria-plc/freight/feat/fixes-v1
Freight/feat/fixes v1
This commit is contained in:
604
apps/edr-freight-api/docs/FREIGHT_FLOW_VARIANTS.md
Normal file
604
apps/edr-freight-api/docs/FREIGHT_FLOW_VARIANTS.md
Normal file
@@ -0,0 +1,604 @@
|
|||||||
|
# EDR Freight — Major Flow Variants (each self-contained)
|
||||||
|
|
||||||
|
The single master graph lives in [`FREIGHT_MASTER_FLOW.md`](./FREIGHT_MASTER_FLOW.md). This file breaks the
|
||||||
|
business logic into **one comprehensive, self-contained diagram per major scenario**, each organised with
|
||||||
|
phase **subgraphs** so it can be read on its own.
|
||||||
|
|
||||||
|
**Axes covered**
|
||||||
|
|
||||||
|
| Axis | Values |
|
||||||
|
| --------------- | -------------------------------------------------------------------------------------------- |
|
||||||
|
| Origin | **One-time booking** · **General contract** (Path A transport-only / Path B GENERAL+customs) |
|
||||||
|
| Trade direction | **Export** · **Import** · **Intercity / Domestic** |
|
||||||
|
| Customs | **With customs** · **Without customs** |
|
||||||
|
|
||||||
|
**Legend** — (P) Portal (customer) · (B) Backoffice (staff) · (sys) System/event · (green) rounded = success end · (red) rounded = fail end · <> decision.
|
||||||
|
|
||||||
|
**Which diagram do I read?**
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
classDef dec fill:#f8fafc,stroke:#475569,color:#111
|
||||||
|
classDef box fill:#e0e7ff,stroke:#3730a3,color:#111
|
||||||
|
A{"Origin?"}:::dec
|
||||||
|
A -->|"one-time"| B{"Trade direction?<br/>(gate is direction-driven, NOT a customs toggle)"}:::dec
|
||||||
|
A -->|"framework agreement"| C{"Contract type?"}:::dec
|
||||||
|
B -->|"DOMESTIC"| D1["§1 One-time · DOMESTIC (no gate)"]:::box
|
||||||
|
B -->|"IMPORT / EXPORT<br/>(with or without customs)"| D2["§2 One-time · IMPORT/EXPORT (gate)"]:::box
|
||||||
|
C -->|"transport-only (self-clearance)"| D3["§3 Contract · Path A"]:::box
|
||||||
|
C -->|"GENERAL + customs"| D4["§4 Contract · Path B"]:::box
|
||||||
|
D1 --> E{"Physical direction?"}:::dec
|
||||||
|
D2 --> E
|
||||||
|
D3 --> E
|
||||||
|
D4 --> E
|
||||||
|
E -->|"export"| F5["§5 EXPORT operations"]:::box
|
||||||
|
E -->|"import"| F6["§6 IMPORT operations"]:::box
|
||||||
|
E -->|"domestic"| F7["§7 INTERCITY operations"]:::box
|
||||||
|
```
|
||||||
|
|
||||||
|
> **How the two halves connect:** §1–§4 are the **commercial** journeys (intake → approval → contract →
|
||||||
|
> clearance → operation → payment). §5–§7 are the **physical** journeys (mile legs → warehouse → train →
|
||||||
|
> delivery). A shipment = one commercial variant **+** one physical variant. Each diagram fully details its
|
||||||
|
> own half and summarises the other so it stands alone.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §1 — One-time booking · DOMESTIC (no clearance gate)
|
||||||
|
|
||||||
|
The commercial lifecycle when the counter-sign gate resolves to **no clearance** — which, in code, means
|
||||||
|
**trade direction = DOMESTIC** (not a customs toggle). Counter-sign goes straight to `FULLY_EXECUTED` and the
|
||||||
|
booking is enqueued **directly into the scheduling batch pipeline, skipping the operation-request/clearance
|
||||||
|
phase**. NOTE: Import/export bookings — _even with customs off_ — do **not** land here; they always hit the
|
||||||
|
clearance gate (§2, just with a lighter "without customs" document set).
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
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
|
||||||
|
|
||||||
|
pre(["Company ACTIVE (approved profile)"]):::sys
|
||||||
|
|
||||||
|
subgraph DRAFT["1 · Create & price"]
|
||||||
|
direction TB
|
||||||
|
d1["POST /bookings → DRAFT<br/>reference, containers, cargo modifiers, files (P)"]:::port
|
||||||
|
d2["POST /bookings/:id/generate-price<br/>rule-engine: LIVE rates + surcharges (P)"]:::port
|
||||||
|
dW{"weight-limit-rules"}:::dec
|
||||||
|
dWx(["HARD BLOCK 400 — VGM > capacity"]):::bad
|
||||||
|
d3["POST /bookings/:id/submit → SUBMITTED<br/>freeze booking_rate_snapshot (P)"]:::port
|
||||||
|
dP{"price moved?"}:::dec
|
||||||
|
d3c["confirm-submit → SUBMITTED (P)"]:::port
|
||||||
|
d1 --> d2 --> dW
|
||||||
|
dW -->|"over capacity"| dWx
|
||||||
|
dW -->|"ok / warn+surcharge"| d3 --> dP
|
||||||
|
dP -->|"yes"| d3c
|
||||||
|
dP -->|"no"| out1
|
||||||
|
d3c --> out1
|
||||||
|
d1 -.->|"delete draft"| ddx(["removed"]):::bad
|
||||||
|
d3 -.->|"reject price"| drx(["REJECTED"]):::bad
|
||||||
|
end
|
||||||
|
out1[" "]:::sys
|
||||||
|
|
||||||
|
subgraph INTAKE["2 · Staff intake & approval"]
|
||||||
|
direction TB
|
||||||
|
g{"government?"}:::dec
|
||||||
|
gexp["governmentExpedite → PAID + Eligible (B)"]:::back
|
||||||
|
i{"staff/accept | request-changes | reject (B)"}:::dec
|
||||||
|
ir["CHANGES_REQUESTED (B)"]:::back
|
||||||
|
irx(["REJECTED"]):::bad
|
||||||
|
ia["→ PENDING_APPROVAL<br/>instantiate approval steps + validity window (B)"]:::back
|
||||||
|
ac{"chain: LINE_STAFF → DIRECTOR → CEO (B)"}:::dec
|
||||||
|
acx(["rejectStep → REJECTED"]):::bad
|
||||||
|
g -->|"yes"| gexp
|
||||||
|
g -->|"no"| i
|
||||||
|
i -->|"request-changes"| ir
|
||||||
|
i -->|"reject"| irx
|
||||||
|
i -->|"accept"| ia --> ac
|
||||||
|
ac -->|"rejectStep"| acx
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph SIGN["3 · Contract doc & sign (DOMESTIC → no gate)"]
|
||||||
|
direction TB
|
||||||
|
s1["contract/generate → CONTRACT_READY (B)"]:::back
|
||||||
|
s2["customer sign → SIGNED_CUSTOMER (P)"]:::port
|
||||||
|
s3["staff counter-sign (DOMESTIC) → FULLY_EXECUTED<br/>enqueueScheduleProcessing (no op-request) (B)(sys)"]:::back
|
||||||
|
s1 --> s2 --> s3
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph OPPAY["4 · Batch pipeline & payment"]
|
||||||
|
direction TB
|
||||||
|
fe["FULLY_EXECUTED enters day batch pool (sys)"]:::sys
|
||||||
|
b1["batch engine offers wagons → SELECTED_FOR_BATCH<br/>invoice generated (sys)"]:::sys
|
||||||
|
p1["customer pays → gateway → PAID (P)"]:::port
|
||||||
|
pexp(["pay window lapses → reservation EXPIRED"]):::bad
|
||||||
|
fe --> b1 --> p1
|
||||||
|
p1 -.->|"unpaid"| pexp
|
||||||
|
end
|
||||||
|
|
||||||
|
phys(["Physical execution:<br/>§7 intercity → COMPLETED (done)"]):::good
|
||||||
|
|
||||||
|
pre --> DRAFT
|
||||||
|
out1 --> INTAKE
|
||||||
|
ac -->|"APPROVED"| SIGN
|
||||||
|
SIGN --> OPPAY
|
||||||
|
p1 --> phys
|
||||||
|
gexp -.->|"gov → PAID/Eligible"| phys
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Road-mode note:** a domestic booking billed by road (truck) instead of rail goes through
|
||||||
|
> `operation/review` → `ROAD_DISPATCH_PENDING` (the road branch shown in the master graph), not the rail
|
||||||
|
> batch pool above.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §2 — One-time booking · IMPORT / EXPORT (clearance gate)
|
||||||
|
|
||||||
|
Every IMPORT/EXPORT one-time booking traverses the clearance gate — **whether or not customs is enabled**
|
||||||
|
(the customs flag only selects a heavier vs lighter `clearance_*` document set; both go through
|
||||||
|
`AWAITING_DOCUMENTS`). Commercial spine as §1 (phases 1–3) **plus** the gate: counter-sign → `AWAITING_DOCUMENTS`
|
||||||
|
→ document review loop → `CLEARANCE_READY`, phased ET/DJ actions, then operation-request → payment.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
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
|
||||||
|
|
||||||
|
a0(["Booking APPROVED & signed by customer<br/>(see §1 phases 1–3)"]):::sys
|
||||||
|
|
||||||
|
subgraph CS["Counter-sign with customs"]
|
||||||
|
direction TB
|
||||||
|
cs1["staff counter-sign (IMPORT/EXPORT) → AWAITING_DOCUMENTS (B)"]:::back
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph DOCS["5 · Document clearance gate"]
|
||||||
|
direction TB
|
||||||
|
x1["customer clearance/documents<br/>→ DOCUMENTS_UNDER_REVIEW (P)"]:::port
|
||||||
|
x2{"GL clearance/review each doc (B)"}:::dec
|
||||||
|
x2q["doc Queried → customer re-uploads (B)"]:::back
|
||||||
|
x3["clearance/finalize (100% approved) → CLEARANCE_READY (B)"]:::back
|
||||||
|
x1 --> x2
|
||||||
|
x2 -->|"query"| x2q --> x1
|
||||||
|
x2 -->|"approve all"| x3
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph PHASED["6 · Phased ET / DJ clearance (as applicable)"]
|
||||||
|
direction TB
|
||||||
|
ph1["upload declaration (serial) (B)"]:::back
|
||||||
|
ph2["duty/tax advise → customer duty-slip (P)(B)"]:::back
|
||||||
|
ph3["transit permit (ET) (B)"]:::back
|
||||||
|
ph4["delivery order / release order (DJ) (B)"]:::back
|
||||||
|
ph5["T1 docs → T1 close (B)"]:::back
|
||||||
|
ph6["export release / finalize-pre-clearance (B)"]:::back
|
||||||
|
ph1 --> ph2 --> ph3 --> ph4 --> ph5 --> ph6
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph OPPAY2["7 · Operation request & payment"]
|
||||||
|
direction TB
|
||||||
|
o1["clearance/proceed → OPERATION_REQUEST_PENDING (P)"]:::port
|
||||||
|
o2{"operation/review (B)"}:::dec
|
||||||
|
o2c["OPERATION_CHANGES_REQUESTED (B)"]:::back
|
||||||
|
om{"mode?"}:::dec
|
||||||
|
ot["accept=train: invoice → FULLY_EXECUTED<br/>→ batch offer → SELECTED_FOR_BATCH (B)(sys)"]:::back
|
||||||
|
orr["accept=road: invoice → ROAD_DISPATCH_PENDING (B)"]:::back
|
||||||
|
p1["customer pays → PAID (sys)(P)"]:::sys
|
||||||
|
pexp(["pay window lapses → EXPIRED"]):::bad
|
||||||
|
o1 --> o2
|
||||||
|
o2 -->|"request-changes"| o2c --> o1
|
||||||
|
o2 -->|"accept"| om
|
||||||
|
om -->|"train"| ot --> p1
|
||||||
|
om -->|"road"| orr --> p1
|
||||||
|
p1 -.->|"unpaid"| pexp
|
||||||
|
end
|
||||||
|
|
||||||
|
cancel(["CANCELLED — staff-only, only from<br/>OPERATION_REQUEST_PENDING here (not from<br/>AWAITING_DOCUMENTS / DOCUMENTS_UNDER_REVIEW)"]):::bad
|
||||||
|
phys(["Physical execution:<br/>§5 export · §6 import → COMPLETED (done)"]):::good
|
||||||
|
|
||||||
|
a0 --> CS --> DOCS
|
||||||
|
x3 --> PHASED
|
||||||
|
ph6 --> OPPAY2
|
||||||
|
p1 --> phys
|
||||||
|
o1 -.->|"cancel"| cancel
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §3 — General contract · Path A (transport-only, self-clearance)
|
||||||
|
|
||||||
|
Framework agreement where the customer clears customs independently. After the contract is active and
|
||||||
|
operations verify self-clearance (`SELF_CLEARED`), the **customer books directly** under the contract; each
|
||||||
|
booking then runs the operation/payment/physical flow.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
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
|
||||||
|
|
||||||
|
pre(["Company ACTIVE"]):::sys
|
||||||
|
|
||||||
|
subgraph CTR["1 · Contract lifecycle"]
|
||||||
|
direction TB
|
||||||
|
c1["POST /contracts → DRAFT<br/>routes + cargo scope + unit rates (NO quantities) (P)"]:::port
|
||||||
|
c2["generate-price → submit → SUBMITTED<br/>freeze contract_rate_snapshots (P)"]:::port
|
||||||
|
c3{"staff/accept | request-changes | reject (B)"}:::dec
|
||||||
|
c3r["CHANGES_REQUESTED (B)"]:::back
|
||||||
|
c3x(["Contract REJECTED"]):::bad
|
||||||
|
c4["→ PENDING_APPROVAL (B)"]:::back
|
||||||
|
c4a{"approval chain (B)"}:::dec
|
||||||
|
c4x(["rejectStep → REJECTED"]):::bad
|
||||||
|
c5["generate-contract → CONTRACT_READY (B)"]:::back
|
||||||
|
c6["customer sign → SIGNED_CUSTOMER (P)"]:::port
|
||||||
|
c7["staff counter-sign (IMPORT/EXPORT self-clearance) →<br/>AWAITING_CLEARANCE_DOCUMENTS (B)"]:::back
|
||||||
|
c1 --> c2 --> c3
|
||||||
|
c3 -->|"request-changes"| c3r --> c2
|
||||||
|
c3 -->|"reject"| c3x
|
||||||
|
c3 -->|"accept"| c4 --> c4a
|
||||||
|
c4a -->|"reject"| c4x
|
||||||
|
c4a -->|"approve"| c5 --> c6 --> c7
|
||||||
|
c7 -.->|"lapse"| cexp(["EXPIRED"]):::bad
|
||||||
|
c7 -.->|"renew"| cren(["RENEWAL_DRAFT → new cycle"]):::bad
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph SELF["2 · Self-clearance verification"]
|
||||||
|
direction TB
|
||||||
|
o1["customer uploads self-clearance docs (P)"]:::port
|
||||||
|
o2{"ops-review each doc (B)"}:::dec
|
||||||
|
o2q["query → re-upload (B)"]:::back
|
||||||
|
o3["ops-finalize → clearanceStatus SELF_CLEARED (B)"]:::back
|
||||||
|
o1 --> o2
|
||||||
|
o2 -->|"query"| o2q --> o1
|
||||||
|
o2 -->|"approve"| o3
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph BK["3 · Book directly under contract"]
|
||||||
|
direction TB
|
||||||
|
b1["customer POST /contracts/:id/bookings (P)"]:::port
|
||||||
|
bv{"validate-shipment:<br/>window + capacity draw-down + pairing"}:::dec
|
||||||
|
bvx(["rejected: over capacity /<br/>20ft pairing hard-block"]):::bad
|
||||||
|
b2["Booking created under contract<br/>(bookings.contract_id) (sys)"]:::sys
|
||||||
|
b1 --> bv
|
||||||
|
bv -->|"fail"| bvx
|
||||||
|
bv -->|"ok"| b2
|
||||||
|
end
|
||||||
|
|
||||||
|
op(["Booking runs operation + payment<br/>(see §1 phase 4) then §5/§6/§7 → COMPLETED (done)"]):::good
|
||||||
|
|
||||||
|
pre --> CTR
|
||||||
|
c7 --> SELF
|
||||||
|
o3 --> BK
|
||||||
|
b2 --> op
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §4 — General contract · Path B (GENERAL + customs)
|
||||||
|
|
||||||
|
Framework agreement **with** customs. The customer cannot book directly — they submit a **BookingRequest**
|
||||||
|
(date + quantities only); GL Ethiopia accepts it and creates the booking, which then runs **per-booking
|
||||||
|
phased customs** on the `/contracts/bookings/:bookingId/*` surface.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
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
|
||||||
|
|
||||||
|
pre(["Company ACTIVE"]):::sys
|
||||||
|
|
||||||
|
subgraph CTR["1 · Contract lifecycle (customs)"]
|
||||||
|
direction TB
|
||||||
|
c1["POST /contracts → DRAFT → submit → approve → sign (P)(B)"]:::port
|
||||||
|
c7["staff counter-sign (GENERAL + customs) → CONTRACT_ACTIVE<br/>(contract clearance cycle SKIPPED — runs per-booking) (B)"]:::back
|
||||||
|
c1 --> c7
|
||||||
|
c1 -.->|"reject / lapse"| cx(["REJECTED / EXPIRED"]):::bad
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph REQ["2 · Booking request → GL creates booking"]
|
||||||
|
direction TB
|
||||||
|
r1["customer POST /contracts/:id/booking-requests<br/>(date + quantities, no per-unit data) (P)"]:::port
|
||||||
|
r2{"GL booking-request queue (B)"}:::dec
|
||||||
|
r2x(["reject / customer cancel →<br/>REJECTED / CANCELLED"]):::bad
|
||||||
|
r3["GL accept → GL creates booking under contract<br/>(ct:create_booking) (B)"]:::back
|
||||||
|
r1 --> r2
|
||||||
|
r2 -->|"reject/cancel"| r2x
|
||||||
|
r2 -->|"accept"| r3
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph GLC["3 · Per-booking GL clearance & milestones"]
|
||||||
|
direction TB
|
||||||
|
g1["station-assign (route + bind staff) (B)"]:::back
|
||||||
|
g2["declaration → duty advise (GREEN/YELLOW/RED risk) (B)"]:::back
|
||||||
|
g3["customer duty-slip → transit / delivery / release order (P)(B)"]:::back
|
||||||
|
g4["T1 docs → T1 close (B)"]:::back
|
||||||
|
g5["final-invoice → customer slip → confirm paid (P)(B)"]:::back
|
||||||
|
g6["second-duty (post-arrival import) → slip (P)(B)"]:::back
|
||||||
|
gi["incident reports (photos) as needed (B)"]:::back
|
||||||
|
g1 --> g2 --> g3 --> g4 --> g5 --> g6
|
||||||
|
g4 -.-> gi
|
||||||
|
end
|
||||||
|
|
||||||
|
op(["Booking runs operation + payment (see §1 phase 4)<br/>then §5/§6 physical → COMPLETED (done)"]):::good
|
||||||
|
|
||||||
|
pre --> CTR
|
||||||
|
c7 --> REQ
|
||||||
|
r3 --> GLC
|
||||||
|
g6 --> op
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §5 — EXPORT operations (physical execution)
|
||||||
|
|
||||||
|
Given a PAID, scheduled **export** booking: optional first-mile road leg → origin warehouse inbound → train
|
||||||
|
build & dispatch → corridor transit → Djibouti port unload → interchange handover. Cargo leaves the country
|
||||||
|
at the port; the booking's terminal here is **dispatched/handed-over at Djibouti**.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
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
|
||||||
|
|
||||||
|
pre(["Export booking PAID & Eligible<br/>(see §1/§2/§3/§4)"]):::sys
|
||||||
|
|
||||||
|
subgraph FM["1 · First-mile (if EXPORT + requested)"]
|
||||||
|
direction TB
|
||||||
|
fmq{"first-mile requested?"}:::dec
|
||||||
|
fm1["leg auto-created firstMile.acceptBooking<br/>READY_TO_TRANSIT (sys)"]:::sys
|
||||||
|
fm2["setVehicles → vehicle BUSY, SMS driver, fleet_events (B)"]:::back
|
||||||
|
fm3["IN_TRANSIT (needs vehicle) → RECEIVED_TO_PORT (B)"]:::back
|
||||||
|
fm4["first-mile invoice (FIRST_MILE fee); distances lock once invoiced (B)"]:::back
|
||||||
|
fmq -->|"yes"| fm1 --> fm2 --> fm3 --> fm4
|
||||||
|
fmq -->|"no"| fmskip[" "]:::sys
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph WH["2 · Origin warehouse inbound"]
|
||||||
|
direction TB
|
||||||
|
w1["receive / bulkReceive → RECEIVED<br/>capacity assert, GRN, notify owner SMS (B)"]:::back
|
||||||
|
w2{"inspection"}:::dec
|
||||||
|
w2f["FAILED / NEEDS_REVIEW → hold + re-inspect (B)"]:::back
|
||||||
|
w3["store (allocation rule → yard/zone) → STORED (B)"]:::back
|
||||||
|
w4["reserve (booking PAID) → RESERVED (B)"]:::back
|
||||||
|
w5["mark-ready-for-loading (inspection PASSED) → READY_FOR_LOADING (B)"]:::back
|
||||||
|
w6["load onto wagon → LOADED (+ warehouse_loadings) (B)"]:::back
|
||||||
|
w1 --> w2
|
||||||
|
w2 -->|"fail"| w2f --> w2
|
||||||
|
w2 -->|"PASSED"| w3 --> w4 --> w5 --> w6
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph SCHED["3 · Train build & schedule"]
|
||||||
|
direction TB
|
||||||
|
s1["schedule DRAFT (≥2 locos, derive EXPORT direction) (B)"]:::back
|
||||||
|
s2["assign-bookings + run-allocation (wagons) (B)"]:::back
|
||||||
|
s3["pin wagons → finalize → SCHEDULED (bookings Scheduled) (B)"]:::back
|
||||||
|
s3x["cancel schedule → bookings Eligible (B)"]:::back
|
||||||
|
s1 --> s2 --> s3
|
||||||
|
s3 -.->|"cancel"| s3x -.-> s1
|
||||||
|
s3 -.->|"gov preempt / maintenance"| sr["reschedule: retained/displaced/readmitted (B)"]:::back
|
||||||
|
sr -.-> s2
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph RUN["4 · Dispatch → Djibouti"]
|
||||||
|
direction TB
|
||||||
|
r1["dispatch → DISPATCHED<br/>train_number, locos ASSIGNED, window CLOSED, unpaid EXPIRED (B)"]:::back
|
||||||
|
r2["checkpoints (corridor) → train_checkpoint_events (B)"]:::back
|
||||||
|
rc["customer tracking page GET /tracking/:id (JWT) (P)<br/>NOTE: tracking_events has no writer — timeline empty"]:::port
|
||||||
|
r3["arrive → ARRIVED (bookings IN_TRANSIT, wagons/locos freed) (B)"]:::back
|
||||||
|
ru["export/auto-unload-at-djibouti →<br/>UNLOADED_AT_DJIBOUTI_PORT (B)"]:::back
|
||||||
|
ri["interchange document generate-from-schedule → GENERATED (B)"]:::back
|
||||||
|
ria{"port acknowledges?"}:::dec
|
||||||
|
r1 --> r2 --> r3 --> ru --> ri --> ria
|
||||||
|
r2 -.-> rc
|
||||||
|
end
|
||||||
|
|
||||||
|
done(["Export dispatched & handed over at Djibouti (done)"]):::good
|
||||||
|
disp(["interchange DISPUTED → remarks / re-issue"]):::bad
|
||||||
|
|
||||||
|
pre --> FM
|
||||||
|
fm4 --> WH
|
||||||
|
fmskip --> WH
|
||||||
|
w6 --> SCHED
|
||||||
|
s3 --> RUN
|
||||||
|
ria -->|"acknowledge"| done
|
||||||
|
ria -->|"dispute"| disp
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §6 — IMPORT operations (physical execution)
|
||||||
|
|
||||||
|
Given a PAID, scheduled **import** booking arriving by train from Djibouti: destination warehouse unload →
|
||||||
|
inspection → import customs finalization → optional last-mile → fee gate-clearance → release → delivery →
|
||||||
|
**COMPLETED**.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
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
|
||||||
|
|
||||||
|
pre(["Import booking PAID & scheduled<br/>(see §1/§2/§4)"]):::sys
|
||||||
|
|
||||||
|
subgraph RAIL["1 · Import train arrival"]
|
||||||
|
direction TB
|
||||||
|
t1["import train dispatched from Djibouti → DISPATCHED (B)"]:::back
|
||||||
|
t2["checkpoints → train_checkpoint_events (B)"]:::back
|
||||||
|
tc["customer tracking page GET /tracking/:id (JWT) (P)<br/>NOTE: tracking_events has no writer — timeline empty"]:::port
|
||||||
|
t3["arrive → ARRIVED (bookings IN_TRANSIT) → warehouse arrival automation (B)"]:::back
|
||||||
|
t1 --> t2 --> t3
|
||||||
|
t2 -.-> tc
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph WH["2 · Destination warehouse"]
|
||||||
|
direction TB
|
||||||
|
w1["import/auto-unload-arrived-bookings (assign WH/yard/zone) → UNLOADED (B)"]:::back
|
||||||
|
w2{"inspection PASSED?"}:::dec
|
||||||
|
w2f["FAILED → hold / re-inspect / djibouti-incident report (B)"]:::back
|
||||||
|
w3["→ READY_FOR_PICKUP (IMPORT) (B)"]:::back
|
||||||
|
w1 --> w2
|
||||||
|
w2 -->|"no"| w2f --> w2
|
||||||
|
w2 -->|"yes"| w3
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph CUST["3 · Import customs finalization (timestamp-driven)"]
|
||||||
|
direction TB
|
||||||
|
u1["upload docs (IM4/IM5/T1_CLOSURE/TRANSIT_PERMIT/…) (B)"]:::back
|
||||||
|
u2["record declaration serial (B)"]:::back
|
||||||
|
u3["notify duties/taxes (B)"]:::back
|
||||||
|
u4["mark duties paid (needs CUSTOMER_PAYMENT_SLIP) (B)"]:::back
|
||||||
|
u5["assign risk (GREEN/YELLOW/BLUE/RED) (B)"]:::back
|
||||||
|
u6{"release gates satisfied?<br/>T1 + release permit + declaration + risk + paid"}:::dec
|
||||||
|
u6x["blocked — missing gate → resolve (B)"]:::back
|
||||||
|
u7["release-permitted → completedAt (B)"]:::back
|
||||||
|
u1 --> u2 --> u3 --> u4 --> u5 --> u6
|
||||||
|
u6 -->|"no"| u6x --> u6
|
||||||
|
u6 -->|"yes"| u7
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph LM["4 · Last-mile (if requested)"]
|
||||||
|
direction TB
|
||||||
|
lq{"last-mile requested?"}:::dec
|
||||||
|
l1["leg auto-created lastMile.acceptBooking<br/>READY_TO_TRANSIT (sys)"]:::sys
|
||||||
|
l2["setVehicles → IN_TRANSIT → DELIVERED (free vehicles) (B)"]:::back
|
||||||
|
l3["last-mile invoice (LAST_MILE fee) (B)"]:::back
|
||||||
|
lq -->|"yes"| l1 --> l2 --> l3
|
||||||
|
lq -->|"no"| lskip[" "]:::sys
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph DEL["5 · Release & delivery"]
|
||||||
|
direction TB
|
||||||
|
d0{"warehouse/storage fees fully PAID?"}:::dec
|
||||||
|
d0x["gate-clearance BLOCKED (findBlockingInvoice) (B)"]:::back
|
||||||
|
d0p["customer pays storage/demurrage online (P)"]:::port
|
||||||
|
d1["release order (DO) + gate-clearance → deliver (B)"]:::back
|
||||||
|
d2["customer approve-delivery (saved signature) → POD (P)"]:::port
|
||||||
|
d3["inventory DELIVERED, POD to cargo, container freed (sys)"]:::sys
|
||||||
|
d0 -->|"no"| d0x --> d0p --> d0
|
||||||
|
d0 -->|"yes"| d1 --> d2 --> d3
|
||||||
|
end
|
||||||
|
|
||||||
|
ecr(["empty-container-return chain (post-import):<br/>RETURNED → … → HANDOVER_ISSUED → COMPLETED"]):::sys
|
||||||
|
done(["Booking COMPLETED (done) (operations/complete)"]):::good
|
||||||
|
|
||||||
|
pre --> RAIL
|
||||||
|
t3 --> WH
|
||||||
|
w3 --> CUST
|
||||||
|
u7 --> LM
|
||||||
|
l3 --> DEL
|
||||||
|
lskip --> DEL
|
||||||
|
d3 --> done
|
||||||
|
done -.-> ecr
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## §7 — INTERCITY / DOMESTIC operations (no cross-border customs)
|
||||||
|
|
||||||
|
Rail movement **between Ethiopian yards** (e.g. inland dry ports). No import/export customs, no Djibouti
|
||||||
|
port unload or interchange handover. Optional road mile legs if the service includes door delivery.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
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
|
||||||
|
|
||||||
|
pre(["Domestic booking PAID & Eligible<br/>(see §1 — customs OFF)"]):::sys
|
||||||
|
|
||||||
|
subgraph FMD["1 · Optional origin pickup (road)"]
|
||||||
|
direction TB
|
||||||
|
fq{"door pickup / first-mile requested?"}:::dec
|
||||||
|
f1["first-mile leg → vehicle assign → RECEIVED_TO_PORT (B)"]:::back
|
||||||
|
fq -->|"yes"| f1 --> fnext[" "]:::sys
|
||||||
|
fq -->|"no (drop at origin yard)"| fnext
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph WHO["2 · Origin warehouse"]
|
||||||
|
direction TB
|
||||||
|
w1["receive → RECEIVED (B)"]:::back
|
||||||
|
w2{"inspection PASSED?"}:::dec
|
||||||
|
w2f["hold + re-inspect (B)"]:::back
|
||||||
|
w3["store → STORED → reserve (PAID) → RESERVED (B)"]:::back
|
||||||
|
w4["ready-for-loading → LOADED onto wagon (B)"]:::back
|
||||||
|
w1 --> w2
|
||||||
|
w2 -->|"no"| w2f --> w2
|
||||||
|
w2 -->|"yes"| w3 --> w4
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph RUN["3 · Train (ET yard → ET yard)"]
|
||||||
|
direction TB
|
||||||
|
s1["schedule DRAFT (direction DOMESTIC) → assign → finalize → SCHEDULED (B)"]:::back
|
||||||
|
r1["dispatch → DISPATCHED (unpaid EXPIRED) (B)"]:::back
|
||||||
|
r2["checkpoints → train_checkpoint_events; customer tracking (JWT) (P)(B)"]:::back
|
||||||
|
r3["arrive → ARRIVED (bookings IN_TRANSIT) (B)"]:::back
|
||||||
|
s1 --> r1 --> r2 --> r3
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph WHD["4 · Destination warehouse & delivery"]
|
||||||
|
direction TB
|
||||||
|
d1["auto-unload arrived → UNLOADED / RECEIVED (B)"]:::back
|
||||||
|
di{"inspection PASSED?"}:::dec
|
||||||
|
dif["hold + re-inspect (B)"]:::back
|
||||||
|
d2["READY_FOR_PICKUP (B)"]:::back
|
||||||
|
fee{"storage fees paid?"}:::dec
|
||||||
|
feex["gate-clearance blocked → customer pays (P)"]:::port
|
||||||
|
d3["release order → deliver (B)"]:::back
|
||||||
|
lq{"door delivery / last-mile?"}:::dec
|
||||||
|
l1["last-mile leg → DELIVERED (B)"]:::back
|
||||||
|
d4["customer approve-delivery → POD → inventory DELIVERED (P)(sys)"]:::sys
|
||||||
|
d1 --> di
|
||||||
|
di -->|"no"| dif --> di
|
||||||
|
di -->|"yes"| d2 --> fee
|
||||||
|
fee -->|"no"| feex --> fee
|
||||||
|
fee -->|"yes"| d3 --> lq
|
||||||
|
lq -->|"yes"| l1 --> d4
|
||||||
|
lq -->|"no (pickup at yard)"| d4
|
||||||
|
end
|
||||||
|
|
||||||
|
done(["Booking COMPLETED (done)"]):::good
|
||||||
|
|
||||||
|
pre --> FMD
|
||||||
|
fnext --> WHO
|
||||||
|
w4 --> RUN
|
||||||
|
r3 --> WHD
|
||||||
|
d4 --> done
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Cross-reference
|
||||||
|
|
||||||
|
| Variant | Distinctive gate(s) | Terminal ends unique to it |
|
||||||
|
| --------------------------- | --------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- |
|
||||||
|
| §1 One-time · DOMESTIC | counter-sign → `FULLY_EXECUTED` (skips clearance **and** op-request; enters batch directly) | HARD BLOCK, price/intake/approval REJECTED, EXPIRED |
|
||||||
|
| §2 One-time · IMPORT/EXPORT | `AWAITING_DOCUMENTS` → review loop → phased ET/DJ → op-request (gate applies with **or without** customs) | + doc-query loop, CANCELLED (only from OPERATION_REQUEST_PENDING) |
|
||||||
|
| §3 Contract · Path A | `SELF_CLEARED` via ops-review; customer books direct | contract REJECTED/EXPIRED/RENEWAL, capacity/pairing block |
|
||||||
|
| §4 Contract · Path B | BookingRequest → GL creates booking; per-booking milestones | BookingRequest REJECTED/CANCELLED |
|
||||||
|
| §5 Export ops | first-mile → Djibouti unload → interchange handover | dispatched@Djibouti (success), interchange DISPUTED |
|
||||||
|
| §6 Import ops | import customs finalization gates → last-mile → gate-clearance | COMPLETED (+ empty-container-return chain) |
|
||||||
|
| §7 Intercity ops | ET→ET rail, no cross-border customs, optional mile legs | COMPLETED |
|
||||||
|
|
||||||
|
Full endpoint tables & per-domain state machines: [`FREIGHT_SYSTEM_FLOW.md`](./FREIGHT_SYSTEM_FLOW.md).
|
||||||
|
Single all-in-one branching graph: [`FREIGHT_MASTER_FLOW.md`](./FREIGHT_MASTER_FLOW.md).
|
||||||
260
apps/edr-freight-api/docs/FREIGHT_MASTER_FLOW.md
Normal file
260
apps/edr-freight-api/docs/FREIGHT_MASTER_FLOW.md
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
# 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<br/>GET /auth/check-availability @Public<br/>POST /otp/send + /otp/verify (P)"]:::port
|
||||||
|
S1 --> S2{"Identity proofing<br/>(VeriFayda)?"}:::dec
|
||||||
|
S2 -->|"Yes"| S3["POST /fayda/verification/start →<br/>/callback → /complete<br/>upsert iam.users (verified_by=fayda) (P)"]:::port
|
||||||
|
S2 -->|"No"| S4
|
||||||
|
S3 --> S4["POST /companies/onboarding/start<br/>draft company (placeholder TIN, PENDING) (P)"]:::port
|
||||||
|
S4 --> S4b["Wizard: PATCH /profile, /onboarding-step,<br/>upload license + docs<br/>GET /onboarding/requirements (P)"]:::port
|
||||||
|
S4b --> S5["POST /companies/onboarding/complete<br/>re-validate → company+profiles = PENDING (P)"]:::port
|
||||||
|
S5 --> S6{"Backoffice reviews profile<br/>PATCH /company-profiles/:id/status (B)"}:::dec
|
||||||
|
S6 -->|"Reject / suspend"| S6x(["SUSPENDED / BLACKLISTED<br/>cannot transact"]):::bad
|
||||||
|
S6 -->|"Approve"| S7["Mint reference (EX-#####),<br/>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<br/>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<br/>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<br/>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<br/>instantiate approval steps (B)"]:::back
|
||||||
|
C4 --> C4a{"Approval chain<br/>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<br/>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<br/>under the contract?"}:::dec
|
||||||
|
CPATH -->|"Path A: transport-only"| CPA["Ops review self-clearance<br/>ops-review → ops-finalize → SELF_CLEARED (B)<br/>then customer books direct<br/>POST /contracts/:id/bookings (P)"]:::port
|
||||||
|
CPATH -->|"Path B: GENERAL + customs"| CPB["Customer submits BookingRequest<br/>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<br/>(window + capacity draw-down check) (sys)"]:::sys
|
||||||
|
B0u --> BFLOW
|
||||||
|
|
||||||
|
%% ---- One-time booking ----
|
||||||
|
B0["Create BOOKING DRAFT<br/>POST /bookings (reference, containers,<br/>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<br/>waits for partner shipment<br/>(shares a wagon) → rejoins"]):::sys
|
||||||
|
BCONS -.-> BFLOW
|
||||||
|
|
||||||
|
%% ================= PHASE 3: PRICING & SUBMIT =================
|
||||||
|
BFLOW["Configure shipment<br/>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<br/>rule-engine: LIVE rates + surcharges<br/>HAZARDOUS / REEFER / OVERWEIGHT /<br/>SHIPPING_LINE / CONSOLIDATION (P)"]:::port
|
||||||
|
B1 --> B1w{"weight-limit-rules check"}:::dec
|
||||||
|
B1w -->|"VGM > maxCapacity"| B1x(["HARD BLOCK (400)<br/>cannot submit"]):::bad
|
||||||
|
B1w -->|"over maxVgm, within cap"| B1warn["warning + OVERWEIGHT surcharge"]:::sys
|
||||||
|
B1w -->|"ok"| B2
|
||||||
|
B1warn --> B2
|
||||||
|
B2["POST /bookings/:id/submit → SUBMITTED<br/>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 →<br/>PAID + schedulingStatus Eligible (B)"]:::back
|
||||||
|
GOV -->|"No (commercial)"| BI{"Staff intake<br/>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<br/>instantiate approval steps<br/>(set validity window) (B)"]:::back
|
||||||
|
BA --> BAc{"Approval chain<br/>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<br/>POST /bookings/:id/contract/sign (P)"]:::port
|
||||||
|
BC2 --> BC3{"Staff counter-sign:<br/>trade direction?"}:::dec
|
||||||
|
BC3 -->|"IMPORT / EXPORT<br/>(clearance gate, even if customs off)"| CL1
|
||||||
|
BC3 -->|"DOMESTIC"| FEXD["counter-sign → FULLY_EXECUTED<br/>enqueue batch (skips clearance + op-request) (sys)(B)"]:::back
|
||||||
|
FEXD --> FEB
|
||||||
|
|
||||||
|
%% ================= PHASE 6: CUSTOMS CLEARANCE =================
|
||||||
|
CL1["AWAITING_DOCUMENTS → customer uploads<br/>POST /bookings/:id/clearance/documents<br/>→ DOCUMENTS_UNDER_REVIEW (P)"]:::port
|
||||||
|
CL1 --> CL2{"GL reviews each doc<br/>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):<br/>declaration → duty advise → duty slip →<br/>transit permit → delivery/release order →<br/>T1 docs/close → export release (sys)(B)"]:::back
|
||||||
|
CLph --> OP1
|
||||||
|
|
||||||
|
%% ================= PHASE 7: OPERATION REQUEST =================
|
||||||
|
OP1["clearance/proceed: pick binding schedule day<br/>→ OPERATION_REQUEST_PENDING (P)"]:::port
|
||||||
|
OP1 --> OP2{"Operations review<br/>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<br/>(day batch pool) (B)"]:::back
|
||||||
|
OPM -->|"ROAD (truck)"| OP3r["invoice generated →<br/>ROAD_DISPATCH_PENDING (billed by KM) (B)"]:::back
|
||||||
|
OP3t --> FEB["batch engine offers wagons →<br/>SELECTED_FOR_BATCH (sys)"]:::sys
|
||||||
|
|
||||||
|
%% ================= PHASE 8: INVOICE & PAYMENT =================
|
||||||
|
GEXP --> SCH
|
||||||
|
FEB --> PAY1
|
||||||
|
OP3r --> PAY1
|
||||||
|
PAY1["Invoice (source=booking, INV-YYYYMMDD-#####, due +14d)<br/>booking invoice starts DRAFT → ISSUED at operation-accept (sys)"]:::sys
|
||||||
|
PAY1 --> PAY2["Customer pays<br/>POST /billing/my-invoices/:id/pay →<br/>billing.payInvoice → payment-api initiate (P)"]:::port
|
||||||
|
PAY2 --> PAYp{"Provider result<br/>(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 →<br/>POST /internal/payments/mark-paid →<br/>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<br/>POST /train-scheduling/{container|bulk}/schedules<br/>≥2 locomotives, derive direction (B)"]:::back
|
||||||
|
SC2 --> SC3["assign-bookings + run-allocation<br/>(wagon_booking_allocations) (B)"]:::back
|
||||||
|
SC3 --> SC4["pin physical wagons → finalize → SCHEDULED<br/>bookings → Scheduled (B)"]:::back
|
||||||
|
SC4 -.->|"cancel schedule"| SC4x["bookings back to Eligible (B)"]:::back
|
||||||
|
SC4x -.-> SC2
|
||||||
|
SC4 -.->|"gov preempt / maintenance"| RESCH["reschedule: retained / displaced /<br/>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<br/>firstMile.acceptBooking (READY_TO_TRANSIT) (sys)"]:::sys
|
||||||
|
FMQ0 -->|"No"| WO1
|
||||||
|
FM1 --> FM2["setVehicles → vehicle BUSY, SMS driver,<br/>fleet_events (B)"]:::back
|
||||||
|
FM2 --> FM3["IN_TRANSIT (needs vehicle) →<br/>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<br/>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<br/>(+ warehouse_loadings) (B)"]:::back
|
||||||
|
WO6 --> TR1
|
||||||
|
|
||||||
|
%% ================= PHASE 12: DISPATCH & TRANSIT =================
|
||||||
|
TR1["dispatch → DISPATCHED<br/>assign train_number, locos ASSIGNED,<br/>window CLOSED, unpaid reservations EXPIRED (B)"]:::back
|
||||||
|
TR1 --> TR2["record checkpoints (corridor stations) →<br/>train_checkpoint_events (B)"]:::back
|
||||||
|
TR2 --> TRC["Customer tracking page<br/>GET /tracking/:consignmentId (JWT) (P)"]:::port
|
||||||
|
TR2 --> TR3["arrive (final checkpoint) → ARRIVED<br/>bookings IN_TRANSIT, locos+wagons freed,<br/>warehouse arrival automation (B)"]:::back
|
||||||
|
TR3 --> WD1
|
||||||
|
|
||||||
|
%% ================= PHASE 13: WAREHOUSE DEST + IMPORT CUSTOMS =================
|
||||||
|
WD1["destination warehouse: auto-unload arrived<br/>→ 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 /<br/>UNLOADED_AT_DJIBOUTI_PORT (B)"]:::back
|
||||||
|
WD3 --> IMP1["Import customs finalization:<br/>upload docs → declaration → notify duties →<br/>duties paid (needs slip) → assign risk →<br/>release-permitted (all gates) (sys)(B)"]:::back
|
||||||
|
IMP1 --> LMQ
|
||||||
|
WDX --> ICD["interchange document (handover manifest)<br/>generate-from-schedule → GENERATED →<br/>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<br/>(IMPORT inspection PASSED only) (sys)"]:::sys
|
||||||
|
LMQ -->|"No"| DE1
|
||||||
|
LM1 --> LM2["setVehicles → IN_TRANSIT → DELIVERED<br/>(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<br/>findBlockingInvoice / assertClearanceAllowed (B)"]:::back
|
||||||
|
DE1x --> DE1p["Customer pays storage/demurrage<br/>warehouse-fee-invoices/:id/pay-online (P)"]:::port
|
||||||
|
DE1p --> DE1
|
||||||
|
DE1 -->|"Yes"| DE2["release order (DO) + gate-clearance →<br/>deliver (B)"]:::back
|
||||||
|
DE2 --> DE3["Customer approves delivery (saved signature)<br/>POST /warehouse-inventory/bookings/:id/approve-delivery (P)"]:::port
|
||||||
|
DE3 --> DE4["inventory DELIVERED, POD to cargo,<br/>container freed, capacity released (sys)"]:::sys
|
||||||
|
DE4 --> DONE(["Booking COMPLETED (done) <br/>operations/complete"]):::good
|
||||||
|
|
||||||
|
%% ================= GLOBAL EXITS =================
|
||||||
|
GEXIT(["CANCELLED — POST /bookings/:id/cancel (staff-only)<br/>ONLY from DRAFT, SUBMITTED, PRICE_CHANGED_PENDING_CONFIRM,<br/>CHANGES_REQUESTED, PENDING_APPROVAL, CONTRACT_READY,<br/>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.
|
||||||
872
apps/edr-freight-api/docs/FREIGHT_SYSTEM_FLOW.md
Normal file
872
apps/edr-freight-api/docs/FREIGHT_SYSTEM_FLOW.md
Normal file
@@ -0,0 +1,872 @@
|
|||||||
|
# EDR Freight System — End-to-End Flow Map (FE → Backend)
|
||||||
|
|
||||||
|
> Comprehensive flow documentation for the **Freight Management** slice of the EDR Platform:
|
||||||
|
> the two frontends (**Customer Portal** + **Backoffice**) and how every action reaches the
|
||||||
|
> **freight API**, the **payment microservice**, Postgres, MinIO and RabbitMQ.
|
||||||
|
>
|
||||||
|
> Generated by mapping all ~45 API modules (~300 routes / 60 controllers) against both FE apps.
|
||||||
|
> Layered on purpose: a **master business-flow** view first, then **per-domain endpoint-level** charts.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. How to read this document
|
||||||
|
|
||||||
|
- **Master view** (§3) is the freight journey at business altitude — state transitions across domains.
|
||||||
|
- **Per-domain views** (§5–§13) drop to endpoint altitude — every `Method /path`, its guard, and the FE caller.
|
||||||
|
- Diagrams are [Mermaid](https://mermaid.js.org). GitHub / VS Code (Markdown Preview Mermaid) render them inline.
|
||||||
|
- **Legend** used throughout:
|
||||||
|
- (P) **Portal** = `@edr/freight-portal` (customer users, port `5173`)
|
||||||
|
- (B) **Backoffice** = `@edr/freight-backoffice` (EDR employees, port `5183`)
|
||||||
|
- (green) **API** = `@edr/freight-api` (NestJS, port `3001`)
|
||||||
|
- (red) **Payment** = `@edr/payment-api` (NestJS microservice, port `3003`)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. System architecture & apps
|
||||||
|
|
||||||
|
| Layer | Package | Port | Base URL / notes |
|
||||||
|
| ----- | ------- | ---- | ---------------- |
|
||||||
|
| Customer Portal | `@edr/freight-portal` | 5173 | axios `utils/api.ts`, baseURL `VITE_BASE_API_URL`, React Query |
|
||||||
|
| Backoffice | `@edr/freight-backoffice` | 5183 | axios `auth/http.ts`, baseURL `${VITE_BASE_API_URL}/api`, React Query |
|
||||||
|
| Freight API | `@edr/freight-api` | 3001 | NestJS, global prefix `/api`, Postgres schema `freight` |
|
||||||
|
| Payment API | `@edr/payment-api` | 3003 | NestJS, separate schema `edr_payment`, providers in `@edr/payment-providers` |
|
||||||
|
| Datastores | — | 5433 | Postgres `edr_freight`; MinIO (files); RabbitMQ (SMS/email/payment events) |
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
subgraph FE["Frontends (React + Vite + React Query)"]
|
||||||
|
P["(P) Customer Portal :5173"]
|
||||||
|
B["(B) Backoffice :5183"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph EDGE["Freight API edge (NestJS :3001)"]
|
||||||
|
direction TB
|
||||||
|
CORS["CORS (reflect origin,<br/>credentials, IAM headers)"]
|
||||||
|
JWT["JwtGuard (global APP_GUARD)<br/>+ HasActiveDelegationGuard"]
|
||||||
|
PERM["FreightPermissionGuard<br/>(per-route perms)"]
|
||||||
|
VP["ValidationPipe<br/>(implicitConversion OFF)"]
|
||||||
|
RTI["ResponseTransformInterceptor<br/>→ { success, data }"]
|
||||||
|
HEF["HttpExceptionFilter"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph DOM["Domain modules (~45)"]
|
||||||
|
direction TB
|
||||||
|
D1["Identity / Companies / Auth"]
|
||||||
|
D2["Bookings + Contracts"]
|
||||||
|
D3["Rule Engine"]
|
||||||
|
D4["Train Scheduling"]
|
||||||
|
D5["Warehouse"]
|
||||||
|
D6["Field Ops (mile/import)"]
|
||||||
|
D7["Billing"]
|
||||||
|
D8["Notifications / Inbox"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph INFRA["Backing services"]
|
||||||
|
PG[("Postgres<br/>schema: freight")]
|
||||||
|
MINIO[("MinIO<br/>object store")]
|
||||||
|
MQ{{"RabbitMQ"}}
|
||||||
|
PAY["(red) Payment API :3003<br/>schema: edr_payment"]
|
||||||
|
end
|
||||||
|
|
||||||
|
P -->|"Bearer token (cookie)<br/>axios interceptor"| CORS
|
||||||
|
B -->|"Bearer token (cookie)<br/>axios interceptor"| CORS
|
||||||
|
CORS --> JWT --> PERM --> VP --> DOM
|
||||||
|
DOM --> RTI
|
||||||
|
DOM --> PG
|
||||||
|
DOM --> MINIO
|
||||||
|
DOM -->|"send-sms / send-email"| MQ
|
||||||
|
DOM -->|"POST /payments/initiate<br/>x-service-token"| PAY
|
||||||
|
PAY -->|"payment.succeeded webhook<br/>→ /api/internal/payments/mark-paid"| DOM
|
||||||
|
PAY -.->|"or RabbitMQ payment events"| MQ --> DOM
|
||||||
|
D8 -->|"socket.io push"| FE
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. The request pipeline (every FE call)
|
||||||
|
|
||||||
|
Both frontends wrap each API method in an `endpoint(service, action, fn)` helper feeding React Query.
|
||||||
|
The axios client attaches the JWT and transparently refreshes on `401`.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
autonumber
|
||||||
|
participant C as React component / hook
|
||||||
|
participant Q as React Query
|
||||||
|
participant AX as axios client (interceptors)
|
||||||
|
participant API as Freight API (:3001)
|
||||||
|
participant DB as Postgres
|
||||||
|
|
||||||
|
C->>Q: useQuery / useMutation(endpoint)
|
||||||
|
Q->>AX: call(input)
|
||||||
|
AX->>AX: request interceptor →<br/>Authorization: Bearer [auth-token cookie]
|
||||||
|
AX->>API: HTTP /api/<controller>/<path>
|
||||||
|
API->>API: CORS → JwtGuard → PermissionGuard → ValidationPipe
|
||||||
|
alt token valid & permitted
|
||||||
|
API->>DB: repository query (schema freight)
|
||||||
|
DB-->>API: rows
|
||||||
|
API->>API: ResponseTransformInterceptor → { success:true, data }
|
||||||
|
API-->>AX: 200 { success, data }
|
||||||
|
AX->>AX: backoffice interceptor unwraps .data<br/>(portal returns raw envelope; callers read .data)
|
||||||
|
AX-->>Q: payload
|
||||||
|
Q-->>C: data (+ cache by queryKey)
|
||||||
|
else 401 Unauthorized
|
||||||
|
API-->>AX: 401
|
||||||
|
AX->>API: POST /api/auth/refresh-token { refreshToken cookie }
|
||||||
|
alt refresh ok
|
||||||
|
API-->>AX: { token, refreshToken }
|
||||||
|
AX->>AX: set cookies, retry original request (_retry)
|
||||||
|
AX-->>Q: payload
|
||||||
|
else refresh fails
|
||||||
|
AX->>AX: clear cookies →<br/>portal: reject · backoffice: redirect /auth
|
||||||
|
end
|
||||||
|
else 4xx/5xx
|
||||||
|
API->>API: HttpExceptionFilter → { success:false, message }
|
||||||
|
API-->>AX: error
|
||||||
|
AX-->>Q: throw → onError toast
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
**Auth model (important):** `SharedAuthModule` (`@tria-plc/api-common`) registers `JwtGuard` +
|
||||||
|
`HasActiveDelegationGuard` as **global `APP_GUARD`s** — *every* route is JWT-protected unless it
|
||||||
|
carries `@Public()`. Fine-grained `FreightPermissionGuard([perm])` decorators add permission checks
|
||||||
|
on staff routes. Explicitly **public** endpoints: `GET /api/files/:fileId`, `POST /api/otp/{send,verify}`,
|
||||||
|
`GET /api/auth/check-availability`, the `fayda/verification/*` + `/callback` endpoints,
|
||||||
|
`GET /api/payments/{checkout,receipt/:orderId}`, and the service-to-service `POST /api/internal/payments/mark-paid`.
|
||||||
|
Real login / JWT issuance lives in the **external IAM package**, not this repo. (Note: `@edr/api-common`'s
|
||||||
|
`@Public` and `@tria-plc/api-common`'s `@IsPublic` both set the same `"isPublic"` metadata key the guard reads.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. MASTER FLOW — the freight journey (business altitude)
|
||||||
|
|
||||||
|
This is the spine. A shipment travels **customer intake → pricing → approval → contract → customs
|
||||||
|
clearance → operation request → payment → scheduling/allocation → first-mile → warehouse → train →
|
||||||
|
arrival → warehouse → last-mile → delivery → tracking**, branching on _container vs bulk_,
|
||||||
|
_import vs export_, _commercial vs government_, and _one-time vs contract_.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
start(["Customer signs up<br/>(IAM + Fayda + company onboarding)"]) --> mode{"Booking origin?"}
|
||||||
|
|
||||||
|
mode -->|"One-time shipment"| draft["Create BOOKING (DRAFT) (P)"]
|
||||||
|
mode -->|"Framework agreement"| cdraft["Create CONTRACT (DRAFT) (P)"]
|
||||||
|
|
||||||
|
%% Contract branch
|
||||||
|
cdraft --> cprice["Generate price → Submit → Approvals → Sign"]
|
||||||
|
cprice --> cactive{"Contract type / customs?"}
|
||||||
|
cactive -->|"Path A: transport-only,<br/>self-clearance"| bookA["Customer books directly<br/>POST /contracts/:id/bookings (P)"]
|
||||||
|
cactive -->|"Path B: GENERAL + customs"| breq["Customer submits BookingRequest (P)<br/>→ GL accepts → GL creates booking (B)"]
|
||||||
|
bookA --> draft2["Booking created under contract"]
|
||||||
|
breq --> draft2
|
||||||
|
|
||||||
|
%% Booking spine
|
||||||
|
draft --> price["Generate price (P)<br/>(rule-engine: rates + surcharges)"]
|
||||||
|
draft2 --> price
|
||||||
|
price --> submit["Submit → SUBMITTED (P)"]
|
||||||
|
submit --> intake["Staff accept → PENDING_APPROVAL (B)<br/>(instantiate approval steps)"]
|
||||||
|
intake --> appr["Approval chain:<br/>LINE_STAFF → DIRECTOR → CEO (B)"]
|
||||||
|
appr --> gen["Generate contract → CONTRACT_READY (B)"]
|
||||||
|
gen --> sign["Customer signs (P) → Staff counter-signs (B)"]
|
||||||
|
sign --> customs{"customsClearingEnabled?"}
|
||||||
|
|
||||||
|
customs -->|"No"| ready["FULLY_EXECUTED"]
|
||||||
|
customs -->|"Yes"| clr["AWAITING_DOCUMENTS →<br/>customer uploads docs (P) →<br/>GL review/finalize (B) → CLEARANCE_READY"]
|
||||||
|
clr --> opreq["Customer requests operation (P)<br/>(binding schedule date)"]
|
||||||
|
ready --> opreq
|
||||||
|
opreq --> oprev{"Operations review (B)"}
|
||||||
|
oprev -->|"Request changes"| clr
|
||||||
|
oprev -->|"Accept: train"| inv["Invoice generated →<br/>enters day batch pool"]
|
||||||
|
oprev -->|"Accept: road/truck"| road["ROAD_DISPATCH_PENDING<br/>(billed by KM)"]
|
||||||
|
|
||||||
|
inv --> pay["Customer pays invoice (P)<br/>→ (red) gateway → PAID"]
|
||||||
|
road --> pay
|
||||||
|
pay --> gov{"Government?"}
|
||||||
|
gov -->|"Yes"| expedite["governmentExpedite → PAID/Eligible (B)"]
|
||||||
|
gov -->|"No"| eligible["schedulingStatus = Eligible"]
|
||||||
|
expedite --> sched
|
||||||
|
eligible --> sched
|
||||||
|
|
||||||
|
subgraph JOURNEY["Physical movement"]
|
||||||
|
direction TB
|
||||||
|
sched["Train schedule: DRAFT → assign bookings →<br/>allocate wagons → finalize → SCHEDULED (B)"]
|
||||||
|
fm{"EXPORT + first-mile?"}
|
||||||
|
fmleg["First-mile leg: truck pickup →<br/>RECEIVED_TO_PORT (B)"]
|
||||||
|
wh_in["Warehouse receive → inspect →<br/>STORED → READY_FOR_LOADING → LOADED (B)"]
|
||||||
|
disp["Dispatch train → DISPATCHED<br/>(locos ASSIGNED, unpaid EXPIRED) (B)"]
|
||||||
|
track["Checkpoints logged →<br/>tracking events (customer sees) (P)"]
|
||||||
|
arrive["Arrive → ARRIVED<br/>(bookings IN_TRANSIT, wagons freed) (B)"]
|
||||||
|
wh_out["Destination warehouse:<br/>unload → inspect → READY_FOR_PICKUP (B)"]
|
||||||
|
lm{"IMPORT + last-mile?"}
|
||||||
|
lmleg["Last-mile leg: truck delivery →<br/>DELIVERED (B)"]
|
||||||
|
imp["Import customs finalization<br/>(declaration/duty/risk/release) (B)"]
|
||||||
|
end
|
||||||
|
|
||||||
|
sched --> fm
|
||||||
|
fm -->|"Yes"| fmleg --> wh_in
|
||||||
|
fm -->|"No"| wh_in
|
||||||
|
wh_in --> disp --> track --> arrive --> wh_out
|
||||||
|
wh_out --> imp
|
||||||
|
imp --> lm
|
||||||
|
lm -->|"Yes"| lmleg --> deliver
|
||||||
|
lm -->|"No"| deliver
|
||||||
|
|
||||||
|
deliver["Release order + gate clearance<br/>(warehouse fees must be PAID) (B)"]
|
||||||
|
deliver --> pod["Customer approves delivery /<br/>POD captured → DELIVERED (P)"]
|
||||||
|
pod --> complete(["Booking COMPLETED"])
|
||||||
|
|
||||||
|
track -.->|"public timeline"| custview["Customer tracking page (P)"]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cross-cutting truth:** almost every hop between domains is fired by a **domain event** (`@OnEvent`),
|
||||||
|
not a direct call. See §13 for the event web (e.g. `booking.invoice.paid` → advance booking → auto-create
|
||||||
|
first-mile; `warehouse inspection PASSED` → auto-create last-mile; payment webhook → settle invoice).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Domain map (where each module lives in the journey)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
subgraph INTAKE["Intake & Identity"]
|
||||||
|
auth[auth / otp / verifayda]
|
||||||
|
comp[companies]
|
||||||
|
sig[signatures]
|
||||||
|
end
|
||||||
|
subgraph COMMERCIAL["Commercial"]
|
||||||
|
bk[bookings]
|
||||||
|
ct[contracts]
|
||||||
|
re[rule-engine]
|
||||||
|
end
|
||||||
|
subgraph OPS["Rail Operations"]
|
||||||
|
ts[train-scheduling]
|
||||||
|
resch[scheduling-reschedule]
|
||||||
|
fleet[trains/wagons/locomotives/wagon-types]
|
||||||
|
track[tracking]
|
||||||
|
end
|
||||||
|
subgraph GROUND["Ground Operations"]
|
||||||
|
fmlm[first-mile / last-mile]
|
||||||
|
imp[import-operations / interchange-documents]
|
||||||
|
dv[drivers / vehicles]
|
||||||
|
end
|
||||||
|
subgraph WH["Warehouse"]
|
||||||
|
wh[warehouses + inventory + loadings + inspection + rules]
|
||||||
|
whinv[warehouse-fee-invoices]
|
||||||
|
end
|
||||||
|
subgraph MONEY["Money"]
|
||||||
|
bill[billing]
|
||||||
|
paymod[payment]
|
||||||
|
payapi[edr-payment-api]
|
||||||
|
end
|
||||||
|
subgraph PLATFORM["Platform / Config"]
|
||||||
|
dd[dropdown-settings]
|
||||||
|
fus[file-upload-settings]
|
||||||
|
files[files / minio]
|
||||||
|
fac[facilities / routes]
|
||||||
|
notif[notifications / inbox]
|
||||||
|
ov[overview]
|
||||||
|
bo[backoffice/IAM]
|
||||||
|
end
|
||||||
|
|
||||||
|
INTAKE --> COMMERCIAL --> OPS --> GROUND --> WH --> MONEY
|
||||||
|
re -.->|rates/approval/priority| COMMERCIAL
|
||||||
|
COMMERCIAL -.->|invoices| MONEY
|
||||||
|
WH -.->|storage invoices| MONEY
|
||||||
|
GROUND -.->|mile invoices| MONEY
|
||||||
|
PLATFORM -.-> COMMERCIAL
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Identity, Access & Onboarding
|
||||||
|
|
||||||
|
### 5.1 Flow
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
su["Signup (external IAM)"] --> chk["GET /auth/check-availability (P)<br/>(email/phone taken?) @Public"]
|
||||||
|
chk --> otp["POST /otp/send + /otp/verify (P) @Public"]
|
||||||
|
otp --> fayda{"Identity proofing?"}
|
||||||
|
fayda -->|"VeriFayda 2.0"| fstart["POST /fayda/verification/start<br/>→ eSignet authorize URL"]
|
||||||
|
fstart --> fcb["Fayda redirect → GET /callback (ack)<br/>→ GET /fayda/verification/complete<br/>(PKCE code exchange → upsert iam.users)"]
|
||||||
|
fcb --> onb
|
||||||
|
fayda -->|"skip"| onb
|
||||||
|
|
||||||
|
onb["POST /companies/onboarding/start (P)<br/>(draft company, placeholder TIN, PENDING)"]
|
||||||
|
onb --> wiz["Wizard saves incrementally (P):<br/>PATCH /profile · /onboarding-step ·<br/>upload license & docs"]
|
||||||
|
wiz --> reqs["GET /companies/onboarding/requirements<br/>(server-driven checklist)"]
|
||||||
|
reqs --> comp["POST /companies/onboarding/complete<br/>→ profiles + company = PENDING"]
|
||||||
|
comp --> review["Backoffice approves (B):<br/>PATCH /companies/company-profiles/:id/status<br/>→ mint reference, company → ACTIVE"]
|
||||||
|
review --> book(["Can now book<br/>(assertCompanyProfileApprovedForBooking)"])
|
||||||
|
```
|
||||||
|
|
||||||
|
Company status: `PENDING → ACTIVE` (+ `SUSPENDED`, `BLACKLISTED`). Nationality (`ethiopian`/`foreign`)
|
||||||
|
drives the required document set. Booking guards elsewhere `403` if the acting profile is not `ACTIVE`.
|
||||||
|
|
||||||
|
### 5.2 Endpoints
|
||||||
|
|
||||||
|
| Method | Path | Action | Guard | FE |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| GET | `/api/auth/check-availability` | email/phone dedupe | `@Public` | (P) auth.service |
|
||||||
|
| GET | `/api/me` | enriched profile + `permissionKeys` + catalog | JwtGuard | (B) auth/api |
|
||||||
|
| POST | `/api/otp/send` · `/api/otp/verify` | send / verify 6-digit code | `@Public` | (P) auth.service |
|
||||||
|
| POST | `/api/fayda/verification/start` | start eSignet session (PKCE) | `@Public` + OptionalJwt | (B) verifayda.service |
|
||||||
|
| GET | `/api/fayda/verification/complete` | code→identity, upsert `iam.users` | `@Public` | (B) verifayda.service |
|
||||||
|
| GET | `/api/fayda/verification/status` | current user's Fayda link | JwtGuard | — |
|
||||||
|
| GET | `/callback` | passive Fayda redirect ack (no `/api`) | `@Public` | popup postMessage |
|
||||||
|
| GET·PUT | `/api/me/signature` | reusable signature (MinIO, base64) | JwtGuard | (P)(B) signatures.service |
|
||||||
|
| GET | `/api/test_user1` · `/api/test_user2` | permission-guard demo | `PermissionGuard` | (B) demo pages |
|
||||||
|
| GET | `/api/companies/getInfo` · `/profile` · `/dashboard` | company info / KPIs | JwtGuard | (P) companies.service |
|
||||||
|
| POST | `/api/companies/fetch-etrade-info` | pull reg data by TIN | JwtGuard | (P) |
|
||||||
|
| PATCH | `/api/companies/profile` | update profile (JSONB attrs) | JwtGuard | (P) |
|
||||||
|
| POST | `/api/companies/onboarding/start` · `/complete` | onboarding lifecycle | JwtGuard | (P) |
|
||||||
|
| PATCH | `/api/companies/onboarding-step` · `/active-mode` | wizard state / mode switch | JwtGuard | (P) |
|
||||||
|
| GET | `/api/companies/onboarding/requirements` | server checklist | JwtGuard | (P) |
|
||||||
|
| POST·GET | `/api/companies/company-profiles/:id/license` | license file up/list | JwtGuard | (P) |
|
||||||
|
| POST | `/api/companies/company-profiles` · `/company-profile` | add operational profile(s) | JwtGuard | (P) |
|
||||||
|
| POST·GET·PATCH·DELETE | `/api/companies` (+`/:id`) | company CRUD | `@FreightAdmin` writes | (B) customers.service |
|
||||||
|
| GET | `/api/companies/stats` | KPI strip | JwtGuard | (B) |
|
||||||
|
| PATCH | `/api/companies/company-profiles/:id/status` | approve profile → mint ref | `@FreightAdmin` | (B) |
|
||||||
|
| GET·POST | `/api/companies/:companyId/documents` | company docs (signed URLs) | JwtGuard | (P)(B) |
|
||||||
|
| GET | `/api/notifications` (+`/unread-count`) | inbox list / unread | JwtGuard | (P)(B) notificationsApi |
|
||||||
|
| PATCH·POST | `/api/notifications/:id/read` · `/read-all` | mark read (WS re-emit) | JwtGuard | (P)(B) |
|
||||||
|
| WS | `NOTIFICATION_WS_NAMESPACE` | live push (server→client) | WsAuth handshake | (P)(B) useNotificationSocket |
|
||||||
|
|
||||||
|
`notifications` module = SMS/email transport over RabbitMQ (no HTTP routes). Inbox fan-out writes one
|
||||||
|
`notifications` row + WS push per recipient; **HIGH** priority also emails + SMSs (best-effort).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Bookings — the core state machine
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
stateDiagram-v2
|
||||||
|
[*] --> DRAFT: create (P) (staff commercial auto price+submit)
|
||||||
|
DRAFT --> SUBMITTED: submit (P)
|
||||||
|
DRAFT --> PRICE_CHANGED_PENDING_CONFIRM: price moved
|
||||||
|
PRICE_CHANGED_PENDING_CONFIRM --> SUBMITTED: confirm-submit (P)
|
||||||
|
SUBMITTED --> REJECTED: customer reject (price) (P)
|
||||||
|
SUBMITTED --> CHANGES_REQUESTED: staff request-changes (B)
|
||||||
|
CHANGES_REQUESTED --> SUBMITTED: edit + resubmit (P)
|
||||||
|
SUBMITTED --> PENDING_APPROVAL: staff/accept (B) (instantiate approval steps)
|
||||||
|
SUBMITTED --> REJECTED: staff/reject (B)
|
||||||
|
PENDING_APPROVAL --> APPROVED_PENDING_SIGNATURE: line-staff approve (B)
|
||||||
|
APPROVED_PENDING_SIGNATURE --> APPROVED: director + ceo approve (B)
|
||||||
|
PENDING_APPROVAL --> REJECTED: rejectStep (B)
|
||||||
|
APPROVED --> CONTRACT_READY: contract/generate (B)
|
||||||
|
CONTRACT_READY --> SIGNED_CUSTOMER: customer sign (P)
|
||||||
|
SIGNED_CUSTOMER --> AWAITING_DOCUMENTS: counter-sign, IMPORT/EXPORT (B)
|
||||||
|
SIGNED_CUSTOMER --> FULLY_EXECUTED: counter-sign, DOMESTIC (B)
|
||||||
|
AWAITING_DOCUMENTS --> DOCUMENTS_UNDER_REVIEW: clearance/documents (P)
|
||||||
|
DOCUMENTS_UNDER_REVIEW --> CLEARANCE_READY: review + finalize (B)
|
||||||
|
CLEARANCE_READY --> OPERATION_REQUEST_PENDING: clearance/proceed (P)
|
||||||
|
OPERATION_REQUEST_PENDING --> OPERATION_CHANGES_REQUESTED: review=REQUEST_CHANGES (B)
|
||||||
|
OPERATION_CHANGES_REQUESTED --> OPERATION_REQUEST_PENDING: re-proceed (P)
|
||||||
|
OPERATION_REQUEST_PENDING --> ROAD_DISPATCH_PENDING: accept=road (B) (invoice, KM billed)
|
||||||
|
OPERATION_REQUEST_PENDING --> FULLY_EXECUTED: accept=train (B) (invoice)
|
||||||
|
FULLY_EXECUTED --> SELECTED_FOR_BATCH: batch engine offers wagons (sys)
|
||||||
|
SELECTED_FOR_BATCH --> PAID: pay invoice (P)
|
||||||
|
ROAD_DISPATCH_PENDING --> PAID: pay invoice (P)
|
||||||
|
PAID --> IN_TRANSIT: operations/start-transit (B)
|
||||||
|
IN_TRANSIT --> COMPLETED: operations/complete (B)
|
||||||
|
DRAFT --> PENDING_CONSOLIDATION: requestConsolidation (usually set on submit)
|
||||||
|
DRAFT --> CANCELLED: cancel (B)
|
||||||
|
PENDING_APPROVAL --> CANCELLED: cancel (B)
|
||||||
|
OPERATION_REQUEST_PENDING --> CANCELLED: cancel (B)
|
||||||
|
note right of PAID
|
||||||
|
governmentExpedite (B) jumps
|
||||||
|
gov bookings straight to PAID/Eligible
|
||||||
|
end note
|
||||||
|
note left of CANCELLED
|
||||||
|
cancel is staff-only and allowed ONLY from
|
||||||
|
DRAFT, SUBMITTED, PRICE_CHANGED_PENDING_CONFIRM,
|
||||||
|
CHANGES_REQUESTED, PENDING_APPROVAL,
|
||||||
|
CONTRACT_READY, OPERATION_REQUEST_PENDING
|
||||||
|
end note
|
||||||
|
REJECTED --> [*]
|
||||||
|
CANCELLED --> [*]
|
||||||
|
COMPLETED --> [*]
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Accuracy notes (verified against code):**
|
||||||
|
> - Counter-sign branch is keyed on **trade direction**, not a customs flag: `IMPORT`/`EXPORT` →
|
||||||
|
> `AWAITING_DOCUMENTS` (even when customs is off — a lighter "without customs" clearance doc-set still
|
||||||
|
> applies); only `DOMESTIC` → `FULLY_EXECUTED`.
|
||||||
|
> - **Domestic** bookings skip the operation-request/clearance phase entirely — counter-sign enqueues
|
||||||
|
> them straight into the scheduling batch pipeline (`enqueueScheduleProcessing`).
|
||||||
|
> - Train `operation/review` accept sets **`FULLY_EXECUTED`** (day batch holding pool). `SELECTED_FOR_BATCH`
|
||||||
|
> is set **later** by the batch engine when a wagon offer/reservation is made — not at accept.
|
||||||
|
> - `APPROVED_PENDING_SIGNATURE` is a real intermediate (line-staff approves first, then director+CEO).
|
||||||
|
> - Full `BOOKING_STATUSES` has 35 values; this diagram is the live commercial subset (legacy statuses
|
||||||
|
> like `WAGON_ASSIGNED`, `INVOICED`, `PNR_GENERATED` are unused).
|
||||||
|
|
||||||
|
Key endpoints (customer (P) / staff (B), `bk:` = `bookings:` perms):
|
||||||
|
|
||||||
|
| Method | Path | Action | Guard |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| POST | `/api/bookings` | create | in-body (gov→`bk:staff_accept`) |
|
||||||
|
| PATCH·DELETE | `/api/bookings/:id` | update / soft-delete DRAFT | company-scoped |
|
||||||
|
| POST | `/api/bookings/:id/generate-price` | price preview (rule-engine) | company-scoped |
|
||||||
|
| POST | `/api/bookings/:id/submit` · `/confirm-submit` | submit (rate snapshot) | company-scoped |
|
||||||
|
| POST | `/api/bookings/:id/reject` | customer rejects price | company-scoped |
|
||||||
|
| POST | `/api/bookings/:id/staff/accept` | → PENDING_APPROVAL | `bk:staff_accept` |
|
||||||
|
| POST | `/api/bookings/:id/staff/request-changes` · `/staff/reject` | intake outcomes | `bk:request_changes` / `bk:reject` |
|
||||||
|
| POST | `/api/bookings/:id/approval-steps/:stepId/approve` · `/reject` | approval chain | role perms |
|
||||||
|
| POST | `/api/bookings/:id/contract/generate` | → CONTRACT_READY | `bk:generate_contract` |
|
||||||
|
| POST | `/api/bookings/:id/contract/sign` · `/marketing/approve` | sign / counter-sign | in-body / `bk:sign_staff` |
|
||||||
|
| GET | `/api/bookings/:id/contract/{view,document}` | HTML / PDF | company-scoped |
|
||||||
|
| GET·POST | `/api/bookings/:id/clearance` (+`/documents`,`/review`,`/finalize`,…) | customs clearance | `bk:*` / `ct:clearance_*` |
|
||||||
|
| POST | `/api/bookings/:id/clearance/proceed` | request operation | company-scoped |
|
||||||
|
| POST | `/api/bookings/:id/operation/review` | accept/changes (+invoice) | `bk:operations` |
|
||||||
|
| POST | `/api/bookings/:id/government-expedite` | gov shortcut → PAID | `bk:staff_accept` |
|
||||||
|
| POST | `/api/bookings/:id/operations/start-transit` · `/complete` | transit lifecycle | `bk:operations` |
|
||||||
|
| POST | `/api/bookings/:id/allocate-containers` | assign containers↔vehicles | `allocation:manage` |
|
||||||
|
| POST·GET·DELETE | `/api/bookings/:id/consolidation` | pair/unpair wagon-share | company-scoped |
|
||||||
|
| POST | `/api/bookings/:id/customer-truck-assignment` | external truck for pickup | company-scoped |
|
||||||
|
| GET | `/api/bookings/:id/tracking` | consignment + tracking events | company-scoped |
|
||||||
|
| GET | `/api/bookings` · `/list-summary` · `/queues/:queue` | lists (branch by perm) | `bk:view` / `bk:clearance_view` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Contracts — framework agreements & booking paths
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
stateDiagram-v2
|
||||||
|
[*] --> DRAFT: create (P)
|
||||||
|
DRAFT --> SUBMITTED: submit (P) (freeze rate snapshots)
|
||||||
|
SUBMITTED --> PENDING_APPROVAL: staff/accept (B)
|
||||||
|
SUBMITTED --> CHANGES_REQUESTED: request-changes (B)
|
||||||
|
CHANGES_REQUESTED --> SUBMITTED: resubmit (P)
|
||||||
|
SUBMITTED --> REJECTED: reject (B)
|
||||||
|
PENDING_APPROVAL --> APPROVED: approve chain (B)
|
||||||
|
APPROVED --> CONTRACT_READY: contract/generate (B)
|
||||||
|
CONTRACT_READY --> SIGNED_CUSTOMER: customer sign (P)
|
||||||
|
SIGNED_CUSTOMER --> AWAITING_CLEARANCE_DOCUMENTS: counter-sign, IMPORT/EXPORT (not GENERAL+customs) (B)
|
||||||
|
SIGNED_CUSTOMER --> CONTRACT_ACTIVE: counter-sign, GENERAL+customs or DOMESTIC (B)
|
||||||
|
SIGNED_CUSTOMER --> FULLY_EXECUTED: counter-sign, ONE_TIME DOMESTIC (B)
|
||||||
|
CONTRACT_ACTIVE --> [*]: renew → RENEWAL_DRAFT
|
||||||
|
```
|
||||||
|
|
||||||
|
> Diagram shows the live path; `CONTRACT_STATUSES` has **24 values** total (adds APPROVED_PENDING_SIGNATURE,
|
||||||
|
> CLEARANCE_UNDER_REVIEW, CLEARANCE_READY_FOR_BOOKING, ACTIVE_SHIPMENT_IN_PROGRESS, CONTRACT_CLOSED, CANCELLED,
|
||||||
|
> RENEWAL_SUBMITTED/PENDING_APPROVAL, AMENDMENTS_PROPOSED, ARCHIVED — see §15). **GENERAL+customs skips the
|
||||||
|
> contract clearance cycle → `CONTRACT_ACTIVE` directly** (clearance runs per-booking, Path B); only IMPORT/EXPORT
|
||||||
|
> one-time or self-clearance opens the contract-level `AWAITING_CLEARANCE_DOCUMENTS` cycle.
|
||||||
|
|
||||||
|
**Two ways a contract spawns shipment bookings:**
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
active["Contract ACTIVE / SELF_CLEARED"] --> path{"Contract path"}
|
||||||
|
path -->|"Path A: transport-only"| a1["Ops reviews self-clearance docs (B)<br/>(ops-review → ops-finalize → SELF_CLEARED)"]
|
||||||
|
a1 --> a2["Customer books directly (P)<br/>POST /contracts/:id/bookings"]
|
||||||
|
path -->|"Path B: GENERAL + customs"| b1["Customer submits BookingRequest (P)<br/>POST /contracts/:id/booking-requests"]
|
||||||
|
b1 --> b2["GL queue → accept (B)<br/>(ct:create_booking) → GL creates booking"]
|
||||||
|
a2 --> cap["createUnderContract:<br/>window + capacity draw-down check"]
|
||||||
|
b2 --> cap
|
||||||
|
cap --> spawn(["New Booking under contract<br/>(bookings.contract_id)"])
|
||||||
|
```
|
||||||
|
|
||||||
|
Contract clearance is **phased** (ET vs DJ permissioned): declaration → duty advice → duty slip →
|
||||||
|
transit permit → delivery/release order → T1 docs/close → final invoice → incidents. `clearanceStatus`
|
||||||
|
is a separate axis (`AWAITING_DOCUMENTS → DOCUMENTS_UNDER_REVIEW → CLEARANCE_READY_FOR_BOOKING /
|
||||||
|
SELF_CLEARED / ACTIVE_SHIPMENT_IN_PROGRESS`). ~60 contract routes total. Per-booking clearance
|
||||||
|
actions (`duty`, `risk`, `t1-documents`, `t1-close`, `final-invoice`, `second-duty`, `transport-document`,
|
||||||
|
`station-assign`, `incidents`) hang off `/api/contracts/bookings/:bookingId/*`; note **`declaration` is a
|
||||||
|
contract-level route** (`/api/contracts/:id/clearance/declaration`), not a booking-scoped one.
|
||||||
|
|
||||||
|
### Consignment / Cargo / Container (fleet-side records)
|
||||||
|
|
||||||
|
| Entity | Controller | Guard | Lifecycle | Callers |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| Consignment | `/api/consignments` (create/list/get) | `FleetView`/`Manage` | `Pending → …` (downstream shipment record; booking `:id/tracking` reads it) | (P) read-only |
|
||||||
|
| Cargo | `/api/cargoes` (+ `/load` `/unload` `/deliver`) | `FleetView`/`Manage` | `PENDING → LOADED → UNLOADED / DELIVERED` (POD) | (B) cargoService |
|
||||||
|
| Container | `/api/containers` (+ `/assign-wagon` `/unassign-wagon`) | `FleetView`/`Manage` | `AVAILABLE → LOADED → IN_TRANSIT …` | (B) containerService |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Rule Engine — the pricing & approval brain
|
||||||
|
|
||||||
|
`RuleEngineService.evaluate(input)` is injected into bookings & contracts pricing. One pass pulls all
|
||||||
|
rule tables and returns `{ priorityScore, appliedModifiers, containerWeightResults, warnings,
|
||||||
|
hardBlocked, requiresDirectorApproval }`.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
ev["evaluate(BookingEvaluationInput)"] --> rates["rates.findLiveRates()<br/>(only LIVE)"]
|
||||||
|
rates --> sur{"surcharge triggers"}
|
||||||
|
sur -->|"HAZARDOUS/REEFER/OVERWEIGHT/<br/>SHIPPING_LINE/CONSOLIDATION"| mods["appliedModifiers →<br/>surcharge line-items"]
|
||||||
|
mods --> snap["snapshotRates → booking_rate_snapshots<br/>(freeze exact rate used)"]
|
||||||
|
ev --> wlr["weight-limit-rules"]
|
||||||
|
wlr --> block{"VGM > maxCapacityTons?"}
|
||||||
|
block -->|Yes| hard["HARD BLOCK (400)"]
|
||||||
|
block -->|"over maxVgm, within cap"| warn["warning + OVERWEIGHT surcharge"]
|
||||||
|
ev --> appr["approval-rules →<br/>instantiateApprovalSteps<br/>(requiredRole/blocksRole/stepOrder)"]
|
||||||
|
ev --> prio["priority-configs + serviceType bonus +<br/>GOVERNMENT_PRIORITY_BONUS → priorityScore"]
|
||||||
|
prio --> schedorder["train-scheduling orders by priorityScore"]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Rate lifecycle:** `DRAFT → (submit) PENDING_APPROVAL → (CEO approve) LIVE`. Only LIVE rates apply.
|
||||||
|
|
||||||
|
| Resource | Base path | Guard | Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| approval-rules | `/api/approval-rules` (+`/chain`,`/reorder`,`/:id/move-order`) | `ruleEngine.view/manage(approval-rules)` | ordered chain |
|
||||||
|
| cargo-types | `/api/cargo-types` | `…(cargo-types)` | portal reads via `bookings/reference-data` |
|
||||||
|
| container-types | `/api/container-types` | `…(container-types)` | portal indirect |
|
||||||
|
| priority-configs | `/api/priority-configs` | `…(priority-configs)` | WAGON / CURRENCY bands |
|
||||||
|
| rates | `/api/rates` (+`/live`,`/:id/submit`,`/approve`) | `…(rates)` | DRAFT→PENDING→LIVE |
|
||||||
|
| service-types | `/api/service-types` | `…(service-types)` | `includesFirstMile/LastMile` flags |
|
||||||
|
| shipping-lines | `/api/shipping-lines` | `…(shipping-lines)` | surcharge trigger |
|
||||||
|
| weight-limit-rules | `/api/weight-limit-rules` | `…(weight-limit-rules)` | VGM hard-block |
|
||||||
|
| yards | `/api/yards` | `…(yards)` | routes/warehouses read |
|
||||||
|
|
||||||
|
**Settings & files** (reads open, writes `@FreightAdmin`):
|
||||||
|
`/api/dropdown-settings/*`, `/api/file-upload-settings/*` (config that drives portal forms;
|
||||||
|
server enforces *required-doc presence* at clearance, not size/MIME — those are client-side).
|
||||||
|
`GET /api/files/:fileId` is `@Public` (browsers load `<img>/<a>` without a bearer); upload is **direct
|
||||||
|
multipart** (multer memory → `FilesService` → MinIO `putObject`), presigned URLs used only for authenticated
|
||||||
|
reads (300 s TTL). `routes` (`route_milestones`) and `facilities` are config readers with **no permission
|
||||||
|
decorator** (behind the global JwtGuard only). NOTE: Note `FacilitiesModule` is never imported into `AppModule`,
|
||||||
|
so `/api/facilities` is unmounted/dead.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Train Scheduling & Rail Operations
|
||||||
|
|
||||||
|
Two consist models: **`trains`** = static fleet inventory (never created by scheduling);
|
||||||
|
**`train_sets`** = the operational consist scheduling builds per departure (**≥2 locomotives** +
|
||||||
|
wagon-type slots, physical wagons *pinned* later).
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
stateDiagram-v2
|
||||||
|
[*] --> DRAFT: create schedule (B) (≥2 locos, derive direction, freeze window rule)
|
||||||
|
DRAFT --> DRAFT: assign-bookings / run-allocation / pin-wagons (B)
|
||||||
|
DRAFT --> SCHEDULED: finalize (B) (bookings → Scheduled, ≥1 booking)
|
||||||
|
SCHEDULED --> DISPATCHED: dispatch (B) (train_number, locos ASSIGNED, window CLOSED, unpaid EXPIRED)
|
||||||
|
DISPATCHED --> DISPATCHED: recordCheckpoint (B) (corridor stations)
|
||||||
|
DISPATCHED --> ARRIVED: arrive (B) (bookings IN_TRANSIT, locos+wagons freed, warehouse arrival automation)
|
||||||
|
DRAFT --> CANCELLED: cancel (B)
|
||||||
|
SCHEDULED --> CANCELLED: cancel (B)
|
||||||
|
ARRIVED --> [*]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Reschedule** (`DRAFT`/`SCHEDULED` only): `preview` merges current + incoming bookings, sorts by
|
||||||
|
`compareSchedulingPriority` (**government first, then priorityScore**), greedily keeps those that still
|
||||||
|
fit → *retained*; overflow → *displaced*; commercial displaced are *readmitted* if room remains.
|
||||||
|
`execute` re-verifies, optionally sets new departure, unassigns displaced, re-assigns final, writes a
|
||||||
|
`scheduling_events` audit row. Triggers: `GOVERNMENT_PREEMPT`, `TRAIN_MAINTENANCE`.
|
||||||
|
|
||||||
|
Endpoint groups (`train-scheduling` prefix, `trainScheduling.view/manage`):
|
||||||
|
|
||||||
|
| Group | Representative routes |
|
||||||
|
| --- | --- |
|
||||||
|
| Discovery (customer (P), unguarded) | `bookable-schedules`, `available-days`, `available-days-for-cargo`, `my-booking-windows`, `contracts/:id/booking-windows` |
|
||||||
|
| Board (staff (B)) | `batch-board`, `batch-board/:id`, `eligible-bookings`, `container|bulk/eligible-bookings`, `global-rules` |
|
||||||
|
| Build | `container|bulk/preview`, `container|bulk/schedules` (create), `:id/assign-bookings`, `:id/assign-unassigned-booking`, `:id/pin-wagons`, `:id/run-allocation`, `:id/run-batch` |
|
||||||
|
| Composition edits | `:id/wagons/:wagonId` (remove slot), `:id/container-items/:itemId`, `bookings/:bookingId/{mark-paid,expire,move-schedule}` |
|
||||||
|
| Lifecycle | `schedules/:id/{finalize,dispatch,arrive,booking-window,doc-review-complete}`, `{container\|bulk}/schedules/:id/cancel` |
|
||||||
|
| Tracking | `schedules/:id/checkpoints` (GET/POST) → writes `train_checkpoint_events` |
|
||||||
|
| Djibouti import ops | `:id/import-djibouti/*` (gatepass, ready-for-loading, loaded, depart, load-list) |
|
||||||
|
| Reschedule | `:id/reschedule/preview`, `:id/reschedule/execute`, `:id/maintenance` |
|
||||||
|
|
||||||
|
**Fleet master data** (`FleetView/Manage`): `/api/trains` (+`/:trainId/reorder-wagons`), `/api/wagons`
|
||||||
|
(+`/assign-train`,`/unassign-train`), `/api/locomotives` (+`/decommission`), `/api/wagon-types`
|
||||||
|
(rule-engine guarded). **Asset records** (unguarded, keyed by *road* `vehicleId`): `/api/maintenance/*`,
|
||||||
|
`/api/fuel/*`. **`fleet_events`** is an append-only audit read via `drivers/:id/history` &
|
||||||
|
`vehicles/:id/history`.
|
||||||
|
|
||||||
|
**Tracking (customer-facing):** `GET /api/tracking/:consignmentId` → `tracking_events` timeline
|
||||||
|
(`location`, `ConsignmentStatus`, `occurredAt`), consumed by portal `TrackingPage`. It is **JWT-guarded**
|
||||||
|
(no permission decorator — not public). Distinct from staff train **checkpoints** (`train_checkpoint_events`,
|
||||||
|
written by `recordCheckpoint`/`arrive`). NOTE: **`TrackingService.record()` has no caller anywhere in the
|
||||||
|
codebase — nothing writes `tracking_events`, so the customer tracking timeline is currently unpopulated;
|
||||||
|
only the staff `train_checkpoint_events` store is written.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Warehouse — inventory lifecycle
|
||||||
|
|
||||||
|
Hierarchy `Facility → Warehouse → Yard → Zone`; every level tracks weight/volume/container capacity kept
|
||||||
|
in sync by `applyCapacityDelta`. `warehouse_inventory` carries nullable FKs to `booking`, `cargo`,
|
||||||
|
`container` — the join point between warehouse and the shipment.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
stateDiagram-v2
|
||||||
|
[*] --> RECEIVED: receive / bulkReceive (B)
|
||||||
|
[*] --> UNLOADED: auto-unload-arrived (import train) (B)
|
||||||
|
RECEIVED --> STORED: store (B) (allocation rule picks yard/zone)
|
||||||
|
UNLOADED --> STORED: store (B)
|
||||||
|
UNLOADED --> READY_FOR_PICKUP: inspection PASSED + IMPORT (B)
|
||||||
|
RECEIVED --> READY_FOR_PICKUP: inspection PASSED + IMPORT (B)
|
||||||
|
STORED --> RESERVED: reserve (B) (booking PAID)
|
||||||
|
RESERVED --> READY_FOR_LOADING: mark-ready-for-loading (B) (inspection PASSED)
|
||||||
|
READY_FOR_LOADING --> LOADED: load onto wagon (B) (+ warehouse_loadings)
|
||||||
|
LOADED --> DISPATCHED: dispatch / bulk-dispatch-export (B)
|
||||||
|
DISPATCHED --> UNLOADED_AT_DJIBOUTI_PORT: auto-unload-at-djibouti (B)
|
||||||
|
READY_FOR_PICKUP --> DELIVERED: release → deliver (B) (POD, fees PAID via gate-clearance)
|
||||||
|
READY_FOR_PICKUP --> STORED: re-store import item (B)
|
||||||
|
READY_FOR_PICKUP --> DISPATCHED: dispatch out (B)
|
||||||
|
UNLOADED_AT_DJIBOUTI_PORT --> [*]
|
||||||
|
DELIVERED --> [*]
|
||||||
|
```
|
||||||
|
|
||||||
|
**Export branch:** receive → STORED → RESERVED (booking PAID) → READY_FOR_LOADING → LOADED → DISPATCHED
|
||||||
|
→ UNLOADED_AT_DJIBOUTI_PORT. Note `store()` does **not** check inspection — `RECEIVED → STORED` is legal
|
||||||
|
without it; inspection **PASSED** is enforced only at `mark-ready-for-loading`. **Import branch:** UNLOADED
|
||||||
|
→ inspect PASSED → READY_FOR_PICKUP (auto-creates
|
||||||
|
last-mile if requested) → release → DELIVERED. Every transition writes `warehouse_activity_log`.
|
||||||
|
|
||||||
|
**Storage billing** is *not* a separate table — `WarehouseInvoiceService` is a thin layer over the
|
||||||
|
central **billing** module (global `Invoice` rows, `source='warehouse'`, `sourceId=inventoryId`). Fees =
|
||||||
|
`STORAGE_FEE` + `DEMURRAGE` via `warehouse_fee_rules` (free-days grace, tiers, FX-converted). **Unpaid
|
||||||
|
warehouse fees block exit:** `gateClearance`/`release` call `findBlockingInvoice` / `assertClearanceAllowed`.
|
||||||
|
|
||||||
|
Controller families (84 routes, all called by backoffice `warehouse.service.ts`; **no per-route
|
||||||
|
permission decorators → behind the global JwtGuard only**): `warehouses` · `warehouse-yards` ·
|
||||||
|
`warehouse-zones` · `warehouse-inventory` (queries + 25 mutation actions incl. bulk + import/export
|
||||||
|
queues, and the gate release `warehouse-inventory/:id/gate-clearance`) · `warehouse-loadings` ·
|
||||||
|
inspection (`…/inspection-reports`) · fee-invoices (`…/generate-fee-invoice`, `warehouse-fee-invoices/*`
|
||||||
|
incl. `pay`, `pay-online`) · rules (`warehouse-allocation-rules`, `warehouse-fee-rules`,
|
||||||
|
`warehouse-allocation/preview`).
|
||||||
|
|
||||||
|
> Portal touches only: `bookings/:id/warehouse-fee-invoices`, invoice `by-id`/`document`/`receipt`/
|
||||||
|
> `pay-online`, and `bookings/:bookingId/approve-delivery` (customer signs handover). Everything else (B).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Field Operations — first/last mile & import customs
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
binv["booking.invoice.paid event"] --> advance["advanceBookingOnPayment → booking PAID"]
|
||||||
|
advance --> fmreq{"EXPORT + first-mile requested?"}
|
||||||
|
fmreq -->|Yes| fmaccept["firstMileService.acceptBooking<br/>(auto-create leg)"]
|
||||||
|
fmreq -->|No| skip1["—"]
|
||||||
|
fmaccept --> fmleg
|
||||||
|
|
||||||
|
subgraph FM["First-mile (EXPORT origin road leg)"]
|
||||||
|
fmleg["READY_TO_TRANSIT"] --> fmveh["setVehicles → vehicle BUSY,<br/>SMS driver, fleet_events"]
|
||||||
|
fmveh --> fmtransit["IN_TRANSIT (needs assigned vehicle)"]
|
||||||
|
fmtransit --> fmdone["RECEIVED_TO_PORT (free vehicles)"]
|
||||||
|
fmleg --> fminv["generate-invoice → FIRST_MILE fee<br/>(locks distances once invoiced)"]
|
||||||
|
end
|
||||||
|
|
||||||
|
whrcv["warehouse inspection PASSED (IMPORT only)"] --> lmaccept["lastMileService.acceptBooking(reference)"]
|
||||||
|
lmaccept --> lmleg
|
||||||
|
subgraph LM["Last-mile (IMPORT destination road leg)"]
|
||||||
|
lmleg["READY_TO_TRANSIT"] --> lmtransit["IN_TRANSIT"] --> lmdone["DELIVERED (free vehicles)"]
|
||||||
|
lmleg --> lminv["generate-invoice → LAST_MILE fee"]
|
||||||
|
end
|
||||||
|
|
||||||
|
subgraph IMP["Import customs finalization (per booking, timestamp-driven)"]
|
||||||
|
direction TB
|
||||||
|
up["upload docs (IM4/IM5/T1_CLOSURE/…)"] --> decl["record declaration serial"]
|
||||||
|
decl --> notify["notify duties/taxes"]
|
||||||
|
notify --> paid["mark duties paid (needs CUSTOMER_PAYMENT_SLIP)"]
|
||||||
|
paid --> risk["assign risk (GREEN/YELLOW/BLUE/RED)"]
|
||||||
|
risk --> rel["release-permitted (asserts T1 + release permit + declaration + risk + paid)"]
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
| Module | Base | Guard | Terminal state |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| first-mile | `/api/first-mile` (+`/accept/:ref`,`/:id/{vehicles,distances,invoice}`) | `trainScheduling.view/manage` | `RECEIVED_TO_PORT` |
|
||||||
|
| last-mile | `/api/last-mile` (same shape) | `trainScheduling.view/manage` | `DELIVERED` |
|
||||||
|
| import-operations | `/api/import-operations/{customs,djibouti-incidents,empty-container-returns}/*` | none (global JwtGuard) | `completedAt` |
|
||||||
|
| interchange-documents | `/api/interchange-documents` (+`/generate-from-schedule`,`/:id/{acknowledge,dispute,cancel}`) | none | `ACKNOWLEDGED / DISPUTED` |
|
||||||
|
| drivers | `/api/drivers` (+`/:id/history`) | `FleetView/Manage` | soft-delete |
|
||||||
|
| vehicles | `/api/vehicles` (+`/:id/history`) | `FleetView/Manage` | soft-delete |
|
||||||
|
|
||||||
|
Interchange documents are the **rail↔port handover manifest** — generated from a train schedule,
|
||||||
|
snapshotting booking/container/cargo lines with per-item `conditionStatus` derived from warehouse
|
||||||
|
inspection flags. Status `DRAFT → GENERATED → ACKNOWLEDGED | DISPUTED | CANCELLED`. All FE callers (B).
|
||||||
|
|
||||||
|
**Mile trigger precision (verified):** first-mile is created only on `booking.invoice.paid` →
|
||||||
|
`advanceBookingOnPayment` → `firstMile.acceptBooking(bookingId)` when `EXPORT` + first-mile requested.
|
||||||
|
Last-mile is created **only on IMPORT inspection PASSED** (two call sites: `warehouse-inspection.service`
|
||||||
|
and the bulk-inspect branch of `warehouse-inventory.service`) — **not** on warehouse *receive*. The IMPORT
|
||||||
|
constraint is enforced by the warehouse caller, not inside `lastMile.acceptBooking(reference)`. Auto-created
|
||||||
|
legs enter at **`READY_TO_TRANSIT`** (the `PAYMENT_PENDING` entity default is bypassed).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Billing & Payment
|
||||||
|
|
||||||
|
### 12.1 Invoice lifecycle
|
||||||
|
|
||||||
|
Invoices are **source-agnostic** — `BillingService.generateInvoice()` is the single factory called by
|
||||||
|
domain services (never a controller): `source ∈ {booking, warehouse, first_mile, last_mile}` (the enum
|
||||||
|
also defines an unused `demurrage`), numbered `INV-YYYYMMDD-#####`, `dueAt = now + 14d`. **Initial status
|
||||||
|
varies by source:** booking invoices start `DRAFT` (issued at operation-accept via `billing.updateStatus`);
|
||||||
|
the `generateInvoice` default is `PENDING`; warehouse + contract-GL invoices start `ISSUED`.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
stateDiagram-v2
|
||||||
|
[*] --> DRAFT: booking invoice (issued at operation-accept)
|
||||||
|
[*] --> PENDING: generateInvoice default
|
||||||
|
[*] --> ISSUED: warehouse / contract-GL invoice
|
||||||
|
DRAFT --> ISSUED: billing.updateStatus
|
||||||
|
PENDING --> PARTIALLY_PAID: recordPayment (partial, offline)
|
||||||
|
ISSUED --> PARTIALLY_PAID: recordPayment (partial, offline)
|
||||||
|
PENDING --> PAID: markInvoiceAsPaid (gateway, full)
|
||||||
|
ISSUED --> PAID: markInvoiceAsPaid (gateway, full)
|
||||||
|
PARTIALLY_PAID --> PAID: final payment
|
||||||
|
PENDING --> EXPIRED: expirePayable (pay window lapses)
|
||||||
|
ISSUED --> CANCELLED: cancelInvoice (no payments)
|
||||||
|
PAID --> REFUNDED: markInvoiceAsRefunded (paidAmount>0)
|
||||||
|
PAID --> [*]
|
||||||
|
note right of PAID
|
||||||
|
emits ${source}.invoice.paid
|
||||||
|
(sources use first_mile / last_mile, underscores)
|
||||||
|
→ domain listeners advance booking / mile / warehouse
|
||||||
|
end note
|
||||||
|
note left of ISSUED
|
||||||
|
OVERDUE exists in the enum but NO code sets it
|
||||||
|
(no cron / setter) — effectively unused
|
||||||
|
end note
|
||||||
|
```
|
||||||
|
|
||||||
|
`OPEN_STATUSES = {Issued, Pending, PartiallyPaid, Overdue}` are payable. Transitions lock the row
|
||||||
|
(`pessimistic_write`); the event fires **after commit** for self-managed transitions, but **inline before
|
||||||
|
commit** when the transition is enlisted in a caller-supplied transaction `manager`.
|
||||||
|
|
||||||
|
### 12.2 freight-api ↔ payment-api integration
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
autonumber
|
||||||
|
participant U as Customer (P)
|
||||||
|
participant FB as Freight billing (:3001)
|
||||||
|
participant PA as Payment API (red) (:3003)
|
||||||
|
participant PV as Provider (Telebirr/Waafi/…)
|
||||||
|
participant OB as Payment outbox
|
||||||
|
participant FI as Freight internal ctrl
|
||||||
|
|
||||||
|
U->>FB: POST /billing/my-invoices/:id/pay
|
||||||
|
FB->>FB: payInvoice → validate OPEN, balance>0
|
||||||
|
FB->>PA: POST /payments/initiate (x-service-token)<br/>service=FREIGHT, referenceId=sourceId, amountMinor
|
||||||
|
PA-->>FB: { intentId, clientAction (REDIRECT/LAUNCH_APP/COLLECT_OTP) }
|
||||||
|
FB->>FB: store paymentId on invoice (correlation)
|
||||||
|
FB-->>U: clientAction → redirect to provider
|
||||||
|
U->>PV: authorize payment
|
||||||
|
PV->>PA: webhook POST /webhooks/{provider}
|
||||||
|
PA->>PA: verify signature, dedupe, intent state machine
|
||||||
|
PA->>OB: write PaymentEvent (payment.succeeded)
|
||||||
|
OB->>FI: POST /api/internal/payments/mark-paid (@Public, x-service-token)
|
||||||
|
Note over OB,FI: or RabbitMQ → PaymentEventsConsumer
|
||||||
|
FI->>FB: handlePaymentEvent → settleByPaymentId → markInvoiceAsPaid
|
||||||
|
FB->>FB: emit booking.invoice.paid
|
||||||
|
FB-->>U: invoice PAID (poll / notification)
|
||||||
|
```
|
||||||
|
|
||||||
|
> NOTE: **Demo shortcut in `payInvoice`:** today, if the provider doesn't settle synchronously, freight
|
||||||
|
> self-fires `handlePaymentEvent(payment.succeeded)` inline (marked TODO/remove) — invoices settle at
|
||||||
|
> pay-time without a real webhook. Providers: **Telebirr / CBE_BIRR / EBIRR** (ET), **Waafi / DMoney**
|
||||||
|
> (DJ), **CARD** (intl), **CAC_BANK** (OTP). Payment DB is a **separate `edr_payment` schema** — no
|
||||||
|
> cross-schema FKs; `referenceId` is a soft link.
|
||||||
|
|
||||||
|
### 12.3 Endpoints
|
||||||
|
|
||||||
|
| Method | Path | Facing | Guard |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| GET | `/api/billing/invoices` (+`/:id`,`/:id/document`,`/receipt`) | (B) backoffice | `bookings.view` |
|
||||||
|
| GET | `/api/billing/my-invoices` (+`/:id`,`/document`,`/receipt`) | (P) portal | `@CurrentUser` ownership |
|
||||||
|
| POST | `/api/billing/my-invoices/:id/pay` | (P) portal | ownership |
|
||||||
|
| POST | `/api/payments/initiate` | central | JwtGuard (no permission — **not** public) |
|
||||||
|
| GET | `/api/payments/checkout` | redirect | `@Public` |
|
||||||
|
| GET | `/api/payments/{summary,all}` | (B) backoffice | `bookings.view` |
|
||||||
|
| GET | `/api/payments/by-company/:companyId/customer-view` | (B) | none |
|
||||||
|
| GET | `/api/payments/intents/:bookingId` · `/receipt/:orderId` | reconcile / receipt | none / `@Public` |
|
||||||
|
| POST | `/api/internal/payments/mark-paid` | (red) service→service | `@Public` (NOTE: currently unauthenticated) |
|
||||||
|
| POST·GET·PUT | `/api/backoffice/organizations/:orgId/*` | (B) IAM user/role mgmt (NOT billing) | `@FreightAdmin` |
|
||||||
|
|
||||||
|
`GET /api/overview*` (7 tabs, `bookings.view`) is the backoffice dashboard aggregator over 11 repos.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Cross-cutting event web (`@OnEvent`)
|
||||||
|
|
||||||
|
The domains are stitched together by events, not direct calls. This is why the master flow "just happens".
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
pay["Payment webhook / demo shortcut"] --> settle["billing.settleByPaymentId → markInvoiceAsPaid"]
|
||||||
|
settle --> ev1{{"${source}.invoice.paid"}}
|
||||||
|
ev1 -->|source=booking| adv["booking-invoice: advanceBookingOnPayment → PAID"]
|
||||||
|
adv --> fm["firstMile.acceptBooking (EXPORT + requested)"]
|
||||||
|
adv --> batch["train-scheduling batch: markPaid → allocate"]
|
||||||
|
ev1 -->|source=first_mile| e2{{"first_mile.invoice.paid"}} --> fmp["NOTE: intended: leg paid=true<br/>(listener typo 'firstmile.invoice.paid' → never fires)"]
|
||||||
|
ev1 -->|source=last_mile| e3{{"last_mile.invoice.paid"}} --> lmp["last-mile → DELIVERED, paid=true"]
|
||||||
|
ev1 -->|source=warehouse| e4{{"warehouse.invoice.paid"}} --> whp["settle warehouse fee → unblock gate"]
|
||||||
|
|
||||||
|
arrive["train arrive"] --> whauto["warehouse arrival automation:<br/>auto-unload arrived bookings"]
|
||||||
|
insp["warehouse inspection PASSED (IMPORT)"] --> lmacc["lastMile.acceptBooking → READY_FOR_PICKUP"]
|
||||||
|
|
||||||
|
assign["mile setVehicles"] --> veh["vehicle BUSY + SMS driver + fleet_events"]
|
||||||
|
release["mile terminal / delete"] --> free["vehicle FREE (releaseIfUnused)"]
|
||||||
|
|
||||||
|
notify["notification-inbox.notify"] --> ws["WebSocket push (always)"]
|
||||||
|
notify -->|HIGH priority| smsemail["+ SMS + email via RabbitMQ"]
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. Portal vs Backoffice — who does what
|
||||||
|
|
||||||
|
| Capability | (P) Portal (customer) | (B) Backoffice (employee) |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Identity / onboarding | signup, OTP, Fayda, company onboarding, profile | company approval, IAM user/role mgmt |
|
||||||
|
| Bookings | create, price, submit, sign, upload docs, pay, approve delivery, track | accept, approve chain, generate contract, clearance review, operation review, allocate, dispatch |
|
||||||
|
| Contracts | create, submit, sign, booking-requests, self-clearance slips | approve, generate, phased clearance, GL booking creation |
|
||||||
|
| Rule engine | reads only via `bookings/reference-data` | full CRUD (rates approval, approval rules, priorities) |
|
||||||
|
| Scheduling | discover bookable days/schedules | build/finalize/dispatch/arrive/reschedule trains |
|
||||||
|
| Warehouse | pay storage fees, view invoices, approve handover | full inventory lifecycle + fees + rules |
|
||||||
|
| Field ops | (none direct) | first/last mile, import customs, interchange docs, fleet |
|
||||||
|
| Billing | pay own invoices, download docs/receipts | invoice + payment dashboards, per-customer views |
|
||||||
|
| Notifications | inbox + WS | inbox + WS |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 15. Status / state reference
|
||||||
|
|
||||||
|
| Entity | States (happy path → terminal) |
|
||||||
|
| --- | --- |
|
||||||
|
| Company | `PENDING → ACTIVE` (+ SUSPENDED, BLACKLISTED) |
|
||||||
|
| Booking | `DRAFT → SUBMITTED → PENDING_APPROVAL → APPROVED_PENDING_SIGNATURE → APPROVED → CONTRACT_READY → SIGNED_CUSTOMER → [IMPORT/EXPORT: AWAITING_DOCUMENTS → DOCUMENTS_UNDER_REVIEW → CLEARANCE_READY → OPERATION_REQUEST_PENDING → (train) FULLY_EXECUTED → SELECTED_FOR_BATCH / (road) ROAD_DISPATCH_PENDING] · [DOMESTIC: → FULLY_EXECUTED → SELECTED_FOR_BATCH] → PAID → IN_TRANSIT → COMPLETED` (branches: REJECTED, CANCELLED, CHANGES_REQUESTED, OPERATION_CHANGES_REQUESTED, PENDING_CONSOLIDATION). 35 statuses total; ~10 legacy ones unused. |
|
||||||
|
| Contract | `DRAFT → SUBMITTED → PENDING_APPROVAL → APPROVED_PENDING_SIGNATURE → APPROVED → CONTRACT_READY → SIGNED_CUSTOMER → AWAITING_CLEARANCE_DOCUMENTS / CONTRACT_ACTIVE / FULLY_EXECUTED` (+ CLEARANCE_UNDER_REVIEW, CLEARANCE_READY_FOR_BOOKING, ACTIVE_SHIPMENT_IN_PROGRESS, CONTRACT_CLOSED, CANCELLED, RENEWAL_DRAFT/SUBMITTED/PENDING_APPROVAL, AMENDMENTS_PROPOSED, ARCHIVED, EXPIRED — **24 total**). Separate `clearanceStatus` axis: NOT_APPLICABLE / AWAITING_DOCUMENTS / DOCUMENTS_UNDER_REVIEW / CLEARANCE_READY_FOR_BOOKING / SELF_CLEARED / ACTIVE_SHIPMENT_IN_PROGRESS. |
|
||||||
|
| Rate | `DRAFT → PENDING_APPROVAL → LIVE` (+ SUPERSEDED) |
|
||||||
|
| Train schedule | `DRAFT → SCHEDULED → DISPATCHED → ARRIVED` (+ CANCELLED) |
|
||||||
|
| Warehouse inventory | export: `RECEIVED → STORED → RESERVED → READY_FOR_LOADING → LOADED → DISPATCHED → UNLOADED_AT_DJIBOUTI_PORT` · import: `UNLOADED → READY_FOR_PICKUP → DELIVERED` (READY_FOR_PICKUP may also → STORED / DISPATCHED) |
|
||||||
|
| First-mile | `PAYMENT_PENDING → READY_TO_TRANSIT → IN_TRANSIT → RECEIVED_TO_PORT` (auto-created legs enter at READY_TO_TRANSIT) |
|
||||||
|
| Last-mile | `PAYMENT_PENDING → READY_TO_TRANSIT → IN_TRANSIT → DELIVERED` (auto-created legs enter at READY_TO_TRANSIT) |
|
||||||
|
| Invoice | `DRAFT / PENDING / ISSUED → PARTIALLY_PAID → PAID` (+ EXPIRED, CANCELLED, REFUNDED; **OVERDUE defined but never set**) |
|
||||||
|
| Interchange doc | `DRAFT → GENERATED → ACKNOWLEDGED / DISPUTED / CANCELLED` |
|
||||||
|
| Empty container return | `RETURNED → ASSIGNED_STORAGE → DOCUMENTATION_CLEARED → WAGON_ALLOCATED → TRANSPORTED_TO_DJIBOUTI → HANDOVER_ISSUED → COMPLETED` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 16. Notable gaps & caveats (verified against code)
|
||||||
|
|
||||||
|
- **Counter-sign is direction-based, not customs-based** (bookings *and* contracts): `IMPORT`/`EXPORT` open a
|
||||||
|
clearance gate even with customs off; only `DOMESTIC` skips it. The customs flag only selects the clearance
|
||||||
|
document set.
|
||||||
|
- **`SELECTED_FOR_BATCH` is set by the batch engine, not at operation-accept** — train accept sets
|
||||||
|
`FULLY_EXECUTED` first.
|
||||||
|
- **`FacilitiesModule` is never imported into `AppModule`** — `/api/facilities` is **unmounted / dead** (not
|
||||||
|
reachable at all). `tracking`, `maintenance`, `fuel` carry no permission decorator but *are* mounted — behind
|
||||||
|
the **global JwtGuard**, just not permission-gated.
|
||||||
|
- **`tracking_events` has no writer anywhere** — `TrackingService.record()` is never called, so the customer
|
||||||
|
tracking timeline is unpopulated. Only staff `train_checkpoint_events` are written (by `recordCheckpoint`/`arrive`).
|
||||||
|
- **First-mile paid-flag listener is a dead code path** — `@OnEvent("firstmile.invoice.paid")` (no underscore)
|
||||||
|
never fires because the emitted event is `first_mile.invoice.paid`; the first-mile `paid` flag is never flipped by settlement.
|
||||||
|
- **Invoice `OVERDUE` status is never set** — no cron/setter transitions to it, though it is in the enum and `OPEN_STATUSES`.
|
||||||
|
- `POST /api/internal/payments/mark-paid` is `@Public` with **no service auth** (ServiceAuthGuard removed; noted in code).
|
||||||
|
- **Demo settlement shortcut** in `billing.payInvoice` bypasses real webhooks (TODO/remove) — invoices settle inline at pay-time.
|
||||||
|
- Server does **not** validate upload size/MIME (client-side only); it enforces *required-doc presence* at clearance.
|
||||||
|
- Dead / unwired FE calls: `trains/:id/details` (no route), portal `consignments` service hits `/consignments` without `/api`, and list endpoints `bookings/my`, `queues/:queue`, `by-company/customer-view` have no active caller.
|
||||||
|
- NOTE: The repo `CLAUDE.md` states auth is stubbed/unwired — **this is stale**: auth is live via `@tria-plc/api-common` (global `JwtGuard` + `HasActiveDelegationGuard` `APP_GUARD`s).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Generated from `apps/edr-freight-api`, `apps/edr-freight-web/{portal,backoffice}`, `apps/edr-payment-api`
|
||||||
|
on branch `freight/feat/fixes-v1`. Reflects code at scan time; regenerate after major module changes.*
|
||||||
@@ -60,6 +60,7 @@ export const BOOKING_PERMISSIONS: FreightPermissionSeed[] = [
|
|||||||
perm('a1000001-0001-4000-8000-000000000011', 'edr_freight_app:fleet:view', 'View fleet'),
|
perm('a1000001-0001-4000-8000-000000000011', 'edr_freight_app:fleet:view', 'View fleet'),
|
||||||
perm('a1000001-0001-4000-8000-000000000012', 'edr_freight_app:fleet:manage', 'Manage fleet'),
|
perm('a1000001-0001-4000-8000-000000000012', 'edr_freight_app:fleet:manage', 'Manage fleet'),
|
||||||
perm('a1000001-0001-4000-8000-000000000013', 'edr_freight_app:admin', 'Freight administration'),
|
perm('a1000001-0001-4000-8000-000000000013', 'edr_freight_app:admin', 'Freight administration'),
|
||||||
|
perm('a1000001-0001-4000-8000-000000000024', 'edr_freight_app:bookings:create', 'Create booking'),
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -117,11 +118,209 @@ export const GAP_CONTROLLER_PERMISSIONS: FreightPermissionSeed[] = [
|
|||||||
perm('c1000001-0001-4000-8000-000000000001', 'edr_freight_app:allocation:manage', 'Allocate containers to vehicles'),
|
perm('c1000001-0001-4000-8000-000000000001', 'edr_freight_app:allocation:manage', 'Allocate containers to vehicles'),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Advanced backoffice resources — full CRUD + workflow-action keys.
|
||||||
|
* See docs/rbac/freight-backoffice-permissions.md. Additive only: the existing
|
||||||
|
* bookings/contracts/rule-engine/allocation keys above are unchanged.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// C. Customers
|
||||||
|
export const CUSTOMER_PERMISSIONS: FreightPermissionSeed[] = [
|
||||||
|
perm('d1a00001-0001-4000-8000-000000000001', 'edr_freight_app:customers:view', 'View customers'),
|
||||||
|
perm('d1a00001-0001-4000-8000-000000000002', 'edr_freight_app:customers:create', 'Create customer'),
|
||||||
|
perm('d1a00001-0001-4000-8000-000000000003', 'edr_freight_app:customers:update', 'Update customer'),
|
||||||
|
perm('d1a00001-0001-4000-8000-000000000004', 'edr_freight_app:customers:deactivate', 'Deactivate customer'),
|
||||||
|
perm('d1a00001-0001-4000-8000-000000000005', 'edr_freight_app:customers:verify', 'Verify customer (KYC/Fayda)'),
|
||||||
|
];
|
||||||
|
|
||||||
|
// D. Finance — payments + invoices
|
||||||
|
export const FINANCE_PERMISSIONS: FreightPermissionSeed[] = [
|
||||||
|
perm('d2a00001-0001-4000-8000-000000000001', 'edr_freight_app:payments:view', 'View payments'),
|
||||||
|
perm('d2a00001-0001-4000-8000-000000000002', 'edr_freight_app:payments:verify', 'Verify/settle payment'),
|
||||||
|
perm('d2a00001-0001-4000-8000-000000000003', 'edr_freight_app:payments:refund', 'Refund payment'),
|
||||||
|
perm('d2b00001-0001-4000-8000-000000000001', 'edr_freight_app:invoices:view', 'View invoices'),
|
||||||
|
perm('d2b00001-0001-4000-8000-000000000002', 'edr_freight_app:invoices:create', 'Generate invoice'),
|
||||||
|
perm('d2b00001-0001-4000-8000-000000000003', 'edr_freight_app:invoices:cancel', 'Cancel invoice'),
|
||||||
|
perm('d2b00001-0001-4000-8000-000000000004', 'edr_freight_app:invoices:export', 'Download invoice document'),
|
||||||
|
];
|
||||||
|
|
||||||
|
// E. First / last mile operations
|
||||||
|
export const MILE_PERMISSIONS: FreightPermissionSeed[] = [
|
||||||
|
perm('d3a00001-0001-4000-8000-000000000001', 'edr_freight_app:first_mile:view', 'View first-mile'),
|
||||||
|
perm('d3a00001-0001-4000-8000-000000000002', 'edr_freight_app:first_mile:accept', 'Accept first-mile request'),
|
||||||
|
perm('d3a00001-0001-4000-8000-000000000003', 'edr_freight_app:first_mile:create', 'Create first-mile'),
|
||||||
|
perm('d3a00001-0001-4000-8000-000000000004', 'edr_freight_app:first_mile:update', 'Update first-mile'),
|
||||||
|
perm('d3a00001-0001-4000-8000-000000000005', 'edr_freight_app:first_mile:delete', 'Delete first-mile'),
|
||||||
|
perm('d3a00001-0001-4000-8000-000000000006', 'edr_freight_app:first_mile:assign_vehicles', 'Assign first-mile vehicles'),
|
||||||
|
perm('d3a00001-0001-4000-8000-000000000007', 'edr_freight_app:first_mile:set_distances', 'Set first-mile distances'),
|
||||||
|
perm('d3a00001-0001-4000-8000-000000000008', 'edr_freight_app:first_mile:generate_invoice', 'Generate first-mile invoice'),
|
||||||
|
perm('d3b00001-0001-4000-8000-000000000001', 'edr_freight_app:last_mile:view', 'View last-mile'),
|
||||||
|
perm('d3b00001-0001-4000-8000-000000000002', 'edr_freight_app:last_mile:accept', 'Accept last-mile request'),
|
||||||
|
perm('d3b00001-0001-4000-8000-000000000003', 'edr_freight_app:last_mile:create', 'Create last-mile'),
|
||||||
|
perm('d3b00001-0001-4000-8000-000000000004', 'edr_freight_app:last_mile:update', 'Update last-mile'),
|
||||||
|
perm('d3b00001-0001-4000-8000-000000000005', 'edr_freight_app:last_mile:delete', 'Delete last-mile'),
|
||||||
|
perm('d3b00001-0001-4000-8000-000000000006', 'edr_freight_app:last_mile:assign_vehicles', 'Assign last-mile vehicles'),
|
||||||
|
perm('d3b00001-0001-4000-8000-000000000007', 'edr_freight_app:last_mile:set_distances', 'Set last-mile distances'),
|
||||||
|
perm('d3b00001-0001-4000-8000-000000000008', 'edr_freight_app:last_mile:generate_invoice', 'Generate last-mile invoice'),
|
||||||
|
];
|
||||||
|
|
||||||
|
// F. Fleet — rail assets (splits the flat fleet:view/manage)
|
||||||
|
export const FLEET_RAIL_PERMISSIONS: FreightPermissionSeed[] = [
|
||||||
|
perm('e1a00001-0001-4000-8000-000000000001', 'edr_freight_app:locomotives:view', 'View locomotives'),
|
||||||
|
perm('e1a00001-0001-4000-8000-000000000002', 'edr_freight_app:locomotives:create', 'Create locomotive'),
|
||||||
|
perm('e1a00001-0001-4000-8000-000000000003', 'edr_freight_app:locomotives:update', 'Update locomotive'),
|
||||||
|
perm('e1a00001-0001-4000-8000-000000000004', 'edr_freight_app:locomotives:delete', 'Delete locomotive'),
|
||||||
|
perm('e1b00001-0001-4000-8000-000000000001', 'edr_freight_app:wagons:view', 'View wagons'),
|
||||||
|
perm('e1b00001-0001-4000-8000-000000000002', 'edr_freight_app:wagons:create', 'Create wagon'),
|
||||||
|
perm('e1b00001-0001-4000-8000-000000000003', 'edr_freight_app:wagons:update', 'Update wagon'),
|
||||||
|
perm('e1b00001-0001-4000-8000-000000000004', 'edr_freight_app:wagons:delete', 'Delete wagon'),
|
||||||
|
perm('e1c00001-0001-4000-8000-000000000001', 'edr_freight_app:trains:view', 'View trains'),
|
||||||
|
perm('e1c00001-0001-4000-8000-000000000002', 'edr_freight_app:trains:create', 'Create train'),
|
||||||
|
perm('e1c00001-0001-4000-8000-000000000003', 'edr_freight_app:trains:update', 'Update train'),
|
||||||
|
perm('e1c00001-0001-4000-8000-000000000004', 'edr_freight_app:trains:delete', 'Delete train'),
|
||||||
|
perm('e1c00001-0001-4000-8000-000000000005', 'edr_freight_app:trains:assign_wagons', 'Assign wagons to train'),
|
||||||
|
perm('e1d00001-0001-4000-8000-000000000001', 'edr_freight_app:routes:view', 'View routes'),
|
||||||
|
perm('e1d00001-0001-4000-8000-000000000002', 'edr_freight_app:routes:create', 'Create route'),
|
||||||
|
perm('e1d00001-0001-4000-8000-000000000003', 'edr_freight_app:routes:update', 'Update route'),
|
||||||
|
perm('e1d00001-0001-4000-8000-000000000004', 'edr_freight_app:routes:delete', 'Delete route'),
|
||||||
|
perm('e1e00001-0001-4000-8000-000000000001', 'edr_freight_app:containers:view', 'View containers'),
|
||||||
|
perm('e1e00001-0001-4000-8000-000000000002', 'edr_freight_app:containers:create', 'Create container'),
|
||||||
|
perm('e1e00001-0001-4000-8000-000000000003', 'edr_freight_app:containers:update', 'Update container'),
|
||||||
|
perm('e1e00001-0001-4000-8000-000000000004', 'edr_freight_app:containers:delete', 'Delete container'),
|
||||||
|
perm('e1f00001-0001-4000-8000-000000000001', 'edr_freight_app:cargoes:view', 'View cargoes'),
|
||||||
|
perm('e1f00001-0001-4000-8000-000000000002', 'edr_freight_app:cargoes:create', 'Create cargo'),
|
||||||
|
perm('e1f00001-0001-4000-8000-000000000003', 'edr_freight_app:cargoes:update', 'Update cargo'),
|
||||||
|
perm('e1f00001-0001-4000-8000-000000000004', 'edr_freight_app:cargoes:delete', 'Delete cargo'),
|
||||||
|
];
|
||||||
|
|
||||||
|
// G. Fleet — road & telemetry
|
||||||
|
export const FLEET_ROAD_PERMISSIONS: FreightPermissionSeed[] = [
|
||||||
|
perm('e2a00001-0001-4000-8000-000000000001', 'edr_freight_app:vehicles:view', 'View vehicles'),
|
||||||
|
perm('e2a00001-0001-4000-8000-000000000002', 'edr_freight_app:vehicles:create', 'Create vehicle'),
|
||||||
|
perm('e2a00001-0001-4000-8000-000000000003', 'edr_freight_app:vehicles:update', 'Update vehicle'),
|
||||||
|
perm('e2a00001-0001-4000-8000-000000000004', 'edr_freight_app:vehicles:delete', 'Delete vehicle'),
|
||||||
|
perm('e2b00001-0001-4000-8000-000000000001', 'edr_freight_app:drivers:view', 'View drivers'),
|
||||||
|
perm('e2b00001-0001-4000-8000-000000000002', 'edr_freight_app:drivers:create', 'Create driver'),
|
||||||
|
perm('e2b00001-0001-4000-8000-000000000003', 'edr_freight_app:drivers:update', 'Update driver'),
|
||||||
|
perm('e2b00001-0001-4000-8000-000000000004', 'edr_freight_app:drivers:delete', 'Delete driver'),
|
||||||
|
perm('e2c00001-0001-4000-8000-000000000001', 'edr_freight_app:tracking:view', 'Track vehicles'),
|
||||||
|
perm('e2d00001-0001-4000-8000-000000000001', 'edr_freight_app:fuel:view', 'View fuel purchases'),
|
||||||
|
perm('e2d00001-0001-4000-8000-000000000002', 'edr_freight_app:fuel:create', 'Create fuel purchase'),
|
||||||
|
perm('e2d00001-0001-4000-8000-000000000003', 'edr_freight_app:fuel:update', 'Update fuel purchase'),
|
||||||
|
perm('e2d00001-0001-4000-8000-000000000004', 'edr_freight_app:fuel:delete', 'Delete fuel purchase'),
|
||||||
|
perm('e2d00001-0001-4000-8000-000000000005', 'edr_freight_app:fuel:approve', 'Approve fuel purchase'),
|
||||||
|
perm('e2e00001-0001-4000-8000-000000000001', 'edr_freight_app:maintenance:view', 'View maintenance'),
|
||||||
|
perm('e2e00001-0001-4000-8000-000000000002', 'edr_freight_app:maintenance:create', 'Create maintenance'),
|
||||||
|
perm('e2e00001-0001-4000-8000-000000000003', 'edr_freight_app:maintenance:update', 'Update maintenance'),
|
||||||
|
perm('e2e00001-0001-4000-8000-000000000004', 'edr_freight_app:maintenance:delete', 'Delete maintenance'),
|
||||||
|
perm('e2e00001-0001-4000-8000-000000000005', 'edr_freight_app:maintenance:complete', 'Complete maintenance'),
|
||||||
|
perm('e2f00001-0001-4000-8000-000000000001', 'edr_freight_app:fleet_reports:view', 'View fleet financial reports'),
|
||||||
|
perm('e2f00001-0001-4000-8000-000000000002', 'edr_freight_app:fleet_reports:export', 'Export fleet financial reports'),
|
||||||
|
perm('e2000001-0001-4000-8000-000000000001', 'edr_freight_app:fleet_dashboard:view', 'View fleet dashboard'),
|
||||||
|
];
|
||||||
|
|
||||||
|
// H. Warehouse management
|
||||||
|
export const WAREHOUSE_PERMISSIONS: FreightPermissionSeed[] = [
|
||||||
|
perm('f1000001-0001-4000-8000-000000000001', 'edr_freight_app:warehouse_dashboard:view', 'View warehouse dashboard'),
|
||||||
|
perm('f1a00001-0001-4000-8000-000000000001', 'edr_freight_app:warehouses:view', 'View warehouses'),
|
||||||
|
perm('f1a00001-0001-4000-8000-000000000002', 'edr_freight_app:warehouses:create', 'Create warehouse'),
|
||||||
|
perm('f1a00001-0001-4000-8000-000000000003', 'edr_freight_app:warehouses:update', 'Update warehouse'),
|
||||||
|
perm('f1a00001-0001-4000-8000-000000000004', 'edr_freight_app:warehouses:delete', 'Delete warehouse'),
|
||||||
|
perm('f1b00001-0001-4000-8000-000000000001', 'edr_freight_app:warehouse_yards:view', 'View warehouse yards'),
|
||||||
|
perm('f1b00001-0001-4000-8000-000000000002', 'edr_freight_app:warehouse_yards:create', 'Create warehouse yard'),
|
||||||
|
perm('f1b00001-0001-4000-8000-000000000003', 'edr_freight_app:warehouse_yards:update', 'Update warehouse yard'),
|
||||||
|
perm('f1b00001-0001-4000-8000-000000000004', 'edr_freight_app:warehouse_yards:delete', 'Delete warehouse yard'),
|
||||||
|
perm('f1c00001-0001-4000-8000-000000000001', 'edr_freight_app:warehouse_zones:view', 'View warehouse zones'),
|
||||||
|
perm('f1c00001-0001-4000-8000-000000000002', 'edr_freight_app:warehouse_zones:create', 'Create warehouse zone'),
|
||||||
|
perm('f1c00001-0001-4000-8000-000000000003', 'edr_freight_app:warehouse_zones:update', 'Update warehouse zone'),
|
||||||
|
perm('f1d00001-0001-4000-8000-000000000001', 'edr_freight_app:warehouse_allocation_rules:view', 'View allocation rules'),
|
||||||
|
perm('f1d00001-0001-4000-8000-000000000002', 'edr_freight_app:warehouse_allocation_rules:create', 'Create allocation rule'),
|
||||||
|
perm('f1d00001-0001-4000-8000-000000000003', 'edr_freight_app:warehouse_allocation_rules:update', 'Update allocation rule'),
|
||||||
|
perm('f1d00001-0001-4000-8000-000000000004', 'edr_freight_app:warehouse_allocation_rules:delete', 'Delete allocation rule'),
|
||||||
|
perm('f1e00001-0001-4000-8000-000000000001', 'edr_freight_app:warehouse_fee_rules:view', 'View fee rules'),
|
||||||
|
perm('f1e00001-0001-4000-8000-000000000002', 'edr_freight_app:warehouse_fee_rules:create', 'Create fee rule'),
|
||||||
|
perm('f1e00001-0001-4000-8000-000000000003', 'edr_freight_app:warehouse_fee_rules:update', 'Update fee rule'),
|
||||||
|
perm('f1e00001-0001-4000-8000-000000000004', 'edr_freight_app:warehouse_fee_rules:delete', 'Delete fee rule'),
|
||||||
|
perm('f1f00001-0001-4000-8000-000000000001', 'edr_freight_app:warehouse_inspection_reports:view', 'View inspection reports'),
|
||||||
|
perm('f1f00001-0001-4000-8000-000000000002', 'edr_freight_app:warehouse_inspection_reports:create', 'Create inspection report'),
|
||||||
|
perm('f1f00001-0001-4000-8000-000000000003', 'edr_freight_app:warehouse_inspection_reports:update', 'Update inspection report'),
|
||||||
|
];
|
||||||
|
|
||||||
|
// I. Port & terminal — inventory movement + interchange + fee invoices
|
||||||
|
export const PORT_TERMINAL_PERMISSIONS: FreightPermissionSeed[] = [
|
||||||
|
perm('f2a00001-0001-4000-8000-000000000001', 'edr_freight_app:warehouse_inventory:view', 'View terminal inventory'),
|
||||||
|
perm('f2a00001-0001-4000-8000-000000000002', 'edr_freight_app:warehouse_inventory:receive', 'Receive inventory'),
|
||||||
|
perm('f2a00001-0001-4000-8000-000000000003', 'edr_freight_app:warehouse_inventory:move', 'Move/store/reserve inventory'),
|
||||||
|
perm('f2a00001-0001-4000-8000-000000000004', 'edr_freight_app:warehouse_inventory:load', 'Load inventory'),
|
||||||
|
perm('f2a00001-0001-4000-8000-000000000005', 'edr_freight_app:warehouse_inventory:unload', 'Unload inventory'),
|
||||||
|
perm('f2a00001-0001-4000-8000-000000000006', 'edr_freight_app:warehouse_inventory:dispatch', 'Dispatch inventory'),
|
||||||
|
perm('f2a00001-0001-4000-8000-000000000007', 'edr_freight_app:warehouse_inventory:gate_pass', 'Gate-clearance inventory'),
|
||||||
|
perm('f2a00001-0001-4000-8000-000000000008', 'edr_freight_app:warehouse_inventory:release', 'Release inventory'),
|
||||||
|
perm('f2a00001-0001-4000-8000-000000000009', 'edr_freight_app:warehouse_inventory:deliver', 'Deliver inventory'),
|
||||||
|
perm('f2a00001-0001-4000-8000-00000000000a', 'edr_freight_app:warehouse_inventory:inspect', 'Inspect inventory'),
|
||||||
|
perm('f2b00001-0001-4000-8000-000000000001', 'edr_freight_app:interchange_documents:view', 'View interchange documents'),
|
||||||
|
perm('f2b00001-0001-4000-8000-000000000002', 'edr_freight_app:interchange_documents:generate', 'Generate interchange document'),
|
||||||
|
perm('f2b00001-0001-4000-8000-000000000003', 'edr_freight_app:interchange_documents:acknowledge', 'Acknowledge interchange document'),
|
||||||
|
perm('f2b00001-0001-4000-8000-000000000004', 'edr_freight_app:interchange_documents:dispute', 'Dispute interchange document'),
|
||||||
|
perm('f2b00001-0001-4000-8000-000000000005', 'edr_freight_app:interchange_documents:cancel', 'Cancel interchange document'),
|
||||||
|
perm('f2c00001-0001-4000-8000-000000000001', 'edr_freight_app:warehouse_fee_invoices:view', 'View warehouse fee invoices'),
|
||||||
|
perm('f2c00001-0001-4000-8000-000000000002', 'edr_freight_app:warehouse_fee_invoices:generate', 'Generate warehouse fee invoice'),
|
||||||
|
perm('f2c00001-0001-4000-8000-000000000003', 'edr_freight_app:warehouse_fee_invoices:cancel', 'Cancel warehouse fee invoice'),
|
||||||
|
perm('f2c00001-0001-4000-8000-000000000004', 'edr_freight_app:warehouse_fee_invoices:pay', 'Pay warehouse fee invoice'),
|
||||||
|
];
|
||||||
|
|
||||||
|
// E'. Train-scheduling finer actions (augment existing view/manage)
|
||||||
|
export const SCHEDULING_EXTRA_PERMISSIONS: FreightPermissionSeed[] = [
|
||||||
|
perm('a2a00001-0001-4000-8000-000000000001', 'edr_freight_app:train_scheduling:create', 'Create train schedule'),
|
||||||
|
perm('a2a00001-0001-4000-8000-000000000002', 'edr_freight_app:train_scheduling:update', 'Update train schedule'),
|
||||||
|
perm('a2a00001-0001-4000-8000-000000000003', 'edr_freight_app:train_scheduling:cancel', 'Cancel train schedule'),
|
||||||
|
perm('a2a00001-0001-4000-8000-000000000004', 'edr_freight_app:train_scheduling:reschedule', 'Reschedule train'),
|
||||||
|
perm('a2a00001-0001-4000-8000-000000000005', 'edr_freight_app:train_scheduling:rules_manage', 'Manage global scheduling rules'),
|
||||||
|
];
|
||||||
|
|
||||||
|
// L. Administration & settings (split from the coarse admin umbrella)
|
||||||
|
export const CONFIG_SETTINGS_PERMISSIONS: FreightPermissionSeed[] = [
|
||||||
|
perm('b3a00001-0001-4000-8000-000000000001', 'edr_freight_app:config:contract_validity:view', 'View contract validity periods'),
|
||||||
|
perm('b3a00001-0001-4000-8000-000000000002', 'edr_freight_app:config:contract_validity:manage', 'Manage contract validity periods'),
|
||||||
|
perm('b4a00001-0001-4000-8000-000000000001', 'edr_freight_app:settings:file_upload:view', 'View file-upload settings'),
|
||||||
|
perm('b4a00001-0001-4000-8000-000000000002', 'edr_freight_app:settings:file_upload:manage', 'Manage file-upload settings'),
|
||||||
|
perm('b4b00001-0001-4000-8000-000000000001', 'edr_freight_app:settings:dropdown:view', 'View dropdown settings'),
|
||||||
|
perm('b4b00001-0001-4000-8000-000000000002', 'edr_freight_app:settings:dropdown:manage', 'Manage dropdown settings'),
|
||||||
|
];
|
||||||
|
|
||||||
|
// M. Staff / IAM admin — NEW keys only. The employee_registration / role_assignment
|
||||||
|
// / hierarchy_* / position_types:view keys are seeded separately in edr-freight.seed.ts.
|
||||||
|
export const STAFF_IAM_PERMISSIONS: FreightPermissionSeed[] = [
|
||||||
|
perm('c2a00001-0001-4000-8000-000000000001', 'edr_freight_app:staff:roles:view', 'View roles'),
|
||||||
|
perm('c2a00001-0001-4000-8000-000000000002', 'edr_freight_app:staff:roles:create', 'Create role'),
|
||||||
|
perm('c2a00001-0001-4000-8000-000000000003', 'edr_freight_app:staff:roles:update', 'Update role'),
|
||||||
|
perm('c2a00001-0001-4000-8000-000000000004', 'edr_freight_app:staff:roles:delete', 'Delete role'),
|
||||||
|
perm('c2b00001-0001-4000-8000-000000000001', 'edr_freight_app:staff:permissions:view', 'View permission assignments'),
|
||||||
|
perm('c2b00001-0001-4000-8000-000000000002', 'edr_freight_app:staff:permissions:assign', 'Assign permissions'),
|
||||||
|
perm('c2c00001-0001-4000-8000-000000000001', 'edr_freight_app:position_types:create', 'Create position type'),
|
||||||
|
perm('c2c00001-0001-4000-8000-000000000002', 'edr_freight_app:position_types:update', 'Update position type'),
|
||||||
|
perm('c2c00001-0001-4000-8000-000000000003', 'edr_freight_app:position_types:delete', 'Delete position type'),
|
||||||
|
];
|
||||||
|
|
||||||
|
export const ADVANCED_BACKOFFICE_PERMISSIONS: FreightPermissionSeed[] = [
|
||||||
|
...CUSTOMER_PERMISSIONS,
|
||||||
|
...FINANCE_PERMISSIONS,
|
||||||
|
...MILE_PERMISSIONS,
|
||||||
|
...FLEET_RAIL_PERMISSIONS,
|
||||||
|
...FLEET_ROAD_PERMISSIONS,
|
||||||
|
...WAREHOUSE_PERMISSIONS,
|
||||||
|
...PORT_TERMINAL_PERMISSIONS,
|
||||||
|
...SCHEDULING_EXTRA_PERMISSIONS,
|
||||||
|
...CONFIG_SETTINGS_PERMISSIONS,
|
||||||
|
...STAFF_IAM_PERMISSIONS,
|
||||||
|
];
|
||||||
|
|
||||||
export const BOOKING_RULE_ENGINE_PERMISSIONS = [
|
export const BOOKING_RULE_ENGINE_PERMISSIONS = [
|
||||||
...BOOKING_PERMISSIONS,
|
...BOOKING_PERMISSIONS,
|
||||||
...CONTRACT_PERMISSIONS,
|
...CONTRACT_PERMISSIONS,
|
||||||
...RULE_ENGINE_PERMISSIONS,
|
...RULE_ENGINE_PERMISSIONS,
|
||||||
...GAP_CONTROLLER_PERMISSIONS,
|
...GAP_CONTROLLER_PERMISSIONS,
|
||||||
|
...ADVANCED_BACKOFFICE_PERMISSIONS,
|
||||||
];
|
];
|
||||||
|
|
||||||
export const BOOKING_RULE_ENGINE_PERMISSION_KEYS = BOOKING_RULE_ENGINE_PERMISSIONS.map(
|
export const BOOKING_RULE_ENGINE_PERMISSION_KEYS = BOOKING_RULE_ENGINE_PERMISSIONS.map(
|
||||||
@@ -131,6 +330,7 @@ export const BOOKING_RULE_ENGINE_PERMISSION_KEYS = BOOKING_RULE_ENGINE_PERMISSIO
|
|||||||
export const FREIGHT_PERMS = {
|
export const FREIGHT_PERMS = {
|
||||||
bookings: {
|
bookings: {
|
||||||
view: 'edr_freight_app:bookings:view',
|
view: 'edr_freight_app:bookings:view',
|
||||||
|
create: 'edr_freight_app:bookings:create',
|
||||||
clearanceView: 'edr_freight_app:bookings:clearance_view',
|
clearanceView: 'edr_freight_app:bookings:clearance_view',
|
||||||
staffAccept: 'edr_freight_app:bookings:staff_accept',
|
staffAccept: 'edr_freight_app:bookings:staff_accept',
|
||||||
requestChanges: 'edr_freight_app:bookings:request_changes',
|
requestChanges: 'edr_freight_app:bookings:request_changes',
|
||||||
@@ -168,6 +368,11 @@ export const FREIGHT_PERMS = {
|
|||||||
trainScheduling: {
|
trainScheduling: {
|
||||||
view: 'edr_freight_app:train_scheduling:view',
|
view: 'edr_freight_app:train_scheduling:view',
|
||||||
manage: 'edr_freight_app:train_scheduling:manage',
|
manage: 'edr_freight_app:train_scheduling:manage',
|
||||||
|
create: 'edr_freight_app:train_scheduling:create',
|
||||||
|
update: 'edr_freight_app:train_scheduling:update',
|
||||||
|
cancel: 'edr_freight_app:train_scheduling:cancel',
|
||||||
|
reschedule: 'edr_freight_app:train_scheduling:reschedule',
|
||||||
|
rulesManage: 'edr_freight_app:train_scheduling:rules_manage',
|
||||||
},
|
},
|
||||||
fleet: {
|
fleet: {
|
||||||
view: 'edr_freight_app:fleet:view',
|
view: 'edr_freight_app:fleet:view',
|
||||||
@@ -183,6 +388,244 @@ export const FREIGHT_PERMS = {
|
|||||||
allocation: {
|
allocation: {
|
||||||
manage: 'edr_freight_app:allocation:manage',
|
manage: 'edr_freight_app:allocation:manage',
|
||||||
},
|
},
|
||||||
|
customers: {
|
||||||
|
view: 'edr_freight_app:customers:view',
|
||||||
|
create: 'edr_freight_app:customers:create',
|
||||||
|
update: 'edr_freight_app:customers:update',
|
||||||
|
deactivate: 'edr_freight_app:customers:deactivate',
|
||||||
|
verify: 'edr_freight_app:customers:verify',
|
||||||
|
},
|
||||||
|
payments: {
|
||||||
|
view: 'edr_freight_app:payments:view',
|
||||||
|
verify: 'edr_freight_app:payments:verify',
|
||||||
|
refund: 'edr_freight_app:payments:refund',
|
||||||
|
},
|
||||||
|
invoices: {
|
||||||
|
view: 'edr_freight_app:invoices:view',
|
||||||
|
create: 'edr_freight_app:invoices:create',
|
||||||
|
cancel: 'edr_freight_app:invoices:cancel',
|
||||||
|
export: 'edr_freight_app:invoices:export',
|
||||||
|
},
|
||||||
|
firstMile: {
|
||||||
|
view: 'edr_freight_app:first_mile:view',
|
||||||
|
accept: 'edr_freight_app:first_mile:accept',
|
||||||
|
create: 'edr_freight_app:first_mile:create',
|
||||||
|
update: 'edr_freight_app:first_mile:update',
|
||||||
|
delete: 'edr_freight_app:first_mile:delete',
|
||||||
|
assignVehicles: 'edr_freight_app:first_mile:assign_vehicles',
|
||||||
|
setDistances: 'edr_freight_app:first_mile:set_distances',
|
||||||
|
generateInvoice: 'edr_freight_app:first_mile:generate_invoice',
|
||||||
|
},
|
||||||
|
lastMile: {
|
||||||
|
view: 'edr_freight_app:last_mile:view',
|
||||||
|
accept: 'edr_freight_app:last_mile:accept',
|
||||||
|
create: 'edr_freight_app:last_mile:create',
|
||||||
|
update: 'edr_freight_app:last_mile:update',
|
||||||
|
delete: 'edr_freight_app:last_mile:delete',
|
||||||
|
assignVehicles: 'edr_freight_app:last_mile:assign_vehicles',
|
||||||
|
setDistances: 'edr_freight_app:last_mile:set_distances',
|
||||||
|
generateInvoice: 'edr_freight_app:last_mile:generate_invoice',
|
||||||
|
},
|
||||||
|
locomotives: {
|
||||||
|
view: 'edr_freight_app:locomotives:view',
|
||||||
|
create: 'edr_freight_app:locomotives:create',
|
||||||
|
update: 'edr_freight_app:locomotives:update',
|
||||||
|
delete: 'edr_freight_app:locomotives:delete',
|
||||||
|
},
|
||||||
|
wagons: {
|
||||||
|
view: 'edr_freight_app:wagons:view',
|
||||||
|
create: 'edr_freight_app:wagons:create',
|
||||||
|
update: 'edr_freight_app:wagons:update',
|
||||||
|
delete: 'edr_freight_app:wagons:delete',
|
||||||
|
},
|
||||||
|
trains: {
|
||||||
|
view: 'edr_freight_app:trains:view',
|
||||||
|
create: 'edr_freight_app:trains:create',
|
||||||
|
update: 'edr_freight_app:trains:update',
|
||||||
|
delete: 'edr_freight_app:trains:delete',
|
||||||
|
assignWagons: 'edr_freight_app:trains:assign_wagons',
|
||||||
|
},
|
||||||
|
routes: {
|
||||||
|
view: 'edr_freight_app:routes:view',
|
||||||
|
create: 'edr_freight_app:routes:create',
|
||||||
|
update: 'edr_freight_app:routes:update',
|
||||||
|
delete: 'edr_freight_app:routes:delete',
|
||||||
|
},
|
||||||
|
containers: {
|
||||||
|
view: 'edr_freight_app:containers:view',
|
||||||
|
create: 'edr_freight_app:containers:create',
|
||||||
|
update: 'edr_freight_app:containers:update',
|
||||||
|
delete: 'edr_freight_app:containers:delete',
|
||||||
|
},
|
||||||
|
cargoes: {
|
||||||
|
view: 'edr_freight_app:cargoes:view',
|
||||||
|
create: 'edr_freight_app:cargoes:create',
|
||||||
|
update: 'edr_freight_app:cargoes:update',
|
||||||
|
delete: 'edr_freight_app:cargoes:delete',
|
||||||
|
},
|
||||||
|
vehicles: {
|
||||||
|
view: 'edr_freight_app:vehicles:view',
|
||||||
|
create: 'edr_freight_app:vehicles:create',
|
||||||
|
update: 'edr_freight_app:vehicles:update',
|
||||||
|
delete: 'edr_freight_app:vehicles:delete',
|
||||||
|
},
|
||||||
|
drivers: {
|
||||||
|
view: 'edr_freight_app:drivers:view',
|
||||||
|
create: 'edr_freight_app:drivers:create',
|
||||||
|
update: 'edr_freight_app:drivers:update',
|
||||||
|
delete: 'edr_freight_app:drivers:delete',
|
||||||
|
},
|
||||||
|
tracking: {
|
||||||
|
view: 'edr_freight_app:tracking:view',
|
||||||
|
},
|
||||||
|
fuel: {
|
||||||
|
view: 'edr_freight_app:fuel:view',
|
||||||
|
create: 'edr_freight_app:fuel:create',
|
||||||
|
update: 'edr_freight_app:fuel:update',
|
||||||
|
delete: 'edr_freight_app:fuel:delete',
|
||||||
|
approve: 'edr_freight_app:fuel:approve',
|
||||||
|
},
|
||||||
|
maintenance: {
|
||||||
|
view: 'edr_freight_app:maintenance:view',
|
||||||
|
create: 'edr_freight_app:maintenance:create',
|
||||||
|
update: 'edr_freight_app:maintenance:update',
|
||||||
|
delete: 'edr_freight_app:maintenance:delete',
|
||||||
|
complete: 'edr_freight_app:maintenance:complete',
|
||||||
|
},
|
||||||
|
fleetReports: {
|
||||||
|
view: 'edr_freight_app:fleet_reports:view',
|
||||||
|
export: 'edr_freight_app:fleet_reports:export',
|
||||||
|
},
|
||||||
|
fleetDashboard: {
|
||||||
|
view: 'edr_freight_app:fleet_dashboard:view',
|
||||||
|
},
|
||||||
|
warehouseDashboard: {
|
||||||
|
view: 'edr_freight_app:warehouse_dashboard:view',
|
||||||
|
},
|
||||||
|
warehouses: {
|
||||||
|
view: 'edr_freight_app:warehouses:view',
|
||||||
|
create: 'edr_freight_app:warehouses:create',
|
||||||
|
update: 'edr_freight_app:warehouses:update',
|
||||||
|
delete: 'edr_freight_app:warehouses:delete',
|
||||||
|
},
|
||||||
|
warehouseYards: {
|
||||||
|
view: 'edr_freight_app:warehouse_yards:view',
|
||||||
|
create: 'edr_freight_app:warehouse_yards:create',
|
||||||
|
update: 'edr_freight_app:warehouse_yards:update',
|
||||||
|
delete: 'edr_freight_app:warehouse_yards:delete',
|
||||||
|
},
|
||||||
|
warehouseZones: {
|
||||||
|
view: 'edr_freight_app:warehouse_zones:view',
|
||||||
|
create: 'edr_freight_app:warehouse_zones:create',
|
||||||
|
update: 'edr_freight_app:warehouse_zones:update',
|
||||||
|
},
|
||||||
|
warehouseAllocationRules: {
|
||||||
|
view: 'edr_freight_app:warehouse_allocation_rules:view',
|
||||||
|
create: 'edr_freight_app:warehouse_allocation_rules:create',
|
||||||
|
update: 'edr_freight_app:warehouse_allocation_rules:update',
|
||||||
|
delete: 'edr_freight_app:warehouse_allocation_rules:delete',
|
||||||
|
},
|
||||||
|
warehouseFeeRules: {
|
||||||
|
view: 'edr_freight_app:warehouse_fee_rules:view',
|
||||||
|
create: 'edr_freight_app:warehouse_fee_rules:create',
|
||||||
|
update: 'edr_freight_app:warehouse_fee_rules:update',
|
||||||
|
delete: 'edr_freight_app:warehouse_fee_rules:delete',
|
||||||
|
},
|
||||||
|
warehouseInspectionReports: {
|
||||||
|
view: 'edr_freight_app:warehouse_inspection_reports:view',
|
||||||
|
create: 'edr_freight_app:warehouse_inspection_reports:create',
|
||||||
|
update: 'edr_freight_app:warehouse_inspection_reports:update',
|
||||||
|
},
|
||||||
|
warehouseInventory: {
|
||||||
|
view: 'edr_freight_app:warehouse_inventory:view',
|
||||||
|
receive: 'edr_freight_app:warehouse_inventory:receive',
|
||||||
|
move: 'edr_freight_app:warehouse_inventory:move',
|
||||||
|
load: 'edr_freight_app:warehouse_inventory:load',
|
||||||
|
unload: 'edr_freight_app:warehouse_inventory:unload',
|
||||||
|
dispatch: 'edr_freight_app:warehouse_inventory:dispatch',
|
||||||
|
gatePass: 'edr_freight_app:warehouse_inventory:gate_pass',
|
||||||
|
release: 'edr_freight_app:warehouse_inventory:release',
|
||||||
|
deliver: 'edr_freight_app:warehouse_inventory:deliver',
|
||||||
|
inspect: 'edr_freight_app:warehouse_inventory:inspect',
|
||||||
|
},
|
||||||
|
interchangeDocuments: {
|
||||||
|
view: 'edr_freight_app:interchange_documents:view',
|
||||||
|
generate: 'edr_freight_app:interchange_documents:generate',
|
||||||
|
acknowledge: 'edr_freight_app:interchange_documents:acknowledge',
|
||||||
|
dispute: 'edr_freight_app:interchange_documents:dispute',
|
||||||
|
cancel: 'edr_freight_app:interchange_documents:cancel',
|
||||||
|
},
|
||||||
|
warehouseFeeInvoices: {
|
||||||
|
view: 'edr_freight_app:warehouse_fee_invoices:view',
|
||||||
|
generate: 'edr_freight_app:warehouse_fee_invoices:generate',
|
||||||
|
cancel: 'edr_freight_app:warehouse_fee_invoices:cancel',
|
||||||
|
pay: 'edr_freight_app:warehouse_fee_invoices:pay',
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
contractValidity: {
|
||||||
|
view: 'edr_freight_app:config:contract_validity:view',
|
||||||
|
manage: 'edr_freight_app:config:contract_validity:manage',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
fileUpload: {
|
||||||
|
view: 'edr_freight_app:settings:file_upload:view',
|
||||||
|
manage: 'edr_freight_app:settings:file_upload:manage',
|
||||||
|
},
|
||||||
|
dropdown: {
|
||||||
|
view: 'edr_freight_app:settings:dropdown:view',
|
||||||
|
manage: 'edr_freight_app:settings:dropdown:manage',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
staff: {
|
||||||
|
roles: {
|
||||||
|
view: 'edr_freight_app:staff:roles:view',
|
||||||
|
create: 'edr_freight_app:staff:roles:create',
|
||||||
|
update: 'edr_freight_app:staff:roles:update',
|
||||||
|
delete: 'edr_freight_app:staff:roles:delete',
|
||||||
|
},
|
||||||
|
permissions: {
|
||||||
|
view: 'edr_freight_app:staff:permissions:view',
|
||||||
|
assign: 'edr_freight_app:staff:permissions:assign',
|
||||||
|
},
|
||||||
|
// Seeded in edr-freight.seed.ts (EDR_FREIGHT_PERMISSIONS) — surfaced here for gating.
|
||||||
|
employeeRegistration: {
|
||||||
|
view: 'edr_freight_app:employee_registration:view',
|
||||||
|
create: 'edr_freight_app:employee_registration:create',
|
||||||
|
update: 'edr_freight_app:employee_registration:update',
|
||||||
|
activate: 'edr_freight_app:employee_registration:activate',
|
||||||
|
deactivate: 'edr_freight_app:employee_registration:deactivate',
|
||||||
|
},
|
||||||
|
roleAssignment: {
|
||||||
|
view: 'edr_freight_app:role_assignment:view',
|
||||||
|
assign: 'edr_freight_app:role_assignment:assign',
|
||||||
|
replace: 'edr_freight_app:role_assignment:replace',
|
||||||
|
},
|
||||||
|
hierarchyUnits: {
|
||||||
|
view: 'edr_freight_app:hierarchy_units:view',
|
||||||
|
create: 'edr_freight_app:hierarchy_units:create',
|
||||||
|
update: 'edr_freight_app:hierarchy_units:update',
|
||||||
|
delete: 'edr_freight_app:hierarchy_units:delete',
|
||||||
|
},
|
||||||
|
hierarchyPositions: {
|
||||||
|
view: 'edr_freight_app:hierarchy_positions:view',
|
||||||
|
create: 'edr_freight_app:hierarchy_positions:create',
|
||||||
|
update: 'edr_freight_app:hierarchy_positions:update',
|
||||||
|
delete: 'edr_freight_app:hierarchy_positions:delete',
|
||||||
|
changeParent: 'edr_freight_app:hierarchy_positions:change_parent',
|
||||||
|
},
|
||||||
|
hierarchyEmployeeAssignment: {
|
||||||
|
view: 'edr_freight_app:hierarchy_employee_assignment:view',
|
||||||
|
invite: 'edr_freight_app:hierarchy_employee_assignment:invite',
|
||||||
|
assign: 'edr_freight_app:hierarchy_employee_assignment:assign',
|
||||||
|
},
|
||||||
|
positionTypes: {
|
||||||
|
view: 'edr_freight_app:position_types:view',
|
||||||
|
create: 'edr_freight_app:position_types:create',
|
||||||
|
update: 'edr_freight_app:position_types:update',
|
||||||
|
delete: 'edr_freight_app:position_types:delete',
|
||||||
|
},
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const allRuleEngineViewKeys = () =>
|
const allRuleEngineViewKeys = () =>
|
||||||
@@ -326,12 +769,11 @@ export const POSITION_PERMISSION_PRESETS = {
|
|||||||
]),
|
]),
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
/** Derive the module bucket from the resource segment of a permission key. */
|
||||||
|
const moduleOf = (key: string): string => key.split(':')[1] ?? 'other';
|
||||||
|
|
||||||
export const PERMISSIONS_CATALOG = BOOKING_RULE_ENGINE_PERMISSIONS.map((p) => ({
|
export const PERMISSIONS_CATALOG = BOOKING_RULE_ENGINE_PERMISSIONS.map((p) => ({
|
||||||
key: p.key,
|
key: p.key,
|
||||||
label: p.name.en,
|
label: p.name.en,
|
||||||
module: p.key.includes(':bookings:')
|
module: moduleOf(p.key),
|
||||||
? 'bookings'
|
|
||||||
: p.key.includes(':contracts:')
|
|
||||||
? 'contracts'
|
|
||||||
: 'rule_engine',
|
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { RuleEngineResourceSlug } from "@/types/rule-engine";
|
|||||||
export const FREIGHT_PERMS = {
|
export const FREIGHT_PERMS = {
|
||||||
bookings: {
|
bookings: {
|
||||||
view: "edr_freight_app:bookings:view",
|
view: "edr_freight_app:bookings:view",
|
||||||
|
create: "edr_freight_app:bookings:create",
|
||||||
clearanceView: "edr_freight_app:bookings:clearance_view",
|
clearanceView: "edr_freight_app:bookings:clearance_view",
|
||||||
staffAccept: "edr_freight_app:bookings:staff_accept",
|
staffAccept: "edr_freight_app:bookings:staff_accept",
|
||||||
requestChanges: "edr_freight_app:bookings:request_changes",
|
requestChanges: "edr_freight_app:bookings:request_changes",
|
||||||
@@ -41,6 +42,11 @@ export const FREIGHT_PERMS = {
|
|||||||
trainScheduling: {
|
trainScheduling: {
|
||||||
view: "edr_freight_app:train_scheduling:view",
|
view: "edr_freight_app:train_scheduling:view",
|
||||||
manage: "edr_freight_app:train_scheduling:manage",
|
manage: "edr_freight_app:train_scheduling:manage",
|
||||||
|
create: "edr_freight_app:train_scheduling:create",
|
||||||
|
update: "edr_freight_app:train_scheduling:update",
|
||||||
|
cancel: "edr_freight_app:train_scheduling:cancel",
|
||||||
|
reschedule: "edr_freight_app:train_scheduling:reschedule",
|
||||||
|
rulesManage: "edr_freight_app:train_scheduling:rules_manage",
|
||||||
},
|
},
|
||||||
fleet: {
|
fleet: {
|
||||||
view: "edr_freight_app:fleet:view",
|
view: "edr_freight_app:fleet:view",
|
||||||
@@ -50,6 +56,243 @@ export const FREIGHT_PERMS = {
|
|||||||
allocation: {
|
allocation: {
|
||||||
manage: "edr_freight_app:allocation:manage",
|
manage: "edr_freight_app:allocation:manage",
|
||||||
},
|
},
|
||||||
|
customers: {
|
||||||
|
view: "edr_freight_app:customers:view",
|
||||||
|
create: "edr_freight_app:customers:create",
|
||||||
|
update: "edr_freight_app:customers:update",
|
||||||
|
deactivate: "edr_freight_app:customers:deactivate",
|
||||||
|
verify: "edr_freight_app:customers:verify",
|
||||||
|
},
|
||||||
|
payments: {
|
||||||
|
view: "edr_freight_app:payments:view",
|
||||||
|
verify: "edr_freight_app:payments:verify",
|
||||||
|
refund: "edr_freight_app:payments:refund",
|
||||||
|
},
|
||||||
|
invoices: {
|
||||||
|
view: "edr_freight_app:invoices:view",
|
||||||
|
create: "edr_freight_app:invoices:create",
|
||||||
|
cancel: "edr_freight_app:invoices:cancel",
|
||||||
|
export: "edr_freight_app:invoices:export",
|
||||||
|
},
|
||||||
|
firstMile: {
|
||||||
|
view: "edr_freight_app:first_mile:view",
|
||||||
|
accept: "edr_freight_app:first_mile:accept",
|
||||||
|
create: "edr_freight_app:first_mile:create",
|
||||||
|
update: "edr_freight_app:first_mile:update",
|
||||||
|
delete: "edr_freight_app:first_mile:delete",
|
||||||
|
assignVehicles: "edr_freight_app:first_mile:assign_vehicles",
|
||||||
|
setDistances: "edr_freight_app:first_mile:set_distances",
|
||||||
|
generateInvoice: "edr_freight_app:first_mile:generate_invoice",
|
||||||
|
},
|
||||||
|
lastMile: {
|
||||||
|
view: "edr_freight_app:last_mile:view",
|
||||||
|
accept: "edr_freight_app:last_mile:accept",
|
||||||
|
create: "edr_freight_app:last_mile:create",
|
||||||
|
update: "edr_freight_app:last_mile:update",
|
||||||
|
delete: "edr_freight_app:last_mile:delete",
|
||||||
|
assignVehicles: "edr_freight_app:last_mile:assign_vehicles",
|
||||||
|
setDistances: "edr_freight_app:last_mile:set_distances",
|
||||||
|
generateInvoice: "edr_freight_app:last_mile:generate_invoice",
|
||||||
|
},
|
||||||
|
locomotives: {
|
||||||
|
view: "edr_freight_app:locomotives:view",
|
||||||
|
create: "edr_freight_app:locomotives:create",
|
||||||
|
update: "edr_freight_app:locomotives:update",
|
||||||
|
delete: "edr_freight_app:locomotives:delete",
|
||||||
|
},
|
||||||
|
wagons: {
|
||||||
|
view: "edr_freight_app:wagons:view",
|
||||||
|
create: "edr_freight_app:wagons:create",
|
||||||
|
update: "edr_freight_app:wagons:update",
|
||||||
|
delete: "edr_freight_app:wagons:delete",
|
||||||
|
},
|
||||||
|
trains: {
|
||||||
|
view: "edr_freight_app:trains:view",
|
||||||
|
create: "edr_freight_app:trains:create",
|
||||||
|
update: "edr_freight_app:trains:update",
|
||||||
|
delete: "edr_freight_app:trains:delete",
|
||||||
|
assignWagons: "edr_freight_app:trains:assign_wagons",
|
||||||
|
},
|
||||||
|
routes: {
|
||||||
|
view: "edr_freight_app:routes:view",
|
||||||
|
create: "edr_freight_app:routes:create",
|
||||||
|
update: "edr_freight_app:routes:update",
|
||||||
|
delete: "edr_freight_app:routes:delete",
|
||||||
|
},
|
||||||
|
containers: {
|
||||||
|
view: "edr_freight_app:containers:view",
|
||||||
|
create: "edr_freight_app:containers:create",
|
||||||
|
update: "edr_freight_app:containers:update",
|
||||||
|
delete: "edr_freight_app:containers:delete",
|
||||||
|
},
|
||||||
|
cargoes: {
|
||||||
|
view: "edr_freight_app:cargoes:view",
|
||||||
|
create: "edr_freight_app:cargoes:create",
|
||||||
|
update: "edr_freight_app:cargoes:update",
|
||||||
|
delete: "edr_freight_app:cargoes:delete",
|
||||||
|
},
|
||||||
|
vehicles: {
|
||||||
|
view: "edr_freight_app:vehicles:view",
|
||||||
|
create: "edr_freight_app:vehicles:create",
|
||||||
|
update: "edr_freight_app:vehicles:update",
|
||||||
|
delete: "edr_freight_app:vehicles:delete",
|
||||||
|
},
|
||||||
|
drivers: {
|
||||||
|
view: "edr_freight_app:drivers:view",
|
||||||
|
create: "edr_freight_app:drivers:create",
|
||||||
|
update: "edr_freight_app:drivers:update",
|
||||||
|
delete: "edr_freight_app:drivers:delete",
|
||||||
|
},
|
||||||
|
tracking: {
|
||||||
|
view: "edr_freight_app:tracking:view",
|
||||||
|
},
|
||||||
|
fuel: {
|
||||||
|
view: "edr_freight_app:fuel:view",
|
||||||
|
create: "edr_freight_app:fuel:create",
|
||||||
|
update: "edr_freight_app:fuel:update",
|
||||||
|
delete: "edr_freight_app:fuel:delete",
|
||||||
|
approve: "edr_freight_app:fuel:approve",
|
||||||
|
},
|
||||||
|
maintenance: {
|
||||||
|
view: "edr_freight_app:maintenance:view",
|
||||||
|
create: "edr_freight_app:maintenance:create",
|
||||||
|
update: "edr_freight_app:maintenance:update",
|
||||||
|
delete: "edr_freight_app:maintenance:delete",
|
||||||
|
complete: "edr_freight_app:maintenance:complete",
|
||||||
|
},
|
||||||
|
fleetReports: {
|
||||||
|
view: "edr_freight_app:fleet_reports:view",
|
||||||
|
export: "edr_freight_app:fleet_reports:export",
|
||||||
|
},
|
||||||
|
fleetDashboard: {
|
||||||
|
view: "edr_freight_app:fleet_dashboard:view",
|
||||||
|
},
|
||||||
|
warehouseDashboard: {
|
||||||
|
view: "edr_freight_app:warehouse_dashboard:view",
|
||||||
|
},
|
||||||
|
warehouses: {
|
||||||
|
view: "edr_freight_app:warehouses:view",
|
||||||
|
create: "edr_freight_app:warehouses:create",
|
||||||
|
update: "edr_freight_app:warehouses:update",
|
||||||
|
delete: "edr_freight_app:warehouses:delete",
|
||||||
|
},
|
||||||
|
warehouseYards: {
|
||||||
|
view: "edr_freight_app:warehouse_yards:view",
|
||||||
|
create: "edr_freight_app:warehouse_yards:create",
|
||||||
|
update: "edr_freight_app:warehouse_yards:update",
|
||||||
|
delete: "edr_freight_app:warehouse_yards:delete",
|
||||||
|
},
|
||||||
|
warehouseZones: {
|
||||||
|
view: "edr_freight_app:warehouse_zones:view",
|
||||||
|
create: "edr_freight_app:warehouse_zones:create",
|
||||||
|
update: "edr_freight_app:warehouse_zones:update",
|
||||||
|
},
|
||||||
|
warehouseAllocationRules: {
|
||||||
|
view: "edr_freight_app:warehouse_allocation_rules:view",
|
||||||
|
create: "edr_freight_app:warehouse_allocation_rules:create",
|
||||||
|
update: "edr_freight_app:warehouse_allocation_rules:update",
|
||||||
|
delete: "edr_freight_app:warehouse_allocation_rules:delete",
|
||||||
|
},
|
||||||
|
warehouseFeeRules: {
|
||||||
|
view: "edr_freight_app:warehouse_fee_rules:view",
|
||||||
|
create: "edr_freight_app:warehouse_fee_rules:create",
|
||||||
|
update: "edr_freight_app:warehouse_fee_rules:update",
|
||||||
|
delete: "edr_freight_app:warehouse_fee_rules:delete",
|
||||||
|
},
|
||||||
|
warehouseInspectionReports: {
|
||||||
|
view: "edr_freight_app:warehouse_inspection_reports:view",
|
||||||
|
create: "edr_freight_app:warehouse_inspection_reports:create",
|
||||||
|
update: "edr_freight_app:warehouse_inspection_reports:update",
|
||||||
|
},
|
||||||
|
warehouseInventory: {
|
||||||
|
view: "edr_freight_app:warehouse_inventory:view",
|
||||||
|
receive: "edr_freight_app:warehouse_inventory:receive",
|
||||||
|
move: "edr_freight_app:warehouse_inventory:move",
|
||||||
|
load: "edr_freight_app:warehouse_inventory:load",
|
||||||
|
unload: "edr_freight_app:warehouse_inventory:unload",
|
||||||
|
dispatch: "edr_freight_app:warehouse_inventory:dispatch",
|
||||||
|
gatePass: "edr_freight_app:warehouse_inventory:gate_pass",
|
||||||
|
release: "edr_freight_app:warehouse_inventory:release",
|
||||||
|
deliver: "edr_freight_app:warehouse_inventory:deliver",
|
||||||
|
inspect: "edr_freight_app:warehouse_inventory:inspect",
|
||||||
|
},
|
||||||
|
interchangeDocuments: {
|
||||||
|
view: "edr_freight_app:interchange_documents:view",
|
||||||
|
generate: "edr_freight_app:interchange_documents:generate",
|
||||||
|
acknowledge: "edr_freight_app:interchange_documents:acknowledge",
|
||||||
|
dispute: "edr_freight_app:interchange_documents:dispute",
|
||||||
|
cancel: "edr_freight_app:interchange_documents:cancel",
|
||||||
|
},
|
||||||
|
warehouseFeeInvoices: {
|
||||||
|
view: "edr_freight_app:warehouse_fee_invoices:view",
|
||||||
|
generate: "edr_freight_app:warehouse_fee_invoices:generate",
|
||||||
|
cancel: "edr_freight_app:warehouse_fee_invoices:cancel",
|
||||||
|
pay: "edr_freight_app:warehouse_fee_invoices:pay",
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
contractValidity: {
|
||||||
|
view: "edr_freight_app:config:contract_validity:view",
|
||||||
|
manage: "edr_freight_app:config:contract_validity:manage",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
settings: {
|
||||||
|
fileUpload: {
|
||||||
|
view: "edr_freight_app:settings:file_upload:view",
|
||||||
|
manage: "edr_freight_app:settings:file_upload:manage",
|
||||||
|
},
|
||||||
|
dropdown: {
|
||||||
|
view: "edr_freight_app:settings:dropdown:view",
|
||||||
|
manage: "edr_freight_app:settings:dropdown:manage",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
staff: {
|
||||||
|
roles: {
|
||||||
|
view: "edr_freight_app:staff:roles:view",
|
||||||
|
create: "edr_freight_app:staff:roles:create",
|
||||||
|
update: "edr_freight_app:staff:roles:update",
|
||||||
|
delete: "edr_freight_app:staff:roles:delete",
|
||||||
|
},
|
||||||
|
permissions: {
|
||||||
|
view: "edr_freight_app:staff:permissions:view",
|
||||||
|
assign: "edr_freight_app:staff:permissions:assign",
|
||||||
|
},
|
||||||
|
employeeRegistration: {
|
||||||
|
view: "edr_freight_app:employee_registration:view",
|
||||||
|
create: "edr_freight_app:employee_registration:create",
|
||||||
|
update: "edr_freight_app:employee_registration:update",
|
||||||
|
activate: "edr_freight_app:employee_registration:activate",
|
||||||
|
deactivate: "edr_freight_app:employee_registration:deactivate",
|
||||||
|
},
|
||||||
|
roleAssignment: {
|
||||||
|
view: "edr_freight_app:role_assignment:view",
|
||||||
|
assign: "edr_freight_app:role_assignment:assign",
|
||||||
|
replace: "edr_freight_app:role_assignment:replace",
|
||||||
|
},
|
||||||
|
hierarchyUnits: {
|
||||||
|
view: "edr_freight_app:hierarchy_units:view",
|
||||||
|
create: "edr_freight_app:hierarchy_units:create",
|
||||||
|
update: "edr_freight_app:hierarchy_units:update",
|
||||||
|
delete: "edr_freight_app:hierarchy_units:delete",
|
||||||
|
},
|
||||||
|
hierarchyPositions: {
|
||||||
|
view: "edr_freight_app:hierarchy_positions:view",
|
||||||
|
create: "edr_freight_app:hierarchy_positions:create",
|
||||||
|
update: "edr_freight_app:hierarchy_positions:update",
|
||||||
|
delete: "edr_freight_app:hierarchy_positions:delete",
|
||||||
|
changeParent: "edr_freight_app:hierarchy_positions:change_parent",
|
||||||
|
},
|
||||||
|
hierarchyEmployeeAssignment: {
|
||||||
|
view: "edr_freight_app:hierarchy_employee_assignment:view",
|
||||||
|
invite: "edr_freight_app:hierarchy_employee_assignment:invite",
|
||||||
|
assign: "edr_freight_app:hierarchy_employee_assignment:assign",
|
||||||
|
},
|
||||||
|
positionTypes: {
|
||||||
|
view: "edr_freight_app:position_types:view",
|
||||||
|
create: "edr_freight_app:position_types:create",
|
||||||
|
update: "edr_freight_app:position_types:update",
|
||||||
|
delete: "edr_freight_app:position_types:delete",
|
||||||
|
},
|
||||||
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const slugToResourceKey = (slug: RuleEngineResourceSlug): string =>
|
const slugToResourceKey = (slug: RuleEngineResourceSlug): string =>
|
||||||
|
|||||||
@@ -32,12 +32,7 @@ import {
|
|||||||
User,
|
User,
|
||||||
X,
|
X,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import {
|
import { type CSSProperties, Fragment, type ReactNode, useState } from "react";
|
||||||
type CSSProperties,
|
|
||||||
Fragment,
|
|
||||||
type ReactNode,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import { PROFILE_TYPE_LABELS } from "@/constants/profileMode";
|
import { PROFILE_TYPE_LABELS } from "@/constants/profileMode";
|
||||||
import NotificationBellContainer from "@/features/notifications/NotificationBellContainer";
|
import NotificationBellContainer from "@/features/notifications/NotificationBellContainer";
|
||||||
|
|
||||||
@@ -159,7 +154,6 @@ export function AppLayout({
|
|||||||
const borderColor = theme.colors["edr-border"][6];
|
const borderColor = theme.colors["edr-border"][6];
|
||||||
const mutedColor = theme.colors["edr-muted"][6];
|
const mutedColor = theme.colors["edr-muted"][6];
|
||||||
const textColor = theme.colors["edr-text"][6];
|
const textColor = theme.colors["edr-text"][6];
|
||||||
const accentColor = theme.colors["edr-accent"][6];
|
|
||||||
const primaryColor = theme.colors["edr-green"][5];
|
const primaryColor = theme.colors["edr-green"][5];
|
||||||
const primaryDarkColor = theme.colors["edr-green"][7];
|
const primaryDarkColor = theme.colors["edr-green"][7];
|
||||||
|
|
||||||
@@ -469,7 +463,9 @@ export function AppLayout({
|
|||||||
<Menu.Item
|
<Menu.Item
|
||||||
leftSection={<Plus size={15} />}
|
leftSection={<Plus size={15} />}
|
||||||
color="edr-green"
|
color="edr-green"
|
||||||
onClick={() => navigate("/bookings/new", { state: { fresh: true } })}
|
onClick={() =>
|
||||||
|
navigate("/bookings/new", { state: { fresh: true } })
|
||||||
|
}
|
||||||
>
|
>
|
||||||
New Booking
|
New Booking
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
|||||||
Reference in New Issue
Block a user