mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-30 09:58:12 +00:00
Merge branch 'dev' of github.com:Tria-plc/edr-platform into freight_feature/usermanagement
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
/**
|
||||
* Double handling becomes an explicit per-booking decision instead of an
|
||||
* implicit "every import" charge. Warehouse staff record Yes/No after
|
||||
* unloading (whether the goods actually had to be re-handled); the
|
||||
* DOUBLE_HANDLING_FEE rule only bills when the answer is Yes.
|
||||
*
|
||||
* NULL = not decided yet → no charge, and the UI shows "not set" so the
|
||||
* operator is prompted. Existing rows stay NULL deliberately: back-billing a
|
||||
* fee nobody confirmed would be wrong.
|
||||
*/
|
||||
export class AddBookingDoubleHandling2850000000000 implements MigrationInterface {
|
||||
name = 'AddBookingDoubleHandling2850000000000';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS double_handling boolean;`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS double_handling_set_at timestamptz;`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE freight.bookings ADD COLUMN IF NOT EXISTS double_handling_set_by varchar(160);`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE freight.bookings DROP COLUMN IF EXISTS double_handling_set_by;`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE freight.bookings DROP COLUMN IF EXISTS double_handling_set_at;`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE freight.bookings DROP COLUMN IF EXISTS double_handling;`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
/**
|
||||
* Per-truck detention clocks. Detention was timed once per last-mile leg
|
||||
* (last_mile.arrived_at / delivered_at), so every truck on a multi-truck
|
||||
* delivery shared one window and was billed identical days — wrong the moment
|
||||
* two trucks arrive or return at different times.
|
||||
*
|
||||
* Deliberately NEW columns rather than reusing the existing per-truck
|
||||
* arrived_at / departed_at on this table: those are WAREHOUSE gate-in/gate-out
|
||||
* events stamped by release(), whereas detention runs from arrival at the
|
||||
* DESTINATION until the truck is released/returned.
|
||||
*
|
||||
* Both nullable — a truck without its own window falls back to the leg-level
|
||||
* timestamps, so legacy legs keep billing exactly as before.
|
||||
*/
|
||||
export class AddPerTruckDetentionWindow2860000000000 implements MigrationInterface {
|
||||
name = 'AddPerTruckDetentionWindow2860000000000';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.last_mile_vehicle_assignments
|
||||
ADD COLUMN IF NOT EXISTS destination_arrived_at timestamptz,
|
||||
ADD COLUMN IF NOT EXISTS returned_at timestamptz;
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.last_mile_vehicle_assignments
|
||||
DROP COLUMN IF EXISTS returned_at,
|
||||
DROP COLUMN IF EXISTS destination_arrived_at;
|
||||
`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
/**
|
||||
* Livestock is billed and counted per head, not per ton — line it up with the
|
||||
* other break-bulk cargo types (Machinery, Truck, Automobile) so bulk
|
||||
* storage/demurrage fees charge per item instead of per ton for it.
|
||||
*/
|
||||
export class LivestockPerItem2900000000000 implements MigrationInterface {
|
||||
name = "LivestockPerItem2900000000000";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
UPDATE freight.cargo_types
|
||||
SET unit_of_measure = 'PER_ITEM'
|
||||
WHERE code = 'LIVESTOCK'
|
||||
AND unit_of_measure IS DISTINCT FROM 'PER_ITEM'
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
UPDATE freight.cargo_types
|
||||
SET unit_of_measure = 'PER_TON'
|
||||
WHERE code = 'LIVESTOCK'
|
||||
AND unit_of_measure IS DISTINCT FROM 'PER_TON'
|
||||
`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
/**
|
||||
* Indode's real 11-yard layout, plus the plumbing to auto-route a booking to
|
||||
* the right yard by cargo type (and, for container yards, trade direction):
|
||||
*
|
||||
* - `warehouse_yards.direction` — IMPORT | EXPORT | BOTH | null. Only
|
||||
* meaningful for CONTAINER_YARD, where import and export stacks are
|
||||
* physically separate (Yard 5 vs Yard 6). Everything else takes cargo
|
||||
* either way. A CONTAINER_YARD left at null/BOTH is a signal too: it means
|
||||
* "not a customer cargo yard" — Yards 10/11 (service/equipment) are
|
||||
* CONTAINER_YARD structurally but must never be offered for ordinary
|
||||
* import/export cargo, so the frontend match requires an EXACT IMPORT/
|
||||
* EXPORT direction hit for container freight rather than treating BOTH as
|
||||
* a wildcard.
|
||||
* - `warehouse_yard_cargo_types` — which cargo types a yard accepts (mirrors
|
||||
* the existing `cargo_type_wagon_types` join table). Empty = open to any
|
||||
* cargo type of the yard's structural type (additive, never restrictive
|
||||
* by default), so this cannot break a yard nobody has configured yet.
|
||||
*
|
||||
* Three cargo types didn't exist yet (Fertilizer, Coffee, Tea) — added here
|
||||
* so Yards 1 and 9 have a real mapping ready for when they reopen.
|
||||
*/
|
||||
export class IndodeYardsAndCargoRouting2990000000000 implements MigrationInterface {
|
||||
name = "IndodeYardsAndCargoRouting2990000000000";
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE freight.warehouse_yards
|
||||
ADD COLUMN IF NOT EXISTS direction varchar(10)
|
||||
`);
|
||||
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE IF NOT EXISTS freight.warehouse_yard_cargo_types (
|
||||
yard_id uuid NOT NULL REFERENCES freight.warehouse_yards (id) ON DELETE CASCADE,
|
||||
cargo_type_id uuid NOT NULL REFERENCES freight.cargo_types (id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (yard_id, cargo_type_id)
|
||||
)
|
||||
`);
|
||||
|
||||
// New cargo types Indode's yard list names but the catalog didn't have yet.
|
||||
await queryRunner.query(`
|
||||
INSERT INTO freight.cargo_types (code, cargo_type_name, unit_of_measure, is_active)
|
||||
VALUES
|
||||
('FERTILIZER', 'Fertilizer', 'PER_TON', true),
|
||||
('COFFEE', 'Coffee', 'PER_TON', true),
|
||||
('TEA', 'Tea', 'PER_TON', true)
|
||||
ON CONFLICT (code) DO NOTHING
|
||||
`);
|
||||
|
||||
// The 11 real yards at Indode Open Warehouse (code 'IOW').
|
||||
await queryRunner.query(`
|
||||
INSERT INTO freight.warehouse_yards
|
||||
(warehouse_id, name, code, type, direction, status, is_active)
|
||||
SELECT w.id, y.name, y.code, y.type, y.direction, y.status, y.status = 'ACTIVE'
|
||||
FROM freight.warehouses w
|
||||
CROSS JOIN (VALUES
|
||||
('Y1', 'Bagged Cargo Discharge - Fertilizer', 'BULK_YARD', NULL, 'INACTIVE'),
|
||||
('Y2', 'Break Bulk', 'GENERAL_CARGO_YARD', NULL, 'ACTIVE'),
|
||||
('Y3', 'Ro-Ro / Pac', 'GENERAL_CARGO_YARD', NULL, 'ACTIVE'),
|
||||
('Y4', 'Dry Bulk', 'BULK_YARD', NULL, 'INACTIVE'),
|
||||
('Y5', 'Container Terminal - Import (Stack Area)', 'CONTAINER_YARD', 'IMPORT', 'ACTIVE'),
|
||||
('Y6', 'Container Terminal - Export', 'CONTAINER_YARD', 'EXPORT', 'ACTIVE'),
|
||||
('Y7', 'Cold Chain', 'COLD_STORAGE_YARD', NULL, 'INACTIVE'),
|
||||
('Y8', 'Chemical', 'HAZARDOUS_YARD', NULL, 'INACTIVE'),
|
||||
('Y9', 'Coffee and Tea', 'GENERAL_CARGO_YARD', NULL, 'INACTIVE'),
|
||||
('Y10', 'Container Service Yard - Maintenance', 'CONTAINER_YARD', 'BOTH', 'ACTIVE'),
|
||||
('Y11', 'Equipment (Empty Container)', 'CONTAINER_YARD', 'BOTH', 'ACTIVE')
|
||||
) AS y(code, name, type, direction, status)
|
||||
WHERE w.code = 'IOW'
|
||||
ON CONFLICT (warehouse_id, code) DO NOTHING
|
||||
`);
|
||||
|
||||
// One default zone per new yard, matching its yard's type — every existing
|
||||
// yard (CY-1, CY-A) already follows this one-zone-per-yard shape.
|
||||
await queryRunner.query(`
|
||||
INSERT INTO freight.warehouse_zones (yard_id, name, code, type, status, is_active)
|
||||
SELECT y.id, y.name || ' Zone 1', 'Z1',
|
||||
CASE y.type
|
||||
WHEN 'CONTAINER_YARD' THEN 'CONTAINER_ZONE'
|
||||
WHEN 'COLD_STORAGE_YARD' THEN 'COLD_STORAGE_ZONE'
|
||||
WHEN 'HAZARDOUS_YARD' THEN 'HAZARDOUS_ZONE'
|
||||
WHEN 'BULK_YARD' THEN 'BULK_ZONE'
|
||||
ELSE 'GENERAL_CARGO_ZONE'
|
||||
END,
|
||||
y.status, y.status = 'ACTIVE'
|
||||
FROM freight.warehouse_yards y
|
||||
JOIN freight.warehouses w ON w.id = y.warehouse_id
|
||||
WHERE w.code = 'IOW' AND y.code LIKE 'Y%'
|
||||
ON CONFLICT (yard_id, code) DO NOTHING
|
||||
`);
|
||||
|
||||
// Cargo-type routing. Yards 5/6/10/11 (CONTAINER_YARD) are intentionally
|
||||
// left with no rows — direction alone decides those, per the entity comment.
|
||||
await queryRunner.query(`
|
||||
INSERT INTO freight.warehouse_yard_cargo_types (yard_id, cargo_type_id)
|
||||
SELECT y.id, ct.id
|
||||
FROM freight.warehouses w
|
||||
JOIN freight.warehouse_yards y ON y.warehouse_id = w.id
|
||||
JOIN (VALUES
|
||||
('Y1', 'FERTILIZER'),
|
||||
('Y2', 'STEEL_BILLET'), ('Y2', 'PLASTIC_BARREL'), ('Y2', 'MACHINERY'), ('Y2', 'LIVESTOCK'),
|
||||
('Y3', 'AUTOMOBILE'), ('Y3', 'TRUCK'),
|
||||
('Y4', 'BARLY'), ('Y4', 'BEANS'), ('Y4', 'BULK'), ('Y4', 'CEREAL'),
|
||||
('Y4', 'EDIBLE_OIL'), ('Y4', 'RICE'), ('Y4', 'SUGAR'), ('Y4', 'WHEAT'),
|
||||
('Y7', 'PERISHABLE'),
|
||||
('Y9', 'COFFEE'), ('Y9', 'TEA')
|
||||
) AS m(yard_code, cargo_code) ON m.yard_code = y.code
|
||||
JOIN freight.cargo_types ct ON ct.code = m.cargo_code
|
||||
WHERE w.code = 'IOW'
|
||||
ON CONFLICT (yard_id, cargo_type_id) DO NOTHING
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
DELETE FROM freight.warehouse_zones z
|
||||
USING freight.warehouse_yards y, freight.warehouses w
|
||||
WHERE z.yard_id = y.id AND y.warehouse_id = w.id
|
||||
AND w.code = 'IOW' AND y.code LIKE 'Y%'
|
||||
`);
|
||||
await queryRunner.query(`
|
||||
DELETE FROM freight.warehouse_yards y
|
||||
USING freight.warehouses w
|
||||
WHERE y.warehouse_id = w.id AND w.code = 'IOW' AND y.code LIKE 'Y%'
|
||||
`);
|
||||
// Cargo types and the join table are left in place — other data may have
|
||||
// started referencing them since; dropping columns/tables is not reversible
|
||||
// once real rows exist, and leaving them is harmless.
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user