mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 12:58:13 +00:00
approve-delivery exit-gate fix + Import Loading Confirmation frontend panel — done this session, not yet committed
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
/**
|
||||
* Support email as a second OTP channel alongside phone (e.g. signup lets the
|
||||
* user choose which one to verify). `phone` becomes nullable since an
|
||||
* email-channel row has none, and `email` is added as a nullable unique column
|
||||
* mirroring `phone`'s shape.
|
||||
*/
|
||||
export class AddEmailToOtpVerifications1900000000000
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = "AddEmailToOtpVerifications1900000000000";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE public.otp_verifications
|
||||
ALTER COLUMN phone DROP NOT NULL
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE public.otp_verifications
|
||||
ADD COLUMN IF NOT EXISTS email varchar UNIQUE
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE public.otp_verifications
|
||||
DROP COLUMN IF EXISTS email
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE public.otp_verifications
|
||||
ALTER COLUMN phone SET NOT NULL
|
||||
`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
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<void> {
|
||||
// ── 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<void> {
|
||||
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);`,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user