import { BadRequestException, Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, ParseUUIDPipe, Patch, Post, Query, UploadedFiles, UseInterceptors, } from '@nestjs/common'; 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 { CreateLastMileDto } from './dto/create-last-mile.dto'; import { UpdateLastMileDto } from './dto/update-last-mile.dto'; import { SetVehiclesDto } from './dto/set-vehicles.dto'; import { SetDistancesDto } from './dto/set-distances.dto'; import { RecordProofOfDeliveryDto } from './dto/record-proof-of-delivery.dto'; import { LastMileStatus } from './entities/last-mile.entity'; import { LastMileService } from './last-mile.service'; import { LastMileInvoiceService } from './last-mile-invoice.service'; @ApiTags('last-mile') @ApiBearerAuth() @Controller('last-mile') @BookingStaff(FREIGHT_PERMS.lastMile.view) export class LastMileController { constructor( private readonly lastMileService: LastMileService, private readonly lastMileInvoiceService: LastMileInvoiceService, ) {} @Get() @ApiOperation({ summary: 'List last-mile legs' }) findAll( @Query('status') status?: string, @Query('bookingId') bookingId?: string, @Query('vehicleId') vehicleId?: string, @Query('page') page?: string, @Query('pageSize') pageSize?: string, @Query('sortBy') sortBy?: string, @Query('sortOrder') sortOrder?: 'ASC' | 'DESC', ) { return this.lastMileService.findAll({ status: status as LastMileStatus | undefined, bookingId, vehicleId, page: page ? parseInt(page, 10) : undefined, pageSize: pageSize ? parseInt(pageSize, 10) : undefined, sortBy, sortOrder, }); } @Get(':id') @ApiOperation({ summary: 'Get a last-mile leg by ID' }) findOne(@Param('id', ParseUUIDPipe) id: string) { return this.lastMileService.findById(id); } @Post('accept/:reference') @BookingStaff(FREIGHT_PERMS.lastMile.accept) @ApiOperation({ summary: 'Accept a paid booking and create a last-mile leg' }) acceptBooking(@Param('reference') reference: string) { return this.lastMileService.acceptBookingByReference(reference); } @Post() @BookingStaff(FREIGHT_PERMS.lastMile.create) @ApiOperation({ summary: 'Create a last-mile leg' }) create(@Body() dto: CreateLastMileDto) { return this.lastMileService.create(dto); } @Patch(':id') @BookingStaff(FREIGHT_PERMS.lastMile.update) @ApiOperation({ summary: 'Update a last-mile leg' }) async update(@Param('id', ParseUUIDPipe) id: string, @Body() dto: UpdateLastMileDto) { // No invoice side-effects here — invoices are generated only via the // explicit POST :id/invoice endpoint (the "Generate Invoice" action). return this.lastMileService.update(id, dto); } @Delete(':id') @BookingStaff(FREIGHT_PERMS.lastMile.delete) @HttpCode(HttpStatus.NO_CONTENT) @ApiOperation({ summary: 'Soft-delete a last-mile leg' }) remove(@Param('id', ParseUUIDPipe) id: string) { return this.lastMileService.remove(id); } @Post(':id/vehicles') @BookingStaff(FREIGHT_PERMS.lastMile.assignVehicles) @ApiOperation({ summary: 'Set the vehicles assigned to a last-mile delivery (multi-truck)' }) async setVehicles( @Param('id', ParseUUIDPipe) id: string, @Body() dto: SetVehiclesDto, ) { return this.lastMileService.setVehicles(id, dto.vehicles); } @Post(':id/distances') @BookingStaff(FREIGHT_PERMS.lastMile.setDistances) @ApiOperation({ summary: 'Set per-vehicle actual distances (does not generate an invoice)' }) async setDistances( @Param('id', ParseUUIDPipe) id: string, @Body() dto: SetDistancesDto, ) { return this.lastMileService.setDistances(id, dto.distances, dto.remainingPayment); } @Post(':id/proof-of-delivery') @BookingStaff(FREIGHT_PERMS.lastMile.update) @UseInterceptors(AnyFilesInterceptor()) @ApiConsumes('multipart/form-data') @ApiOperation({ summary: 'Record proof of delivery (signature + photos) and complete the leg' }) async recordProofOfDelivery( @Param('id', ParseUUIDPipe) id: string, @Body() dto: RecordProofOfDeliveryDto, @UploadedFiles() files: Express.Multer.File[], ) { return this.lastMileService.recordProofOfDelivery(id, dto, files ?? []); } @Post(':id/invoice') @BookingStaff(FREIGHT_PERMS.lastMile.generateInvoice) @ApiOperation({ summary: 'Generate the delivery-fee invoice for a last-mile leg' }) async generateInvoice(@Param('id', ParseUUIDPipe) id: string) { const record = await this.lastMileService.findById(id); const invoice = await this.lastMileInvoiceService.ensureInvoiceFor(record); if (!invoice) { throw new BadRequestException( 'Cannot generate invoice: the leg has no billable amount. Add distance and ensure a LAST_MILE rate is configured, and the booking has a company.', ); } return invoice; } }