feat(warehouses): map GRN numbers to the goods owner

GRN-<DIR>-<DATE>-<REF8> carried no owner, so a note couldn't be
identified by who owns the cargo. Add an owner segment sourced from the
booking's company at every generation point (import, export, facility,
manual receive), keep REF8 for uniqueness, and label the GRN document
row Owner's Name.
This commit is contained in:
Hagernesh
2026-07-27 09:55:15 +00:00
parent 2a97bd4235
commit 101bf69271
15 changed files with 690 additions and 64 deletions

View File

@@ -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.
}
}