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

82 lines
2.9 KiB
TypeScript

import { Body, Controller, Delete, Get, Param, ParseUUIDPipe, Patch } 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 { UpdateWarehouseZoneDto } from './dto/update-warehouse-zone.dto';
import { WarehouseZonesService } from './warehouse-zones.service';
@ApiTags('warehouse-zones')
@ApiBearerAuth()
// Baseline read: zone reference data also serves inventory flows (allocation,
// receive/move pickers) — either view permission grants reads; writes stack
// their specific permission per route.
@Controller('warehouse-zones')
// Class gate lists every key its routes use: Nest runs class AND method
// guards, so a key missing here would deny before the route's own key runs.
@BookingStaff([
FREIGHT_PERMS.warehouseZones.view,
FREIGHT_PERMS.warehouseInventory.view,
FREIGHT_PERMS.warehouseZones.update,
FREIGHT_PERMS.warehouseZones.delete,
])
export class WarehouseZonesController {
constructor(private readonly zonesService: WarehouseZonesService) {}
@Get()
@ApiOperation({ summary: 'List all warehouse zones' })
findAll() {
return this.zonesService.findAll();
}
@Get(':id')
@ApiOperation({ summary: 'Get warehouse zone by ID' })
findOne(@Param('id', ParseUUIDPipe) id: string) {
return this.zonesService.findById(id);
}
@Patch(':id')
@BookingStaff(FREIGHT_PERMS.warehouseZones.update)
@ApiOperation({ summary: 'Update warehouse zone' })
update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateWarehouseZoneDto) {
return this.zonesService.update(id, dto);
}
@Get(':id/contents')
@ApiOperation({
summary: 'What is currently stored in a zone',
description: 'A row per container — booked units and backlog-registered containers alike.',
})
contents(@Param('id', ParseUUIDPipe) id: string) {
return this.zonesService.contents(id);
}
@Get(':id/layout')
@ApiOperation({
summary: 'Physical layout of a zone',
description: 'Every ground stack with its levels, what stands on each, and the slot summary.',
})
layout(@Param('id', ParseUUIDPipe) id: string) {
return this.zonesService.layout(id);
}
@Get(':id/slot-summary')
@ApiOperation({
summary: 'Configured capacity vs physical slots vs occupancy',
description: 'Flags a zone whose built slots exceed its configured container capacity.',
})
slotSummary(@Param('id', ParseUUIDPipe) id: string) {
return this.zonesService.slotSummary(id);
}
@Delete(':id')
@BookingStaff(FREIGHT_PERMS.warehouseZones.delete)
@ApiOperation({
summary: 'Delete warehouse zone',
description: 'Soft-deletes the zone. Refused while inventory still sits in it.',
})
remove(@Param('id', ParseUUIDPipe) id: string) {
return this.zonesService.remove(id);
}
}