diff --git a/apps/edr-freight-api/src/modules/warehouses/dto/invoice.dto.ts b/apps/edr-freight-api/src/modules/warehouses/dto/invoice.dto.ts new file mode 100644 index 000000000..9d25c974a --- /dev/null +++ b/apps/edr-freight-api/src/modules/warehouses/dto/invoice.dto.ts @@ -0,0 +1,31 @@ +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { IsBoolean, IsNumber, IsOptional, IsString, Min } from 'class-validator'; + +export class GenerateInvoiceDto { + @ApiPropertyOptional({ description: 'Create even when the calculated amount is zero.' }) + @IsOptional() + @IsBoolean() + confirmZero?: boolean; + + @ApiPropertyOptional() + @IsOptional() + @IsString() + performedBy?: string; +} + +export class PayInvoiceBodyDto { + @ApiPropertyOptional() + @IsNumber() + @Min(0.01) + amount!: number; + + @ApiPropertyOptional() + @IsOptional() + @IsString() + method?: string; + + @ApiPropertyOptional() + @IsOptional() + @IsString() + reference?: string; +} 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 c4f40c1f3..ce8a2c188 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 @@ -65,6 +65,12 @@ export class WarehouseInventoryController { return this.inventoryService.unloadBooking(bookingId, dto); } + @Post(':id/gate-clearance') + @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('loadable-wagons') @ApiOperation({ summary: 'List wagons usable for loading (read-only from scheduling)' }) loadableWagons() { diff --git a/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.service.ts b/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.service.ts index c150747d9..652be600c 100644 --- a/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.service.ts +++ b/apps/edr-freight-api/src/modules/warehouses/warehouse-inventory.service.ts @@ -122,8 +122,37 @@ export class WarehouseInventoryService { private readonly activityLog: WarehouseActivityLogService, private readonly scheduling: SchedulingReadFacade, private readonly allocation: WarehouseAllocationService, + private readonly invoices: WarehouseInvoiceService, ) {} + /** + * Batch 6 — final terminal release / gate clearance. + * Blocked while an unpaid demurrage/storage invoice exists. Does NOT touch + * inspection / storage / loading steps — only the final release. + */ + async gateClearance(id: string, performedBy?: string): Promise { + const item = await this.findById(id); + const blocking = await this.invoices.findBlockingInvoice(id); + if (blocking) { + throw new BadRequestException( + 'Warehouse demurrage/storage fee must be paid before terminal release.', + ); + } + const now = new Date(); + await this.inventoryRepository.update(id, { + gateClearedAt: now, + releaseDate: item.releaseDate ?? now, + }); + await this.activityLog.record({ + activityType: 'INVENTORY_DISPATCHED', + inventoryId: id, + warehouseId: item.warehouseId, + description: 'Gate clearance / terminal release', + performedBy, + }); + return this.findById(id); + } + // ── Listing ──────────────────────────────────────────────────────────── findAll(filter: FilterWarehouseInventoryDto): Promise { 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 new file mode 100644 index 000000000..da818b5a3 --- /dev/null +++ b/apps/edr-freight-api/src/modules/warehouses/warehouse-invoice.controller.ts @@ -0,0 +1,68 @@ +import { Body, Controller, Get, Param, ParseUUIDPipe, Patch, Post, Query } from '@nestjs/common'; +import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger'; + +import { GenerateInvoiceDto, PayInvoiceBodyDto } from './dto/invoice.dto'; +import { WarehouseInvoiceService } from './warehouse-invoice.service'; + +@ApiTags('warehouse-fee-invoices') +@ApiBearerAuth() +@Controller() +export class WarehouseInvoiceController { + constructor(private readonly invoiceService: WarehouseInvoiceService) {} + + @Post('warehouse-inventory/:id/generate-fee-invoice') + @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); + } + + @Get('warehouse-inventory/:id/fee-invoices') + @ApiOperation({ summary: 'List fee invoices for an inventory item' }) + listForInventory(@Param('id', ParseUUIDPipe) id: string) { + return this.invoiceService.listForInventory(id); + } + + @Get('bookings/:id/warehouse-fee-invoices') + @ApiOperation({ summary: 'List warehouse fee invoices for a booking' }) + listForBooking(@Param('id', ParseUUIDPipe) id: string) { + return this.invoiceService.listForBooking(id); + } + + @Get('warehouse-fee-invoices') + @ApiOperation({ summary: 'List / filter warehouse fee invoices' }) + findAll( + @Query('status') status?: string, + @Query('invoiceType') invoiceType?: string, + @Query('warehouseId') warehouseId?: string, + @Query('facilityId') facilityId?: string, + @Query('customerId') customerId?: string, + @Query('bookingId') bookingId?: string, + ) { + return this.invoiceService.findAll({ + status: status as never, + invoiceType: invoiceType as never, + warehouseId, + facilityId, + customerId, + bookingId, + }); + } + + @Get('warehouse-fee-invoices/:id') + @ApiOperation({ summary: 'Get a warehouse fee invoice with items + payment history' }) + findOne(@Param('id', ParseUUIDPipe) id: string) { + return this.invoiceService.findById(id); + } + + @Patch('warehouse-fee-invoices/:id/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') + @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/warehouses.module.ts b/apps/edr-freight-api/src/modules/warehouses/warehouses.module.ts index ba62772c1..60ecc260a 100644 --- a/apps/edr-freight-api/src/modules/warehouses/warehouses.module.ts +++ b/apps/edr-freight-api/src/modules/warehouses/warehouses.module.ts @@ -4,6 +4,8 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { FilesModule } from '../files/files.module'; import { WarehouseActivityLog } from './entities/warehouse-activity-log.entity'; import { WarehouseAllocationRule } from './entities/warehouse-allocation-rule.entity'; +import { WarehouseFeeInvoice } from './entities/warehouse-fee-invoice.entity'; +import { WarehouseFeeInvoiceItem } from './entities/warehouse-fee-invoice-item.entity'; import { WarehouseFeeRule } from './entities/warehouse-fee-rule.entity'; import { WarehouseInspectionReport } from './entities/warehouse-inspection-report.entity'; import { WarehouseInventory } from './entities/warehouse-inventory.entity'; @@ -29,6 +31,10 @@ import { WarehouseAllocationRuleRepository } from './warehouse-allocation-rule.r import { WarehouseAllocationService } from './warehouse-allocation.service'; import { WarehouseFeeRuleRepository } from './warehouse-fee-rule.repository'; import { WarehouseFeeService } from './warehouse-fee.service'; +import { WarehouseFeeInvoiceItemRepository } from './warehouse-fee-invoice-item.repository'; +import { WarehouseFeeInvoiceRepository } from './warehouse-fee-invoice.repository'; +import { WarehouseInvoiceController } from './warehouse-invoice.controller'; +import { WarehouseInvoiceService } from './warehouse-invoice.service'; import { WarehouseRulesController } from './warehouse-rules.controller'; import { WarehouseSchedulingAdapterService } from './warehouse-scheduling-adapter.service'; import { WarehouseYardsController } from './warehouse-yards.controller'; @@ -54,6 +60,8 @@ import { WarehousesService } from './warehouses.service'; WarehouseInspectionReport, WarehouseAllocationRule, WarehouseFeeRule, + WarehouseFeeInvoice, + WarehouseFeeInvoiceItem, ]), FilesModule, ], @@ -65,6 +73,7 @@ import { WarehousesService } from './warehouses.service'; WarehouseLoadingsController, WarehouseInspectionController, WarehouseRulesController, + WarehouseInvoiceController, ], providers: [ WarehousesRepository,