mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
141 lines
6.3 KiB
TypeScript
141 lines
6.3 KiB
TypeScript
import { MigrationInterface, QueryRunner } from 'typeorm';
|
|
|
|
/**
|
|
* Scope base rail freight to a route (origin yard → destination yard).
|
|
*
|
|
* Until now a base-freight rate was keyed by direction + container/bulk scope
|
|
* only, so "container import" cost the same whether the box was railed to Dire
|
|
* Dawa or to Mojo. Rates now carry the yard pair the price is quoted for, which
|
|
* is what the business actually sells: `container import, Djibouti → Dire Dawa,
|
|
* 500 USD`.
|
|
*
|
|
* Existing base-freight rates predate the yard pair and cannot be backfilled —
|
|
* there is no way to know which route each was meant for. They are retired
|
|
* (SUPERSEDED + soft-deleted) rather than deleted, because booking_rate_snapshot
|
|
* and rate_change_requests hold FKs to them (RESTRICT) and those rows are price
|
|
* history. Retiring drops them out of pricing and the admin UI just the same;
|
|
* the yard-scoped replacements must be re-entered.
|
|
*
|
|
* Surcharges, first-mile and last-mile rates are untouched: they are not
|
|
* route-scoped and keep NULL yards.
|
|
*/
|
|
export class AddRateYardScope2320000000000 implements MigrationInterface {
|
|
name = 'AddRateYardScope2320000000000';
|
|
|
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
// ── 1. Yard columns + FKs ──────────────────────────────────────────────
|
|
await queryRunner.query(`
|
|
ALTER TABLE freight.rates
|
|
ADD COLUMN IF NOT EXISTS origin_yard_id uuid NULL,
|
|
ADD COLUMN IF NOT EXISTS destination_yard_id uuid NULL;
|
|
`);
|
|
|
|
await queryRunner.query(`
|
|
DO $$ BEGIN
|
|
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'FK_rates_origin_yard_id') THEN
|
|
ALTER TABLE freight.rates
|
|
ADD CONSTRAINT "FK_rates_origin_yard_id"
|
|
FOREIGN KEY (origin_yard_id) REFERENCES freight.yards(id);
|
|
END IF;
|
|
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'FK_rates_destination_yard_id') THEN
|
|
ALTER TABLE freight.rates
|
|
ADD CONSTRAINT "FK_rates_destination_yard_id"
|
|
FOREIGN KEY (destination_yard_id) REFERENCES freight.yards(id);
|
|
END IF;
|
|
END $$;
|
|
`);
|
|
|
|
await queryRunner.query(
|
|
`CREATE INDEX IF NOT EXISTS "IDX_rates_origin_yard_id" ON freight.rates (origin_yard_id);`,
|
|
);
|
|
await queryRunner.query(
|
|
`CREATE INDEX IF NOT EXISTS "IDX_rates_destination_yard_id" ON freight.rates (destination_yard_id);`,
|
|
);
|
|
|
|
// ── 2. Retire route-less base freight ──────────────────────────────────
|
|
// Soft-delete, not DELETE: booking_rate_snapshot.rate_id is ON DELETE
|
|
// RESTRICT and those snapshots are what past bookings were charged.
|
|
await queryRunner.query(`
|
|
UPDATE freight.rates
|
|
SET status = 'SUPERSEDED',
|
|
deleted_at = now(),
|
|
updated_at = now()
|
|
WHERE deleted_at IS NULL
|
|
AND "trigger" = 'ALWAYS'
|
|
AND applies_to IN ('BULK', 'CONTAINER', 'INTERCITY');
|
|
`);
|
|
|
|
// ── 3. Route is part of a rate's identity ──────────────────────────────
|
|
// Two rates may now share rateType + scope + unit as long as they price
|
|
// different legs, so the yard pair joins the uniqueness tuple.
|
|
await queryRunner.query(`DROP INDEX IF EXISTS freight."UQ_rates_pattern";`);
|
|
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, ''),
|
|
COALESCE(origin_yard_id, '00000000-0000-0000-0000-000000000000'::uuid),
|
|
COALESCE(destination_yard_id, '00000000-0000-0000-0000-000000000000'::uuid),
|
|
rate_unit
|
|
)
|
|
WHERE deleted_at IS NULL AND status <> 'SUPERSEDED';
|
|
`);
|
|
|
|
// ── 4. Base freight must carry a route; nothing else may ───────────────
|
|
// Retired rows are exempt — they are the route-less rates step 2 just
|
|
// superseded, and they must stay readable for snapshot history.
|
|
await queryRunner.query(`
|
|
DO $$ BEGIN
|
|
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'CK_rates_yard_scope') THEN
|
|
ALTER TABLE freight.rates
|
|
ADD CONSTRAINT "CK_rates_yard_scope" CHECK (
|
|
deleted_at IS NOT NULL
|
|
OR status = 'SUPERSEDED'
|
|
OR CASE
|
|
WHEN "trigger" = 'ALWAYS' AND applies_to IN ('BULK', 'CONTAINER', 'INTERCITY')
|
|
THEN origin_yard_id IS NOT NULL AND destination_yard_id IS NOT NULL
|
|
ELSE origin_yard_id IS NULL AND destination_yard_id IS NULL
|
|
END
|
|
);
|
|
END IF;
|
|
END $$;
|
|
`);
|
|
}
|
|
|
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
// The retired rates are not un-superseded: which route each belonged to was
|
|
// never recorded, so reviving them would restore rates that price the wrong
|
|
// legs. Down only reverses the schema.
|
|
await queryRunner.query(
|
|
`ALTER TABLE freight.rates DROP CONSTRAINT IF EXISTS "CK_rates_yard_scope";`,
|
|
);
|
|
await queryRunner.query(`DROP INDEX IF EXISTS freight."UQ_rates_pattern";`);
|
|
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(`DROP INDEX IF EXISTS freight."IDX_rates_destination_yard_id";`);
|
|
await queryRunner.query(`DROP INDEX IF EXISTS freight."IDX_rates_origin_yard_id";`);
|
|
await queryRunner.query(
|
|
`ALTER TABLE freight.rates DROP CONSTRAINT IF EXISTS "FK_rates_destination_yard_id";`,
|
|
);
|
|
await queryRunner.query(
|
|
`ALTER TABLE freight.rates DROP CONSTRAINT IF EXISTS "FK_rates_origin_yard_id";`,
|
|
);
|
|
await queryRunner.query(`
|
|
ALTER TABLE freight.rates
|
|
DROP COLUMN IF EXISTS destination_yard_id,
|
|
DROP COLUMN IF EXISTS origin_yard_id;
|
|
`);
|
|
}
|
|
}
|