Files
edr-platform/docs/freight-platform/SYSTEM-FLOW.md
Marshal 0f7cac2b68 add booking request functionality for GENERAL customs contracts
- Create migration for booking_requests table with necessary fields and indexes.
- Implement BookingRequestRepository for database operations related to booking requests.
- Develop BookingRequestService to handle business logic for submitting, accepting, rejecting, and canceling booking requests.
- Create DTOs for creating booking requests and reviewing them.
- Define BookingRequest entity to map to the booking_requests table.
- Add UI components for managing shipment requests, including detail and list pages.
- Implement OperationDatePicker component for selecting available shipment days.
2026-06-29 09:30:44 +00:00

543 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# EDR Freight — System Flow (API)
How the freight API drives a shipment end to end:
**Onboarding → Contract → Booking → Clearance → Operation → Schedule → Delivery**
This document tracks the **API logic only** — exact statuses, transitions, branches, and endpoints as the `edr-freight-api` implements them. Read arrows (`→`) as "then". **IF** blocks are the branches.
> Two facts shape everything below:
> 1. **Clearance is per-booking, not pre-booking** (except the one-time-customs special case). The contract agrees terms; each booking carries its own customs document loop.
> 2. **Who creates the booking depends on customs.** No customs → the customer. Customs (Path B) → Global Logistics (GL) on the customer's behalf.
---
## 1) Onboarding
> Register the company so a profile can transact. Only an **active profile** may create contracts or bookings.
```
Company → status PENDING
Each operation profile (importer | exporter | freight_forwarder) → status PENDING
Backoffice approves each profile → ACTIVE + reference (IM-00001 / EX-00001 / FF-00001)
→ only an ACTIVE profile can create contracts / bookings
```
Documents required depend **only on nationality**:
| Nationality | Company documents |
|-------------|-------------------|
| Ethiopian | TIN Certificate · Commercial License · National ID |
| Foreign | TIN Certificate · Investment License · National ID · Passport |
Operation type adds one business-license card per profile — nothing else.
---
## 2) Contract
> Agree terms (route, cargo, price), run the approval chain, sign. Service: `ContractTransitionService`.
### Setup choices that decide later paths
```
contractKind: ONE_TIME (one shipment per contract)
GENERAL (many shipments over a validity window)
tradeDirection: IMPORT | EXPORT | DOMESTIC
customsClearingEnabled: true → Path B (GL clears + books) — IMPORT/EXPORT
false → Path A (customer self-clears + books) — IMPORT/EXPORT
(DOMESTIC → no clearance at all)
```
### Status enum (`CONTRACT_STATUSES`)
```
DRAFT · SUBMITTED · PRICE_CHANGED_PENDING_CONFIRM · CHANGES_REQUESTED
PENDING_APPROVAL · APPROVED · APPROVED_PENDING_SIGNATURE · CONTRACT_READY
SIGNED_CUSTOMER · FULLY_EXECUTED · CONTRACT_ACTIVE
AWAITING_CLEARANCE_DOCUMENTS · CLEARANCE_UNDER_REVIEW · CLEARANCE_READY_FOR_BOOKING
ACTIVE_SHIPMENT_IN_PROGRESS · CONTRACT_CLOSED · EXPIRED
REJECTED · CANCELLED
RENEWAL_DRAFT · RENEWAL_SUBMITTED · RENEWAL_PENDING_APPROVAL · AMENDMENTS_PROPOSED · ARCHIVED
```
Clearance enum (`CONTRACT_CLEARANCE_STATUSES`): `NOT_APPLICABLE · AWAITING_DOCUMENTS · DOCUMENTS_UNDER_REVIEW · CLEARANCE_READY_FOR_BOOKING · SELF_CLEARED · ACTIVE_SHIPMENT_IN_PROGRESS`
### Transition table
| Method | Guard (allowed status) | Result |
|--------|------------------------|--------|
| `submit()` | DRAFT, CHANGES_REQUESTED | freeze rates → `SUBMITTED` |
| `confirmSubmit()` | PRICE_CHANGED_PENDING_CONFIRM | freeze rates → `SUBMITTED` |
| `staffAccept(validityDays)` | SUBMITTED | set validity window, build approval chain → `PENDING_APPROVAL` |
| `requestChanges()` | SUBMITTED | review note → `CHANGES_REQUESTED` |
| `reject()` | SUBMITTED, PENDING_APPROVAL | → `REJECTED` |
| `approveStep(stepId, role)` | PENDING_APPROVAL, APPROVED_PENDING_SIGNATURE | complete step in order; all done → `APPROVED` |
| `generateContract()` | APPROVED, APPROVED_PENDING_SIGNATURE | render PDF → `CONTRACT_READY` |
| `sign(CUSTOMER)` | CONTRACT_READY | apply signature → `SIGNED_CUSTOMER` → auto `counterSign()` |
| `counterSign(STAFF/DIRECTOR/CEO)` | SIGNED_CUSTOMER | **branch on path** ↓ |
| `renew()` | any | clone with `renewalOfId`, version++ → `RENEWAL_DRAFT` |
**Approval chain** (`instantiateApprovalSteps`): `LINE_STAFF` → optional `DIRECTOR` → optional `CEO`. Director required when `freightType = BULK` OR `cargoType.requiresDirectorApproval`.
### The counter-sign branch — THIS is where the model differs from "clearance first"
```
IF GENERAL + customsClearingEnabled (Path B):
NO contract-level clearance cycle.
status → CONTRACT_ACTIVE, clearanceStatus → NOT_APPLICABLE
→ customer requests shipments; GL creates + clears each booking (per-booking)
IF ONE_TIME + customs (Path A self-clear OR one-time-customs):
open a contract clearance cycle
status → AWAITING_CLEARANCE_DOCUMENTS, clearanceStatus → AWAITING_DOCUMENTS
→ contract-level clearance loop (section 3), then booking
IF DOMESTIC (no customs):
status → FULLY_EXECUTED (ONE_TIME) or CONTRACT_ACTIVE (GENERAL)
→ customer books immediately (section 4)
```
> So contract-level clearance (section 3) only runs for the **one-time + customs** case. The common GENERAL-customs case goes straight to `CONTRACT_ACTIVE` and defers all clearance to the booking (section 5).
### Side branches
```
staff requestChanges → CHANGES_REQUESTED → customer edits → submit → SUBMITTED
staff reject → REJECTED
GENERAL contract → renew → RENEWAL_DRAFT (clone of prior version)
```
### Endpoints (`contracts.controller.ts`)
```
POST /contracts/:id/submit submit()
POST /contracts/:id/confirm-submit confirmSubmit()
POST /contracts/:id/staff/accept staffAccept()
POST /contracts/:id/staff/request-changes requestChanges()
POST /contracts/:id/staff/reject reject()
POST /contracts/:id/approval-steps/:stepId/approve approveStep()
POST /contracts/:id/contract/generate generateContract()
GET /contracts/:id/contract/view view PDF/HTML
POST /contracts/:id/contract/sign sign()
POST /contracts/:id/renew renew()
GET /contracts/:id/capacity remaining drawdown (GENERAL)
POST /contracts/:id/bookings create booking under contract (section 4)
GET /contracts/list-summary list
GET /contracts/booking-requests/queue staff shipment-request queue
```
---
## 3) Contract-level clearance (ONE_TIME + customs only)
> Runs only when the counter-sign branch opened a contract clearance cycle.
> Service: `ContractClearanceService`. Loop: **upload → review (approve/query) → re-upload → finalize.**
### Who reviews
```
Path A (customsClearingEnabled = false) → OPERATIONS
Path B (customsClearingEnabled = true) → GLOBAL LOGISTICS Ethiopia (GL ET)
```
### Document states & loop
```
each document: PENDING → APPROVED (reviewer approves)
→ QUERIED (reviewer demands re-upload, note required)
→ contract back to AWAITING_CLEARANCE_DOCUMENTS
(only queried docs re-upload; approved stay)
1. customer uploads all required docs → CLEARANCE_UNDER_REVIEW, each doc PENDING
2. reviewer goes doc by doc (approve / query)
3. customer re-uploads queried docs → back to step 2
4. all required docs APPROVED → finalize
```
### Required-doc resolution (`contract-clearance.util.ts`)
```
input (customer uploads):
Path B: contract_clearance_{import|export}_{container|bulk}
Path A: contract_clearance_selfclear_{import|export}_{container|bulk}
DOMESTIC: null (no gate)
output (GL uploads, container customs only):
contract_clearance_output_{import|export}_container
(bulk or non-customs → null)
```
### Finalize
```
opsFinalize() Path A — requires every required input doc APPROVED
→ clearanceStatus = SELF_CLEARED
→ contract status = CONTRACT_ACTIVE (GENERAL) | FULLY_EXECUTED (ONE_TIME)
→ CUSTOMER creates booking
finalize() Path B — requires every required input APPROVED + every required output uploaded
→ clearanceStatus = CLEARANCE_READY_FOR_BOOKING
→ GL creates booking
```
### Endpoints
```
GET /contracts/:id/clearance document grid
POST /contracts/:id/clearance/documents customer upload (multipart)
POST /contracts/:id/clearance/review GL approve | query
POST /contracts/:id/clearance/output-documents GL upload output docs
POST /contracts/:id/clearance/finalize GL finalize → CLEARANCE_READY_FOR_BOOKING
POST /contracts/:id/clearance/ops-review Ops approve | query
POST /contracts/:id/clearance/ops-finalize Ops finalize → SELF_CLEARED
GET /contracts/clearance/queue GL ET queue (customs contracts)
GET /contracts/clearance/ops-queue Operations queue (self-clear)
GET /contracts/clearance/history GL completed
GET /contracts/clearance/ops-history Ops completed
```
---
## 4) Booking — who creates it & the gate
> Turn a ready contract into a shipment. Service: `ContractBookingService` (creation gate), `BookingTransitionService` (lifecycle).
> Endpoint: `POST /contracts/:id/bookings`.
### The gate (`assertGate`)
```
Customs (Path B) — only GL ET (needs contracts.createBooking permission):
ONE_TIME : contract.clearanceStatus = CLEARANCE_READY_FOR_BOOKING
GENERAL : contract.status = CONTRACT_ACTIVE
booking starts in AWAITING_DOCUMENTS (per-booking clearance), createdByRole = GL_ET
No customs (Path A / DOMESTIC) — customer or staff:
contract.status = FULLY_EXECUTED or CONTRACT_ACTIVE
booking starts in OPERATION_REQUEST_PENDING (no clearance gate),
createdByRole = CUSTOMER | STAFF
```
So per-booking clearance applies to **every customs booking** — both the GENERAL drawdown and the one-time case (whose contract cycle already ran). Path A / domestic bookings skip straight to the operation request.
### Booking status enum (`BOOKING_STATUSES`)
```
DRAFT · SUBMITTED · PRICE_CHANGED_PENDING_CONFIRM · CHANGES_REQUESTED
PENDING_APPROVAL · APPROVED_PENDING_SIGNATURE · APPROVED · CONTRACT_READY
SIGNED_CUSTOMER · FULLY_EXECUTED
AWAITING_DOCUMENTS · DOCUMENTS_UNDER_REVIEW · CLEARANCE_READY
OPERATION_REQUEST_PENDING · OPERATION_CHANGES_REQUESTED
SELECTED_FOR_BATCH · ROAD_DISPATCH_PENDING · PAID · IN_TRANSIT · COMPLETED
REJECTED · CANCELLED · EXPIRED · PENDING_CONSOLIDATION · CONSOLIDATED
```
Payment (`paymentStatus`): `PENDING → PNR_GENERATED → VERIFICATION_IN_PROGRESS → PAID` (or `FAILED`).
Scheduling (`schedulingStatus`): `NOT_SCHEDULED · HOLDING · ELIGIBLE · SCHEDULED · DISPATCHED`.
### Customer-self-booking lifecycle (Path A / domestic — same approval shape as a contract)
```
DRAFT
→ generate-price → submit
price unchanged → SUBMITTED
price changed → PRICE_CHANGED_PENDING_CONFIRM → confirm-submit → SUBMITTED
→ acceptIntake (staff) → PENDING_APPROVAL (validity window + approval steps)
→ approveStep ×N (LINE_STAFF → DIRECTOR → CEO) → APPROVED (auto-generates contract)
→ CONTRACT_READY → customerSign → SIGNED_CUSTOMER
→ marketingApprove → FULLY_EXECUTED (sets fullyExecutedAt, lockedAt)
→ (then operation request, section 5)
```
Side branches: `requestChanges → CHANGES_REQUESTED`; `staffReject / reject → REJECTED`; `cancel → CANCELLED` (DRAFT…CONTRACT_READY).
### Transition methods (`BookingTransitionService`)
```
submit · confirmSubmit · requestChanges · acceptIntake · staffReject · reject
approveStep · rejectStep · generateContract · customerSign · marketingApprove
governmentExpedite (govt fast-path → PAID)
requestOperation · proceedToOperation · reviewOperationRequest
submitClearanceDocuments · reviewDocument · uploadClearanceOutputDocuments · finalizeClearance
startTransit · complete · cancel · requestConsolidation · removeConsolidation
```
---
## 5) Per-booking clearance + operation request
> The customs booking's own document loop, then everyone funnels into the operation request that puts the shipment in the schedule pool.
### Per-booking clearance (customs bookings only)
```
GL creates booking → AWAITING_DOCUMENTS
submitClearanceDocuments (customer upload) → DOCUMENTS_UNDER_REVIEW
reviewDocument (GL) → doc APPROVED | QUERIED (note)
uploadClearanceOutputDocuments (GL, customs output)
finalizeClearance requires 100% required inputs APPROVED + required outputs present
→ CLEARANCE_READY
proceedToOperation → OPERATION_REQUEST_PENDING
```
inputCode / outputCode resolve from `(tradeDirection, freightType, customsClearingEnabled)` — same scheme as the contract loop. Doc review status: `PENDING · APPROVED · QUERIED`.
### Operation request (all paths)
```
requestOperation() guard CLEARANCE_READY | OPERATION_CHANGES_REQUESTED, valid schedule day
→ OPERATION_REQUEST_PENDING
(Path A / domestic bookings begin life here directly)
reviewOperationRequest(decision):
ACCEPT → acceptOperationRequest():
train service → FULLY_EXECUTED + enqueue batch fill (origin, dest, day)
truck service → ROAD_DISPATCH_PENDING (skips train pool, section 7)
REQUEST_CHANGES → OPERATION_CHANGES_REQUESTED (note; customer resubmits)
```
### Clearance / operation endpoints (`bookings.controller.ts`)
```
GET /bookings/:id/clearance docs grid + GL review state
POST /bookings/:id/clearance/documents submitClearanceDocuments → DOCUMENTS_UNDER_REVIEW
POST /bookings/:id/clearance/review reviewClearanceDocument (approve | query)
POST /bookings/:id/clearance/output-documents GL upload output
POST /bookings/:id/clearance/finalize finalizeClearance → CLEARANCE_READY
POST /bookings/:id/clearance/proceed proceedToOperation → OPERATION_REQUEST_PENDING
POST /bookings/:id/operation/review reviewOperationRequest (ACCEPT | REQUEST_CHANGES)
```
Booking lifecycle endpoints (selected):
```
POST /bookings · PATCH /bookings/:id · DELETE /bookings/:id (DRAFT only)
POST /bookings/:id/generate-price · /submit · /confirm-submit · /reject
POST /bookings/:id/staff/accept · /staff/request-changes · /staff/reject
POST /bookings/:id/approval-steps/:stepId/approve · /reject
POST /bookings/:id/contract/generate · /contract/sign · GET /contract/view
POST /bookings/:id/marketing/approve → FULLY_EXECUTED
POST /bookings/:id/government-expedite govt → PAID
POST /bookings/:id/operations/start-transit → IN_TRANSIT
POST /bookings/:id/operations/complete → COMPLETED
POST /bookings/:id/cancel → CANCELLED
GET /bookings/my customer payable list
GET /bookings/queues/:queue intake | approval | signatures | marketing | finance
GET /bookings/:id/tracking shipment tracking
POST /bookings/:id/consolidation · DELETE · GET pair partial-wagon bookings
```
---
## 6) GL Phase 2 — post-booking milestones (customs / Path B)
> Once GL creates a customs booking, the system seeds the **post-booking milestone timeline**.
> Services: `ClearanceMilestoneService` (seed/advance), `GlOperationsService` (structured actions).
> `seedPostBookingMilestones(booking)` runs at GL booking creation. Each milestone: `PENDING → COMPLETED` (or `SKIPPED`).
### Milestone owners
`ET` (GL Ethiopia) · `DJ` (GL Djibouti) · `OPS` (Operations) · `CUST` (Customer)
### Import timeline (catalog order, post-booking segment)
```
WAGON_REQUESTED → FREIGHT_PAYMENT_SETTLED → WAGON_ALLOCATED → GATEPASS_GRANTED
→ READY_FOR_LOADING → LOADED → DEPARTED_FROM_DJIBOUTI [HANDOFF ET↔DJ]
→ ARRIVED_ETHIOPIA → OFFLOADED → T1_CLOSED → RISK_ASSIGNED
→ IMPORT_RELEASE_GRANTED → IMPORT_PROCESS_COMPLETED
→ STORAGE_INVOICE_RAISED → EXIT_NOTE_GENERATED
```
(Pre-booking import milestones — `IMPORT_DOCS_UPLOADED · PENDING_DOCUMENT_REVIEW · DOCUMENTS_APPROVED · UNDER_CUSTOMS_CLEARANCE · DECLARED · DUTY_TAXES_ADVISED · DUTY_TAX_PAID · DO_COLLECTED` — track the clearance loop and end at `DO_COLLECTED`.)
### Export timeline (post-booking segment)
```
WAGON_REQUESTED → FREIGHT_PAYMENT_PENDING → FREIGHT_PAYMENT_SETTLED → WAGON_ALLOCATED
→ CARGO_ARRIVED → READY_FOR_LOADING → LOADED → DEPARTED_TO_DJIBOUTI [HANDOFF]
→ ARRIVED_AT_DJIBOUTI → GATEPASS_GRANTED → OFFLOADED
```
(Pre-booking export: `EXPORT_DOCS_UPLOADED · PENDING_DOCUMENT_REVIEW · DOCUMENTS_APPROVED · RELEASE_ORDER_SECURED · UNDER_CUSTOMS_CLEARANCE · DECLARED · EXPORT_RELEASED`.)
### Advance logic
```
completeForBooking(bookingId, code, userId?, note?) mark COMPLETED + triggeredAt/By
completeForContract(contractId, code, …) pre-booking contract milestones
completeByDocTrigger(scope, code) auto-complete from a doc upload
onHandoff(bookingId, code) fires on DEPARTED_* (notification reserved)
```
### Structured GL actions (`GlOperationsService` + `gl-operations.dto.ts`)
```
assignRisk riskLevel = GREEN | YELLOW | RED → completes RISK_ASSIGNED
adviseDuty {amount, currency, declarationSerial?} → completes DUTY_TAXES_ADVISED
customer uploads slip → DUTY_TAX_PAID
assignStation {stationYardId, staffId?} sets glStationYardId/glAssignedStaffId (GL US-02)
reportIncident incidentType = SEAL_BROKEN | CONTAINER_OPENED | CONTAINER_DAMAGED | FLUID_LEAKING
+ description + photos → ClearanceIncident
uploadDocuments / uploadDutySlip → doc-triggered milestone auto-complete
```
### Doc-triggered milestones (`DOC_CODE_TO_MILESTONE`)
| Upload (code) | Completes | Owner |
|---------------|-----------|-------|
| `delivery_order` | DO_COLLECTED | GL DJ |
| `release_order` | RELEASE_ORDER_SECURED | GL DJ |
| `t1_transport_document` | T1_CLOSED | GL ET |
| `import_release` | IMPORT_RELEASE_GRANTED | GL ET |
| `full_in_interchange` | OFFLOADED | GL DJ |
| `final_declaration` | IMPORT_PROCESS_COMPLETED | GL ET |
| `duty_tax_receipt` | DUTY_TAX_PAID | **Customer** |
| `incident_photo` | (logging only, no milestone) | — |
### Endpoints
```
POST /contracts/bookings/:bookingId/milestones/:code/complete manual complete
POST /contracts/:id/milestones/:code/complete pre-booking contract milestone
POST /contracts/bookings/:bookingId/risk assignRisk
POST /contracts/bookings/:bookingId/duty adviseDuty
POST /contracts/bookings/:bookingId/station-assign assignStation
POST /contracts/bookings/:bookingId/documents GL doc upload (DO/RO/T1/…)
POST /contracts/bookings/:bookingId/duty-slip customer duty slip
GET /contracts/bookings/:bookingId/incidents list
POST /contracts/bookings/:bookingId/incidents reportIncident
GET /contracts/bookings/:bookingId/milestones timeline
```
> Still manual-only (no fee engine): `STORAGE_INVOICE_RAISED`, `EXIT_NOTE_GENERATED`. Out of scope: demurrage auto-calc, finance AP closure, multimodal, truck waybill/POD.
---
## 7) Schedule — demand batching (`booking-batch.service.ts`)
> Day-level pooling. Customer picks a **day**; the batch engine assigns the actual **train** later.
> Cron groups bookings by `(origin yard, destination yard, day)`. EAT timezone, 3-hour windows (0003 … 2124).
### Pool states (`boardState`, read-only view)
```
READY FULLY_EXECUTED + fullyExecutedAt, no train link yet
SELECTED_FOR_BATCH picked by fill, in pay window (trainScheduleId set, paymentDeadline set)
ALLOCATED linked to train via TrainScheduleBooking, PAID (or govt)
WAITING PAID but not yet linked (staff-reconciled)
EXPIRED failed to pay in window
PENDING_CONTRACT any other non-terminal state
```
### The cron cycle (every 3h; prod `0 */3 * * *`)
```
1. Fill distribute (route, dest, day) pool across OPEN schedules by priorityScore
pick earliest train; fit bookings (govt preempts commercial)
commercial → reserve: SELECTED_FOR_BATCH + paymentDeadline = now + 1h
govt → allocate: PAID, SCHEDULED
2. Settle 1h after window closes — allocate paid reservations,
expire unpaid (→ EXPIRED, unpin train), top up from waiting list
3. Reconcile link orphaned PAID bookings to a schedule
4. Allocate auto-assign wagon slots to allocated bookings
```
`reserve()` → SELECTED_FOR_BATCH. `allocate()` → create `TrainScheduleBooking`, PAID, schedulingStatus SCHEDULED. `expire()` → trainScheduleId null, EXPIRED, schedulingStatus ELIGIBLE (back in pool).
### Payment → transit
```
SELECTED_FOR_BATCH / AWAITING_PAYMENT → pay (Telebirr) → PAID
PAID → startTransit → IN_TRANSIT → complete → COMPLETED
```
### Wagon math
```
wagonsRequired = ⌈ Σ over containers (qty × wagonsPerUnit) ⌉
```
### Key endpoints (`train-scheduling.controller.ts`)
```
GET /train-scheduling/available-days days with OPEN departures
GET /train-scheduling/available-days-for-cargo capacity-aware bookable days
GET /train-scheduling/bookable-schedules OPEN same-route schedules
GET /train-scheduling/batch-board monitoring board (states + counts)
GET /train-scheduling/eligible-bookings PAID/FULLY_EXECUTED ready to allocate
POST /train-scheduling/{container|bulk}/schedules create schedule
POST /train-scheduling/schedules/:id/assign-bookings
POST /train-scheduling/schedules/:id/run-batch staff manual fill
POST /train-scheduling/schedules/:id/run-allocation staff manual wagon allocation
POST /train-scheduling/schedules/:id/finalize · /dispatch
PATCH /train-scheduling/schedules/:id/booking-window OPEN | CLOSE
POST /train-scheduling/bookings/:bookingId/mark-paid · /expire · /move-schedule
GET /train-scheduling/schedules/:id/checkpoints · POST /checkpoints tracking events
```
---
## 8) Delivery / last mile
```
Road service: reviewOperationRequest ACCEPT → ROAD_DISPATCH_PENDING (skips train pool)
billed by KM, dispatched by truck (First-Mile operations)
First/last mile: pickup + delivery addresses captured at booking; equipment return WITH | WITHOUT
last-mile: PAYMENT_PENDING → READY_TO_TRANSIT → IN_TRANSIT → RECEIVED_TO_PORT
```
---
## The whole thing on one page
```
ONBOARD
profile (importer/exporter/FF) approved → ACTIVE → can transact
CONTRACT (ContractTransitionService)
submit → staffAccept → approveStep×N (LINE_STAFF→DIRECTOR→CEO) → CONTRACT_READY
→ sign(CUSTOMER) → counterSign → BRANCH:
GENERAL + customs → CONTRACT_ACTIVE (NOT_APPLICABLE) — clearance deferred to booking
ONE_TIME + customs → AWAITING_CLEARANCE_DOCUMENTS — contract clearance cycle (section 3)
DOMESTIC / no customs → FULLY_EXECUTED | CONTRACT_ACTIVE — book now
BOOKING (POST /contracts/:id/bookings, gate in ContractBookingService)
customs (Path B) → only GL → starts AWAITING_DOCUMENTS
no customs (Path A/domestic) → customer/staff → starts OPERATION_REQUEST_PENDING
PER-BOOKING CLEARANCE (customs only, BookingTransitionService)
upload → review(approve/query) → finalize → CLEARANCE_READY → proceed → OPERATION_REQUEST_PENDING
OPERATION REQUEST (all paths)
requestOperation → OPERATION_REQUEST_PENDING
reviewOperationRequest ACCEPT → FULLY_EXECUTED (train) | ROAD_DISPATCH_PENDING (truck)
GL PHASE 2 (customs, post-booking) — ClearanceMilestoneService + GlOperationsService
milestone timeline: wagon → pay → allocate → load → depart [handoff]
→ arrive → offload → T1 close → risk → release → complete
GL actions: risk (G/Y/R) · duty advise · station assign · DO/RO/T1 upload · incident
customer: watch read-only · upload duty slip · pay freight
SCHEDULE (booking-batch.service.ts, 3h EAT cron)
fill (route,dest,day) → SELECTED_FOR_BATCH (+1h pay) → pay → PAID/ALLOCATED
→ IN_TRANSIT → COMPLETED (road → ROAD_DISPATCH_PENDING → truck)
```
---
### Code map
| Area | Key files (`apps/edr-freight-api/src/modules/…`) |
|------|---------------------------------------------------|
| Contract state machine | `contracts/contract-transition.service.ts`, `contracts/entities/contract.entity.ts`, `contracts/contracts.controller.ts` |
| Contract clearance | `contracts/contract-clearance.service.ts`, `contracts/contract-clearance.util.ts` |
| Booking gate | `contracts/contract-booking.service.ts` |
| Booking lifecycle + per-booking clearance | `bookings/booking-transition.service.ts`, `bookings/entities/booking.entity.ts`, `bookings/bookings.controller.ts` |
| GL Phase 2 | `contracts/clearance-milestone.service.ts`, `contracts/clearance-milestone.catalog.ts`, `contracts/gl-operations.service.ts`, `contracts/dto/gl-operations.dto.ts`, `contracts/entities/clearance-incident.entity.ts` |
| Schedule / batch engine | `train-scheduling/booking-batch.service.ts`, `train-scheduling/train-scheduling.service.ts`, `train-scheduling/train-scheduling.controller.ts` |
```