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

25 KiB
Raw Blame History

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