mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-28 13:40:57 +00:00
Merge branch 'freight/develop' of github.com:Tria-plc/edr-platform into freight_feature/priority
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
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');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
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;
|
||||
`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
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;
|
||||
`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
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';
|
||||
`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
import { MigrationInterface, QueryRunner, Table, TableColumn } from 'typeorm';
|
||||
|
||||
/**
|
||||
* Batch 5 — warehouse allocation rules, storage/demurrage fee rules,
|
||||
* and demurrage lifecycle timestamps on inventory.
|
||||
*/
|
||||
export class AddWarehouseAllocationAndFeeRules1790000000000 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: '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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
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',
|
||||
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;`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import { MigrationInterface, QueryRunner, Table } from 'typeorm';
|
||||
|
||||
/** Batch 6 — warehouse fee invoices + invoice items. */
|
||||
export class AddWarehouseFeeInvoices1790000000001 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
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;
|
||||
`);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
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;`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user