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

1873 lines
83 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

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

# 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:**
- [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 1823 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 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_id`**before 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)
```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 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
```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 | 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 `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 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)
```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.*