import { MigrationInterface, QueryRunner, Table, TableIndex, TableForeignKey, TableColumn, } from 'typeorm'; export class AddRuleEngineTablesAndCodes1748514000000 implements MigrationInterface { name = 'AddRuleEngineTablesAndCodes1748514000000'; public async up(queryRunner: QueryRunner): Promise { // ── 1. Add `code` column to existing tables ─────────────────────────── 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 = 'SERVICE_' || substring(id::text, 1, 8) WHERE code IS NULL`, ); await queryRunner.query( `ALTER TABLE freight.service_types ALTER COLUMN code SET NOT NULL`, ); 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 }), ); } 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 = 'CARGO_' || substring(id::text, 1, 8) WHERE code IS NULL`, ); await queryRunner.query( `ALTER TABLE freight.cargo_types ALTER COLUMN code SET NOT NULL`, ); 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 ──────────────────────────────────────────────── if (!(await queryRunner.hasTable('freight.surcharge_types'))) await queryRunner.createTable( new Table({ name: 'surcharge_types', schema: 'freight', columns: [ { name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' }, { name: 'code', type: 'varchar', length: '50', isNullable: false }, { name: 'name', type: 'varchar', length: '100', isNullable: false }, { name: 'description', type: 'text', isNullable: true }, { 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, ); await queryRunner.createIndex( 'freight.surcharge_types', new TableIndex({ name: 'IDX_surcharge_types_code', columnNames: ['code'], isUnique: true }), ); await queryRunner.createIndex( 'freight.surcharge_types', new TableIndex({ name: 'IDX_surcharge_types_is_active', columnNames: ['is_active'] }), ); // ── 3. surcharges ───────────────────────────────────────────────────── if (!(await queryRunner.hasTable('freight.surcharges'))) await queryRunner.createTable( new Table({ name: 'surcharges', schema: 'freight', columns: [ { name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' }, { name: 'surcharge_type_id', type: 'uuid', isNullable: false }, { name: 'fee_name', type: 'varchar', length: '255', isNullable: false }, { name: 'trigger_description', type: 'text', isNullable: true }, { name: 'calculation_method', type: 'enum', enum: ['PER_TON', 'FLAT_FEE', 'PERCENTAGE'], default: `'PER_TON'`, }, { name: 'rate', type: 'numeric', precision: 10, scale: 2, isNullable: false }, { name: 'currency', type: 'char', length: '3', default: `'USD'` }, { name: 'apply_to_rail', type: 'boolean', default: false }, { name: 'apply_to_first_mile', type: 'boolean', default: false }, { name: 'apply_to_last_mile', 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, ); await queryRunner.createForeignKey( 'freight.surcharges', new TableForeignKey({ name: 'FK_surcharges_surcharge_type', columnNames: ['surcharge_type_id'], referencedTableName: 'freight.surcharge_types', referencedColumnNames: ['id'], onDelete: 'RESTRICT', }), ); await queryRunner.createIndex( 'freight.surcharges', new TableIndex({ name: 'IDX_surcharges_surcharge_type_id', columnNames: ['surcharge_type_id'] }), ); await queryRunner.createIndex( 'freight.surcharges', new TableIndex({ name: 'IDX_surcharges_is_active', columnNames: ['is_active'] }), ); // ── 4. container_types ──────────────────────────────────────────────── if (!(await queryRunner.hasTable('freight.container_types'))) await queryRunner.createTable( new Table({ name: 'container_types', schema: 'freight', columns: [ { name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' }, { name: 'size_code', type: 'varchar', length: '20', isNullable: false }, { name: 'description', type: 'varchar', length: '100', isNullable: true }, { name: 'containers_per_wagon', type: 'int', isNullable: 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, ); await queryRunner.createIndex( 'freight.container_types', new TableIndex({ name: 'IDX_container_types_size_code', columnNames: ['size_code'], isUnique: true }), ); await queryRunner.createIndex( 'freight.container_types', new TableIndex({ name: 'IDX_container_types_is_active', columnNames: ['is_active'] }), ); // ── 5. weight_limit_rules ───────────────────────────────────────────── if (!(await queryRunner.hasTable('freight.weight_limit_rules'))) await queryRunner.createTable( new Table({ name: 'weight_limit_rules', schema: 'freight', columns: [ { name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' }, { name: 'container_type_id', type: 'uuid', isNullable: false }, { name: 'trade_direction', type: 'enum', enum: ['IMPORT', 'EXPORT', 'BOTH'], isNullable: false, }, { name: 'max_weight_tons', type: 'numeric', precision: 10, scale: 2, isNullable: false }, { name: 'warning_threshold_tons', type: 'numeric', precision: 10, scale: 2, isNullable: false }, { name: 'exceeded_action', type: 'enum', enum: ['WARNING_ONLY', 'HARD_BLOCK'], default: `'WARNING_ONLY'`, }, { name: 'surcharge_id', type: 'uuid', isNullable: true }, { 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, ); await queryRunner.createForeignKey( 'freight.weight_limit_rules', new TableForeignKey({ name: 'FK_weight_limit_rules_container_type', columnNames: ['container_type_id'], referencedTableName: 'freight.container_types', referencedColumnNames: ['id'], onDelete: 'RESTRICT', }), ); await queryRunner.createForeignKey( 'freight.weight_limit_rules', new TableForeignKey({ name: 'FK_weight_limit_rules_surcharge', columnNames: ['surcharge_id'], referencedTableName: 'freight.surcharges', referencedColumnNames: ['id'], onDelete: 'SET NULL', }), ); await queryRunner.createIndex( 'freight.weight_limit_rules', new TableIndex({ name: 'IDX_weight_limit_rules_container_type_id', columnNames: ['container_type_id'] }), ); await queryRunner.createIndex( 'freight.weight_limit_rules', new TableIndex({ name: 'IDX_weight_limit_rules_surcharge_id', columnNames: ['surcharge_id'] }), ); await queryRunner.createIndex( 'freight.weight_limit_rules', new TableIndex({ name: 'IDX_weight_limit_rules_is_active', columnNames: ['is_active'] }), ); // ── 6. priority_rules ───────────────────────────────────────────────── if (!(await queryRunner.hasTable('freight.priority_rules'))) await queryRunner.createTable( new Table({ name: 'priority_rules', schema: 'freight', columns: [ { name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' }, { name: 'priority_type', type: 'enum', enum: ['USD_PAYER', 'RAIL_AND_FORWARDING', 'GOVERNMENT_ACCOUNT', 'HIGH_VOLUME_SHIPMENT'], isNullable: false, }, { name: 'rule_name', type: 'varchar', length: '255', isNullable: false }, { name: 'description', type: 'text', isNullable: true }, { name: 'activation_condition', type: 'text', isNullable: true }, { name: 'bonus_points', type: 'int', default: 0 }, { name: 'is_active', type: 'boolean', default: false }, { 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.priority_rules', new TableIndex({ name: 'IDX_priority_rules_priority_type', columnNames: ['priority_type'], isUnique: true }), ); await queryRunner.createIndex( 'freight.priority_rules', new TableIndex({ name: 'IDX_priority_rules_is_active', columnNames: ['is_active'] }), ); } public async down(queryRunner: QueryRunner): Promise { await queryRunner.dropTable('freight.priority_rules', true); await queryRunner.dropTable('freight.weight_limit_rules', true); await queryRunner.dropTable('freight.container_types', true); await queryRunner.dropTable('freight.surcharges', true); await queryRunner.dropTable('freight.surcharge_types', true); await queryRunner.dropIndex('freight.cargo_types', 'IDX_cargo_types_code'); await queryRunner.dropColumn('freight.cargo_types', 'code'); await queryRunner.dropIndex('freight.service_types', 'IDX_service_types_code'); await queryRunner.dropColumn('freight.service_types', 'code'); } }