import { MigrationInterface, QueryRunner } from 'typeorm'; /** * Simplify the rate + weight-limit configuration model: * * 1. Drop the effective_from / effective_to validity window from both * `rates` and `weight_limit_rules`. Rates are now activated purely by * the approval workflow (status = LIVE) and weight limits are always * active for their container + direction. No time-travel scheduling. * * 2. Enforce "one rate per pattern" with partial unique indexes so the same * configuration (e.g. FIRST_MILE for a given container type) cannot be * duplicated. NULL scope columns are COALESCE-normalised because Postgres * treats NULLs as distinct in a plain unique index. * * This migration is destructive on the date columns — existing effective_* * values are dropped. */ export class SimplifyRatesAndWeightLimitRules1900000000000 implements MigrationInterface { name = 'SimplifyRatesAndWeightLimitRules1900000000000'; public async up(queryRunner: QueryRunner): Promise { // ── 1. De-duplicate existing data so the unique indexes can be created ── // Keep the most recently-created row per pattern, soft-delete the rest. await queryRunner.query(` WITH ranked AS ( SELECT id, row_number() OVER ( PARTITION BY rate_type, COALESCE(container_type_id, '00000000-0000-0000-0000-000000000000'::uuid), COALESCE(cargo_type_id, '00000000-0000-0000-0000-000000000000'::uuid), COALESCE(trade_direction, ''), rate_unit ORDER BY created_at DESC, id DESC ) AS rn FROM freight.rates WHERE deleted_at IS NULL AND status <> 'SUPERSEDED' ) UPDATE freight.rates r SET deleted_at = now() FROM ranked WHERE r.id = ranked.id AND ranked.rn > 1; `); await queryRunner.query(` WITH ranked AS ( SELECT id, row_number() OVER ( PARTITION BY container_type_id, trade_direction ORDER BY created_at DESC, id DESC ) AS rn FROM freight.weight_limit_rules WHERE deleted_at IS NULL ) UPDATE freight.weight_limit_rules w SET deleted_at = now() FROM ranked WHERE w.id = ranked.id AND ranked.rn > 1; `); // ── 2. Drop the effective-date indexes + columns ─────────────────────── await queryRunner.query(`DROP INDEX IF EXISTS freight."IDX_rates_effective_from";`); await queryRunner.query(`DROP INDEX IF EXISTS freight."IDX_weight_limit_rules_effective_from";`); // Indexes created by TypeORM's @Index carry generated hashed names — drop // any index that references the effective_from column defensively. await queryRunner.query(` DO $$ DECLARE idx record; BEGIN FOR idx IN SELECT indexname FROM pg_indexes WHERE schemaname = 'freight' AND tablename IN ('rates', 'weight_limit_rules') AND indexdef ILIKE '%effective_from%' LOOP EXECUTE format('DROP INDEX IF EXISTS freight.%I', idx.indexname); END LOOP; END $$; `); await queryRunner.query(`ALTER TABLE freight.rates DROP COLUMN IF EXISTS effective_from;`); await queryRunner.query(`ALTER TABLE freight.rates DROP COLUMN IF EXISTS effective_to;`); await queryRunner.query(`ALTER TABLE freight.weight_limit_rules DROP COLUMN IF EXISTS effective_from;`); await queryRunner.query(`ALTER TABLE freight.weight_limit_rules DROP COLUMN IF EXISTS effective_to;`); // ── 3. One-rate-per-pattern partial unique indexes ───────────────────── // The unit is part of the identity so a surcharge can legitimately carry two // rows that bill different ways (e.g. reefer PER_CONTAINER + reefer PER_TON), // while still blocking a true duplicate (same rateType + scope + unit). await queryRunner.query(` CREATE UNIQUE INDEX IF NOT EXISTS "UQ_rates_pattern" ON freight.rates ( rate_type, COALESCE(container_type_id, '00000000-0000-0000-0000-000000000000'::uuid), COALESCE(cargo_type_id, '00000000-0000-0000-0000-000000000000'::uuid), COALESCE(trade_direction, ''), rate_unit ) WHERE deleted_at IS NULL AND status <> 'SUPERSEDED'; `); await queryRunner.query(` CREATE UNIQUE INDEX IF NOT EXISTS "UQ_weight_limit_rules_pattern" ON freight.weight_limit_rules (container_type_id, trade_direction) WHERE deleted_at IS NULL; `); } public async down(queryRunner: QueryRunner): Promise { await queryRunner.query(`DROP INDEX IF EXISTS freight."UQ_rates_pattern";`); await queryRunner.query(`DROP INDEX IF EXISTS freight."UQ_weight_limit_rules_pattern";`); await queryRunner.query(`ALTER TABLE freight.rates ADD COLUMN IF NOT EXISTS effective_from date;`); await queryRunner.query(`UPDATE freight.rates SET effective_from = COALESCE(effective_from, created_at::date);`); await queryRunner.query(`ALTER TABLE freight.rates ALTER COLUMN effective_from SET NOT NULL;`); await queryRunner.query(`ALTER TABLE freight.rates ADD COLUMN IF NOT EXISTS effective_to date;`); await queryRunner.query(`ALTER TABLE freight.weight_limit_rules ADD COLUMN IF NOT EXISTS effective_from date;`); await queryRunner.query(`ALTER TABLE freight.weight_limit_rules ADD COLUMN IF NOT EXISTS effective_to date;`); await queryRunner.query(`CREATE INDEX IF NOT EXISTS "IDX_rates_effective_from" ON freight.rates (effective_from);`); await queryRunner.query( `CREATE INDEX IF NOT EXISTS "IDX_weight_limit_rules_effective_from" ON freight.weight_limit_rules (effective_from);`, ); } }