mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 02:58:11 +00:00
Merge branch 'dev' into freight/feat/invoice
This commit is contained in:
@@ -78,6 +78,7 @@ export class CreateWarehouseModule1790000000000 implements MigrationInterface {
|
||||
weight NUMERIC(14,3) NOT NULL DEFAULT 0,
|
||||
volume NUMERIC(12,3) NULL,
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'ARRIVED_AT_WAREHOUSE',
|
||||
inspection_status VARCHAR(20) NULL,
|
||||
arrived_at TIMESTAMPTZ NULL,
|
||||
inspected_at TIMESTAMPTZ NULL,
|
||||
ready_for_loading_at TIMESTAMPTZ NULL,
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
/**
|
||||
* Seeds the admin-configurable "contract validity periods" setting (days). Stored
|
||||
* as a dropdown_settings row whose options each hold a day count in `value`, so
|
||||
* backoffice manages them through the existing Dropdown Settings UI and the
|
||||
* contract staff-accept dialog only offers the configured durations.
|
||||
*/
|
||||
export class SeedContractValidityPeriods1792000000004
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'SeedContractValidityPeriods1792000000004';
|
||||
private readonly code = 'contract_validity_periods';
|
||||
private readonly options: Array<{ value: string; label: string }> = [
|
||||
{ value: '180', label: '6 months' },
|
||||
{ value: '365', label: '1 year' },
|
||||
{ value: '730', label: '2 years' },
|
||||
];
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
const existing = await queryRunner.query(
|
||||
`SELECT id FROM freight.dropdown_settings WHERE code = $1 LIMIT 1;`,
|
||||
[this.code],
|
||||
);
|
||||
if (existing.length > 0) return;
|
||||
|
||||
const inserted = await queryRunner.query(
|
||||
`INSERT INTO freight.dropdown_settings (code, label, description, multiple)
|
||||
VALUES ($1, $2, $3, false)
|
||||
RETURNING id;`,
|
||||
[
|
||||
this.code,
|
||||
'Contract Validity Periods (days)',
|
||||
'Validity durations (in days) a staff can choose when accepting a submitted contract.',
|
||||
],
|
||||
);
|
||||
const settingId = inserted[0].id;
|
||||
|
||||
for (let i = 0; i < this.options.length; i++) {
|
||||
const opt = this.options[i];
|
||||
await queryRunner.query(
|
||||
`INSERT INTO freight.dropdown_options (setting_id, value, label, display_order)
|
||||
VALUES ($1, $2, $3, $4);`,
|
||||
[settingId, opt.value, opt.label, i],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`DELETE FROM freight.dropdown_settings WHERE code = $1;`,
|
||||
[this.code],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
|
||||
|
||||
/**
|
||||
* Catch-up for environments where AddWarehouseInspection ran before the
|
||||
* warehouse module table existed. Production needs this column for unload and
|
||||
* inspection flows because the WarehouseInventory entity maps inspectionStatus.
|
||||
*/
|
||||
export class EnsureWarehouseInventoryInspectionStatus1821000000001 implements MigrationInterface {
|
||||
private readonly table = 'freight.warehouse_inventory';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
if (!(await queryRunner.hasColumn(this.table, 'inspection_status'))) {
|
||||
await queryRunner.addColumn(
|
||||
this.table,
|
||||
new TableColumn({
|
||||
name: 'inspection_status',
|
||||
type: 'varchar',
|
||||
length: '20',
|
||||
isNullable: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
await queryRunner.query(`
|
||||
CREATE INDEX IF NOT EXISTS idx_warehouse_inventory_inspection_status
|
||||
ON freight.warehouse_inventory(inspection_status)
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
DROP INDEX IF EXISTS freight.idx_warehouse_inventory_inspection_status
|
||||
`);
|
||||
|
||||
if (await queryRunner.hasColumn(this.table, 'inspection_status')) {
|
||||
await queryRunner.dropColumn(this.table, 'inspection_status');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddDistanceColumnsToVehicles1821000000002 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.vehicles
|
||||
ADD COLUMN IF NOT EXISTS estimated_distance_km NUMERIC,
|
||||
ADD COLUMN IF NOT EXISTS actual_distance_km NUMERIC;
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.vehicles
|
||||
DROP COLUMN IF EXISTS estimated_distance_km,
|
||||
DROP COLUMN IF EXISTS actual_distance_km;
|
||||
`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,357 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
/**
|
||||
* Contract–Booking separation (additive phase). Introduces a first-class
|
||||
* `freight.contracts` aggregate that owns the legal/commercial agreement (scope
|
||||
* + unit rates, no quantities) and spawns shipment `bookings` via `contract_id`.
|
||||
*
|
||||
* Purely additive: no legacy columns are dropped here. The data backfill and
|
||||
* legacy-column removal happen in a later cutover migration.
|
||||
*
|
||||
* See docs/new-doc.md §5.
|
||||
*/
|
||||
export class CreateContracts1822000000000 implements MigrationInterface {
|
||||
name = 'CreateContracts1822000000000';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
// ── contracts ───────────────────────────────────────────────────────────
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE IF NOT EXISTS freight.contracts (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
reference VARCHAR(64) NOT NULL UNIQUE,
|
||||
|
||||
company_id UUID,
|
||||
company_profile_id UUID,
|
||||
is_government BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
government_institution VARCHAR(255),
|
||||
|
||||
contract_kind VARCHAR(20) NOT NULL,
|
||||
renewal_of_id UUID REFERENCES freight.contracts(id),
|
||||
trade_direction VARCHAR(10) NOT NULL,
|
||||
freight_type VARCHAR(20) NOT NULL,
|
||||
|
||||
service_type_id UUID NOT NULL,
|
||||
payment_currency VARCHAR(5) NOT NULL,
|
||||
customs_clearing_enabled BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
customs_clearing_agent VARCHAR(200),
|
||||
equipment_return VARCHAR(20),
|
||||
|
||||
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),
|
||||
|
||||
is_hazardous BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
is_reefer BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
|
||||
estimated_shipment_date TIMESTAMPTZ,
|
||||
contract_validity_days INT,
|
||||
contract_valid_from TIMESTAMPTZ,
|
||||
contract_valid_until TIMESTAMPTZ,
|
||||
expires_at TIMESTAMPTZ,
|
||||
|
||||
status VARCHAR(40) NOT NULL DEFAULT 'DRAFT',
|
||||
clearance_status VARCHAR(40) NOT NULL DEFAULT 'NOT_APPLICABLE',
|
||||
clearance_cycle_number INT NOT NULL DEFAULT 0,
|
||||
|
||||
pricing_breakdown JSONB,
|
||||
pricing_display_mode VARCHAR(20) DEFAULT 'UNIT_RATES',
|
||||
|
||||
contract_type VARCHAR(20),
|
||||
contract_template_key VARCHAR(128),
|
||||
contract_generated_at TIMESTAMPTZ,
|
||||
contract_summary TEXT,
|
||||
version_number INT NOT NULL DEFAULT 1,
|
||||
financial_terms JSONB,
|
||||
|
||||
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,
|
||||
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
`);
|
||||
await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_contracts_company ON freight.contracts(company_id);`);
|
||||
await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_contracts_status ON freight.contracts(status);`);
|
||||
await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_contracts_kind ON freight.contracts(contract_kind);`);
|
||||
await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_contracts_valid_until ON freight.contracts(contract_valid_until);`);
|
||||
|
||||
// ── contract_routes ──────────────────────────────────────────────────────
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE IF NOT EXISTS 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,
|
||||
destination_yard_id UUID NOT NULL,
|
||||
km NUMERIC(10,2),
|
||||
sort_order SMALLINT NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
CONSTRAINT uq_contract_route UNIQUE (contract_id, origin_yard_id, destination_yard_id)
|
||||
);
|
||||
`);
|
||||
await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_contract_routes_contract ON freight.contract_routes(contract_id);`);
|
||||
|
||||
// ── contract_cargo_scope ─────────────────────────────────────────────────
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE IF NOT EXISTS 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_size VARCHAR(10),
|
||||
cargo_type_id UUID,
|
||||
cargo_free_text VARCHAR(200),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
CONSTRAINT uq_contract_container_size UNIQUE NULLS NOT DISTINCT (contract_id, container_size)
|
||||
);
|
||||
`);
|
||||
await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_contract_cargo_scope_contract ON freight.contract_cargo_scope(contract_id);`);
|
||||
|
||||
// ── contract_signatures ──────────────────────────────────────────────────
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE IF NOT EXISTS 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,
|
||||
signer_display_name VARCHAR(255) NOT NULL,
|
||||
signature_file_id UUID,
|
||||
consent_text TEXT,
|
||||
signed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
`);
|
||||
await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_contract_signatures_contract ON freight.contract_signatures(contract_id);`);
|
||||
|
||||
// ── contract_approval_steps ──────────────────────────────────────────────
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE IF NOT EXISTS freight.contract_approval_steps (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
contract_id UUID NOT NULL REFERENCES freight.contracts(id) ON DELETE CASCADE,
|
||||
step_order SMALLINT NOT NULL DEFAULT 0,
|
||||
required_role VARCHAR(40) NOT NULL,
|
||||
blocks_role VARCHAR(40),
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
|
||||
acted_by_staff_id UUID,
|
||||
acted_at TIMESTAMPTZ,
|
||||
note TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
`);
|
||||
await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_contract_approval_steps_contract ON freight.contract_approval_steps(contract_id);`);
|
||||
|
||||
// ── contract_rate_snapshots ──────────────────────────────────────────────
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE IF NOT EXISTS 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,
|
||||
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),
|
||||
is_surcharge BOOLEAN DEFAULT FALSE,
|
||||
conditional_on VARCHAR(32),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
`);
|
||||
await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_contract_rate_snapshots_contract ON freight.contract_rate_snapshots(contract_id);`);
|
||||
|
||||
// ── contract_review_notes ────────────────────────────────────────────────
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE IF NOT EXISTS freight.contract_review_notes (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
contract_id UUID NOT NULL REFERENCES freight.contracts(id) ON DELETE CASCADE,
|
||||
note_type VARCHAR(40) NOT NULL,
|
||||
body TEXT NOT NULL,
|
||||
author_role VARCHAR(20),
|
||||
author_user_id UUID,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
`);
|
||||
await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_contract_review_notes_contract ON freight.contract_review_notes(contract_id);`);
|
||||
|
||||
// ── contract_clearance_cycles ────────────────────────────────────────────
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE IF NOT EXISTS 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,
|
||||
started_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
clearance_ready_at TIMESTAMPTZ,
|
||||
completed_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
CONSTRAINT uq_contract_clearance_cycle UNIQUE (contract_id, cycle_number)
|
||||
);
|
||||
`);
|
||||
await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_contract_clearance_cycles_contract ON freight.contract_clearance_cycles(contract_id);`);
|
||||
|
||||
// ── contract_document_review (pre-booking clearance, Path B) ─────────────
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE IF NOT EXISTS 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',
|
||||
note TEXT,
|
||||
uploaded_by_role VARCHAR(20) NOT NULL DEFAULT 'CUSTOMER',
|
||||
reviewed_by_staff_id UUID,
|
||||
reviewed_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
CONSTRAINT uq_contract_document_review_doc
|
||||
UNIQUE NULLS NOT DISTINCT (contract_id, clearance_cycle_id, setting_code, file_key)
|
||||
);
|
||||
`);
|
||||
await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_contract_doc_review_contract ON freight.contract_document_review(contract_id);`);
|
||||
await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_contract_doc_review_status ON freight.contract_document_review(status);`);
|
||||
|
||||
// ── clearance_milestones (GL tracking) ───────────────────────────────────
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE IF NOT EXISTS freight.clearance_milestones (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
booking_id UUID REFERENCES freight.bookings(id) ON DELETE CASCADE,
|
||||
contract_id UUID REFERENCES freight.contracts(id) ON DELETE CASCADE,
|
||||
clearance_cycle_id UUID REFERENCES freight.contract_clearance_cycles(id),
|
||||
milestone_code VARCHAR(64) NOT NULL,
|
||||
milestone_label VARCHAR(255) NOT NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
|
||||
owner_region VARCHAR(5),
|
||||
triggered_by_doc BOOLEAN DEFAULT FALSE,
|
||||
triggered_at TIMESTAMPTZ,
|
||||
triggered_by_user_id UUID,
|
||||
note TEXT,
|
||||
sort_order SMALLINT NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
`);
|
||||
await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_clearance_milestones_booking ON freight.clearance_milestones(booking_id);`);
|
||||
await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_clearance_milestones_contract ON freight.clearance_milestones(contract_id);`);
|
||||
await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_clearance_milestones_region ON freight.clearance_milestones(owner_region, status);`);
|
||||
// booking-scoped and contract-cycle-scoped uniqueness for milestone codes
|
||||
await queryRunner.query(`
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_clearance_milestone_booking
|
||||
ON freight.clearance_milestones(booking_id, milestone_code) WHERE booking_id IS NOT NULL;
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_clearance_milestone_cycle
|
||||
ON freight.clearance_milestones(clearance_cycle_id, milestone_code) WHERE clearance_cycle_id IS NOT NULL;
|
||||
`);
|
||||
|
||||
// ── booking_container_units (per-unit container detail) ──────────────────
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE IF NOT EXISTS 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(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||
deleted_at TIMESTAMPTZ,
|
||||
CONSTRAINT uq_booking_container_unit_number UNIQUE (booking_container_id, container_number)
|
||||
);
|
||||
`);
|
||||
|
||||
// ── ALTER bookings ───────────────────────────────────────────────────────
|
||||
await queryRunner.query(`ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS contract_id UUID REFERENCES freight.contracts(id);`);
|
||||
await queryRunner.query(`ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS contract_route_id UUID REFERENCES freight.contract_routes(id);`);
|
||||
await queryRunner.query(`ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS created_by_role VARCHAR(20) DEFAULT 'CUSTOMER';`);
|
||||
await queryRunner.query(`ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS created_by_user_id UUID;`);
|
||||
await queryRunner.query(`CREATE INDEX IF NOT EXISTS idx_bookings_contract ON freight.bookings(contract_id);`);
|
||||
// One active booking per ONE_TIME contract. Postgres forbids a subquery in an
|
||||
// index predicate, so we denormalize the contract kind onto the booking and
|
||||
// predicate on that. The column is stamped at booking creation from the
|
||||
// contract; the app layer (ContractBookingService) is the primary guard and
|
||||
// this index is the backstop.
|
||||
await queryRunner.query(`ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS contract_kind VARCHAR(20);`);
|
||||
await queryRunner.query(`
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uq_one_active_booking_per_one_time_contract
|
||||
ON freight.bookings (contract_id)
|
||||
WHERE status NOT IN ('EXPIRED', 'CANCELLED', 'COMPLETED', 'REJECTED')
|
||||
AND contract_id IS NOT NULL
|
||||
AND contract_kind = 'ONE_TIME';
|
||||
`);
|
||||
|
||||
// ── ALTER booking_container ──────────────────────────────────────────────
|
||||
await queryRunner.query(`ALTER TABLE freight.booking_container ADD COLUMN IF NOT EXISTS container_size VARCHAR(10);`);
|
||||
await queryRunner.query(`ALTER TABLE freight.booking_container ADD COLUMN IF NOT EXISTS hazardous_quantity SMALLINT DEFAULT 0;`);
|
||||
await queryRunner.query(`ALTER TABLE freight.booking_container ADD COLUMN IF NOT EXISTS reefer_quantity SMALLINT DEFAULT 0;`);
|
||||
|
||||
// ── ALTER booking_document_review (denormalized contract link) ───────────
|
||||
await queryRunner.query(`ALTER TABLE freight.booking_document_review ADD COLUMN IF NOT EXISTS contract_id UUID REFERENCES freight.contracts(id);`);
|
||||
|
||||
// ── Extend file_upload_fields with phased GL metadata ────────────────────
|
||||
await queryRunner.query(`ALTER TABLE freight.file_upload_fields ADD COLUMN IF NOT EXISTS phase VARCHAR(40);`);
|
||||
await queryRunner.query(`ALTER TABLE freight.file_upload_fields ADD COLUMN IF NOT EXISTS owner_region VARCHAR(5);`);
|
||||
await queryRunner.query(`ALTER TABLE freight.file_upload_fields ADD COLUMN IF NOT EXISTS trade_direction VARCHAR(10);`);
|
||||
await queryRunner.query(`ALTER TABLE freight.file_upload_fields ADD COLUMN IF NOT EXISTS triggers_milestone_code VARCHAR(64);`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE freight.file_upload_fields DROP COLUMN IF EXISTS triggers_milestone_code;`);
|
||||
await queryRunner.query(`ALTER TABLE freight.file_upload_fields DROP COLUMN IF EXISTS trade_direction;`);
|
||||
await queryRunner.query(`ALTER TABLE freight.file_upload_fields DROP COLUMN IF EXISTS owner_region;`);
|
||||
await queryRunner.query(`ALTER TABLE freight.file_upload_fields DROP COLUMN IF EXISTS phase;`);
|
||||
|
||||
await queryRunner.query(`ALTER TABLE freight.booking_document_review DROP COLUMN IF EXISTS contract_id;`);
|
||||
|
||||
await queryRunner.query(`ALTER TABLE freight.booking_container DROP COLUMN IF EXISTS reefer_quantity;`);
|
||||
await queryRunner.query(`ALTER TABLE freight.booking_container DROP COLUMN IF EXISTS hazardous_quantity;`);
|
||||
await queryRunner.query(`ALTER TABLE freight.booking_container DROP COLUMN IF EXISTS container_size;`);
|
||||
|
||||
await queryRunner.query(`DROP INDEX IF EXISTS freight.uq_one_active_booking_per_one_time_contract;`);
|
||||
await queryRunner.query(`DROP INDEX IF EXISTS freight.idx_bookings_contract;`);
|
||||
await queryRunner.query(`ALTER TABLE freight.bookings DROP COLUMN IF EXISTS contract_kind;`);
|
||||
await queryRunner.query(`ALTER TABLE freight.bookings DROP COLUMN IF EXISTS created_by_user_id;`);
|
||||
await queryRunner.query(`ALTER TABLE freight.bookings DROP COLUMN IF EXISTS created_by_role;`);
|
||||
await queryRunner.query(`ALTER TABLE freight.bookings DROP COLUMN IF EXISTS contract_route_id;`);
|
||||
await queryRunner.query(`ALTER TABLE freight.bookings DROP COLUMN IF EXISTS contract_id;`);
|
||||
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS freight.booking_container_units;`);
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS freight.clearance_milestones;`);
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS freight.contract_document_review;`);
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS freight.contract_clearance_cycles;`);
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS freight.contract_review_notes;`);
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS freight.contract_rate_snapshots;`);
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS freight.contract_approval_steps;`);
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS freight.contract_signatures;`);
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS freight.contract_cargo_scope;`);
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS freight.contract_routes;`);
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS freight.contracts;`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import { MigrationInterface, QueryRunner, Table, TableForeignKey, TableIndex } from 'typeorm';
|
||||
|
||||
export class CreateImportDjiboutiOperations1822000000000 implements MigrationInterface {
|
||||
name = 'CreateImportDjiboutiOperations1822000000000';
|
||||
|
||||
async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.createTable(
|
||||
new Table({
|
||||
schema: 'freight',
|
||||
name: 'import_djibouti_operations',
|
||||
columns: [
|
||||
{ name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' },
|
||||
{ name: 'train_schedule_id', type: 'uuid', isUnique: true },
|
||||
{ name: 'documents', type: 'jsonb', default: "'{}'::jsonb" },
|
||||
{ name: 'gatepass_granted_at', type: 'timestamptz', isNullable: true },
|
||||
{ name: 'ready_for_loading_at', type: 'timestamptz', isNullable: true },
|
||||
{ name: 'loaded_on_train_at', type: 'timestamptz', isNullable: true },
|
||||
{ name: 'departed_from_djibouti_at', type: 'timestamptz', isNullable: true },
|
||||
{ name: 'load_list_generated_at', type: 'timestamptz', isNullable: true },
|
||||
{ name: 'performed_by', type: 'varchar', length: '120', isNullable: true },
|
||||
{ name: 'notes', type: 'text', isNullable: true },
|
||||
{ name: 'created_at', type: 'timestamptz', default: 'now()' },
|
||||
{ name: 'updated_at', type: 'timestamptz', default: 'now()' },
|
||||
{ name: 'deleted_at', type: 'timestamptz', isNullable: true },
|
||||
],
|
||||
}),
|
||||
true,
|
||||
);
|
||||
|
||||
await queryRunner.createIndex(
|
||||
'freight.import_djibouti_operations',
|
||||
new TableIndex({
|
||||
name: 'idx_import_djibouti_operations_schedule',
|
||||
columnNames: ['train_schedule_id'],
|
||||
}),
|
||||
);
|
||||
|
||||
await queryRunner.createForeignKey(
|
||||
'freight.import_djibouti_operations',
|
||||
new TableForeignKey({
|
||||
columnNames: ['train_schedule_id'],
|
||||
referencedTableName: 'train_schedules',
|
||||
referencedSchema: 'freight',
|
||||
referencedColumnNames: ['id'],
|
||||
onDelete: 'CASCADE',
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.dropTable('freight.import_djibouti_operations', true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
/**
|
||||
* Data backfill for the contract–booking separation (docs/new-doc.md §17).
|
||||
*
|
||||
* For every legacy `booking_type = 'GENERAL_CONTRACT'` booking we synthesise a
|
||||
* `freight.contracts` row from its contract-phase columns, copy its routes
|
||||
* (contract_route_lines → contract_routes, dropping quantity), and point the
|
||||
* contract + every child shipment booking (linked via booking_orders) at it.
|
||||
*
|
||||
* Per §19 item 1, historical ONE_TIME bookings that went through the full
|
||||
* contract flow get a contract parent inserted and `contract_id` set on the same
|
||||
* booking row (no row split).
|
||||
*
|
||||
* Idempotent: skips bookings that already have `contract_id` set, and matches a
|
||||
* synthesised contract by a deterministic `CTR-<bookingId>` reference.
|
||||
*/
|
||||
export class BackfillContractsFromBookings1823000000000
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'BackfillContractsFromBookings1823000000000';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
// 1. One contract per GENERAL_CONTRACT booking, carrying the contract-phase
|
||||
// columns. Reference is derived from the source booking id so re-runs are
|
||||
// idempotent (ON CONFLICT DO NOTHING on the unique reference).
|
||||
await queryRunner.query(`
|
||||
INSERT INTO freight.contracts (
|
||||
reference, company_id, company_profile_id, is_government, government_institution,
|
||||
contract_kind, trade_direction, freight_type, service_type_id, payment_currency,
|
||||
customs_clearing_enabled, customs_clearing_agent, equipment_return,
|
||||
first_mile_pickup_address, first_mile_pickup_lat, first_mile_pickup_lng,
|
||||
last_mile_delivery_address, last_mile_delivery_lat, last_mile_delivery_lng,
|
||||
is_hazardous, is_reefer, estimated_shipment_date,
|
||||
contract_validity_days, contract_valid_from, contract_valid_until, expires_at,
|
||||
status, clearance_status, clearance_cycle_number,
|
||||
pricing_breakdown, contract_type, contract_template_key, contract_generated_at,
|
||||
contract_summary, version_number,
|
||||
approved_by_staff_id, approved_by_staff_at,
|
||||
signed_by_director_id, signed_by_director_at,
|
||||
signed_by_ceo_id, signed_by_ceo_at, customer_signed_at, fully_executed_at,
|
||||
created_at, updated_at
|
||||
)
|
||||
SELECT
|
||||
'CTR-' || b.id::text, b.company_id, b.company_profile_id, b.is_government, b.government_institution,
|
||||
'GENERAL', b.trade_direction, b.freight_type, b.service_type_id, b.payment_currency,
|
||||
b.customs_clearing_enabled, b.customs_clearing_agent, b.equipment_return,
|
||||
b.first_mile_pickup_address, b.first_mile_pickup_lat, b.first_mile_pickup_lng,
|
||||
b.last_mile_delivery_address, b.last_mile_delivery_lat, b.last_mile_delivery_lng,
|
||||
b.is_hazardous, b.is_reefer, b.estimated_shipment_date,
|
||||
b.contract_validity_days, b.contract_valid_from, b.contract_valid_until, b.expires_at,
|
||||
CASE
|
||||
WHEN b.status IN ('CONTRACT_ACTIVE') THEN 'CONTRACT_ACTIVE'
|
||||
WHEN b.status IN ('CONTRACT_CLOSED') THEN 'CONTRACT_CLOSED'
|
||||
WHEN b.status IN ('EXPIRED') THEN 'EXPIRED'
|
||||
WHEN b.status IN ('CANCELLED') THEN 'CANCELLED'
|
||||
WHEN b.status IN ('REJECTED') THEN 'REJECTED'
|
||||
ELSE 'CONTRACT_ACTIVE'
|
||||
END,
|
||||
CASE WHEN b.customs_clearing_enabled THEN 'NOT_APPLICABLE' ELSE 'NOT_APPLICABLE' END,
|
||||
0,
|
||||
b.pricing_breakdown,
|
||||
b.contract_type, b.contract_template_key, b.contract_generated_at,
|
||||
b.contract_summary, COALESCE(b.version_number, 1),
|
||||
b.approved_by_staff_id, b.approved_by_staff_at,
|
||||
b.signed_by_director_id, b.signed_by_director_at,
|
||||
b.signed_by_ceo_id, b.signed_by_ceo_at, b.customer_signed_at, b.fully_executed_at,
|
||||
b.created_at, b.updated_at
|
||||
FROM freight.bookings b
|
||||
WHERE b.booking_type = 'GENERAL_CONTRACT'
|
||||
ON CONFLICT (reference) DO NOTHING;
|
||||
`);
|
||||
|
||||
// 2. Copy each general contract's route lines into contract_routes (no qty).
|
||||
await queryRunner.query(`
|
||||
INSERT INTO freight.contract_routes (contract_id, origin_yard_id, destination_yard_id, km, sort_order, created_at, updated_at)
|
||||
SELECT c.id, crl.origin_yard_id, crl.destination_yard_id, crl.km, 0, now(), now()
|
||||
FROM freight.contract_route_lines crl
|
||||
JOIN freight.contracts c ON c.reference = 'CTR-' || crl.contract_booking_id::text
|
||||
ON CONFLICT (contract_id, origin_yard_id, destination_yard_id) DO NOTHING;
|
||||
`);
|
||||
|
||||
// 3. Point the general-contract booking itself at its new contract, and stamp
|
||||
// the denormalized contract_kind for the active-booking index.
|
||||
await queryRunner.query(`
|
||||
UPDATE freight.bookings b
|
||||
SET contract_id = c.id, contract_kind = 'GENERAL', created_by_role = 'CUSTOMER'
|
||||
FROM freight.contracts c
|
||||
WHERE c.reference = 'CTR-' || b.id::text
|
||||
AND b.booking_type = 'GENERAL_CONTRACT'
|
||||
AND b.contract_id IS NULL;
|
||||
`);
|
||||
|
||||
// 4. Point each child shipment booking (spawned via booking_orders) at the
|
||||
// same contract as its parent general contract.
|
||||
await queryRunner.query(`
|
||||
UPDATE freight.bookings child
|
||||
SET contract_id = c.id, contract_kind = 'GENERAL', created_by_role = 'CUSTOMER'
|
||||
FROM freight.booking_orders bo
|
||||
JOIN freight.contracts c ON c.reference = 'CTR-' || bo.contract_booking_id::text
|
||||
WHERE child.id = bo.booking_id
|
||||
AND child.contract_id IS NULL;
|
||||
`);
|
||||
|
||||
// 5. Historical ONE_TIME bookings that completed the contract flow: synthesise
|
||||
// a contract parent and point the same booking row at it (no row split).
|
||||
await queryRunner.query(`
|
||||
INSERT INTO freight.contracts (
|
||||
reference, company_id, company_profile_id, is_government, government_institution,
|
||||
contract_kind, trade_direction, freight_type, service_type_id, payment_currency,
|
||||
customs_clearing_enabled, customs_clearing_agent, equipment_return,
|
||||
first_mile_pickup_address, first_mile_pickup_lat, first_mile_pickup_lng,
|
||||
last_mile_delivery_address, last_mile_delivery_lat, last_mile_delivery_lng,
|
||||
is_hazardous, is_reefer, estimated_shipment_date,
|
||||
contract_validity_days, contract_valid_from, contract_valid_until,
|
||||
status, clearance_status, clearance_cycle_number,
|
||||
pricing_breakdown, contract_type, contract_template_key, contract_generated_at,
|
||||
contract_summary, version_number,
|
||||
approved_by_staff_id, approved_by_staff_at,
|
||||
signed_by_director_id, signed_by_director_at,
|
||||
signed_by_ceo_id, signed_by_ceo_at, customer_signed_at, fully_executed_at,
|
||||
created_at, updated_at
|
||||
)
|
||||
SELECT
|
||||
'CTR-' || b.id::text, b.company_id, b.company_profile_id, b.is_government, b.government_institution,
|
||||
'ONE_TIME', b.trade_direction, b.freight_type, b.service_type_id, b.payment_currency,
|
||||
b.customs_clearing_enabled, b.customs_clearing_agent, b.equipment_return,
|
||||
b.first_mile_pickup_address, b.first_mile_pickup_lat, b.first_mile_pickup_lng,
|
||||
b.last_mile_delivery_address, b.last_mile_delivery_lat, b.last_mile_delivery_lng,
|
||||
b.is_hazardous, b.is_reefer, b.estimated_shipment_date,
|
||||
b.contract_validity_days, b.contract_valid_from, b.contract_valid_until,
|
||||
'FULLY_EXECUTED', 'NOT_APPLICABLE', 0,
|
||||
b.pricing_breakdown, b.contract_type, b.contract_template_key, b.contract_generated_at,
|
||||
b.contract_summary, COALESCE(b.version_number, 1),
|
||||
b.approved_by_staff_id, b.approved_by_staff_at,
|
||||
b.signed_by_director_id, b.signed_by_director_at,
|
||||
b.signed_by_ceo_id, b.signed_by_ceo_at, b.customer_signed_at, b.fully_executed_at,
|
||||
b.created_at, b.updated_at
|
||||
FROM freight.bookings b
|
||||
WHERE COALESCE(b.booking_type, 'ONE_TIME') = 'ONE_TIME'
|
||||
AND b.contract_id IS NULL
|
||||
AND b.contract_generated_at IS NOT NULL
|
||||
ON CONFLICT (reference) DO NOTHING;
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
UPDATE freight.bookings b
|
||||
SET contract_id = c.id, contract_kind = 'ONE_TIME', created_by_role = 'CUSTOMER'
|
||||
FROM freight.contracts c
|
||||
WHERE c.reference = 'CTR-' || b.id::text
|
||||
AND COALESCE(b.booking_type, 'ONE_TIME') = 'ONE_TIME'
|
||||
AND b.contract_id IS NULL;
|
||||
`);
|
||||
|
||||
// 6. Build a single route per ONE_TIME contract from the booking's own
|
||||
// origin/destination (general contracts already got their routes in step 2).
|
||||
await queryRunner.query(`
|
||||
INSERT INTO freight.contract_routes (contract_id, origin_yard_id, destination_yard_id, sort_order, created_at, updated_at)
|
||||
SELECT c.id, b.origin_yard_id, b.destination_yard_id, 0, now(), now()
|
||||
FROM freight.bookings b
|
||||
JOIN freight.contracts c ON c.id = b.contract_id AND c.contract_kind = 'ONE_TIME'
|
||||
WHERE b.origin_yard_id IS NOT NULL AND b.destination_yard_id IS NOT NULL
|
||||
ON CONFLICT (contract_id, origin_yard_id, destination_yard_id) DO NOTHING;
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
// Unlink bookings and drop the synthesised contracts (and their cascaded routes).
|
||||
await queryRunner.query(`
|
||||
UPDATE freight.bookings SET contract_id = NULL, contract_route_id = NULL
|
||||
WHERE contract_id IN (SELECT id FROM freight.contracts WHERE reference LIKE 'CTR-%');
|
||||
`);
|
||||
await queryRunner.query(`DELETE FROM freight.contracts WHERE reference LIKE 'CTR-%';`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
import { MigrationInterface, QueryRunner, Table, TableIndex } from 'typeorm';
|
||||
|
||||
export class CreateImportOperationsTables1823000000000 implements MigrationInterface {
|
||||
name = 'CreateImportOperationsTables1823000000000';
|
||||
|
||||
async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.createTable(
|
||||
new Table({
|
||||
schema: 'freight',
|
||||
name: 'djibouti_import_incidents',
|
||||
columns: [
|
||||
{ name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' },
|
||||
{ name: 'booking_id', type: 'uuid' },
|
||||
{ name: 'container_number', type: 'varchar', length: '80', isNullable: true },
|
||||
{ name: 'cargo_id', type: 'uuid', isNullable: true },
|
||||
{ name: 'facility', type: 'varchar', length: '120', isNullable: true },
|
||||
{ name: 'station', type: 'varchar', length: '120', isNullable: true },
|
||||
{ name: 'incident_type', type: 'varchar', length: '40' },
|
||||
{ name: 'description', type: 'text' },
|
||||
{ name: 'photos', type: 'jsonb', default: "'[]'::jsonb" },
|
||||
{ name: 'reported_by', type: 'varchar', length: '120', isNullable: true },
|
||||
{ name: 'reported_at', type: 'timestamptz' },
|
||||
{ name: 'created_at', type: 'timestamptz', default: 'now()' },
|
||||
{ name: 'updated_at', type: 'timestamptz', default: 'now()' },
|
||||
{ name: 'deleted_at', type: 'timestamptz', isNullable: true },
|
||||
],
|
||||
}),
|
||||
true,
|
||||
);
|
||||
await queryRunner.createIndex('freight.djibouti_import_incidents', new TableIndex({ name: 'idx_djibouti_incidents_booking', columnNames: ['booking_id'] }));
|
||||
await queryRunner.createIndex('freight.djibouti_import_incidents', new TableIndex({ name: 'idx_djibouti_incidents_container', columnNames: ['container_number'] }));
|
||||
await queryRunner.createIndex('freight.djibouti_import_incidents', new TableIndex({ name: 'idx_djibouti_incidents_type', columnNames: ['incident_type'] }));
|
||||
|
||||
await queryRunner.createTable(
|
||||
new Table({
|
||||
schema: 'freight',
|
||||
name: 'import_customs_finalizations',
|
||||
columns: [
|
||||
{ name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' },
|
||||
{ name: 'booking_id', type: 'uuid', isUnique: true },
|
||||
{ name: 'documents', type: 'jsonb', default: "'{}'::jsonb" },
|
||||
{ name: 'declaration_serial_number', type: 'varchar', length: '120', isNullable: true },
|
||||
{ name: 'duties_taxes_notified_at', type: 'timestamptz', isNullable: true },
|
||||
{ name: 'duties_taxes_paid_at', type: 'timestamptz', isNullable: true },
|
||||
{ name: 'customs_risk', type: 'varchar', length: '12', isNullable: true },
|
||||
{ name: 'import_release_permitted_at', type: 'timestamptz', isNullable: true },
|
||||
{ name: 'completed_at', type: 'timestamptz', isNullable: true },
|
||||
{ name: 'performed_by', type: 'varchar', length: '120', isNullable: true },
|
||||
{ name: 'notes', type: 'text', isNullable: true },
|
||||
{ name: 'created_at', type: 'timestamptz', default: 'now()' },
|
||||
{ name: 'updated_at', type: 'timestamptz', default: 'now()' },
|
||||
{ name: 'deleted_at', type: 'timestamptz', isNullable: true },
|
||||
],
|
||||
}),
|
||||
true,
|
||||
);
|
||||
await queryRunner.createIndex('freight.import_customs_finalizations', new TableIndex({ name: 'idx_import_customs_booking', columnNames: ['booking_id'] }));
|
||||
await queryRunner.createIndex('freight.import_customs_finalizations', new TableIndex({ name: 'idx_import_customs_risk', columnNames: ['customs_risk'] }));
|
||||
|
||||
await queryRunner.createTable(
|
||||
new Table({
|
||||
schema: 'freight',
|
||||
name: 'empty_container_returns',
|
||||
columns: [
|
||||
{ name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' },
|
||||
{ name: 'container_number', type: 'varchar', length: '80' },
|
||||
{ name: 'booking_id', type: 'uuid', isNullable: true },
|
||||
{ name: 'customer_id', type: 'uuid', isNullable: true },
|
||||
{ name: 'return_date', type: 'timestamptz' },
|
||||
{ name: 'facility', type: 'varchar', length: '120', isNullable: true },
|
||||
{ name: 'yard', type: 'varchar', length: '120', isNullable: true },
|
||||
{ name: 'zone', type: 'varchar', length: '120', isNullable: true },
|
||||
{ name: 'condition', type: 'text', isNullable: true },
|
||||
{ name: 'handover_note', type: 'text', isNullable: true },
|
||||
{ name: 'status', type: 'varchar', length: '40', default: "'RETURNED'" },
|
||||
{ name: 'wagon_allocation_reference', type: 'varchar', length: '120', isNullable: true },
|
||||
{ name: 'performed_by', type: 'varchar', length: '120', isNullable: true },
|
||||
{ name: 'created_at', type: 'timestamptz', default: 'now()' },
|
||||
{ name: 'updated_at', type: 'timestamptz', default: 'now()' },
|
||||
{ name: 'deleted_at', type: 'timestamptz', isNullable: true },
|
||||
],
|
||||
}),
|
||||
true,
|
||||
);
|
||||
await queryRunner.createIndex('freight.empty_container_returns', new TableIndex({ name: 'idx_empty_returns_container', columnNames: ['container_number'] }));
|
||||
await queryRunner.createIndex('freight.empty_container_returns', new TableIndex({ name: 'idx_empty_returns_booking', columnNames: ['booking_id'] }));
|
||||
await queryRunner.createIndex('freight.empty_container_returns', new TableIndex({ name: 'idx_empty_returns_status', columnNames: ['status'] }));
|
||||
}
|
||||
|
||||
async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.dropTable('freight.empty_container_returns', true);
|
||||
await queryRunner.dropTable('freight.import_customs_finalizations', true);
|
||||
await queryRunner.dropTable('freight.djibouti_import_incidents', true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
/**
|
||||
* Cutover cleanup (docs/new-doc.md §17 Phase 4). Runs AFTER the backfill
|
||||
* (1823…) so every legacy general contract + drawdown already lives in the
|
||||
* `contracts` aggregate.
|
||||
*
|
||||
* Drops the now-unused booking-as-contract artifacts:
|
||||
* - `bookings.booking_type` (every booking is a real shipment now)
|
||||
* - `bookings.previous_contract_id` (renewal lives on `contracts.renewal_of_id`)
|
||||
* - the `booking_orders` / `booking_order_lines` drawdown ledger
|
||||
* - `contract_route_lines` (superseded by `contract_routes`)
|
||||
*
|
||||
* The shipment/payment/scheduling/allocation columns on `bookings` are kept —
|
||||
* the operational pipeline is unchanged.
|
||||
*/
|
||||
export class DropLegacyContractTables1824000000000 implements MigrationInterface {
|
||||
name = 'DropLegacyContractTables1824000000000';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
// booking_order_lines references booking_orders → drop child first.
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS freight.booking_order_lines CASCADE;`);
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS freight.booking_orders CASCADE;`);
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS freight.contract_route_lines CASCADE;`);
|
||||
|
||||
await queryRunner.query(`ALTER TABLE freight.bookings DROP COLUMN IF EXISTS booking_type;`);
|
||||
await queryRunner.query(`ALTER TABLE freight.bookings DROP COLUMN IF EXISTS previous_contract_id;`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
// Re-add the dropped columns (data is not restored — this is a one-way cutover).
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS booking_type VARCHAR(20) DEFAULT 'ONE_TIME';`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS previous_contract_id UUID;`,
|
||||
);
|
||||
// The legacy ledger/route tables are intentionally NOT recreated here; restore
|
||||
// from a backup if a rollback past the cutover is ever required.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
/**
|
||||
* Global Logistics Phase-2 operational features (docs/new-doc.md §11–§13, gap
|
||||
* matrix #14/#16/#17/#18):
|
||||
* - `clearance_milestones.metadata` — structured payload for RISK_ASSIGNED
|
||||
* (risk level) and DUTY_TAXES_ADVISED (amount, currency, declaration serial)
|
||||
* - `bookings.gl_station_yard_id` / `gl_assigned_staff_id` / `gl_assigned_at`
|
||||
* — station routing + staff binding (GL US-02)
|
||||
* - `freight.clearance_incidents` — cargo exception/damage reports with photos
|
||||
* (GL Import US-07)
|
||||
*/
|
||||
export class AddGlOperations1825000000000 implements MigrationInterface {
|
||||
name = 'AddGlOperations1825000000000';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE freight.clearance_milestones ADD COLUMN IF NOT EXISTS metadata JSONB;`,
|
||||
);
|
||||
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS gl_station_yard_id UUID;`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS gl_assigned_staff_id UUID;`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS gl_assigned_at TIMESTAMPTZ;`,
|
||||
);
|
||||
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE IF NOT EXISTS freight.clearance_incidents (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
booking_id UUID NOT NULL REFERENCES freight.bookings(id) ON DELETE CASCADE,
|
||||
incident_type VARCHAR(32) NOT NULL,
|
||||
description TEXT NOT NULL,
|
||||
photo_file_ids JSONB NOT NULL DEFAULT '[]',
|
||||
reported_by_user_id UUID,
|
||||
reported_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
`);
|
||||
await queryRunner.query(
|
||||
`CREATE INDEX IF NOT EXISTS idx_clearance_incidents_booking ON freight.clearance_incidents(booking_id);`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS freight.clearance_incidents CASCADE;`);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE freight.bookings DROP COLUMN IF EXISTS gl_assigned_at;`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE freight.bookings DROP COLUMN IF EXISTS gl_assigned_staff_id;`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE freight.bookings DROP COLUMN IF EXISTS gl_station_yard_id;`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE freight.clearance_milestones DROP COLUMN IF EXISTS metadata;`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
/**
|
||||
* GENERAL contracts can be booked repeatedly until a total cargo quantity cap is
|
||||
* reached (e.g. 100 containers across many shipments). `quantity_cap` on each
|
||||
* cargo-scope line holds that ceiling (containers per size, or tons/items for
|
||||
* bulk). NULL = uncapped; always NULL for ONE_TIME (single booking).
|
||||
*/
|
||||
export class AddCargoScopeQuantityCap1826000000000 implements MigrationInterface {
|
||||
name = 'AddCargoScopeQuantityCap1826000000000';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE freight.contract_cargo_scope ADD COLUMN IF NOT EXISTS quantity_cap NUMERIC(12,2);`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE freight.contract_cargo_scope DROP COLUMN IF EXISTS quantity_cap;`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import { MigrationInterface, QueryRunner, Table, TableIndex } from 'typeorm';
|
||||
|
||||
/**
|
||||
* Customer shipment requests for GENERAL customs (Path B) contracts. The customer
|
||||
* submits date + quantities; Global Logistics reviews, then creates the booking
|
||||
* on their behalf and per-booking clearance begins. Additive — no change to
|
||||
* existing tables; ONE_TIME contracts are unaffected.
|
||||
*/
|
||||
export class CreateBookingRequests1827000000000 implements MigrationInterface {
|
||||
name = 'CreateBookingRequests1827000000000';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.createTable(
|
||||
new Table({
|
||||
schema: 'freight',
|
||||
name: 'booking_requests',
|
||||
columns: [
|
||||
{ name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'gen_random_uuid()' },
|
||||
{ name: 'reference', type: 'varchar', length: '40', default: "''" },
|
||||
{ name: 'contract_id', type: 'uuid' },
|
||||
{ name: 'requested_by_user_id', type: 'uuid', isNullable: true },
|
||||
{ name: 'contract_route_id', type: 'uuid', isNullable: true },
|
||||
{ name: 'scheduled_date', type: 'timestamptz', isNullable: true },
|
||||
{ name: 'status', type: 'varchar', length: '16', default: "'PENDING'" },
|
||||
{ name: 'requested_lines', type: 'jsonb', default: "'{}'::jsonb" },
|
||||
{ name: 'notes', type: 'text', isNullable: true },
|
||||
{ name: 'created_booking_id', type: 'uuid', isNullable: true },
|
||||
{ name: 'reviewed_by_staff_id', type: 'uuid', isNullable: true },
|
||||
{ name: 'reviewed_at', type: 'timestamptz', isNullable: true },
|
||||
{ name: 'review_note', type: 'text', isNullable: true },
|
||||
{ name: 'created_at', type: 'timestamptz', default: 'now()' },
|
||||
{ name: 'updated_at', type: 'timestamptz', default: 'now()' },
|
||||
{ name: 'deleted_at', type: 'timestamptz', isNullable: true },
|
||||
],
|
||||
foreignKeys: [
|
||||
{
|
||||
columnNames: ['contract_id'],
|
||||
referencedSchema: 'freight',
|
||||
referencedTableName: 'contracts',
|
||||
referencedColumnNames: ['id'],
|
||||
onDelete: 'CASCADE',
|
||||
},
|
||||
{
|
||||
columnNames: ['created_booking_id'],
|
||||
referencedSchema: 'freight',
|
||||
referencedTableName: 'bookings',
|
||||
referencedColumnNames: ['id'],
|
||||
onDelete: 'SET NULL',
|
||||
},
|
||||
],
|
||||
}),
|
||||
true,
|
||||
);
|
||||
|
||||
await queryRunner.createIndex(
|
||||
'freight.booking_requests',
|
||||
new TableIndex({ name: 'idx_booking_requests_contract', columnNames: ['contract_id'] }),
|
||||
);
|
||||
await queryRunner.createIndex(
|
||||
'freight.booking_requests',
|
||||
new TableIndex({ name: 'idx_booking_requests_status', columnNames: ['status'] }),
|
||||
);
|
||||
await queryRunner.createIndex(
|
||||
'freight.booking_requests',
|
||||
new TableIndex({
|
||||
name: 'idx_booking_requests_contract_status',
|
||||
columnNames: ['contract_id', 'status'],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.dropTable('freight.booking_requests', true);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user