mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
Empty container returns now generate a downloadable Equipment Interchange Receipt: container number & size, exact return timestamp, depot/location, condition, and the carrier/booking reference. Customers can download it from their booking's Documents tab (own-booking returns only); staff can pull any via the same endpoint.
196 lines
7.7 KiB
TypeScript
196 lines
7.7 KiB
TypeScript
import { Body, Controller, Get, NotFoundException, Param, ParseUUIDPipe, Post, Query, 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 { BookingStaff, MixedAudience } from '../../common/booking-guards';
|
||
import { hasFreightPermission } from '../../common/freight-permission.util';
|
||
import { FREIGHT_PERMS } from '../../seed/freight-permissions.registry';
|
||
import { BookingsService } from '../bookings/bookings.service';
|
||
import {
|
||
AssignCustomsRiskDto,
|
||
CreateDjiboutiIncidentDto,
|
||
CreateEmptyContainerReturnDto,
|
||
ImportOperationActionDto,
|
||
LoadEmptyContainersOnTrainDto,
|
||
RecordDeclarationDto,
|
||
UpdateEmptyContainerReturnStatusDto,
|
||
UploadImportCustomsDocumentDto,
|
||
} from './dto/import-operations.dto';
|
||
import { ImportOperationsService } from './import-operations.service';
|
||
|
||
@ApiTags('import-operations')
|
||
@ApiBearerAuth()
|
||
@Controller('import-operations')
|
||
// Post-booking customs / import-operations actions are GL/Ops work, mirroring the
|
||
// contracts controller's GL operational endpoints (risk, duty, milestones). No
|
||
// class-level guard: the equipment interchange receipt below is customer-reachable,
|
||
// every other route here stays staff-only via its own @BookingStaff.
|
||
export class ImportOperationsController {
|
||
constructor(
|
||
private readonly service: ImportOperationsService,
|
||
private readonly bookingsService: BookingsService,
|
||
) {}
|
||
|
||
@Get('djibouti-incidents')
|
||
@BookingStaff(FREIGHT_PERMS.bookings.operations)
|
||
@ApiOperation({ summary: 'Batch 8: list Djibouti import incidents' })
|
||
listIncidents(@Query('bookingId') bookingId?: string) {
|
||
return this.service.listIncidents(bookingId);
|
||
}
|
||
|
||
@Post('djibouti-incidents')
|
||
@BookingStaff(FREIGHT_PERMS.bookings.operations)
|
||
@ApiOperation({ summary: 'Batch 8: report a Djibouti import incident / exception' })
|
||
createIncident(@Body() dto: CreateDjiboutiIncidentDto) {
|
||
return this.service.createIncident(dto);
|
||
}
|
||
|
||
@Get('customs/:bookingId')
|
||
@BookingStaff(FREIGHT_PERMS.bookings.operations)
|
||
@ApiOperation({ summary: 'Batch 12: import customs finalization state' })
|
||
getCustoms(@Param('bookingId', ParseUUIDPipe) bookingId: string) {
|
||
return this.service.getCustoms(bookingId);
|
||
}
|
||
|
||
@Post('customs/:bookingId/documents')
|
||
@BookingStaff(FREIGHT_PERMS.bookings.operations)
|
||
@ApiOperation({ summary: 'Batch 12: upload IM4/IM5/T1/permit/payment-slip documents' })
|
||
uploadCustomsDocument(
|
||
@Param('bookingId', ParseUUIDPipe) bookingId: string,
|
||
@Body() dto: UploadImportCustomsDocumentDto,
|
||
) {
|
||
return this.service.uploadCustomsDocument(bookingId, dto);
|
||
}
|
||
|
||
@Post('customs/:bookingId/declaration')
|
||
@BookingStaff(FREIGHT_PERMS.bookings.operations)
|
||
@ApiOperation({ summary: 'Batch 12: record declaration serial number' })
|
||
recordDeclaration(
|
||
@Param('bookingId', ParseUUIDPipe) bookingId: string,
|
||
@Body() dto: RecordDeclarationDto,
|
||
) {
|
||
return this.service.recordDeclaration(bookingId, dto);
|
||
}
|
||
|
||
@Post('customs/:bookingId/notify-duties-taxes')
|
||
@BookingStaff(FREIGHT_PERMS.bookings.operations)
|
||
@ApiOperation({ summary: 'Batch 12: notify duties and taxes' })
|
||
notifyDutiesTaxes(
|
||
@Param('bookingId', ParseUUIDPipe) bookingId: string,
|
||
@Body() dto: ImportOperationActionDto,
|
||
) {
|
||
return this.service.notifyDutiesTaxes(bookingId, dto);
|
||
}
|
||
|
||
@Post('customs/:bookingId/duties-taxes-paid')
|
||
@BookingStaff(FREIGHT_PERMS.bookings.operations)
|
||
@ApiOperation({ summary: 'Batch 12: mark duties and taxes paid' })
|
||
markDutiesTaxesPaid(
|
||
@Param('bookingId', ParseUUIDPipe) bookingId: string,
|
||
@Body() dto: ImportOperationActionDto,
|
||
) {
|
||
return this.service.markDutiesTaxesPaid(bookingId, dto);
|
||
}
|
||
|
||
@Post('customs/:bookingId/risk')
|
||
@BookingStaff(FREIGHT_PERMS.bookings.operations)
|
||
@ApiOperation({ summary: 'Batch 12: assign customs risk' })
|
||
assignRisk(@Param('bookingId', ParseUUIDPipe) bookingId: string, @Body() dto: AssignCustomsRiskDto) {
|
||
return this.service.assignRisk(bookingId, dto);
|
||
}
|
||
|
||
@Post('customs/:bookingId/release-permitted')
|
||
@BookingStaff(FREIGHT_PERMS.bookings.operations)
|
||
@ApiOperation({ summary: 'Batch 12: mark import release permitted' })
|
||
markReleasePermitted(
|
||
@Param('bookingId', ParseUUIDPipe) bookingId: string,
|
||
@Body() dto: ImportOperationActionDto,
|
||
) {
|
||
return this.service.markReleasePermitted(bookingId, dto);
|
||
}
|
||
|
||
@Get('empty-container-returns')
|
||
@BookingStaff(FREIGHT_PERMS.bookings.operations)
|
||
@ApiOperation({ summary: 'Batch 16: list empty container returns' })
|
||
listEmptyReturns() {
|
||
return this.service.listEmptyReturns();
|
||
}
|
||
|
||
@Post('empty-container-returns')
|
||
@BookingStaff(FREIGHT_PERMS.bookings.operations)
|
||
@ApiOperation({ summary: 'Batch 16: create an empty container return record' })
|
||
createEmptyReturn(@Body() dto: CreateEmptyContainerReturnDto) {
|
||
return this.service.createEmptyReturn(dto);
|
||
}
|
||
|
||
@Post('empty-container-returns/load-on-train')
|
||
@BookingStaff(FREIGHT_PERMS.bookings.operations)
|
||
@ApiOperation({
|
||
summary: 'Load returned empties onto an export train (1×40ft or 2×20ft per wagon)',
|
||
})
|
||
loadEmptyReturnsOnTrain(@Body() dto: LoadEmptyContainersOnTrainDto) {
|
||
return this.service.loadEmptyReturnsOnTrain(dto);
|
||
}
|
||
|
||
@Post('empty-container-returns/:id/status')
|
||
@BookingStaff(FREIGHT_PERMS.bookings.operations)
|
||
@ApiOperation({ summary: 'Batch 16: advance empty container return workflow' })
|
||
updateEmptyReturnStatus(
|
||
@Param('id', ParseUUIDPipe) id: string,
|
||
@Body() dto: UpdateEmptyContainerReturnStatusDto,
|
||
) {
|
||
return this.service.updateEmptyReturnStatus(id, dto);
|
||
}
|
||
|
||
@Get('bookings/:bookingId/empty-container-returns')
|
||
@MixedAudience(FREIGHT_PERMS.bookings.operations)
|
||
@ApiOperation({ summary: 'List empty container returns for a booking (customer portal)' })
|
||
async listEmptyReturnsForBooking(
|
||
@Param('bookingId', ParseUUIDPipe) bookingId: string,
|
||
@CurrentUser() user: TCurrentUser,
|
||
) {
|
||
await this.assertCanAccessBooking(user, bookingId);
|
||
return this.service.listEmptyReturnsForBooking(bookingId);
|
||
}
|
||
|
||
@Get('empty-container-returns/:id/document')
|
||
@MixedAudience(FREIGHT_PERMS.bookings.operations)
|
||
@ApiOperation({ summary: 'Download the equipment interchange receipt PDF (customer portal)' })
|
||
async equipmentInterchangeDocument(
|
||
@Param('id', ParseUUIDPipe) id: string,
|
||
@CurrentUser() user: TCurrentUser,
|
||
@Res() res: Response,
|
||
) {
|
||
const row = await this.service.getEmptyReturnOrThrow(id);
|
||
// A standalone (no-booking) return has no owner to check against, so it
|
||
// stays staff-only.
|
||
if (!row.bookingId) {
|
||
await this.assertCanAccessBooking(user, null);
|
||
} else {
|
||
await this.assertCanAccessBooking(user, row.bookingId);
|
||
}
|
||
|
||
const { filename, buffer } = await this.service.equipmentInterchangeDocument(row);
|
||
res.setHeader('Content-Type', 'application/pdf');
|
||
res.setHeader('Content-Disposition', `inline; filename="${filename}"`);
|
||
res.setHeader('Content-Length', buffer.length);
|
||
return res.send(buffer);
|
||
}
|
||
|
||
/**
|
||
* Staff pass on permission alone. A customer must own the booking; `null`
|
||
* (a standalone, booking-less return) has no owner for a customer to match,
|
||
* so it 404s them the same way a foreign booking would.
|
||
*/
|
||
private async assertCanAccessBooking(user: TCurrentUser, bookingId: string | null): Promise<void> {
|
||
if (hasFreightPermission(user, FREIGHT_PERMS.bookings.operations)) return;
|
||
if (!bookingId) {
|
||
throw new NotFoundException('Not found');
|
||
}
|
||
const booking = await this.bookingsService.findById(bookingId);
|
||
await this.bookingsService.assertCustomerCanAccessBooking(user?.id, booking);
|
||
}
|
||
}
|