diff --git a/apps/edr-freight-api/src/modules/warehouses/warehouse-inspection.controller.ts b/apps/edr-freight-api/src/modules/warehouses/warehouse-inspection.controller.ts index 533b0c6ae..7bd56e593 100644 --- a/apps/edr-freight-api/src/modules/warehouses/warehouse-inspection.controller.ts +++ b/apps/edr-freight-api/src/modules/warehouses/warehouse-inspection.controller.ts @@ -12,6 +12,8 @@ import { import { AnyFilesInterceptor } from '@nestjs/platform-express'; import { ApiBearerAuth, ApiConsumes, ApiOperation, ApiTags } from '@nestjs/swagger'; +import { BookingStaff } from '../../common/booking-guards'; +import { FREIGHT_PERMS } from '../../seed/freight-permissions.registry'; import { CreateInspectionReportDto } from './dto/create-inspection-report.dto'; import { UpdateInspectionReportDto } from './dto/update-inspection-report.dto'; import { WarehouseInspectionService } from './warehouse-inspection.service'; @@ -19,10 +21,12 @@ import { WarehouseInspectionService } from './warehouse-inspection.service'; @ApiTags('warehouse-inspection') @ApiBearerAuth() @Controller() +@BookingStaff(FREIGHT_PERMS.warehouseInspectionReports.view) export class WarehouseInspectionController { constructor(private readonly inspectionService: WarehouseInspectionService) {} @Post('warehouse-inventory/:inventoryId/inspection-reports') + @BookingStaff(FREIGHT_PERMS.warehouseInspectionReports.create) @ApiOperation({ summary: 'Create an inspection / damage report for an inventory item' }) create( @Param('inventoryId', ParseUUIDPipe) inventoryId: string, @@ -46,12 +50,14 @@ export class WarehouseInspectionController { } @Patch('warehouse-inspection-reports/:id') + @BookingStaff(FREIGHT_PERMS.warehouseInspectionReports.update) @ApiOperation({ summary: 'Update an inspection report' }) update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateInspectionReportDto) { return this.inspectionService.update(id, dto); } @Post('warehouse-inspection-reports/:id/attachments') + @BookingStaff(FREIGHT_PERMS.warehouseInspectionReports.update) @UseInterceptors(AnyFilesInterceptor()) @ApiConsumes('multipart/form-data') @ApiOperation({ summary: 'Upload inspection images / documents' }) diff --git a/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.controller.ts b/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.controller.ts index 055c9bac9..8b14e6cdc 100644 --- a/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.controller.ts +++ b/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.controller.ts @@ -2,6 +2,8 @@ import { Body, Controller, Get, Param, ParseUUIDPipe, Patch, Post, Query, Reques import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; import type { Response } from 'express'; +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'; @@ -30,54 +32,63 @@ export class WarehouseInventoryController { ) {} @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('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); } @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; @@ -85,6 +96,7 @@ export class WarehouseInventoryController { } @Post('receive-bulk') + @BookingStaff(FREIGHT_PERMS.warehouseInventory.receive) @ApiOperation({ summary: 'Bulk-receive selected eligible PAID bookings into a location' }) receiveBulk(@Body() dto: BulkReceiveDto) { return this.inventoryService.bulkReceive(dto); @@ -92,36 +104,42 @@ export class WarehouseInventoryController { @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, @@ -131,18 +149,21 @@ export class WarehouseInventoryController { } @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 }) { return this.inventoryService.bulkDispatchExport(dto.inventoryIds ?? [], 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, @@ -152,24 +173,28 @@ export class WarehouseInventoryController { } @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) { return this.inventoryService.gateClearance(id, 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; @@ -186,12 +211,14 @@ export class WarehouseInventoryController { } @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, @@ -210,102 +237,119 @@ export class WarehouseInventoryController { } @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 }) { return this.inventoryService.autoUnloadExportAtDjibouti(dto.scheduleId, 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) { 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) { 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) { return this.inventoryService.store(id, 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) { return this.inventoryService.readyForLoading(id, 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) { return this.inventoryService.readyForPickup(id, 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); @@ -316,6 +360,7 @@ export class WarehouseInventoryController { } @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, @@ -329,6 +374,7 @@ export class WarehouseInventoryController { } @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); @@ -417,12 +463,14 @@ export class WarehouseInventoryController { } @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) { 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) { return this.inventoryService.dispatch(id, performedBy); diff --git a/apps/edr-freight-api/src/modules/warehouses/warehouse-invoice.controller.ts b/apps/edr-freight-api/src/modules/warehouses/warehouse-invoice.controller.ts index a1c5db837..635ca1a10 100644 --- a/apps/edr-freight-api/src/modules/warehouses/warehouse-invoice.controller.ts +++ b/apps/edr-freight-api/src/modules/warehouses/warehouse-invoice.controller.ts @@ -2,6 +2,8 @@ import { Body, Controller, Get, Param, ParseUUIDPipe, Patch, Post, Query, Res } import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; import type { Response } from 'express'; +import { BookingStaff } from '../../common/booking-guards'; +import { FREIGHT_PERMS } from '../../seed/freight-permissions.registry'; import { PayInvoiceDto as GatewayPayInvoiceDto } from '../billing/dto/pay-invoice.dto'; import { GenerateInvoiceDto, PayInvoiceBodyDto } from './dto/invoice.dto'; import { WarehouseInvoiceService } from './warehouse-invoice.service'; @@ -13,12 +15,14 @@ export class WarehouseInvoiceController { constructor(private readonly invoiceService: WarehouseInvoiceService) {} @Post('warehouse-inventory/:id/generate-fee-invoice') + @BookingStaff(FREIGHT_PERMS.warehouseFeeInvoices.generate) @ApiOperation({ summary: 'Generate a warehouse fee invoice from Batch 5 fee calculation' }) generate(@Param('id', ParseUUIDPipe) id: string, @Body() dto: GenerateInvoiceDto) { return this.invoiceService.generateForInventory(id, dto); } @Post('last-mile/:id/generate-truck-detention-invoice') + @BookingStaff(FREIGHT_PERMS.warehouseFeeInvoices.generate) @ApiOperation({ summary: 'Generate a truck-detention invoice for a last-mile leg (per truck per day)' }) generateTruckDetention( @Param('id', ParseUUIDPipe) id: string, @@ -28,6 +32,7 @@ export class WarehouseInvoiceController { } @Get('warehouse-inventory/:id/fee-invoices') + @BookingStaff(FREIGHT_PERMS.warehouseFeeInvoices.view) @ApiOperation({ summary: 'List fee invoices for an inventory item' }) listForInventory(@Param('id', ParseUUIDPipe) id: string) { return this.invoiceService.listForInventory(id); @@ -40,6 +45,7 @@ export class WarehouseInvoiceController { } @Get('warehouse-fee-invoices') + @BookingStaff(FREIGHT_PERMS.warehouseFeeInvoices.view) @ApiOperation({ summary: 'List / filter warehouse fee invoices' }) findAll( @Query('status') status?: string, @@ -86,12 +92,14 @@ export class WarehouseInvoiceController { } @Patch('warehouse-fee-invoices/:id/cancel') + @BookingStaff(FREIGHT_PERMS.warehouseFeeInvoices.cancel) @ApiOperation({ summary: 'Cancel a warehouse fee invoice' }) cancel(@Param('id', ParseUUIDPipe) id: string) { return this.invoiceService.cancel(id); } @Post('warehouse-fee-invoices/:id/pay') + @BookingStaff(FREIGHT_PERMS.warehouseFeeInvoices.pay) @ApiOperation({ summary: 'Record a payment against a warehouse fee invoice' }) pay(@Param('id', ParseUUIDPipe) id: string, @Body() dto: PayInvoiceBodyDto) { return this.invoiceService.pay(id, dto); diff --git a/apps/edr-freight-api/src/modules/warehouses/warehouse-loadings.controller.ts b/apps/edr-freight-api/src/modules/warehouses/warehouse-loadings.controller.ts index aab8e18b2..32860082e 100644 --- a/apps/edr-freight-api/src/modules/warehouses/warehouse-loadings.controller.ts +++ b/apps/edr-freight-api/src/modules/warehouses/warehouse-loadings.controller.ts @@ -1,11 +1,14 @@ import { Controller, Get, 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 { WarehouseInventoryService } from './warehouse-inventory.service'; @ApiTags('warehouse-loadings') @ApiBearerAuth() @Controller('warehouse-loadings') +@BookingStaff(FREIGHT_PERMS.warehouseInventory.view) export class WarehouseLoadingsController { constructor(private readonly inventoryService: WarehouseInventoryService) {} diff --git a/apps/edr-freight-api/src/modules/warehouses/warehouse-rules.controller.ts b/apps/edr-freight-api/src/modules/warehouses/warehouse-rules.controller.ts index 19788e027..7a5c53d38 100644 --- a/apps/edr-freight-api/src/modules/warehouses/warehouse-rules.controller.ts +++ b/apps/edr-freight-api/src/modules/warehouses/warehouse-rules.controller.ts @@ -1,6 +1,8 @@ import { Body, Controller, Delete, Get, HttpCode, 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 { AllocationPreviewDto, CreateAllocationRuleDto, @@ -22,18 +24,21 @@ export class WarehouseRulesController { // ── Allocation rules ─────────────────────────────────────────────────────── @Get('warehouse-allocation-rules') + @BookingStaff(FREIGHT_PERMS.warehouseAllocationRules.view) @ApiOperation({ summary: 'List warehouse allocation rules' }) listAllocationRules() { return this.allocationService.listRules(); } @Post('warehouse-allocation-rules') + @BookingStaff(FREIGHT_PERMS.warehouseAllocationRules.create) @ApiOperation({ summary: 'Create a warehouse allocation rule' }) createAllocationRule(@Body() dto: CreateAllocationRuleDto) { return this.allocationService.createRule(dto); } @Patch('warehouse-allocation-rules/:id') + @BookingStaff(FREIGHT_PERMS.warehouseAllocationRules.update) @ApiOperation({ summary: 'Update a warehouse allocation rule' }) updateAllocationRule(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateAllocationRuleDto) { return this.allocationService.updateRule(id, dto); @@ -41,12 +46,14 @@ export class WarehouseRulesController { @Delete('warehouse-allocation-rules/:id') @HttpCode(204) + @BookingStaff(FREIGHT_PERMS.warehouseAllocationRules.delete) @ApiOperation({ summary: 'Delete a warehouse allocation rule' }) deleteAllocationRule(@Param('id', ParseUUIDPipe) id: string) { return this.allocationService.deleteRule(id); } @Post('warehouse-allocation/preview') + @BookingStaff(FREIGHT_PERMS.warehouseAllocationRules.view) @ApiOperation({ summary: 'Preview the yard/warehouse/zone a booking would be allocated to' }) previewAllocation(@Body() dto: AllocationPreviewDto) { return this.allocationService.resolveLocation(dto); @@ -54,18 +61,21 @@ export class WarehouseRulesController { // ── Fee rules ──────────────────────────────────────────────────────────────── @Get('warehouse-fee-rules') + @BookingStaff(FREIGHT_PERMS.warehouseFeeRules.view) @ApiOperation({ summary: 'List storage / demurrage fee rules' }) listFeeRules() { return this.feeService.listRules(); } @Post('warehouse-fee-rules') + @BookingStaff(FREIGHT_PERMS.warehouseFeeRules.create) @ApiOperation({ summary: 'Create a storage / demurrage fee rule' }) createFeeRule(@Body() dto: CreateFeeRuleDto) { return this.feeService.createRule(dto); } @Patch('warehouse-fee-rules/:id') + @BookingStaff(FREIGHT_PERMS.warehouseFeeRules.update) @ApiOperation({ summary: 'Update a fee rule' }) updateFeeRule(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateFeeRuleDto) { return this.feeService.updateRule(id, dto); @@ -73,18 +83,21 @@ export class WarehouseRulesController { @Delete('warehouse-fee-rules/:id') @HttpCode(204) + @BookingStaff(FREIGHT_PERMS.warehouseFeeRules.delete) @ApiOperation({ summary: 'Delete a fee rule' }) deleteFeeRule(@Param('id', ParseUUIDPipe) id: string) { return this.feeService.deleteRule(id); } @Get('warehouse-fees/accrual-dashboard') + @BookingStaff(FREIGHT_PERMS.warehouseFeeRules.view) @ApiOperation({ summary: 'Live per-item fee accrual (storage/demurrage) with alerts' }) accrualDashboard(@Query('billingCurrency') billingCurrency?: string) { return this.feeService.accrualDashboard(billingCurrency); } @Post('warehouse-fees/accrual/:inventoryId/acknowledge') + @BookingStaff(FREIGHT_PERMS.warehouseFeeRules.update) @ApiOperation({ summary: 'Acknowledge / snooze an item fee-accrual alert' }) acknowledgeAccrual( @Param('inventoryId', ParseUUIDPipe) inventoryId: string, @@ -98,12 +111,14 @@ export class WarehouseRulesController { @Delete('warehouse-fees/accrual/:inventoryId/acknowledge') @HttpCode(204) + @BookingStaff(FREIGHT_PERMS.warehouseFeeRules.update) @ApiOperation({ summary: 'Remove an accrual acknowledgement (re-surface for alerts)' }) unacknowledgeAccrual(@Param('inventoryId', ParseUUIDPipe) inventoryId: string) { return this.feeService.unacknowledgeAccrual(inventoryId); } @Get('warehouse-inventory/:id/fee-preview') + @BookingStaff(FREIGHT_PERMS.warehouseFeeRules.view) @ApiOperation({ summary: 'Preview demurrage + storage fees for an inventory item' }) feePreview( @Param('id', ParseUUIDPipe) id: string, @@ -113,6 +128,7 @@ export class WarehouseRulesController { } @Get('last-mile/:id/truck-detention-preview') + @BookingStaff(FREIGHT_PERMS.warehouseFeeRules.view) @ApiOperation({ summary: 'Preview truck detention for a last-mile leg (per truck per day after grace)' }) truckDetentionPreview( @Param('id', ParseUUIDPipe) id: string, diff --git a/apps/edr-freight-api/src/modules/warehouses/warehouse-yards.controller.ts b/apps/edr-freight-api/src/modules/warehouses/warehouse-yards.controller.ts index c14cea7a4..fdfbc36be 100644 --- a/apps/edr-freight-api/src/modules/warehouses/warehouse-yards.controller.ts +++ b/apps/edr-freight-api/src/modules/warehouses/warehouse-yards.controller.ts @@ -1,6 +1,8 @@ import { Body, Controller, Get, Param, ParseUUIDPipe, Patch, Post } 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 { CreateWarehouseZoneDto } from './dto/create-warehouse-zone.dto'; import { UpdateWarehouseYardDto } from './dto/update-warehouse-yard.dto'; import { WarehouseYardsService } from './warehouse-yards.service'; @@ -9,6 +11,7 @@ import { WarehouseZonesService } from './warehouse-zones.service'; @ApiTags('warehouse-yards') @ApiBearerAuth() @Controller('warehouse-yards') +@BookingStaff(FREIGHT_PERMS.warehouseYards.view) export class WarehouseYardsController { constructor( private readonly yardsService: WarehouseYardsService, @@ -28,18 +31,21 @@ export class WarehouseYardsController { } @Patch(':id') + @BookingStaff(FREIGHT_PERMS.warehouseYards.update) @ApiOperation({ summary: 'Update warehouse yard' }) update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateWarehouseYardDto) { return this.yardsService.update(id, dto); } @Get(':yardId/zones') + @BookingStaff(FREIGHT_PERMS.warehouseZones.view) @ApiOperation({ summary: 'List zones within a yard' }) listZones(@Param('yardId', ParseUUIDPipe) yardId: string) { return this.zonesService.findByYard(yardId); } @Post(':yardId/zones') + @BookingStaff(FREIGHT_PERMS.warehouseZones.create) @ApiOperation({ summary: 'Create a zone within a yard' }) createZone( @Param('yardId', ParseUUIDPipe) yardId: string, diff --git a/apps/edr-freight-api/src/modules/warehouses/warehouse-zones.controller.ts b/apps/edr-freight-api/src/modules/warehouses/warehouse-zones.controller.ts index 7d51feac3..b0371cbcc 100644 --- a/apps/edr-freight-api/src/modules/warehouses/warehouse-zones.controller.ts +++ b/apps/edr-freight-api/src/modules/warehouses/warehouse-zones.controller.ts @@ -1,12 +1,15 @@ import { Body, Controller, 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() @Controller('warehouse-zones') +@BookingStaff(FREIGHT_PERMS.warehouseZones.view) export class WarehouseZonesController { constructor(private readonly zonesService: WarehouseZonesService) {} @@ -23,6 +26,7 @@ export class WarehouseZonesController { } @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); diff --git a/apps/edr-freight-api/src/modules/warehouses/warehouses.controller.ts b/apps/edr-freight-api/src/modules/warehouses/warehouses.controller.ts index bb7702603..63c40de94 100644 --- a/apps/edr-freight-api/src/modules/warehouses/warehouses.controller.ts +++ b/apps/edr-freight-api/src/modules/warehouses/warehouses.controller.ts @@ -1,6 +1,8 @@ import { Body, Controller, 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 { CreateWarehouseDto } from './dto/create-warehouse.dto'; import { CreateWarehouseYardDto } from './dto/create-warehouse-yard.dto'; import { FilterWarehouseDto } from './dto/filter-warehouse.dto'; @@ -12,6 +14,7 @@ import { WarehousesService } from './warehouses.service'; @ApiTags('warehouses') @ApiBearerAuth() @Controller('warehouses') +@BookingStaff(FREIGHT_PERMS.warehouses.view) export class WarehousesController { constructor( private readonly warehousesService: WarehousesService, @@ -26,12 +29,14 @@ export class WarehousesController { } @Get('dashboard') + @BookingStaff(FREIGHT_PERMS.warehouseDashboard.view) @ApiOperation({ summary: 'Warehouse dashboard metrics' }) dashboard() { return this.dashboardService.getDashboard(); } @Post() + @BookingStaff(FREIGHT_PERMS.warehouses.create) @ApiOperation({ summary: 'Create warehouse' }) create(@Body() dto: CreateWarehouseDto) { return this.warehousesService.create(dto); @@ -44,18 +49,21 @@ export class WarehousesController { } @Patch(':id') + @BookingStaff(FREIGHT_PERMS.warehouses.update) @ApiOperation({ summary: 'Update warehouse' }) update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateWarehouseDto) { return this.warehousesService.update(id, dto); } @Get(':warehouseId/yards') + @BookingStaff(FREIGHT_PERMS.warehouseYards.view) @ApiOperation({ summary: 'List yards within a warehouse' }) listYards(@Param('warehouseId', ParseUUIDPipe) warehouseId: string) { return this.yardsService.findByWarehouse(warehouseId); } @Post(':warehouseId/yards') + @BookingStaff(FREIGHT_PERMS.warehouseYards.create) @ApiOperation({ summary: 'Create a yard within a warehouse' }) createYard( @Param('warehouseId', ParseUUIDPipe) warehouseId: string,