Merge branch 'dev' into freight/fix/type-errors

This commit is contained in:
Nathnael Wondisha
2026-06-25 09:27:14 +03:00
committed by GitHub
409 changed files with 26637 additions and 10747 deletions

View File

@@ -0,0 +1,35 @@
import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
export class AddVehicleCodeAndPlates1810000000002 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
const hasCode = await queryRunner.hasColumn('freight.vehicles', 'code');
if (!hasCode) {
await queryRunner.addColumn(
'freight.vehicles',
new TableColumn({ name: 'code', type: 'varchar', isNullable: true }),
);
}
const hasPower = await queryRunner.hasColumn('freight.vehicles', 'power_plate_no');
if (!hasPower) {
await queryRunner.addColumn(
'freight.vehicles',
new TableColumn({ name: 'power_plate_no', type: 'varchar', isNullable: true }),
);
}
const hasTrailer = await queryRunner.hasColumn('freight.vehicles', 'trailer_plate_no');
if (!hasTrailer) {
await queryRunner.addColumn(
'freight.vehicles',
new TableColumn({ name: 'trailer_plate_no', type: 'varchar', isNullable: true }),
);
}
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropColumn('freight.vehicles', 'trailer_plate_no');
await queryRunner.dropColumn('freight.vehicles', 'power_plate_no');
await queryRunner.dropColumn('freight.vehicles', 'code');
}
}

View File

@@ -0,0 +1,23 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
/**
* Consolidation is now system-managed: the backend consolidates partial-wagon
* container bookings automatically, derived from the container quantities. The
* `allow_consolidation` opt-in flag is therefore redundant and is dropped.
* `consolidation_partner_id` (the actual pairing link) is unaffected.
*/
export class DropAllowConsolidation1820000000000 implements MigrationInterface {
name = 'DropAllowConsolidation1820000000000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE freight.bookings DROP COLUMN IF EXISTS allow_consolidation;`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS allow_consolidation BOOLEAN NOT NULL DEFAULT false;`,
);
}
}

View File

@@ -0,0 +1,61 @@
import { MigrationInterface, QueryRunner, Table, TableIndex } from 'typeorm';
/**
* Multi-route general contracts: a contract may reserve quantity across several
* routes. Each (contract, route, container type) is a row here; drawdown orders
* reference the route line they drew from via booking_orders.route_line_id.
*/
export class CreateContractRouteLines1820000000001
implements MigrationInterface
{
name = 'CreateContractRouteLines1820000000001';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
schema: 'freight',
name: 'contract_route_lines',
columns: [
{ name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'gen_random_uuid()' },
{ name: 'contract_booking_id', type: 'uuid' },
{ name: 'origin_yard_id', type: 'uuid' },
{ name: 'destination_yard_id', type: 'uuid' },
{ name: 'container_type_id', type: 'uuid', isNullable: true },
{ name: 'quantity', type: 'numeric', precision: 12, scale: 3 },
{ name: 'created_at', type: 'timestamptz', default: 'now()' },
{ name: 'updated_at', type: 'timestamptz', default: 'now()' },
{ name: 'deleted_at', type: 'timestamptz', isNullable: true },
],
foreignKeys: [
{
columnNames: ['contract_booking_id'],
referencedSchema: 'freight',
referencedTableName: 'bookings',
referencedColumnNames: ['id'],
onDelete: 'CASCADE',
},
],
}),
true,
);
await queryRunner.createIndex(
'freight.contract_route_lines',
new TableIndex({
name: 'idx_contract_route_lines_contract',
columnNames: ['contract_booking_id'],
}),
);
await queryRunner.query(
`ALTER TABLE freight.booking_orders ADD COLUMN IF NOT EXISTS route_line_id uuid;`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE freight.booking_orders DROP COLUMN IF EXISTS route_line_id;`,
);
await queryRunner.dropTable('freight.contract_route_lines', true);
}
}

View File

@@ -0,0 +1,66 @@
import { MigrationInterface, QueryRunner, Table, TableIndex } from 'typeorm';
/**
* Per-document GL review for the post-counter-sign clearance gate. One row per
* required clearance document; GL marks each APPROVED or QUERIED before the
* booking can proceed to operations.
*/
export class CreateBookingDocumentReview1820000000002
implements MigrationInterface
{
name = 'CreateBookingDocumentReview1820000000002';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
schema: 'freight',
name: 'booking_document_review',
columns: [
{ name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'gen_random_uuid()' },
{ name: 'booking_id', type: 'uuid' },
{ name: 'setting_code', type: 'varchar', length: '128' },
{ name: 'file_key', type: 'varchar', length: '128' },
{ name: 'file_record_id', type: 'uuid', isNullable: true },
{ name: 'status', type: 'varchar', length: '20', default: "'PENDING'" },
{ name: 'note', type: 'text', isNullable: true },
{ name: 'reviewed_by_staff_id', type: 'uuid', isNullable: true },
{ name: 'reviewed_at', type: 'timestamptz', 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: ['booking_id'],
referencedSchema: 'freight',
referencedTableName: 'bookings',
referencedColumnNames: ['id'],
onDelete: 'CASCADE',
},
],
}),
true,
);
await queryRunner.createIndex(
'freight.booking_document_review',
new TableIndex({ name: 'idx_booking_document_review_booking', columnNames: ['booking_id'] }),
);
await queryRunner.createIndex(
'freight.booking_document_review',
new TableIndex({ name: 'idx_booking_document_review_status', columnNames: ['status'] }),
);
await queryRunner.createIndex(
'freight.booking_document_review',
new TableIndex({
name: 'uq_booking_document_review_doc',
columnNames: ['booking_id', 'setting_code', 'file_key'],
isUnique: true,
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('freight.booking_document_review', true);
}
}

View File

@@ -0,0 +1,39 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
/**
* Staff price adjustment: an optional override of a booking's computed total,
* with who/when/why. When set, the customer sees the adjusted total + a badge.
*/
export class AddPriceAdjustment1820000000003 implements MigrationInterface {
name = 'AddPriceAdjustment1820000000003';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS adjusted_total_amount numeric(14,2);`,
);
await queryRunner.query(
`ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS adjusted_by_staff_id uuid;`,
);
await queryRunner.query(
`ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS adjusted_at timestamptz;`,
);
await queryRunner.query(
`ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS adjustment_reason text;`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE freight.bookings DROP COLUMN IF EXISTS adjustment_reason;`,
);
await queryRunner.query(
`ALTER TABLE freight.bookings DROP COLUMN IF EXISTS adjusted_at;`,
);
await queryRunner.query(
`ALTER TABLE freight.bookings DROP COLUMN IF EXISTS adjusted_by_staff_id;`,
);
await queryRunner.query(
`ALTER TABLE freight.bookings DROP COLUMN IF EXISTS adjusted_total_amount;`,
);
}
}

View File

@@ -0,0 +1,187 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
/**
* Fold the `surcharge_types` table into self-describing rates.
*
* Previously a surcharge was a separate row {trigger_condition, rate_id}. Now
* each rate carries its own `applies_to` (friendly category) and `trigger`
* (ALWAYS = base freight, otherwise a surcharge condition), plus an optional
* `cargo_type_id` for bulk leaf commodities. The rule engine reads triggers
* directly off LIVE rates, so the join table is no longer needed.
*
* This migration:
* 1. adds applies_to / trigger / cargo_type_id to rates and backfills them
* from the existing rate_type matrix,
* 2. repoints booking_cargo_modifier from surcharge_type_id → rate_id
* (backfilled via surcharge_types.rate_id),
* 3. drops surcharge_types and its FK.
*/
export class FoldSurchargeTypesIntoRates1820000000004 implements MigrationInterface {
name = 'FoldSurchargeTypesIntoRates1820000000004';
public async up(queryRunner: QueryRunner): Promise<void> {
// ── 1. New rate columns ────────────────────────────────────────────────
await queryRunner.query(`
ALTER TABLE freight.rates
ADD COLUMN IF NOT EXISTS applies_to varchar(20) NOT NULL DEFAULT 'OTHER',
ADD COLUMN IF NOT EXISTS "trigger" varchar(20) NOT NULL DEFAULT 'ALWAYS',
ADD COLUMN IF NOT EXISTS cargo_type_id uuid NULL;
`);
await queryRunner.query(`
ALTER TABLE freight.rates
ADD CONSTRAINT "FK_rates_cargo_type_id"
FOREIGN KEY (cargo_type_id) REFERENCES freight.cargo_types(id)
ON DELETE SET NULL;
`);
await queryRunner.query(
`CREATE INDEX IF NOT EXISTS "IDX_rates_trigger" ON freight.rates ("trigger");`,
);
// ── 1a. Backfill applies_to from the legacy rate_type matrix ────────────
await queryRunner.query(`
UPDATE freight.rates SET applies_to = CASE
WHEN rate_type IN ('CONTAINER_IMPORT','CONTAINER_EXPORT','CONTAINER_WITH_RETURN') THEN 'CONTAINER'
WHEN rate_type IN ('BULK_IMPORT','BULK_EXPORT') THEN 'BULK'
WHEN rate_type IN ('INTERCITY_CONTAINER','INTERCITY_BULK') THEN 'INTERCITY'
WHEN rate_type = 'FIRST_MILE' THEN 'FIRST_MILE'
WHEN rate_type = 'LAST_MILE' THEN 'LAST_MILE'
ELSE 'OTHER'
END;
`);
// ── 1b. Backfill trigger from the legacy rate_type matrix ───────────────
await queryRunner.query(`
UPDATE freight.rates SET "trigger" = CASE
WHEN rate_type = 'HAZARD_SURCHARGE' THEN 'HAZARDOUS'
WHEN rate_type = 'REEFER_SURCHARGE' THEN 'REEFER'
WHEN rate_type = 'OVERWEIGHT_PER_TON' THEN 'OVERWEIGHT'
WHEN rate_type = 'DOUBLE_HANDLING' THEN 'SHIPPING_LINE'
WHEN rate_type = 'LASHING' THEN 'CONSOLIDATION'
WHEN rate_type = 'CANCELLATION_FEE' THEN 'CANCELLATION'
WHEN rate_type = 'DEMURRAGE' THEN 'DEMURRAGE'
WHEN rate_type = 'PIL_EXTRA_FEE' THEN 'PIL_EXTRA_FEE'
ELSE 'ALWAYS'
END;
`);
// Align the trigger to the actual surcharge_types mapping where one exists
// (covers any rate wired as a surcharge with a non-obvious rate_type).
await queryRunner.query(`
UPDATE freight.rates r SET "trigger" = m.trig
FROM (
SELECT st.rate_id, CASE st.trigger_condition
WHEN 'CARGO_FLAG_HAZARDOUS' THEN 'HAZARDOUS'
WHEN 'CARGO_FLAG_REEFER' THEN 'REEFER'
WHEN 'VGM_EXCEEDS_LIMIT' THEN 'OVERWEIGHT'
WHEN 'SHIPPING_LINE_MAPPED' THEN 'SHIPPING_LINE'
WHEN 'CONSOLIDATION_ENABLED' THEN 'CONSOLIDATION'
ELSE 'ALWAYS'
END AS trig
FROM freight.surcharge_types st
WHERE st.rate_id IS NOT NULL AND st.deleted_at IS NULL
) m
WHERE r.id = m.rate_id AND m.trig <> 'ALWAYS';
`);
// ── 2. Repoint booking_cargo_modifier to rate_id ────────────────────────
await queryRunner.query(`
ALTER TABLE freight.booking_cargo_modifier
ADD COLUMN IF NOT EXISTS rate_id uuid NULL;
`);
await queryRunner.query(`
UPDATE freight.booking_cargo_modifier bcm
SET rate_id = st.rate_id
FROM freight.surcharge_types st
WHERE bcm.surcharge_type_id = st.id AND st.rate_id IS NOT NULL;
`);
// Rows whose surcharge lost its rate can't be repointed — they reference a
// now-defunct surcharge. Remove them so the NOT NULL + FK can be enforced.
await queryRunner.query(`
DELETE FROM freight.booking_cargo_modifier WHERE rate_id IS NULL;
`);
await queryRunner.query(`
ALTER TABLE freight.booking_cargo_modifier
ALTER COLUMN rate_id SET NOT NULL;
`);
// Drop the old FK + column + index for surcharge_type_id.
await queryRunner.query(`
ALTER TABLE freight.booking_cargo_modifier
DROP CONSTRAINT IF EXISTS "FK_booking_cargo_modifier_surcharge_type_id";
`);
await queryRunner.query(
`DROP INDEX IF EXISTS freight."IDX_booking_cargo_modifier_surcharge_type_id";`,
);
await queryRunner.query(`
ALTER TABLE freight.booking_cargo_modifier
DROP COLUMN IF EXISTS surcharge_type_id;
`);
await queryRunner.query(`
ALTER TABLE freight.booking_cargo_modifier
ADD CONSTRAINT "FK_booking_cargo_modifier_rate_id"
FOREIGN KEY (rate_id) REFERENCES freight.rates(id);
`);
await queryRunner.query(
`CREATE INDEX IF NOT EXISTS "IDX_booking_cargo_modifier_rate_id" ON freight.booking_cargo_modifier (rate_id);`,
);
// ── 3. Drop the surcharge_types table ───────────────────────────────────
await queryRunner.query(`DROP TABLE IF EXISTS freight.surcharge_types;`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
// Recreate surcharge_types (structure only — data is not restored).
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS freight.surcharge_types (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
code varchar(40) NOT NULL,
label varchar(100),
trigger_condition varchar(50),
rate_id uuid,
is_active boolean NOT NULL DEFAULT true,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
deleted_at timestamptz
);
`);
await queryRunner.query(
`CREATE UNIQUE INDEX IF NOT EXISTS "IDX_surcharge_types_code" ON freight.surcharge_types (code);`,
);
// Restore booking_cargo_modifier.surcharge_type_id (nullable; not backfilled).
await queryRunner.query(`
ALTER TABLE freight.booking_cargo_modifier
DROP CONSTRAINT IF EXISTS "FK_booking_cargo_modifier_rate_id";
`);
await queryRunner.query(
`DROP INDEX IF EXISTS freight."IDX_booking_cargo_modifier_rate_id";`,
);
await queryRunner.query(`
ALTER TABLE freight.booking_cargo_modifier
ADD COLUMN IF NOT EXISTS surcharge_type_id uuid NULL;
`);
await queryRunner.query(`
ALTER TABLE freight.booking_cargo_modifier DROP COLUMN IF EXISTS rate_id;
`);
// Drop the new rate columns.
await queryRunner.query(
`DROP INDEX IF EXISTS freight."IDX_rates_trigger";`,
);
await queryRunner.query(`
ALTER TABLE freight.rates DROP CONSTRAINT IF EXISTS "FK_rates_cargo_type_id";
`);
await queryRunner.query(`
ALTER TABLE freight.rates
DROP COLUMN IF EXISTS cargo_type_id,
DROP COLUMN IF EXISTS "trigger",
DROP COLUMN IF EXISTS applies_to;
`);
}
}

View File

@@ -0,0 +1,37 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
/**
* Contract validity window. When the backoffice accepts a price-confirmed
* booking, staff define how many days the contract stays valid. The window runs
* from the accept moment (valid_from) through valid_from + N days (valid_until).
* Outside that window the contract is considered expired.
*/
export class AddContractValidityWindow1820000000005
implements MigrationInterface
{
name = 'AddContractValidityWindow1820000000005';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS contract_validity_days integer;`,
);
await queryRunner.query(
`ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS contract_valid_from timestamptz;`,
);
await queryRunner.query(
`ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS contract_valid_until timestamptz;`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE freight.bookings DROP COLUMN IF EXISTS contract_valid_until;`,
);
await queryRunner.query(
`ALTER TABLE freight.bookings DROP COLUMN IF EXISTS contract_valid_from;`,
);
await queryRunner.query(
`ALTER TABLE freight.bookings DROP COLUMN IF EXISTS contract_validity_days;`,
);
}
}

View File

@@ -0,0 +1,41 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
/**
* Booking now captures:
* - customs clearing as an explicit flag + the customs clearing agent name
* (shown when the service includes customs), and
* - first/last-mile pickup & delivery coordinates (lat/lng) alongside the
* existing address text, so the map picker can store and restore the pin.
*
* Shipping line is no longer collected from the booking form; the column stays
* for historical data and the (now dormant) shipping-line pricing trigger.
*/
export class AddCustomsAgentAndMileCoordinates1820000000006
implements MigrationInterface
{
name = 'AddCustomsAgentAndMileCoordinates1820000000006';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.bookings
ADD COLUMN IF NOT EXISTS first_mile_pickup_lat numeric(10,7) NULL,
ADD COLUMN IF NOT EXISTS first_mile_pickup_lng numeric(10,7) NULL,
ADD COLUMN IF NOT EXISTS last_mile_delivery_lat numeric(10,7) NULL,
ADD COLUMN IF NOT EXISTS last_mile_delivery_lng numeric(10,7) NULL,
ADD COLUMN IF NOT EXISTS customs_clearing_enabled boolean NOT NULL DEFAULT false,
ADD COLUMN IF NOT EXISTS customs_clearing_agent varchar(200) NULL;
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.bookings
DROP COLUMN IF EXISTS customs_clearing_agent,
DROP COLUMN IF EXISTS customs_clearing_enabled,
DROP COLUMN IF EXISTS last_mile_delivery_lng,
DROP COLUMN IF EXISTS last_mile_delivery_lat,
DROP COLUMN IF EXISTS first_mile_pickup_lng,
DROP COLUMN IF EXISTS first_mile_pickup_lat;
`);
}
}

View File

@@ -0,0 +1,50 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
/**
* General-contract drawdown order fields:
* - booking_order_lines.hazardous_quantity / reefer_quantity — per-order counts
* the customer enters when toggling hazardous/reefer; drive the surcharge
* rates on the spawned child booking.
* - bookings.is_reefer — booking-level refrigerated flag so REEFER_SURCHARGE
* applies to a contract order even when the container type is not a reefer.
* - contract_route_lines.km — road distance configured with the route; road
* orders bill KM × the PER_KM rate.
*
* NOTE: the shared dev DB has no applied migration history, so these columns
* are also hand-applied there. ADD COLUMN IF NOT EXISTS keeps that idempotent.
*/
export class AddGeneralContractOrderFields1820000000010
implements MigrationInterface
{
name = 'AddGeneralContractOrderFields1820000000010';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE freight.booking_order_lines ADD COLUMN IF NOT EXISTS hazardous_quantity numeric(12,3) NOT NULL DEFAULT 0;`,
);
await queryRunner.query(
`ALTER TABLE freight.booking_order_lines ADD COLUMN IF NOT EXISTS reefer_quantity numeric(12,3) NOT NULL DEFAULT 0;`,
);
await queryRunner.query(
`ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS is_reefer boolean NOT NULL DEFAULT false;`,
);
await queryRunner.query(
`ALTER TABLE freight.contract_route_lines ADD COLUMN IF NOT EXISTS km numeric(10,2);`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE freight.contract_route_lines DROP COLUMN IF EXISTS km;`,
);
await queryRunner.query(
`ALTER TABLE freight.bookings DROP COLUMN IF EXISTS is_reefer;`,
);
await queryRunner.query(
`ALTER TABLE freight.booking_order_lines DROP COLUMN IF EXISTS reefer_quantity;`,
);
await queryRunner.query(
`ALTER TABLE freight.booking_order_lines DROP COLUMN IF EXISTS hazardous_quantity;`,
);
}
}