mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 06:35:42 +00:00
Extends the warehouse hierarchy below zone with ground stacks and vertical slots, so a container's exact position is recorded rather than only its zone. - freight.warehouse_zone_stacks / warehouse_zone_slots, plus nullable stack_id / slot_id on warehouse_inventory (existing rows stay valid) - slot occupancy is derived from inventory status, guarded by a partial unique index, so no exit path has to remember to free a slot - placement service: hierarchy validation, bottom-up stacking rules, accessibility/blocking-container reads, capacity vs slot summaries - stack CRUD with auto-generated slots; reuses warehouse-zone permissions - slot support folded into the existing move()/store() paths - fix: validateLocation now rejects a mismatched warehouse/yard/zone triple - seed:warehouse-layout builds the layout from a JSON config
129 lines
5.8 KiB
TypeScript
129 lines
5.8 KiB
TypeScript
import { MigrationInterface, QueryRunner } from 'typeorm';
|
|
|
|
/**
|
|
* Physical container positions below the zone: a stack is the ground footprint,
|
|
* a slot is one level in it. Adds `stack_id` / `slot_id` to warehouse inventory.
|
|
*
|
|
* Everything is additive and nullable. Existing inventory keeps warehouse /
|
|
* yard / zone as its only location and stays valid — nothing is backfilled,
|
|
* because no one can know where a box already in the yard is actually stacked.
|
|
*
|
|
* Occupancy is not stored on the slot. `uq_warehouse_inventory_active_slot`
|
|
* makes the inventory row the single source of truth: one live placement per
|
|
* slot, enforced by Postgres. Its status list must stay in step with
|
|
* `SLOT_OCCUPYING_STATUSES` in warehouse-inventory.entity.ts.
|
|
*/
|
|
export class WarehouseZoneStacksSlots3830000000000 implements MigrationInterface {
|
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
|
await queryRunner.query(`
|
|
CREATE TABLE IF NOT EXISTS freight.warehouse_zone_stacks (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
zone_id uuid NOT NULL REFERENCES freight.warehouse_zones(id) ON DELETE CASCADE,
|
|
code varchar(40) NOT NULL,
|
|
name varchar(160),
|
|
"row" varchar(20),
|
|
bay varchar(20),
|
|
"position" varchar(20),
|
|
max_stack_height int NOT NULL DEFAULT 3,
|
|
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,
|
|
CONSTRAINT chk_warehouse_zone_stacks_height CHECK (max_stack_height >= 1)
|
|
)
|
|
`);
|
|
|
|
await queryRunner.query(
|
|
`CREATE INDEX IF NOT EXISTS idx_warehouse_zone_stacks_zone ON freight.warehouse_zone_stacks (zone_id)`,
|
|
);
|
|
await queryRunner.query(
|
|
`CREATE INDEX IF NOT EXISTS idx_warehouse_zone_stacks_status ON freight.warehouse_zone_stacks (status)`,
|
|
);
|
|
// Partial: a soft-deleted stack must not block reusing its code.
|
|
await queryRunner.query(
|
|
`CREATE UNIQUE INDEX IF NOT EXISTS uq_warehouse_zone_stacks_zone_code
|
|
ON freight.warehouse_zone_stacks (zone_id, code) WHERE deleted_at IS NULL`,
|
|
);
|
|
|
|
await queryRunner.query(`
|
|
CREATE TABLE IF NOT EXISTS freight.warehouse_zone_slots (
|
|
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
stack_id uuid NOT NULL REFERENCES freight.warehouse_zone_stacks(id) ON DELETE CASCADE,
|
|
level int NOT NULL,
|
|
status varchar(16) NOT NULL DEFAULT 'AVAILABLE',
|
|
is_active boolean NOT NULL DEFAULT true,
|
|
created_at timestamptz NOT NULL DEFAULT now(),
|
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
deleted_at timestamptz,
|
|
CONSTRAINT chk_warehouse_zone_slots_level CHECK (level >= 1)
|
|
)
|
|
`);
|
|
|
|
await queryRunner.query(
|
|
`CREATE INDEX IF NOT EXISTS idx_warehouse_zone_slots_stack ON freight.warehouse_zone_slots (stack_id, level)`,
|
|
);
|
|
await queryRunner.query(
|
|
`CREATE UNIQUE INDEX IF NOT EXISTS uq_warehouse_zone_slots_stack_level
|
|
ON freight.warehouse_zone_slots (stack_id, level) WHERE deleted_at IS NULL`,
|
|
);
|
|
|
|
await queryRunner.query(
|
|
`ALTER TABLE freight.warehouse_inventory ADD COLUMN IF NOT EXISTS stack_id uuid`,
|
|
);
|
|
await queryRunner.query(
|
|
`ALTER TABLE freight.warehouse_inventory ADD COLUMN IF NOT EXISTS slot_id uuid`,
|
|
);
|
|
|
|
// Named FKs added defensively — ADD CONSTRAINT has no IF NOT EXISTS.
|
|
await queryRunner.query(`
|
|
DO $$ BEGIN
|
|
ALTER TABLE freight.warehouse_inventory
|
|
ADD CONSTRAINT fk_warehouse_inventory_stack
|
|
FOREIGN KEY (stack_id) REFERENCES freight.warehouse_zone_stacks(id);
|
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$
|
|
`);
|
|
await queryRunner.query(`
|
|
DO $$ BEGIN
|
|
ALTER TABLE freight.warehouse_inventory
|
|
ADD CONSTRAINT fk_warehouse_inventory_slot
|
|
FOREIGN KEY (slot_id) REFERENCES freight.warehouse_zone_slots(id);
|
|
EXCEPTION WHEN duplicate_object THEN NULL; END $$
|
|
`);
|
|
|
|
await queryRunner.query(
|
|
`CREATE INDEX IF NOT EXISTS idx_warehouse_inventory_stack ON freight.warehouse_inventory (stack_id)`,
|
|
);
|
|
await queryRunner.query(
|
|
`CREATE INDEX IF NOT EXISTS idx_warehouse_inventory_slot ON freight.warehouse_inventory (slot_id)`,
|
|
);
|
|
|
|
// One live container per slot. Statuses past the yard gate (LOADED,
|
|
// DISPATCHED, DELIVERED, UNLOADED_AT_DJIBOUTI_PORT) free the position
|
|
// without any exit path having to clear the column.
|
|
await queryRunner.query(`
|
|
CREATE UNIQUE INDEX IF NOT EXISTS uq_warehouse_inventory_active_slot
|
|
ON freight.warehouse_inventory (slot_id)
|
|
WHERE deleted_at IS NULL
|
|
AND slot_id IS NOT NULL
|
|
AND status IN ('UNLOADED','RECEIVED','STORED','RESERVED','READY_FOR_LOADING','READY_FOR_PICKUP')
|
|
`);
|
|
}
|
|
|
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
|
await queryRunner.query(`DROP INDEX IF EXISTS freight.uq_warehouse_inventory_active_slot`);
|
|
await queryRunner.query(`DROP INDEX IF EXISTS freight.idx_warehouse_inventory_slot`);
|
|
await queryRunner.query(`DROP INDEX IF EXISTS freight.idx_warehouse_inventory_stack`);
|
|
await queryRunner.query(
|
|
`ALTER TABLE freight.warehouse_inventory DROP CONSTRAINT IF EXISTS fk_warehouse_inventory_slot`,
|
|
);
|
|
await queryRunner.query(
|
|
`ALTER TABLE freight.warehouse_inventory DROP CONSTRAINT IF EXISTS fk_warehouse_inventory_stack`,
|
|
);
|
|
await queryRunner.query(`ALTER TABLE freight.warehouse_inventory DROP COLUMN IF EXISTS slot_id`);
|
|
await queryRunner.query(`ALTER TABLE freight.warehouse_inventory DROP COLUMN IF EXISTS stack_id`);
|
|
await queryRunner.query(`DROP TABLE IF EXISTS freight.warehouse_zone_slots`);
|
|
await queryRunner.query(`DROP TABLE IF EXISTS freight.warehouse_zone_stacks`);
|
|
}
|
|
}
|