import { Body, Controller, Get, Param, ParseUUIDPipe, Patch, Post, Query, Request, Res } from '@nestjs/common'; import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; import type { Response } from 'express'; import { CurrentUser } from '@edr/api-common'; import type { TCurrentUser } from '@tria-plc/api-common/modules/auth/types/current-user.type'; import { actorLabel } from './current-actor.util'; import { BookingStaff } from '../../common/booking-guards'; import { FREIGHT_PERMS } from '../../seed/freight-permissions.registry'; import { BulkReceiveDto } from './dto/bulk-receive.dto'; import { BulkInspectDto } from './dto/bulk-inspect.dto'; import { DeliverInventoryDto } from './dto/deliver-inventory.dto'; import { FilterWarehouseInventoryDto } from './dto/filter-inventory.dto'; import { InquiryWarehouseInventoryDto } from './dto/inquiry-inventory.dto'; import { LoadInventoryDto } from './dto/load-inventory.dto'; import { MoveInventoryDto } from './dto/move-inventory.dto'; import { StoreInventoryDto } from './dto/store-inventory.dto'; import { ReceiveWarehouseInventoryDto } from './dto/receive-inventory.dto'; import { ApproveDeliveryDto } from './dto/approve-delivery.dto'; import { ReleaseOrderDto } from './dto/release-order.dto'; import { ReserveInventoryDto } from './dto/reserve-inventory.dto'; import { UnloadBookingDto } from './dto/unload-booking.dto'; import { SchedulingReadFacade } from './scheduling-read.facade'; import { WarehouseInventoryService } from './warehouse-inventory.service'; import { HandoverService } from './handover.service'; @ApiTags('warehouse-inventory') @ApiBearerAuth() @Controller('warehouse-inventory') export class WarehouseInventoryController { constructor( private readonly inventoryService: WarehouseInventoryService, private readonly scheduling: SchedulingReadFacade, private readonly handoverService: HandoverService, ) {} @Get() @BookingStaff(FREIGHT_PERMS.warehouseInventory.view) @ApiOperation({ summary: 'List warehouse inventory' }) findAll(@Query() filter: FilterWarehouseInventoryDto) { return this.inventoryService.findAll(filter); } @Get('ready-for-loading') @BookingStaff(FREIGHT_PERMS.warehouseInventory.view) @ApiOperation({ summary: 'List inventory ready for loading' }) findReadyForLoading(@Query() filter: FilterWarehouseInventoryDto) { return this.inventoryService.findReadyForLoading(filter); } @Get('inquiry') @BookingStaff(FREIGHT_PERMS.warehouseInventory.view) @ApiOperation({ summary: 'Locate any item inside the warehouse' }) inquiry(@Query() filter: InquiryWarehouseInventoryDto) { return this.inventoryService.inquiry(filter); } @Get('arrival-queue') @BookingStaff(FREIGHT_PERMS.warehouseInventory.view) @ApiOperation({ summary: 'Arrived bookings awaiting unload / inspection' }) arrivalQueue() { return this.inventoryService.arrivalQueue(); } @Get('ops-stats') @BookingStaff(FREIGHT_PERMS.warehouseInventory.view) @ApiOperation({ summary: 'At-a-glance warehouse ops counters for the KPI strip' }) opsStats() { return this.inventoryService.opsStats(); } @Get('trucks-on-site') @BookingStaff(FREIGHT_PERMS.warehouseInventory.view) @ApiOperation({ summary: 'Trucks currently in the yard (customer self-haul + EDR last-mile)', }) trucksOnSite() { return this.inventoryService.trucksOnSite(); } @Get('zone-occupancy') @BookingStaff(FREIGHT_PERMS.warehouseInventory.view) @ApiOperation({ summary: 'Live occupancy per zone (rated capacity vs held) — heatmap data' }) zoneOccupancy(@Query('yardId') yardId?: string) { return this.inventoryService.zoneOccupancy(yardId); } @Get('throughput') @BookingStaff(FREIGHT_PERMS.warehouseInventory.view) @ApiOperation({ summary: 'Received-vs-dispatched throughput time series (week/month/year)' }) throughput(@Query('granularity') granularity?: string) { const g = granularity === 'week' || granularity === 'year' ? granularity : 'month'; return this.inventoryService.throughput(g); } @Get('dwell-stats') @BookingStaff(FREIGHT_PERMS.warehouseInventory.view) @ApiOperation({ summary: 'Dwell time of in-warehouse items: average + aging buckets' }) dwellStats() { return this.inventoryService.dwellStats(); } @Get('cycle-stats') @BookingStaff(FREIGHT_PERMS.warehouseInventory.view) @ApiOperation({ summary: 'Average stage cycle times over recently dispatched items' }) cycleStats() { return this.inventoryService.cycleStats(); } @Get('gate-stats') @BookingStaff(FREIGHT_PERMS.warehouseInventory.view) @ApiOperation({ summary: 'Gate/dock throughput: cleared today, turnaround, hourly clearances' }) gateStats() { return this.inventoryService.gateStats(); } @Post('auto-unload-arrived') @BookingStaff(FREIGHT_PERMS.warehouseInventory.unload) @ApiOperation({ summary: 'Bulk auto-unload all arrived bookings into the warehouse' }) autoUnloadArrived() { return this.inventoryService.autoUnloadArrived(); } @Post('auto-load-ready') @BookingStaff(FREIGHT_PERMS.warehouseInventory.load) @ApiOperation({ summary: 'Auto-load READY_FOR_LOADING inventory with PAID bookings' }) autoLoadReady() { return this.inventoryService.autoLoadReady(); } @Get('eligible-bookings') @BookingStaff(FREIGHT_PERMS.warehouseInventory.view) @ApiOperation({ summary: 'PAID bookings not yet received, classified IMPORT/EXPORT by route; omit direction for all' }) eligibleBookings(@Query('direction') direction?: string) { const dir = direction === 'IMPORT' || direction === 'EXPORT' ? direction : undefined; return this.inventoryService.eligibleBookings(dir); } @Post('receive-bulk') @BookingStaff(FREIGHT_PERMS.warehouseInventory.receive) @ApiOperation({ summary: 'Bulk-receive selected eligible PAID bookings into a location' }) receiveBulk(@Body() dto: BulkReceiveDto, @CurrentUser() user: TCurrentUser) { dto.performedBy = actorLabel(user) ?? dto.performedBy; return this.inventoryService.bulkReceive(dto); } @Get('ready-to-load-export') @BookingStaff(FREIGHT_PERMS.warehouseInventory.view) @ApiOperation({ summary: 'EXPORT inventory that passed inspection and is READY_FOR_LOADING' }) readyToLoadExport() { return this.inventoryService.readyToLoadExport(); } @Get('received-export') @BookingStaff(FREIGHT_PERMS.warehouseInventory.view) @ApiOperation({ summary: 'EXPORT inventory that has been received and is awaiting inspection' }) receivedExport() { return this.inventoryService.receivedExport(); } @Get('loaded-export') @BookingStaff(FREIGHT_PERMS.warehouseInventory.view) @ApiOperation({ summary: 'EXPORT inventory that is LOADED and queued for dispatch' }) loadedExport() { return this.inventoryService.loadedExport(); } @Get('loadable-trains') @BookingStaff(FREIGHT_PERMS.warehouseInventory.view) @ApiOperation({ summary: 'EXPORT trains (pre-dispatch) with inventory waiting to be loaded' }) loadableTrains() { return this.inventoryService.loadableTrains(); } @Get('train/:scheduleId/loadable-items') @BookingStaff(FREIGHT_PERMS.warehouseInventory.view) @ApiOperation({ summary: 'Container/cargo inventory assigned to a train, with allocated wagons' }) trainLoadableItems(@Param('scheduleId', ParseUUIDPipe) scheduleId: string) { return this.inventoryService.trainLoadableItems(scheduleId); } @Post('train/:scheduleId/load') @BookingStaff(FREIGHT_PERMS.warehouseInventory.load) @ApiOperation({ summary: 'Load selected inventory items onto their allocated wagons for a train' }) loadItemsOntoTrain( @Param('scheduleId', ParseUUIDPipe) scheduleId: string, @Body() dto: { inventoryIds: string[]; performedBy?: string }, @CurrentUser() user: TCurrentUser, ) { return this.inventoryService.loadItemsOntoTrain(scheduleId, dto.inventoryIds ?? [], actorLabel(user) ?? dto.performedBy); } @Post('bulk-dispatch-export') @BookingStaff(FREIGHT_PERMS.warehouseInventory.dispatch) @ApiOperation({ summary: 'Bulk-dispatch loaded EXPORT inventory (LOADED → DISPATCHED)' }) bulkDispatchExport(@Body() dto: { inventoryIds: string[]; performedBy?: string }, @CurrentUser() user: TCurrentUser) { return this.inventoryService.bulkDispatchExport(dto.inventoryIds ?? [], actorLabel(user) ?? dto.performedBy); } @Post('bulk-mark-inspected') @BookingStaff(FREIGHT_PERMS.warehouseInventory.inspect) @ApiOperation({ summary: 'Bulk mark received inventory inspection PASSED (EXPORT → READY_FOR_LOADING)' }) bulkMarkInspected(@Body() dto: BulkInspectDto) { return this.inventoryService.bulkMarkInspected(dto); } @Post('bookings/:bookingId/unload') @BookingStaff(FREIGHT_PERMS.warehouseInventory.unload) @ApiOperation({ summary: 'Unload a single arrived booking into a location' }) unloadBooking( @Param('bookingId', ParseUUIDPipe) bookingId: string, @Body() dto: UnloadBookingDto, ) { return this.inventoryService.unloadBooking(bookingId, dto); } @Post(':id/gate-clearance') @BookingStaff(FREIGHT_PERMS.warehouseInventory.gatePass) @ApiOperation({ summary: 'Final terminal release / gate clearance (blocked while fees unpaid)' }) gateClearance( @Param('id', ParseUUIDPipe) id: string, @Body('performedBy') performedBy: string | undefined, @CurrentUser() user: TCurrentUser, ) { return this.inventoryService.gateClearance(id, actorLabel(user) ?? performedBy); } @Get('import/arrive-queue') @BookingStaff(FREIGHT_PERMS.warehouseInventory.view) @ApiOperation({ summary: 'Arrived IMPORT train schedules (route-derived), read-only from scheduling' }) importArriveQueue() { return this.scheduling.importArriveQueue(); } @Get('import/trains/:scheduleId/items') @BookingStaff(FREIGHT_PERMS.warehouseInventory.view) @ApiOperation({ summary: 'Assigned bookings/items for an arrived import train (read-only)' }) importTrainDetail(@Param('scheduleId', ParseUUIDPipe) scheduleId: string) { return this.scheduling.importTrainDetail(scheduleId); } @Post('import/auto-unload-arrived-bookings') @BookingStaff(FREIGHT_PERMS.warehouseInventory.unload) @ApiOperation({ summary: 'Unload all eligible assigned bookings of an ARRIVED import train (→ UNLOADED)' }) autoUnloadArrivedBookings(@Body() dto: { scheduleId: string; warehouseId?: string; performedBy?: string; assignments?: { bookingId: string; warehouseId: string; yardId: string; zoneId: string }[]; }, @CurrentUser() user: TCurrentUser) { return this.inventoryService.autoUnloadArrivedBookings( dto.scheduleId, actorLabel(user) ?? dto.performedBy, dto.warehouseId, dto.assignments, ); } @Get('import/unloaded-queue') @BookingStaff(FREIGHT_PERMS.warehouseInventory.view) @ApiOperation({ summary: 'IMPORT inventory in the Unloaded Queue (UNLOADED / destination inspection)' }) importUnloadedQueue() { return this.inventoryService.importUnloadedQueue(); } @Get('export/djibouti-arrival-queue') @BookingStaff(FREIGHT_PERMS.warehouseInventory.view) @ApiOperation({ summary: 'Arrived EXPORT train schedules at Djibouti-side ports, ready for unloading' }) exportDjiboutiArrivalQueue( @Query('scheduleId') scheduleId?: string, @Query('destination') destination?: string, @Query('status') status?: string, @Query('dateFrom') dateFrom?: string, @Query('dateTo') dateTo?: string, ) { return this.scheduling.exportDjiboutiArrivalQueue({ scheduleId, destination, status, dateFrom, dateTo, }); } @Get('export/djibouti-trains/:scheduleId/items') @BookingStaff(FREIGHT_PERMS.warehouseInventory.view) @ApiOperation({ summary: 'Assigned export bookings/items for an arrived Djibouti-side train' }) exportDjiboutiTrainDetail(@Param('scheduleId', ParseUUIDPipe) scheduleId: string) { return this.scheduling.exportDjiboutiTrainDetail(scheduleId); } @Post('export/auto-unload-at-djibouti') @BookingStaff(FREIGHT_PERMS.warehouseInventory.unload) @ApiOperation({ summary: 'Unload all eligible export items assigned to an arrived Djibouti-side train' }) autoUnloadExportAtDjibouti(@Body() dto: { scheduleId: string; performedBy?: string }, @CurrentUser() user: TCurrentUser) { return this.inventoryService.autoUnloadExportAtDjibouti(dto.scheduleId, actorLabel(user) ?? dto.performedBy); } @Get('import/pickup-ready-queue') @BookingStaff(FREIGHT_PERMS.warehouseInventory.view) @ApiOperation({ summary: 'IMPORT inventory that is PICKUP_READY (READY_FOR_PICKUP) awaiting pickup/dispatch' }) importPickupReadyQueue() { return this.inventoryService.importPickupReadyQueue(); } @Get('loadable-wagons') @BookingStaff(FREIGHT_PERMS.warehouseInventory.view) @ApiOperation({ summary: 'List wagons usable for loading (read-only from scheduling)' }) loadableWagons() { return this.scheduling.listLoadableWagons(); } @Get('booking/:bookingId/schedule') @BookingStaff(FREIGHT_PERMS.warehouseInventory.view) @ApiOperation({ summary: 'Read-only schedule + wagon + departure status for a booking' }) bookingSchedule(@Param('bookingId', ParseUUIDPipe) bookingId: string) { return this.scheduling.getBookingSchedule(bookingId); } @Post('receive') @BookingStaff(FREIGHT_PERMS.warehouseInventory.receive) @ApiOperation({ summary: 'Receive inventory at a warehouse location' }) receive(@Body() dto: ReceiveWarehouseInventoryDto, @CurrentUser() user: TCurrentUser) { dto.performedBy = actorLabel(user) ?? dto.performedBy; return this.inventoryService.receive(dto); } @Post('reserve') @BookingStaff(FREIGHT_PERMS.warehouseInventory.move) @ApiOperation({ summary: 'Reserve stored inventory for a PAID booking' }) reserve(@Body() dto: ReserveInventoryDto, @CurrentUser() user: TCurrentUser) { dto.performedBy = actorLabel(user) ?? dto.performedBy; return this.inventoryService.reserve(dto); } @Get(':id/movements') @BookingStaff(FREIGHT_PERMS.warehouseInventory.view) @ApiOperation({ summary: 'Inventory movement history' }) movements(@Param('id', ParseUUIDPipe) id: string) { return this.inventoryService.findMovements(id); } @Get(':id/activity') @BookingStaff(FREIGHT_PERMS.warehouseInventory.view) @ApiOperation({ summary: 'Inventory activity log' }) activity(@Param('id', ParseUUIDPipe) id: string) { return this.inventoryService.findActivity(id); } @Get(':id/loadings') @BookingStaff(FREIGHT_PERMS.warehouseInventory.view) @ApiOperation({ summary: 'Loading records for an inventory item' }) loadings(@Param('id', ParseUUIDPipe) id: string) { return this.inventoryService.findLoadingsByInventory(id); } @Post(':id/move') @BookingStaff(FREIGHT_PERMS.warehouseInventory.move) @ApiOperation({ summary: 'Move inventory to another warehouse/yard/zone' }) move(@Param('id', ParseUUIDPipe) id: string, @Body() dto: MoveInventoryDto) { return this.inventoryService.move(id, dto); } @Post(':id/store') @BookingStaff(FREIGHT_PERMS.warehouseInventory.move) @ApiOperation({ summary: 'Mark received inventory as STORED (optional explicit warehouse/yard/zone)' }) store(@Param('id', ParseUUIDPipe) id: string, @Body() dto: StoreInventoryDto, @CurrentUser() user: TCurrentUser) { return this.inventoryService.store(id, actorLabel(user) ?? dto.performedBy, dto); } @Post(':id/ready-for-loading') @BookingStaff(FREIGHT_PERMS.warehouseInventory.move) @ApiOperation({ summary: 'Mark reserved inventory READY_FOR_LOADING' }) readyForLoading( @Param('id', ParseUUIDPipe) id: string, @Body('performedBy') performedBy: string | undefined, @CurrentUser() user: TCurrentUser, ) { return this.inventoryService.readyForLoading(id, actorLabel(user) ?? performedBy); } @Post(':id/load') @BookingStaff(FREIGHT_PERMS.warehouseInventory.load) @ApiOperation({ summary: 'Load READY_FOR_LOADING inventory onto a wagon' }) load(@Param('id', ParseUUIDPipe) id: string, @Body() dto: LoadInventoryDto) { return this.inventoryService.load(id, dto); } @Post(':id/ready-for-pickup') @BookingStaff(FREIGHT_PERMS.warehouseInventory.move) @ApiOperation({ summary: 'Mark inspected IMPORT inventory READY_FOR_PICKUP' }) readyForPickup( @Param('id', ParseUUIDPipe) id: string, @Body('performedBy') performedBy: string | undefined, @CurrentUser() user: TCurrentUser, ) { return this.inventoryService.readyForPickup(id, actorLabel(user) ?? performedBy); } @Post(':id/release') @BookingStaff(FREIGHT_PERMS.warehouseInventory.release) @ApiOperation({ summary: 'Issue a DO / release order for ready-for-pickup inventory' }) release(@Param('id', ParseUUIDPipe) id: string, @Body() dto: ReleaseOrderDto) { return this.inventoryService.release(id, dto); } @Get(':id/release-document') @BookingStaff(FREIGHT_PERMS.warehouseInventory.view) @ApiOperation({ summary: 'View warehouse release / exit paper PDF' }) async releaseDocument(@Param('id', ParseUUIDPipe) id: string, @Res() res: Response) { const { filename, buffer } = await this.inventoryService.releaseDocument(id); res.setHeader('Content-Type', 'application/pdf'); res.setHeader('Content-Disposition', `inline; filename="${filename}"`); res.setHeader('Content-Length', buffer.length); return res.send(buffer); } @Get('customer-truck-exit-paper/:assignmentId') @BookingStaff(FREIGHT_PERMS.warehouseInventory.view) @ApiOperation({ summary: 'Per-truck exit paper PDF (containers loaded on one customer truck)' }) async truckExitPaper( @Param('assignmentId', ParseUUIDPipe) assignmentId: string, @Res() res: Response, ) { const { filename, buffer } = await this.inventoryService.truckExitPaper(assignmentId); res.setHeader('Content-Type', 'application/pdf'); res.setHeader('Content-Disposition', `inline; filename="${filename}"`); res.setHeader('Content-Length', buffer.length); return res.send(buffer); } @Get('edr-truck-exit-paper/:assignmentId') @BookingStaff(FREIGHT_PERMS.warehouseInventory.view) @ApiOperation({ summary: 'Per-truck exit paper PDF for an EDR last-mile truck' }) async edrTruckExitPaper( @Param('assignmentId', ParseUUIDPipe) assignmentId: string, @Res() res: Response, ) { const { filename, buffer } = await this.inventoryService.edrTruckExitPaper(assignmentId); res.setHeader('Content-Type', 'application/pdf'); res.setHeader('Content-Disposition', `inline; filename="${filename}"`); res.setHeader('Content-Length', buffer.length); return res.send(buffer); } @Get(':id/grn-document') @BookingStaff(FREIGHT_PERMS.warehouseInventory.view) @ApiOperation({ summary: 'View goods received note PDF' }) async grnDocument(@Param('id', ParseUUIDPipe) id: string, @Res() res: Response) { const { filename, buffer } = await this.inventoryService.grnDocument(id); res.setHeader('Content-Type', 'application/pdf'); res.setHeader('Content-Disposition', `inline; filename="${filename}"`); res.setHeader('Content-Length', buffer.length); return res.send(buffer); } @Get(':id/handover-document') @ApiOperation({ summary: 'View import goods handover document PDF' }) async handoverDocument(@Param('id', ParseUUIDPipe) id: string, @Res() res: Response) { const { filename, buffer } = await this.inventoryService.handoverDocument(id); res.setHeader('Content-Type', 'application/pdf'); res.setHeader('Content-Disposition', `inline; filename="${filename}"`); res.setHeader('Content-Length', buffer.length); return res.send(buffer); } @Post('bookings/:bookingId/approve-delivery') @ApiOperation({ summary: "Approve delivery — customer records their full name (signature optional)" }) approveDeliveryForBooking( @Param('bookingId', ParseUUIDPipe) bookingId: string, @Body() dto: ApproveDeliveryDto, @Request() req: { user?: { id?: string; sub?: string } }, @CurrentUser() user: TCurrentUser, ) { return this.inventoryService.approveDeliveryForBooking( bookingId, user?.id ?? req.user?.id ?? req.user?.sub, dto.signerName, ); } @Get('bookings/:bookingId/handovers') @ApiOperation({ summary: 'Handover records for a booking (per-booking or per-truck)' }) bookingHandovers(@Param('bookingId', ParseUUIDPipe) bookingId: string) { return this.handoverService.list(bookingId); } @Post('handovers/:handoverId/sign') @ApiOperation({ summary: 'Customer signs one handover (EDR last-mile: one signature per truck)' }) signHandover( @Param('handoverId', ParseUUIDPipe) handoverId: string, @Body() dto: ApproveDeliveryDto, @Request() req: { user?: { id?: string; sub?: string } }, @CurrentUser() user: TCurrentUser, ) { return this.inventoryService.signHandover( handoverId, user?.id ?? req.user?.id ?? req.user?.sub, dto.signerName, ); } @Post('bookings/:bookingId/request-handover-signature') @ApiOperation({ summary: 'Ask the customer to sign the handover (creates one if none, then notifies)' }) requestHandoverSignature(@Param('bookingId', ParseUUIDPipe) bookingId: string) { return this.handoverService.requestSignature(bookingId); } @Get('bookings/:bookingId/grn-document') @ApiOperation({ summary: 'View GRN PDF for a booking (customer portal)' }) async bookingGrnDocument(@Param('bookingId', ParseUUIDPipe) bookingId: string, @Res() res: Response) { const { filename, buffer } = await this.inventoryService.grnDocumentForBooking(bookingId); res.setHeader('Content-Type', 'application/pdf'); res.setHeader('Content-Disposition', `inline; filename="${filename}"`); res.setHeader('Content-Length', buffer.length); return res.send(buffer); } @Get('bookings/:bookingId/release-document') @ApiOperation({ summary: 'View gate-clearance / release-order PDF for a booking (customer portal)' }) async bookingReleaseDocument(@Param('bookingId', ParseUUIDPipe) bookingId: string, @Res() res: Response) { const { filename, buffer } = await this.inventoryService.releaseDocumentForBooking(bookingId); res.setHeader('Content-Type', 'application/pdf'); res.setHeader('Content-Disposition', `inline; filename="${filename}"`); res.setHeader('Content-Length', buffer.length); return res.send(buffer); } @Get('bookings/:bookingId/handover-document') @ApiOperation({ summary: 'View import goods handover document PDF (resolved by booking; ?handoverId= for the per-truck variant)' }) async bookingHandoverDocument( @Param('bookingId', ParseUUIDPipe) bookingId: string, @Res() res: Response, @Query('handoverId') handoverId?: string, ) { const { filename, buffer } = await this.inventoryService.handoverDocumentForBooking( bookingId, handoverId || undefined, ); res.setHeader('Content-Type', 'application/pdf'); res.setHeader('Content-Disposition', `inline; filename="${filename}"`); res.setHeader('Content-Length', buffer.length); return res.send(buffer); } @Get('bookings/:bookingId/container-items') @ApiOperation({ summary: 'Per-container/bulk items of a booking with lifecycle stage + refs' }) containerItems(@Param('bookingId', ParseUUIDPipe) bookingId: string) { return this.inventoryService.containerItems(bookingId); } @Get('bookings/:bookingId/container-weights') @ApiOperation({ summary: "A booking's containers + VGM cargo weight (tonnes) for exit weighing" }) containerWeights(@Param('bookingId', ParseUUIDPipe) bookingId: string) { return this.inventoryService.bookingContainerWeights(bookingId); } @Get('bookings/:bookingId/location') @ApiOperation({ summary: "Warehouse location of a booking's inventory (customer portal)" }) bookingLocation(@Param('bookingId', ParseUUIDPipe) bookingId: string) { return this.inventoryService.bookingLocation(bookingId); } @Post(':id/deliver') @BookingStaff(FREIGHT_PERMS.warehouseInventory.deliver) @ApiOperation({ summary: 'Deliver import goods to the customer + capture proof of delivery' }) deliver(@Param('id', ParseUUIDPipe) id: string, @Body() dto: DeliverInventoryDto, @CurrentUser() user: TCurrentUser) { dto.performedBy = actorLabel(user) ?? dto.performedBy; return this.inventoryService.deliver(id, dto); } @Patch(':id/dispatch') @BookingStaff(FREIGHT_PERMS.warehouseInventory.dispatch) @ApiOperation({ summary: 'Mark loaded inventory DISPATCHED (left the terminal)' }) dispatch( @Param('id', ParseUUIDPipe) id: string, @Body('performedBy') performedBy: string | undefined, @CurrentUser() user: TCurrentUser, ) { return this.inventoryService.dispatch(id, actorLabel(user) ?? performedBy); } }