fix conflict

This commit is contained in:
yaschalew
2026-07-03 17:31:29 +03:00
209 changed files with 13776 additions and 2892 deletions

View File

@@ -0,0 +1,31 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddBookingWindowGlobalRules1861000000000 implements MigrationInterface {
name = "AddBookingWindowGlobalRules1861000000000";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.train_scheduling_global_rules
ADD COLUMN import_window_lead_days integer NOT NULL DEFAULT 3,
ADD COLUMN export_booking_lead_hours integer NOT NULL DEFAULT 24,
ADD COLUMN window_open_hour integer NOT NULL DEFAULT 8,
ADD COLUMN window_duration_hours numeric(4, 2) NOT NULL DEFAULT 3,
ADD COLUMN doc_review_minutes integer NOT NULL DEFAULT 30,
ADD COLUMN payment_window_minutes integer NOT NULL DEFAULT 60,
ADD COLUMN reopen_delay_minutes integer NOT NULL DEFAULT 90;
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.train_scheduling_global_rules
DROP COLUMN IF EXISTS import_window_lead_days,
DROP COLUMN IF EXISTS export_booking_lead_hours,
DROP COLUMN IF EXISTS window_open_hour,
DROP COLUMN IF EXISTS window_duration_hours,
DROP COLUMN IF EXISTS doc_review_minutes,
DROP COLUMN IF EXISTS payment_window_minutes,
DROP COLUMN IF EXISTS reopen_delay_minutes;
`);
}
}

View File

@@ -0,0 +1,40 @@
import { MigrationInterface, QueryRunner } from "typeorm";
/**
* arriveSchedule used to release only the primary locomotive of a train set, leaving
* secondary locomotives ASSIGNED forever. Locomotives are now only ASSIGNED while out
* on a dispatched train — release every ASSIGNED locomotive that is not attached to a
* currently-DISPATCHED schedule.
*/
export class ReleaseStuckAssignedLocomotives1861000000001 implements MigrationInterface {
name = "ReleaseStuckAssignedLocomotives1861000000001";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
UPDATE freight.locomotives l
SET status = 'AVAILABLE'
WHERE l.status = 'ASSIGNED'
AND NOT EXISTS (
SELECT 1
FROM freight.train_schedules ts
JOIN freight.train_sets tset ON tset.id = ts.train_set_id
JOIN (
SELECT tsl.train_set_id, tsl.locomotive_id
FROM freight.train_set_locomotives tsl
WHERE tsl.deleted_at IS NULL
UNION
SELECT t.id AS train_set_id, t.locomotive_id
FROM freight.train_sets t
WHERE t.locomotive_id IS NOT NULL
) loco ON loco.train_set_id = tset.id
WHERE ts.status = 'DISPATCHED'
AND ts.deleted_at IS NULL
AND loco.locomotive_id = l.id
);
`);
}
public async down(): Promise<void> {
// Data fix — not reversible.
}
}

View File

@@ -0,0 +1,37 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddScheduleWindowPhases1862000000000 implements MigrationInterface {
name = "AddScheduleWindowPhases1862000000000";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.train_schedules
ADD COLUMN window_phase varchar(20) NULL,
ADD COLUMN window_opens_at timestamptz NULL,
ADD COLUMN window_closes_at timestamptz NULL,
ADD COLUMN doc_review_ends_at timestamptz NULL,
ADD COLUMN doc_review_completed_at timestamptz NULL,
ADD COLUMN payment_phase_ends_at timestamptz NULL,
ADD COLUMN booking_cycle_no integer NOT NULL DEFAULT 0;
`);
await queryRunner.query(`
CREATE INDEX idx_train_schedules_window_phase
ON freight.train_schedules (window_phase)
WHERE window_phase IS NOT NULL;
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX IF EXISTS freight.idx_train_schedules_window_phase;`);
await queryRunner.query(`
ALTER TABLE freight.train_schedules
DROP COLUMN IF EXISTS window_phase,
DROP COLUMN IF EXISTS window_opens_at,
DROP COLUMN IF EXISTS window_closes_at,
DROP COLUMN IF EXISTS doc_review_ends_at,
DROP COLUMN IF EXISTS doc_review_completed_at,
DROP COLUMN IF EXISTS payment_phase_ends_at,
DROP COLUMN IF EXISTS booking_cycle_no;
`);
}
}

View File

@@ -0,0 +1,40 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class CreateBookingBatchOffers1863000000000 implements MigrationInterface {
name = "CreateBookingBatchOffers1863000000000";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE TABLE freight.booking_batch_offers (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
booking_id uuid NOT NULL REFERENCES freight.bookings(id) ON DELETE CASCADE,
train_schedule_id uuid NOT NULL REFERENCES freight.train_schedules(id) ON DELETE CASCADE,
offered_wagons integer NOT NULL,
total_wagons integer NOT NULL,
offered_lines jsonb NULL,
offered_weight_tons numeric(12, 3) NOT NULL,
offered_amount numeric(14, 2) NOT NULL,
offered_pricing_breakdown jsonb NULL,
invoice_id uuid NULL,
payment_deadline timestamptz NOT NULL,
status varchar(10) NOT NULL DEFAULT 'OFFERED',
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
deleted_at timestamptz NULL
);
`);
await queryRunner.query(
`CREATE INDEX idx_booking_batch_offers_booking ON freight.booking_batch_offers (booking_id);`,
);
await queryRunner.query(
`CREATE INDEX idx_booking_batch_offers_schedule ON freight.booking_batch_offers (train_schedule_id);`,
);
await queryRunner.query(
`CREATE INDEX idx_booking_batch_offers_status ON freight.booking_batch_offers (status);`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE IF EXISTS freight.booking_batch_offers;`);
}
}

View File

@@ -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
`);
}
}

View File

@@ -0,0 +1,25 @@
import { MigrationInterface, QueryRunner } from "typeorm";
/**
* Track per-booking loading confirmation (LOADED/UNLOADED) on train_schedule_bookings.
* Tracking only — does not gate dispatch.
*/
export class AddLoadingStatusToTrainScheduleBookings1900000000000
implements MigrationInterface
{
name = "AddLoadingStatusToTrainScheduleBookings1900000000000";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.train_schedule_bookings
ADD COLUMN IF NOT EXISTS loading_status varchar(20) NOT NULL DEFAULT 'UNLOADED'
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.train_schedule_bookings
DROP COLUMN IF EXISTS loading_status
`);
}
}

View File

@@ -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);`,
);
}
}