Files
edr-platform/docs/new-doc.md
2026-06-26 23:24:48 +00:00

83 KiB
Raw Blame History

ContractBooking Separation & Global Logistics Design

Version: 1.1
Date: 2026-06-26
Revision: Customs-clearance-first path — GL owns booking creation; customer pays only.
Scope: Contracts, bookings, scheduling, allocation, and Global Logistics (GL) workflows
References:


Table of Contents

  1. Executive Summary
  2. Design Principles
  3. Current Architecture
  4. Target Architecture
  5. Database Schema
  6. Status Machines
  7. Contract Wizard Specification
  8. Booking Creation Specification
  9. Pricing Model
  10. Renewal and Expiry Rules
  11. Global Logistics — Import Unimodal
  12. Global Logistics — Export Unimodal
  13. Customs Clearance Path — GL-Owned Execution
  14. Gap Analysis Matrix
  15. API Endpoint Mapping
  16. Frontend Route & Component Mapping
  17. Migration Plan
  18. Out of Scope / Future Work
  19. Open Items for Sign-Off

1. Executive Summary

The EDR freight platform currently collapses the legal contract and the operational shipment into a single freight.bookings row. The customer wizard collects container quantities, weights, and binding shipment details at intake; clearance documents, contract signatures, batch scheduling, and payment all attach to that same row.

Stakeholder requirements and the reference PDFs describe a two-phase lifecycle:

  1. Contract phase — customer selects operation type, contract kind (general / one-time), service, cargo scope (20ft/40ft sizes or bulk commodity — no quantities), routes, hazard/reefer flags, estimated date, intake documents, and unit-rate pricing. Marketing approves, both parties sign. No booking exists yet.

  2. Execution phase — behaviour splits by whether the contract includes customs clearance (Global Logistics service):

    Path A — Transport only (no customs): After contract is fully executed, the customer creates a shipment booking (schedule date, quantities, container numbers, VGM, hazard/reefer counts), then enters batch → payment → allocation → transit as today.

    Path B — Transport with customs clearance: After contract is fully executed, the customer uploads clearance documents to GL (no booking yet). Ethiopian GL reviews and approves customer docs, uploads GL output documents, and runs the clearance workflow. GL Ethiopia exclusively creates the booking — entering schedule date, quantities, container numbers, VGM, bulk ton/item counts, and all other shipment details. The customer does not use the booking wizard. From booking creation onward, batch selection, wagon allocation, milestones, and transit proceed as today; the only customer action in the operational pipeline is payment (freight PNR, and duty/tax slips when advised during clearance).

This document specifies a minimal-change evolution: introduce a first-class freight.contracts table by extracting contract-phase columns from bookings, add contract_id to bookings, and preserve all existing scheduling, payment, and allocation mechanics.

Confirmed stakeholder decisions:

  • Customs clearance path (Path B): Clearance documents are collected after contract signing and before any booking row exists. GL owns booking creation and data entry; customer pays only.
  • Transport-only path (Path A): Customer creates bookings under the contract as normal.
  • Each shipment cycle under a customs contract follows: sign contract → customer clearance docs → GL approval → GL creates booking → customer pays → ops pipeline.
  • One-time contracts allow one active booking at a time; if payment expires the booking expires but the contract stays valid for a new clearance cycle + GL re-booking until contract validity ends.
  • General contracts allow multiple shipment cycles over the validity window (each cycle: clearance docs → GL booking → payment) with no quantity pool caps at contract level.

2. Design Principles

Principle Rationale
Extract, don't rewrite Keep payments, train_schedules, wagon_booking_allocations, and batch engine untouched; link via bookings.contract_id.
Unit rates at contract, totals at booking Contract pricing shows per-container / per-ton / per-item rates; booking computes actual totals from entered quantities.
Scope vs. execution Contract defines what is allowed (sizes, commodities, routes, flags); booking defines what is shipped (qty, container numbers, dates).
Pre-booking clearance (customs path) For contracts with customs clearance, customer uploads clearance docs after signing, before booking exists — stored on contract_id via contract_document_review.
GL-owned booking (customs path) When customs_clearing_enabled = true, only Ethiopian GL creates bookings and enters all shipment fields; customer portal has no booking wizard.
Customer pays only (customs path) After GL creates the booking and it reaches the payment gate, the customer pays freight PNR (and duty/tax slips during clearance when advised). All other steps are GL/Ops.
Contract intake docs separate Wizard step 6 documents (commercial framework / onboarding attachments) attach to contract_id at submission — distinct from post-sign clearance docs.
Preserve approval hierarchy Line Staff → Director → CEO routing from US-06 moves to contract_approval_steps; booking operations review stays on booking.
Dual GL teams Ethiopian GL (edr_gl_ethiopia) and Djibouti GL (edr_gl_djibouti) with region-scoped queues and milestone ownership.
Backward-compatible payments Payment webhooks continue using ref_id = booking.id; no payment microservice change.

3. Current Architecture

3.1 Data Model (Today)

All entities live in PostgreSQL schema freight.

erDiagram
  companies ||--o{ bookings : owns
  bookings ||--o{ booking_container : has_qty_at_intake
  bookings ||--o{ booking_contract_signatures : signs
  bookings ||--o{ booking_document_review : clearance
  bookings ||--o{ booking_approval_step : approves
  bookings ||--o{ booking_rate_snapshot : rates
  bookings ||--o{ payments : pays
  bookings ||--o| train_schedule_bookings : scheduled
  bookings ||--o{ booking_orders : general_contract_drawdown
  booking_orders ||--o| bookings : spawns_child_ONE_TIME
  bookings ||--o{ contract_route_lines : multi_route_qty_pools

Key observation: booking_type = GENERAL_CONTRACT bookings are contracts; booking_orders spawns child ONE_TIME bookings for each drawdown. One-time bookings are simultaneously the contract and the shipment.

3.2 Current API Modules

Module Path Role
Bookings src/modules/bookings/ Create, price, submit, approve, sign, clearance, operation review
Booking Orders src/modules/booking-orders/ General contract drawdown (POST /booking-orders)
Payment src/modules/payment/ PNR, webhook finalization
Train Scheduling src/modules/train-scheduling/ Batch pool, wagon allocation
Contracts (PDF) src/contracts/ Handlebars templates, PDF generation — operates on booking today

3.3 Current Booking API Endpoints

Base path: /bookings

Method Path Phase
POST / Create (contract + shipment combined)
POST /:id/generate-price Pricing
POST /:id/submit Submit for approval
POST /:id/staff/accept Staff intake + contract validity
POST /:id/approval-steps/:stepId/approve Approval chain
POST /:id/contract/generate Generate contract PDF
POST /:id/contract/sign Customer / staff signature
POST /:id/clearance/* GL document gate
POST /:id/clearance/proceed Customer picks shipment day
POST /:id/operation/review Operations accept
POST /:id/payment/pay Payment
GET /queues/:queue Staff queues

Booking orders: POST /booking-orders, GET /booking-orders/contract/:id/pool

3.4 Current Frontend Flow

Portal (edr-freight-web/portal/):

Route Component Purpose
/bookings/new NewBookingPage.tsx 7-step wizard (operation → contract type → service → cargo → route → docs → review)
/bookings/:id BookingDetailPage Lifecycle actions
/bookings/:id/contract BookingContractPage Sign contract
/contracts ContractsList.tsx Lists GENERAL_CONTRACT bookings
/contracts/:id ContractDetailPage Pool + PlaceOrderDialog drawdown

Backoffice (edr-freight-web/backoffice/):

Route Component Purpose
/dashboard/booking-requests BookingRequestsPage Marketing approval
/dashboard/clearance DocumentClearanceListPage Single GL queue

3.5 Current Lifecycle (One-Time Booking)

flowchart TD
  A[DRAFT wizard with qty and weight] --> B[generate-price total amount]
  B --> C[SUBMITTED]
  C --> D[PENDING_APPROVAL staff accept sets validity]
  D --> E[Approval chain APPROVED]
  E --> F[CONTRACT_READY generate PDF]
  F --> G[SIGNED_CUSTOMER]
  G --> H[Staff counter-sign FULLY_EXECUTED or AWAITING_DOCUMENTS]
  H --> I{Customs service?}
  I -->|Yes IMPORT/EXPORT| J[Clearance on booking]
  I -->|No DOMESTIC| K[Operation request]
  J --> K[OPERATION_REQUEST_PENDING pick day]
  K --> L[Ops review FULLY_EXECUTED]
  L --> M[Batch pool SELECTED_FOR_BATCH]
  M --> N[Payment PAID]
  N --> O[Wagon allocation IN_TRANSIT]
  O --> P[COMPLETED]

General contract today: Same through signing → CONTRACT_ACTIVEPlaceOrderDialog spawns child booking via booking_orders → child enters clearance/batch pipeline. Quantity pools tracked in contract_route_lines.quantity and booking_container.quantity.

3.6 Current Clearance Model

  • Utility: clearance.util.ts resolves setting codes from trade_direction, freight_type, includesCustoms.
  • Review rows: booking_document_review keyed by (booking_id, setting_code, file_key).
  • Single IAM role: edr_global_logistics with bookings:clearance_view, bookings:review_documents, bookings:upload_clearance_output, bookings:finalize_clearance.
  • Three booking statuses for clearance gate: AWAITING_DOCUMENTSDOCUMENTS_UNDER_REVIEWCLEARANCE_READY.

4. Target Architecture

4.1 Conceptual Split

flowchart TD
  subgraph contractPhase [Contract Phase]
    C[contracts]
    CR[contract_routes]
    CCS[contract_cargo_scope]
    CRS[contract_rate_snapshots]
    CAS[contract_approval_steps]
    CSIG[contract_signatures]
    CDOC[contract intake files]
  end

  subgraph bookingPhase [Booking Phase per Shipment]
    B[bookings]
    BC[booking_container + container_units]
    BDR[booking_document_review]
    BRS[booking_rate_snapshots]
    PAY[payments]
    TSB[train_schedule_bookings]
    WBA[wagon_booking_allocations]
  end

  C --> CR
  C --> CCS
  C --> CRS
  C --> CAS
  C --> CSIG
  C --> CDOC
  C --> B
  B --> BC
  B --> BDR
  B --> BRS
  B --> PAY
  B --> TSB
  B --> WBA

4.2 Target Lifecycle — Dual Execution Paths

flowchart TD
  subgraph contract [Contract Lifecycle — both paths]
    C1[DRAFT contract wizard unit rates only]
    C1 --> C2[SUBMITTED approval sign]
    C2 --> C3[FULLY_EXECUTED or CONTRACT_ACTIVE]
  end

  C3 --> Fork{Customs clearance service?}

  subgraph pathA [Path A — Transport only]
    A1[Customer creates booking wizard]
    A1 --> A2[Operation request batch]
    A2 --> A3[Customer pays]
    A3 --> A4[Allocation transit COMPLETED]
  end

  subgraph pathB [Path B — Transport with customs clearance]
    B1[Customer uploads clearance docs on contract]
    B1 --> B2[GL ET reviews approves customer docs]
    B2 --> B3[GL ET and GL DJ upload output docs milestones]
    B3 --> B4[GL ET creates booking all shipment data]
    B4 --> B5[Batch pool customer pays freight only]
    B5 --> B6[GL Ops milestones allocation transit]
    B6 --> B7[COMPLETED or EXPIRED payment]
  end

  Fork -->|No| A1
  Fork -->|Yes| B1

  B7 -->|ONE_TIME or GENERAL next cycle| B1
  A4 -->|GENERAL next shipment| A1

Path B summary (customs clearance contracts):

Step Actor Action
1 Customer Sign contract (Marketing counter-sign → FULLY_EXECUTED / CONTRACT_ACTIVE)
2 Customer Upload clearance documents on contract (BL, invoice, license, etc.) — no booking row yet
3 GL Ethiopia Review, approve, or query each document
4 GL Ethiopia / Djibouti Upload customs output docs, advance clearance milestones
5 GL Ethiopia Create booking — enter route, binding schedule date, container qty/numbers/VGM, bulk ton/item count, hazard/reefer counts
6 System / Ops Batch selection, PNR generation
7 Customer Pay freight (and duty/tax slips when advised during step 4)
8 GL / Ops Wagon allocation, loading, departure, arrival, remaining milestones
9 Customer Track shipment; no further data entry unless queried docs

Path A summary (transport-only contracts): Unchanged from prior design — customer uses booking wizard after contract sign; customer pays at batch gate.

4.2.1 Shipment Cycles on General Contracts (Customs)

For contract_kind = GENERAL with customs clearance, each new shipment repeats the Path B clearance cycle on the same contract:

CONTRACT_ACTIVE
  → AWAITING_CLEARANCE_DOCUMENTS   (customer uploads for this shipment)
  → CLEARANCE_UNDER_REVIEW         (GL ET reviews)
  → CLEARANCE_READY_FOR_BOOKING    (GL cleared to create booking)
  → [GL creates booking]           (contract may show ACTIVE_BOOKING_IN_PROGRESS)
  → [booking completes / expires]
  → CONTRACT_ACTIVE                (ready for next shipment cycle)

Use optional contract_clearance_cycles (see §5.16) to distinguish multiple clearance rounds on one contract.

4.3 What Stays Unchanged

  • freight.paymentsref_id remains booking UUID
  • freight.train_schedules, train_schedule_bookings
  • freight.wagon_booking_allocations, wagon_allocation_container_items, wagon_allocation_bulk_loads
  • BookingBatchService — day-level pool, payment window, EXPIRED on booking
  • RuleEngineService — rates, modifiers, weight limits (applied at booking with actual qty)
  • file_upload_settings seeder pattern — extend with phased GL codes
  • Contract PDF templates in src/contracts/templates/ — resolver reads contracts instead of bookings

4.4 What Is Deprecated (Phased)

Current Replacement
bookings.booking_type = GENERAL_CONTRACT contracts.contract_kind
booking_orders ledger Direct bookings.contract_id FK
contract_route_lines.quantity Removed — routes only
general-contract.service.getQuantityLines() pool math Validity window + optional soft limits only
POST /bookings for contract creation POST /contracts
Contract fields on bookings Migrated to contracts

5. Database Schema

5.1 Entity Relationship (Target)

erDiagram
  companies ||--o{ contracts : owns
  contracts ||--o{ contract_routes : has
  contracts ||--o{ contract_cargo_scope : defines_scope
  contracts ||--o{ contract_rate_snapshots : unit_rates
  contracts ||--o{ contract_approval_steps : has
  contracts ||--o{ contract_signatures : has
  contracts ||--o{ contract_review_notes : has
  contracts ||--o{ contract_document_review : pre_booking_clearance
  contracts ||--o{ contract_clearance_cycles : shipment_cycles
  contracts ||--o| contracts : renewal_of
  contracts ||--o{ bookings : spawns
  bookings ||--o{ booking_container : has
  bookings ||--o{ booking_container_units : container_numbers
  bookings ||--o{ booking_document_review : clearance
  bookings ||--o{ booking_rate_snapshot : computed_total
  bookings ||--o{ clearance_milestones : gl_tracking
  bookings ||--o{ payments : pays
  bookings ||--o| train_schedule_bookings : scheduled
  yards ||--o{ contract_routes : origin_dest
  service_types ||--o{ contracts : defines
  cargo_types ||--o{ contract_cargo_scope : commodity

5.2 freight.contracts (NEW)

Primary legal/commercial agreement. Replaces the contract-phase portion of bookings.

CREATE TABLE freight.contracts (
  id                          UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  reference                   VARCHAR(64) NOT NULL UNIQUE,  -- CTR-2026-00001

  -- Ownership
  company_id                  UUID REFERENCES freight.companies(id),
  company_profile_id          UUID REFERENCES freight.company_profiles(id),
  is_government               BOOLEAN NOT NULL DEFAULT FALSE,
  government_institution      VARCHAR(255),

  -- Classification
  contract_kind               VARCHAR(20) NOT NULL,  -- ONE_TIME | GENERAL
  renewal_of_id               UUID REFERENCES freight.contracts(id),
  trade_direction             VARCHAR(10) NOT NULL, -- IMPORT | EXPORT | DOMESTIC
  freight_type                VARCHAR(20) NOT NULL, -- CONTAINER | BULK

  -- Service & commercial
  service_type_id             UUID NOT NULL REFERENCES freight.service_types(id),
  payment_currency            VARCHAR(5) NOT NULL,  -- ETB | USD
  customs_clearing_enabled    BOOLEAN NOT NULL DEFAULT FALSE,
  customs_clearing_agent      VARCHAR(200),
  equipment_return            VARCHAR(20),          -- with_return | without_return

  -- First / last mile (copied from booking.entity.ts)
  first_mile_pickup_address   TEXT,
  first_mile_pickup_lat       NUMERIC(10,7),
  first_mile_pickup_lng       NUMERIC(10,7),
  last_mile_delivery_address  TEXT,
  last_mile_delivery_lat      NUMERIC(10,7),
  last_mile_delivery_lng      NUMERIC(10,7),

  -- Cargo flags at contract level (NOT derived from container type)
  is_hazardous                BOOLEAN NOT NULL DEFAULT FALSE,
  is_reefer                   BOOLEAN NOT NULL DEFAULT FALSE,

  -- Dates
  estimated_shipment_date     TIMESTAMPTZ,  -- non-binding estimate from wizard
  contract_validity_days      INT,
  contract_valid_from         TIMESTAMPTZ,
  contract_valid_until        TIMESTAMPTZ,
  expires_at                  TIMESTAMPTZ,  -- GENERAL ordering window end

  -- Workflow
  status                      VARCHAR(40) NOT NULL DEFAULT 'DRAFT',

  -- Pricing (unit rates only at contract phase)
  pricing_breakdown           JSONB,        -- displayMode: UNIT_RATES
  pricing_display_mode        VARCHAR(20) DEFAULT 'UNIT_RATES',

  -- Contract document generation
  contract_type               VARCHAR(20),  -- SPOT etc.
  contract_template_key       VARCHAR(128),
  contract_generated_at       TIMESTAMPTZ,
  contract_summary            TEXT,
  version_number              INT NOT NULL DEFAULT 1,
  financial_terms               JSONB,

  -- Signature timestamps (denormalized for queries)
  approved_by_staff_id        UUID,
  approved_by_staff_at        TIMESTAMPTZ,
  signed_by_director_id       UUID,
  signed_by_director_at       TIMESTAMPTZ,
  signed_by_ceo_id            UUID,
  signed_by_ceo_at            TIMESTAMPTZ,
  customer_signed_at          TIMESTAMPTZ,
  fully_executed_at           TIMESTAMPTZ,
  locked_at                   TIMESTAMPTZ,

  -- Audit
  created_at                  TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  updated_at                  TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  deleted_at                  TIMESTAMPTZ
);

CREATE INDEX idx_contracts_company ON freight.contracts(company_id);
CREATE INDEX idx_contracts_status ON freight.contracts(status);
CREATE INDEX idx_contracts_kind ON freight.contracts(contract_kind);
CREATE INDEX idx_contracts_valid_until ON freight.contracts(contract_valid_until);

Columns migrated FROM bookings: contract_validity_days, contract_valid_from, contract_valid_until, contract_type, contract_template_key, contract_generated_at, contract_summary, version_number, financial_terms, signature timestamp columns, expires_at (GENERAL only), is_hazardous, is_reefer, service/mile/customs fields, estimated_shipment_date.

Columns NOT on contracts: scheduled_date, payment_status, pnr_code, train_schedule_id, scheduling_status, priority_score, total_amount (booking computed total).

Columns ADDED for customs path on contracts:

Column Type Purpose
clearance_status varchar Pre-booking clearance gate: NOT_APPLICABLE, AWAITING_DOCUMENTS, DOCUMENTS_UNDER_REVIEW, CLEARANCE_READY_FOR_BOOKING, ACTIVE_SHIPMENT_IN_PROGRESS
clearance_cycle_number int Incremented per shipment cycle on GENERAL contracts

5.3 freight.contract_routes (EVOLVE contract_route_lines)

Defines allowed origin/destination pairs. No quantity.

CREATE TABLE freight.contract_routes (
  id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  contract_id         UUID NOT NULL REFERENCES freight.contracts(id) ON DELETE CASCADE,
  origin_yard_id      UUID NOT NULL REFERENCES freight.yards(id),
  destination_yard_id UUID NOT NULL REFERENCES freight.yards(id),
  km                  NUMERIC(10,2),  -- road billing distance; null for rail-only
  sort_order          SMALLINT NOT NULL DEFAULT 0,
  created_at          TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  updated_at          TIMESTAMPTZ NOT NULL DEFAULT NOW(),

  UNIQUE (contract_id, origin_yard_id, destination_yard_id)
);

CREATE INDEX idx_contract_routes_contract ON freight.contract_routes(contract_id);

Migration: ALTER TABLE freight.contract_route_lines RENAME TO contract_routes; ALTER ... RENAME COLUMN contract_booking_id TO contract_id; DROP COLUMN quantity; DROP COLUMN container_type_id;

Rules:

  • ONE_TIME: exactly 1 route row (enforce via application or CHECK)
  • GENERAL: 1..N route rows

5.4 freight.contract_cargo_scope (NEW)

Defines what cargo sizes/types are in scope without quantities.

CREATE TABLE freight.contract_cargo_scope (
  id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  contract_id         UUID NOT NULL REFERENCES freight.contracts(id) ON DELETE CASCADE,

  -- Container scope: one row per enabled size
  container_size      VARCHAR(10),  -- '20ft' | '40ft'; NULL for bulk

  -- Bulk scope
  cargo_type_id       UUID REFERENCES freight.cargo_types(id),
  cargo_free_text     VARCHAR(200),

  created_at          TIMESTAMPTZ NOT NULL DEFAULT NOW(),

  -- Container: unique per size per contract
  CONSTRAINT uq_contract_container_size
    UNIQUE NULLS NOT DISTINCT (contract_id, container_size)
);

CREATE INDEX idx_contract_cargo_scope_contract ON freight.contract_cargo_scope(contract_id);

Validation rules:

  • freight_type = CONTAINER: at least one row with container_size IN ('20ft','40ft'); cargo_type_id optional (commodity label for contract PDF)
  • freight_type = BULK: exactly one row with cargo_type_id required; container_size must be NULL
  • No quantity, vgm, or detailed container_type_id (no "20ft Reefer" — reefer is contracts.is_reefer boolean)

5.5 freight.contract_signatures (RENAME booking_contract_signatures)

CREATE TABLE freight.contract_signatures (
  id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  contract_id         UUID NOT NULL REFERENCES freight.contracts(id) ON DELETE CASCADE,
  role                VARCHAR(20) NOT NULL,  -- CUSTOMER | STAFF | DIRECTOR | CEO
  signer_display_name VARCHAR(255) NOT NULL,
  signature_file_id   UUID REFERENCES freight.files(id),
  consent_text        TEXT,
  signed_at           TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  created_at          TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX idx_contract_signatures_contract ON freight.contract_signatures(contract_id);

5.6 freight.contract_approval_steps (CLONE pattern from booking_approval_step)

Same structure as booking_approval_step but FK → contract_id. Instantiated at staff accept from approval_rules based on cargo classification (US-06: Standard Container → Line Staff + Director; Bulk → Directors + CEO).


5.7 freight.contract_rate_snapshots (CLONE from booking_rate_snapshot)

Frozen unit rates at contract submit time. One row per rate line with unit_of_measure (per_container, per_ton, per_item, per_km).

CREATE TABLE freight.contract_rate_snapshots (
  id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  contract_id         UUID NOT NULL REFERENCES freight.contracts(id) ON DELETE CASCADE,
  rate_id             UUID REFERENCES freight.rates(id),
  rate_code           VARCHAR(64) NOT NULL,
  description         VARCHAR(255),
  unit_price          NUMERIC(14,2) NOT NULL,
  unit_of_measure     VARCHAR(32) NOT NULL,
  currency            VARCHAR(5) NOT NULL,
  container_size      VARCHAR(10),   -- 20ft | 40ft when applicable
  is_surcharge        BOOLEAN DEFAULT FALSE,
  conditional_on      VARCHAR(32),   -- is_hazardous | is_reefer
  created_at          TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

5.8 freight.contract_review_notes (CLONE from booking_review_note)

Staff/customer notes during contract approval (CHANGES_REQUESTED, rejection reasons).


5.9 freight.bookings (MODIFY — shipment only)

ALTER TABLE freight.bookings
  ADD COLUMN contract_id UUID REFERENCES freight.contracts(id),
  ADD COLUMN contract_route_id UUID REFERENCES freight.contract_routes(id),
  ADD COLUMN created_by_role VARCHAR(20) DEFAULT 'CUSTOMER',  -- CUSTOMER | GL_ET | STAFF
  ADD COLUMN created_by_user_id UUID;

-- Partial unique: one active booking per ONE_TIME contract
CREATE UNIQUE INDEX uq_one_active_booking_per_one_time_contract
  ON freight.bookings (contract_id)
  WHERE status NOT IN ('EXPIRED', 'CANCELLED', 'COMPLETED', 'REJECTED')
    AND contract_id IN (
      SELECT id FROM freight.contracts WHERE contract_kind = 'ONE_TIME'
    );

Keep on bookings: reference, scheduled_date, status, scheduling_status, train_schedule_id, payment_status, pnr_code, total_amount, adjusted_total_amount, priority_score, payment_deadline, selected_for_batch_at, hold_started_at, hold_expires_at, wagons_required, consolidation_partner_id, operational timestamps.

Remove from bookings (after migration): booking_type, contract_validity_*, contract_template_key, contract_generated_at, contract_summary, previous_contract_id, expires_at, estimated_shipment_date, is_hazardous, is_reefer (move to contract; booking stores counts), primary origin_yard_id/destination_yard_id (use contract_route_id or denormalize at booking create).

Denormalize for performance: Copy origin_yard_id, destination_yard_id, trade_direction, freight_type onto booking at creation from contract + selected route.


5.10 freight.booking_container (ENRICH)

Existing table extended for per-unit detail at booking time.

ALTER TABLE freight.booking_container
  ADD COLUMN container_size VARCHAR(10),       -- 20ft | 40ft
  ADD COLUMN hazardous_quantity SMALLINT DEFAULT 0,
  ADD COLUMN reefer_quantity SMALLINT DEFAULT 0;

New child table for individual container numbers:

CREATE TABLE freight.booking_container_units (
  id                    UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  booking_container_id  UUID NOT NULL REFERENCES freight.booking_container(id) ON DELETE CASCADE,
  container_number      VARCHAR(64) NOT NULL,
  seal_number           VARCHAR(64),
  vgm_tons              NUMERIC(10,3) NOT NULL,
  is_hazardous          BOOLEAN DEFAULT FALSE,
  is_reefer             BOOLEAN DEFAULT FALSE,
  sort_order            SMALLINT NOT NULL DEFAULT 0,
  created_at            TIMESTAMPTZ NOT NULL DEFAULT NOW(),

  UNIQUE (booking_container_id, container_number)
);

Bulk bookings: use existing cargo_total_weight_vgm on booking for tons, or new item_count column when cargo_types.unit_of_measure = PER_ITEM. Add hazardous_quantity / bulk-specific counts on booking row or booking_bulk_lines if multiple bulk lines needed.


5.11 freight.booking_document_review (KEEP — per booking clearance)

No structural change. Add optional denormalized column:

ALTER TABLE freight.booking_document_review
  ADD COLUMN contract_id UUID REFERENCES freight.contracts(id);

Populated at insert from bookings.contract_id for GL read-only contract viewer queries.


5.12 freight.clearance_milestones (NEW — GL gap)

Tracks the 1823 milestones from GL PDF that are not represented by the current 3 clearance statuses.

CREATE TABLE freight.clearance_milestones (
  id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  booking_id          UUID NOT NULL REFERENCES freight.bookings(id) ON DELETE CASCADE,
  milestone_code      VARCHAR(64) NOT NULL,
  milestone_label     VARCHAR(255) NOT NULL,
  status              VARCHAR(20) NOT NULL DEFAULT 'PENDING',  -- PENDING | COMPLETED | SKIPPED
  owner_region        VARCHAR(5),   -- ET | DJ | OPS | CUST
  triggered_by_doc    BOOLEAN DEFAULT FALSE,
  triggered_at        TIMESTAMPTZ,
  triggered_by_user_id UUID,
  notes               TEXT,
  created_at          TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  updated_at          TIMESTAMPTZ NOT NULL DEFAULT NOW(),

  UNIQUE (booking_id, milestone_code)
);

CREATE INDEX idx_clearance_milestones_booking ON freight.clearance_milestones(booking_id);
CREATE INDEX idx_clearance_milestones_region ON freight.clearance_milestones(owner_region, status);

5.13 freight.clearance_document_phases (NEW — phased upload slots)

Extends file_upload_settings with phase metadata for GL document matrix.

CREATE TYPE freight.clearance_doc_phase AS ENUM (
  'CUSTOMER_INTAKE',      -- customer uploads before GL review
  'GL_ET_REVIEW',         -- ET GL internal
  'GL_DJ_COLLECTION',     -- DJ GL collects RO, DO
  'GL_ET_OUTPUT',         -- IM4, EX3, EX8, T1 uploaded by ET GL
  'CUSTOMER_DUTY',        -- duty/tax payment slips
  'GL_ET_POST_CLEARANCE', -- import release, T1 closure
  'GL_DJ_LOADING',        -- gatepass, loading docs
  'POST_TRANSIT'          -- demurrage slips, final declaration
);

-- Extend file_upload_fields with:
-- phase clearance_doc_phase NOT NULL
-- owner_region VARCHAR(5)  -- ET | DJ | CUST
-- trade_direction VARCHAR(10)
-- triggers_milestone_code VARCHAR(64)

5.14 Files / MinIO Resource Tagging

Resource Entity Examples
contracts Contract PDF, intake docs, post-sign clearance docs (Path B) Commercial invoice at wizard; BL, packing list after sign
bookings Post-booking ops docs, copied GL outputs VGM on booking row, IM4 linked after GL creates booking
contract_signatures Signature images PNG from signature pad

5.15 IAM Roles (GL Split)

Role code Permissions
edr_gl_ethiopia View contract, review pre-booking clearance docs on contract, upload IM4/IM5/EX3/EX8/T1, create booking with full shipment data, request wagon, assign station staff, post-booking ET milestones
edr_gl_djibouti View contract + booking (read-only rates), upload Release Order / DO / gatepass, loading milestones, damage reports, handoff triggers
edr_global_logistics Deprecated — split into ET/DJ; keep temporarily with union permissions for migration

Station routing (GL US-02): on booking create by GL, set bookings.gl_station_yard_id from contract route origin yard; queue filters by station assignment.


5.16 Pre-Booking Clearance (Path B — customs contracts)

When customs_clearing_enabled = true, clearance documents and review happen on the contract before any booking exists.

freight.contract_document_review

Same structure as booking_document_review, keyed on contract_id (and optionally clearance_cycle_id):

CREATE TABLE freight.contract_document_review (
  id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  contract_id         UUID NOT NULL REFERENCES freight.contracts(id) ON DELETE CASCADE,
  clearance_cycle_id  UUID REFERENCES freight.contract_clearance_cycles(id),
  setting_code        VARCHAR(128) NOT NULL,
  file_key            VARCHAR(128) NOT NULL,
  file_record_id      UUID,
  status              VARCHAR(20) NOT NULL DEFAULT 'PENDING',  -- PENDING | APPROVED | QUERIED
  note                TEXT,
  uploaded_by_role    VARCHAR(20) NOT NULL DEFAULT 'CUSTOMER',  -- CUSTOMER | GL_ET | GL_DJ
  reviewed_by_staff_id UUID,
  reviewed_at         TIMESTAMPTZ,
  created_at          TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  updated_at          TIMESTAMPTZ NOT NULL DEFAULT NOW(),

  UNIQUE (contract_id, clearance_cycle_id, setting_code, file_key)
);

CREATE INDEX idx_contract_doc_review_contract ON freight.contract_document_review(contract_id);
CREATE INDEX idx_contract_doc_review_status ON freight.contract_document_review(status);

Customer uploads attach here after contract sign. GL approves/queries here. When all required docs are APPROVED and GL output docs for the pre-booking phase are uploaded, contract moves to CLEARANCE_READY_FOR_BOOKING — GL may then create the booking.

freight.contract_clearance_cycles (GENERAL multi-shipment)

CREATE TABLE freight.contract_clearance_cycles (
  id                  UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  contract_id         UUID NOT NULL REFERENCES freight.contracts(id) ON DELETE CASCADE,
  cycle_number        INT NOT NULL,
  status              VARCHAR(40) NOT NULL DEFAULT 'AWAITING_DOCUMENTS',
  booking_id          UUID REFERENCES freight.bookings(id),  -- set when GL creates booking
  started_at          TIMESTAMPTZ NOT NULL DEFAULT NOW(),
  clearance_ready_at  TIMESTAMPTZ,
  completed_at        TIMESTAMPTZ,

  UNIQUE (contract_id, cycle_number)
);

ONE_TIME contracts use a single implicit cycle (cycle_number = 1, no separate row required — or always row 1).

freight.clearance_milestones — split ownership

Phase Attached to When
Pre-booking milestones (docs uploaded, declared, DO collected, etc. before wagon request) contract_id + clearance_cycle_id Before booking exists
Post-booking milestones (wagon allocated, loaded, departed, offloaded, etc.) booking_id After GL creates booking

Add nullable contract_id and clearance_cycle_id to clearance_milestones alongside booking_id.


6. Status Machines

6.1 Contract Statuses

DRAFT
  → SUBMITTED
  → PRICE_CHANGED_PENDING_CONFIRM  (if rates changed on resubmit)
  → CHANGES_REQUESTED
  → PENDING_APPROVAL               (after staff accept + validity window set)
  → APPROVED
  → APPROVED_PENDING_SIGNATURE
  → CONTRACT_READY                 (PDF generated)
  → SIGNED_CUSTOMER
  → FULLY_EXECUTED                 (ONE_TIME, transport-only: customer may book)
  → CONTRACT_ACTIVE                (GENERAL, transport-only: customer may book)
  -- Path B only (customs_clearing_enabled = true), after counter-sign:
  → AWAITING_CLEARANCE_DOCUMENTS   (customer uploads clearance docs — no booking yet)
  → CLEARANCE_UNDER_REVIEW         (GL ET reviews customer docs)
  → CLEARANCE_READY_FOR_BOOKING    (GL cleared to create booking; output docs in place)
  → ACTIVE_SHIPMENT_IN_PROGRESS    (GL created booking; cycle in ops pipeline)
  → CONTRACT_CLOSED                (GENERAL: validity ended or manually closed)
  → EXPIRED                        (contract_valid_until passed)
  → REJECTED
  → CANCELLED

For transport-only contracts (customs_clearing_enabled = false), counter-sign goes directly to FULLY_EXECUTED / CONTRACT_ACTIVE with clearance_status = NOT_APPLICABLE.

For customs contracts, counter-sign goes to AWAITING_CLEARANCE_DOCUMENTS with clearance_status = AWAITING_DOCUMENTS.

Renewal branch:

RENEWAL_DRAFT
  → RENEWAL_SUBMITTED
  → RENEWAL_PENDING_APPROVAL
  → AMENDMENTS_PROPOSED            (staff changed price/terms)
  → (customer Accept) → CONTRACT_READY
  → (customer Reject) → ARCHIVED

6.2 Contract Transition Table

From Action Actor To
DRAFT Submit Customer SUBMITTED
SUBMITTED Accept Line Staff PENDING_APPROVAL
SUBMITTED Request changes Line Staff CHANGES_REQUESTED
SUBMITTED Reject Line Staff REJECTED
PENDING_APPROVAL Approve step Line Staff / Director / CEO APPROVED (when all steps done)
APPROVED Generate contract System CONTRACT_READY
CONTRACT_READY Sign Customer SIGNED_CUSTOMER
SIGNED_CUSTOMER Counter-sign Staff/Director/CEO FULLY_EXECUTED / CONTRACT_ACTIVE (transport-only) or AWAITING_CLEARANCE_DOCUMENTS (customs)
AWAITING_CLEARANCE_DOCUMENTS Upload all required docs Customer CLEARANCE_UNDER_REVIEW (auto when complete)
CLEARANCE_UNDER_REVIEW Approve all docs GL ET CLEARANCE_READY_FOR_BOOKING
CLEARANCE_UNDER_REVIEW Query doc GL ET AWAITING_CLEARANCE_DOCUMENTS (customer re-upload)
CLEARANCE_READY_FOR_BOOKING Create booking GL ET ACTIVE_SHIPMENT_IN_PROGRESS
ACTIVE_SHIPMENT_IN_PROGRESS Booking completes/expired System CONTRACT_ACTIVE / FULLY_EXECUTED (ready for next cycle)
CONTRACT_ACTIVE Validity ends System cron CONTRACT_CLOSED or EXPIRED
FULLY_EXECUTED Validity ends System cron EXPIRED

Contract carries pre-booking clearance statuses (Path B). Batch and freight payment statuses remain on bookings only.

6.3 Booking Statuses (Shipment Pipeline)

Bookings are only created after contract signing. For Path B (customs), GL creates the booking only after contract clearance reaches CLEARANCE_READY_FOR_BOOKING.

Path A (transport-only) — customer creates booking; may start at OPERATION_REQUEST_PENDING or DRAFT:

[Customer creates booking]
  → OPERATION_REQUEST_PENDING    (customer picked binding scheduled_date)
  → OPERATION_CHANGES_REQUESTED
  → OPERATION_PRICE_PENDING_CONFIRM
  → FULLY_EXECUTED               (enters batch holding pool)
  → SELECTED_FOR_BATCH
  → EXPIRED                      (payment window — contract unaffected)
  → PNR_GENERATED / PAID
  → IN_TRANSIT
  → COMPLETED
  → CANCELLED / REJECTED
  → ROAD_DISPATCH_PENDING        (road services)

Path B (customs — GL creates booking) — booking skips pre-booking doc gates; GL has already entered schedule date and cargo details at creation:

[GL ET creates booking with full shipment data]
  → FULLY_EXECUTED or OPERATION_REQUEST_PENDING  (based on whether ops review required)
  → SELECTED_FOR_BATCH
  → EXPIRED                      (payment window — customer action: pay only)
  → PNR_GENERATED / PAID         (customer pays freight)
  → IN_TRANSIT                   (GL/Ops milestones continue)
  → COMPLETED

Post-booking GL milestones (wagon allocated, loaded, departed, offloaded, T1 closed, etc.) attach to booking_id via clearance_milestones as today.

Gate to create booking (Path A — customer):

contract.status IN ('FULLY_EXECUTED', 'CONTRACT_ACTIVE')
AND contract.customs_clearing_enabled = false
AND contract.contract_valid_until > NOW()
AND (contract_kind = 'GENERAL'
     OR NOT EXISTS active booking for this contract)

Gate to create booking (Path B — GL only):

contract.clearance_status = 'CLEARANCE_READY_FOR_BOOKING'
AND contract.customs_clearing_enabled = true
AND contract.contract_valid_until > NOW()
AND (contract_kind = 'GENERAL'
     OR NOT EXISTS active booking for this contract)
AND caller.role = 'edr_gl_ethiopia'

Where active booking = status NOT IN (EXPIRED, CANCELLED, COMPLETED, REJECTED).

6.4 Booking Creation Rules

Path Who creates booking UI Initial booking status
A — Transport only Customer Portal booking wizard (§8.1) OPERATION_REQUEST_PENDING or DRAFT
B — Customs clearance GL Ethiopia only Backoffice GL booking form (§8.2) FULLY_EXECUTED or OPERATION_REQUEST_PENDING (schedule date + cargo already filled by GL)

Customer portal (Path B): After contract sign, show Upload Clearance Documents on contract detail — not a booking wizard. After GL creates booking, customer sees shipment on /bookings/:id with Pay as primary action when batch-selected.

No customer confirmation step before GL booking goes live — GL enters authoritative shipment data; customer is notified when booking is created and when payment is due.


7. Contract Wizard Specification

Route: /contracts/new
Replaces: /bookings/new for contract creation

Step 0 — Operation Type

Field Type Required Notes
operationType enum Yes import, export, intercity, import_ff, export_ff
Gated by company profile types (existing logic from step0-operation-type.tsx)

Maps to contracts.trade_direction: import/export → IMPORT/EXPORT; intercity → DOMESTIC.


Step 1 — Contract Type

Field Type Required Notes
contractKind enum Yes one_time → ONE_TIME, general_contract → GENERAL
renewalMode enum No new | renewal
renewalOfReference combobox If renewal Search prior contracts; pre-fill service, routes, cargo scope

Maps to contracts.contract_kind, contracts.renewal_of_id.


Step 2 — Service Type & Currency

Field Type Required Notes
serviceTypeId select Yes From reference data; canBeBookedAlone
paymentCurrency enum Yes USD | ETB
firstMile.* toggle + map If service includes first mile Address, lat, lng
lastMile.* toggle + map If service includes last mile Address, lat, lng
equipmentReturn enum If last mile with_return | without_return
customsClearingAgent text Optional When customs not bundled

Auto-set customs_clearing_enabled from serviceType.includesCustoms.


Step 3 — Cargo Scope (NO quantities)

Container freight:

Field Type Required Notes
enabledContainerSizes checkbox[] Yes, min 1 20ft, 40ft — creates contract_cargo_scope rows
cargoCommodityId select Optional Commodity label for contract PDF (Coffee, etc.)

Do NOT collect: quantity, VGM, container type detail (dry/reefer/high-cube), shipping line.

Bulk freight:

Field Type Required Notes
cargoTypePath cascader Yes Bulk group → commodity (Coffee, Fertilizer, …)
cargoFreeText text If "Others"

Do NOT collect: tonnage, item count, weight.

Flags (both):

Field Type Notes
isHazardous toggle Sets contracts.is_hazardous; surcharge shown as unit rate
isRefrigerated toggle Sets contracts.is_reefer; bulk + container

Step 4 — Route & Estimated Date

Field Type Required Notes
originYardId select Yes Filtered by operation type
destinationYardId select Yes
extraRoutes[] repeater GENERAL only Additional origin/destination pairs → contract_routes
estimatedShipmentDate date Yes Non-binding; maps to contracts.estimated_shipment_date

Do NOT collect: scheduledDate (binding date is at booking step).


Step 5 — Contract Intake Documents

Field Type Required Notes
documents file map Per setting Attach to contract_id via FilesService resource=contracts

Setting codes (new or repurpose):

  • contract_intake_documents_import_container
  • contract_intake_documents_export_bulk
  • etc. (mirror clearance pattern by direction × freight × customs)

These are framework / commercial documents at contract submission — distinct from post-sign clearance documents (BL, import license, etc.) uploaded on /contracts/:id/clearance after signing (Path B).


Step 6 — Review & Submit

Field Type Notes
notes textarea Optional special instructions
Pricing panel read-only Unit rates only — see §9
Actions buttons Save draft, Generate price, Submit

Post-submit: existing approval + contract sign flow on /contracts/:id/contract.

Post-sign (Path B only): Customer is routed to /contracts/:id/clearance to upload clearance documents — not to a booking wizard.


8. Booking Creation Specification

Booking creation is split by execution path (see §4.2). Only Path A (transport-only) uses the customer portal booking wizard. Path B (customs clearance) uses a GL-only backoffice form — the customer never enters shipment data.


8.1 Customer Booking Wizard (Path A — transport only)

Route: /contracts/:contractId/bookings/new
Actor: Customer
Preconditions:

  • contract.customs_clearing_enabled = false
  • contract.status IN ('FULLY_EXECUTED', 'CONTRACT_ACTIVE')
  • Contract validity not expired
  • ONE_TIME: no other active booking

Step 1 — Route Selection

Field Type Required Notes
contractRouteId select If GENERAL multi-route From contract_routes; ONE_TIME auto-selected

Denormalize origin/destination onto booking.

Step 2 — Schedule Date

Field Type Required Notes
scheduledDate date Yes Binding day; validated against open train departures

Step 3 — Cargo Details

Container (for each enabled size from contract_cargo_scope):

Field Type Required
containers[].size 20ft/40ft Yes
containers[].quantity int ≥ 1 Yes
containers[].units[].containerNumber text Yes, one per container
containers[].units[].sealNumber text Optional
containers[].units[].vgmTons decimal Yes, per unit
containers[].hazardousQuantity int If contract.is_hazardous
containers[].reeferQuantity int If contract.is_reefer

Bulk:

Field Type Required
cargoWeightTons OR itemCount decimal/int Yes
hazardousQuantity int If hazardous

Step 4 — Review & Submit

  • Compute total from contract unit rates × quantities
  • Submit → OPERATION_REQUEST_PENDING → ops review → batch → customer pays

8.2 GL Booking Form (Path B — customs clearance)

Route: /dashboard/contracts/:contractId/create-booking (backoffice)
Actor: edr_gl_ethiopia only
Preconditions:

  • contract.customs_clearing_enabled = true
  • contract.clearance_status = 'CLEARANCE_READY_FOR_BOOKING'
  • Contract validity not expired; ONE_TIME has no active booking

GL enters all shipment fields that the customer would enter in §8.1:

Field Type Required Notes
contractRouteId select If GENERAL multi-route
scheduledDate date Yes Binding schedule day
containers[].size 20ft/40ft If container contract From contract scope
containers[].quantity int Yes
containers[].units[].containerNumber text Yes GL enters on behalf of customer
containers[].units[].sealNumber text Optional
containers[].units[].vgmTons decimal Yes
containers[].hazardousQuantity int If hazardous
containers[].reeferQuantity int If reefer
cargoWeightTons / itemCount decimal/int If bulk Ton or item per cargo type
notes text Optional Internal GL notes

On submit:

  1. Create bookings row with created_by_role = 'GL_ET', created_by_user_id = gl staff
  2. Compute total_amount from contract unit rates × GL-entered quantities
  3. Set booking status → FULLY_EXECUTED or OPERATION_REQUEST_PENDING
  4. Set contract → ACTIVE_SHIPMENT_IN_PROGRESS; link contract_clearance_cycles.booking_id
  5. Notify customer: "Your shipment has been booked by Global Logistics — you will be notified when payment is due"
  6. Continue post-booking pipeline (batch, milestones) — customer action = pay only

Customer portal: No booking wizard. Contract detail shows clearance upload; after GL books, /bookings/:id shows read-only shipment summary + Pay button.


8.3 Customer Post-Sign Clearance Upload (Path B)

Route: /contracts/:id/clearance (portal)
Actor: Customer
When: After contract counter-sign → AWAITING_CLEARANCE_DOCUMENTS

Field Type Notes
Clearance documents file map Per contract_clearance_* file-upload setting (import/export × container/bulk)
Ad-hoc documents name + file Optional additional regulatory docs

Documents stored in contract_document_reviewnot on a booking row (booking does not exist yet).

After all required uploads: contract auto-transitions to CLEARANCE_UNDER_REVIEW; GL ET queue receives the contract.


9. Pricing Model

9.1 Contract Phase — Unit Rate Display

Example UI (matches stakeholder requirement):

Pricing Schedule (estimated — final amount calculated at booking)

  20ft container ...................... 2,000 ETB / container
  40ft container ........................ 40,000 ETB / container
  Hazardous surcharge ................. 3,000 ETB / container  (if enabled)
  Reefer surcharge .................... 5,000 ETB / container  (if enabled)
  Coffee (bulk) ....................... 1,500 ETB / ton

  No total amount shown — quantities unknown at contract stage.

JSON storage (contracts.pricing_breakdown):

{
  "displayMode": "UNIT_RATES",
  "currency": "ETB",
  "lineItems": [
    {
      "code": "CONTAINER_20FT",
      "label": "20ft container",
      "unit": "per_container",
      "unitPrice": 2000,
      "containerSize": "20ft"
    },
    {
      "code": "CONTAINER_40FT",
      "label": "40ft container",
      "unit": "per_container",
      "unitPrice": 40000,
      "containerSize": "40ft"
    },
    {
      "code": "HAZARD_SURCHARGE",
      "label": "Hazardous surcharge",
      "unit": "per_container",
      "unitPrice": 3000,
      "conditionalOn": "is_hazardous"
    },
    {
      "code": "REEFER_SURCHARGE",
      "label": "Reefer surcharge",
      "unit": "per_container",
      "unitPrice": 5000,
      "conditionalOn": "is_reefer"
    },
    {
      "code": "BULK_COFFEE",
      "label": "Coffee",
      "unit": "per_ton",
      "unitPrice": 1500,
      "cargoTypeCode": "COFFEE"
    }
  ]
}

Contract PDF (US-07) embeds unit rate schedule, not totals.

9.2 Booking Phase — Total Calculation

booking_total =
  Σ (container_qty[size] × unit_rate[size])
  + Σ (hazardous_qty × hazard_unit_rate)
  + Σ (reefer_qty × reefer_unit_rate)
  + bulk_qty × bulk_unit_rate
  + first_mile_km × per_km_rate (if applicable)
  + last_mile_km × per_km_rate (if applicable)
  + overweight surcharges (when VGM entered per US-11)

Algorithm reuses BookingPricingService.computePriceForBooking() but inputs come from booking quantities + frozen contract_rate_snapshots instead of contract-time qty.

Store result in bookings.total_amount and booking_rate_snapshot rows.

9.3 Quotation Approval (US-04)

At contract submit, customer sees unit-rate quotation → Approve Quotation → advances to approval/signing. Reject → REJECTED / archived.

This replaces the current total-amount confirmation modal in NewBookingPage.tsx.


10. Renewal and Expiry Rules

10.1 Contract Validity Expiry

Event Contract behavior Booking behavior
contract_valid_until passed Status → EXPIRED or CONTRACT_CLOSED; no new bookings In-flight bookings continue to completion
Customer requests renewal RENEWAL_DRAFT linked via renewal_of_id N/A
Staff approves without changes CONTRACT_READY → sign N/A
Staff proposes amendments AMENDMENTS_PROPOSED; customer accept/reject N/A

Renewal UI: extend current step1-contract-type.tsx renewal combobox to search contracts table.

10.2 Payment Window Expiry (Booking Only)

Event Contract Booking
Payment deadline passes (BookingBatchService) Unchanged — stays FULLY_EXECUTED or CONTRACT_ACTIVE Status → EXPIRED; wagons released
Customer re-books (Path A) Same contract Customer creates new booking
Customer re-ships (Path B) Returns to AWAITING_CLEARANCE_DOCUMENTS for new cycle GL creates new booking after clearance

UX Path A: Contract detail shows "Create new booking" when prior booking expired.

UX Path B: Contract detail shows "Upload clearance documents" for next shipment cycle; customer never sees a booking creation form.

10.3 General Contract Ordering Window

contracts.expires_at (ordering window, from general_contract_period setting) is separate from contract_valid_until (legal validity). Both must be open for new bookings.

10.4 ONE_TIME Single Active Booking Rule

Enforced by partial unique index (§5.9). Terminal statuses free the slot:

  • EXPIRED (payment)
  • CANCELLED
  • COMPLETED
  • REJECTED

11. Global Logistics — Import Unimodal

11.1 Actors

Actor Role code Primary responsibilities
Customer Portal user Path B: Upload clearance docs on contract after sign; pay freight PNR and duty/tax slips when advised. Does not create bookings or enter shipment data.
GL Ethiopia edr_gl_ethiopia Review pre-booking clearance docs on contract; upload IM4/IM5/EX3/EX8/T1; create booking with full shipment data; wagon request; post-booking milestones
GL Djibouti edr_gl_djibouti DO collection, gatepass, loading milestones, damage reports, departure
Operations edr_operations Wagon allocation, train dispatch, marshalling
Port/Terminal edr_terminal Arrival, offload, yard assignment

11.2 Import Document Matrix

# Customer uploads GL Ethiopia uploads GL Djibouti uploads Phase
1 Commercial Invoice* contract_id — CUSTOMER_INTAKE (post-sign)
2 Packing List* contract_id
3 Certificate of Origin* contract_id
4 Bank Permit / Franco Valuta* contract_id
5 Bill of Lading / SWB* contract_id
6 Power of Attorney* contract_id
7 Import License* contract_id
8 Other regulatory docs contract_id
9 Import Declaration (IM4/IM5)* GL_ET_OUTPUT
10 Transit permit screenshot GL_ET_OUTPUT
11 Duty/tax payment slip* CUSTOMER_DUTY
12 Delivery Order GL_DJ_COLLECTION
13 T1 transport document GL_DJ_LOADING
14 Import release* GL_ET_POST_CLEARANCE
15 Full out Interchange GL_DJ_LOADING
16 Damage report photos GL_DJ_LOADING (conditional)
17 Storage/demurrage payment slip POST_TRANSIT

*Mandatory per GL PDF Import Documents table.

Document ownership (Path B):

  • Rows 18, 11, 17 (customer uploads): attach to contract_id via contract_document_review before booking exists
  • Rows 910, 14 (GL ET output): attach to contract_id during pre-booking clearance, copied/referenced on booking_id when GL creates booking
  • Rows 1213, 1516 (GL DJ): may attach to contract_id pre-booking or booking_id post-booking depending on milestone timing (see §5.16)
  • After GL creates booking, post-booking milestones (wagon+, rows 923 in §11.3) track on booking_id

Contract terms visible read-only to GL throughout; customer pays freight at milestone 10 only.

11.3 Import Milestone Sequence

# Milestone Owner Triggered by doc upload?
1 Import Documents Uploaded Customer No
2 Pending Document Review GL-ET Yes (all customer docs uploaded)
3 Documents Approved GL-ET No
4 Under Customs Clearance GL-ET No
5 Declared GL-ET Yes (IM4/IM5 uploaded)
6 Duty and Taxes Advised GL-ET No
7 Duty and Tax Paid Customer Yes (payment slip)
8 DO Collected GL-DJ Yes (DO uploaded)
9 Wagon Allocation Requested GL-ET No
10 Payment Settled (freight) Customer Yes (PNR paid)
11 Wagon Allocated Operations No
12 Gatepass Granted GL-DJ No
13 Ready for Loading GL-DJ No
14 Loaded GL-DJ No
15 Departed from Djibouti GL-DJ No
16 Arrived at Port in Ethiopia Port/Terminal No
17 Offloaded Port/Terminal No
18 T1 Closed GL-ET No
19 Risk Assigned (GREEN/YELLOW/RED) GL-ET No
20 Import Release Granted GL-ET Yes (release doc)
21 Import Process Completed GL-ET Yes
22 Storage Invoice Raised System No (demurrage engine)
23 Exit Note Generated Port Yes (storage paid)

Map to clearance_milestones.milestone_code enum constants.

Milestone phase split (Path B):

Milestones Phase Attached to
18 Pre-booking clearance contract_idbefore booking exists
8a GL creates booking (schedule, qty, container numbers entered by GL ET) booking_id created
923 Post-booking operations booking_id — batch, payment (customer), allocation, transit

Customer actions in this table: #1 upload, #7 duty slip, #10 pay freight, #17 storage slip. All other steps are GL/Ops/System.

11.4 Import Sequence Diagram (Path B — customs clearance)

sequenceDiagram
  participant Cust as Customer
  participant CTR as Contract
  participant BK as Booking
  participant GLET as GL_Ethiopia
  participant GLDJ as GL_Djibouti
  participant Ops as Operations

  Cust->>CTR: Sign contract
  Cust->>CTR: Upload clearance docs on contract
  CTR->>GLET: CLEARANCE_UNDER_REVIEW
  GLET->>CTR: Approve or query per document
  GLET->>CTR: Upload IM4 IM5 output docs
  GLET->>Cust: Duty and taxes advised
  Cust->>CTR: Upload duty payment slip
  GLDJ->>CTR: Upload DO collected
  Note over GLET,CTR: CLEARANCE_READY_FOR_BOOKING
  GLET->>BK: GL creates booking schedule qty container numbers
  GLET->>Ops: Wagon allocation requested
  Ops->>Cust: PNR pending payment
  Cust->>BK: Pay freight only
  Ops->>BK: Wagon allocated
  GLDJ->>BK: Gatepass loading loaded
  GLDJ->>BK: Departed from Djibouti
  Ops->>BK: Arrived offloaded Ethiopia
  GLET->>BK: T1 closed import release granted

11.5 ET ↔ DJ Handoff (Import US-09)

When milestone DEPARTED_FROM_DJIBOUTI completes:

  • Primary owner → GL Ethiopia + Operations (read-only copy for GL Djibouti)
  • Notifications: ATD to all; ETA to GL-ET and customer

12. Global Logistics — Export Unimodal

12.1 Export Document Matrix

# Customer uploads GL Ethiopia uploads GL Djibouti uploads
1 Booking Confirmation*
2 Verified Gross Mass (VGM)*
3 Shipping Instructions*
4 Train Waybill*
5 Commercial Invoice*
6 Packing List*
7 Bank Permit*
8 Sales Contract*
9 Power of Attorney*
10 Export License*
11 Export declaration docs
12 Export Release
13 T1 transport document
14 Release Order*
15 Full in Interchange
16 Final Declaration

Commodity-specific (Coffee, Teff, etc.): additional regulatory docs per GL PDF US-03.

Path B (customs export): Customer uploads rows 110 on contract_id after contract sign (same pre-booking pattern as import §11.2). GL ET creates booking after CLEARANCE_READY_FOR_BOOKING. VGM and container numbers are entered by GL ET at booking creation, not by the customer at contract sign.

12.2 Export Milestone Sequence

# Milestone Owner Doc triggered?
1 Export Documents Uploaded Customer No
2 Pending Document Review GL-ET Yes
3 Documents Approved GL-ET No
4 Release Order Secured GL-DJ Yes (RO upload)
5 Under Customs Clearance GL-ET No
6 Declared GL-ET Yes (EX3/EX8)
7 Export Released GL-ET No
8 Wagon Requested GL-ET No
9 Pending Payment Customer No
10 Payment Settled Customer Yes
11 Wagon Allocated Operations No
12 Cargo Arrived Port/Terminal No
13 Ready for Loading Port/Terminal No
14 Loaded Port/Terminal No
15 Departed to Djibouti Port/Terminal No
16 Arrived at Djibouti GL-DJ No
17 Gatepass Granted GL-DJ No
18 Offloaded GL-DJ Yes (Full-in Interchange)

12.3 Export ET ↔ DJ Handoff (US-09 Export)

When DEPARTED_TO_DJIBOUTI from Ethiopia:

  • Primary owner → GL Djibouti
  • GL Ethiopia receives read-only copy + ATD notification

13. Customs Clearance Path — GL-Owned Execution

Applies when: customs_clearing_enabled = true (transport with Global Logistics / customs clearance service).

This is the default execution path for import, export, and intercity contracts that bundle customs clearance. The customer never creates a booking or enters shipment quantities, container numbers, or schedule dates.

13.1 End-to-End Flow

flowchart TD
  A[Contract signed FULLY_EXECUTED or CONTRACT_ACTIVE] --> B[Customer uploads clearance docs on contract]
  B --> C[CLEARANCE_UNDER_REVIEW]
  C --> D[GL ET approves or queries each document]
  D --> E[GL ET and GL DJ upload output docs milestones]
  E --> F[CLEARANCE_READY_FOR_BOOKING]
  F --> G[GL ET creates booking — enters ALL shipment data]
  G --> H[Batch pool PNR generated]
  H --> I[Customer pays freight]
  I --> J[GL Ops milestones wagon allocation transit]
  J --> K[COMPLETED or EXPIRED payment]
  K -->|GENERAL next shipment| B

13.2 Customer Responsibilities (Path B only)

Action When Where
Sign contract After Marketing approval /contracts/:id/contract
Upload clearance documents After contract counter-sign /contracts/:id/clearance
Re-upload queried documents When GL queries a specific doc /contracts/:id/clearance
Upload duty/tax payment slip When GL advises amount /contracts/:id/clearance
Pay freight PNR When booking selected for batch /bookings/:idPay button
Upload storage/demurrage slip If applicable post-transit /bookings/:id
Track shipment Read-only /bookings/:id, /tracking

Customer does NOT: create booking, pick schedule date, enter container numbers, enter quantities, enter VGM, or confirm GL-entered booking data.

13.3 GL Ethiopia Responsibilities

Action When
Review/approve/query customer clearance docs CLEARANCE_UNDER_REVIEW on contract
Upload IM4/IM5/EX3/EX8/T1 and other ET output docs During pre-booking clearance
Request wagon / advance clearance milestones Before or after booking create per import/export matrix
Create booking with route, scheduledDate, container qty/numbers/VGM, bulk ton/item count, hazard/reefer counts When CLEARANCE_READY_FOR_BOOKING
Continue post-booking ET milestones After booking exists

13.4 API (Path B)

# Customer — post-sign clearance on contract (no booking yet)
GET  /contracts/:id/clearance
POST /contracts/:id/clearance/documents
POST /contracts/:id/clearance/adhoc-documents

# GL ET — review pre-booking clearance
GET  /contracts/clearance/queue?region=ET
POST /contracts/:id/clearance/review          { fileKey, status, note }
POST /contracts/:id/clearance/output-documents
POST /contracts/:id/clearance/finalize        → CLEARANCE_READY_FOR_BOOKING

# GL ET — create booking (exclusive; replaces customer POST)
POST /contracts/:contractId/bookings
  Authorization: edr_gl_ethiopia
  Body: { contractRouteId, scheduledDate, containers[], bulkLines[], notes }
  Response: booking { id, reference, totalAmount, status }
  Side effects:
    - contract.status → ACTIVE_SHIPMENT_IN_PROGRESS
    - contract_clearance_cycles.booking_id set
    - customer notified (email + in-app)

# Customer — payment only (unchanged)
POST /bookings/:id/payment/pay

Removed: POST /bookings/:id/customer/confirm-gl-booking — GL booking is authoritative; no customer confirmation gate.

13.5 Field Parity

GL booking form (§8.2) collects the same shipment fields the customer would enter in Path A (§8.1). GL staff source container numbers and VGM from physical documents and port data — not from customer portal entry.


14. Gap Analysis Matrix

# Topic PDF / Stakeholder requirement Current implementation Target design Gap severity
1 Contract vs booking separation US-03, US-07, US-08; ITLMS §1 "Upon contract signature, invite booking" Single bookings row contracts + bookings Critical
2 Contract cargo = scope only US-03 Case 2: no weight/qty at contract; 20ft/40ft only Wizard step 3 collects qty, VGM contract_cargo_scope Critical
3 Unit-rate contract pricing US-04; stakeholder step 8 Total amount from qty at intake UNIT_RATES display mode Critical
4 Hazard/reefer at contract US-03 Case 3: billing flags at contract Route step on booking; reefer from container type contracts.is_hazardous/is_reefer; counts at booking High
5 Binding vs estimated date US-03 one-time: departure at contract; stakeholder: estimate only at contract estimatedShipmentDate + scheduledDate both on booking Estimate on contract; binding on booking High
6 Contract intake docs Stakeholder step 7 → contract Wizard docs + draft booking docs mixed resource=contracts intake settings High
7 Pre-booking clearance (Path B) Stakeholder: sign → upload docs → GL approves → GL books Clearance on booking after booking create Customer docs on contract_id before booking; GL creates booking Critical
8 General contract qty pool Stakeholder: no qty at contract contract_route_lines.quantity + pool math Remove quantity; validity-only Critical
9 ONE_TIME re-book after payment expiry Stakeholder requirement EXPIRED on booking; no contract link for re-book contract_id FK + partial unique index High
10 Contract renewal/amendment US-03 renewal paths previous_contract_id on booking contracts.renewal_of_id + amendment statuses High
11 GL ET vs DJ split GL PDF throughout Single edr_global_logistics role edr_gl_ethiopia, edr_gl_djibouti Critical
12 GL milestones 1823 milestones per direction 3 clearance statuses clearance_milestones table Critical
13 Phased document uploads GL PDF: before pay / after pay / after data Flat clearance doc list clearance_document_phases High
14 Station routing GL US-02 Not implemented gl_station_yard_id + queue routing High
15 GL-owned booking (Path B) Stakeholder + GL US-06: GL enters all shipment data; customer pays only Customer-only booking creation GL ET exclusive POST /contracts/:id/bookings; no customer booking wizard Critical
16 Djibouti Release Order Export milestone 4; unlocks ET clearance Single clearance_output_* code DJ upload slot + milestone trigger High
17 Duty/tax payment slip Import milestone 7 Not modeled CUSTOMER_DUTY phase slot Medium
18 Damage/exception reporting Import US-07 AC1 Not implemented Incident form on GL-DJ loading Medium
19 Demurrage auto-calc GL US-12; ITLMS §8 Not in freight-api Future — finance module Low (out of scope)
20 Multimodal GL GL PDF multimodal section Not implemented Future phase Low (out of scope)
21 MSRN reference prefix GL V01: MSRNI/MSRNE Booking reference format differs Optional: align reference prefixes Low
22 Approval hierarchy on contract US-06 container vs bulk routing On booking today Move to contract_approval_steps Medium
23 Container numbers at booking ITLMS §1; stakeholder booking step container_number nullable on line; not per-unit booking_container_units High
24 Domestic/intercity clearance No customs gate clearance.util returns null for DOMESTIC Unchanged None
25 Batch/scheduling/allocation ITLMS §1 priority, wagon calc Implemented on booking Unchanged — operates on booking after GL/customer creates it None
26 Customer payment-only in customs path Stakeholder: customer pays; GL does rest Customer enters all data + pays Customer: upload docs + pay PNR/duty only Critical

15. API Endpoint Mapping

15.1 New Contract Endpoints

Method Path Replaces Notes
POST /contracts POST /bookings Multipart; creates contract + routes + cargo scope
PATCH /contracts/:id PATCH /bookings/:id Customer editable in DRAFT, CHANGES_REQUESTED
GET /contracts GET /bookings?bookingType=GENERAL_CONTRACT List/filter
GET /contracts/my Portal scoped list
GET /contracts/:id Contract detail Includes routes, cargo scope, unit rates
POST /contracts/:id/documents POST /bookings/:id/documents Intake docs
POST /contracts/:id/generate-price Same on booking Returns unit rates
POST /contracts/:id/submit Same on booking Freezes contract_rate_snapshots
POST /contracts/:id/confirm-submit Same Price change confirm
POST /contracts/:id/staff/accept Same Sets validity window
POST /contracts/:id/staff/request-changes Same
POST /contracts/:id/staff/reject Same
POST /contracts/:id/approval-steps/:stepId/approve Same
POST /contracts/:id/contract/generate Same PDF from contract
GET /contracts/:id/contract/view Same
POST /contracts/:id/contract/sign Same
POST /contracts/:id/renew Partial via previousContractRef Renewal workflow
POST /contracts/:id/renew/accept-amendments New Customer accepts
POST /contracts/:id/renew/reject-amendments New Customer rejects

15.2 Booking-under-Contract Endpoints

Method Path Actor Notes
POST /contracts/:contractId/bookings Customer (Path A only) customs_clearing_enabled = false
POST /contracts/:contractId/bookings GL ET (Path B only) clearance_status = CLEARANCE_READY_FOR_BOOKING
GET /contracts/:contractId/bookings Customer / staff List shipments under contract

15.2.1 Contract Pre-Booking Clearance Endpoints (Path B)

Method Path Actor Notes
GET /contracts/:id/clearance Customer / GL ET Document grid on contract (no booking)
POST /contracts/:id/clearance/documents Customer Upload clearance docs after sign
POST /contracts/:id/clearance/review GL ET Approve/query per fileKey
POST /contracts/:id/clearance/output-documents GL ET / GL DJ GL output uploads pre-booking
POST /contracts/:id/clearance/finalize GL ET CLEARANCE_READY_FOR_BOOKING
GET /contracts/clearance/queue GL ET Contracts awaiting review (not bookings)

15.3 Unchanged Booking Endpoints (operate on shipment)

All clearance, operation, payment, batch, transit endpoints remain on /bookings/:id/*:

  • /bookings/:id/clearance/*
  • /bookings/:id/clearance/proceed
  • /bookings/:id/operation/review
  • /bookings/:id/payment/pay
  • /bookings/:id/operations/start-transit
  • /bookings/:id/operations/complete

15.4 New GL Endpoints

Method Path Actor
GET /contracts/clearance/queue?region=ET GL ET — contracts awaiting doc review
GET /bookings/clearance/queue?region=ET|DJ GL ET/DJ — post-booking milestone queue
POST /bookings/:id/clearance/milestones/:code/complete GL/Ops/Terminal
POST /bookings/:id/clearance/incidents GL-DJ damage report
POST /bookings/:id/gl/station-assign GL station manager
GET /contracts/:id/read-only GL read-only contract view

15.5 Deprecated Endpoints (Phase 4)

Endpoint Replacement
POST /bookings (contract creation) POST /contracts
POST /booking-orders POST /contracts/:id/bookings
GET /booking-orders/contract/:id/pool Removed — no qty pool

16. Frontend Route & Component Mapping

16.1 Portal Routes (Target)

Current Target Action
/bookings/new /contracts/new New contract wizard (§7)
/bookings/:id (contract phase) /contracts/:id Contract detail + sign
/bookings/:id/contract /contracts/:id/contract Move BookingContractPageContractPage
/contracts /contracts Query contracts table (both kinds)
/contracts/:id /contracts/:id Show unit rates, validity, booking list
PlaceOrderDialog /contracts/:id/bookings/new Path A only — customer booking wizard (§8.1)
New /contracts/:id/clearance Path B — customer uploads clearance docs after sign
/contracts/:id /contracts/:id Path B: clearance CTA; Path A: "New booking" CTA
/bookings/:id (shipment) /bookings/:id Shipment detail — Pay (Path B primary action), track
/bookings /bookings My shipments list

16.2 Portal Component Changes

File Change
new-booking-form/schema.ts Split into new-contract-form/schema.ts + new-shipment-form/schema.ts
step5-cargo-details.tsx Contract: sizes/commodity only; Booking: qty + container numbers
step4-route.tsx Contract: estimated date only; Booking: binding scheduled date
step-documents.tsx Contract: intake docs → contract API
NewBookingPage.tsx Rename/refactor → NewContractPage.tsx
New NewShipmentPage.tsx under contract
ContractDetailPage.tsx Path A: "New booking" CTA; Path B: "Upload clearance documents" + read-only booking list after GL books
New ContractClearanceFlow.tsx
ClearanceFlow.tsx Path A post-booking only; Path B uses ContractClearanceFlow until booking exists
BookingDetailPage/constants.ts Split PROGRESS_STAGES into contract vs booking

16.3 Backoffice Routes (Target)

Current Target
/dashboard/booking-requests /dashboard/contract-requests (contract approval queue)
/dashboard/booking-requests/:id /dashboard/contract-requests/:id
/dashboard/clearance /dashboard/clearance/ethiopia + /dashboard/clearance/djibouti (post-booking)
(new) /dashboard/contracts/clearance — GL ET queue for contracts in CLEARANCE_UNDER_REVIEW
(new) /dashboard/contracts/:id/create-booking — GL ET booking form (§8.2); exclusive Path B booking creation
(new) /dashboard/bookings/:id/milestones — GL post-booking milestone panel

16.4 Backoffice Component Changes

File Change
DocumentClearanceListPage.tsx Filter by region=ET|DJ; show milestone progress
ClearanceReviewSection.tsx Phase-grouped document tabs
New ClearanceMilestoneTimeline.tsx
New ContractClearanceReviewSection.tsx
New GlCreateBookingForm.tsx
BookingRequestsPage.tsx ContractRequestsPage.tsx

17. Migration Plan

Phase 1 — Additive Schema (no breaking changes)

  1. Create contracts, contract_routes, contract_cargo_scope, contract_signatures, contract_approval_steps, contract_rate_snapshots, contract_review_notes, contract_document_review, contract_clearance_cycles.
  2. Create clearance_milestones, extend file_upload_fields with phase columns.
  3. Add bookings.contract_id, bookings.contract_route_id, booking_container_units.
  4. Dual-write: new contract wizard writes both contracts and legacy bookings row (feature flag CONTRACT_SPLIT_DUAL_WRITE=true).
  5. Backfill: for each booking_type = GENERAL_CONTRACT booking, insert contracts row and link existing child bookings.

Rollback: Drop new tables; null bookings.contract_id.

Phase 2 — UI Switch

  1. Deploy contract wizard at /contracts/new; redirect /bookings/new/contracts/new.
  2. Deploy booking wizard at /contracts/:id/bookings/new.
  3. Marketing queue reads from contracts.
  4. Portal contract list reads contracts table.

Rollback: Feature flag revert to legacy wizard.

Phase 3 — Read Switch + GL

  1. GL queues split ET/DJ; milestone panel live.
  2. Contract PDF generator reads contracts.
  3. Stop dual-write; POST /bookings rejects contract creation (returns 410 with redirect hint).
  4. Payment, batch, allocation verified on booking-with-contract_id in staging.

Phase 4 — Cleanup

  1. Drop bookings.booking_type, previous_contract_id, contract columns migrated to contracts.
  2. Drop booking_orders table (after data migration to direct bookings).
  3. Drop quantity from contract_routes.
  4. Remove edr_global_logistics union role.
  5. Remove dual-write flag and legacy code paths.

Data Migration Script Outline

-- For each booking WHERE booking_type = 'GENERAL_CONTRACT':
INSERT INTO freight.contracts (...) SELECT ... FROM freight.bookings;
UPDATE freight.bookings SET contract_id = ... WHERE id = general_contract_id;
UPDATE freight.bookings SET contract_id = ... WHERE id IN (child booking ids via booking_orders);

-- For each ONE_TIME booking that went through full contract flow:
INSERT INTO freight.contracts (...);
INSERT INTO freight.bookings (contract_id, ...) -- new shipment row from operational fields;
-- Or: same row gets contract_id pointing to newly inserted contract parent.

Exact backfill strategy depends on whether historical ONE_TIME rows should split into contract + booking parent-child or coalesce (recommend: insert contract from row; same row becomes shipment with contract_id set).


18. Out of Scope / Future Work

Item Source Notes
Demurrage auto-calculation GL US-12, ITLMS §8 Requires dwell-time engine + finance billing module
Storage invoice / exit note block GL US-12 AC3-4 Terminal module integration
Multimodal GL (sea/air legs) GL PDF multimodal section Email-handled steps (US 0305) need separate design
Finance AP closure / Permanently Closed GL US-10, US-13 Corporate finance module
MSRN reference prefix (MSRNI/MSRNE) GL V01 AC4 Cosmetic alignment with booking reference generator
Wagon type selection (NW7, PW2, etc.) ITLMS §1 Operations scheduling enhancement
3-hour holding period aggregation ITLMS §1 Batch engine enhancement
Priority scoring syntax ITLMS §1 Already partially in priority_score; full matrix TBD
Ministry of Trade TIN lookup US-01 Onboarding module
Grievance/ticketing US-09 Separate module
Marketing analytics dashboard US-10 Separate module

19. Open Items for Sign-Off

# Item Options Recommendation
1 Historical ONE_TIME backfill Split row vs. insert contract sibling Insert contract; same booking row gets contract_id
2 Contract reference prefix CTR-, CON-, keep booking ref CTR-YYYY-NNNNN
3 GL booking customer confirm Required vs. informational Not required — GL booking is authoritative; customer notified + pays when due
4 GENERAL contract soft cap Unlimited bookings vs. optional max Unlimited within validity (per stakeholder)
5 Contract intake doc setting codes Reuse onboarding settings vs. new New contract_intake_* codes
6 Intercity contract flow Same split vs. simplified Same split; skip clearance gate
7 Feature flag duration Dual-write period 2 sprints minimum
8 Bulk hazard count semantics Per-ton vs. per-shipment Per booking line matching container pattern

Appendix A — Contract Status Enum (Implementation)

export const CONTRACT_STATUSES = [
  'DRAFT',
  'SUBMITTED',
  'PRICE_CHANGED_PENDING_CONFIRM',
  'CHANGES_REQUESTED',
  'PENDING_APPROVAL',
  'APPROVED',
  'APPROVED_PENDING_SIGNATURE',
  'CONTRACT_READY',
  'SIGNED_CUSTOMER',
  'FULLY_EXECUTED',              // transport-only ONE_TIME
  'CONTRACT_ACTIVE',             // transport-only GENERAL
  // Path B — customs clearance (pre-booking)
  'AWAITING_CLEARANCE_DOCUMENTS',
  'CLEARANCE_UNDER_REVIEW',
  'CLEARANCE_READY_FOR_BOOKING',
  'ACTIVE_SHIPMENT_IN_PROGRESS',
  'CONTRACT_CLOSED',
  'EXPIRED',
  'REJECTED',
  'CANCELLED',
  // Renewal
  'RENEWAL_DRAFT',
  'RENEWAL_SUBMITTED',
  'RENEWAL_PENDING_APPROVAL',
  'AMENDMENTS_PROPOSED',
  'ARCHIVED',
] as const;

Appendix B — Clearance Milestone Codes (Import)

export const IMPORT_MILESTONES = [
  'IMPORT_DOCS_UPLOADED',
  'PENDING_DOCUMENT_REVIEW',
  'DOCUMENTS_APPROVED',
  'UNDER_CUSTOMS_CLEARANCE',
  'DECLARED',
  'DUTY_TAXES_ADVISED',
  'DUTY_TAX_PAID',
  'DO_COLLECTED',
  'WAGON_REQUESTED',
  'FREIGHT_PAYMENT_SETTLED',
  'WAGON_ALLOCATED',
  'GATEPASS_GRANTED',
  'READY_FOR_LOADING',
  'LOADED',
  'DEPARTED_FROM_DJIBOUTI',
  'ARRIVED_ETHIOPIA',
  'OFFLOADED',
  'T1_CLOSED',
  'RISK_ASSIGNED',
  'IMPORT_RELEASE_GRANTED',
  'IMPORT_PROCESS_COMPLETED',
  'STORAGE_INVOICE_RAISED',
  'EXIT_NOTE_GENERATED',
] as const;

Appendix C — Clearance Milestone Codes (Export)

export const EXPORT_MILESTONES = [
  'EXPORT_DOCS_UPLOADED',
  'PENDING_DOCUMENT_REVIEW',
  'DOCUMENTS_APPROVED',
  'RELEASE_ORDER_SECURED',
  'UNDER_CUSTOMS_CLEARANCE',
  'DECLARED',
  'EXPORT_RELEASED',
  'WAGON_REQUESTED',
  'FREIGHT_PAYMENT_PENDING',
  'FREIGHT_PAYMENT_SETTLED',
  'WAGON_ALLOCATED',
  'CARGO_ARRIVED',
  'READY_FOR_LOADING',
  'LOADED',
  'DEPARTED_TO_DJIBOUTI',
  'ARRIVED_AT_DJIBOUTI',
  'GATEPASS_GRANTED',
  'OFFLOADED',
] as const;

End of document.