mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-08 21:48:18 +00:00
feat(warehouses): track physical container stack and slot positions
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
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
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`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user