Merge pull request #1119 from Tria-plc/dev

syncing
This commit is contained in:
Nathnael Wondisha
2026-08-05 11:51:46 +03:00
committed by GitHub
307 changed files with 5873 additions and 17985 deletions

View File

@@ -11,7 +11,7 @@ One script, runs from anywhere in the repo (resolves `pg` from `apps/edr-freight
node .claude/skills/edr-db/query.cjs "SELECT ... " # run SQL, console.table output node .claude/skills/edr-db/query.cjs "SELECT ... " # run SQL, console.table output
node .claude/skills/edr-db/query.cjs explain "SELECT ..." # EXPLAIN-validate only (no rows touched) node .claude/skills/edr-db/query.cjs explain "SELECT ..." # EXPLAIN-validate only (no rows touched)
node .claude/skills/edr-db/query.cjs columns <table> # freight.<table> column list node .claude/skills/edr-db/query.cjs columns <table> # freight.<table> column list
node .claude/skills/edr-db/query.cjs migrations [like] # public.migrations rows (newest first) node .claude/skills/edr-db/query.cjs migrations [like] # migration rows, newest first (freight.migrations + iam.typeorm_migrations)
node .claude/skills/edr-db/query.cjs drift <table> # bare column names, for diffing vs the entity node .claude/skills/edr-db/query.cjs drift <table> # bare column names, for diffing vs the entity
``` ```
@@ -31,7 +31,11 @@ defaulting to the shared dev database (`edr_dev`).
(`WHERE NOT EXISTS` guards) — watch-mode API instances race `migrationsRun`, (`WHERE NOT EXISTS` guards) — watch-mode API instances race `migrationsRun`,
and non-idempotent statements have double-run here before. and non-idempotent statements have double-run here before.
- Timestamps for new migrations: must be unique across `src/migrations/` AND - Timestamps for new migrations: must be unique across `src/migrations/` AND
higher than `SELECT max(timestamp) FROM public.migrations`. higher than `SELECT max(timestamp) FROM freight.migrations`.
- Freight and IAM keep separate histories: `freight.migrations` for
`apps/edr-freight-api/src/migrations/*`, `iam.typeorm_migrations` for the
`@tria-plc/iamapi-common` migrations. `public.migrations` is the pre-split
table, left in place for rollback — never write to it.
## Diagnosing a pasted 400/500 (the recurring loop) ## Diagnosing a pasted 400/500 (the recurring loop)

View File

@@ -8,7 +8,7 @@
* node .claude/skills/edr-db/query.cjs "SELECT ... " run SQL (console.table) * node .claude/skills/edr-db/query.cjs "SELECT ... " run SQL (console.table)
* node .claude/skills/edr-db/query.cjs explain "SELECT..." EXPLAIN-validate only * node .claude/skills/edr-db/query.cjs explain "SELECT..." EXPLAIN-validate only
* node .claude/skills/edr-db/query.cjs columns <table> list freight.<table> columns * node .claude/skills/edr-db/query.cjs columns <table> list freight.<table> columns
* node .claude/skills/edr-db/query.cjs migrations [like] public.migrations rows * node .claude/skills/edr-db/query.cjs migrations [like] freight/iam migration rows
* node .claude/skills/edr-db/query.cjs drift <table> columns vs entity check helper * node .claude/skills/edr-db/query.cjs drift <table> columns vs entity check helper
* *
* Connection: DB_HOST/DB_PORT/DB_USER/DB_PASSWORD/DB_NAME env vars, falling * Connection: DB_HOST/DB_PORT/DB_USER/DB_PASSWORD/DB_NAME env vars, falling
@@ -52,12 +52,27 @@ async function main() {
console.table(r.rows); console.table(r.rows);
} else if (first === 'migrations') { } else if (first === 'migrations') {
const like = rest[0] ? `%${rest[0]}%` : '%'; const like = rest[0] ? `%${rest[0]}%` : '%';
const r = await c.query( // Histories are split per owner: freight.migrations (this app) and
`SELECT id, timestamp, name FROM public.migrations // iam.typeorm_migrations (@tria-plc/iamapi-common). public.migrations is the
WHERE name ILIKE $1 ORDER BY id DESC LIMIT 40`, // pre-split table, kept for rollback — read it only if the split has not
[like], // been applied to this DB yet.
); const sources = [
console.table(r.rows); ['freight', 'freight.migrations'],
['iam', 'iam.typeorm_migrations'],
['legacy', 'public.migrations'],
];
const rows = [];
for (const [owner, table] of sources) {
const present = await c.query(`SELECT to_regclass($1) IS NOT NULL AS ok`, [table]);
if (!present.rows[0].ok) continue;
const r = await c.query(
`SELECT id, timestamp, name FROM ${table}
WHERE name ILIKE $1 ORDER BY id DESC LIMIT 40`,
[like],
);
rows.push(...r.rows.map((row) => ({ owner, ...row })));
}
console.table(rows);
} else if (first === 'drift') { } else if (first === 'drift') {
// Quick drift signal: DB columns for the table. Compare by eye against // Quick drift signal: DB columns for the table. Compare by eye against
// the entity's @Column names; a recorded-but-absent column = drift. // the entity's @Column names; a recorded-but-absent column = drift.

View File

@@ -118,30 +118,91 @@ const iamMigrationsGlob = join(
); );
const freightMigrationsGlob = join(__dirname, "../migrations/*.js"); const freightMigrationsGlob = join(__dirname, "../migrations/*.js");
export function buildDataSourceOptions(): DataSourceOptions { /**
* Migration history is split per owner instead of sharing one `public.migrations`
* table:
*
* - IAM migrations ship with `@tria-plc/iamapi-common`, target the `iam` schema
* and are recorded in `iam.typeorm_migrations` — the same table the package's
* own CLI (`pnpm iam:migration:run|show|revert`) uses, so both paths agree on
* what has been applied.
* - Freight migrations are recorded in `freight.migrations`.
*
* `public.migrations` is the pre-split table; `src/scripts/migrate.ts` adopts its
* rows into the two tables above on first run and then leaves it untouched.
*/
export const IAM_MIGRATIONS = {
schema: "iam",
table: "typeorm_migrations",
} as const;
export const FREIGHT_MIGRATIONS = {
schema: "freight",
table: "migrations",
} as const;
export const LEGACY_MIGRATIONS = {
schema: "public",
table: "migrations",
} as const;
function buildConnectionOptions() {
return { return {
type: "postgres", type: "postgres" as const,
host: process.env.DB_HOST ?? "localhost", host: process.env.DB_HOST ?? "localhost",
port: parseInt(process.env.DB_PORT ?? "5433", 10), port: parseInt(process.env.DB_PORT ?? "5433", 10),
username: process.env.DB_USER ?? "postgres", username: process.env.DB_USER ?? "postgres",
password: process.env.DB_PASSWORD ?? "", password: process.env.DB_PASSWORD ?? "",
database: process.env.DB_NAME ?? "edr_freight", database: process.env.DB_NAME ?? "edr_freight",
schema: "public",
// NOTE: do NOT pass `extra.options: '-c search_path=...'`. That sends the // NOTE: do NOT pass `extra.options: '-c search_path=...'`. That sends the
// Postgres startup `options` parameter, which connection poolers (PgBouncer / // Postgres startup `options` parameter, which connection poolers (PgBouncer /
// proxies fronting the remote edr_dev DB) reject with // proxies fronting the remote edr_dev DB) reject with
// `08P01 unsupported startup parameter in options: search_path`. // `08P01 unsupported startup parameter in options: search_path`.
// The search_path is instead applied per-connection via a pool `connect` // The search_path is instead applied per-connection via a pool `connect`
// handler in app.module.ts (see setPoolSearchPath). // handler (app.module.ts `setPoolSearchPath`, migrate.ts `applySearchPath`).
entities: [__dirname + "/../**/*.entity.{ts,js}", ...iamEntities],
migrations: [
iamMigrationsGlob,
freightMigrationsGlob,
],
migrationsTransactionMode: "each",
synchronize: false, synchronize: false,
logging: logging:
process.env.TYPEORM_LOGGING === "true" ? true : ["error", "warn"], process.env.TYPEORM_LOGGING === "true"
? true
: (["error", "warn"] as DataSourceOptions["logging"]),
};
}
/**
* Runtime options for the API (and the seed scripts using `AppDataSource`).
* Carries no migrations: migrations run only through `src/scripts/migrate.ts`,
* which uses the two dedicated DataSources below.
*/
export function buildDataSourceOptions(): DataSourceOptions {
return {
...buildConnectionOptions(),
schema: "public",
entities: [__dirname + "/../**/*.entity.{ts,js}", ...iamEntities],
migrations: [],
};
}
/** IAM migrations only, recorded in `iam.typeorm_migrations`. */
export function buildIamMigrationDataSourceOptions(): DataSourceOptions {
return {
...buildConnectionOptions(),
schema: IAM_MIGRATIONS.schema,
entities: [],
migrations: [iamMigrationsGlob],
migrationsTableName: IAM_MIGRATIONS.table,
migrationsTransactionMode: "each",
};
}
/** Freight migrations only, recorded in `freight.migrations`. */
export function buildFreightMigrationDataSourceOptions(): DataSourceOptions {
return {
...buildConnectionOptions(),
schema: FREIGHT_MIGRATIONS.schema,
entities: [],
migrations: [freightMigrationsGlob],
migrationsTableName: FREIGHT_MIGRATIONS.table,
migrationsTransactionMode: "each",
}; };
} }

View File

@@ -10,6 +10,13 @@ export const APPLICATION_SCHEMAS = [
export const APPLICATION_SEARCH_PATH = APPLICATION_SCHEMAS.join(","); export const APPLICATION_SEARCH_PATH = APPLICATION_SCHEMAS.join(",");
/**
* Extensions the migrations call into but never create themselves — both the IAM
* package's migrations and the freight baseline default columns to
* `uuid_generate_v4()` / `gen_random_uuid()`.
*/
export const APPLICATION_EXTENSIONS = ["uuid-ossp", "pgcrypto"] as const;
/** /**
* TypeORM creates the migrations table before any migration runs. If `public` was * TypeORM creates the migrations table before any migration runs. If `public` was
* dropped, current_schema() is null and CREATE TABLE migrations fails. * dropped, current_schema() is null and CREATE TABLE migrations fails.
@@ -42,6 +49,20 @@ export async function ensurePostgresSchemas(
} }
} }
// Best effort: creating an extension needs elevated rights the app user may not
// have. On an established database they are already installed and this is a
// no-op, so a failure here is only fatal for a brand-new database — where the
// first migration will fail loudly on the missing function anyway.
for (const extension of APPLICATION_EXTENSIONS) {
try {
await bootstrap.query(`CREATE EXTENSION IF NOT EXISTS "${extension}"`);
} catch (err) {
console.warn(
`could not ensure extension "${extension}": ${(err as Error).message}`,
);
}
}
await bootstrap.query( await bootstrap.query(
`SET search_path TO ${APPLICATION_SEARCH_PATH}`, `SET search_path TO ${APPLICATION_SEARCH_PATH}`,
); );

View File

@@ -1,233 +0,0 @@
import { MigrationInterface, QueryRunner, Table, TableIndex, TableForeignKey } from "typeorm";
export class AddServiceTypesAndCargoTypes1748427600000 implements MigrationInterface {
name = "AddServiceTypesAndCargoTypes1748427600000";
public async up(queryRunner: QueryRunner): Promise<void> {
// Create service_types table
if (!(await queryRunner.hasTable("freight.service_types"))) await queryRunner.createTable(
new Table({
name: "service_types",
schema: "freight",
columns: [
{
name: "id",
type: "uuid",
isPrimary: true,
generationStrategy: "uuid",
default: "uuid_generate_v4()",
},
{
name: "service_name",
type: "varchar",
length: "255",
isNullable: false,
},
{
name: "description",
type: "text",
isNullable: true,
},
{
name: "can_be_booked_alone",
type: "boolean",
default: true,
isNullable: false,
},
{
name: "includes_first_mile",
type: "boolean",
default: false,
isNullable: false,
},
{
name: "includes_last_mile",
type: "boolean",
default: false,
isNullable: false,
},
{
name: "includes_customs",
type: "boolean",
default: false,
isNullable: false,
},
{
name: "priority_bonus_points",
type: "int",
default: 0,
isNullable: false,
},
{
name: "is_active",
type: "boolean",
default: true,
isNullable: false,
},
{
name: "display_order",
type: "int",
default: 1,
isNullable: false,
},
{
name: "created_at",
type: "timestamptz",
default: "now()",
isNullable: false,
},
{
name: "updated_at",
type: "timestamptz",
default: "now()",
isNullable: false,
},
{
name: "deleted_at",
type: "timestamptz",
isNullable: true,
},
],
}),
true,
);
// Create indexes for service_types
const table = await queryRunner.getTable("freight.service_types");
if (table && !table.indices.some((idx) => idx.name === "IDX_SERVICE_TYPES_IS_ACTIVE")) {
await queryRunner.createIndex(
"freight.service_types",
new TableIndex({
name: "IDX_SERVICE_TYPES_IS_ACTIVE",
columnNames: ["is_active"],
}),
);
}
if (table && !table.indices.some((idx) => idx.name === "IDX_SERVICE_TYPES_DISPLAY_ORDER")) {
await queryRunner.createIndex(
"freight.service_types",
new TableIndex({
name: "IDX_SERVICE_TYPES_DISPLAY_ORDER",
columnNames: ["display_order"],
}),
);
}
// Create cargo_types table
if (!(await queryRunner.hasTable("freight.cargo_types"))) await queryRunner.createTable(
new Table({
name: "cargo_types",
schema: "freight",
columns: [
{
name: "id",
type: "uuid",
isPrimary: true,
generationStrategy: "uuid",
default: "uuid_generate_v4()",
},
{
name: "cargo_type_name",
type: "varchar",
length: "255",
isNullable: false,
},
{
name: "parent_group_id",
type: "uuid",
isNullable: true,
},
{
name: "show_free_text_box",
type: "boolean",
default: false,
isNullable: false,
},
{
name: "requires_director_approval",
type: "boolean",
default: false,
isNullable: false,
},
{
name: "is_active",
type: "boolean",
default: true,
isNullable: false,
},
{
name: "display_order",
type: "int",
default: 1,
isNullable: false,
},
{
name: "created_at",
type: "timestamptz",
default: "now()",
isNullable: false,
},
{
name: "updated_at",
type: "timestamptz",
default: "now()",
isNullable: false,
},
{
name: "deleted_at",
type: "timestamptz",
isNullable: true,
},
],
}),
true,
);
// Create indexes for cargo_types
await queryRunner.createIndex(
"freight.cargo_types",
new TableIndex({
name: "IDX_CARGO_TYPES_IS_ACTIVE",
columnNames: ["is_active"],
}),
);
await queryRunner.createIndex(
"freight.cargo_types",
new TableIndex({
name: "IDX_CARGO_TYPES_DISPLAY_ORDER",
columnNames: ["display_order"],
}),
);
await queryRunner.createIndex(
"freight.cargo_types",
new TableIndex({
name: "IDX_CARGO_TYPES_PARENT_GROUP_ID",
columnNames: ["parent_group_id"],
}),
);
// Create self-referencing foreign key for cargo_types
await queryRunner.createForeignKey(
"freight.cargo_types",
new TableForeignKey({
name: "FK_CARGO_TYPES_PARENT_GROUP",
columnNames: ["parent_group_id"],
referencedSchema: "freight",
referencedTableName: "cargo_types",
referencedColumnNames: ["id"],
onDelete: "SET NULL",
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
// Drop foreign key first
await queryRunner.dropForeignKey("freight.cargo_types", "FK_CARGO_TYPES_PARENT_GROUP");
// Drop cargo_types table
await queryRunner.dropTable("freight.cargo_types", true);
// Drop service_types table
await queryRunner.dropTable("freight.service_types", true);
}
}

View File

@@ -1,293 +0,0 @@
import {
MigrationInterface,
QueryRunner,
Table,
TableIndex,
TableForeignKey,
TableColumn,
} from 'typeorm';
export class AddRuleEngineTablesAndCodes1748514000000 implements MigrationInterface {
name = 'AddRuleEngineTablesAndCodes1748514000000';
public async up(queryRunner: QueryRunner): Promise<void> {
// ── 1. Add `code` column to existing tables ───────────────────────────
if (!(await queryRunner.hasColumn('freight.service_types', 'code'))) {
await queryRunner.addColumn(
'freight.service_types',
new TableColumn({
name: 'code',
type: 'varchar',
length: '50',
isNullable: true,
}),
);
}
await queryRunner.query(
`UPDATE freight.service_types SET code = upper(replace(service_name, ' ', '_')) WHERE code IS NULL OR code = ''`,
);
await queryRunner.query(
`UPDATE freight.service_types SET code = 'SERVICE_' || substring(id::text, 1, 8) WHERE code IS NULL`,
);
await queryRunner.query(
`ALTER TABLE freight.service_types ALTER COLUMN code SET NOT NULL`,
);
const serviceTypesCodeIdx = await queryRunner.query(
`SELECT 1 FROM pg_indexes WHERE schemaname = 'freight' AND indexname = 'idx_service_types_code' LIMIT 1`,
);
if (serviceTypesCodeIdx.length === 0) {
await queryRunner.createIndex(
'freight.service_types',
new TableIndex({ name: 'IDX_service_types_code', columnNames: ['code'], isUnique: true }),
);
}
if (!(await queryRunner.hasColumn('freight.cargo_types', 'code'))) {
await queryRunner.addColumn(
'freight.cargo_types',
new TableColumn({
name: 'code',
type: 'varchar',
length: '50',
isNullable: true,
}),
);
}
await queryRunner.query(
`UPDATE freight.cargo_types SET code = upper(replace(cargo_type_name, ' ', '_')) WHERE code IS NULL OR code = ''`,
);
await queryRunner.query(
`UPDATE freight.cargo_types SET code = 'CARGO_' || substring(id::text, 1, 8) WHERE code IS NULL`,
);
await queryRunner.query(
`ALTER TABLE freight.cargo_types ALTER COLUMN code SET NOT NULL`,
);
const cargoTypesCodeIdx = await queryRunner.query(
`SELECT 1 FROM pg_indexes WHERE schemaname = 'freight' AND indexname = 'idx_cargo_types_code' LIMIT 1`,
);
if (cargoTypesCodeIdx.length === 0) {
await queryRunner.createIndex(
'freight.cargo_types',
new TableIndex({ name: 'IDX_cargo_types_code', columnNames: ['code'], isUnique: true }),
);
}
// ── 2. surcharge_types ────────────────────────────────────────────────
if (!(await queryRunner.hasTable('freight.surcharge_types'))) await queryRunner.createTable(
new Table({
name: 'surcharge_types',
schema: 'freight',
columns: [
{ name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' },
{ name: 'code', type: 'varchar', length: '50', isNullable: false },
{ name: 'name', type: 'varchar', length: '100', isNullable: false },
{ name: 'description', type: 'text', isNullable: true },
{ name: 'is_active', type: 'boolean', default: true },
{ name: 'created_at', type: 'timestamptz', default: 'now()' },
{ name: 'updated_at', type: 'timestamptz', default: 'now()' },
{ name: 'deleted_at', type: 'timestamptz', isNullable: true },
],
}),
true,
);
await queryRunner.createIndex(
'freight.surcharge_types',
new TableIndex({ name: 'IDX_surcharge_types_code', columnNames: ['code'], isUnique: true }),
);
await queryRunner.createIndex(
'freight.surcharge_types',
new TableIndex({ name: 'IDX_surcharge_types_is_active', columnNames: ['is_active'] }),
);
// ── 3. surcharges ─────────────────────────────────────────────────────
if (!(await queryRunner.hasTable('freight.surcharges'))) await queryRunner.createTable(
new Table({
name: 'surcharges',
schema: 'freight',
columns: [
{ name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' },
{ name: 'surcharge_type_id', type: 'uuid', isNullable: false },
{ name: 'fee_name', type: 'varchar', length: '255', isNullable: false },
{ name: 'trigger_description', type: 'text', isNullable: true },
{
name: 'calculation_method',
type: 'enum',
enum: ['PER_TON', 'FLAT_FEE', 'PERCENTAGE'],
default: `'PER_TON'`,
},
{ name: 'rate', type: 'numeric', precision: 10, scale: 2, isNullable: false },
{ name: 'currency', type: 'char', length: '3', default: `'USD'` },
{ name: 'apply_to_rail', type: 'boolean', default: false },
{ name: 'apply_to_first_mile', type: 'boolean', default: false },
{ name: 'apply_to_last_mile', type: 'boolean', default: false },
{ name: 'is_active', type: 'boolean', default: true },
{ name: 'created_at', type: 'timestamptz', default: 'now()' },
{ name: 'updated_at', type: 'timestamptz', default: 'now()' },
{ name: 'deleted_at', type: 'timestamptz', isNullable: true },
],
}),
true,
);
await queryRunner.createForeignKey(
'freight.surcharges',
new TableForeignKey({
name: 'FK_surcharges_surcharge_type',
columnNames: ['surcharge_type_id'],
referencedTableName: 'freight.surcharge_types',
referencedColumnNames: ['id'],
onDelete: 'RESTRICT',
}),
);
await queryRunner.createIndex(
'freight.surcharges',
new TableIndex({ name: 'IDX_surcharges_surcharge_type_id', columnNames: ['surcharge_type_id'] }),
);
await queryRunner.createIndex(
'freight.surcharges',
new TableIndex({ name: 'IDX_surcharges_is_active', columnNames: ['is_active'] }),
);
// ── 4. container_types ────────────────────────────────────────────────
if (!(await queryRunner.hasTable('freight.container_types'))) await queryRunner.createTable(
new Table({
name: 'container_types',
schema: 'freight',
columns: [
{ name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' },
{ name: 'size_code', type: 'varchar', length: '20', isNullable: false },
{ name: 'description', type: 'varchar', length: '100', isNullable: true },
{ name: 'containers_per_wagon', type: 'int', isNullable: false },
{ name: 'is_active', type: 'boolean', default: true },
{ name: 'created_at', type: 'timestamptz', default: 'now()' },
{ name: 'updated_at', type: 'timestamptz', default: 'now()' },
{ name: 'deleted_at', type: 'timestamptz', isNullable: true },
],
}),
true,
);
await queryRunner.createIndex(
'freight.container_types',
new TableIndex({ name: 'IDX_container_types_size_code', columnNames: ['size_code'], isUnique: true }),
);
await queryRunner.createIndex(
'freight.container_types',
new TableIndex({ name: 'IDX_container_types_is_active', columnNames: ['is_active'] }),
);
// ── 5. weight_limit_rules ─────────────────────────────────────────────
if (!(await queryRunner.hasTable('freight.weight_limit_rules'))) await queryRunner.createTable(
new Table({
name: 'weight_limit_rules',
schema: 'freight',
columns: [
{ name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' },
{ name: 'container_type_id', type: 'uuid', isNullable: false },
{
name: 'trade_direction',
type: 'enum',
enum: ['IMPORT', 'EXPORT', 'BOTH'],
isNullable: false,
},
{ name: 'max_weight_tons', type: 'numeric', precision: 10, scale: 2, isNullable: false },
{ name: 'warning_threshold_tons', type: 'numeric', precision: 10, scale: 2, isNullable: false },
{
name: 'exceeded_action',
type: 'enum',
enum: ['WARNING_ONLY', 'HARD_BLOCK'],
default: `'WARNING_ONLY'`,
},
{ name: 'surcharge_id', type: 'uuid', isNullable: true },
{ name: 'is_active', type: 'boolean', default: true },
{ name: 'created_at', type: 'timestamptz', default: 'now()' },
{ name: 'updated_at', type: 'timestamptz', default: 'now()' },
{ name: 'deleted_at', type: 'timestamptz', isNullable: true },
],
}),
true,
);
await queryRunner.createForeignKey(
'freight.weight_limit_rules',
new TableForeignKey({
name: 'FK_weight_limit_rules_container_type',
columnNames: ['container_type_id'],
referencedTableName: 'freight.container_types',
referencedColumnNames: ['id'],
onDelete: 'RESTRICT',
}),
);
await queryRunner.createForeignKey(
'freight.weight_limit_rules',
new TableForeignKey({
name: 'FK_weight_limit_rules_surcharge',
columnNames: ['surcharge_id'],
referencedTableName: 'freight.surcharges',
referencedColumnNames: ['id'],
onDelete: 'SET NULL',
}),
);
await queryRunner.createIndex(
'freight.weight_limit_rules',
new TableIndex({ name: 'IDX_weight_limit_rules_container_type_id', columnNames: ['container_type_id'] }),
);
await queryRunner.createIndex(
'freight.weight_limit_rules',
new TableIndex({ name: 'IDX_weight_limit_rules_surcharge_id', columnNames: ['surcharge_id'] }),
);
await queryRunner.createIndex(
'freight.weight_limit_rules',
new TableIndex({ name: 'IDX_weight_limit_rules_is_active', columnNames: ['is_active'] }),
);
// ── 6. priority_rules ─────────────────────────────────────────────────
if (!(await queryRunner.hasTable('freight.priority_rules'))) await queryRunner.createTable(
new Table({
name: 'priority_rules',
schema: 'freight',
columns: [
{ name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' },
{
name: 'priority_type',
type: 'enum',
enum: ['USD_PAYER', 'RAIL_AND_FORWARDING', 'GOVERNMENT_ACCOUNT', 'HIGH_VOLUME_SHIPMENT'],
isNullable: false,
},
{ name: 'rule_name', type: 'varchar', length: '255', isNullable: false },
{ name: 'description', type: 'text', isNullable: true },
{ name: 'activation_condition', type: 'text', isNullable: true },
{ name: 'bonus_points', type: 'int', default: 0 },
{ name: 'is_active', type: 'boolean', default: false },
{ name: 'created_at', type: 'timestamptz', default: 'now()' },
{ name: 'updated_at', type: 'timestamptz', default: 'now()' },
{ name: 'deleted_at', type: 'timestamptz', isNullable: true },
],
}),
true,
);
await queryRunner.createIndex(
'freight.priority_rules',
new TableIndex({ name: 'IDX_priority_rules_priority_type', columnNames: ['priority_type'], isUnique: true }),
);
await queryRunner.createIndex(
'freight.priority_rules',
new TableIndex({ name: 'IDX_priority_rules_is_active', columnNames: ['is_active'] }),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('freight.priority_rules', true);
await queryRunner.dropTable('freight.weight_limit_rules', true);
await queryRunner.dropTable('freight.container_types', true);
await queryRunner.dropTable('freight.surcharges', true);
await queryRunner.dropTable('freight.surcharge_types', true);
await queryRunner.dropIndex('freight.cargo_types', 'IDX_cargo_types_code');
await queryRunner.dropColumn('freight.cargo_types', 'code');
await queryRunner.dropIndex('freight.service_types', 'IDX_service_types_code');
await queryRunner.dropColumn('freight.service_types', 'code');
}
}

View File

@@ -1,88 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
/**
* Pre-ITMLS baseline for fresh databases. Older environments created `freight.bookings`
* via synchronize or manual SQL; ItmlsFullSchemaRewrite only ALTERs that table.
*/
export class CreateFreightLegacyBaseline1748550000000 implements MigrationInterface {
name = 'CreateFreightLegacyBaseline1748550000000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE SCHEMA IF NOT EXISTS freight`);
await queryRunner.query(`
DO $$ BEGIN
CREATE TYPE freight.train_status AS ENUM (
'AVAILABLE', 'SCHEDULED', 'IN_SERVICE', 'UNDER_MAINTENANCE', 'OUT_OF_SERVICE'
);
EXCEPTION WHEN duplicate_object THEN NULL;
END $$;
`);
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS freight.trains (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
code VARCHAR(32) NOT NULL UNIQUE,
capacity_tons NUMERIC(10, 2) NOT NULL DEFAULT 0,
status freight.train_status NOT NULL DEFAULT 'AVAILABLE',
notes TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
deleted_at TIMESTAMPTZ
);
`);
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS freight.bookings (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
reference VARCHAR(64) NOT NULL UNIQUE,
customer_id UUID NOT NULL,
train_id UUID,
status VARCHAR(40) NOT NULL DEFAULT 'DRAFT',
scheduled_date TIMESTAMPTZ NOT NULL DEFAULT now(),
total_amount NUMERIC(14, 2) NOT NULL DEFAULT 0,
payment_status VARCHAR(20) NOT NULL DEFAULT 'PENDING',
contract_type VARCHAR(20) NOT NULL DEFAULT 'SPOT',
previous_contract_id UUID,
trade_direction VARCHAR(10) NOT NULL DEFAULT 'IMPORT',
equipment_return VARCHAR(20) NOT NULL DEFAULT 'RETURN',
first_mile_pickup_address TEXT,
last_mile_delivery_address TEXT,
cargo_total_weight_vgm NUMERIC(12, 3) NOT NULL DEFAULT 0,
is_hazardous BOOLEAN NOT NULL DEFAULT false,
payment_currency VARCHAR(5) NOT NULL DEFAULT 'USD',
start_date DATE,
end_date DATE,
financial_terms TEXT,
version_number INT NOT NULL DEFAULT 1,
approved_by_staff_id UUID,
approved_by_staff_at TIMESTAMPTZ,
signed_by_director_id UUID,
signed_by_director_at TIMESTAMPTZ,
signed_by_ceo_id UUID,
signed_by_ceo_at TIMESTAMPTZ,
priority_score INT NOT NULL DEFAULT 0,
allow_consolidation BOOLEAN NOT NULL DEFAULT false,
consolidation_partner_id UUID,
origin_station VARCHAR(255),
destination_station VARCHAR(255),
service_type VARCHAR(100),
freight_type VARCHAR(100),
freight_subtype VARCHAR(255),
containers JSONB,
first_mile_enabled BOOLEAN DEFAULT false,
last_mile_enabled BOOLEAN DEFAULT false,
is_refrigerated BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
deleted_at TIMESTAMPTZ
);
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE IF EXISTS freight.bookings CASCADE`);
await queryRunner.query(`DROP TABLE IF EXISTS freight.trains CASCADE`);
await queryRunner.query(`DROP TYPE IF EXISTS freight.train_status`);
}
}

View File

@@ -1,544 +0,0 @@
import {
MigrationInterface,
QueryRunner,
Table,
TableForeignKey,
TableIndex,
TableUnique,
} from 'typeorm';
export class ItmlsFullSchemaRewrite1748600000000 implements MigrationInterface {
name = 'ItmlsFullSchemaRewrite1748600000000';
public async up(queryRunner: QueryRunner): Promise<void> {
// ── container_types ───────────────────────────────────────────────────
await queryRunner.query(`
DO $$ BEGIN
ALTER TABLE freight.container_types RENAME COLUMN size_code TO code;
EXCEPTION WHEN undefined_column THEN NULL;
END $$;
`);
await queryRunner.query(`
DO $$ BEGIN
ALTER TABLE freight.container_types RENAME COLUMN description TO label;
EXCEPTION WHEN undefined_column THEN NULL;
END $$;
`);
await queryRunner.query(`
ALTER TABLE freight.container_types
ADD COLUMN IF NOT EXISTS size_ft SMALLINT,
ADD COLUMN IF NOT EXISTS is_reefer BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN IF NOT EXISTS is_open_top BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN IF NOT EXISTS display_order INT NOT NULL DEFAULT 1;
`);
await queryRunner.query(`
ALTER TABLE freight.container_types
ADD COLUMN IF NOT EXISTS wagons_per_unit NUMERIC(4,2);
`);
await queryRunner.query(`
UPDATE freight.container_types
SET wagons_per_unit = CASE
WHEN containers_per_wagon > 0 THEN ROUND(1.0 / containers_per_wagon, 2)
ELSE 1.00
END
WHERE wagons_per_unit IS NULL;
`);
await queryRunner.query(`
UPDATE freight.container_types
SET size_ft = CASE WHEN code LIKE '40%' OR code LIKE '%40%' THEN 40 ELSE 20 END
WHERE size_ft IS NULL;
`);
await queryRunner.query(`
ALTER TABLE freight.container_types
ALTER COLUMN wagons_per_unit SET NOT NULL,
DROP COLUMN IF EXISTS containers_per_wagon;
`);
// ── weight_limit_rules ──────────────────────────────────────────────────
await queryRunner.query(`
DO $$ BEGIN
ALTER TABLE freight.weight_limit_rules RENAME COLUMN max_weight_tons TO max_vgm_tons;
EXCEPTION WHEN undefined_column THEN NULL;
END $$;
`);
await queryRunner.query(`
ALTER TABLE freight.weight_limit_rules
ALTER COLUMN max_vgm_tons TYPE NUMERIC(8,3);
`);
await queryRunner.query(`
ALTER TABLE freight.weight_limit_rules
ADD COLUMN IF NOT EXISTS effective_from DATE NOT NULL DEFAULT CURRENT_DATE,
ADD COLUMN IF NOT EXISTS effective_to DATE;
`);
await queryRunner.query(`
ALTER TABLE freight.weight_limit_rules
DROP COLUMN IF EXISTS warning_threshold_tons,
DROP COLUMN IF EXISTS exceeded_action,
DROP COLUMN IF EXISTS surcharge_id,
DROP COLUMN IF EXISTS is_active;
`);
// ── priority_rules ────────────────────────────────────────────────────
await queryRunner.query(`
ALTER TABLE freight.priority_rules
ADD COLUMN IF NOT EXISTS code VARCHAR(40),
ADD COLUMN IF NOT EXISTS label VARCHAR(100),
ADD COLUMN IF NOT EXISTS score INT NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS condition_currency VARCHAR(5);
`);
await queryRunner.query(`
UPDATE freight.priority_rules
SET code = COALESCE(code, upper(priority_type::text)),
label = COALESCE(label, rule_name),
score = COALESCE(score, bonus_points)
WHERE code IS NULL OR label IS NULL;
`);
await queryRunner.query(`
ALTER TABLE freight.priority_rules
DROP COLUMN IF EXISTS priority_type,
DROP COLUMN IF EXISTS rule_name,
DROP COLUMN IF EXISTS bonus_points,
DROP COLUMN IF EXISTS activation_condition,
DROP COLUMN IF EXISTS description;
`);
await queryRunner.query(`
ALTER TABLE freight.priority_rules
ALTER COLUMN code SET NOT NULL,
ALTER COLUMN label SET NOT NULL;
`);
await queryRunner.createIndex(
'freight.priority_rules',
new TableIndex({ name: 'UQ_priority_rules_code', columnNames: ['code'], isUnique: true }),
);
// ── rates (before surcharge_types.rate_id) ────────────────────────────
await queryRunner.createTable(
new Table({
name: 'rates',
schema: 'freight',
columns: [
{ name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' },
{ name: 'rate_type', type: 'varchar', length: '50' },
{ name: 'container_type_id', type: 'uuid', isNullable: true },
{ name: 'trade_direction', type: 'varchar', length: '10', isNullable: true },
{ name: 'currency', type: 'varchar', length: '5' },
{ name: 'rate_value', type: 'numeric', precision: 14, scale: 4 },
{ name: 'rate_unit', type: 'varchar', length: '30' },
{ name: 'status', type: 'varchar', length: '20', default: "'DRAFT'" },
{ name: 'proposed_by_staff_id', type: 'uuid' },
{ name: 'approved_by_ceo_id', type: 'uuid', isNullable: true },
{ name: 'approved_at', type: 'timestamptz', isNullable: true },
{ name: 'effective_from', type: 'date' },
{ name: 'effective_to', type: 'date', isNullable: true },
{ name: 'created_at', type: 'timestamptz', default: 'now()' },
{ name: 'updated_at', type: 'timestamptz', default: 'now()' },
{ name: 'deleted_at', type: 'timestamptz', isNullable: true },
],
}),
true,
);
// ── surcharge_types ───────────────────────────────────────────────────
await queryRunner.query(`
DO $$ BEGIN
ALTER TABLE freight.surcharge_types RENAME COLUMN name TO label;
EXCEPTION WHEN undefined_column THEN NULL;
END $$;
`);
await queryRunner.query(`
ALTER TABLE freight.surcharge_types
ADD COLUMN IF NOT EXISTS trigger_condition VARCHAR(50),
ADD COLUMN IF NOT EXISTS rate_id UUID;
`);
await queryRunner.query(`ALTER TABLE freight.surcharge_types DROP COLUMN IF EXISTS description`);
await queryRunner.query(`DROP TABLE IF EXISTS freight.surcharges CASCADE`);
// ── yards ─────────────────────────────────────────────────────────────
await queryRunner.createTable(
new Table({
name: 'yards',
schema: 'freight',
columns: [
{ name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' },
{ name: 'code', type: 'varchar', length: '20' },
{ name: 'label', type: 'varchar', length: '100' },
{ name: 'country', type: 'varchar', length: '50' },
{ name: 'is_active', type: 'boolean', default: true },
{ name: 'display_order', type: 'int', default: 1 },
{ name: 'created_at', type: 'timestamptz', default: 'now()' },
{ name: 'updated_at', type: 'timestamptz', default: 'now()' },
{ name: 'deleted_at', type: 'timestamptz', isNullable: true },
],
}),
true,
);
await queryRunner.createIndex(
'freight.yards',
new TableIndex({ name: 'UQ_yards_code', columnNames: ['code'], isUnique: true }),
);
// ── shipping_lines ────────────────────────────────────────────────────
await queryRunner.createTable(
new Table({
name: 'shipping_lines',
schema: 'freight',
columns: [
{ name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' },
{ name: 'code', type: 'varchar', length: '20' },
{ name: 'label', type: 'varchar', length: '100' },
{ name: 'mapped_to_code', type: 'varchar', length: '20', isNullable: true },
{ name: 'show_extra_fee_notice', type: 'boolean', default: false },
{ name: 'is_active', type: 'boolean', default: true },
{ name: 'created_at', type: 'timestamptz', default: 'now()' },
{ name: 'updated_at', type: 'timestamptz', default: 'now()' },
{ name: 'deleted_at', type: 'timestamptz', isNullable: true },
],
}),
true,
);
// ── approval_rules ────────────────────────────────────────────────────
await queryRunner.createTable(
new Table({
name: 'approval_rules',
schema: 'freight',
columns: [
{ name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' },
{ name: 'requires_director_approval', type: 'boolean' },
{ name: 'step_order', type: 'smallint' },
{ name: 'required_role', type: 'varchar', length: '30' },
{ name: 'action_label', type: 'varchar', length: '50' },
{ name: 'blocks_role', type: 'varchar', length: '30', isNullable: true },
{ name: 'created_at', type: 'timestamptz', default: 'now()' },
{ name: 'updated_at', type: 'timestamptz', default: 'now()' },
{ name: 'deleted_at', type: 'timestamptz', isNullable: true },
],
}),
true,
);
await queryRunner.createUniqueConstraint(
'freight.approval_rules',
new TableUnique({
name: 'UQ_approval_rules_chain_step',
columnNames: ['requires_director_approval', 'step_order'],
}),
);
// ── bookings ────────────────────────────────────────────────────────
await queryRunner.query(`
ALTER TABLE freight.bookings
ADD COLUMN IF NOT EXISTS origin_yard_id UUID,
ADD COLUMN IF NOT EXISTS destination_yard_id UUID,
ADD COLUMN IF NOT EXISTS service_type_id UUID,
ADD COLUMN IF NOT EXISTS cargo_type_id UUID,
ADD COLUMN IF NOT EXISTS cargo_free_text VARCHAR(200),
ADD COLUMN IF NOT EXISTS shipping_line_id UUID,
ADD COLUMN IF NOT EXISTS pnr_code VARCHAR(50),
ADD COLUMN IF NOT EXISTS customer_signed_at TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS fully_executed_at TIMESTAMPTZ;
`);
await queryRunner.query(`
INSERT INTO freight.yards (id, code, label, country, is_active, display_order, created_at, updated_at)
VALUES
(uuid_generate_v4(), 'KALITY', 'Kality Rail Terminal', 'Ethiopia', true, 1, now(), now()),
(uuid_generate_v4(), 'MOJO', 'Mojo Dry Port', 'Ethiopia', true, 2, now(), now()),
(uuid_generate_v4(), 'DIRE_DAWA', 'Dire Dawa Yard', 'Ethiopia', true, 3, now(), now()),
(uuid_generate_v4(), 'DJIB_PORT', 'Djibouti Port Terminal', 'Djibouti', true, 4, now(), now()),
(uuid_generate_v4(), 'NAGAD', 'Nagad Terminal, Djibouti', 'Djibouti', true, 5, now(), now()),
(uuid_generate_v4(), 'LEGACY_ORIGIN', 'Legacy Origin', 'Ethiopia', true, 99, now(), now()),
(uuid_generate_v4(), 'LEGACY_DEST', 'Legacy Destination', 'Ethiopia', true, 100, now(), now())
ON CONFLICT (code) DO NOTHING;
`);
await queryRunner.query(`
INSERT INTO freight.service_types (id, code, service_name, can_be_booked_alone, includes_first_mile, includes_last_mile, includes_customs, priority_bonus_points, is_active, display_order, created_at, updated_at)
SELECT uuid_generate_v4(), 'RAIL', 'Rail Transport Only', true, false, false, false, 0, true, 1, now(), now()
WHERE NOT EXISTS (SELECT 1 FROM freight.service_types LIMIT 1);
`);
await queryRunner.query(`
INSERT INTO freight.cargo_types (id, code, cargo_type_name, requires_director_approval, show_free_text_box, is_active, display_order, created_at, updated_at)
SELECT uuid_generate_v4(), 'GENERAL', 'General Cargo', false, false, true, 1, now(), now()
WHERE NOT EXISTS (SELECT 1 FROM freight.cargo_types LIMIT 1);
`);
const hasServiceTypeCol = await queryRunner.query(`
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'freight' AND table_name = 'bookings' AND column_name = 'service_type'
LIMIT 1;
`);
if (hasServiceTypeCol.length > 0) {
await queryRunner.query(`
UPDATE freight.bookings b
SET service_type_id = st.id
FROM freight.service_types st
WHERE b.service_type_id IS NULL
AND (
st.code = b.service_type
OR upper(replace(st.service_name, ' ', '_')) = upper(b.service_type)
OR st.code = upper(replace(b.service_type, ' ', '_'))
);
`);
await queryRunner.query(`
UPDATE freight.bookings b
SET cargo_type_id = ct.id
FROM freight.cargo_types ct
WHERE b.cargo_type_id IS NULL
AND (
ct.code = upper(b.freight_type)
OR ct.code = upper(concat(b.freight_type, '_', coalesce(b.freight_subtype, '')))
);
`);
await queryRunner.query(`
UPDATE freight.bookings b
SET cargo_free_text = b.freight_subtype
WHERE b.cargo_free_text IS NULL AND b.freight_subtype IS NOT NULL;
`);
await queryRunner.query(`
UPDATE freight.bookings b
SET origin_yard_id = y.id
FROM freight.yards y
WHERE b.origin_yard_id IS NULL
AND (y.label ILIKE b.origin_station OR y.code = upper(replace(b.origin_station, ' ', '_')));
`);
await queryRunner.query(`
UPDATE freight.bookings b
SET destination_yard_id = y.id
FROM freight.yards y
WHERE b.destination_yard_id IS NULL
AND (y.label ILIKE b.destination_station OR y.code = upper(replace(b.destination_station, ' ', '_')));
`);
}
const defaultServiceTypeId = await queryRunner.query(
`SELECT id FROM freight.service_types ORDER BY display_order LIMIT 1`,
);
const defaultCargoTypeId = await queryRunner.query(
`SELECT id FROM freight.cargo_types ORDER BY display_order LIMIT 1`,
);
const legacyOriginId = await queryRunner.query(
`SELECT id FROM freight.yards WHERE code = 'LEGACY_ORIGIN' LIMIT 1`,
);
const legacyDestId = await queryRunner.query(
`SELECT id FROM freight.yards WHERE code = 'LEGACY_DEST' LIMIT 1`,
);
if (defaultServiceTypeId[0]?.id) {
await queryRunner.query(
`UPDATE freight.bookings SET service_type_id = $1 WHERE service_type_id IS NULL`,
[defaultServiceTypeId[0].id],
);
}
if (defaultCargoTypeId[0]?.id) {
await queryRunner.query(
`UPDATE freight.bookings SET cargo_type_id = $1 WHERE cargo_type_id IS NULL`,
[defaultCargoTypeId[0].id],
);
}
if (legacyOriginId[0]?.id) {
await queryRunner.query(
`UPDATE freight.bookings SET origin_yard_id = $1 WHERE origin_yard_id IS NULL`,
[legacyOriginId[0].id],
);
}
if (legacyDestId[0]?.id) {
await queryRunner.query(
`UPDATE freight.bookings SET destination_yard_id = $1 WHERE destination_yard_id IS NULL`,
[legacyDestId[0].id],
);
}
const nullBookings = await queryRunner.query(
`SELECT COUNT(*)::int AS cnt FROM freight.bookings WHERE service_type_id IS NULL OR cargo_type_id IS NULL OR origin_yard_id IS NULL OR destination_yard_id IS NULL`,
);
if (nullBookings[0]?.cnt > 0) {
await queryRunner.query(`DELETE FROM freight.bookings WHERE service_type_id IS NULL OR cargo_type_id IS NULL OR origin_yard_id IS NULL OR destination_yard_id IS NULL`);
}
await queryRunner.query(`
ALTER TABLE freight.bookings
ALTER COLUMN service_type_id SET NOT NULL,
ALTER COLUMN cargo_type_id SET NOT NULL,
ALTER COLUMN origin_yard_id SET NOT NULL,
ALTER COLUMN destination_yard_id SET NOT NULL;
`);
await queryRunner.query(`
ALTER TABLE freight.bookings
DROP COLUMN IF EXISTS origin_station,
DROP COLUMN IF EXISTS destination_station,
DROP COLUMN IF EXISTS service_type,
DROP COLUMN IF EXISTS freight_type,
DROP COLUMN IF EXISTS freight_subtype,
DROP COLUMN IF EXISTS containers,
DROP COLUMN IF EXISTS first_mile_enabled,
DROP COLUMN IF EXISTS last_mile_enabled,
DROP COLUMN IF EXISTS is_refrigerated;
`);
// ── booking_container ─────────────────────────────────────────────────
await queryRunner.createTable(
new Table({
name: 'booking_container',
schema: 'freight',
columns: [
{ name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' },
{ name: 'booking_id', type: 'uuid' },
{ name: 'container_type_id', type: 'uuid' },
{ name: 'quantity', type: 'smallint' },
{ name: 'vgm_per_unit_tons', type: 'numeric', precision: 10, scale: 3 },
{ name: 'total_vgm_tons', type: 'numeric', precision: 12, scale: 3 },
{ name: 'wagons_required', type: 'numeric', precision: 6, scale: 2 },
{ name: 'weight_limit_rule_id', type: 'uuid', isNullable: true },
{ name: 'is_overweight', type: 'boolean', default: false },
{ name: 'overweight_excess_tons', type: 'numeric', precision: 10, scale: 3, isNullable: true },
{ name: 'created_at', type: 'timestamptz', default: 'now()' },
{ name: 'updated_at', type: 'timestamptz', default: 'now()' },
{ name: 'deleted_at', type: 'timestamptz', isNullable: true },
],
}),
true,
);
await queryRunner.createTable(
new Table({
name: 'booking_rate_snapshot',
schema: 'freight',
columns: [
{ name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' },
{ name: 'booking_id', type: 'uuid' },
{ name: 'rate_id', type: 'uuid' },
{ name: 'rate_type', type: 'varchar', length: '50' },
{ name: 'rate_value', type: 'numeric', precision: 14, scale: 4 },
{ name: 'rate_unit', type: 'varchar', length: '30' },
{ name: 'currency', type: 'varchar', length: '5' },
{ name: 'snapshotted_at', type: 'timestamptz' },
{ name: 'created_at', type: 'timestamptz', default: 'now()' },
{ name: 'updated_at', type: 'timestamptz', default: 'now()' },
{ name: 'deleted_at', type: 'timestamptz', isNullable: true },
],
}),
true,
);
await queryRunner.createTable(
new Table({
name: 'booking_cargo_modifier',
schema: 'freight',
columns: [
{ name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' },
{ name: 'booking_id', type: 'uuid' },
{ name: 'surcharge_type_id', type: 'uuid' },
{ name: 'trigger_value', type: 'numeric', precision: 14, scale: 4, isNullable: true },
{ name: 'calculated_amount', type: 'numeric', precision: 14, scale: 2 },
{ name: 'rate_snapshot_id', type: 'uuid' },
{ name: 'created_at', type: 'timestamptz', default: 'now()' },
{ name: 'updated_at', type: 'timestamptz', default: 'now()' },
{ name: 'deleted_at', type: 'timestamptz', isNullable: true },
],
}),
true,
);
await queryRunner.createTable(
new Table({
name: 'booking_approval_step',
schema: 'freight',
columns: [
{ name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'uuid_generate_v4()' },
{ name: 'booking_id', type: 'uuid' },
{ name: 'approval_rule_id', type: 'uuid' },
{ name: 'step_order', type: 'smallint' },
{ name: 'required_role', type: 'varchar', length: '30' },
{ name: 'status', type: 'varchar', length: '20', default: "'PENDING'" },
{ name: 'actioned_by_staff_id', type: 'uuid', isNullable: true },
{ name: 'actioned_at', type: 'timestamptz', isNullable: true },
{ name: 'remarks', type: 'text', isNullable: true },
{ name: 'created_at', type: 'timestamptz', default: 'now()' },
{ name: 'updated_at', type: 'timestamptz', default: 'now()' },
{ name: 'deleted_at', type: 'timestamptz', isNullable: true },
],
}),
true,
);
// Foreign keys
await queryRunner.createForeignKey(
'freight.surcharge_types',
new TableForeignKey({
name: 'FK_surcharge_types_rate_id',
columnNames: ['rate_id'],
referencedTableName: 'rates',
referencedSchema: 'freight',
referencedColumnNames: ['id'],
}),
);
await queryRunner.createForeignKey(
'freight.booking_container',
new TableForeignKey({
columnNames: ['booking_id'],
referencedTableName: 'bookings',
referencedSchema: 'freight',
referencedColumnNames: ['id'],
onDelete: 'CASCADE',
}),
);
await queryRunner.createForeignKey(
'freight.bookings',
new TableForeignKey({
columnNames: ['origin_yard_id'],
referencedTableName: 'yards',
referencedSchema: 'freight',
referencedColumnNames: ['id'],
}),
);
await queryRunner.createForeignKey(
'freight.bookings',
new TableForeignKey({
columnNames: ['destination_yard_id'],
referencedTableName: 'yards',
referencedSchema: 'freight',
referencedColumnNames: ['id'],
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('freight.booking_approval_step', true);
await queryRunner.dropTable('freight.booking_cargo_modifier', true);
await queryRunner.dropTable('freight.booking_rate_snapshot', true);
await queryRunner.dropTable('freight.booking_container', true);
await queryRunner.query(`
ALTER TABLE freight.bookings
ADD COLUMN IF NOT EXISTS origin_station VARCHAR(255),
ADD COLUMN IF NOT EXISTS destination_station VARCHAR(255),
ADD COLUMN IF NOT EXISTS service_type VARCHAR(30),
ADD COLUMN IF NOT EXISTS freight_type VARCHAR(20),
ADD COLUMN IF NOT EXISTS freight_subtype VARCHAR(100),
ADD COLUMN IF NOT EXISTS containers JSONB,
ADD COLUMN IF NOT EXISTS first_mile_enabled BOOLEAN DEFAULT false,
ADD COLUMN IF NOT EXISTS last_mile_enabled BOOLEAN DEFAULT false,
ADD COLUMN IF NOT EXISTS is_refrigerated BOOLEAN DEFAULT false;
`);
await queryRunner.query(`
ALTER TABLE freight.bookings
DROP COLUMN IF EXISTS origin_yard_id,
DROP COLUMN IF EXISTS destination_yard_id,
DROP COLUMN IF EXISTS service_type_id,
DROP COLUMN IF EXISTS cargo_type_id,
DROP COLUMN IF EXISTS cargo_free_text,
DROP COLUMN IF EXISTS shipping_line_id,
DROP COLUMN IF EXISTS pnr_code,
DROP COLUMN IF EXISTS customer_signed_at,
DROP COLUMN IF EXISTS fully_executed_at;
`);
await queryRunner.dropTable('freight.approval_rules', true);
await queryRunner.dropTable('freight.shipping_lines', true);
await queryRunner.dropTable('freight.yards', true);
await queryRunner.dropTable('freight.rates', true);
}
}

View File

@@ -1,94 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddBookingsConfigForeignKeys1748700000000 implements MigrationInterface {
name = 'AddBookingsConfigForeignKeys1748700000000';
public async up(queryRunner: QueryRunner): Promise<void> {
// Ensure parent config rows exist for backfill
await queryRunner.query(`
INSERT INTO freight.service_types (id, code, service_name, can_be_booked_alone, includes_first_mile, includes_last_mile, includes_customs, priority_bonus_points, is_active, display_order, created_at, updated_at)
SELECT uuid_generate_v4(), 'RAIL', 'Rail Transport Only', true, false, false, false, 0, true, 1, now(), now()
WHERE NOT EXISTS (SELECT 1 FROM freight.service_types LIMIT 1);
`);
await queryRunner.query(`
INSERT INTO freight.cargo_types (id, code, cargo_type_name, requires_director_approval, show_free_text_box, is_active, display_order, created_at, updated_at)
SELECT uuid_generate_v4(), 'GENERAL', 'General Cargo', false, false, true, 1, now(), now()
WHERE NOT EXISTS (SELECT 1 FROM freight.cargo_types LIMIT 1);
`);
// Clear orphan shipping_line references (nullable FK)
await queryRunner.query(`
UPDATE freight.bookings b
SET shipping_line_id = NULL
WHERE b.shipping_line_id IS NOT NULL
AND NOT EXISTS (
SELECT 1 FROM freight.shipping_lines sl WHERE sl.id = b.shipping_line_id
);
`);
// Backfill required FK columns
await queryRunner.query(`
UPDATE freight.bookings
SET service_type_id = (SELECT id FROM freight.service_types ORDER BY display_order LIMIT 1)
WHERE service_type_id IS NULL
OR NOT EXISTS (SELECT 1 FROM freight.service_types st WHERE st.id = service_type_id);
`);
await queryRunner.query(`
UPDATE freight.bookings
SET cargo_type_id = (SELECT id FROM freight.cargo_types ORDER BY display_order LIMIT 1)
WHERE cargo_type_id IS NULL
OR NOT EXISTS (SELECT 1 FROM freight.cargo_types ct WHERE ct.id = cargo_type_id);
`);
await queryRunner.query(`
DO $$ BEGIN
ALTER TABLE freight.bookings
ADD CONSTRAINT "FK_bookings_service_type_id"
FOREIGN KEY (service_type_id)
REFERENCES freight.service_types(id)
ON DELETE RESTRICT;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;
`);
await queryRunner.query(`
DO $$ BEGIN
ALTER TABLE freight.bookings
ADD CONSTRAINT "FK_bookings_cargo_type_id"
FOREIGN KEY (cargo_type_id)
REFERENCES freight.cargo_types(id)
ON DELETE RESTRICT;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;
`);
await queryRunner.query(`
DO $$ BEGIN
ALTER TABLE freight.bookings
ADD CONSTRAINT "FK_bookings_shipping_line_id"
FOREIGN KEY (shipping_line_id)
REFERENCES freight.shipping_lines(id)
ON DELETE SET NULL;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.bookings
DROP CONSTRAINT IF EXISTS "FK_bookings_shipping_line_id";
`);
await queryRunner.query(`
ALTER TABLE freight.bookings
DROP CONSTRAINT IF EXISTS "FK_bookings_cargo_type_id";
`);
await queryRunner.query(`
ALTER TABLE freight.bookings
DROP CONSTRAINT IF EXISTS "FK_bookings_service_type_id";
`);
}
}

View File

@@ -1,331 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddBookingsRemainingForeignKeys1748800000000 implements MigrationInterface {
name = 'AddBookingsRemainingForeignKeys1748800000000';
public async up(queryRunner: QueryRunner): Promise<void> {
const publicCustomersExists = await queryRunner.query(`
SELECT EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_schema = 'public' AND table_name = 'customers'
) AS exists
`);
const hasPublicCustomers = Boolean(publicCustomersExists[0]?.exists);
// ── freight.bookings: nullable FK cleanup ─────────────────────────────
await queryRunner.query(`
UPDATE freight.bookings b
SET train_id = NULL
WHERE b.train_id IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM freight.trains t WHERE t.id = b.train_id);
`);
await queryRunner.query(`
UPDATE freight.bookings b
SET previous_contract_id = NULL
WHERE b.previous_contract_id IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM freight.bookings pb WHERE pb.id = b.previous_contract_id);
`);
await queryRunner.query(`
UPDATE freight.bookings b
SET consolidation_partner_id = NULL
WHERE b.consolidation_partner_id IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM freight.bookings pb WHERE pb.id = b.consolidation_partner_id);
`);
// Legacy DBs only: public.customers is created/moved in MoveCustomersToFreightSchema (174890).
if (hasPublicCustomers) {
await queryRunner.query(`
DELETE FROM freight.booking_cargo_modifier bcm
USING freight.bookings b
WHERE bcm.booking_id = b.id
AND NOT EXISTS (SELECT 1 FROM public.customers c WHERE c.id = b.customer_id);
`);
await queryRunner.query(`
DELETE FROM freight.booking_approval_step bas
USING freight.bookings b
WHERE bas.booking_id = b.id
AND NOT EXISTS (SELECT 1 FROM public.customers c WHERE c.id = b.customer_id);
`);
await queryRunner.query(`
DELETE FROM freight.booking_rate_snapshot brs
USING freight.bookings b
WHERE brs.booking_id = b.id
AND NOT EXISTS (SELECT 1 FROM public.customers c WHERE c.id = b.customer_id);
`);
await queryRunner.query(`
DELETE FROM freight.booking_container bc
USING freight.bookings b
WHERE bc.booking_id = b.id
AND NOT EXISTS (SELECT 1 FROM public.customers c WHERE c.id = b.customer_id);
`);
await queryRunner.query(`
DELETE FROM freight.bookings b
WHERE NOT EXISTS (SELECT 1 FROM public.customers c WHERE c.id = b.customer_id);
`);
await queryRunner.query(`
DO $$ BEGIN
ALTER TABLE freight.bookings
ADD CONSTRAINT "FK_bookings_customer_id"
FOREIGN KEY (customer_id)
REFERENCES public.customers(id)
ON DELETE RESTRICT;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;
`);
}
// ── freight.bookings FKs ────────────────────────────────────────────
await queryRunner.query(`
DO $$ BEGIN
ALTER TABLE freight.bookings
ADD CONSTRAINT "FK_bookings_train_id"
FOREIGN KEY (train_id)
REFERENCES freight.trains(id)
ON DELETE SET NULL;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;
`);
await queryRunner.query(`
DO $$ BEGIN
ALTER TABLE freight.bookings
ADD CONSTRAINT "FK_bookings_previous_contract_id"
FOREIGN KEY (previous_contract_id)
REFERENCES freight.bookings(id)
ON DELETE SET NULL;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;
`);
await queryRunner.query(`
DO $$ BEGIN
ALTER TABLE freight.bookings
ADD CONSTRAINT "FK_bookings_consolidation_partner_id"
FOREIGN KEY (consolidation_partner_id)
REFERENCES freight.bookings(id)
ON DELETE SET NULL;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;
`);
// ── freight.booking_container ─────────────────────────────────────────
await queryRunner.query(`
UPDATE freight.booking_container bc
SET weight_limit_rule_id = NULL
WHERE bc.weight_limit_rule_id IS NOT NULL
AND NOT EXISTS (
SELECT 1 FROM freight.weight_limit_rules wlr WHERE wlr.id = bc.weight_limit_rule_id
);
`);
await queryRunner.query(`
DELETE FROM freight.booking_container bc
WHERE NOT EXISTS (SELECT 1 FROM freight.container_types ct WHERE ct.id = bc.container_type_id);
`);
await queryRunner.query(`
DO $$ BEGIN
ALTER TABLE freight.booking_container
ADD CONSTRAINT "FK_booking_container_container_type_id"
FOREIGN KEY (container_type_id)
REFERENCES freight.container_types(id)
ON DELETE RESTRICT;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;
`);
await queryRunner.query(`
DO $$ BEGIN
ALTER TABLE freight.booking_container
ADD CONSTRAINT "FK_booking_container_weight_limit_rule_id"
FOREIGN KEY (weight_limit_rule_id)
REFERENCES freight.weight_limit_rules(id)
ON DELETE SET NULL;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;
`);
// ── freight.booking_rate_snapshot ─────────────────────────────────────
await queryRunner.query(`
DELETE FROM freight.booking_cargo_modifier bcm
USING freight.booking_rate_snapshot brs
WHERE bcm.rate_snapshot_id = brs.id
AND (
NOT EXISTS (SELECT 1 FROM freight.bookings b WHERE b.id = brs.booking_id)
OR NOT EXISTS (SELECT 1 FROM freight.rates r WHERE r.id = brs.rate_id)
);
`);
await queryRunner.query(`
DELETE FROM freight.booking_rate_snapshot brs
WHERE NOT EXISTS (SELECT 1 FROM freight.bookings b WHERE b.id = brs.booking_id)
OR NOT EXISTS (SELECT 1 FROM freight.rates r WHERE r.id = brs.rate_id);
`);
await queryRunner.query(`
DO $$ BEGIN
ALTER TABLE freight.booking_rate_snapshot
ADD CONSTRAINT "FK_booking_rate_snapshot_booking_id"
FOREIGN KEY (booking_id)
REFERENCES freight.bookings(id)
ON DELETE CASCADE;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;
`);
await queryRunner.query(`
DO $$ BEGIN
ALTER TABLE freight.booking_rate_snapshot
ADD CONSTRAINT "FK_booking_rate_snapshot_rate_id"
FOREIGN KEY (rate_id)
REFERENCES freight.rates(id)
ON DELETE RESTRICT;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;
`);
// ── freight.booking_approval_step ─────────────────────────────────────
await queryRunner.query(`
DELETE FROM freight.booking_approval_step bas
WHERE NOT EXISTS (SELECT 1 FROM freight.bookings b WHERE b.id = bas.booking_id)
OR NOT EXISTS (SELECT 1 FROM freight.approval_rules ar WHERE ar.id = bas.approval_rule_id);
`);
await queryRunner.query(`
DO $$ BEGIN
ALTER TABLE freight.booking_approval_step
ADD CONSTRAINT "FK_booking_approval_step_booking_id"
FOREIGN KEY (booking_id)
REFERENCES freight.bookings(id)
ON DELETE CASCADE;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;
`);
await queryRunner.query(`
DO $$ BEGIN
ALTER TABLE freight.booking_approval_step
ADD CONSTRAINT "FK_booking_approval_step_approval_rule_id"
FOREIGN KEY (approval_rule_id)
REFERENCES freight.approval_rules(id)
ON DELETE RESTRICT;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;
`);
// ── freight.booking_cargo_modifier ────────────────────────────────────
await queryRunner.query(`
DELETE FROM freight.booking_cargo_modifier bcm
WHERE NOT EXISTS (SELECT 1 FROM freight.bookings b WHERE b.id = bcm.booking_id)
OR NOT EXISTS (SELECT 1 FROM freight.surcharge_types st WHERE st.id = bcm.surcharge_type_id)
OR NOT EXISTS (
SELECT 1 FROM freight.booking_rate_snapshot brs WHERE brs.id = bcm.rate_snapshot_id
);
`);
await queryRunner.query(`
DO $$ BEGIN
ALTER TABLE freight.booking_cargo_modifier
ADD CONSTRAINT "FK_booking_cargo_modifier_booking_id"
FOREIGN KEY (booking_id)
REFERENCES freight.bookings(id)
ON DELETE CASCADE;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;
`);
await queryRunner.query(`
DO $$ BEGIN
ALTER TABLE freight.booking_cargo_modifier
ADD CONSTRAINT "FK_booking_cargo_modifier_surcharge_type_id"
FOREIGN KEY (surcharge_type_id)
REFERENCES freight.surcharge_types(id)
ON DELETE RESTRICT;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;
`);
await queryRunner.query(`
DO $$ BEGIN
ALTER TABLE freight.booking_cargo_modifier
ADD CONSTRAINT "FK_booking_cargo_modifier_rate_snapshot_id"
FOREIGN KEY (rate_snapshot_id)
REFERENCES freight.booking_rate_snapshot(id)
ON DELETE RESTRICT;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.booking_cargo_modifier
DROP CONSTRAINT IF EXISTS "FK_booking_cargo_modifier_rate_snapshot_id";
`);
await queryRunner.query(`
ALTER TABLE freight.booking_cargo_modifier
DROP CONSTRAINT IF EXISTS "FK_booking_cargo_modifier_surcharge_type_id";
`);
await queryRunner.query(`
ALTER TABLE freight.booking_cargo_modifier
DROP CONSTRAINT IF EXISTS "FK_booking_cargo_modifier_booking_id";
`);
await queryRunner.query(`
ALTER TABLE freight.booking_approval_step
DROP CONSTRAINT IF EXISTS "FK_booking_approval_step_approval_rule_id";
`);
await queryRunner.query(`
ALTER TABLE freight.booking_approval_step
DROP CONSTRAINT IF EXISTS "FK_booking_approval_step_booking_id";
`);
await queryRunner.query(`
ALTER TABLE freight.booking_rate_snapshot
DROP CONSTRAINT IF EXISTS "FK_booking_rate_snapshot_rate_id";
`);
await queryRunner.query(`
ALTER TABLE freight.booking_rate_snapshot
DROP CONSTRAINT IF EXISTS "FK_booking_rate_snapshot_booking_id";
`);
await queryRunner.query(`
ALTER TABLE freight.booking_container
DROP CONSTRAINT IF EXISTS "FK_booking_container_weight_limit_rule_id";
`);
await queryRunner.query(`
ALTER TABLE freight.booking_container
DROP CONSTRAINT IF EXISTS "FK_booking_container_container_type_id";
`);
await queryRunner.query(`
ALTER TABLE freight.bookings
DROP CONSTRAINT IF EXISTS "FK_bookings_consolidation_partner_id";
`);
await queryRunner.query(`
ALTER TABLE freight.bookings
DROP CONSTRAINT IF EXISTS "FK_bookings_previous_contract_id";
`);
await queryRunner.query(`
ALTER TABLE freight.bookings
DROP CONSTRAINT IF EXISTS "FK_bookings_train_id";
`);
await queryRunner.query(`
ALTER TABLE freight.bookings
DROP CONSTRAINT IF EXISTS "FK_bookings_customer_id";
`);
}
}

View File

@@ -1,185 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class MoveCustomersToFreightSchema1748900000000 implements MigrationInterface {
name = 'MoveCustomersToFreightSchema1748900000000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS freight.customers (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL,
first_name VARCHAR(100) NOT NULL,
last_name VARCHAR(100) NOT NULL,
email VARCHAR(150) NOT NULL UNIQUE,
phone VARCHAR(20) NOT NULL,
company_name VARCHAR(200) NOT NULL,
company_email VARCHAR(150) NOT NULL,
company_phone VARCHAR(20) NOT NULL,
company_location VARCHAR(100) NOT NULL,
company_address TEXT NOT NULL,
customer_type VARCHAR(32),
status VARCHAR(32),
contact_person_name VARCHAR(100) NOT NULL,
contact_person_phone VARCHAR(20) NOT NULL,
tin_number VARCHAR(10) NOT NULL UNIQUE,
vat_number VARCHAR(50),
fan_number VARCHAR(16) NOT NULL UNIQUE,
general_manager_name VARCHAR(100) NOT NULL,
general_manager_email VARCHAR(150) NOT NULL,
general_manager_phone VARCHAR(20) NOT NULL,
poa_name VARCHAR(100),
poa_phone VARCHAR(20),
poa_address TEXT,
poa_email VARCHAR(150),
poa_location VARCHAR(100),
notes TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
deleted_at TIMESTAMPTZ
);
`);
await queryRunner.query(`
CREATE INDEX IF NOT EXISTS "IDX_freight_customers_email"
ON freight.customers (email);
`);
// await queryRunner.query(`
// CREATE INDEX IF NOT EXISTS "IDX_freight_customers_user_id"
// ON freight.customers (user_id);
//`);
// Copy rows from public.customers when that legacy table exists
await queryRunner.query(`
DO $$
DECLARE
has_public boolean;
has_user_id boolean;
has_userid boolean;
BEGIN
SELECT EXISTS (
SELECT 1 FROM information_schema.tables
WHERE table_schema = 'public' AND table_name = 'customers'
) INTO has_public;
IF NOT has_public THEN
RETURN;
END IF;
SELECT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'customers' AND column_name = 'user_id'
) INTO has_user_id;
SELECT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'public' AND table_name = 'customers' AND column_name = 'userid'
) INTO has_userid;
IF has_user_id THEN
INSERT INTO freight.customers (
id, user_id, first_name, last_name, email, phone,
company_name, company_email, company_phone, company_location, company_address,
contact_person_name, contact_person_phone, tin_number, vat_number, fan_number,
general_manager_name, general_manager_email, general_manager_phone,
poa_name, poa_phone, poa_address, poa_email, poa_location, notes,
created_at, updated_at
)
SELECT
id, user_id, first_name, last_name, email, phone,
company_name, company_email, company_phone, company_location, company_address,
contact_person_name, contact_person_phone, tin_number, vat_number, fan_number,
general_manager_name, general_manager_email, general_manager_phone,
poa_name, poa_phone, poa_address, poa_email, poa_location, notes,
COALESCE(created_at, now()), COALESCE(updated_at, now())
FROM public.customers
ON CONFLICT (id) DO NOTHING;
ELSIF has_userid THEN
INSERT INTO freight.customers (
id, user_id, first_name, last_name, email, phone,
company_name, company_email, company_phone, company_location, company_address,
contact_person_name, contact_person_phone, tin_number, vat_number, fan_number,
general_manager_name, general_manager_email, general_manager_phone,
poa_name, poa_phone, poa_address, poa_email, poa_location, notes,
created_at, updated_at
)
SELECT
id, userid, firstname, lastname, email, phone,
companyname, companyemail, companyphone, companylocation, companyaddress,
contactpersonname, contactpersonphone, tinnumber, vatnumber, fannumber,
generalmanagername, generalmanageremail, generalmanagerphone,
poaname, poaphone, poaaddress, poaemail, poalocation, notes,
COALESCE("createdAt", now()), COALESCE("updatedAt", now())
FROM public.customers
ON CONFLICT (id) DO NOTHING;
END IF;
END $$;
`);
await queryRunner.query(`
ALTER TABLE freight.bookings
DROP CONSTRAINT IF EXISTS "FK_bookings_customer_id";
`);
await queryRunner.query(`
DELETE FROM freight.booking_cargo_modifier bcm
USING freight.bookings b
WHERE bcm.booking_id = b.id
AND NOT EXISTS (SELECT 1 FROM freight.customers c WHERE c.id = b.customer_id);
`);
await queryRunner.query(`
DELETE FROM freight.booking_approval_step bas
USING freight.bookings b
WHERE bas.booking_id = b.id
AND NOT EXISTS (SELECT 1 FROM freight.customers c WHERE c.id = b.customer_id);
`);
await queryRunner.query(`
DELETE FROM freight.booking_rate_snapshot brs
USING freight.bookings b
WHERE brs.booking_id = b.id
AND NOT EXISTS (SELECT 1 FROM freight.customers c WHERE c.id = b.customer_id);
`);
await queryRunner.query(`
DELETE FROM freight.booking_container bc
USING freight.bookings b
WHERE bc.booking_id = b.id
AND NOT EXISTS (SELECT 1 FROM freight.customers c WHERE c.id = b.customer_id);
`);
await queryRunner.query(`
DELETE FROM freight.bookings b
WHERE NOT EXISTS (SELECT 1 FROM freight.customers c WHERE c.id = b.customer_id);
`);
await queryRunner.query(`
DO $$ BEGIN
ALTER TABLE freight.bookings
ADD CONSTRAINT "FK_bookings_customer_id"
FOREIGN KEY (customer_id)
REFERENCES freight.customers(id)
ON DELETE RESTRICT;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.bookings
DROP CONSTRAINT IF EXISTS "FK_bookings_customer_id";
`);
await queryRunner.query(`
DO $$ BEGIN
ALTER TABLE freight.bookings
ADD CONSTRAINT "FK_bookings_customer_id"
FOREIGN KEY (customer_id)
REFERENCES public.customers(id)
ON DELETE RESTRICT;
EXCEPTION
WHEN duplicate_object THEN NULL;
END $$;
`);
await queryRunner.query(`DROP TABLE IF EXISTS freight.customers CASCADE;`);
}
}

View File

@@ -1,29 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
/**
* Align weight_limit_rules.trade_direction with app code: IMPORT, EXPORT, BOTH (not ANY).
*/
export class NormalizeWeightLimitTradeDirectionBoth1749000000000
implements MigrationInterface
{
name = 'NormalizeWeightLimitTradeDirectionBoth1749000000000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
DO $$ BEGIN
UPDATE freight.weight_limit_rules
SET trade_direction = 'BOTH'
WHERE trade_direction::text = 'ANY';
UPDATE freight.weight_limit_rules
SET trade_direction = 'IMPORT'
WHERE trade_direction IS NULL;
EXCEPTION WHEN undefined_table OR undefined_column THEN NULL;
END $$;
`);
}
public async down(_queryRunner: QueryRunner): Promise<void> {
// No-op: ANY is not a valid enum value in PostgreSQL.
}
}

View File

@@ -1,37 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class CreateFreightFilesTable1749100000000 implements MigrationInterface {
name = 'CreateFreightFilesTable1749100000000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS freight.files (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
resource_id UUID NOT NULL,
resource VARCHAR(100) NOT NULL,
code VARCHAR(100) NOT NULL,
name VARCHAR(500) NOT NULL,
url TEXT NOT NULL,
size INTEGER NOT NULL,
mime_type VARCHAR(255) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
deleted_at TIMESTAMPTZ
);
`);
await queryRunner.query(`
CREATE INDEX IF NOT EXISTS "IDX_freight_files_resource"
ON freight.files (resource_id, resource);
`);
await queryRunner.query(`
CREATE INDEX IF NOT EXISTS "IDX_freight_files_resource_code"
ON freight.files (resource_id, resource, code);
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE IF EXISTS freight.files CASCADE`);
}
}

View File

@@ -1,50 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class BookingFlowRefactor1749200000000 implements MigrationInterface {
name = 'BookingFlowRefactor1749200000000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS freight.booking_review_note (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
booking_id UUID NOT NULL REFERENCES freight.bookings(id) ON DELETE CASCADE,
author_id UUID,
note TEXT NOT NULL,
type VARCHAR(30) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ
);
CREATE INDEX IF NOT EXISTS idx_booking_review_note_booking_id
ON freight.booking_review_note(booking_id);
`);
await queryRunner.query(`
ALTER TABLE freight.bookings
ADD COLUMN IF NOT EXISTS marketing_approved_by_id UUID,
ADD COLUMN IF NOT EXISTS marketing_approved_at TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS contract_summary TEXT,
ADD COLUMN IF NOT EXISTS locked_at TIMESTAMPTZ;
`);
await queryRunner.query(`
UPDATE freight.bookings SET status = 'SUBMITTED'
WHERE status IN ('RFQ_SUBMITTED', 'QUOTATION_SENT', 'QUOTATION_APPROVED');
UPDATE freight.bookings SET status = 'REJECTED'
WHERE status = 'QUOTATION_REJECTED';
UPDATE freight.bookings SET status = 'CANCELLED'
WHERE status = 'CANCELLED';
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.bookings
DROP COLUMN IF EXISTS locked_at,
DROP COLUMN IF EXISTS contract_summary,
DROP COLUMN IF EXISTS marketing_approved_at,
DROP COLUMN IF EXISTS marketing_approved_by_id;
`);
await queryRunner.query(`DROP TABLE IF EXISTS freight.booking_review_note;`);
}
}

View File

@@ -1,115 +0,0 @@
import { MigrationInterface, QueryRunner, Table, TableIndex, TableUnique } from 'typeorm';
export class CreateCompaniesModule1749200000000 implements MigrationInterface {
name = 'CreateCompaniesModule1749200000000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
schema: 'freight',
name: 'companies',
columns: [
{ name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'gen_random_uuid()' },
{ name: 'name', type: 'varchar', length: '200' },
{ name: 'type', type: 'varchar', length: '32' },
{ name: 'status', type: 'varchar', length: '32', default: "'pending'" },
{ name: 'tin', type: 'varchar', length: '10', isUnique: true },
{ name: 'vat_number', type: 'varchar', length: '50', isNullable: true },
{ name: 'business_license', type: 'varchar', length: '100', isNullable: true },
{ name: 'fan_number', type: 'varchar', length: '16', isNullable: true },
{ name: 'country', type: 'varchar', length: '32', default: "'Ethiopia'" },
{ name: 'address', type: 'text', isNullable: true },
{ name: 'phone', type: 'varchar', length: '20', isNullable: true },
{ name: 'email', type: 'varchar', length: '150', isNullable: true },
{ name: 'website', type: 'varchar', length: '200', isNullable: true },
{ name: 'attributes', type: 'jsonb', isNullable: true },
{ name: 'created_at', type: 'timestamptz', default: 'now()' },
{ name: 'updated_at', type: 'timestamptz', default: 'now()' },
{ name: 'deleted_at', type: 'timestamptz', isNullable: true },
],
}),
true,
);
await queryRunner.createTable(
new Table({
schema: 'freight',
name: 'external_profiles',
columns: [
{ name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'gen_random_uuid()' },
{ name: 'user_id', type: 'uuid' },
{ name: 'company_id', type: 'uuid' },
{ name: 'first_name', type: 'varchar', length: '100' },
{ name: 'last_name', type: 'varchar', length: '100' },
{ name: 'email', type: 'varchar', length: '150', isUnique: true },
{ name: 'phone', type: 'varchar', length: '20', isNullable: true },
{ name: 'national_id', type: 'varchar', length: '50', isNullable: true },
{ name: 'job_title', type: 'varchar', length: '100', isNullable: true },
{ name: 'is_primary_contact', type: 'boolean', default: false },
{ name: 'created_at', type: 'timestamptz', default: 'now()' },
{ name: 'updated_at', type: 'timestamptz', default: 'now()' },
{ name: 'deleted_at', type: 'timestamptz', isNullable: true },
],
foreignKeys: [
{
columnNames: ['company_id'],
referencedTableName: 'companies',
referencedSchema: 'freight',
referencedColumnNames: ['id'],
},
],
}),
true,
);
await queryRunner.createTable(
new Table({
schema: 'freight',
name: 'ff_clients',
columns: [
{ name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'gen_random_uuid()' },
{ name: 'forwarder_company_id', type: 'uuid' },
{ name: 'client_company_id', type: 'uuid' },
{ name: 'relationship_type', type: 'varchar', length: '32', default: "'managed_account'" },
{ name: 'can_book_on_behalf', type: 'boolean', default: true },
{ name: 'can_view_documents', type: 'boolean', default: true },
{ name: 'created_at', type: 'timestamptz', default: 'now()' },
{ name: 'updated_at', type: 'timestamptz', default: 'now()' },
{ name: 'deleted_at', type: 'timestamptz', isNullable: true },
],
foreignKeys: [
{
columnNames: ['forwarder_company_id'],
referencedTableName: 'companies',
referencedSchema: 'freight',
referencedColumnNames: ['id'],
},
{
columnNames: ['client_company_id'],
referencedTableName: 'companies',
referencedSchema: 'freight',
referencedColumnNames: ['id'],
},
],
}),
true,
);
await queryRunner.createIndex('freight.companies', new TableIndex({ columnNames: ['tin'] }));
await queryRunner.createIndex('freight.companies', new TableIndex({ columnNames: ['type'] }));
await queryRunner.createIndex('freight.external_profiles', new TableIndex({ columnNames: ['user_id'] }));
await queryRunner.createIndex('freight.external_profiles', new TableIndex({ columnNames: ['company_id'] }));
await queryRunner.createIndex('freight.ff_clients', new TableIndex({ columnNames: ['forwarder_company_id'] }));
await queryRunner.createIndex('freight.ff_clients', new TableIndex({ columnNames: ['client_company_id'] }));
await queryRunner.createUniqueConstraint('freight.ff_clients', new TableUnique({
columnNames: ['forwarder_company_id', 'client_company_id'],
}));
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('freight.ff_clients');
await queryRunner.dropTable('freight.external_profiles');
await queryRunner.dropTable('freight.companies');
}
}

View File

@@ -1,71 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddBookingFreightType1749300000000 implements MigrationInterface {
name = 'AddBookingFreightType1749300000000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.bookings
ADD COLUMN IF NOT EXISTS freight_type VARCHAR(20);
`);
await queryRunner.query(`
UPDATE freight.bookings b
SET freight_type = 'CONTAINER'
WHERE EXISTS (
SELECT 1 FROM freight.booking_container bc WHERE bc.booking_id = b.id
);
`);
await queryRunner.query(`
UPDATE freight.bookings b
SET freight_type = 'BULK'
WHERE freight_type IS NULL
AND b.cargo_type_id IS NOT NULL
AND EXISTS (
SELECT 1 FROM freight.cargo_types ct
WHERE ct.id = b.cargo_type_id AND ct.requires_director_approval = true
);
`);
await queryRunner.query(`
UPDATE freight.bookings
SET freight_type = 'CONTAINER'
WHERE freight_type IS NULL;
`);
await queryRunner.query(`
ALTER TABLE freight.bookings
ALTER COLUMN cargo_type_id DROP NOT NULL;
`);
await queryRunner.query(`
ALTER TABLE freight.bookings
ALTER COLUMN freight_type SET NOT NULL;
`);
await queryRunner.query(`
ALTER TABLE freight.bookings
ADD CONSTRAINT chk_bookings_freight_type
CHECK (freight_type IN ('CONTAINER', 'BULK'));
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.bookings DROP CONSTRAINT IF EXISTS chk_bookings_freight_type;
`);
await queryRunner.query(`
ALTER TABLE freight.bookings DROP COLUMN IF EXISTS freight_type;
`);
await queryRunner.query(`
UPDATE freight.bookings SET cargo_type_id = (
SELECT id FROM freight.cargo_types LIMIT 1
) WHERE cargo_type_id IS NULL;
`);
await queryRunner.query(`
ALTER TABLE freight.bookings
ALTER COLUMN cargo_type_id SET NOT NULL;
`);
}
}

View File

@@ -1,20 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddFanNumberToCompanies1749300000000 implements MigrationInterface {
name = 'AddFanNumberToCompanies1749300000000';
public async up(queryRunner: QueryRunner): Promise<void> {
// fan_number may already exist when CreateCompaniesModule ran with the full schema
await queryRunner.query(`
ALTER TABLE freight.companies
ADD COLUMN IF NOT EXISTS fan_number varchar(16) NULL;
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.companies
DROP COLUMN IF EXISTS fan_number;
`);
}
}

View File

@@ -1,45 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddContractSignatures1749400000000 implements MigrationInterface {
name = 'AddContractSignatures1749400000000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.bookings
ADD COLUMN IF NOT EXISTS contract_template_key VARCHAR(80),
ADD COLUMN IF NOT EXISTS contract_generated_at TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS pricing_breakdown JSONB;
`);
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS freight.booking_contract_signatures (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
booking_id UUID NOT NULL REFERENCES freight.bookings(id) ON DELETE CASCADE,
signer_role VARCHAR(20) NOT NULL,
signer_user_id UUID,
signer_display_name VARCHAR(200) NOT NULL,
signed_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
signature_file_id UUID REFERENCES freight.files(id) ON DELETE SET NULL,
consent_text TEXT,
ip_address VARCHAR(64),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
CONSTRAINT uq_booking_contract_signatures_role
UNIQUE (booking_id, signer_role)
);
CREATE INDEX IF NOT EXISTS idx_booking_contract_signatures_booking_id
ON freight.booking_contract_signatures(booking_id);
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE IF EXISTS freight.booking_contract_signatures;`);
await queryRunner.query(`
ALTER TABLE freight.bookings
DROP COLUMN IF EXISTS pricing_breakdown,
DROP COLUMN IF EXISTS contract_generated_at,
DROP COLUMN IF EXISTS contract_template_key;
`);
}
}

View File

@@ -1,153 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddTrainScheduling1749400000000 implements MigrationInterface {
name = 'AddTrainScheduling1749400000000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS freight.wagon_types (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
code VARCHAR(32) NOT NULL UNIQUE,
name VARCHAR(100) NOT NULL,
capacity_tons NUMERIC(10,3) NOT NULL,
length_meters NUMERIC(10,3) NOT NULL,
max_wagons_per_train INT NULL,
supported_load_types TEXT[] NOT NULL DEFAULT '{}',
is_active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
deleted_at TIMESTAMPTZ NULL
);
`);
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS freight.locomotives (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
code VARCHAR(32) NOT NULL UNIQUE,
name VARCHAR(100) NULL,
max_pull_weight_tons NUMERIC(10,3) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'AVAILABLE',
available_from TIMESTAMPTZ NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
deleted_at TIMESTAMPTZ NULL
);
`);
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS freight.train_sets (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
locomotive_id UUID NOT NULL,
total_weight_tons NUMERIC(10,3) NOT NULL,
total_length_meters NUMERIC(10,3) NOT NULL,
wagon_count INT NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'DRAFT',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
deleted_at TIMESTAMPTZ NULL,
CONSTRAINT fk_train_sets_locomotive FOREIGN KEY (locomotive_id)
REFERENCES freight.locomotives(id)
);
`);
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS freight.train_set_wagons (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
train_set_id UUID NOT NULL,
wagon_type_id UUID NOT NULL,
sequence_no INT NOT NULL,
capacity_tons NUMERIC(10,3) NOT NULL,
length_meters NUMERIC(10,3) NOT NULL,
assigned_weight_tons NUMERIC(10,3) NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
deleted_at TIMESTAMPTZ NULL,
CONSTRAINT uq_train_set_wagons_sequence UNIQUE (train_set_id, sequence_no),
CONSTRAINT fk_train_set_wagons_train_set FOREIGN KEY (train_set_id)
REFERENCES freight.train_sets(id) ON DELETE CASCADE,
CONSTRAINT fk_train_set_wagons_wagon_type FOREIGN KEY (wagon_type_id)
REFERENCES freight.wagon_types(id)
);
`);
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS freight.train_schedules (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
train_set_id UUID NOT NULL UNIQUE,
origin_station_id UUID NOT NULL,
destination_station_id UUID NOT NULL,
scheduled_departure_date TIMESTAMPTZ NOT NULL,
scheduled_arrival_date TIMESTAMPTZ NULL,
status VARCHAR(20) NOT NULL DEFAULT 'DRAFT',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
deleted_at TIMESTAMPTZ NULL,
CONSTRAINT fk_train_schedules_train_set FOREIGN KEY (train_set_id)
REFERENCES freight.train_sets(id),
CONSTRAINT fk_train_schedules_origin FOREIGN KEY (origin_station_id)
REFERENCES freight.yards(id),
CONSTRAINT fk_train_schedules_destination FOREIGN KEY (destination_station_id)
REFERENCES freight.yards(id)
);
`);
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS freight.train_schedule_bookings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
train_schedule_id UUID NOT NULL,
booking_id UUID NOT NULL UNIQUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
deleted_at TIMESTAMPTZ NULL,
CONSTRAINT uq_train_schedule_booking UNIQUE (train_schedule_id, booking_id),
CONSTRAINT fk_train_schedule_bookings_schedule FOREIGN KEY (train_schedule_id)
REFERENCES freight.train_schedules(id) ON DELETE CASCADE,
CONSTRAINT fk_train_schedule_bookings_booking FOREIGN KEY (booking_id)
REFERENCES freight.bookings(id)
);
`);
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS freight.wagon_booking_allocations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
train_set_wagon_id UUID NOT NULL,
booking_id UUID NOT NULL,
allocated_weight_tons NUMERIC(10,3) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
deleted_at TIMESTAMPTZ NULL,
CONSTRAINT fk_wagon_booking_allocations_wagon FOREIGN KEY (train_set_wagon_id)
REFERENCES freight.train_set_wagons(id) ON DELETE CASCADE,
CONSTRAINT fk_wagon_booking_allocations_booking FOREIGN KEY (booking_id)
REFERENCES freight.bookings(id)
);
`);
await queryRunner.query(`
CREATE INDEX IF NOT EXISTS idx_locomotives_status
ON freight.locomotives(status);
`);
await queryRunner.query(`
CREATE INDEX IF NOT EXISTS idx_train_sets_status
ON freight.train_sets(status);
`);
await queryRunner.query(`
CREATE INDEX IF NOT EXISTS idx_train_schedules_departure_status
ON freight.train_schedules(scheduled_departure_date, status);
`);
await queryRunner.query(`
CREATE INDEX IF NOT EXISTS idx_wagon_booking_allocations_booking
ON freight.wagon_booking_allocations(booking_id);
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE IF EXISTS freight.wagon_booking_allocations;`);
await queryRunner.query(`DROP TABLE IF EXISTS freight.train_schedule_bookings;`);
await queryRunner.query(`DROP TABLE IF EXISTS freight.train_schedules;`);
await queryRunner.query(`DROP TABLE IF EXISTS freight.train_set_wagons;`);
await queryRunner.query(`DROP TABLE IF EXISTS freight.train_sets;`);
await queryRunner.query(`DROP TABLE IF EXISTS freight.locomotives;`);
await queryRunner.query(`DROP TABLE IF EXISTS freight.wagon_types;`);
}
}

View File

@@ -1,59 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddCompanyIdToBookings1749500000000 implements MigrationInterface {
name = 'AddCompanyIdToBookings1749500000000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.bookings
ALTER COLUMN customer_id DROP NOT NULL;
`);
await queryRunner.query(`
ALTER TABLE freight.bookings
ADD COLUMN IF NOT EXISTS company_id UUID;
`);
await queryRunner.query(`
CREATE INDEX IF NOT EXISTS idx_bookings_company_id
ON freight.bookings(company_id);
`);
await queryRunner.query(`
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'FK_bookings_company_id'
) THEN
ALTER TABLE freight.bookings
ADD CONSTRAINT "FK_bookings_company_id"
FOREIGN KEY (company_id)
REFERENCES freight.companies(id);
END IF;
END $$;
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.bookings
DROP CONSTRAINT IF EXISTS "FK_bookings_company_id";
`);
await queryRunner.query(`
UPDATE freight.bookings SET customer_id = company_id WHERE customer_id IS NULL AND company_id IS NOT NULL;
`);
await queryRunner.query(`
ALTER TABLE freight.bookings
ALTER COLUMN customer_id SET NOT NULL;
`);
await queryRunner.query(`
DROP INDEX IF EXISTS freight.idx_bookings_company_id;
`);
await queryRunner.query(`
ALTER TABLE freight.bookings
DROP COLUMN IF EXISTS company_id;
`);
}
}

View File

@@ -1,19 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddBlocksRoleToApprovalStep1749600000000 implements MigrationInterface {
name = 'AddBlocksRoleToApprovalStep1749600000000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.booking_approval_step
ADD COLUMN IF NOT EXISTS blocks_role VARCHAR(30) NULL;
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.booking_approval_step
DROP COLUMN IF EXISTS blocks_role;
`);
}
}

View File

@@ -1,48 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
/**
* Seed ITMLS US-06 approval chains if missing (standard + bulk).
*/
export class SeedDefaultApprovalRules1749700000000 implements MigrationInterface {
name = 'SeedDefaultApprovalRules1749700000000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
INSERT INTO freight.approval_rules
(id, requires_director_approval, step_order, required_role, action_label, blocks_role, created_at, updated_at)
SELECT uuid_generate_v4(), false, 1, 'LINE_STAFF', 'Review & Approve', NULL, now(), now()
WHERE NOT EXISTS (
SELECT 1 FROM freight.approval_rules
WHERE requires_director_approval = false AND step_order = 1 AND deleted_at IS NULL
);
INSERT INTO freight.approval_rules
(id, requires_director_approval, step_order, required_role, action_label, blocks_role, created_at, updated_at)
SELECT uuid_generate_v4(), false, 2, 'DIRECTOR', 'Final Signature', 'LINE_STAFF', now(), now()
WHERE NOT EXISTS (
SELECT 1 FROM freight.approval_rules
WHERE requires_director_approval = false AND step_order = 2 AND deleted_at IS NULL
);
INSERT INTO freight.approval_rules
(id, requires_director_approval, step_order, required_role, action_label, blocks_role, created_at, updated_at)
SELECT uuid_generate_v4(), true, 1, 'DIRECTOR', 'Review & Approve', 'LINE_STAFF', now(), now()
WHERE NOT EXISTS (
SELECT 1 FROM freight.approval_rules
WHERE requires_director_approval = true AND step_order = 1 AND deleted_at IS NULL
);
INSERT INTO freight.approval_rules
(id, requires_director_approval, step_order, required_role, action_label, blocks_role, created_at, updated_at)
SELECT uuid_generate_v4(), true, 2, 'CEO', 'Final Signature', NULL, now(), now()
WHERE NOT EXISTS (
SELECT 1 FROM freight.approval_rules
WHERE requires_director_approval = true AND step_order = 2 AND deleted_at IS NULL
);
`);
}
public async down(_queryRunner: QueryRunner): Promise<void> {
// Keep seeded rules on rollback to avoid breaking in-flight bookings.
}
}

View File

@@ -1,28 +0,0 @@
import { MigrationInterface, QueryRunner, TableIndex } from 'typeorm';
/**
* shipping_lines was created without a unique index on code; seeder upserts require it.
*/
export class AddShippingLinesCodeUniqueIndex1749800000000 implements MigrationInterface {
name = 'AddShippingLinesCodeUniqueIndex1749800000000';
public async up(queryRunner: QueryRunner): Promise<void> {
const existing = await queryRunner.query(
`SELECT 1 FROM pg_indexes WHERE schemaname = 'freight' AND tablename = 'shipping_lines' AND indexdef ILIKE '%UNIQUE%code%' LIMIT 1`,
);
if (existing.length === 0) {
await queryRunner.createIndex(
'freight.shipping_lines',
new TableIndex({
name: 'UQ_shipping_lines_code',
columnNames: ['code'],
isUnique: true,
}),
);
}
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropIndex('freight.shipping_lines', 'UQ_shipping_lines_code');
}
}

View File

@@ -1,63 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
/**
* file_upload_settings / file_upload_fields entities had no migration; seeder requires both tables.
*/
export class CreateFileUploadSettingsTables1749900000000 implements MigrationInterface {
name = 'CreateFileUploadSettingsTables1749900000000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS freight.file_upload_settings (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
code VARCHAR(128) NOT NULL,
label VARCHAR(256) NOT NULL,
description TEXT,
entity VARCHAR(32) NOT NULL DEFAULT 'other',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
deleted_at TIMESTAMPTZ
);
`);
await queryRunner.query(`
CREATE UNIQUE INDEX IF NOT EXISTS "UQ_file_upload_settings_code"
ON freight.file_upload_settings (code);
`);
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS freight.file_upload_fields (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
setting_id UUID NOT NULL,
file_key VARCHAR(128) NOT NULL,
file_label VARCHAR(256) NOT NULL,
help_text TEXT,
is_required BOOLEAN NOT NULL DEFAULT false,
is_multiple BOOLEAN NOT NULL DEFAULT false,
max_files INTEGER NOT NULL DEFAULT 1,
allowed_extensions TEXT[] NOT NULL DEFAULT '{}'::text[],
max_size_mb INTEGER NOT NULL DEFAULT 10,
display_order INTEGER NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
deleted_at TIMESTAMPTZ,
CONSTRAINT "CHK_file_upload_fields_max_files" CHECK (max_files > 0),
CONSTRAINT "CHK_file_upload_fields_max_size_mb" CHECK (max_size_mb > 0),
CONSTRAINT "FK_file_upload_fields_setting"
FOREIGN KEY (setting_id)
REFERENCES freight.file_upload_settings(id)
ON DELETE CASCADE
);
`);
await queryRunner.query(`
CREATE UNIQUE INDEX IF NOT EXISTS "UQ_file_upload_fields_setting_file_key"
ON freight.file_upload_fields (setting_id, file_key);
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE IF EXISTS freight.file_upload_fields CASCADE`);
await queryRunner.query(`DROP TABLE IF EXISTS freight.file_upload_settings CASCADE`);
}
}

View File

@@ -1,21 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddCompanyContactColumns1750000000000 implements MigrationInterface {
name = 'AddCompanyContactColumns1750000000000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE freight.companies ADD COLUMN IF NOT EXISTS contact_person_name VARCHAR(100);`);
await queryRunner.query(`ALTER TABLE freight.companies ADD COLUMN IF NOT EXISTS contact_person_phone VARCHAR(20);`);
await queryRunner.query(`ALTER TABLE freight.companies ADD COLUMN IF NOT EXISTS general_manager_name VARCHAR(100);`);
await queryRunner.query(`ALTER TABLE freight.companies ADD COLUMN IF NOT EXISTS general_manager_email VARCHAR(150);`);
await queryRunner.query(`ALTER TABLE freight.companies ADD COLUMN IF NOT EXISTS general_manager_phone VARCHAR(20);`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE freight.companies DROP COLUMN IF EXISTS general_manager_phone;`);
await queryRunner.query(`ALTER TABLE freight.companies DROP COLUMN IF EXISTS general_manager_email;`);
await queryRunner.query(`ALTER TABLE freight.companies DROP COLUMN IF EXISTS general_manager_name;`);
await queryRunner.query(`ALTER TABLE freight.companies DROP COLUMN IF EXISTS contact_person_phone;`);
await queryRunner.query(`ALTER TABLE freight.companies DROP COLUMN IF EXISTS contact_person_name;`);
}
}

View File

@@ -1,45 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
/**
* Train entity gained extended fields; baseline trains table only had code/capacity/status/notes.
*/
export class AddTrainExtendedColumns1750000000000 implements MigrationInterface {
name = 'AddTrainExtendedColumns1750000000000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.trains
ADD COLUMN IF NOT EXISTS train_number VARCHAR(20),
ADD COLUMN IF NOT EXISTS train_name VARCHAR(100),
ADD COLUMN IF NOT EXISTS route_id UUID,
ADD COLUMN IF NOT EXISTS origin_station_id UUID,
ADD COLUMN IF NOT EXISTS destination_station_id UUID,
ADD COLUMN IF NOT EXISTS departure_time TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS arrival_time TIMESTAMPTZ,
ADD COLUMN IF NOT EXISTS locomotive_number VARCHAR(50),
ADD COLUMN IF NOT EXISTS remarks TEXT;
`);
await queryRunner.query(`
CREATE UNIQUE INDEX IF NOT EXISTS "UQ_trains_train_number"
ON freight.trains (train_number)
WHERE train_number IS NOT NULL;
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX IF EXISTS freight."UQ_trains_train_number"`);
await queryRunner.query(`
ALTER TABLE freight.trains
DROP COLUMN IF EXISTS remarks,
DROP COLUMN IF EXISTS locomotive_number,
DROP COLUMN IF EXISTS arrival_time,
DROP COLUMN IF EXISTS departure_time,
DROP COLUMN IF EXISTS destination_station_id,
DROP COLUMN IF EXISTS origin_station_id,
DROP COLUMN IF EXISTS route_id,
DROP COLUMN IF EXISTS train_name,
DROP COLUMN IF EXISTS train_number;
`);
}
}

View File

@@ -1,138 +0,0 @@
import { MigrationInterface, QueryRunner, Table, TableIndex } from 'typeorm';
export class CreateFacilitiesTable1750000000000 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
schema: 'freight',
name: 'facilities',
columns: [
{
name: 'id',
type: 'uuid',
isPrimary: true,
generationStrategy: 'uuid',
default: 'gen_random_uuid()',
},
{
name: 'code',
type: 'varchar',
length: '40',
isUnique: true,
},
{
name: 'name',
type: 'varchar',
length: '160',
},
{
name: 'description',
type: 'text',
isNullable: true,
},
{
name: 'facility_type',
type: 'varchar',
length: '32',
},
{
name: 'facility_status',
type: 'varchar',
length: '32',
default: "'ACTIVE'",
},
{
name: 'location_name',
type: 'varchar',
length: '200',
isNullable: true,
},
{
name: 'country',
type: 'varchar',
length: '100',
isNullable: true,
},
{
name: 'city',
type: 'varchar',
length: '100',
isNullable: true,
},
{
name: 'address',
type: 'text',
isNullable: true,
},
{
name: 'latitude',
type: 'numeric',
precision: 10,
scale: 8,
isNullable: true,
},
{
name: 'longitude',
type: 'numeric',
precision: 11,
scale: 8,
isNullable: true,
},
{
name: 'capacity',
type: 'numeric',
precision: 14,
scale: 3,
isNullable: true,
},
{
name: 'is_active',
type: 'boolean',
default: true,
},
{
name: 'notes',
type: 'text',
isNullable: true,
},
{
name: 'created_at',
type: 'timestamp',
default: 'CURRENT_TIMESTAMP',
},
{
name: 'updated_at',
type: 'timestamp',
default: 'CURRENT_TIMESTAMP',
},
{
name: 'deleted_at',
type: 'timestamp',
isNullable: true,
},
],
}),
);
await queryRunner.createIndex(
'freight.facilities',
new TableIndex({
name: 'idx_facilities_code',
columnNames: ['code'],
isUnique: true,
}),
);
await queryRunner.createIndex(
'freight.facilities',
new TableIndex({
name: 'idx_facilities_status',
columnNames: ['facility_status'],
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('freight.facilities');
}
}

View File

@@ -1,52 +0,0 @@
import { MigrationInterface, QueryRunner, TableColumn, TableForeignKey } from 'typeorm';
export class AddFacilityIdToWarehouses1750000000001 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
const table = await queryRunner.getTable('freight.warehouses');
if (!table) {
// warehouses table doesn't exist yet, skip this migration
return;
}
const hasColumn = table.columns.some((col) => col.name === 'facility_id');
if (hasColumn) {
// Column already exists, skip
return;
}
await queryRunner.addColumn(
'freight.warehouses',
new TableColumn({
name: 'facility_id',
type: 'uuid',
isNullable: true,
}),
);
await queryRunner.createForeignKey(
'freight.warehouses',
new TableForeignKey({
columnNames: ['facility_id'],
referencedColumnNames: ['id'],
referencedTableName: 'facilities',
referencedSchema: 'freight',
onDelete: 'SET NULL',
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
const table = await queryRunner.getTable('freight.warehouses');
if (!table) {
return;
}
const foreignKey = table.foreignKeys.find((fk) => fk.columnNames.includes('facility_id'));
if (foreignKey) {
await queryRunner.dropForeignKey('freight.warehouses', foreignKey);
}
const hasColumn = table.columns.some((col) => col.name === 'facility_id');
if (hasColumn) {
await queryRunner.dropColumn('freight.warehouses', 'facility_id');
}
}
}

View File

@@ -1,48 +0,0 @@
import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
/**
* Proof of Delivery (customer pickup) capture on cargoes:
* receiver name, delivered/picked-up timestamp, and delivery remarks.
*/
export class AddProofOfDeliveryToCargoes1750000000002 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
const table = await queryRunner.getTable('freight.cargoes');
if (!table) {
// cargoes table doesn't exist yet, skip this migration
return;
}
const columnsToAdd = [
{ name: 'receiver_name', type: 'varchar', isNullable: true },
{ name: 'delivered_at', type: 'timestamp', isNullable: true },
{ name: 'delivery_remarks', type: 'text', isNullable: true },
];
const columnsToCreate = columnsToAdd.filter(
(col) => !table.columns.some((c) => c.name === col.name),
);
if (columnsToCreate.length > 0) {
await queryRunner.addColumns(
'freight.cargoes',
columnsToCreate.map((col) => new TableColumn(col)),
);
}
}
public async down(queryRunner: QueryRunner): Promise<void> {
const table = await queryRunner.getTable('freight.cargoes');
if (!table) {
return;
}
const columnNames = ['receiver_name', 'delivered_at', 'delivery_remarks'];
const columnsToRemove = columnNames.filter((name) =>
table.columns.some((c) => c.name === name),
);
if (columnsToRemove.length > 0) {
await queryRunner.dropColumns('freight.cargoes', columnsToRemove);
}
}
}

View File

@@ -1,75 +0,0 @@
import { MigrationInterface, QueryRunner, Table, TableColumn } from 'typeorm';
/**
* Batch 4.5 — warehouse inspection reports + inventory inspection status.
*/
export class AddWarehouseInspection1750000000003 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
// inventory.inspection_status
const inventoryTable = await queryRunner.getTable('freight.warehouse_inventory');
if (inventoryTable) {
const hasColumn = inventoryTable.columns.some((col) => col.name === 'inspection_status');
if (!hasColumn) {
await queryRunner.addColumn(
'freight.warehouse_inventory',
new TableColumn({ name: 'inspection_status', type: 'varchar', length: '20', isNullable: true }),
);
}
}
// warehouse_inspection_reports table
const inspectionTable = await queryRunner.getTable('freight.warehouse_inspection_reports');
if (!inspectionTable) {
await queryRunner.createTable(
new Table({
schema: 'freight',
name: 'warehouse_inspection_reports',
columns: [
{ name: 'id', type: 'uuid', isPrimary: true, default: 'uuid_generate_v4()' },
{ name: 'inventory_id', type: 'uuid' },
{ name: 'booking_id', type: 'uuid', isNullable: true },
{ name: 'customer_id', type: 'uuid', isNullable: true },
{ name: 'report_type', type: 'varchar', length: '32', default: "'INSPECTION'" },
{ name: 'inspection_status', type: 'varchar', length: '20', default: "'NEEDS_REVIEW'" },
{ name: 'has_damage', type: 'boolean', default: false },
{ name: 'damage_description', type: 'text', isNullable: true },
{ name: 'has_weight_loss', type: 'boolean', default: false },
{ name: 'expected_weight', type: 'numeric', precision: 14, scale: 3, isNullable: true },
{ name: 'actual_weight', type: 'numeric', precision: 14, scale: 3, isNullable: true },
{ name: 'weight_loss', type: 'numeric', precision: 14, scale: 3, isNullable: true },
{ name: 'weight_loss_unit', type: 'varchar', length: '12', isNullable: true },
{ name: 'has_missing_items', type: 'boolean', default: false },
{ name: 'missing_items_description', type: 'text', isNullable: true },
{ name: 'remarks', type: 'text', isNullable: true },
{ name: 'inspected_by_id', type: 'uuid', isNullable: true },
{ name: 'inspected_at', type: 'timestamptz', isNullable: true },
{ name: 'created_at', type: 'timestamptz', default: 'now()' },
{ name: 'updated_at', type: 'timestamptz', default: 'now()' },
{ name: 'deleted_at', type: 'timestamptz', isNullable: true },
],
indices: [
{ name: 'idx_wir_inventory', columnNames: ['inventory_id'] },
{ name: 'idx_wir_booking', columnNames: ['booking_id'] },
{ name: 'idx_wir_status', columnNames: ['inspection_status'] },
],
}),
true,
);
}
}
public async down(queryRunner: QueryRunner): Promise<void> {
const inspectionTable = await queryRunner.getTable('freight.warehouse_inspection_reports');
if (inspectionTable) {
await queryRunner.dropTable('freight.warehouse_inspection_reports', true);
}
const inventoryTable = await queryRunner.getTable('freight.warehouse_inventory');
if (inventoryTable) {
const hasColumn = inventoryTable.columns.some((col) => col.name === 'inspection_status');
if (hasColumn) {
await queryRunner.dropColumn('freight.warehouse_inventory', 'inspection_status');
}
}
}
}

View File

@@ -1,87 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddRoutesAndExtendLocomotives1750100000000 implements MigrationInterface {
name = 'AddRoutesAndExtendLocomotives1750100000000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.locomotives
ADD COLUMN IF NOT EXISTS locomotive_type VARCHAR(20) NOT NULL DEFAULT 'DIESEL',
ADD COLUMN IF NOT EXISTS max_train_length_meters NUMERIC(10,3) NOT NULL DEFAULT 760,
ADD COLUMN IF NOT EXISTS power_kw NUMERIC(10,3) NULL,
ADD COLUMN IF NOT EXISTS traction_force_kn NUMERIC(10,3) NULL,
ADD COLUMN IF NOT EXISTS max_speed_kmh NUMERIC(10,3) NULL;
`);
await queryRunner.query(`
UPDATE freight.locomotives
SET status = 'OUT_OF_SERVICE'
WHERE status = 'INACTIVE';
`);
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS freight.routes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(120) NOT NULL UNIQUE,
origin_yard_id UUID NOT NULL REFERENCES freight.yards(id),
destination_yard_id UUID NOT NULL REFERENCES freight.yards(id),
is_active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
deleted_at TIMESTAMPTZ NULL
);
`);
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS freight.route_milestones (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
route_id UUID NOT NULL REFERENCES freight.routes(id) ON DELETE CASCADE,
yard_id UUID NOT NULL REFERENCES freight.yards(id),
sequence_no INT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
deleted_at TIMESTAMPTZ NULL,
CONSTRAINT uq_route_milestones_route_sequence UNIQUE (route_id, sequence_no)
);
`);
await queryRunner.query(`
CREATE INDEX IF NOT EXISTS idx_routes_origin_yard_id
ON freight.routes(origin_yard_id);
`);
await queryRunner.query(`
CREATE INDEX IF NOT EXISTS idx_routes_destination_yard_id
ON freight.routes(destination_yard_id);
`);
await queryRunner.query(`
CREATE INDEX IF NOT EXISTS idx_routes_is_active
ON freight.routes(is_active);
`);
await queryRunner.query(`
CREATE INDEX IF NOT EXISTS idx_route_milestones_route_id
ON freight.route_milestones(route_id);
`);
await queryRunner.query(`
CREATE INDEX IF NOT EXISTS idx_route_milestones_yard_id
ON freight.route_milestones(yard_id);
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE IF EXISTS freight.route_milestones;`);
await queryRunner.query(`DROP TABLE IF EXISTS freight.routes;`);
await queryRunner.query(`
ALTER TABLE freight.locomotives
DROP COLUMN IF EXISTS max_speed_kmh,
DROP COLUMN IF EXISTS traction_force_kn,
DROP COLUMN IF EXISTS power_kw,
DROP COLUMN IF EXISTS max_train_length_meters,
DROP COLUMN IF EXISTS locomotive_type;
`);
await queryRunner.query(`
UPDATE freight.locomotives
SET status = 'INACTIVE'
WHERE status = 'OUT_OF_SERVICE';
`);
}
}

View File

@@ -1,127 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class CreateFleetCrudTables1750100000000 implements MigrationInterface {
name = 'CreateFleetCrudTables1750100000000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE SCHEMA IF NOT EXISTS freight`);
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS freight.wagons (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
wagon_number VARCHAR NOT NULL UNIQUE,
wagon_type_id UUID NOT NULL,
train_id UUID,
sequence_number INT,
tare_weight NUMERIC(10, 2) NOT NULL,
max_payload_weight NUMERIC(10, 2) NOT NULL,
status VARCHAR NOT NULL DEFAULT 'AVAILABLE',
notes TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
deleted_at TIMESTAMPTZ
);
`);
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS freight.containers (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
container_number VARCHAR NOT NULL UNIQUE,
container_type_id UUID NOT NULL,
wagon_id UUID,
position INT,
tare_weight NUMERIC(10, 2) NOT NULL,
max_gross_weight NUMERIC(10, 2) NOT NULL,
seal_number VARCHAR,
status VARCHAR NOT NULL DEFAULT 'AVAILABLE',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
deleted_at TIMESTAMPTZ
);
`);
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS freight.cargoes (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
cargo_reference VARCHAR NOT NULL UNIQUE,
shipment_id UUID NOT NULL,
container_id UUID NOT NULL,
cargo_type_id UUID,
description TEXT,
quantity NUMERIC(12, 3) NOT NULL,
weight NUMERIC(10, 2) NOT NULL,
volume NUMERIC(10, 2),
status VARCHAR NOT NULL DEFAULT 'PENDING',
loaded_at TIMESTAMP,
unloaded_at TIMESTAMP,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
deleted_at TIMESTAMPTZ
);
`);
await queryRunner.query(`CREATE INDEX IF NOT EXISTS "IDX_wagons_train_id" ON freight.wagons (train_id)`);
await queryRunner.query(`CREATE INDEX IF NOT EXISTS "IDX_containers_wagon_id" ON freight.containers (wagon_id)`);
await queryRunner.query(`CREATE INDEX IF NOT EXISTS "IDX_cargoes_container_id" ON freight.cargoes (container_id)`);
await queryRunner.query(`
DO $$ BEGIN
ALTER TABLE freight.wagons
ADD CONSTRAINT "FK_wagons_train_id"
FOREIGN KEY (train_id) REFERENCES freight.trains(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_wagon_type_id"
FOREIGN KEY (wagon_type_id) REFERENCES freight.wagon_types(id);
EXCEPTION WHEN duplicate_object THEN NULL;
END $$;
`);
await queryRunner.query(`
DO $$ BEGIN
ALTER TABLE freight.containers
ADD CONSTRAINT "FK_containers_wagon_id"
FOREIGN KEY (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.containers
ADD CONSTRAINT "FK_containers_container_type_id"
FOREIGN KEY (container_type_id) REFERENCES freight.container_types(id);
EXCEPTION WHEN duplicate_object THEN NULL;
END $$;
`);
await queryRunner.query(`
DO $$ BEGIN
ALTER TABLE freight.cargoes
ADD CONSTRAINT "FK_cargoes_container_id"
FOREIGN KEY (container_id) REFERENCES freight.containers(id) ON DELETE RESTRICT;
EXCEPTION WHEN duplicate_object THEN NULL;
END $$;
`);
await queryRunner.query(`
DO $$ BEGIN
ALTER TABLE freight.cargoes
ADD CONSTRAINT "FK_cargoes_cargo_type_id"
FOREIGN KEY (cargo_type_id) REFERENCES freight.cargo_types(id);
EXCEPTION WHEN duplicate_object THEN NULL;
END $$;
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE IF EXISTS freight.cargoes CASCADE`);
await queryRunner.query(`DROP TABLE IF EXISTS freight.containers CASCADE`);
await queryRunner.query(`DROP TABLE IF EXISTS freight.wagons CASCADE`);
}
}

View File

@@ -1,48 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddPhysicalWagonToTrainSetWagons1750200000000 implements MigrationInterface {
name = 'AddPhysicalWagonToTrainSetWagons1750200000000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.train_set_wagons
ADD COLUMN IF NOT EXISTS physical_wagon_id UUID NULL;
`);
await queryRunner.query(`
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM information_schema.table_constraints
WHERE constraint_schema = 'freight'
AND table_name = 'train_set_wagons'
AND constraint_name = 'fk_train_set_wagons_physical_wagon'
) THEN
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;
END IF;
END $$;
`);
await queryRunner.query(`
CREATE INDEX IF NOT EXISTS idx_train_set_wagons_physical_wagon
ON freight.train_set_wagons(physical_wagon_id);
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX IF EXISTS freight.idx_train_set_wagons_physical_wagon;`);
await queryRunner.query(`
ALTER TABLE freight.train_set_wagons
DROP CONSTRAINT IF EXISTS fk_train_set_wagons_physical_wagon;
`);
await queryRunner.query(`
ALTER TABLE freight.train_set_wagons
DROP COLUMN IF EXISTS physical_wagon_id;
`);
}
}

View File

@@ -1,46 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class SeedDefaultWagonTypes1750200000000 implements MigrationInterface {
name = 'SeedDefaultWagonTypes1750200000000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
INSERT INTO freight.wagon_types (
code,
name,
capacity_tons,
length_meters,
max_wagons_per_train,
supported_load_types,
is_active
)
VALUES
('NW7', 'Double deck sedan wagon', 22, 26.066, NULL, ARRAY['vehicles', 'sedan'], true),
('NW5', 'Flat wagon (container)', 70, 14.000, 53, ARRAY['container', 'steel', 'machinery'], true),
('PW2', 'Box wagon', 70, 17.066, 18, ARRAY['general cargo', 'break bulk'], true),
('GW2', 'Tank wagon', 70, 12.228, 37, ARRAY['liquid', 'fuel'], true),
('CW4', 'Gondola covered wagon', 70, 13.976, 37, ARRAY['covered bulk cargo'], true),
('CW3', 'Gondola open wagon', 70, 13.976, NULL, ARRAY['open bulk cargo'], true),
('KW2', 'Hopper covered wagon', 69, 16.466, NULL, ARRAY['bulk grains'], true),
('KW3', 'Hopper open wagon', 70, 14.400, NULL, ARRAY['coal', 'bulk cargo'], true),
('NW6', 'Flat wagon (long cargo)', 70, 18.560, NULL, ARRAY['long cargo'], true),
('BW1', 'Refrigerated wagon', 38, 21.996, NULL, ARRAY['refrigerated cargo'], true)
ON CONFLICT (code) DO UPDATE SET
name = EXCLUDED.name,
capacity_tons = EXCLUDED.capacity_tons,
length_meters = EXCLUDED.length_meters,
max_wagons_per_train = EXCLUDED.max_wagons_per_train,
supported_load_types = EXCLUDED.supported_load_types,
is_active = true,
deleted_at = NULL,
updated_at = now();
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
DELETE FROM freight.wagon_types
WHERE code IN ('NW7', 'NW5', 'PW2', 'GW2', 'CW4', 'CW3', 'KW2', 'KW3', 'NW6', 'BW1');
`);
}
}

View File

@@ -1,48 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddCurrentLocationToWagons1750300000000 implements MigrationInterface {
name = 'AddCurrentLocationToWagons1750300000000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.wagons
ADD COLUMN IF NOT EXISTS current_location_yard_id UUID NULL;
`);
await queryRunner.query(`
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM information_schema.table_constraints
WHERE constraint_schema = 'freight'
AND table_name = 'wagons'
AND constraint_name = 'FK_wagons_current_location_yard_id'
) THEN
ALTER TABLE freight.wagons
ADD CONSTRAINT "FK_wagons_current_location_yard_id"
FOREIGN KEY (current_location_yard_id)
REFERENCES freight.yards(id)
ON DELETE SET NULL;
END IF;
END $$;
`);
await queryRunner.query(`
CREATE INDEX IF NOT EXISTS "IDX_wagons_current_location_yard_id"
ON freight.wagons(current_location_yard_id);
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX IF EXISTS freight."IDX_wagons_current_location_yard_id";`);
await queryRunner.query(`
ALTER TABLE freight.wagons
DROP CONSTRAINT IF EXISTS "FK_wagons_current_location_yard_id";
`);
await queryRunner.query(`
ALTER TABLE freight.wagons
DROP COLUMN IF EXISTS current_location_yard_id;
`);
}
}

View File

@@ -1,44 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddRouteToTrainSchedules1750300000000 implements MigrationInterface {
name = 'AddRouteToTrainSchedules1750300000000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.train_schedules
ADD COLUMN IF NOT EXISTS route_id UUID NULL;
`);
await queryRunner.query(`
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'fk_train_schedules_route'
) THEN
ALTER TABLE freight.train_schedules
ADD CONSTRAINT fk_train_schedules_route
FOREIGN KEY (route_id) REFERENCES freight.routes(id);
END IF;
END $$;
`);
await queryRunner.query(`
CREATE INDEX IF NOT EXISTS idx_train_schedules_route_id
ON freight.train_schedules(route_id);
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX IF EXISTS freight.idx_train_schedules_route_id;`);
await queryRunner.query(`
ALTER TABLE freight.train_schedules
DROP CONSTRAINT IF EXISTS fk_train_schedules_route;
`);
await queryRunner.query(`
ALTER TABLE freight.train_schedules
DROP COLUMN IF EXISTS route_id;
`);
}
}

View File

@@ -1,321 +0,0 @@
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

@@ -1,214 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
type FleetRow = {
code: string;
name: string;
count: number;
start: number;
end: number;
capacityTons: number;
tareWeight: number;
lengthMeters: number;
supportedLoadTypes: string[];
};
const FLEET: FleetRow[] = [
{
code: 'PW2',
name: 'Box wagon',
count: 220,
start: 1,
end: 220,
capacityTons: 70,
tareWeight: 25.2,
lengthMeters: 17.066,
supportedLoadTypes: ['BULK', 'GENERAL_CARGO', 'BAGGED_CARGO', 'BOXED_CARGO'],
},
{
code: 'CW4',
name: 'Gondola wagon covered',
count: 110,
start: 221,
end: 330,
capacityTons: 70,
tareWeight: 24.8,
lengthMeters: 13.976,
supportedLoadTypes: ['CONTAINER'],
},
{
code: 'CW3',
name: 'Gondola wagon',
count: 20,
start: 331,
end: 350,
capacityTons: 70,
tareWeight: 23.4,
lengthMeters: 13.976,
supportedLoadTypes: ['BULK', 'COAL', 'ORE'],
},
{
code: 'KW2',
name: 'Hopper wagon covered',
count: 20,
start: 351,
end: 370,
capacityTons: 69,
tareWeight: 25.2,
lengthMeters: 16.466,
supportedLoadTypes: ['BULK', 'GRAIN'],
},
{
code: 'KW3',
name: 'Hopper wagon',
count: 20,
start: 371,
end: 390,
capacityTons: 70,
tareWeight: 24,
lengthMeters: 14.4,
supportedLoadTypes: ['BULK', 'COAL'],
},
{
code: 'NW5',
name: 'Flat wagon container',
count: 550,
start: 391,
end: 940,
capacityTons: 70,
tareWeight: 0,
lengthMeters: 14,
supportedLoadTypes: ['CONTAINER'],
},
];
const wagonNumber = (sequence: number) => `ER${String(sequence).padStart(4, '0')}`;
export class SeedEdRWagonFleet1750400000000 implements MigrationInterface {
name = 'SeedEdRWagonFleet1750400000000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
UPDATE freight.wagon_types
SET name = 'Flat wagon container',
capacity_tons = 70,
length_meters = 14.000,
supported_load_types = ARRAY['CONTAINER'],
max_wagons_per_train = 53,
is_active = true,
deleted_at = NULL,
updated_at = now()
WHERE code = 'NW5';
`);
const [defaultLocation] = await queryRunner.query(`
SELECT id
FROM freight.yards
WHERE code IN ('DJIBOUTI', 'DJIB_PORT', 'NAGAD')
OR lower(label) LIKE '%djibouti%'
ORDER BY
CASE code
WHEN 'DJIBOUTI' THEN 1
WHEN 'DJIB_PORT' THEN 2
WHEN 'NAGAD' THEN 3
ELSE 4
END,
display_order ASC
LIMIT 1;
`);
const defaultLocationYardId = defaultLocation?.id ?? null;
for (const row of FLEET) {
await queryRunner.query(
`
INSERT INTO freight.wagon_types (
code,
name,
capacity_tons,
length_meters,
max_wagons_per_train,
supported_load_types,
is_active
)
VALUES ($1, $2, $3, $4, $5, $6::text[], true)
ON CONFLICT (code) DO UPDATE SET
name = EXCLUDED.name,
capacity_tons = EXCLUDED.capacity_tons,
length_meters = EXCLUDED.length_meters,
max_wagons_per_train = EXCLUDED.max_wagons_per_train,
supported_load_types = EXCLUDED.supported_load_types,
is_active = true,
deleted_at = NULL,
updated_at = now();
`,
[
row.code,
row.name,
row.capacityTons,
row.lengthMeters,
row.supportedLoadTypes.includes('CONTAINER') ? 53 : 37,
row.supportedLoadTypes,
],
);
const [typeRecord] = await queryRunner.query(
`SELECT id FROM freight.wagon_types WHERE code = $1 LIMIT 1;`,
[row.code],
);
if (!typeRecord?.id) {
throw new Error(`wagon_type_seed_failed:${row.code}`);
}
if (row.end - row.start + 1 !== row.count) {
throw new Error(`wagon_range_mismatch:${row.code}`);
}
for (let sequence = row.start; sequence <= row.end; sequence += 1) {
await queryRunner.query(
`
INSERT INTO freight.wagons (
wagon_number,
wagon_type_id,
tare_weight,
max_payload_weight,
current_location_yard_id,
status,
notes
)
VALUES ($1, $2, $3, $4, $5, $6, $7)
ON CONFLICT (wagon_number) DO UPDATE SET
wagon_type_id = EXCLUDED.wagon_type_id,
tare_weight = EXCLUDED.tare_weight,
max_payload_weight = EXCLUDED.max_payload_weight,
current_location_yard_id = CASE
WHEN freight.wagons.train_id IS NULL THEN EXCLUDED.current_location_yard_id
ELSE freight.wagons.current_location_yard_id
END,
status = CASE
WHEN freight.wagons.train_id IS NULL THEN EXCLUDED.status
ELSE freight.wagons.status
END,
notes = EXCLUDED.notes,
updated_at = now();
`,
[
wagonNumber(sequence),
typeRecord.id,
row.tareWeight,
row.capacityTons,
defaultLocationYardId,
defaultLocationYardId ? 'IMPORT_READY' : 'AVAILABLE',
`Seeded Ethio-Djibouti Railway ${row.code} fleet record.`,
],
);
}
}
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
DELETE FROM freight.wagons
WHERE wagon_number BETWEEN 'ER0001' AND 'ER0940';
`);
}
}

View File

@@ -1,25 +0,0 @@
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

@@ -1,46 +0,0 @@
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

@@ -1,32 +0,0 @@
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

@@ -1,47 +0,0 @@
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

@@ -1,39 +0,0 @@
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

@@ -1,35 +0,0 @@
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

@@ -1,21 +0,0 @@
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;
`);
}
}

View File

@@ -1,103 +0,0 @@
import {
MigrationInterface,
QueryRunner,
Table,
TableIndex,
TableForeignKey,
} from "typeorm";
export class CreateCompanyProfiles1752000000000 implements MigrationInterface {
name = "CreateCompanyProfiles1752000000000";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
schema: "freight",
name: "company_profiles",
columns: [
{
name: "id",
type: "uuid",
isPrimary: true,
generationStrategy: "uuid",
default: "gen_random_uuid()",
},
{ name: "company_id", type: "uuid" },
{ name: "type", type: "varchar", length: "32" },
{ name: "reference", type: "varchar", length: "20", isUnique: true },
{
name: "status",
type: "varchar",
length: "32",
default: "'active'",
},
{
name: "business_license",
type: "varchar",
length: "100",
isNullable: true,
},
{ name: "attributes", type: "jsonb", isNullable: true },
{ name: "created_at", type: "timestamptz", default: "now()" },
{ name: "updated_at", type: "timestamptz", default: "now()" },
{ name: "deleted_at", type: "timestamptz", isNullable: true },
],
}),
true,
);
await queryRunner.createForeignKey(
"freight.company_profiles",
new TableForeignKey({
columnNames: ["company_id"],
referencedTableName: "companies",
referencedSchema: "freight",
referencedColumnNames: ["id"],
}),
);
await queryRunner.createIndex(
"freight.company_profiles",
new TableIndex({ columnNames: ["company_id"] }),
);
await queryRunner.createIndex(
"freight.company_profiles",
new TableIndex({ columnNames: ["type"] }),
);
await queryRunner.query(
`CREATE SEQUENCE IF NOT EXISTS freight.seq_company_profile_ex START WITH 1`,
);
await queryRunner.query(
`CREATE SEQUENCE IF NOT EXISTS freight.seq_company_profile_im START WITH 1`,
);
await queryRunner.query(
`CREATE SEQUENCE IF NOT EXISTS freight.seq_company_profile_ffe START WITH 1`,
);
await queryRunner.query(
`CREATE SEQUENCE IF NOT EXISTS freight.seq_company_profile_fwj START WITH 1`,
);
await queryRunner.query(
`CREATE SEQUENCE IF NOT EXISTS freight.seq_company_profile_tr START WITH 1`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable("freight.company_profiles");
await queryRunner.query(
`DROP SEQUENCE IF EXISTS freight.seq_company_profile_ex`,
);
await queryRunner.query(
`DROP SEQUENCE IF EXISTS freight.seq_company_profile_im`,
);
await queryRunner.query(
`DROP SEQUENCE IF EXISTS freight.seq_company_profile_ffe`,
);
await queryRunner.query(
`DROP SEQUENCE IF EXISTS freight.seq_company_profile_fwj`,
);
await queryRunner.query(
`DROP SEQUENCE IF EXISTS freight.seq_company_profile_tr`,
);
}
}

View File

@@ -1,39 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class MoveBusinessLicenseToProfile1752000000001
implements MigrationInterface
{
name = 'MoveBusinessLicenseToProfile1752000000001';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
UPDATE freight.company_profiles cp
SET business_license = c.business_license
FROM freight.companies c
WHERE cp.company_id = c.id AND c.business_license IS NOT NULL
`);
await queryRunner.query(
`ALTER TABLE freight.companies DROP COLUMN IF EXISTS business_license`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE freight.companies ADD COLUMN business_license varchar(100) NULL`,
);
await queryRunner.query(`
UPDATE freight.companies c
SET business_license = cp.business_license
FROM (
SELECT DISTINCT ON (cp2.company_id)
cp2.company_id, cp2.business_license
FROM freight.company_profiles cp2
WHERE cp2.business_license IS NOT NULL
ORDER BY cp2.company_id, cp2.created_at
) cp
WHERE cp.company_id = c.id
`);
}
}

View File

@@ -1,39 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class CreateVehiclesTable1770000000000 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'vehicles' AND table_schema = 'freight') THEN
CREATE TABLE freight.vehicles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
plate_number VARCHAR NOT NULL UNIQUE,
registration_number VARCHAR NOT NULL UNIQUE,
vehicle_type VARCHAR NOT NULL,
manufacturer VARCHAR NOT NULL,
model VARCHAR NOT NULL,
year INTEGER NOT NULL,
fuel_type VARCHAR NOT NULL,
capacity NUMERIC NOT NULL,
status VARCHAR DEFAULT 'ACTIVE' NOT NULL,
description TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
deleted_at TIMESTAMP NULL
);
CREATE INDEX idx_vehicles_plate_number ON freight.vehicles(plate_number);
CREATE INDEX idx_vehicles_registration_number ON freight.vehicles(registration_number);
CREATE INDEX idx_vehicles_status ON freight.vehicles(status);
CREATE INDEX idx_vehicles_vehicle_type ON freight.vehicles(vehicle_type);
CREATE INDEX idx_vehicles_manufacturer ON freight.vehicles(manufacturer);
END IF;
END $$;
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE IF EXISTS freight.vehicles CASCADE;`);
}
}

View File

@@ -1,42 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class CreateDriversTable1775000000000 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'drivers' AND table_schema = 'freight') THEN
CREATE TABLE freight.drivers (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
license_number VARCHAR NOT NULL UNIQUE,
first_name VARCHAR NOT NULL,
last_name VARCHAR NOT NULL,
email VARCHAR NOT NULL UNIQUE,
phone_number VARCHAR NOT NULL UNIQUE,
date_of_birth DATE NOT NULL,
license_expiry_date DATE NOT NULL,
status VARCHAR DEFAULT 'ACTIVE' NOT NULL,
vehicle_types_authorized VARCHAR[],
address TEXT,
emergency_contact VARCHAR,
notes TEXT,
total_trips INTEGER DEFAULT 0,
rating NUMERIC(3, 2),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,
deleted_at TIMESTAMP NULL
);
CREATE INDEX idx_drivers_license_number ON freight.drivers(license_number);
CREATE INDEX idx_drivers_email ON freight.drivers(email);
CREATE INDEX idx_drivers_phone_number ON freight.drivers(phone_number);
CREATE INDEX idx_drivers_status ON freight.drivers(status);
END IF;
END $$;
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE IF EXISTS freight.drivers CASCADE;`);
}
}

View File

@@ -1,98 +0,0 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class CreatePaymentTable1780639311366 implements MigrationInterface {
name = "CreatePaymentTable1780639311366";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE TYPE freight.payments_type_enum AS ENUM ('booking');
`);
await queryRunner.query(`
CREATE TYPE freight.payments_method_enum AS ENUM ('telebirr', 'cbe-birr', 'ebirr');
`);
await queryRunner.query(`
CREATE TYPE freight.payments_currency_enum AS ENUM ('ETB', 'USD');
`);
await queryRunner.query(`
CREATE TYPE freight.payments_status_enum AS ENUM (
'action-required',
'processing',
'success',
'failed',
'canceled',
'refunded'
);
`);
await queryRunner.query(`
CREATE TABLE freight.payments (
id uuid NOT NULL DEFAULT uuid_generate_v4(),
ref_id varchar(255) NOT NULL,
type freight.payments_type_enum NOT NULL,
method freight.payments_method_enum NOT NULL,
currency freight.payments_currency_enum NOT NULL,
amount numeric NOT NULL,
raw_initiation jsonb NOT NULL DEFAULT '{}'::jsonb,
client_action json,
merchant_order_id varchar(255) NOT NULL,
transaction_id varchar(255),
status freight.payments_status_enum NOT NULL DEFAULT 'action-required',
paid_at date,
refunded_at date,
expires_at date,
failer_code varchar(30),
failer_message varchar(255),
reason varchar(255),
created_at TIMESTAMP NOT NULL DEFAULT now(),
CONSTRAINT PK_payments PRIMARY KEY (id),
CONSTRAINT UQ_payments_merchant_order_id UNIQUE (merchant_order_id),
CONSTRAINT UQ_payments_transaction_id UNIQUE (transaction_id)
);
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
DROP TABLE IF EXISTS freight.payments;
`);
await queryRunner.query(`
DROP TYPE IF EXISTS freight.payments_status_enum;
`);
await queryRunner.query(`
DROP TYPE IF EXISTS freight.payments_currency_enum;
`);
await queryRunner.query(`
DROP TYPE IF EXISTS freight.payments_method_enum;
`);
await queryRunner.query(`
DROP TYPE IF EXISTS freight.payments_type_enum;
`);
}
}

View File

@@ -1,28 +0,0 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AlterClientActionToJsonb1780639978834 implements MigrationInterface {
name = "AlterClientActionToJsonb1780639978834";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.payments
ALTER COLUMN client_action TYPE jsonb
USING client_action::jsonb;
`);
await queryRunner.query(`
ALTER TABLE freight.payments
ALTER COLUMN client_action DROP DEFAULT;
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.payments
ALTER COLUMN client_action TYPE json
USING client_action::json;
`);
}
}

View File

@@ -1,33 +0,0 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class UpdatePaymentTimestamp1780644945086 implements MigrationInterface {
name = "UpdatePaymentTimestamp1780644945086";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.payments
ALTER COLUMN refunded_at TYPE timestamp
USING refunded_at::timestamp;
`);
await queryRunner.query(`
ALTER TABLE freight.payments
ALTER COLUMN expires_at TYPE timestamp
USING expires_at::timestamp;
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.payments
ALTER COLUMN refunded_at TYPE timestamptz
USING refunded_at::timestamptz;
`);
await queryRunner.query(`
ALTER TABLE freight.payments
ALTER COLUMN expires_at TYPE timestamptz
USING expires_at::timestamptz;
`);
}
}

View File

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

View File

@@ -1,35 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class CreateTrainCheckpointEvents1781000000001 implements MigrationInterface {
name = 'CreateTrainCheckpointEvents1781000000001';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS freight.train_checkpoint_events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
train_schedule_id UUID NOT NULL REFERENCES freight.train_schedules(id) ON DELETE CASCADE,
yard_id UUID NOT NULL,
sequence_no INT NOT NULL,
kind VARCHAR(20) NOT NULL,
occurred_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
note TEXT NULL,
recorded_by_user_id UUID NULL,
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_train_checkpoint_events_schedule
ON freight.train_checkpoint_events (train_schedule_id, sequence_no)
WHERE deleted_at IS NULL
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`DROP INDEX IF EXISTS freight.idx_train_checkpoint_events_schedule`,
);
await queryRunner.query(`DROP TABLE IF EXISTS freight.train_checkpoint_events`);
}
}

View File

@@ -1,45 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddBatchBookingFields1781000000002 implements MigrationInterface {
name = 'AddBatchBookingFields1781000000002';
public async up(queryRunner: QueryRunner): Promise<void> {
// Booking → target schedule (pool membership) + 1h pay-window deadline.
await queryRunner.query(`
ALTER TABLE freight.bookings
ADD COLUMN IF NOT EXISTS train_schedule_id UUID NULL,
ADD COLUMN IF NOT EXISTS payment_deadline TIMESTAMPTZ NULL
`);
await queryRunner.query(`
CREATE INDEX IF NOT EXISTS idx_bookings_train_schedule_id
ON freight.bookings (train_schedule_id)
WHERE deleted_at IS NULL
`);
// TrainSchedule → booking-window status (OPEN/FULL/CLOSED).
await queryRunner.query(`
ALTER TABLE freight.train_schedules
ADD COLUMN IF NOT EXISTS booking_window_status VARCHAR(10) NOT NULL DEFAULT 'OPEN'
`);
await queryRunner.query(`
CREATE INDEX IF NOT EXISTS idx_train_schedules_booking_window_status
ON freight.train_schedules (booking_window_status)
WHERE deleted_at IS NULL
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`DROP INDEX IF EXISTS freight.idx_train_schedules_booking_window_status`,
);
await queryRunner.query(
`ALTER TABLE freight.train_schedules DROP COLUMN IF EXISTS booking_window_status`,
);
await queryRunner.query(`DROP INDEX IF EXISTS freight.idx_bookings_train_schedule_id`);
await queryRunner.query(`
ALTER TABLE freight.bookings
DROP COLUMN IF EXISTS train_schedule_id,
DROP COLUMN IF EXISTS payment_deadline
`);
}
}

View File

@@ -1,36 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddSelectedForBatchStatus1781000000003 implements MigrationInterface {
name = 'AddSelectedForBatchStatus1781000000003';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.bookings
ADD COLUMN IF NOT EXISTS selected_for_batch_at TIMESTAMPTZ NULL
`);
await queryRunner.query(`
UPDATE freight.bookings
SET
status = 'SELECTED_FOR_BATCH',
selected_for_batch_at = COALESCE(
payment_deadline - INTERVAL '5 minutes',
updated_at
)
WHERE status = 'AWAITING_PAYMENT'
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
UPDATE freight.bookings
SET status = 'AWAITING_PAYMENT'
WHERE status = 'SELECTED_FOR_BATCH'
`);
await queryRunner.query(`
ALTER TABLE freight.bookings
DROP COLUMN IF EXISTS selected_for_batch_at
`);
}
}

View File

@@ -1,30 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
/**
* Allow DOMESTIC trade direction on weight_limit_rules (domestic corridor bookings).
*/
export class AddDomesticWeightLimitTradeDirection1781000000004
implements MigrationInterface
{
name = 'AddDomesticWeightLimitTradeDirection1781000000004';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
DO $$ BEGIN
ALTER TYPE freight.weight_limit_rules_trade_direction_enum ADD VALUE 'DOMESTIC';
EXCEPTION
WHEN duplicate_object THEN NULL;
WHEN undefined_object THEN
BEGIN
ALTER TYPE weight_limit_rules_trade_direction_enum ADD VALUE 'DOMESTIC';
EXCEPTION
WHEN duplicate_object THEN NULL;
END;
END $$;
`);
}
public async down(_queryRunner: QueryRunner): Promise<void> {
// PostgreSQL does not support removing enum values safely.
}
}

View File

@@ -1,81 +0,0 @@
import { MigrationInterface, QueryRunner, Table, TableIndex } from 'typeorm';
export class CreateTrainCompositionRemovalLog1781000000005 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
schema: 'freight',
name: 'train_composition_removal_logs',
columns: [
{
name: 'id',
type: 'uuid',
isPrimary: true,
default: 'uuid_generate_v4()',
},
{
name: 'schedule_id',
type: 'uuid',
isNullable: false,
},
{
name: 'booking_id',
type: 'uuid',
isNullable: false,
},
{
name: 'booking_reference',
type: 'varchar',
length: '64',
isNullable: true,
},
{
name: 'removed_by_user_id',
type: 'uuid',
isNullable: true,
},
{
name: 'removed_at',
type: 'timestamptz',
default: 'NOW()',
isNullable: false,
},
{
name: 'notes',
type: 'text',
isNullable: true,
},
{
name: 'created_at',
type: 'timestamptz',
default: 'NOW()',
isNullable: false,
},
{
name: 'updated_at',
type: 'timestamptz',
default: 'NOW()',
isNullable: false,
},
{
name: 'deleted_at',
type: 'timestamptz',
isNullable: true,
},
],
}),
true,
);
await queryRunner.createIndex(
'freight.train_composition_removal_logs',
new TableIndex({
columnNames: ['schedule_id'],
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('freight.train_composition_removal_logs', true);
}
}

View File

@@ -1,88 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class WagonLocomotiveYardLink1782000000000 implements MigrationInterface {
name = 'WagonLocomotiveYardLink1782000000000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.wagons
ADD COLUMN IF NOT EXISTS "current_yard_id" UUID NULL;
`);
await queryRunner.query(`
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'FK_wagon_current_yard'
) THEN
ALTER TABLE freight.wagons
ADD CONSTRAINT "FK_wagon_current_yard"
FOREIGN KEY ("current_yard_id") REFERENCES freight.yards(id) ON DELETE SET NULL;
END IF;
END $$;
`);
await queryRunner.query(`
CREATE INDEX IF NOT EXISTS "IDX_wagon_current_yard_id"
ON freight.wagons ("current_yard_id");
`);
await queryRunner.query(`DROP INDEX IF EXISTS freight.idx_wagons_readiness`);
await queryRunner.query(`ALTER TABLE freight.wagons DROP COLUMN IF EXISTS readiness;`);
await queryRunner.query(`
ALTER TABLE freight.locomotives
ADD COLUMN IF NOT EXISTS "current_yard_id" UUID NULL;
`);
await queryRunner.query(`
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'FK_locomotive_current_yard'
) THEN
ALTER TABLE freight.locomotives
ADD CONSTRAINT "FK_locomotive_current_yard"
FOREIGN KEY ("current_yard_id") REFERENCES freight.yards(id) ON DELETE SET NULL;
END IF;
END $$;
`);
await queryRunner.query(`
CREATE INDEX IF NOT EXISTS "IDX_locomotive_current_yard_id"
ON freight.locomotives ("current_yard_id");
`);
await queryRunner.query(`DROP INDEX IF EXISTS freight.idx_locomotives_readiness`);
await queryRunner.query(`ALTER TABLE freight.locomotives DROP COLUMN IF EXISTS readiness;`);
}
public async down(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(`
ALTER TABLE freight.locomotives
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;
`);
await queryRunner.query(`
CREATE INDEX IF NOT EXISTS idx_locomotives_readiness
ON freight.locomotives (readiness)
WHERE deleted_at IS NULL;
`);
await queryRunner.query(`DROP INDEX IF EXISTS freight."IDX_wagon_current_yard_id"`);
await queryRunner.query(`DROP INDEX IF EXISTS freight."IDX_locomotive_current_yard_id"`);
await queryRunner.query(`
ALTER TABLE freight.wagons DROP CONSTRAINT IF EXISTS "FK_wagon_current_yard";
`);
await queryRunner.query(`
ALTER TABLE freight.locomotives DROP CONSTRAINT IF EXISTS "FK_locomotive_current_yard";
`);
await queryRunner.query(`ALTER TABLE freight.wagons DROP COLUMN IF EXISTS "current_yard_id";`);
await queryRunner.query(`ALTER TABLE freight.locomotives DROP COLUMN IF EXISTS "current_yard_id";`);
}
}

View File

@@ -1,62 +0,0 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddPaymentWebhookEventAndRefund1782000000001 implements MigrationInterface {
name = "AddPaymentWebhookEventAndRefund1782000000001";
public async up(queryRunner: QueryRunner): Promise<void> {
// Enum for webhook provider — shares the same values as payments_method_enum
// but is a separate type so both tables remain independently evolvable.
await queryRunner.query(`
CREATE TYPE freight.payment_webhook_method_enum AS ENUM ('telebirr', 'cbe-birr', 'ebirr');
`);
await queryRunner.query(`
CREATE TABLE freight.payment_webhook_events (
id uuid NOT NULL DEFAULT uuid_generate_v4(),
provider freight.payment_webhook_method_enum NOT NULL,
external_event_id varchar(255) NOT NULL,
merchant_order_id varchar(255),
provider_txn_id varchar(255),
signature_valid boolean NOT NULL,
status varchar(100) NOT NULL,
payload jsonb NOT NULL,
received_at TIMESTAMP NOT NULL DEFAULT now(),
processed_at TIMESTAMP,
processing_error text,
CONSTRAINT PK_payment_webhook_events PRIMARY KEY (id),
CONSTRAINT UQ_payment_webhook_events_provider_event UNIQUE (provider, external_event_id)
);
`);
await queryRunner.query(`
CREATE INDEX IDX_payment_webhook_events_merchant_order_id
ON freight.payment_webhook_events (merchant_order_id);
`);
await queryRunner.query(`
CREATE TABLE freight.payment_refunds (
id uuid NOT NULL DEFAULT uuid_generate_v4(),
payment_id uuid NOT NULL,
amount_minor int NOT NULL,
reason varchar(255),
provider_refund_id varchar(255),
status varchar(50) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT now(),
CONSTRAINT PK_payment_refunds PRIMARY KEY (id),
CONSTRAINT FK_payment_refunds_payment
FOREIGN KEY (payment_id)
REFERENCES freight.payments (id)
ON DELETE RESTRICT
);
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE IF EXISTS freight.payment_refunds;`);
await queryRunner.query(`DROP INDEX IF EXISTS freight.IDX_payment_webhook_events_merchant_order_id;`);
await queryRunner.query(`DROP TABLE IF EXISTS freight.payment_webhook_events;`);
await queryRunner.query(`DROP TYPE IF EXISTS freight.payment_webhook_method_enum;`);
}
}

View File

@@ -1,16 +0,0 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class ExtendPaymentMethodEnum1782000000002 implements MigrationInterface {
name = "ExtendPaymentMethodEnum1782000000002";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TYPE freight.payments_method_enum ADD VALUE IF NOT EXISTS 'waafi';`);
await queryRunner.query(`ALTER TYPE freight.payments_method_enum ADD VALUE IF NOT EXISTS 'card';`);
await queryRunner.query(`ALTER TYPE freight.payments_method_enum ADD VALUE IF NOT EXISTS 'dmoney';`);
}
public async down(_queryRunner: QueryRunner): Promise<void> {
// PostgreSQL does not support removing enum values directly.
// To roll back, recreate the type without the added values and update the column.
}
}

View File

@@ -1,39 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class ReplacePriorityRulesWithPriorityConfigs1783000000000 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE TABLE freight.priority_configs (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
type VARCHAR(20) NOT NULL CHECK (type IN ('WAGON', 'CURRENCY')),
label VARCHAR(100) NOT NULL,
currency VARCHAR(5) NULL,
min_wagon_count INT NOT NULL,
max_wagon_count INT NOT NULL,
score_points INT NOT NULL DEFAULT 0,
is_active BOOLEAN NOT NULL DEFAULT false,
display_order INT NOT NULL DEFAULT 1,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
deleted_at TIMESTAMPTZ NULL,
CONSTRAINT chk_wagon_range CHECK (min_wagon_count <= max_wagon_count),
CONSTRAINT chk_currency_for_type CHECK (
(type = 'WAGON' AND currency IS NULL) OR
(type = 'CURRENCY' AND currency IS NOT NULL)
)
);
`);
await queryRunner.query(`
CREATE INDEX idx_priority_configs_type_active ON freight.priority_configs (type, is_active);
`);
await queryRunner.query(`
CREATE INDEX idx_priority_configs_currency_type ON freight.priority_configs (currency, type);
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE IF EXISTS freight.priority_configs;`);
}
}

View File

@@ -1,22 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class CreateSavedSignatures1784000000000 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE TABLE freight.saved_signatures (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL,
signer_display_name VARCHAR(200) NOT NULL,
signature_file_id UUID NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
deleted_at TIMESTAMPTZ NULL,
CONSTRAINT uq_saved_signatures_user_id UNIQUE (user_id)
);
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE IF EXISTS freight.saved_signatures;`);
}
}

View File

@@ -1,156 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
/**
* Full wagon re-seed — runs in this order:
*
* 1. DELETE all existing wagons (hard delete, not soft).
* 2. UPSERT all 10 standard wagon types so they are guaranteed to exist.
* 3. INSERT 50 wagons per wagon type (500 total), distributed evenly across
* the 5 main operational yards (10 wagons per yard per type):
*
* KALITY — Kality Rail Terminal
* MOJO — Mojo Dry Port
* DIRE_DAWA — Dire Dawa Yard
* DJIB_PORT — Djibouti Port Terminal
* NAGAD — Nagad Terminal, Djibouti
*
* Wagon numbers follow the pattern <TYPE_CODE>-NNNN (e.g. NW5-0001 … NW5-0050).
* Yard IDs are fetched live from freight.yards so the migration is safe across
* all environments regardless of UUID values.
*/
export class SeedWagonsWithYardAssignment1784000000001
implements MigrationInterface
{
name = 'SeedWagonsWithYardAssignment1784000000001';
public async up(queryRunner: QueryRunner): Promise<void> {
// ── STEP 1: Remove all wagons ──────────────────────────────────────────
await queryRunner.query(`DELETE FROM freight.wagons;`);
// ── STEP 2: Ensure all 10 wagon types exist ────────────────────────────
await queryRunner.query(`
INSERT INTO freight.wagon_types (
code,
name,
capacity_tons,
length_meters,
max_wagons_per_train,
supported_load_types,
is_active,
tare_weight_tons
)
VALUES
('NW7', 'Double deck sedan wagon', 22, 26.066, NULL, ARRAY['vehicles', 'sedan'], true, 18.0),
('NW5', 'Flat wagon (container)', 70, 14.000, 53, ARRAY['container', 'steel', 'machinery'], true, 22.0),
('PW2', 'Box wagon', 70, 17.066, 18, ARRAY['general cargo', 'break bulk'], true, 20.0),
('GW2', 'Tank wagon', 70, 12.228, 37, ARRAY['liquid', 'fuel'], true, 25.0),
('CW4', 'Gondola covered wagon', 70, 13.976, 37, ARRAY['covered bulk cargo'], true, 22.0),
('CW3', 'Gondola open wagon', 70, 13.976, NULL, ARRAY['open bulk cargo'], true, 20.0),
('KW2', 'Hopper covered wagon', 69, 16.466, NULL, ARRAY['bulk grains'], true, 22.0),
('KW3', 'Hopper open wagon', 70, 14.400, NULL, ARRAY['coal', 'bulk cargo'], true, 20.0),
('NW6', 'Flat wagon (long cargo)', 70, 18.560, NULL, ARRAY['long cargo'], true, 22.0),
('BW1', 'Refrigerated wagon', 38, 21.996, NULL, ARRAY['refrigerated cargo'], true, 24.0)
ON CONFLICT (code) DO UPDATE SET
name = EXCLUDED.name,
capacity_tons = EXCLUDED.capacity_tons,
length_meters = EXCLUDED.length_meters,
max_wagons_per_train = EXCLUDED.max_wagons_per_train,
supported_load_types = EXCLUDED.supported_load_types,
is_active = true,
tare_weight_tons = EXCLUDED.tare_weight_tons,
deleted_at = NULL,
updated_at = now();
`);
// ── STEP 3: Seed 50 wagons per type across 5 yards ────────────────────
await queryRunner.query(`
DO $$
DECLARE
wt RECORD;
yard_kality UUID;
yard_mojo UUID;
yard_dire_dawa UUID;
yard_djib_port UUID;
yard_nagad UUID;
yards UUID[];
i INT;
yard_id UUID;
wagon_num TEXT;
v_tare NUMERIC;
v_payload NUMERIC;
BEGIN
-- Fetch yard IDs by code (safe across envs — UUIDs differ per DB)
SELECT id INTO yard_kality FROM freight.yards WHERE code = 'KALITY' LIMIT 1;
SELECT id INTO yard_mojo FROM freight.yards WHERE code = 'MOJO' LIMIT 1;
SELECT id INTO yard_dire_dawa FROM freight.yards WHERE code = 'DIRE_DAWA' LIMIT 1;
SELECT id INTO yard_djib_port FROM freight.yards WHERE code = 'DJIB_PORT' LIMIT 1;
SELECT id INTO yard_nagad FROM freight.yards WHERE code = 'NAGAD' LIMIT 1;
IF yard_kality IS NULL OR yard_mojo IS NULL OR yard_dire_dawa IS NULL
OR yard_djib_port IS NULL OR yard_nagad IS NULL
THEN
RAISE EXCEPTION 'One or more operational yards not found. Run the yards seed first.';
END IF;
yards := ARRAY[
yard_kality,
yard_mojo,
yard_dire_dawa,
yard_djib_port,
yard_nagad
];
FOR wt IN
SELECT id, code, capacity_tons, tare_weight_tons
FROM freight.wagon_types
WHERE is_active = true
ORDER BY code
LOOP
v_tare := COALESCE(wt.tare_weight_tons, 20.0);
v_payload := COALESCE(wt.capacity_tons, 60.0);
FOR i IN 1 .. 50 LOOP
wagon_num := wt.code || '-' || LPAD(i::TEXT, 4, '0');
yard_id := yards[ ((i - 1) % 5) + 1 ]; -- round-robin: 1→K, 2→M, 3→D, 4→J, 5→N, 6→K …
INSERT INTO freight.wagons (
id,
wagon_number,
wagon_type_id,
tare_weight,
max_payload_weight,
status,
current_yard_id,
train_id,
sequence_number,
notes,
train_set_wagon_id,
current_train_schedule_id,
created_at,
updated_at
)
VALUES (
uuid_generate_v4(),
wagon_num,
wt.id,
v_tare,
v_payload,
'Available',
yard_id,
NULL, NULL, NULL, NULL, NULL,
now(), now()
)
ON CONFLICT (wagon_number) DO NOTHING;
END LOOP;
RAISE NOTICE 'Seeded 50 wagons for type %.', wt.code;
END LOOP;
END $$;
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
// Remove all seeded wagons (full wipe — mirrors what up() did)
await queryRunner.query(`DELETE FROM freight.wagons;`);
}
}

View File

@@ -1,21 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
/**
* Day-level booking pool: customers select a DAY (route + day), not a specific
* train. The batch engine's pool query filters bookings on
* (origin_yard_id, destination_yard_id, scheduled_date, status); this partial
* index backs that scan.
*/
export class AddBookingRouteDayIndex1784100000000 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE INDEX IF NOT EXISTS idx_bookings_route_day
ON freight.bookings (origin_yard_id, destination_yard_id, scheduled_date, status)
WHERE deleted_at IS NULL;
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP INDEX IF EXISTS freight.idx_bookings_route_day;`);
}
}

View File

@@ -1,125 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class CreateWarehouseModule1790000000000 implements MigrationInterface {
name = 'CreateWarehouseModule1790000000000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS freight.warehouses (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(160) NOT NULL,
code VARCHAR(40) NOT NULL UNIQUE,
type VARCHAR(32) NOT NULL,
station_id UUID NULL,
location_name VARCHAR(200) NULL,
capacity_weight NUMERIC(14,3) NULL,
capacity_containers INT NULL,
current_weight NUMERIC(14,3) NOT NULL DEFAULT 0,
current_containers INT NOT NULL DEFAULT 0,
status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE',
is_active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
deleted_at TIMESTAMPTZ NULL
);
`);
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS freight.warehouse_yards (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
warehouse_id UUID NOT NULL REFERENCES freight.warehouses(id) ON DELETE CASCADE,
name VARCHAR(160) NOT NULL,
code VARCHAR(40) NOT NULL,
type VARCHAR(32) NOT NULL,
capacity_weight NUMERIC(14,3) NULL,
capacity_containers INT NULL,
current_weight NUMERIC(14,3) NOT NULL DEFAULT 0,
current_containers INT NOT NULL DEFAULT 0,
status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE',
is_active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
deleted_at TIMESTAMPTZ NULL,
CONSTRAINT uq_warehouse_yards_code UNIQUE (warehouse_id, code)
);
`);
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS freight.warehouse_zones (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
yard_id UUID NOT NULL REFERENCES freight.warehouse_yards(id) ON DELETE CASCADE,
name VARCHAR(160) NOT NULL,
code VARCHAR(40) NOT NULL,
type VARCHAR(32) NOT NULL,
capacity_weight NUMERIC(14,3) NULL,
capacity_containers INT NULL,
current_weight NUMERIC(14,3) NOT NULL DEFAULT 0,
current_containers INT NOT NULL DEFAULT 0,
status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE',
is_active BOOLEAN NOT NULL DEFAULT true,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
deleted_at TIMESTAMPTZ NULL,
CONSTRAINT uq_warehouse_zones_code UNIQUE (yard_id, code)
);
`);
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS freight.warehouse_inventory (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
warehouse_id UUID NOT NULL REFERENCES freight.warehouses(id),
yard_id UUID NOT NULL REFERENCES freight.warehouse_yards(id),
zone_id UUID NOT NULL REFERENCES freight.warehouse_zones(id),
booking_id UUID NOT NULL,
cargo_id UUID NULL,
container_id UUID NULL,
goods_id UUID NULL,
quantity NUMERIC(12,3) NOT NULL DEFAULT 0,
weight NUMERIC(14,3) NOT NULL DEFAULT 0,
volume NUMERIC(12,3) NULL,
status VARCHAR(32) NOT NULL DEFAULT 'ARRIVED_AT_WAREHOUSE',
inspection_status VARCHAR(20) NULL,
arrived_at TIMESTAMPTZ NULL,
inspected_at TIMESTAMPTZ NULL,
ready_for_loading_at TIMESTAMPTZ NULL,
notes TEXT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
deleted_at TIMESTAMPTZ NULL
);
`);
const indexes: Array<[string, string, string]> = [
['idx_warehouses_type', 'warehouses', 'type'],
['idx_warehouses_status', 'warehouses', 'status'],
['idx_warehouses_station_id', 'warehouses', 'station_id'],
['idx_warehouse_yards_warehouse_id', 'warehouse_yards', 'warehouse_id'],
['idx_warehouse_yards_type', 'warehouse_yards', 'type'],
['idx_warehouse_yards_status', 'warehouse_yards', 'status'],
['idx_warehouse_zones_yard_id', 'warehouse_zones', 'yard_id'],
['idx_warehouse_zones_type', 'warehouse_zones', 'type'],
['idx_warehouse_zones_status', 'warehouse_zones', 'status'],
['idx_warehouse_inventory_warehouse_id', 'warehouse_inventory', 'warehouse_id'],
['idx_warehouse_inventory_yard_id', 'warehouse_inventory', 'yard_id'],
['idx_warehouse_inventory_zone_id', 'warehouse_inventory', 'zone_id'],
['idx_warehouse_inventory_booking_id', 'warehouse_inventory', 'booking_id'],
['idx_warehouse_inventory_cargo_id', 'warehouse_inventory', 'cargo_id'],
['idx_warehouse_inventory_container_id', 'warehouse_inventory', 'container_id'],
['idx_warehouse_inventory_goods_id', 'warehouse_inventory', 'goods_id'],
['idx_warehouse_inventory_status', 'warehouse_inventory', 'status'],
];
for (const [indexName, table, column] of indexes) {
await queryRunner.query(
`CREATE INDEX IF NOT EXISTS ${indexName} ON freight.${table}(${column});`,
);
}
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE IF EXISTS freight.warehouse_inventory;`);
await queryRunner.query(`DROP TABLE IF EXISTS freight.warehouse_zones;`);
await queryRunner.query(`DROP TABLE IF EXISTS freight.warehouse_yards;`);
await queryRunner.query(`DROP TABLE IF EXISTS freight.warehouses;`);
}
}

View File

@@ -1,123 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class WarehouseBatch21790000000001 implements MigrationInterface {
name = 'WarehouseBatch21790000000001';
public async up(queryRunner: QueryRunner): Promise<void> {
// ── Capacity columns (weight + volume) on warehouse / yard / zone ──────
for (const table of ['warehouses', 'warehouse_yards', 'warehouse_zones']) {
await queryRunner.query(`
ALTER TABLE freight.${table}
ADD COLUMN IF NOT EXISTS max_weight NUMERIC(14,3) NULL,
ADD COLUMN IF NOT EXISTS max_volume NUMERIC(14,3) NULL,
ADD COLUMN IF NOT EXISTS current_volume NUMERIC(14,3) NOT NULL DEFAULT 0;
`);
// Backfill max_weight from the Batch 1 capacity_weight column.
await queryRunner.query(`
UPDATE freight.${table} SET max_weight = capacity_weight WHERE max_weight IS NULL;
`);
}
// ── Inventory lifecycle: migrate Batch 1 statuses to Batch 2 set ───────
await queryRunner.query(`
ALTER TABLE freight.warehouse_inventory
ALTER COLUMN status SET DEFAULT 'RECEIVED';
`);
await queryRunner.query(`
UPDATE freight.warehouse_inventory SET status = 'RECEIVED' WHERE status = 'ARRIVED_AT_WAREHOUSE';
`);
await queryRunner.query(`
UPDATE freight.warehouse_inventory SET status = 'STORED' WHERE status = 'UNDER_INSPECTION';
`);
// ── New lifecycle timestamps ──────────────────────────────────────────
await queryRunner.query(`
ALTER TABLE freight.warehouse_inventory
ADD COLUMN IF NOT EXISTS stored_at TIMESTAMPTZ NULL,
ADD COLUMN IF NOT EXISTS reserved_at TIMESTAMPTZ NULL,
ADD COLUMN IF NOT EXISTS loaded_at TIMESTAMPTZ NULL,
ADD COLUMN IF NOT EXISTS dispatched_at TIMESTAMPTZ NULL;
`);
// booking_id becomes nullable (inventory can exist before booking linkage).
await queryRunner.query(`
ALTER TABLE freight.warehouse_inventory ALTER COLUMN booking_id DROP NOT NULL;
`);
// ── Movement history ──────────────────────────────────────────────────
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS freight.warehouse_inventory_movement (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
inventory_id UUID NOT NULL REFERENCES freight.warehouse_inventory(id) ON DELETE CASCADE,
from_warehouse_id UUID NOT NULL,
from_yard_id UUID NOT NULL,
from_zone_id UUID NOT NULL,
to_warehouse_id UUID NOT NULL,
to_yard_id UUID NOT NULL,
to_zone_id UUID NOT NULL,
remarks TEXT NULL,
moved_by VARCHAR(120) NULL,
moved_at TIMESTAMPTZ NOT NULL DEFAULT now(),
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_warehouse_inventory_movement_inventory_id
ON freight.warehouse_inventory_movement(inventory_id);
`);
// ── Activity log ──────────────────────────────────────────────────────
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS freight.warehouse_activity_log (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
inventory_id UUID NULL,
warehouse_id UUID NULL,
activity_type VARCHAR(40) NOT NULL,
description TEXT NULL,
performed_by VARCHAR(120) NULL,
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_warehouse_activity_log_inventory_id
ON freight.warehouse_activity_log(inventory_id);
`);
await queryRunner.query(`
CREATE INDEX IF NOT EXISTS idx_warehouse_activity_log_warehouse_id
ON freight.warehouse_activity_log(warehouse_id);
`);
await queryRunner.query(`
CREATE INDEX IF NOT EXISTS idx_warehouse_activity_log_activity_type
ON freight.warehouse_activity_log(activity_type);
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE IF EXISTS freight.warehouse_activity_log;`);
await queryRunner.query(`DROP TABLE IF EXISTS freight.warehouse_inventory_movement;`);
await queryRunner.query(`
ALTER TABLE freight.warehouse_inventory
DROP COLUMN IF EXISTS stored_at,
DROP COLUMN IF EXISTS reserved_at,
DROP COLUMN IF EXISTS loaded_at,
DROP COLUMN IF EXISTS dispatched_at;
`);
await queryRunner.query(`
ALTER TABLE freight.warehouse_inventory ALTER COLUMN status SET DEFAULT 'RECEIVED';
`);
for (const table of ['warehouses', 'warehouse_yards', 'warehouse_zones']) {
await queryRunner.query(`
ALTER TABLE freight.${table}
DROP COLUMN IF EXISTS max_weight,
DROP COLUMN IF EXISTS max_volume,
DROP COLUMN IF EXISTS current_volume;
`);
}
}
}

View File

@@ -1,44 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
/**
* Batch 3 — Warehouse → Loading → Train Departure visibility.
* Adds the warehouse_loadings record (inventory ↔ wagon). Does NOT touch any
* scheduling / wagon tables — the warehouse only reads from those.
*/
export class WarehouseBatch31790000000002 implements MigrationInterface {
name = 'WarehouseBatch31790000000002';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS freight.warehouse_loadings (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
warehouse_inventory_id UUID NOT NULL REFERENCES freight.warehouse_inventory(id) ON DELETE CASCADE,
booking_id UUID NULL,
wagon_id UUID NOT NULL,
loaded_at TIMESTAMPTZ NOT NULL DEFAULT now(),
loaded_by VARCHAR(120) NULL,
loaded_weight NUMERIC(14,3) NULL,
notes TEXT NULL,
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_warehouse_loadings_inventory_id
ON freight.warehouse_loadings(warehouse_inventory_id);
`);
await queryRunner.query(`
CREATE INDEX IF NOT EXISTS idx_warehouse_loadings_booking_id
ON freight.warehouse_loadings(booking_id);
`);
await queryRunner.query(`
CREATE INDEX IF NOT EXISTS idx_warehouse_loadings_wagon_id
ON freight.warehouse_loadings(wagon_id);
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE IF EXISTS freight.warehouse_loadings;`);
}
}

View File

@@ -1,67 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddActiveModeAndOnboardingToExternalProfiles1791000000000
implements MigrationInterface
{
name = 'AddActiveModeAndOnboardingToExternalProfiles1791000000000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.external_profiles
ADD COLUMN IF NOT EXISTS active_profile_type varchar(32);
`);
await queryRunner.query(`
ALTER TABLE freight.external_profiles
ADD COLUMN IF NOT EXISTS onboarding_step varchar(40);
`);
await queryRunner.query(`
ALTER TABLE freight.external_profiles
ADD COLUMN IF NOT EXISTS onboarding_completed boolean NOT NULL DEFAULT false;
`);
// Existing users already use the portal — never re-gate them behind the
// new onboarding wizard.
await queryRunner.query(`
UPDATE freight.external_profiles
SET onboarding_completed = true
WHERE onboarding_completed = false;
`);
// Backfill the active mode for existing users from their company's
// operational profiles. Prefer importer, then exporter, then whichever
// single profile the company has (forwarder/dj/transporter).
await queryRunner.query(`
UPDATE freight.external_profiles ep
SET active_profile_type = cp.type
FROM (
SELECT DISTINCT ON (company_id) company_id, type
FROM freight.company_profiles
ORDER BY company_id,
CASE type
WHEN 'importer' THEN 0
WHEN 'exporter' THEN 1
ELSE 2
END
) cp
WHERE ep.company_id = cp.company_id
AND ep.active_profile_type IS NULL;
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.external_profiles
DROP COLUMN IF EXISTS onboarding_completed;
`);
await queryRunner.query(`
ALTER TABLE freight.external_profiles
DROP COLUMN IF EXISTS onboarding_step;
`);
await queryRunner.query(`
ALTER TABLE freight.external_profiles
DROP COLUMN IF EXISTS active_profile_type;
`);
}
}

View File

@@ -1,126 +0,0 @@
import { MigrationInterface, QueryRunner, Table, TableColumn } from 'typeorm';
/**
* Batch 5 — warehouse allocation rules, storage/demurrage fee rules,
* and demurrage lifecycle timestamps on inventory. Idempotent.
*/
export class AddWarehouseAllocationAndFeeRules1791000000000 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
schema: 'freight',
name: 'warehouse_allocation_rules',
columns: [
{ name: 'id', type: 'uuid', isPrimary: true, default: 'uuid_generate_v4()' },
{ name: 'name', type: 'varchar', length: '160' },
{ name: 'priority', type: 'int', default: 100 },
{ name: 'freight_type', type: 'varchar', length: '16', isNullable: true },
{ name: 'trade_direction', type: 'varchar', length: '16', isNullable: true },
{ name: 'cargo_type_code', type: 'varchar', length: '50', isNullable: true },
{ name: 'container_status', type: 'varchar', length: '24', isNullable: true },
{ name: 'requires_inspection', type: 'boolean', isNullable: true },
{ name: 'target_facility_code', type: 'varchar', length: '40', isNullable: true },
{ name: 'target_yard_code', type: 'varchar', length: '40' },
{ name: 'target_warehouse_code', type: 'varchar', length: '40', isNullable: true },
{ name: 'target_zone_code', type: 'varchar', length: '40', isNullable: true },
{ name: 'storage_type', type: 'varchar', length: '80', isNullable: true },
{ name: 'is_active', type: 'boolean', default: true },
{ name: 'created_at', type: 'timestamptz', default: 'now()' },
{ name: 'updated_at', type: 'timestamptz', default: 'now()' },
{ name: 'deleted_at', type: 'timestamptz', isNullable: true },
],
indices: [
{ name: 'idx_war_priority', columnNames: ['priority'] },
{ name: 'idx_war_active', columnNames: ['is_active'] },
],
}),
true,
);
await queryRunner.createTable(
new Table({
schema: 'freight',
name: 'warehouse_fee_rules',
columns: [
{ name: 'id', type: 'uuid', isPrimary: true, default: 'uuid_generate_v4()' },
{ name: 'name', type: 'varchar', length: '160' },
{ name: 'rule_type', type: 'varchar', length: '20' },
{ name: 'priority', type: 'int', default: 100 },
{ name: 'freight_type', type: 'varchar', length: '16', isNullable: true },
{ name: 'trade_direction', type: 'varchar', length: '16', isNullable: true },
{ name: 'cargo_type_code', type: 'varchar', length: '50', isNullable: true },
{ name: 'container_type', type: 'varchar', length: '40', isNullable: true },
{ name: 'facility_id', type: 'uuid', isNullable: true },
{ name: 'warehouse_id', type: 'uuid', isNullable: true },
{ name: 'yard_id', type: 'uuid', isNullable: true },
{ name: 'zone_id', type: 'uuid', isNullable: true },
{ name: 'free_days', type: 'int', default: 0 },
{ name: 'rate_per_day', type: 'numeric', precision: 14, scale: 2, default: 0 },
{ name: 'tiers', type: 'jsonb', default: "'[]'" },
{ name: 'currency', type: 'varchar', length: '8', default: "'USD'" },
{ name: 'is_active', type: 'boolean', default: true },
{ name: 'created_at', type: 'timestamptz', default: 'now()' },
{ name: 'updated_at', type: 'timestamptz', default: 'now()' },
{ name: 'deleted_at', type: 'timestamptz', isNullable: true },
],
indices: [
{ name: 'idx_wfr_type', columnNames: ['rule_type'] },
{ name: 'idx_wfr_active', columnNames: ['is_active'] },
],
}),
true,
);
const inventoryTable = await queryRunner.getTable('freight.warehouse_inventory');
if (inventoryTable) {
const columnsToAdd = [
{ name: 'inspection_started_at', type: 'timestamptz', isNullable: true },
{ name: 'inspection_completed_at', type: 'timestamptz', isNullable: true },
{ name: 'ready_for_pickup_at', type: 'timestamptz', isNullable: true },
{ name: 'release_date', type: 'timestamptz', isNullable: true },
{ name: 'gate_cleared_at', type: 'timestamptz', isNullable: true },
];
const columnsToCreate = columnsToAdd.filter(
(col) => !inventoryTable.columns.some((c) => c.name === col.name),
);
if (columnsToCreate.length > 0) {
await queryRunner.addColumns(
'freight.warehouse_inventory',
columnsToCreate.map((col) => new TableColumn(col)),
);
}
}
}
public async down(queryRunner: QueryRunner): Promise<void> {
const inventoryTable = await queryRunner.getTable('freight.warehouse_inventory');
if (inventoryTable) {
const columnNames = [
'inspection_started_at',
'inspection_completed_at',
'ready_for_pickup_at',
'release_date',
'gate_cleared_at',
];
const columnsToRemove = columnNames.filter((name) =>
inventoryTable.columns.some((c) => c.name === name),
);
if (columnsToRemove.length > 0) {
await queryRunner.dropColumns('freight.warehouse_inventory', columnsToRemove);
}
}
const feeRulesTable = await queryRunner.getTable('freight.warehouse_fee_rules');
if (feeRulesTable) {
await queryRunner.dropTable('freight.warehouse_fee_rules', true);
}
const allocationRulesTable = await queryRunner.getTable('freight.warehouse_allocation_rules');
if (allocationRulesTable) {
await queryRunner.dropTable('freight.warehouse_allocation_rules', true);
}
}
}

View File

@@ -1,94 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddCompanyProfileIdToBookings1791000000001
implements MigrationInterface
{
name = 'AddCompanyProfileIdToBookings1791000000001';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.bookings
ADD COLUMN IF NOT EXISTS company_profile_id UUID;
`);
await queryRunner.query(`
CREATE INDEX IF NOT EXISTS idx_bookings_company_profile_id
ON freight.bookings(company_profile_id);
`);
await queryRunner.query(`
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'FK_bookings_company_profile_id'
) THEN
ALTER TABLE freight.bookings
ADD CONSTRAINT "FK_bookings_company_profile_id"
FOREIGN KEY (company_profile_id)
REFERENCES freight.company_profiles(id);
END IF;
END $$;
`);
// Backfill by natural mapping: IMPORT → importer profile, EXPORT → exporter
// profile, for each booking's own company.
await queryRunner.query(`
UPDATE freight.bookings b
SET company_profile_id = cp.id
FROM freight.company_profiles cp
WHERE cp.company_id = b.company_id
AND b.company_profile_id IS NULL
AND (
(b.trade_direction = 'IMPORT' AND cp.type = 'importer') OR
(b.trade_direction = 'EXPORT' AND cp.type = 'exporter')
);
`);
// Forwarder / single-profile companies: one profile per company, so the
// mapping is unambiguous regardless of trade direction.
await queryRunner.query(`
UPDATE freight.bookings b
SET company_profile_id = cp.id
FROM freight.company_profiles cp
JOIN freight.companies c ON c.id = cp.company_id
WHERE cp.company_id = b.company_id
AND c.type <> 'customer'
AND b.company_profile_id IS NULL;
`);
// Remaining customer-owned rows (e.g. DOMESTIC, or a direction with no
// matching profile): attribute to the company's importer profile, else its
// exporter profile, so nothing disappears from the customer's list.
await queryRunner.query(`
UPDATE freight.bookings b
SET company_profile_id = cp.id
FROM (
SELECT DISTINCT ON (company_id) company_id, id
FROM freight.company_profiles
ORDER BY company_id,
CASE type
WHEN 'importer' THEN 0
WHEN 'exporter' THEN 1
ELSE 2
END
) cp
WHERE cp.company_id = b.company_id
AND b.company_id IS NOT NULL
AND b.company_profile_id IS NULL;
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.bookings
DROP CONSTRAINT IF EXISTS "FK_bookings_company_profile_id";
`);
await queryRunner.query(`
DROP INDEX IF EXISTS freight.idx_bookings_company_profile_id;
`);
await queryRunner.query(`
ALTER TABLE freight.bookings
DROP COLUMN IF EXISTS company_profile_id;
`);
}
}

View File

@@ -1,88 +0,0 @@
import { MigrationInterface, QueryRunner, Table } from 'typeorm';
/** Batch 6 — warehouse fee invoices + invoice items. Idempotent (createTable ifNotExists). */
export class AddWarehouseFeeInvoices1791000000001 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
schema: 'freight',
name: 'warehouse_fee_invoices',
columns: [
{ name: 'id', type: 'uuid', isPrimary: true, default: 'uuid_generate_v4()' },
{ name: 'invoice_number', type: 'varchar', length: '40', isUnique: true },
{ name: 'booking_id', type: 'uuid', isNullable: true },
{ name: 'customer_id', type: 'uuid', isNullable: true },
{ name: 'inventory_id', type: 'uuid' },
{ name: 'facility_id', type: 'uuid', isNullable: true },
{ name: 'warehouse_id', type: 'uuid', isNullable: true },
{ name: 'yard_id', type: 'uuid', isNullable: true },
{ name: 'zone_id', type: 'uuid', isNullable: true },
{ name: 'invoice_type', type: 'varchar', length: '32', default: "'MIXED_WAREHOUSE_FEES'" },
{ name: 'status', type: 'varchar', length: '20', default: "'DRAFT'" },
{ name: 'subtotal_amount', type: 'numeric', precision: 14, scale: 2, default: 0 },
{ name: 'tax_amount', type: 'numeric', precision: 14, scale: 2, default: 0 },
{ name: 'total_amount', type: 'numeric', precision: 14, scale: 2, default: 0 },
{ name: 'paid_amount', type: 'numeric', precision: 14, scale: 2, default: 0 },
{ name: 'balance_amount', type: 'numeric', precision: 14, scale: 2, default: 0 },
{ name: 'currency', type: 'varchar', length: '8', default: "'USD'" },
{ name: 'period_start', type: 'timestamptz', isNullable: true },
{ name: 'period_end', type: 'timestamptz', isNullable: true },
{ name: 'issued_at', type: 'timestamptz', isNullable: true },
{ name: 'due_date', type: 'timestamptz', isNullable: true },
{ name: 'paid_at', type: 'timestamptz', isNullable: true },
{ name: 'cancelled_at', type: 'timestamptz', isNullable: true },
{ name: 'payments', type: 'jsonb', default: "'[]'" },
{ name: 'notes', type: 'text', isNullable: true },
{ name: 'created_at', type: 'timestamptz', default: 'now()' },
{ name: 'updated_at', type: 'timestamptz', default: 'now()' },
{ name: 'deleted_at', type: 'timestamptz', isNullable: true },
],
indices: [
{ name: 'idx_wfi_booking', columnNames: ['booking_id'] },
{ name: 'idx_wfi_inventory', columnNames: ['inventory_id'] },
{ name: 'idx_wfi_status', columnNames: ['status'] },
],
}),
true,
);
await queryRunner.createTable(
new Table({
schema: 'freight',
name: 'warehouse_fee_invoice_items',
columns: [
{ name: 'id', type: 'uuid', isPrimary: true, default: 'uuid_generate_v4()' },
{ name: 'invoice_id', type: 'uuid' },
{ name: 'fee_rule_id', type: 'uuid', isNullable: true },
{ name: 'fee_type', type: 'varchar', length: '32' },
{ name: 'description', type: 'varchar', length: '255' },
{ name: 'quantity', type: 'numeric', precision: 12, scale: 2, default: 1 },
{ name: 'unit_rate', type: 'numeric', precision: 14, scale: 2, default: 0 },
{ name: 'amount', type: 'numeric', precision: 14, scale: 2, default: 0 },
{ name: 'currency', type: 'varchar', length: '8', default: "'USD'" },
{ name: 'chargeable_days', type: 'int', isNullable: true },
{ name: 'free_days', type: 'int', isNullable: true },
{ name: 'created_at', type: 'timestamptz', default: 'now()' },
{ name: 'updated_at', type: 'timestamptz', default: 'now()' },
{ name: 'deleted_at', type: 'timestamptz', isNullable: true },
],
foreignKeys: [
{
columnNames: ['invoice_id'],
referencedSchema: 'freight',
referencedTableName: 'warehouse_fee_invoices',
referencedColumnNames: ['id'],
onDelete: 'CASCADE',
},
],
indices: [{ name: 'idx_wfii_invoice', columnNames: ['invoice_id'] }],
}),
true,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('freight.warehouse_fee_invoice_items', true);
await queryRunner.dropTable('freight.warehouse_fee_invoices', true);
}
}

View File

@@ -1,37 +0,0 @@
import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
/**
* Import pickup branch on warehouse_inventory:
* - release_order_reference: DO / release order number sent to the customer
* - delivered_at: when the goods were handed over (proof of delivery)
*
* Idempotent: the shared dev DB may already carry some of these columns
* (added by another checkout), so only add what is missing.
*/
export class AddImportPickupDeliveryColumns1791000000002 implements MigrationInterface {
private readonly table = 'freight.warehouse_inventory';
public async up(queryRunner: QueryRunner): Promise<void> {
if (!(await queryRunner.hasColumn(this.table, 'release_order_reference'))) {
await queryRunner.addColumn(
this.table,
new TableColumn({ name: 'release_order_reference', type: 'varchar', length: '100', isNullable: true }),
);
}
if (!(await queryRunner.hasColumn(this.table, 'delivered_at'))) {
await queryRunner.addColumn(
this.table,
new TableColumn({ name: 'delivered_at', type: 'timestamptz', isNullable: true }),
);
}
}
public async down(queryRunner: QueryRunner): Promise<void> {
if (await queryRunner.hasColumn(this.table, 'release_order_reference')) {
await queryRunner.dropColumn(this.table, 'release_order_reference');
}
if (await queryRunner.hasColumn(this.table, 'delivered_at')) {
await queryRunner.dropColumn(this.table, 'delivered_at');
}
}
}

View File

@@ -1,28 +0,0 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddNationalityToCompanies1791000000002
implements MigrationInterface
{
name = "AddNationalityToCompanies1791000000002";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.companies
ADD COLUMN IF NOT EXISTS nationality varchar(32);
`);
// Existing companies default to Ethiopian (country defaults to Ethiopia).
await queryRunner.query(`
UPDATE freight.companies
SET nationality = 'ethiopian'
WHERE nationality IS NULL;
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.companies
DROP COLUMN IF EXISTS nationality;
`);
}
}

View File

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

View File

@@ -1,109 +0,0 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddETradeFieldsToCompanies1791000000003
implements MigrationInterface
{
name = "AddETradeFieldsToCompanies1791000000003";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.companies
ADD COLUMN IF NOT EXISTS licence_number varchar(100);
`);
await queryRunner.query(`
ALTER TABLE freight.companies
ADD COLUMN IF NOT EXISTS status_description text;
`);
await queryRunner.query(`
ALTER TABLE freight.companies
ADD COLUMN IF NOT EXISTS date_registered varchar(50);
`);
await queryRunner.query(`
ALTER TABLE freight.companies
ADD COLUMN IF NOT EXISTS renewed_from varchar(50);
`);
await queryRunner.query(`
ALTER TABLE freight.companies
ADD COLUMN IF NOT EXISTS renewal_date varchar(50);
`);
await queryRunner.query(`
ALTER TABLE freight.companies
ADD COLUMN IF NOT EXISTS renewed_to varchar(50);
`);
await queryRunner.query(`
ALTER TABLE freight.companies
ADD COLUMN IF NOT EXISTS region varchar(100);
`);
await queryRunner.query(`
ALTER TABLE freight.companies
ADD COLUMN IF NOT EXISTS zone varchar(100);
`);
await queryRunner.query(`
ALTER TABLE freight.companies
ADD COLUMN IF NOT EXISTS woreda varchar(100);
`);
await queryRunner.query(`
ALTER TABLE freight.companies
ADD COLUMN IF NOT EXISTS kebele varchar(100);
`);
await queryRunner.query(`
ALTER TABLE freight.companies
ADD COLUMN IF NOT EXISTS house_no varchar(100);
`);
await queryRunner.query(`
ALTER TABLE freight.companies
ADD COLUMN IF NOT EXISTS etrade_phone varchar(20);
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.companies
DROP COLUMN IF EXISTS licence_number;
`);
await queryRunner.query(`
ALTER TABLE freight.companies
DROP COLUMN IF EXISTS status_description;
`);
await queryRunner.query(`
ALTER TABLE freight.companies
DROP COLUMN IF EXISTS date_registered;
`);
await queryRunner.query(`
ALTER TABLE freight.companies
DROP COLUMN IF EXISTS renewed_from;
`);
await queryRunner.query(`
ALTER TABLE freight.companies
DROP COLUMN IF EXISTS renewal_date;
`);
await queryRunner.query(`
ALTER TABLE freight.companies
DROP COLUMN IF EXISTS renewed_to;
`);
await queryRunner.query(`
ALTER TABLE freight.companies
DROP COLUMN IF EXISTS region;
`);
await queryRunner.query(`
ALTER TABLE freight.companies
DROP COLUMN IF EXISTS zone;
`);
await queryRunner.query(`
ALTER TABLE freight.companies
DROP COLUMN IF EXISTS woreda;
`);
await queryRunner.query(`
ALTER TABLE freight.companies
DROP COLUMN IF EXISTS kebele;
`);
await queryRunner.query(`
ALTER TABLE freight.companies
DROP COLUMN IF EXISTS house_no;
`);
await queryRunner.query(`
ALTER TABLE freight.companies
DROP COLUMN IF EXISTS etrade_phone;
`);
}
}

View File

@@ -1,27 +0,0 @@
import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
/**
* Batch 8 — train-arrival unload landing state on warehouse_inventory:
* - unloaded_at: when the goods were unloaded off the arrived train (before storage/inspection)
*
* The `status` column is a free varchar, so the new 'UNLOADED' value needs no schema change.
* Idempotent: the shared dev DB may already carry this column (added by another checkout).
*/
export class AddInventoryUnloadedAt1791000000003 implements MigrationInterface {
private readonly table = 'freight.warehouse_inventory';
public async up(queryRunner: QueryRunner): Promise<void> {
if (!(await queryRunner.hasColumn(this.table, 'unloaded_at'))) {
await queryRunner.addColumn(
this.table,
new TableColumn({ name: 'unloaded_at', type: 'timestamptz', isNullable: true }),
);
}
}
public async down(queryRunner: QueryRunner): Promise<void> {
if (await queryRunner.hasColumn(this.table, 'unloaded_at')) {
await queryRunner.dropColumn(this.table, 'unloaded_at');
}
}
}

View File

@@ -1,52 +0,0 @@
import { MigrationInterface, QueryRunner, TableColumn, TableForeignKey } from 'typeorm';
/**
* Fix: migration 1750000000001 (AddFacilityIdToWarehouses) silently skipped because
* the freight.warehouses table didn't exist yet at that timestamp. The column was
* never added. Add it now with idempotent guards.
*/
export class AddFacilityIdToWarehousesFix1791000000004 implements MigrationInterface {
private readonly table = 'freight.warehouses';
public async up(queryRunner: QueryRunner): Promise<void> {
if (!(await queryRunner.hasColumn(this.table, 'facility_id'))) {
await queryRunner.addColumn(
this.table,
new TableColumn({
name: 'facility_id',
type: 'uuid',
isNullable: true,
}),
);
}
const table = await queryRunner.getTable(this.table);
const hasFk = table?.foreignKeys.some((fk) => fk.columnNames.includes('facility_id'));
if (!hasFk) {
await queryRunner.createForeignKey(
this.table,
new TableForeignKey({
columnNames: ['facility_id'],
referencedColumnNames: ['id'],
referencedTableName: 'facilities',
referencedSchema: 'freight',
onDelete: 'SET NULL',
}),
);
}
}
public async down(queryRunner: QueryRunner): Promise<void> {
const table = await queryRunner.getTable(this.table);
if (!table) return;
const foreignKey = table.foreignKeys.find((fk) => fk.columnNames.includes('facility_id'));
if (foreignKey) {
await queryRunner.dropForeignKey(this.table, foreignKey);
}
if (await queryRunner.hasColumn(this.table, 'facility_id')) {
await queryRunner.dropColumn(this.table, 'facility_id');
}
}
}

View File

@@ -1,24 +0,0 @@
import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
/**
* Fix for fresh deployments: AddWarehouseInspection1750000000003 runs before
* the warehouse_inventory table exists, so it cannot add inspection_status.
*/
export class AddWarehouseInventoryInspectionStatusFix1791000000005 implements MigrationInterface {
private readonly table = 'freight.warehouse_inventory';
public async up(queryRunner: QueryRunner): Promise<void> {
if ((await queryRunner.hasTable(this.table)) && !(await queryRunner.hasColumn(this.table, 'inspection_status'))) {
await queryRunner.addColumn(
this.table,
new TableColumn({ name: 'inspection_status', type: 'varchar', length: '20', isNullable: true }),
);
}
}
public async down(queryRunner: QueryRunner): Promise<void> {
if ((await queryRunner.hasTable(this.table)) && (await queryRunner.hasColumn(this.table, 'inspection_status'))) {
await queryRunner.dropColumn(this.table, 'inspection_status');
}
}
}

View File

@@ -1,70 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
/**
* Creates the generic dropdown settings tables (freight.dropdown_settings +
* freight.dropdown_options) backing the DropdownSetting / DropdownOption
* entities. These tables previously only existed via `synchronize` on some
* databases; this migration makes them part of the migration history so the
* SeedGeneralContractPeriod migration (which inserts into them) can run on a
* fresh database. Idempotent so it is safe on DBs where the tables already exist.
*/
export class CreateDropdownSettings1791999999999
implements MigrationInterface
{
name = 'CreateDropdownSettings1791999999999';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS "freight"."dropdown_settings" (
"id" uuid NOT NULL DEFAULT gen_random_uuid(),
"code" varchar(128) NOT NULL,
"label" varchar(256) NOT NULL,
"description" text,
"multiple" boolean NOT NULL DEFAULT false,
"meta" jsonb,
"created_at" timestamptz NOT NULL DEFAULT now(),
"updated_at" timestamptz NOT NULL DEFAULT now(),
"deleted_at" timestamptz,
CONSTRAINT "PK_dropdown_settings" PRIMARY KEY ("id")
);
`);
await queryRunner.query(`
CREATE UNIQUE INDEX IF NOT EXISTS "UQ_dropdown_settings_code"
ON "freight"."dropdown_settings" ("code");
`);
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS "freight"."dropdown_options" (
"id" uuid NOT NULL DEFAULT gen_random_uuid(),
"setting_id" uuid NOT NULL,
"value" varchar(256) NOT NULL,
"label" varchar(256) NOT NULL,
"note" text,
"is_disabled" boolean NOT NULL DEFAULT false,
"display_order" integer NOT NULL DEFAULT 0,
"meta" jsonb,
"created_at" timestamptz NOT NULL DEFAULT now(),
"updated_at" timestamptz NOT NULL DEFAULT now(),
"deleted_at" timestamptz,
CONSTRAINT "PK_dropdown_options" PRIMARY KEY ("id"),
CONSTRAINT "FK_dropdown_options_setting" FOREIGN KEY ("setting_id")
REFERENCES "freight"."dropdown_settings" ("id") ON DELETE CASCADE
);
`);
await queryRunner.query(`
CREATE UNIQUE INDEX IF NOT EXISTS "UQ_dropdown_options_setting_value"
ON "freight"."dropdown_options" ("setting_id", "value");
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`DROP TABLE IF EXISTS "freight"."dropdown_options";`,
);
await queryRunner.query(
`DROP TABLE IF EXISTS "freight"."dropdown_settings";`,
);
}
}

View File

@@ -1,19 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddUnitOfMeasureToCargoTypes1792000000000
implements MigrationInterface
{
name = 'AddUnitOfMeasureToCargoTypes1792000000000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE freight.cargo_types ADD COLUMN IF NOT EXISTS unit_of_measure VARCHAR(16);`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE freight.cargo_types DROP COLUMN IF EXISTS unit_of_measure;`,
);
}
}

View File

@@ -1,39 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddBookingTypeAndContractFields1792000000001
implements MigrationInterface
{
name = 'AddBookingTypeAndContractFields1792000000001';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS booking_type VARCHAR(20) NOT NULL DEFAULT 'ONE_TIME';`,
);
await queryRunner.query(
`ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS expires_at TIMESTAMPTZ;`,
);
// General contracts have no shipment date at creation — relax the NOT NULL.
await queryRunner.query(
`ALTER TABLE freight.bookings ALTER COLUMN scheduled_date DROP NOT NULL;`,
);
await queryRunner.query(
`CREATE INDEX IF NOT EXISTS idx_bookings_booking_type ON freight.bookings (booking_type);`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`DROP INDEX IF EXISTS freight.idx_bookings_booking_type;`,
);
// Reinstate NOT NULL only if no null rows exist (general contracts would block it).
await queryRunner.query(
`ALTER TABLE freight.bookings ALTER COLUMN scheduled_date SET NOT NULL;`,
);
await queryRunner.query(
`ALTER TABLE freight.bookings DROP COLUMN IF EXISTS expires_at;`,
);
await queryRunner.query(
`ALTER TABLE freight.bookings DROP COLUMN IF EXISTS booking_type;`,
);
}
}

View File

@@ -1,74 +0,0 @@
import { MigrationInterface, QueryRunner, Table, TableIndex } from 'typeorm';
export class CreateBookingOrders1792000000002 implements MigrationInterface {
name = 'CreateBookingOrders1792000000002';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
schema: 'freight',
name: 'booking_orders',
columns: [
{ name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'gen_random_uuid()' },
{ name: 'reference', type: 'varchar', length: '64', isUnique: true },
{ name: 'contract_booking_id', type: 'uuid' },
{ name: 'booking_id', type: 'uuid', isNullable: true },
{ name: 'company_id', type: 'uuid', isNullable: true },
{ name: 'scheduled_date', type: 'timestamptz' },
{ name: 'status', type: 'varchar', length: '40', default: "'PAID'" },
{ name: 'scheduling_status', type: 'varchar', length: '30', default: "'NOT_SCHEDULED'" },
{ name: 'train_schedule_id', type: 'uuid', isNullable: true },
{ name: 'created_at', type: 'timestamptz', default: 'now()' },
{ name: 'updated_at', type: 'timestamptz', default: 'now()' },
{ name: 'deleted_at', type: 'timestamptz', isNullable: true },
],
}),
true,
);
await queryRunner.createIndex(
'freight.booking_orders',
new TableIndex({ name: 'idx_booking_orders_contract', columnNames: ['contract_booking_id'] }),
);
await queryRunner.createIndex(
'freight.booking_orders',
new TableIndex({ name: 'idx_booking_orders_company', columnNames: ['company_id'] }),
);
await queryRunner.createTable(
new Table({
schema: 'freight',
name: 'booking_order_lines',
columns: [
{ name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'gen_random_uuid()' },
{ name: 'order_id', type: 'uuid' },
{ name: 'container_type_id', type: 'uuid', isNullable: true },
{ name: 'quantity', type: 'numeric', precision: 12, scale: 3 },
{ name: 'created_at', type: 'timestamptz', default: 'now()' },
{ name: 'updated_at', type: 'timestamptz', default: 'now()' },
{ name: 'deleted_at', type: 'timestamptz', isNullable: true },
],
foreignKeys: [
{
columnNames: ['order_id'],
referencedSchema: 'freight',
referencedTableName: 'booking_orders',
referencedColumnNames: ['id'],
onDelete: 'CASCADE',
},
],
}),
true,
);
await queryRunner.createIndex(
'freight.booking_order_lines',
new TableIndex({ name: 'idx_booking_order_lines_order', columnNames: ['order_id'] }),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('freight.booking_order_lines', true);
await queryRunner.dropTable('freight.booking_orders', true);
}
}

View File

@@ -1,46 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
/**
* Seeds the global "general contract period" setting (months). Stored as a
* dropdown_settings row with a single option whose `value` holds the month count
* so backoffice can manage it through the existing settings UI later.
*/
export class SeedGeneralContractPeriod1792000000003
implements MigrationInterface
{
name = 'SeedGeneralContractPeriod1792000000003';
private readonly code = 'general_contract_period';
public async up(queryRunner: QueryRunner): Promise<void> {
const existing = await queryRunner.query(
`SELECT id FROM freight.dropdown_settings WHERE code = $1 LIMIT 1;`,
[this.code],
);
if (existing.length > 0) return;
const inserted = await queryRunner.query(
`INSERT INTO freight.dropdown_settings (code, label, description, multiple)
VALUES ($1, $2, $3, false)
RETURNING id;`,
[
this.code,
'General Contract Period (months)',
'How many months a general contract stays open for ordering after activation.',
],
);
const settingId = inserted[0].id;
await queryRunner.query(
`INSERT INTO freight.dropdown_options (setting_id, value, label, display_order)
VALUES ($1, $2, $3, 0);`,
[settingId, '3', '3 months'],
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`DELETE FROM freight.dropdown_settings WHERE code = $1;`,
[this.code],
);
}
}

View File

@@ -1,55 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
/**
* Seeds the admin-configurable "contract validity periods" setting (days). Stored
* as a dropdown_settings row whose options each hold a day count in `value`, so
* backoffice manages them through the existing Dropdown Settings UI and the
* contract staff-accept dialog only offers the configured durations.
*/
export class SeedContractValidityPeriods1792000000004
implements MigrationInterface
{
name = 'SeedContractValidityPeriods1792000000004';
private readonly code = 'contract_validity_periods';
private readonly options: Array<{ value: string; label: string }> = [
{ value: '180', label: '6 months' },
{ value: '365', label: '1 year' },
{ value: '730', label: '2 years' },
];
public async up(queryRunner: QueryRunner): Promise<void> {
const existing = await queryRunner.query(
`SELECT id FROM freight.dropdown_settings WHERE code = $1 LIMIT 1;`,
[this.code],
);
if (existing.length > 0) return;
const inserted = await queryRunner.query(
`INSERT INTO freight.dropdown_settings (code, label, description, multiple)
VALUES ($1, $2, $3, false)
RETURNING id;`,
[
this.code,
'Contract Validity Periods (days)',
'Validity durations (in days) a staff can choose when accepting a submitted contract.',
],
);
const settingId = inserted[0].id;
for (let i = 0; i < this.options.length; i++) {
const opt = this.options[i];
await queryRunner.query(
`INSERT INTO freight.dropdown_options (setting_id, value, label, display_order)
VALUES ($1, $2, $3, $4);`,
[settingId, opt.value, opt.label, i],
);
}
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`DELETE FROM freight.dropdown_settings WHERE code = $1;`,
[this.code],
);
}
}

View File

@@ -1,50 +0,0 @@
import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
/**
* Add driver assignment fields to vehicles table
*/
export class AddVehicleDriverAssignment1800000000001 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
const vehiclesTable = await queryRunner.getTable('freight.vehicles');
if (vehiclesTable) {
const hasAssignedDriverId = vehiclesTable.columns.some((col) => col.name === 'assigned_driver_id');
if (!hasAssignedDriverId) {
await queryRunner.addColumn(
'freight.vehicles',
new TableColumn({
name: 'assigned_driver_id',
type: 'uuid',
isNullable: true,
}),
);
}
const hasAssignedDriverName = vehiclesTable.columns.some((col) => col.name === 'assigned_driver_name');
if (!hasAssignedDriverName) {
await queryRunner.addColumn(
'freight.vehicles',
new TableColumn({
name: 'assigned_driver_name',
type: 'varchar',
isNullable: true,
}),
);
}
}
}
public async down(queryRunner: QueryRunner): Promise<void> {
const vehiclesTable = await queryRunner.getTable('freight.vehicles');
if (vehiclesTable) {
const hasAssignedDriverId = vehiclesTable.columns.some((col) => col.name === 'assigned_driver_id');
if (hasAssignedDriverId) {
await queryRunner.dropColumn('freight.vehicles', 'assigned_driver_id');
}
const hasAssignedDriverName = vehiclesTable.columns.some((col) => col.name === 'assigned_driver_name');
if (hasAssignedDriverName) {
await queryRunner.dropColumn('freight.vehicles', 'assigned_driver_name');
}
}
}
}

View File

@@ -1,106 +0,0 @@
import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm';
/**
* Create the freight.first_mile table — one row per booking's first-mile
* (door → terminal) leg, with payment split and an optional assigned vehicle.
*/
export class CreateFirstMile1810000000000 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
const exists = await queryRunner.hasTable('freight.first_mile');
if (exists) return;
await queryRunner.createTable(
new Table({
name: 'freight.first_mile',
columns: [
{
name: 'id',
type: 'uuid',
isPrimary: true,
default: 'gen_random_uuid()',
},
{ name: 'booking_id', type: 'uuid', isNullable: false },
{
name: 'status',
type: 'varchar',
length: '30',
default: `'PAYMENT_PENDING'`,
isNullable: false,
},
{
name: 'advanced_payment',
type: 'numeric',
precision: 14,
scale: 2,
default: 0,
isNullable: false,
},
{
name: 'remaining_payment',
type: 'numeric',
precision: 14,
scale: 2,
default: 0,
isNullable: false,
},
{
name: 'estimated_km',
type: 'numeric',
precision: 10,
scale: 2,
isNullable: true,
},
{
name: 'exact_km',
type: 'numeric',
precision: 10,
scale: 2,
isNullable: true,
},
{ name: 'vehicle_id', type: 'uuid', isNullable: true },
{ name: 'created_at', type: 'timestamptz', default: 'now()' },
{ name: 'updated_at', type: 'timestamptz', default: 'now()' },
{ name: 'deleted_at', type: 'timestamptz', isNullable: true },
],
}),
true,
);
await queryRunner.createForeignKey(
'freight.first_mile',
new TableForeignKey({
columnNames: ['booking_id'],
referencedTableName: 'freight.bookings',
referencedColumnNames: ['id'],
onDelete: 'CASCADE',
}),
);
await queryRunner.createForeignKey(
'freight.first_mile',
new TableForeignKey({
columnNames: ['vehicle_id'],
referencedTableName: 'freight.vehicles',
referencedColumnNames: ['id'],
onDelete: 'SET NULL',
}),
);
await queryRunner.query(
`CREATE INDEX "IDX_first_mile_booking_id" ON "freight"."first_mile" ("booking_id")`,
);
await queryRunner.query(
`CREATE INDEX "IDX_first_mile_status" ON "freight"."first_mile" ("status")`,
);
await queryRunner.query(
`CREATE INDEX "IDX_first_mile_vehicle_id" ON "freight"."first_mile" ("vehicle_id")`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
const exists = await queryRunner.hasTable('freight.first_mile');
if (exists) {
await queryRunner.dropTable('freight.first_mile');
}
}
}

View File

@@ -1,106 +0,0 @@
import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm';
/**
* Create the freight.last_mile table — one row per booking's last-mile
* (terminal → door) leg, with payment split and an optional assigned vehicle.
*/
export class CreateLastMile1810000000001 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
const exists = await queryRunner.hasTable('freight.last_mile');
if (exists) return;
await queryRunner.createTable(
new Table({
name: 'freight.last_mile',
columns: [
{
name: 'id',
type: 'uuid',
isPrimary: true,
default: 'gen_random_uuid()',
},
{ name: 'booking_id', type: 'uuid', isNullable: false },
{
name: 'status',
type: 'varchar',
length: '30',
default: `'PAYMENT_PENDING'`,
isNullable: false,
},
{
name: 'advanced_payment',
type: 'numeric',
precision: 14,
scale: 2,
default: 0,
isNullable: false,
},
{
name: 'remaining_payment',
type: 'numeric',
precision: 14,
scale: 2,
default: 0,
isNullable: false,
},
{
name: 'estimated_km',
type: 'numeric',
precision: 10,
scale: 2,
isNullable: true,
},
{
name: 'exact_km',
type: 'numeric',
precision: 10,
scale: 2,
isNullable: true,
},
{ name: 'vehicle_id', type: 'uuid', isNullable: true },
{ name: 'created_at', type: 'timestamptz', default: 'now()' },
{ name: 'updated_at', type: 'timestamptz', default: 'now()' },
{ name: 'deleted_at', type: 'timestamptz', isNullable: true },
],
}),
true,
);
await queryRunner.createForeignKey(
'freight.last_mile',
new TableForeignKey({
columnNames: ['booking_id'],
referencedTableName: 'freight.bookings',
referencedColumnNames: ['id'],
onDelete: 'CASCADE',
}),
);
await queryRunner.createForeignKey(
'freight.last_mile',
new TableForeignKey({
columnNames: ['vehicle_id'],
referencedTableName: 'freight.vehicles',
referencedColumnNames: ['id'],
onDelete: 'SET NULL',
}),
);
await queryRunner.query(
`CREATE INDEX "IDX_last_mile_booking_id" ON "freight"."last_mile" ("booking_id")`,
);
await queryRunner.query(
`CREATE INDEX "IDX_last_mile_status" ON "freight"."last_mile" ("status")`,
);
await queryRunner.query(
`CREATE INDEX "IDX_last_mile_vehicle_id" ON "freight"."last_mile" ("vehicle_id")`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
const exists = await queryRunner.hasTable('freight.last_mile');
if (exists) {
await queryRunner.dropTable('freight.last_mile');
}
}
}

View File

@@ -1,35 +0,0 @@
import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
export class AddVehicleCodeAndPlates1810000000002 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
const hasCode = await queryRunner.hasColumn('freight.vehicles', 'code');
if (!hasCode) {
await queryRunner.addColumn(
'freight.vehicles',
new TableColumn({ name: 'code', type: 'varchar', isNullable: true }),
);
}
const hasPower = await queryRunner.hasColumn('freight.vehicles', 'power_plate_no');
if (!hasPower) {
await queryRunner.addColumn(
'freight.vehicles',
new TableColumn({ name: 'power_plate_no', type: 'varchar', isNullable: true }),
);
}
const hasTrailer = await queryRunner.hasColumn('freight.vehicles', 'trailer_plate_no');
if (!hasTrailer) {
await queryRunner.addColumn(
'freight.vehicles',
new TableColumn({ name: 'trailer_plate_no', type: 'varchar', isNullable: true }),
);
}
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropColumn('freight.vehicles', 'trailer_plate_no');
await queryRunner.dropColumn('freight.vehicles', 'power_plate_no');
await queryRunner.dropColumn('freight.vehicles', 'code');
}
}

View File

@@ -1,78 +0,0 @@
import { MigrationInterface, QueryRunner, Table, TableForeignKey } from 'typeorm';
/**
* Create the freight.last_mile_container_allocations table — container allocation
* records linking last-mile deliveries with containers and vehicles.
*/
export class CreateLastMileContainerAllocations1810000000002 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
const exists = await queryRunner.hasTable('freight.last_mile_container_allocations');
if (exists) return;
await queryRunner.createTable(
new Table({
name: 'freight.last_mile_container_allocations',
columns: [
{
name: 'id',
type: 'uuid',
isPrimary: true,
default: 'gen_random_uuid()',
},
{ name: 'last_mile_id', type: 'uuid', isNullable: false },
{ name: 'container_id', type: 'uuid', isNullable: false },
{ name: 'vehicle_id', type: 'uuid', isNullable: true },
{
name: 'container_type',
type: 'text',
isNullable: false,
},
{
name: 'quantity',
type: 'integer',
default: 1,
isNullable: false,
},
{ name: 'created_at', type: 'timestamptz', default: 'now()' },
{ name: 'updated_at', type: 'timestamptz', default: 'now()' },
{ name: 'deleted_at', type: 'timestamptz', isNullable: true },
],
}),
true,
);
await queryRunner.createForeignKey(
'freight.last_mile_container_allocations',
new TableForeignKey({
columnNames: ['last_mile_id'],
referencedTableName: 'freight.last_mile',
referencedColumnNames: ['id'],
onDelete: 'CASCADE',
}),
);
await queryRunner.createForeignKey(
'freight.last_mile_container_allocations',
new TableForeignKey({
columnNames: ['vehicle_id'],
referencedTableName: 'freight.vehicles',
referencedColumnNames: ['id'],
onDelete: 'SET NULL',
}),
);
await queryRunner.query(
`CREATE INDEX "IDX_last_mile_container_allocations_last_mile_id" ON "freight"."last_mile_container_allocations" ("last_mile_id")`,
);
await queryRunner.query(
`CREATE INDEX "IDX_last_mile_container_allocations_vehicle_id" ON "freight"."last_mile_container_allocations" ("vehicle_id")`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
const exists = await queryRunner.hasTable('freight.last_mile_container_allocations');
if (exists) {
await queryRunner.dropTable('freight.last_mile_container_allocations');
}
}
}

View File

@@ -1,31 +0,0 @@
import { MigrationInterface, QueryRunner } from "typeorm";
/**
* Company-profile references are now minted only when a profile is approved
* (status → Active); pending profiles carry NULL. Drop the NOT NULL constraint
* on freight.company_profiles.reference. The existing unique index is kept —
* Postgres treats NULLs as distinct, so multiple pending (NULL) profiles don't
* collide.
*/
export class MakeCompanyProfileReferenceNullable1810000000002
implements MigrationInterface
{
name = "MakeCompanyProfileReferenceNullable1810000000002";
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "freight"."company_profiles" ALTER COLUMN "reference" DROP NOT NULL`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
// Reinstating NOT NULL requires every row to have a reference; any pending
// (NULL) profiles get a placeholder so the constraint can be re-applied.
await queryRunner.query(
`UPDATE "freight"."company_profiles" SET "reference" = 'PENDING-' || left(replace("id"::text, '-', ''), 12) WHERE "reference" IS NULL`,
);
await queryRunner.query(
`ALTER TABLE "freight"."company_profiles" ALTER COLUMN "reference" SET NOT NULL`,
);
}
}

View File

@@ -1,42 +0,0 @@
import { MigrationInterface, QueryRunner, Table } from "typeorm";
/**
* Create the public.otp_verifications table backing the OTP module
* (OtpVerification entity). One row per phone, holding the latest server-issued
* code and whether that phone has been verified.
*/
export class CreateOtpVerifications1810000000003
implements MigrationInterface
{
name = "CreateOtpVerifications1810000000003";
public async up(queryRunner: QueryRunner): Promise<void> {
const exists = await queryRunner.hasTable("otp_verifications");
if (exists) return;
await queryRunner.createTable(
new Table({
name: "otp_verifications",
columns: [
{
name: "id",
type: "uuid",
isPrimary: true,
default: "gen_random_uuid()",
},
{ name: "phone", type: "varchar", isUnique: true },
{ name: "otp", type: "varchar" },
{ name: "verified", type: "boolean", default: false },
{ name: "created_at", type: "timestamptz", default: "now()" },
{ name: "updated_at", type: "timestamptz", default: "now()" },
{ name: "deleted_at", type: "timestamptz", isNullable: true },
],
}),
true,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable("otp_verifications", true);
}
}

View File

@@ -1,51 +0,0 @@
import { MigrationInterface, QueryRunner, TableColumn } from 'typeorm';
export class AddPostPaymentCompletedColumn1810000000004 implements MigrationInterface {
name = 'AddPostPaymentCompletedColumn1810000000004';
public async up(queryRunner: QueryRunner): Promise<void> {
const firstMileTable = await queryRunner.hasTable('freight.first_mile_deliveries');
if (firstMileTable) {
const hasColumn = await queryRunner.hasColumn('freight.first_mile_deliveries', 'is_post_payment_completed');
if (!hasColumn) {
await queryRunner.addColumn(
'freight.first_mile_deliveries',
new TableColumn({
name: 'is_post_payment_completed',
type: 'boolean',
default: false,
isNullable: false,
})
);
}
}
const lastMileTable = await queryRunner.hasTable('freight.last_mile_deliveries');
if (lastMileTable) {
const hasColumn = await queryRunner.hasColumn('freight.last_mile_deliveries', 'is_post_payment_completed');
if (!hasColumn) {
await queryRunner.addColumn(
'freight.last_mile_deliveries',
new TableColumn({
name: 'is_post_payment_completed',
type: 'boolean',
default: false,
isNullable: false,
})
);
}
}
}
public async down(queryRunner: QueryRunner): Promise<void> {
const lastMileTable = await queryRunner.hasTable('freight.last_mile_deliveries');
if (lastMileTable) {
await queryRunner.dropColumn('freight.last_mile_deliveries', 'is_post_payment_completed');
}
const firstMileTable = await queryRunner.hasTable('freight.first_mile_deliveries');
if (firstMileTable) {
await queryRunner.dropColumn('freight.first_mile_deliveries', 'is_post_payment_completed');
}
}
}

View File

@@ -1,23 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
/**
* Consolidation is now system-managed: the backend consolidates partial-wagon
* container bookings automatically, derived from the container quantities. The
* `allow_consolidation` opt-in flag is therefore redundant and is dropped.
* `consolidation_partner_id` (the actual pairing link) is unaffected.
*/
export class DropAllowConsolidation1820000000000 implements MigrationInterface {
name = 'DropAllowConsolidation1820000000000';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE freight.bookings DROP COLUMN IF EXISTS allow_consolidation;`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS allow_consolidation BOOLEAN NOT NULL DEFAULT false;`,
);
}
}

View File

@@ -1,61 +0,0 @@
import { MigrationInterface, QueryRunner, Table, TableIndex } from 'typeorm';
/**
* Multi-route general contracts: a contract may reserve quantity across several
* routes. Each (contract, route, container type) is a row here; drawdown orders
* reference the route line they drew from via booking_orders.route_line_id.
*/
export class CreateContractRouteLines1820000000001
implements MigrationInterface
{
name = 'CreateContractRouteLines1820000000001';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
schema: 'freight',
name: 'contract_route_lines',
columns: [
{ name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'gen_random_uuid()' },
{ name: 'contract_booking_id', type: 'uuid' },
{ name: 'origin_yard_id', type: 'uuid' },
{ name: 'destination_yard_id', type: 'uuid' },
{ name: 'container_type_id', type: 'uuid', isNullable: true },
{ name: 'quantity', type: 'numeric', precision: 12, scale: 3 },
{ name: 'created_at', type: 'timestamptz', default: 'now()' },
{ name: 'updated_at', type: 'timestamptz', default: 'now()' },
{ name: 'deleted_at', type: 'timestamptz', isNullable: true },
],
foreignKeys: [
{
columnNames: ['contract_booking_id'],
referencedSchema: 'freight',
referencedTableName: 'bookings',
referencedColumnNames: ['id'],
onDelete: 'CASCADE',
},
],
}),
true,
);
await queryRunner.createIndex(
'freight.contract_route_lines',
new TableIndex({
name: 'idx_contract_route_lines_contract',
columnNames: ['contract_booking_id'],
}),
);
await queryRunner.query(
`ALTER TABLE freight.booking_orders ADD COLUMN IF NOT EXISTS route_line_id uuid;`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE freight.booking_orders DROP COLUMN IF EXISTS route_line_id;`,
);
await queryRunner.dropTable('freight.contract_route_lines', true);
}
}

View File

@@ -1,66 +0,0 @@
import { MigrationInterface, QueryRunner, Table, TableIndex } from 'typeorm';
/**
* Per-document GL review for the post-counter-sign clearance gate. One row per
* required clearance document; GL marks each APPROVED or QUERIED before the
* booking can proceed to operations.
*/
export class CreateBookingDocumentReview1820000000002
implements MigrationInterface
{
name = 'CreateBookingDocumentReview1820000000002';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
schema: 'freight',
name: 'booking_document_review',
columns: [
{ name: 'id', type: 'uuid', isPrimary: true, generationStrategy: 'uuid', default: 'gen_random_uuid()' },
{ name: 'booking_id', type: 'uuid' },
{ name: 'setting_code', type: 'varchar', length: '128' },
{ name: 'file_key', type: 'varchar', length: '128' },
{ name: 'file_record_id', type: 'uuid', isNullable: true },
{ name: 'status', type: 'varchar', length: '20', default: "'PENDING'" },
{ name: 'note', type: 'text', isNullable: true },
{ name: 'reviewed_by_staff_id', type: 'uuid', isNullable: true },
{ name: 'reviewed_at', type: 'timestamptz', isNullable: true },
{ name: 'created_at', type: 'timestamptz', default: 'now()' },
{ name: 'updated_at', type: 'timestamptz', default: 'now()' },
{ name: 'deleted_at', type: 'timestamptz', isNullable: true },
],
foreignKeys: [
{
columnNames: ['booking_id'],
referencedSchema: 'freight',
referencedTableName: 'bookings',
referencedColumnNames: ['id'],
onDelete: 'CASCADE',
},
],
}),
true,
);
await queryRunner.createIndex(
'freight.booking_document_review',
new TableIndex({ name: 'idx_booking_document_review_booking', columnNames: ['booking_id'] }),
);
await queryRunner.createIndex(
'freight.booking_document_review',
new TableIndex({ name: 'idx_booking_document_review_status', columnNames: ['status'] }),
);
await queryRunner.createIndex(
'freight.booking_document_review',
new TableIndex({
name: 'uq_booking_document_review_doc',
columnNames: ['booking_id', 'setting_code', 'file_key'],
isUnique: true,
}),
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.dropTable('freight.booking_document_review', true);
}
}

View File

@@ -1,39 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
/**
* Staff price adjustment: an optional override of a booking's computed total,
* with who/when/why. When set, the customer sees the adjusted total + a badge.
*/
export class AddPriceAdjustment1820000000003 implements MigrationInterface {
name = 'AddPriceAdjustment1820000000003';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS adjusted_total_amount numeric(14,2);`,
);
await queryRunner.query(
`ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS adjusted_by_staff_id uuid;`,
);
await queryRunner.query(
`ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS adjusted_at timestamptz;`,
);
await queryRunner.query(
`ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS adjustment_reason text;`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE freight.bookings DROP COLUMN IF EXISTS adjustment_reason;`,
);
await queryRunner.query(
`ALTER TABLE freight.bookings DROP COLUMN IF EXISTS adjusted_at;`,
);
await queryRunner.query(
`ALTER TABLE freight.bookings DROP COLUMN IF EXISTS adjusted_by_staff_id;`,
);
await queryRunner.query(
`ALTER TABLE freight.bookings DROP COLUMN IF EXISTS adjusted_total_amount;`,
);
}
}

View File

@@ -1,187 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
/**
* Fold the `surcharge_types` table into self-describing rates.
*
* Previously a surcharge was a separate row {trigger_condition, rate_id}. Now
* each rate carries its own `applies_to` (friendly category) and `trigger`
* (ALWAYS = base freight, otherwise a surcharge condition), plus an optional
* `cargo_type_id` for bulk leaf commodities. The rule engine reads triggers
* directly off LIVE rates, so the join table is no longer needed.
*
* This migration:
* 1. adds applies_to / trigger / cargo_type_id to rates and backfills them
* from the existing rate_type matrix,
* 2. repoints booking_cargo_modifier from surcharge_type_id → rate_id
* (backfilled via surcharge_types.rate_id),
* 3. drops surcharge_types and its FK.
*/
export class FoldSurchargeTypesIntoRates1820000000004 implements MigrationInterface {
name = 'FoldSurchargeTypesIntoRates1820000000004';
public async up(queryRunner: QueryRunner): Promise<void> {
// ── 1. New rate columns ────────────────────────────────────────────────
await queryRunner.query(`
ALTER TABLE freight.rates
ADD COLUMN IF NOT EXISTS applies_to varchar(20) NOT NULL DEFAULT 'OTHER',
ADD COLUMN IF NOT EXISTS "trigger" varchar(20) NOT NULL DEFAULT 'ALWAYS',
ADD COLUMN IF NOT EXISTS cargo_type_id uuid NULL;
`);
await queryRunner.query(`
ALTER TABLE freight.rates
ADD CONSTRAINT "FK_rates_cargo_type_id"
FOREIGN KEY (cargo_type_id) REFERENCES freight.cargo_types(id)
ON DELETE SET NULL;
`);
await queryRunner.query(
`CREATE INDEX IF NOT EXISTS "IDX_rates_trigger" ON freight.rates ("trigger");`,
);
// ── 1a. Backfill applies_to from the legacy rate_type matrix ────────────
await queryRunner.query(`
UPDATE freight.rates SET applies_to = CASE
WHEN rate_type IN ('CONTAINER_IMPORT','CONTAINER_EXPORT','CONTAINER_WITH_RETURN') THEN 'CONTAINER'
WHEN rate_type IN ('BULK_IMPORT','BULK_EXPORT') THEN 'BULK'
WHEN rate_type IN ('INTERCITY_CONTAINER','INTERCITY_BULK') THEN 'INTERCITY'
WHEN rate_type = 'FIRST_MILE' THEN 'FIRST_MILE'
WHEN rate_type = 'LAST_MILE' THEN 'LAST_MILE'
ELSE 'OTHER'
END;
`);
// ── 1b. Backfill trigger from the legacy rate_type matrix ───────────────
await queryRunner.query(`
UPDATE freight.rates SET "trigger" = CASE
WHEN rate_type = 'HAZARD_SURCHARGE' THEN 'HAZARDOUS'
WHEN rate_type = 'REEFER_SURCHARGE' THEN 'REEFER'
WHEN rate_type = 'OVERWEIGHT_PER_TON' THEN 'OVERWEIGHT'
WHEN rate_type = 'DOUBLE_HANDLING' THEN 'SHIPPING_LINE'
WHEN rate_type = 'LASHING' THEN 'CONSOLIDATION'
WHEN rate_type = 'CANCELLATION_FEE' THEN 'CANCELLATION'
WHEN rate_type = 'DEMURRAGE' THEN 'DEMURRAGE'
WHEN rate_type = 'PIL_EXTRA_FEE' THEN 'PIL_EXTRA_FEE'
ELSE 'ALWAYS'
END;
`);
// Align the trigger to the actual surcharge_types mapping where one exists
// (covers any rate wired as a surcharge with a non-obvious rate_type).
await queryRunner.query(`
UPDATE freight.rates r SET "trigger" = m.trig
FROM (
SELECT st.rate_id, CASE st.trigger_condition
WHEN 'CARGO_FLAG_HAZARDOUS' THEN 'HAZARDOUS'
WHEN 'CARGO_FLAG_REEFER' THEN 'REEFER'
WHEN 'VGM_EXCEEDS_LIMIT' THEN 'OVERWEIGHT'
WHEN 'SHIPPING_LINE_MAPPED' THEN 'SHIPPING_LINE'
WHEN 'CONSOLIDATION_ENABLED' THEN 'CONSOLIDATION'
ELSE 'ALWAYS'
END AS trig
FROM freight.surcharge_types st
WHERE st.rate_id IS NOT NULL AND st.deleted_at IS NULL
) m
WHERE r.id = m.rate_id AND m.trig <> 'ALWAYS';
`);
// ── 2. Repoint booking_cargo_modifier to rate_id ────────────────────────
await queryRunner.query(`
ALTER TABLE freight.booking_cargo_modifier
ADD COLUMN IF NOT EXISTS rate_id uuid NULL;
`);
await queryRunner.query(`
UPDATE freight.booking_cargo_modifier bcm
SET rate_id = st.rate_id
FROM freight.surcharge_types st
WHERE bcm.surcharge_type_id = st.id AND st.rate_id IS NOT NULL;
`);
// Rows whose surcharge lost its rate can't be repointed — they reference a
// now-defunct surcharge. Remove them so the NOT NULL + FK can be enforced.
await queryRunner.query(`
DELETE FROM freight.booking_cargo_modifier WHERE rate_id IS NULL;
`);
await queryRunner.query(`
ALTER TABLE freight.booking_cargo_modifier
ALTER COLUMN rate_id SET NOT NULL;
`);
// Drop the old FK + column + index for surcharge_type_id.
await queryRunner.query(`
ALTER TABLE freight.booking_cargo_modifier
DROP CONSTRAINT IF EXISTS "FK_booking_cargo_modifier_surcharge_type_id";
`);
await queryRunner.query(
`DROP INDEX IF EXISTS freight."IDX_booking_cargo_modifier_surcharge_type_id";`,
);
await queryRunner.query(`
ALTER TABLE freight.booking_cargo_modifier
DROP COLUMN IF EXISTS surcharge_type_id;
`);
await queryRunner.query(`
ALTER TABLE freight.booking_cargo_modifier
ADD CONSTRAINT "FK_booking_cargo_modifier_rate_id"
FOREIGN KEY (rate_id) REFERENCES freight.rates(id);
`);
await queryRunner.query(
`CREATE INDEX IF NOT EXISTS "IDX_booking_cargo_modifier_rate_id" ON freight.booking_cargo_modifier (rate_id);`,
);
// ── 3. Drop the surcharge_types table ───────────────────────────────────
await queryRunner.query(`DROP TABLE IF EXISTS freight.surcharge_types;`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
// Recreate surcharge_types (structure only — data is not restored).
await queryRunner.query(`
CREATE TABLE IF NOT EXISTS freight.surcharge_types (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
code varchar(40) NOT NULL,
label varchar(100),
trigger_condition varchar(50),
rate_id uuid,
is_active boolean NOT NULL DEFAULT true,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
deleted_at timestamptz
);
`);
await queryRunner.query(
`CREATE UNIQUE INDEX IF NOT EXISTS "IDX_surcharge_types_code" ON freight.surcharge_types (code);`,
);
// Restore booking_cargo_modifier.surcharge_type_id (nullable; not backfilled).
await queryRunner.query(`
ALTER TABLE freight.booking_cargo_modifier
DROP CONSTRAINT IF EXISTS "FK_booking_cargo_modifier_rate_id";
`);
await queryRunner.query(
`DROP INDEX IF EXISTS freight."IDX_booking_cargo_modifier_rate_id";`,
);
await queryRunner.query(`
ALTER TABLE freight.booking_cargo_modifier
ADD COLUMN IF NOT EXISTS surcharge_type_id uuid NULL;
`);
await queryRunner.query(`
ALTER TABLE freight.booking_cargo_modifier DROP COLUMN IF EXISTS rate_id;
`);
// Drop the new rate columns.
await queryRunner.query(
`DROP INDEX IF EXISTS freight."IDX_rates_trigger";`,
);
await queryRunner.query(`
ALTER TABLE freight.rates DROP CONSTRAINT IF EXISTS "FK_rates_cargo_type_id";
`);
await queryRunner.query(`
ALTER TABLE freight.rates
DROP COLUMN IF EXISTS cargo_type_id,
DROP COLUMN IF EXISTS "trigger",
DROP COLUMN IF EXISTS applies_to;
`);
}
}

View File

@@ -1,37 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
/**
* Contract validity window. When the backoffice accepts a price-confirmed
* booking, staff define how many days the contract stays valid. The window runs
* from the accept moment (valid_from) through valid_from + N days (valid_until).
* Outside that window the contract is considered expired.
*/
export class AddContractValidityWindow1820000000005
implements MigrationInterface
{
name = 'AddContractValidityWindow1820000000005';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS contract_validity_days integer;`,
);
await queryRunner.query(
`ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS contract_valid_from timestamptz;`,
);
await queryRunner.query(
`ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS contract_valid_until timestamptz;`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE freight.bookings DROP COLUMN IF EXISTS contract_valid_until;`,
);
await queryRunner.query(
`ALTER TABLE freight.bookings DROP COLUMN IF EXISTS contract_valid_from;`,
);
await queryRunner.query(
`ALTER TABLE freight.bookings DROP COLUMN IF EXISTS contract_validity_days;`,
);
}
}

View File

@@ -1,41 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
/**
* Booking now captures:
* - customs clearing as an explicit flag + the customs clearing agent name
* (shown when the service includes customs), and
* - first/last-mile pickup & delivery coordinates (lat/lng) alongside the
* existing address text, so the map picker can store and restore the pin.
*
* Shipping line is no longer collected from the booking form; the column stays
* for historical data and the (now dormant) shipping-line pricing trigger.
*/
export class AddCustomsAgentAndMileCoordinates1820000000006
implements MigrationInterface
{
name = 'AddCustomsAgentAndMileCoordinates1820000000006';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.bookings
ADD COLUMN IF NOT EXISTS first_mile_pickup_lat numeric(10,7) NULL,
ADD COLUMN IF NOT EXISTS first_mile_pickup_lng numeric(10,7) NULL,
ADD COLUMN IF NOT EXISTS last_mile_delivery_lat numeric(10,7) NULL,
ADD COLUMN IF NOT EXISTS last_mile_delivery_lng numeric(10,7) NULL,
ADD COLUMN IF NOT EXISTS customs_clearing_enabled boolean NOT NULL DEFAULT false,
ADD COLUMN IF NOT EXISTS customs_clearing_agent varchar(200) NULL;
`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE freight.bookings
DROP COLUMN IF EXISTS customs_clearing_agent,
DROP COLUMN IF EXISTS customs_clearing_enabled,
DROP COLUMN IF EXISTS last_mile_delivery_lng,
DROP COLUMN IF EXISTS last_mile_delivery_lat,
DROP COLUMN IF EXISTS first_mile_pickup_lng,
DROP COLUMN IF EXISTS first_mile_pickup_lat;
`);
}
}

Some files were not shown because too many files have changed in this diff Show More