mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 19:58:11 +00:00
complete rule engine and booking flow
This commit is contained in:
@@ -5,7 +5,7 @@ export class AddServiceTypesAndCargoTypes1748427600000 implements MigrationInter
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
// Create service_types table
|
||||
await queryRunner.createTable(
|
||||
if (!(await queryRunner.hasTable("freight.service_types"))) await queryRunner.createTable(
|
||||
new Table({
|
||||
name: "service_types",
|
||||
schema: "freight",
|
||||
@@ -109,7 +109,7 @@ export class AddServiceTypesAndCargoTypes1748427600000 implements MigrationInter
|
||||
);
|
||||
|
||||
// Create cargo_types table
|
||||
await queryRunner.createTable(
|
||||
if (!(await queryRunner.hasTable("freight.cargo_types"))) await queryRunner.createTable(
|
||||
new Table({
|
||||
name: "cargo_types",
|
||||
schema: "freight",
|
||||
|
||||
@@ -13,53 +13,69 @@ export class AddRuleEngineTablesAndCodes1748514000000 implements MigrationInterf
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
// ── 1. Add `code` column to existing tables ───────────────────────────
|
||||
|
||||
await queryRunner.addColumn(
|
||||
'freight.service_types',
|
||||
new TableColumn({
|
||||
name: 'code',
|
||||
type: 'varchar',
|
||||
length: '50',
|
||||
isNullable: true,
|
||||
}),
|
||||
if (!(await queryRunner.hasColumn('freight.service_types', 'code'))) {
|
||||
await queryRunner.addColumn(
|
||||
'freight.service_types',
|
||||
new TableColumn({
|
||||
name: 'code',
|
||||
type: 'varchar',
|
||||
length: '50',
|
||||
isNullable: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
await queryRunner.query(
|
||||
`UPDATE freight.service_types SET code = upper(replace(service_name, ' ', '_')) WHERE code IS NULL OR code = ''`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`UPDATE freight.service_types SET code = upper(replace(service_name, ' ', '_')) WHERE code IS NULL`,
|
||||
`UPDATE freight.service_types SET code = 'SERVICE_' || substring(id::text, 1, 8) WHERE code IS NULL`,
|
||||
);
|
||||
await queryRunner.changeColumn(
|
||||
'freight.service_types',
|
||||
'code',
|
||||
new TableColumn({ name: 'code', type: 'varchar', length: '50', isNullable: false }),
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE freight.service_types ALTER COLUMN code SET NOT NULL`,
|
||||
);
|
||||
await queryRunner.createIndex(
|
||||
'freight.service_types',
|
||||
new TableIndex({ name: 'IDX_service_types_code', columnNames: ['code'], isUnique: true }),
|
||||
const serviceTypesCodeIdx = await queryRunner.query(
|
||||
`SELECT 1 FROM pg_indexes WHERE schemaname = 'freight' AND indexname = 'idx_service_types_code' LIMIT 1`,
|
||||
);
|
||||
if (serviceTypesCodeIdx.length === 0) {
|
||||
await queryRunner.createIndex(
|
||||
'freight.service_types',
|
||||
new TableIndex({ name: 'IDX_service_types_code', columnNames: ['code'], isUnique: true }),
|
||||
);
|
||||
}
|
||||
|
||||
await queryRunner.addColumn(
|
||||
'freight.cargo_types',
|
||||
new TableColumn({
|
||||
name: 'code',
|
||||
type: 'varchar',
|
||||
length: '50',
|
||||
isNullable: true,
|
||||
}),
|
||||
if (!(await queryRunner.hasColumn('freight.cargo_types', 'code'))) {
|
||||
await queryRunner.addColumn(
|
||||
'freight.cargo_types',
|
||||
new TableColumn({
|
||||
name: 'code',
|
||||
type: 'varchar',
|
||||
length: '50',
|
||||
isNullable: true,
|
||||
}),
|
||||
);
|
||||
}
|
||||
await queryRunner.query(
|
||||
`UPDATE freight.cargo_types SET code = upper(replace(cargo_type_name, ' ', '_')) WHERE code IS NULL OR code = ''`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`UPDATE freight.cargo_types SET code = upper(replace(cargo_type_name, ' ', '_')) WHERE code IS NULL`,
|
||||
`UPDATE freight.cargo_types SET code = 'CARGO_' || substring(id::text, 1, 8) WHERE code IS NULL`,
|
||||
);
|
||||
await queryRunner.changeColumn(
|
||||
'freight.cargo_types',
|
||||
'code',
|
||||
new TableColumn({ name: 'code', type: 'varchar', length: '50', isNullable: false }),
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE freight.cargo_types ALTER COLUMN code SET NOT NULL`,
|
||||
);
|
||||
await queryRunner.createIndex(
|
||||
'freight.cargo_types',
|
||||
new TableIndex({ name: 'IDX_cargo_types_code', columnNames: ['code'], isUnique: true }),
|
||||
const cargoTypesCodeIdx = await queryRunner.query(
|
||||
`SELECT 1 FROM pg_indexes WHERE schemaname = 'freight' AND indexname = 'idx_cargo_types_code' LIMIT 1`,
|
||||
);
|
||||
if (cargoTypesCodeIdx.length === 0) {
|
||||
await queryRunner.createIndex(
|
||||
'freight.cargo_types',
|
||||
new TableIndex({ name: 'IDX_cargo_types_code', columnNames: ['code'], isUnique: true }),
|
||||
);
|
||||
}
|
||||
|
||||
// ── 2. surcharge_types ────────────────────────────────────────────────
|
||||
|
||||
await queryRunner.createTable(
|
||||
if (!(await queryRunner.hasTable('freight.surcharge_types'))) await queryRunner.createTable(
|
||||
new Table({
|
||||
name: 'surcharge_types',
|
||||
schema: 'freight',
|
||||
@@ -87,7 +103,7 @@ export class AddRuleEngineTablesAndCodes1748514000000 implements MigrationInterf
|
||||
|
||||
// ── 3. surcharges ─────────────────────────────────────────────────────
|
||||
|
||||
await queryRunner.createTable(
|
||||
if (!(await queryRunner.hasTable('freight.surcharges'))) await queryRunner.createTable(
|
||||
new Table({
|
||||
name: 'surcharges',
|
||||
schema: 'freight',
|
||||
@@ -136,7 +152,7 @@ export class AddRuleEngineTablesAndCodes1748514000000 implements MigrationInterf
|
||||
|
||||
// ── 4. container_types ────────────────────────────────────────────────
|
||||
|
||||
await queryRunner.createTable(
|
||||
if (!(await queryRunner.hasTable('freight.container_types'))) await queryRunner.createTable(
|
||||
new Table({
|
||||
name: 'container_types',
|
||||
schema: 'freight',
|
||||
@@ -164,7 +180,7 @@ export class AddRuleEngineTablesAndCodes1748514000000 implements MigrationInterf
|
||||
|
||||
// ── 5. weight_limit_rules ─────────────────────────────────────────────
|
||||
|
||||
await queryRunner.createTable(
|
||||
if (!(await queryRunner.hasTable('freight.weight_limit_rules'))) await queryRunner.createTable(
|
||||
new Table({
|
||||
name: 'weight_limit_rules',
|
||||
schema: 'freight',
|
||||
@@ -229,7 +245,7 @@ export class AddRuleEngineTablesAndCodes1748514000000 implements MigrationInterf
|
||||
|
||||
// ── 6. priority_rules ─────────────────────────────────────────────────
|
||||
|
||||
await queryRunner.createTable(
|
||||
if (!(await queryRunner.hasTable('freight.priority_rules'))) await queryRunner.createTable(
|
||||
new Table({
|
||||
name: 'priority_rules',
|
||||
schema: 'freight',
|
||||
|
||||
@@ -0,0 +1,544 @@
|
||||
import {
|
||||
MigrationInterface,
|
||||
QueryRunner,
|
||||
Table,
|
||||
TableForeignKey,
|
||||
TableIndex,
|
||||
TableUnique,
|
||||
} from 'typeorm';
|
||||
|
||||
export class ItmlsFullSchemaRewrite1748600000000 implements MigrationInterface {
|
||||
name = 'ItmlsFullSchemaRewrite1748600000000';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
// ── container_types ───────────────────────────────────────────────────
|
||||
await queryRunner.query(`
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE freight.container_types RENAME COLUMN size_code TO code;
|
||||
EXCEPTION WHEN undefined_column THEN NULL;
|
||||
END $$;
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE freight.container_types RENAME COLUMN description TO label;
|
||||
EXCEPTION WHEN undefined_column THEN NULL;
|
||||
END $$;
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.container_types
|
||||
ADD COLUMN IF NOT EXISTS size_ft SMALLINT,
|
||||
ADD COLUMN IF NOT EXISTS is_reefer BOOLEAN NOT NULL DEFAULT false,
|
||||
ADD COLUMN IF NOT EXISTS is_open_top BOOLEAN NOT NULL DEFAULT false,
|
||||
ADD COLUMN IF NOT EXISTS display_order INT NOT NULL DEFAULT 1;
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.container_types
|
||||
ADD COLUMN IF NOT EXISTS wagons_per_unit NUMERIC(4,2);
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
UPDATE freight.container_types
|
||||
SET wagons_per_unit = CASE
|
||||
WHEN containers_per_wagon > 0 THEN ROUND(1.0 / containers_per_wagon, 2)
|
||||
ELSE 1.00
|
||||
END
|
||||
WHERE wagons_per_unit IS NULL;
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
UPDATE freight.container_types
|
||||
SET size_ft = CASE WHEN code LIKE '40%' OR code LIKE '%40%' THEN 40 ELSE 20 END
|
||||
WHERE size_ft IS NULL;
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.container_types
|
||||
ALTER COLUMN wagons_per_unit SET NOT NULL,
|
||||
DROP COLUMN IF EXISTS containers_per_wagon;
|
||||
`);
|
||||
|
||||
// ── weight_limit_rules ──────────────────────────────────────────────────
|
||||
await queryRunner.query(`
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE freight.weight_limit_rules RENAME COLUMN max_weight_tons TO max_vgm_tons;
|
||||
EXCEPTION WHEN undefined_column THEN NULL;
|
||||
END $$;
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.weight_limit_rules
|
||||
ALTER COLUMN max_vgm_tons TYPE NUMERIC(8,3);
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.weight_limit_rules
|
||||
ADD COLUMN IF NOT EXISTS effective_from DATE NOT NULL DEFAULT CURRENT_DATE,
|
||||
ADD COLUMN IF NOT EXISTS effective_to DATE;
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.weight_limit_rules
|
||||
DROP COLUMN IF EXISTS warning_threshold_tons,
|
||||
DROP COLUMN IF EXISTS exceeded_action,
|
||||
DROP COLUMN IF EXISTS surcharge_id,
|
||||
DROP COLUMN IF EXISTS is_active;
|
||||
`);
|
||||
|
||||
// ── priority_rules ────────────────────────────────────────────────────
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.priority_rules
|
||||
ADD COLUMN IF NOT EXISTS code VARCHAR(40),
|
||||
ADD COLUMN IF NOT EXISTS label VARCHAR(100),
|
||||
ADD COLUMN IF NOT EXISTS score INT NOT NULL DEFAULT 0,
|
||||
ADD COLUMN IF NOT EXISTS condition_currency VARCHAR(5);
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
UPDATE freight.priority_rules
|
||||
SET code = COALESCE(code, upper(priority_type::text)),
|
||||
label = COALESCE(label, rule_name),
|
||||
score = COALESCE(score, bonus_points)
|
||||
WHERE code IS NULL OR label IS NULL;
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.priority_rules
|
||||
DROP COLUMN IF EXISTS priority_type,
|
||||
DROP COLUMN IF EXISTS rule_name,
|
||||
DROP COLUMN IF EXISTS bonus_points,
|
||||
DROP COLUMN IF EXISTS activation_condition,
|
||||
DROP COLUMN IF EXISTS description;
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.priority_rules
|
||||
ALTER COLUMN code SET NOT NULL,
|
||||
ALTER COLUMN label SET NOT NULL;
|
||||
`);
|
||||
await queryRunner.createIndex(
|
||||
'freight.priority_rules',
|
||||
new TableIndex({ name: 'UQ_priority_rules_code', columnNames: ['code'], isUnique: true }),
|
||||
);
|
||||
|
||||
// ── rates (before surcharge_types.rate_id) ────────────────────────────
|
||||
await queryRunner.createTable(
|
||||
new Table({
|
||||
name: 'rates',
|
||||
schema: 'freight',
|
||||
columns: [
|
||||
{ name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' },
|
||||
{ name: 'rate_type', type: 'varchar', length: '50' },
|
||||
{ name: 'container_type_id', type: 'uuid', isNullable: true },
|
||||
{ name: 'trade_direction', type: 'varchar', length: '10', isNullable: true },
|
||||
{ name: 'currency', type: 'varchar', length: '5' },
|
||||
{ name: 'rate_value', type: 'numeric', precision: 14, scale: 4 },
|
||||
{ name: 'rate_unit', type: 'varchar', length: '30' },
|
||||
{ name: 'status', type: 'varchar', length: '20', default: "'DRAFT'" },
|
||||
{ name: 'proposed_by_staff_id', type: 'uuid' },
|
||||
{ name: 'approved_by_ceo_id', type: 'uuid', isNullable: true },
|
||||
{ name: 'approved_at', type: 'timestamptz', isNullable: true },
|
||||
{ name: 'effective_from', type: 'date' },
|
||||
{ name: 'effective_to', type: 'date', isNullable: true },
|
||||
{ name: 'created_at', type: 'timestamptz', default: 'now()' },
|
||||
{ name: 'updated_at', type: 'timestamptz', default: 'now()' },
|
||||
{ name: 'deleted_at', type: 'timestamptz', isNullable: true },
|
||||
],
|
||||
}),
|
||||
true,
|
||||
);
|
||||
|
||||
// ── surcharge_types ───────────────────────────────────────────────────
|
||||
await queryRunner.query(`
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE freight.surcharge_types RENAME COLUMN name TO label;
|
||||
EXCEPTION WHEN undefined_column THEN NULL;
|
||||
END $$;
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.surcharge_types
|
||||
ADD COLUMN IF NOT EXISTS trigger_condition VARCHAR(50),
|
||||
ADD COLUMN IF NOT EXISTS rate_id UUID;
|
||||
`);
|
||||
await queryRunner.query(`ALTER TABLE freight.surcharge_types DROP COLUMN IF EXISTS description`);
|
||||
|
||||
await queryRunner.query(`DROP TABLE IF EXISTS freight.surcharges CASCADE`);
|
||||
|
||||
// ── yards ─────────────────────────────────────────────────────────────
|
||||
await queryRunner.createTable(
|
||||
new Table({
|
||||
name: 'yards',
|
||||
schema: 'freight',
|
||||
columns: [
|
||||
{ name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' },
|
||||
{ name: 'code', type: 'varchar', length: '20' },
|
||||
{ name: 'label', type: 'varchar', length: '100' },
|
||||
{ name: 'country', type: 'varchar', length: '50' },
|
||||
{ name: 'is_active', type: 'boolean', default: true },
|
||||
{ name: 'display_order', type: 'int', default: 1 },
|
||||
{ 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.yards',
|
||||
new TableIndex({ name: 'UQ_yards_code', columnNames: ['code'], isUnique: true }),
|
||||
);
|
||||
|
||||
// ── shipping_lines ────────────────────────────────────────────────────
|
||||
await queryRunner.createTable(
|
||||
new Table({
|
||||
name: 'shipping_lines',
|
||||
schema: 'freight',
|
||||
columns: [
|
||||
{ name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' },
|
||||
{ name: 'code', type: 'varchar', length: '20' },
|
||||
{ name: 'label', type: 'varchar', length: '100' },
|
||||
{ name: 'mapped_to_code', type: 'varchar', length: '20', isNullable: true },
|
||||
{ name: 'show_extra_fee_notice', type: 'boolean', default: false },
|
||||
{ name: 'is_active', type: 'boolean', default: true },
|
||||
{ name: 'created_at', type: 'timestamptz', default: 'now()' },
|
||||
{ name: 'updated_at', type: 'timestamptz', default: 'now()' },
|
||||
{ name: 'deleted_at', type: 'timestamptz', isNullable: true },
|
||||
],
|
||||
}),
|
||||
true,
|
||||
);
|
||||
|
||||
// ── approval_rules ────────────────────────────────────────────────────
|
||||
await queryRunner.createTable(
|
||||
new Table({
|
||||
name: 'approval_rules',
|
||||
schema: 'freight',
|
||||
columns: [
|
||||
{ name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' },
|
||||
{ name: 'requires_director_approval', type: 'boolean' },
|
||||
{ name: 'step_order', type: 'smallint' },
|
||||
{ name: 'required_role', type: 'varchar', length: '30' },
|
||||
{ name: 'action_label', type: 'varchar', length: '50' },
|
||||
{ name: 'blocks_role', type: 'varchar', length: '30', 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.createUniqueConstraint(
|
||||
'freight.approval_rules',
|
||||
new TableUnique({
|
||||
name: 'UQ_approval_rules_chain_step',
|
||||
columnNames: ['requires_director_approval', 'step_order'],
|
||||
}),
|
||||
);
|
||||
|
||||
// ── bookings ────────────────────────────────────────────────────────
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.bookings
|
||||
ADD COLUMN IF NOT EXISTS origin_yard_id UUID,
|
||||
ADD COLUMN IF NOT EXISTS destination_yard_id UUID,
|
||||
ADD COLUMN IF NOT EXISTS service_type_id UUID,
|
||||
ADD COLUMN IF NOT EXISTS cargo_type_id UUID,
|
||||
ADD COLUMN IF NOT EXISTS cargo_free_text VARCHAR(200),
|
||||
ADD COLUMN IF NOT EXISTS shipping_line_id UUID,
|
||||
ADD COLUMN IF NOT EXISTS pnr_code VARCHAR(50),
|
||||
ADD COLUMN IF NOT EXISTS customer_signed_at TIMESTAMPTZ,
|
||||
ADD COLUMN IF NOT EXISTS fully_executed_at TIMESTAMPTZ;
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
INSERT INTO freight.yards (id, code, label, country, is_active, display_order, created_at, updated_at)
|
||||
VALUES
|
||||
(uuid_generate_v4(), 'KALITY', 'Kality Rail Terminal', 'Ethiopia', true, 1, now(), now()),
|
||||
(uuid_generate_v4(), 'MOJO', 'Mojo Dry Port', 'Ethiopia', true, 2, now(), now()),
|
||||
(uuid_generate_v4(), 'DIRE_DAWA', 'Dire Dawa Yard', 'Ethiopia', true, 3, now(), now()),
|
||||
(uuid_generate_v4(), 'DJIB_PORT', 'Djibouti Port Terminal', 'Djibouti', true, 4, now(), now()),
|
||||
(uuid_generate_v4(), 'NAGAD', 'Nagad Terminal, Djibouti', 'Djibouti', true, 5, now(), now()),
|
||||
(uuid_generate_v4(), 'LEGACY_ORIGIN', 'Legacy Origin', 'Ethiopia', true, 99, now(), now()),
|
||||
(uuid_generate_v4(), 'LEGACY_DEST', 'Legacy Destination', 'Ethiopia', true, 100, now(), now())
|
||||
ON CONFLICT (code) DO NOTHING;
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
INSERT INTO freight.service_types (id, code, service_name, can_be_booked_alone, includes_first_mile, includes_last_mile, includes_customs, priority_bonus_points, is_active, display_order, created_at, updated_at)
|
||||
SELECT uuid_generate_v4(), 'RAIL', 'Rail Transport Only', true, false, false, false, 0, true, 1, now(), now()
|
||||
WHERE NOT EXISTS (SELECT 1 FROM freight.service_types LIMIT 1);
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
INSERT INTO freight.cargo_types (id, code, cargo_type_name, requires_director_approval, show_free_text_box, is_active, display_order, created_at, updated_at)
|
||||
SELECT uuid_generate_v4(), 'GENERAL', 'General Cargo', false, false, true, 1, now(), now()
|
||||
WHERE NOT EXISTS (SELECT 1 FROM freight.cargo_types LIMIT 1);
|
||||
`);
|
||||
|
||||
const hasServiceTypeCol = await queryRunner.query(`
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'freight' AND table_name = 'bookings' AND column_name = 'service_type'
|
||||
LIMIT 1;
|
||||
`);
|
||||
|
||||
if (hasServiceTypeCol.length > 0) {
|
||||
await queryRunner.query(`
|
||||
UPDATE freight.bookings b
|
||||
SET service_type_id = st.id
|
||||
FROM freight.service_types st
|
||||
WHERE b.service_type_id IS NULL
|
||||
AND (
|
||||
st.code = b.service_type
|
||||
OR upper(replace(st.service_name, ' ', '_')) = upper(b.service_type)
|
||||
OR st.code = upper(replace(b.service_type, ' ', '_'))
|
||||
);
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
UPDATE freight.bookings b
|
||||
SET cargo_type_id = ct.id
|
||||
FROM freight.cargo_types ct
|
||||
WHERE b.cargo_type_id IS NULL
|
||||
AND (
|
||||
ct.code = upper(b.freight_type)
|
||||
OR ct.code = upper(concat(b.freight_type, '_', coalesce(b.freight_subtype, '')))
|
||||
);
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
UPDATE freight.bookings b
|
||||
SET cargo_free_text = b.freight_subtype
|
||||
WHERE b.cargo_free_text IS NULL AND b.freight_subtype IS NOT NULL;
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
UPDATE freight.bookings b
|
||||
SET origin_yard_id = y.id
|
||||
FROM freight.yards y
|
||||
WHERE b.origin_yard_id IS NULL
|
||||
AND (y.label ILIKE b.origin_station OR y.code = upper(replace(b.origin_station, ' ', '_')));
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
UPDATE freight.bookings b
|
||||
SET destination_yard_id = y.id
|
||||
FROM freight.yards y
|
||||
WHERE b.destination_yard_id IS NULL
|
||||
AND (y.label ILIKE b.destination_station OR y.code = upper(replace(b.destination_station, ' ', '_')));
|
||||
`);
|
||||
}
|
||||
|
||||
const defaultServiceTypeId = await queryRunner.query(
|
||||
`SELECT id FROM freight.service_types ORDER BY display_order LIMIT 1`,
|
||||
);
|
||||
const defaultCargoTypeId = await queryRunner.query(
|
||||
`SELECT id FROM freight.cargo_types ORDER BY display_order LIMIT 1`,
|
||||
);
|
||||
const legacyOriginId = await queryRunner.query(
|
||||
`SELECT id FROM freight.yards WHERE code = 'LEGACY_ORIGIN' LIMIT 1`,
|
||||
);
|
||||
const legacyDestId = await queryRunner.query(
|
||||
`SELECT id FROM freight.yards WHERE code = 'LEGACY_DEST' LIMIT 1`,
|
||||
);
|
||||
|
||||
if (defaultServiceTypeId[0]?.id) {
|
||||
await queryRunner.query(
|
||||
`UPDATE freight.bookings SET service_type_id = $1 WHERE service_type_id IS NULL`,
|
||||
[defaultServiceTypeId[0].id],
|
||||
);
|
||||
}
|
||||
if (defaultCargoTypeId[0]?.id) {
|
||||
await queryRunner.query(
|
||||
`UPDATE freight.bookings SET cargo_type_id = $1 WHERE cargo_type_id IS NULL`,
|
||||
[defaultCargoTypeId[0].id],
|
||||
);
|
||||
}
|
||||
if (legacyOriginId[0]?.id) {
|
||||
await queryRunner.query(
|
||||
`UPDATE freight.bookings SET origin_yard_id = $1 WHERE origin_yard_id IS NULL`,
|
||||
[legacyOriginId[0].id],
|
||||
);
|
||||
}
|
||||
if (legacyDestId[0]?.id) {
|
||||
await queryRunner.query(
|
||||
`UPDATE freight.bookings SET destination_yard_id = $1 WHERE destination_yard_id IS NULL`,
|
||||
[legacyDestId[0].id],
|
||||
);
|
||||
}
|
||||
|
||||
const nullBookings = await queryRunner.query(
|
||||
`SELECT COUNT(*)::int AS cnt FROM freight.bookings WHERE service_type_id IS NULL OR cargo_type_id IS NULL OR origin_yard_id IS NULL OR destination_yard_id IS NULL`,
|
||||
);
|
||||
if (nullBookings[0]?.cnt > 0) {
|
||||
await queryRunner.query(`DELETE FROM freight.bookings WHERE service_type_id IS NULL OR cargo_type_id IS NULL OR origin_yard_id IS NULL OR destination_yard_id IS NULL`);
|
||||
}
|
||||
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.bookings
|
||||
ALTER COLUMN service_type_id SET NOT NULL,
|
||||
ALTER COLUMN cargo_type_id SET NOT NULL,
|
||||
ALTER COLUMN origin_yard_id SET NOT NULL,
|
||||
ALTER COLUMN destination_yard_id SET NOT NULL;
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.bookings
|
||||
DROP COLUMN IF EXISTS origin_station,
|
||||
DROP COLUMN IF EXISTS destination_station,
|
||||
DROP COLUMN IF EXISTS service_type,
|
||||
DROP COLUMN IF EXISTS freight_type,
|
||||
DROP COLUMN IF EXISTS freight_subtype,
|
||||
DROP COLUMN IF EXISTS containers,
|
||||
DROP COLUMN IF EXISTS first_mile_enabled,
|
||||
DROP COLUMN IF EXISTS last_mile_enabled,
|
||||
DROP COLUMN IF EXISTS is_refrigerated;
|
||||
`);
|
||||
|
||||
// ── booking_container ─────────────────────────────────────────────────
|
||||
await queryRunner.createTable(
|
||||
new Table({
|
||||
name: 'booking_container',
|
||||
schema: 'freight',
|
||||
columns: [
|
||||
{ name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' },
|
||||
{ name: 'booking_id', type: 'uuid' },
|
||||
{ name: 'container_type_id', type: 'uuid' },
|
||||
{ name: 'quantity', type: 'smallint' },
|
||||
{ name: 'vgm_per_unit_tons', type: 'numeric', precision: 10, scale: 3 },
|
||||
{ name: 'total_vgm_tons', type: 'numeric', precision: 12, scale: 3 },
|
||||
{ name: 'wagons_required', type: 'numeric', precision: 6, scale: 2 },
|
||||
{ name: 'weight_limit_rule_id', type: 'uuid', isNullable: true },
|
||||
{ name: 'is_overweight', type: 'boolean', default: false },
|
||||
{ name: 'overweight_excess_tons', type: 'numeric', precision: 10, scale: 3, 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.createTable(
|
||||
new Table({
|
||||
name: 'booking_rate_snapshot',
|
||||
schema: 'freight',
|
||||
columns: [
|
||||
{ name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' },
|
||||
{ name: 'booking_id', type: 'uuid' },
|
||||
{ name: 'rate_id', type: 'uuid' },
|
||||
{ name: 'rate_type', type: 'varchar', length: '50' },
|
||||
{ name: 'rate_value', type: 'numeric', precision: 14, scale: 4 },
|
||||
{ name: 'rate_unit', type: 'varchar', length: '30' },
|
||||
{ name: 'currency', type: 'varchar', length: '5' },
|
||||
{ name: 'snapshotted_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.createTable(
|
||||
new Table({
|
||||
name: 'booking_cargo_modifier',
|
||||
schema: 'freight',
|
||||
columns: [
|
||||
{ name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' },
|
||||
{ name: 'booking_id', type: 'uuid' },
|
||||
{ name: 'surcharge_type_id', type: 'uuid' },
|
||||
{ name: 'trigger_value', type: 'numeric', precision: 14, scale: 4, isNullable: true },
|
||||
{ name: 'calculated_amount', type: 'numeric', precision: 14, scale: 2 },
|
||||
{ name: 'rate_snapshot_id', type: 'uuid' },
|
||||
{ name: 'created_at', type: 'timestamptz', default: 'now()' },
|
||||
{ name: 'updated_at', type: 'timestamptz', default: 'now()' },
|
||||
{ name: 'deleted_at', type: 'timestamptz', isNullable: true },
|
||||
],
|
||||
}),
|
||||
true,
|
||||
);
|
||||
|
||||
await queryRunner.createTable(
|
||||
new Table({
|
||||
name: 'booking_approval_step',
|
||||
schema: 'freight',
|
||||
columns: [
|
||||
{ name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' },
|
||||
{ name: 'booking_id', type: 'uuid' },
|
||||
{ name: 'approval_rule_id', type: 'uuid' },
|
||||
{ name: 'step_order', type: 'smallint' },
|
||||
{ name: 'required_role', type: 'varchar', length: '30' },
|
||||
{ name: 'status', type: 'varchar', length: '20', default: "'PENDING'" },
|
||||
{ name: 'actioned_by_staff_id', type: 'uuid', isNullable: true },
|
||||
{ name: 'actioned_at', type: 'timestamptz', isNullable: true },
|
||||
{ name: 'remarks', 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,
|
||||
);
|
||||
|
||||
// Foreign keys
|
||||
await queryRunner.createForeignKey(
|
||||
'freight.surcharge_types',
|
||||
new TableForeignKey({
|
||||
name: 'FK_surcharge_types_rate_id',
|
||||
columnNames: ['rate_id'],
|
||||
referencedTableName: 'rates',
|
||||
referencedSchema: 'freight',
|
||||
referencedColumnNames: ['id'],
|
||||
}),
|
||||
);
|
||||
await queryRunner.createForeignKey(
|
||||
'freight.booking_container',
|
||||
new TableForeignKey({
|
||||
columnNames: ['booking_id'],
|
||||
referencedTableName: 'bookings',
|
||||
referencedSchema: 'freight',
|
||||
referencedColumnNames: ['id'],
|
||||
onDelete: 'CASCADE',
|
||||
}),
|
||||
);
|
||||
await queryRunner.createForeignKey(
|
||||
'freight.bookings',
|
||||
new TableForeignKey({
|
||||
columnNames: ['origin_yard_id'],
|
||||
referencedTableName: 'yards',
|
||||
referencedSchema: 'freight',
|
||||
referencedColumnNames: ['id'],
|
||||
}),
|
||||
);
|
||||
await queryRunner.createForeignKey(
|
||||
'freight.bookings',
|
||||
new TableForeignKey({
|
||||
columnNames: ['destination_yard_id'],
|
||||
referencedTableName: 'yards',
|
||||
referencedSchema: 'freight',
|
||||
referencedColumnNames: ['id'],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.dropTable('freight.booking_approval_step', true);
|
||||
await queryRunner.dropTable('freight.booking_cargo_modifier', true);
|
||||
await queryRunner.dropTable('freight.booking_rate_snapshot', true);
|
||||
await queryRunner.dropTable('freight.booking_container', true);
|
||||
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.bookings
|
||||
ADD COLUMN IF NOT EXISTS origin_station VARCHAR(255),
|
||||
ADD COLUMN IF NOT EXISTS destination_station VARCHAR(255),
|
||||
ADD COLUMN IF NOT EXISTS service_type VARCHAR(30),
|
||||
ADD COLUMN IF NOT EXISTS freight_type VARCHAR(20),
|
||||
ADD COLUMN IF NOT EXISTS freight_subtype VARCHAR(100),
|
||||
ADD COLUMN IF NOT EXISTS containers JSONB,
|
||||
ADD COLUMN IF NOT EXISTS first_mile_enabled BOOLEAN DEFAULT false,
|
||||
ADD COLUMN IF NOT EXISTS last_mile_enabled BOOLEAN DEFAULT false,
|
||||
ADD COLUMN IF NOT EXISTS is_refrigerated BOOLEAN DEFAULT false;
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.bookings
|
||||
DROP COLUMN IF EXISTS origin_yard_id,
|
||||
DROP COLUMN IF EXISTS destination_yard_id,
|
||||
DROP COLUMN IF EXISTS service_type_id,
|
||||
DROP COLUMN IF EXISTS cargo_type_id,
|
||||
DROP COLUMN IF EXISTS cargo_free_text,
|
||||
DROP COLUMN IF EXISTS shipping_line_id,
|
||||
DROP COLUMN IF EXISTS pnr_code,
|
||||
DROP COLUMN IF EXISTS customer_signed_at,
|
||||
DROP COLUMN IF EXISTS fully_executed_at;
|
||||
`);
|
||||
|
||||
await queryRunner.dropTable('freight.approval_rules', true);
|
||||
await queryRunner.dropTable('freight.shipping_lines', true);
|
||||
await queryRunner.dropTable('freight.yards', true);
|
||||
await queryRunner.dropTable('freight.rates', true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddBookingsConfigForeignKeys1748700000000 implements MigrationInterface {
|
||||
name = 'AddBookingsConfigForeignKeys1748700000000';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
// Ensure parent config rows exist for backfill
|
||||
await queryRunner.query(`
|
||||
INSERT INTO freight.service_types (id, code, service_name, can_be_booked_alone, includes_first_mile, includes_last_mile, includes_customs, priority_bonus_points, is_active, display_order, created_at, updated_at)
|
||||
SELECT uuid_generate_v4(), 'RAIL', 'Rail Transport Only', true, false, false, false, 0, true, 1, now(), now()
|
||||
WHERE NOT EXISTS (SELECT 1 FROM freight.service_types LIMIT 1);
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
INSERT INTO freight.cargo_types (id, code, cargo_type_name, requires_director_approval, show_free_text_box, is_active, display_order, created_at, updated_at)
|
||||
SELECT uuid_generate_v4(), 'GENERAL', 'General Cargo', false, false, true, 1, now(), now()
|
||||
WHERE NOT EXISTS (SELECT 1 FROM freight.cargo_types LIMIT 1);
|
||||
`);
|
||||
|
||||
// Clear orphan shipping_line references (nullable FK)
|
||||
await queryRunner.query(`
|
||||
UPDATE freight.bookings b
|
||||
SET shipping_line_id = NULL
|
||||
WHERE b.shipping_line_id IS NOT NULL
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM freight.shipping_lines sl WHERE sl.id = b.shipping_line_id
|
||||
);
|
||||
`);
|
||||
|
||||
// Backfill required FK columns
|
||||
await queryRunner.query(`
|
||||
UPDATE freight.bookings
|
||||
SET service_type_id = (SELECT id FROM freight.service_types ORDER BY display_order LIMIT 1)
|
||||
WHERE service_type_id IS NULL
|
||||
OR NOT EXISTS (SELECT 1 FROM freight.service_types st WHERE st.id = service_type_id);
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
UPDATE freight.bookings
|
||||
SET cargo_type_id = (SELECT id FROM freight.cargo_types ORDER BY display_order LIMIT 1)
|
||||
WHERE cargo_type_id IS NULL
|
||||
OR NOT EXISTS (SELECT 1 FROM freight.cargo_types ct WHERE ct.id = cargo_type_id);
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE freight.bookings
|
||||
ADD CONSTRAINT "FK_bookings_service_type_id"
|
||||
FOREIGN KEY (service_type_id)
|
||||
REFERENCES freight.service_types(id)
|
||||
ON DELETE RESTRICT;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE freight.bookings
|
||||
ADD CONSTRAINT "FK_bookings_cargo_type_id"
|
||||
FOREIGN KEY (cargo_type_id)
|
||||
REFERENCES freight.cargo_types(id)
|
||||
ON DELETE RESTRICT;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
DO $$ BEGIN
|
||||
ALTER TABLE freight.bookings
|
||||
ADD CONSTRAINT "FK_bookings_shipping_line_id"
|
||||
FOREIGN KEY (shipping_line_id)
|
||||
REFERENCES freight.shipping_lines(id)
|
||||
ON DELETE SET NULL;
|
||||
EXCEPTION
|
||||
WHEN duplicate_object THEN NULL;
|
||||
END $$;
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.bookings
|
||||
DROP CONSTRAINT IF EXISTS "FK_bookings_shipping_line_id";
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.bookings
|
||||
DROP CONSTRAINT IF EXISTS "FK_bookings_cargo_type_id";
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.bookings
|
||||
DROP CONSTRAINT IF EXISTS "FK_bookings_service_type_id";
|
||||
`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user