booking operations and trains scheduling also allocations

This commit is contained in:
marshal
2026-06-10 00:48:32 +03:00
parent 675975bc08
commit 5774d7db9d
180 changed files with 13423 additions and 3877 deletions

View File

@@ -0,0 +1,321 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddSchedulingAllocationEnhancements1750400000000
implements MigrationInterface
{
name = 'AddSchedulingAllocationEnhancements1750400000000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.bookings
ADD COLUMN IF NOT EXISTS wagons_required NUMERIC(6,2) NULL,
ADD COLUMN IF NOT EXISTS scheduling_status VARCHAR(30) NOT NULL DEFAULT 'NOT_SCHEDULED',
ADD COLUMN IF NOT EXISTS hold_started_at TIMESTAMPTZ NULL,
ADD COLUMN IF NOT EXISTS hold_expires_at TIMESTAMPTZ NULL,
ADD COLUMN IF NOT EXISTS scheduled_at TIMESTAMPTZ NULL;
`);
await queryRunner.query(`
ALTER TABLE freight.train_schedules
ADD COLUMN IF NOT EXISTS train_number VARCHAR(20) NULL,
ADD COLUMN IF NOT EXISTS direction VARCHAR(10) NULL,
ADD COLUMN IF NOT EXISTS actual_departure_at TIMESTAMPTZ NULL,
ADD COLUMN IF NOT EXISTS actual_arrival_at TIMESTAMPTZ NULL,
ADD COLUMN IF NOT EXISTS prepared_by_user_id UUID NULL,
ADD COLUMN IF NOT EXISTS checked_by_user_id UUID NULL,
ADD COLUMN IF NOT EXISTS max_wagons INT NOT NULL DEFAULT 53;
`);
await queryRunner.query(`
ALTER TABLE freight.train_set_wagons
ADD COLUMN IF NOT EXISTS physical_wagon_id UUID NULL,
ADD COLUMN IF NOT EXISTS status VARCHAR(20) NOT NULL DEFAULT 'PLANNED';
`);
await queryRunner.query(`
ALTER TABLE freight.wagon_booking_allocations
ADD COLUMN IF NOT EXISTS load_type VARCHAR(20) NULL,
ADD COLUMN IF NOT EXISTS status VARCHAR(20) NOT NULL DEFAULT 'PLANNED',
ADD COLUMN IF NOT EXISTS confirmed_at TIMESTAMPTZ NULL,
ADD COLUMN IF NOT EXISTS confirmed_by_user_id UUID NULL;
`);
await queryRunner.query(`
ALTER TABLE freight.wagon_types
ADD COLUMN IF NOT EXISTS equated_length_m NUMERIC(10,3) NULL,
ADD COLUMN IF NOT EXISTS tare_weight_tons NUMERIC(10,3) NULL,
ADD COLUMN IF NOT EXISTS supports_container BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN IF NOT EXISTS max_container_gross_t NUMERIC(10,3) NULL;
`);
await queryRunner.query(`
ALTER TABLE freight.wagons
ADD COLUMN IF NOT EXISTS train_set_wagon_id UUID NULL,
ADD COLUMN IF NOT EXISTS current_train_schedule_id UUID NULL;
`);
await queryRunner.query(`
ALTER TABLE freight.containers
ADD COLUMN IF NOT EXISTS booking_id UUID NULL,
ADD COLUMN IF NOT EXISTS wagon_booking_allocation_id UUID NULL,
ADD COLUMN IF NOT EXISTS booking_container_id UUID NULL;
`);
await queryRunner.query(`
ALTER TABLE freight.cargoes
ADD COLUMN IF NOT EXISTS wagon_booking_allocation_id UUID NULL,
ADD COLUMN IF NOT EXISTS booking_id UUID NULL,
ADD COLUMN IF NOT EXISTS load_type VARCHAR(20) NULL;
`);
await queryRunner.query(`
ALTER TABLE freight.cargoes
ALTER COLUMN container_id DROP NOT NULL;
`);
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS freight.wagon_allocation_container_items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
wagon_booking_allocation_id UUID NOT NULL,
booking_container_id UUID NULL,
container_id UUID NULL,
container_number VARCHAR(64) NULL,
container_type_id UUID NOT NULL,
position_on_wagon SMALLINT NULL,
seal_number VARCHAR(64) NULL,
chassis_number VARCHAR(64) NULL,
gross_weight_tons NUMERIC(10,3) NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
deleted_at TIMESTAMPTZ NULL,
CONSTRAINT fk_waci_allocation FOREIGN KEY (wagon_booking_allocation_id)
REFERENCES freight.wagon_booking_allocations(id) ON DELETE CASCADE,
CONSTRAINT fk_waci_booking_container FOREIGN KEY (booking_container_id)
REFERENCES freight.booking_container(id) ON DELETE SET NULL,
CONSTRAINT fk_waci_container FOREIGN KEY (container_id)
REFERENCES freight.containers(id) ON DELETE SET NULL,
CONSTRAINT fk_waci_container_type FOREIGN KEY (container_type_id)
REFERENCES freight.container_types(id)
);
`);
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS freight.wagon_allocation_bulk_loads (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
wagon_booking_allocation_id UUID NOT NULL UNIQUE,
booking_id UUID NOT NULL,
cargo_type_id UUID NULL,
cargo_description TEXT NULL,
pricing_unit VARCHAR(20) NOT NULL DEFAULT 'PER_TON',
quantity NUMERIC(12,3) NOT NULL DEFAULT 0,
weight_tons NUMERIC(10,3) NOT NULL DEFAULT 0,
truck_plate_number VARCHAR(32) NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
deleted_at TIMESTAMPTZ NULL,
CONSTRAINT fk_wabl_allocation FOREIGN KEY (wagon_booking_allocation_id)
REFERENCES freight.wagon_booking_allocations(id) ON DELETE CASCADE,
CONSTRAINT fk_wabl_booking FOREIGN KEY (booking_id)
REFERENCES freight.bookings(id),
CONSTRAINT fk_wabl_cargo_type FOREIGN KEY (cargo_type_id)
REFERENCES freight.cargo_types(id) ON DELETE SET NULL
);
`);
await queryRunner.query(`
CREATE INDEX IF NOT EXISTS idx_bookings_scheduling_status
ON freight.bookings(scheduling_status);
`);
await queryRunner.query(`
CREATE INDEX IF NOT EXISTS idx_train_schedules_train_number
ON freight.train_schedules(train_number);
`);
await queryRunner.query(`
CREATE INDEX IF NOT EXISTS idx_train_set_wagons_physical_wagon
ON freight.train_set_wagons(physical_wagon_id);
`);
await queryRunner.query(`
CREATE INDEX IF NOT EXISTS idx_wagons_train_set_wagon_id
ON freight.wagons(train_set_wagon_id);
`);
await queryRunner.query(`
CREATE INDEX IF NOT EXISTS idx_wagons_current_train_schedule_id
ON freight.wagons(current_train_schedule_id);
`);
await queryRunner.query(`
CREATE INDEX IF NOT EXISTS idx_waci_allocation
ON freight.wagon_allocation_container_items(wagon_booking_allocation_id);
`);
await queryRunner.query(`
CREATE INDEX IF NOT EXISTS idx_wabl_booking
ON freight.wagon_allocation_bulk_loads(booking_id);
`);
await queryRunner.query(`
DO $$ BEGIN
ALTER TABLE freight.train_set_wagons
ADD CONSTRAINT fk_train_set_wagons_physical_wagon
FOREIGN KEY (physical_wagon_id) REFERENCES freight.wagons(id) ON DELETE SET NULL;
EXCEPTION WHEN duplicate_object THEN NULL;
END $$;
`);
await queryRunner.query(`
DO $$ BEGIN
ALTER TABLE freight.wagons
ADD CONSTRAINT fk_wagons_train_set_wagon
FOREIGN KEY (train_set_wagon_id) REFERENCES freight.train_set_wagons(id) ON DELETE SET NULL;
EXCEPTION WHEN duplicate_object THEN NULL;
END $$;
`);
await queryRunner.query(`
DO $$ BEGIN
ALTER TABLE freight.wagons
ADD CONSTRAINT fk_wagons_current_train_schedule
FOREIGN KEY (current_train_schedule_id) REFERENCES freight.train_schedules(id) ON DELETE SET NULL;
EXCEPTION WHEN duplicate_object THEN NULL;
END $$;
`);
await queryRunner.query(`
DO $$ BEGIN
ALTER TABLE freight.containers
ADD CONSTRAINT fk_containers_booking
FOREIGN KEY (booking_id) REFERENCES freight.bookings(id) ON DELETE SET NULL;
EXCEPTION WHEN duplicate_object THEN NULL;
END $$;
`);
await queryRunner.query(`
DO $$ BEGIN
ALTER TABLE freight.containers
ADD CONSTRAINT fk_containers_wagon_allocation
FOREIGN KEY (wagon_booking_allocation_id) REFERENCES freight.wagon_booking_allocations(id) ON DELETE SET NULL;
EXCEPTION WHEN duplicate_object THEN NULL;
END $$;
`);
await queryRunner.query(`
DO $$ BEGIN
ALTER TABLE freight.containers
ADD CONSTRAINT fk_containers_booking_container
FOREIGN KEY (booking_container_id) REFERENCES freight.booking_container(id) ON DELETE SET NULL;
EXCEPTION WHEN duplicate_object THEN NULL;
END $$;
`);
await queryRunner.query(`
DO $$ BEGIN
ALTER TABLE freight.cargoes
ADD CONSTRAINT fk_cargoes_wagon_allocation
FOREIGN KEY (wagon_booking_allocation_id) REFERENCES freight.wagon_booking_allocations(id) ON DELETE SET NULL;
EXCEPTION WHEN duplicate_object THEN NULL;
END $$;
`);
await queryRunner.query(`
DO $$ BEGIN
ALTER TABLE freight.cargoes
ADD CONSTRAINT fk_cargoes_booking
FOREIGN KEY (booking_id) REFERENCES freight.bookings(id) ON DELETE SET NULL;
EXCEPTION WHEN duplicate_object THEN NULL;
END $$;
`);
await queryRunner.query(`
UPDATE freight.wagon_types SET
equated_length_m = 1.3,
tare_weight_tons = 22.4,
supports_container = true,
max_container_gross_t = 30.48
WHERE code = 'NW5';
`);
await queryRunner.query(`
UPDATE freight.wagon_types SET
equated_length_m = 1.6,
tare_weight_tons = 25.2,
supports_container = false
WHERE code = 'PW2';
`);
await queryRunner.query(`
UPDATE freight.wagon_types SET
equated_length_m = 1.5,
tare_weight_tons = 25.2,
supports_container = false
WHERE code = 'KW2';
`);
await queryRunner.query(`
UPDATE freight.wagon_types SET
equated_length_m = 1.3,
tare_weight_tons = 23.4,
supports_container = false
WHERE code = 'CW3';
`);
await queryRunner.query(`
UPDATE freight.wagon_types SET
equated_length_m = 1.3,
tare_weight_tons = 24.8,
supports_container = false
WHERE code = 'CW4';
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE IF EXISTS freight.wagon_allocation_bulk_loads;`);
await queryRunner.query(`DROP TABLE IF EXISTS freight.wagon_allocation_container_items;`);
await queryRunner.query(`
ALTER TABLE freight.cargoes
ALTER COLUMN container_id SET NOT NULL;
`);
await queryRunner.query(`
ALTER TABLE freight.bookings
DROP COLUMN IF EXISTS wagons_required,
DROP COLUMN IF EXISTS scheduling_status,
DROP COLUMN IF EXISTS hold_started_at,
DROP COLUMN IF EXISTS hold_expires_at,
DROP COLUMN IF EXISTS scheduled_at;
`);
await queryRunner.query(`
ALTER TABLE freight.train_schedules
DROP COLUMN IF EXISTS train_number,
DROP COLUMN IF EXISTS direction,
DROP COLUMN IF EXISTS actual_departure_at,
DROP COLUMN IF EXISTS actual_arrival_at,
DROP COLUMN IF EXISTS prepared_by_user_id,
DROP COLUMN IF EXISTS checked_by_user_id,
DROP COLUMN IF EXISTS max_wagons;
`);
await queryRunner.query(`
ALTER TABLE freight.train_set_wagons
DROP COLUMN IF EXISTS physical_wagon_id,
DROP COLUMN IF EXISTS status;
`);
await queryRunner.query(`
ALTER TABLE freight.wagon_booking_allocations
DROP COLUMN IF EXISTS load_type,
DROP COLUMN IF EXISTS status,
DROP COLUMN IF EXISTS confirmed_at,
DROP COLUMN IF EXISTS confirmed_by_user_id;
`);
await queryRunner.query(`
ALTER TABLE freight.wagon_types
DROP COLUMN IF EXISTS equated_length_m,
DROP COLUMN IF EXISTS tare_weight_tons,
DROP COLUMN IF EXISTS supports_container,
DROP COLUMN IF EXISTS max_container_gross_t;
`);
await queryRunner.query(`
ALTER TABLE freight.wagons
DROP COLUMN IF EXISTS train_set_wagon_id,
DROP COLUMN IF EXISTS current_train_schedule_id;
`);
await queryRunner.query(`
ALTER TABLE freight.containers
DROP COLUMN IF EXISTS booking_id,
DROP COLUMN IF EXISTS wagon_booking_allocation_id,
DROP COLUMN IF EXISTS booking_container_id;
`);
await queryRunner.query(`
ALTER TABLE freight.cargoes
DROP COLUMN IF EXISTS wagon_booking_allocation_id,
DROP COLUMN IF EXISTS booking_id,
DROP COLUMN IF EXISTS load_type;
`);
}
}

View File

@@ -0,0 +1,25 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddWagonReadiness1750500000000 implements MigrationInterface {
name = 'AddWagonReadiness1750500000000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.wagons
ADD COLUMN IF NOT EXISTS readiness VARCHAR(20) NOT NULL DEFAULT 'IMPORT_READY'
`);
await queryRunner.query(`
CREATE INDEX IF NOT EXISTS idx_wagons_readiness
ON freight.wagons (readiness)
WHERE deleted_at IS NULL
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX IF EXISTS freight.idx_wagons_readiness`);
await queryRunner.query(`
ALTER TABLE freight.wagons
DROP COLUMN IF EXISTS readiness
`);
}
}

View File

@@ -0,0 +1,46 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddGovernmentBookingFields1750600000000 implements MigrationInterface {
name = 'AddGovernmentBookingFields1750600000000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.bookings
ADD COLUMN IF NOT EXISTS is_government BOOLEAN NOT NULL DEFAULT false
`);
await queryRunner.query(`
ALTER TABLE freight.bookings
ADD COLUMN IF NOT EXISTS government_institution VARCHAR(255) NULL
`);
await queryRunner.query(`
ALTER TABLE freight.bookings
ALTER COLUMN company_id DROP NOT NULL
`);
await queryRunner.query(`
CREATE INDEX IF NOT EXISTS idx_bookings_is_government
ON freight.bookings (is_government)
WHERE is_government = true AND deleted_at IS NULL
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX IF EXISTS freight.idx_bookings_is_government`);
await queryRunner.query(`
UPDATE freight.bookings
SET company_id = '00000000-0000-0000-0000-000000000000'
WHERE company_id IS NULL
`);
await queryRunner.query(`
ALTER TABLE freight.bookings
ALTER COLUMN company_id SET NOT NULL
`);
await queryRunner.query(`
ALTER TABLE freight.bookings
DROP COLUMN IF EXISTS government_institution
`);
await queryRunner.query(`
ALTER TABLE freight.bookings
DROP COLUMN IF EXISTS is_government
`);
}
}

View File

@@ -0,0 +1,32 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class CreateSchedulingEvents1750700000000 implements MigrationInterface {
name = 'CreateSchedulingEvents1750700000000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS freight.scheduling_events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
train_schedule_id UUID NOT NULL,
trigger VARCHAR(40) NOT NULL,
actor_user_id UUID NULL,
reason TEXT NULL,
plan_snapshot JSONB NOT NULL DEFAULT '{}',
displaced_booking_ids JSONB NOT NULL DEFAULT '[]',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ NULL
)
`);
await queryRunner.query(`
CREATE INDEX IF NOT EXISTS idx_scheduling_events_train_schedule_id
ON freight.scheduling_events (train_schedule_id)
WHERE deleted_at IS NULL
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX IF EXISTS freight.idx_scheduling_events_train_schedule_id`);
await queryRunner.query(`DROP TABLE IF EXISTS freight.scheduling_events`);
}
}

View File

@@ -0,0 +1,47 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
/** 20ft = 0.5 wagon slots (2 per wagon); 40ft = 1.0 wagon slot (1 per wagon). */
export class FixContainerWagonsPerUnit1750800000000 implements MigrationInterface {
name = 'FixContainerWagonsPerUnit1750800000000';
public async up(queryRunner: QueryRunner): Promise<void> {
const hasContainerTypes = await queryRunner.hasTable('freight.container_types');
if (!hasContainerTypes) {
return;
}
await queryRunner.query(`
UPDATE freight.container_types
SET wagons_per_unit = 0.50
WHERE size_ft = 20 OR code LIKE '20%';
`);
await queryRunner.query(`
UPDATE freight.container_types
SET wagons_per_unit = 1.00
WHERE size_ft = 40 OR code LIKE '40%';
`);
const hasBookingContainer = await queryRunner.hasTable('freight.booking_container');
if (!hasBookingContainer) {
return;
}
await queryRunner.query(`
UPDATE freight.booking_container bc
SET wagons_required = CEILING(bc.quantity * ct.wagons_per_unit)
FROM freight.container_types ct
WHERE ct.id = bc.container_type_id;
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
const hasContainerTypes = await queryRunner.hasTable('freight.container_types');
if (!hasContainerTypes) {
return;
}
await queryRunner.query(`
UPDATE freight.container_types SET wagons_per_unit = 1.00;
`);
}
}

View File

@@ -0,0 +1,39 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddContainerNumberToBookingContainer1750900000000 implements MigrationInterface {
name = "AddContainerNumberToBookingContainer1750900000000";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.booking_container
ALTER COLUMN container_type_id DROP NOT NULL;
`);
await queryRunner.query(`
ALTER TABLE freight.booking_container
ADD COLUMN container_number varchar(64);
`);
await queryRunner.query(`
ALTER TABLE freight.wagon_allocation_container_items
ALTER COLUMN container_type_id DROP NOT NULL;
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.wagon_allocation_container_items
ALTER COLUMN container_type_id SET NOT NULL;
`);
await queryRunner.query(`
ALTER TABLE freight.booking_container
DROP COLUMN container_number;
`);
await queryRunner.query(`
ALTER TABLE freight.booking_container
ALTER COLUMN container_type_id SET NOT NULL;
`);
}
}

View File

@@ -0,0 +1,35 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class CreateTrainSchedulingGlobalRules1751000000000 implements MigrationInterface {
name = "CreateTrainSchedulingGlobalRules1751000000000";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE TABLE freight.train_scheduling_global_rules (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
max_train_length_meters numeric(10, 2) NOT NULL DEFAULT 760,
max_train_weight_tons numeric(10, 3) NOT NULL DEFAULT 3500,
max_wagons_per_train integer NOT NULL DEFAULT 53,
max_20ft_container_weight_tons numeric(8, 3) NOT NULL DEFAULT 30,
max_20ft_pair_weight_diff_tons numeric(8, 3) NOT NULL DEFAULT 10,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
deleted_at timestamptz NULL
);
`);
await queryRunner.query(`
INSERT INTO freight.train_scheduling_global_rules (
max_train_length_meters,
max_train_weight_tons,
max_wagons_per_train,
max_20ft_container_weight_tons,
max_20ft_pair_weight_diff_tons
) VALUES (760, 3500, 53, 30, 10);
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE IF EXISTS freight.train_scheduling_global_rules;`);
}
}

View File

@@ -0,0 +1,21 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddDeletedAtToTrainSchedulingGlobalRules1751000000001
implements MigrationInterface
{
name = "AddDeletedAtToTrainSchedulingGlobalRules1751000000001";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.train_scheduling_global_rules
ADD COLUMN IF NOT EXISTS deleted_at timestamptz NULL;
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.train_scheduling_global_rules
DROP COLUMN IF EXISTS deleted_at;
`);
}
}