mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
@@ -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)
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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}`,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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";
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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";
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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';
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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');
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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';
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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";`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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";`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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],
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
Reference in New Issue
Block a user