# Contract–Booking 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:** - [ITLMS Operation Workflow V2.pdf](./ITLMS%20Operation%20Workflow%20V2.pdf) - [ITMLS Customer and Marketing User Stories V3.0 (2).pdf](./ITMLS%20Customer%20and%20Marketing%20User%20Stories%20V3.0%20(2).pdf) - [GlobaL Logistics- Unimodal Import and Export and Multimodal Import V02.docx.pdf](./GlobaL%20Logistics-%20Unimodal%20Import%20and%20Export%20and%20Multimodal%20Import%20V02.docx.pdf) - Codebase: `edr-platform/apps/edr-freight-api`, `edr-platform/apps/edr-freight-web` --- ## Table of Contents 1. [Executive Summary](#1-executive-summary) 2. [Design Principles](#2-design-principles) 3. [Current Architecture](#3-current-architecture) 4. [Target Architecture](#4-target-architecture) 5. [Database Schema](#5-database-schema) 6. [Status Machines](#6-status-machines) 7. [Contract Wizard Specification](#7-contract-wizard-specification) 8. [Booking Creation Specification](#8-booking-creation-specification) 9. [Pricing Model](#9-pricing-model) 10. [Renewal and Expiry Rules](#10-renewal-and-expiry-rules) 11. [Global Logistics — Import Unimodal](#11-global-logistics--import-unimodal) 12. [Global Logistics — Export Unimodal](#12-global-logistics--export-unimodal) 13. [Customs Clearance Path — GL-Owned Execution](#13-customs-clearance-path--gl-owned-execution) 14. [Gap Analysis Matrix](#14-gap-analysis-matrix) 15. [API Endpoint Mapping](#15-api-endpoint-mapping) 16. [Frontend Route & Component Mapping](#16-frontend-route--component-mapping) 17. [Migration Plan](#17-migration-plan) 18. [Out of Scope / Future Work](#18-out-of-scope--future-work) 19. [Open Items for Sign-Off](#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`**. ```mermaid 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) ```mermaid 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_ACTIVE` → `PlaceOrderDialog` 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_DOCUMENTS` → `DOCUMENTS_UNDER_REVIEW` → `CLEARANCE_READY`. --- ## 4. Target Architecture ### 4.1 Conceptual Split ```mermaid 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 ```mermaid 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.payments` — `ref_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) ```mermaid 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`. ```sql 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.** ```sql 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. ```sql 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`) ```sql 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`). ```sql 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) ```sql 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. ```sql 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:** ```sql 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: ```sql 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 18–23 milestones from GL PDF that are not represented by the current 3 clearance statuses. ```sql 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. ```sql 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`): ```sql 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) ```sql 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):** ```sql 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):** ```sql 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_review` — **not** 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`):** ```json { "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 1–8, 11, 17 (customer uploads): attach to **`contract_id`** via `contract_document_review` **before booking exists** - Rows 9–10, 14 (GL ET output): attach to **`contract_id`** during pre-booking clearance, copied/referenced on `booking_id` when GL creates booking - Rows 12–13, 15–16 (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 9–23 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 | |------------|-------|-------------| | 1–8 | Pre-booking clearance | `contract_id` — **before booking exists** | | 8a | **GL creates booking** (schedule, qty, container numbers entered by GL ET) | `booking_id` created | | 9–23 | 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) ```mermaid 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 1–10 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 ```mermaid 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/:id` — **Pay** 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 | 18–23 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 `BookingContractPage` → `ContractPage` | | `/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` | Path B customer clearance upload on contract (mirrors `ClearanceFlow` but targets `contract_id`) | | `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` | GL review of docs on contract (pre-booking) | | New | `GlCreateBookingForm.tsx` | GL ET enters schedule, qty, container numbers, bulk data (§8.2) | | `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 ```sql -- 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 03–05) 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) ```typescript 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) ```typescript 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) ```typescript 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.*