Files
edr-platform/apps/edr-freight-api/src/modules/warehouses/dto/warehouse-zone-stack.dto.ts
Hagernesh a6ea1a48ac 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
2026-08-28 16:09:41 +00:00

121 lines
2.8 KiB
TypeScript

import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsBoolean, IsEnum, IsInt, IsOptional, IsString, IsUUID, Max, MaxLength, Min } from 'class-validator';
import {
DEFAULT_MAX_STACK_HEIGHT,
WAREHOUSE_ZONE_STACK_STATUSES,
WarehouseZoneStackStatus,
} from '../entities/warehouse-zone-stack.entity';
import {
WAREHOUSE_ZONE_SLOT_STATUSES,
WarehouseZoneSlotStatus,
} from '../entities/warehouse-zone-slot.entity';
/** Nobody stacks boxes this high; the cap is here to catch a typo'd 30. */
const MAX_SUPPORTED_STACK_HEIGHT = 10;
export class CreateWarehouseZoneStackDto {
@ApiPropertyOptional({ format: 'uuid', description: 'Optional — taken from the route param when omitted' })
@IsOptional()
@IsUUID()
zoneId?: string;
@ApiProperty({ example: 'ZA-001' })
@IsString()
@MaxLength(40)
code!: string;
@ApiPropertyOptional()
@IsOptional()
@IsString()
@MaxLength(160)
name?: string;
@ApiPropertyOptional({ description: 'Physical row label' })
@IsOptional()
@IsString()
@MaxLength(20)
row?: string;
@ApiPropertyOptional({ description: 'Physical bay label' })
@IsOptional()
@IsString()
@MaxLength(20)
bay?: string;
@ApiPropertyOptional({ description: 'Physical position label' })
@IsOptional()
@IsString()
@MaxLength(20)
position?: string;
@ApiPropertyOptional({
default: DEFAULT_MAX_STACK_HEIGHT,
description: 'One slot is generated per level, 1 to this height.',
})
@IsOptional()
@IsInt()
@Min(1)
@Max(MAX_SUPPORTED_STACK_HEIGHT)
maxStackHeight?: number;
}
export class UpdateWarehouseZoneStackDto {
@ApiPropertyOptional()
@IsOptional()
@IsString()
@MaxLength(40)
code?: string;
@ApiPropertyOptional()
@IsOptional()
@IsString()
@MaxLength(160)
name?: string;
@ApiPropertyOptional()
@IsOptional()
@IsString()
@MaxLength(20)
row?: string;
@ApiPropertyOptional()
@IsOptional()
@IsString()
@MaxLength(20)
bay?: string;
@ApiPropertyOptional()
@IsOptional()
@IsString()
@MaxLength(20)
position?: string;
@ApiPropertyOptional({ description: 'Raising it adds slots; lowering it removes the empty top levels.' })
@IsOptional()
@IsInt()
@Min(1)
@Max(MAX_SUPPORTED_STACK_HEIGHT)
maxStackHeight?: number;
@ApiPropertyOptional({ enum: WAREHOUSE_ZONE_STACK_STATUSES })
@IsOptional()
@IsEnum(WAREHOUSE_ZONE_STACK_STATUSES)
status?: WarehouseZoneStackStatus;
}
export class UpdateWarehouseZoneSlotDto {
@ApiPropertyOptional({
enum: WAREHOUSE_ZONE_SLOT_STATUSES,
description: 'Operator intent only. OCCUPIED is derived from inventory and cannot be set here.',
})
@IsOptional()
@IsEnum(WAREHOUSE_ZONE_SLOT_STATUSES)
status?: WarehouseZoneSlotStatus;
@ApiPropertyOptional()
@IsOptional()
@IsBoolean()
isActive?: boolean;
}