Files
edr-platform/apps/edr-freight-api/src/modules/warehouses/warehouse-zone-stacks.controller.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

103 lines
3.4 KiB
TypeScript

import {
BadRequestException,
Body,
Controller,
Delete,
Get,
Param,
ParseUUIDPipe,
Patch,
Post,
Query,
} from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { BookingStaff } from '../../common/booking-guards';
import { FREIGHT_PERMS } from '../../seed/freight-permissions.registry';
import {
CreateWarehouseZoneStackDto,
UpdateWarehouseZoneSlotDto,
UpdateWarehouseZoneStackDto,
} from './dto/warehouse-zone-stack.dto';
import { WarehouseZoneStacksService } from './warehouse-zone-stacks.service';
/**
* Stacks and slots are zone configuration, so they ride the warehouse-zone
* permissions rather than introducing new keys — a new key needs a matching
* `iam.permissions` row in every environment or boot fails.
*/
@ApiTags('warehouse-zone-stacks')
@ApiBearerAuth()
@Controller('warehouse-zone-stacks')
// Class gate lists every key its routes use: Nest runs class AND method guards.
@BookingStaff([
FREIGHT_PERMS.warehouseZones.view,
FREIGHT_PERMS.warehouseInventory.view,
FREIGHT_PERMS.warehouseZones.create,
FREIGHT_PERMS.warehouseZones.update,
FREIGHT_PERMS.warehouseZones.delete,
])
export class WarehouseZoneStacksController {
constructor(private readonly stacksService: WarehouseZoneStacksService) {}
@Get()
@ApiOperation({ summary: 'List the ground stacks configured in a zone' })
findByZone(@Query('zoneId', ParseUUIDPipe) zoneId: string) {
return this.stacksService.findByZone(zoneId);
}
@Post()
@BookingStaff(FREIGHT_PERMS.warehouseZones.create)
@ApiOperation({
summary: 'Create a ground stack',
description: 'One slot per level is generated automatically, from 1 to maxStackHeight (default 3).',
})
create(@Body() dto: CreateWarehouseZoneStackDto, @Query('zoneId') zoneIdQuery?: string) {
const zoneId = dto.zoneId ?? zoneIdQuery;
if (!zoneId) {
throw new BadRequestException('zoneId is required');
}
return this.stacksService.create(zoneId, dto);
}
// Declared before ':id' so 'slots' is never swallowed as a stack id.
@Patch('slots/:slotId')
@BookingStaff(FREIGHT_PERMS.warehouseZones.update)
@ApiOperation({
summary: 'Block, reserve, or reactivate one slot',
description: 'Occupancy is derived from inventory and cannot be set here.',
})
updateSlot(@Param('slotId', ParseUUIDPipe) slotId: string, @Body() dto: UpdateWarehouseZoneSlotDto) {
return this.stacksService.updateSlot(slotId, dto);
}
@Get(':id')
@ApiOperation({ summary: 'Get one stack with its slots' })
findOne(@Param('id', ParseUUIDPipe) id: string) {
return this.stacksService.findById(id);
}
@Get(':id/occupancy')
@ApiOperation({ summary: 'Level-by-level occupancy of one stack' })
occupancy(@Param('id', ParseUUIDPipe) id: string) {
return this.stacksService.slotOccupancy(id);
}
@Patch(':id')
@BookingStaff(FREIGHT_PERMS.warehouseZones.update)
@ApiOperation({
summary: 'Update a stack',
description: 'Raising maxStackHeight adds slots; lowering it trims the empty top levels.',
})
update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateWarehouseZoneStackDto) {
return this.stacksService.update(id, dto);
}
@Delete(':id')
@BookingStaff(FREIGHT_PERMS.warehouseZones.delete)
@ApiOperation({ summary: 'Delete a stack', description: 'Refused while containers still stand in it.' })
remove(@Param('id', ParseUUIDPipe) id: string) {
return this.stacksService.remove(id);
}
}