feat: implement document clearance workflow for bookings

- Added ClearanceCard component to display and manage clearance documents in ReadonlyBookingView.
- Introduced new API endpoints for clearance operations: getClearance, submitClearanceDocuments, and proceedToOperation.
- Created BookingDocumentReview entity and migration for document review status tracking.
- Developed GlClearancePage for Global Logistics to review and manage document submissions.
- Implemented utility functions for determining clearance setting codes based on trade direction and freight type.
- Added tests for booking transition clearance logic and clearance utility functions.
This commit is contained in:
Marshal
2026-06-23 21:33:34 +00:00
parent 29f6928059
commit 6485cbdd71
24 changed files with 1924 additions and 6 deletions

View File

@@ -47,6 +47,7 @@ import {
RejectBookingDto,
RejectStepDto,
RequestChangesDto,
ReviewDocumentDto,
StaffRejectDto,
} from './dto/request-changes.dto';
import { ContractViewDto } from './dto/contract-view.dto';
@@ -328,6 +329,86 @@ export class BookingsController {
return this.transitionService.enrichBookingResponse(booking);
}
// ── Document clearance (post counter-sign) ────────────────────────────────
@Get(':id/clearance')
@ApiOperation({
summary: 'Document-clearance grid (required docs + upload + GL review status)',
})
getClearance(@Param('id', ParseUUIDPipe) id: string) {
return this.transitionService.getClearanceView(id);
}
@Post(':id/clearance/documents')
@UseInterceptors(AnyFilesInterceptor())
@ApiConsumes('multipart/form-data')
@ApiOperation({
summary: 'Customer uploads clearance documents (fieldname = document key)',
})
async submitClearanceDocuments(
@Param('id', ParseUUIDPipe) id: string,
@UploadedFiles() files: Express.Multer.File[],
) {
const booking = await this.transitionService.submitClearanceDocuments(
id,
files ?? [],
);
return this.transitionService.enrichBookingResponse(booking);
}
@Post(':id/clearance/proceed')
@ApiOperation({
summary: 'Customer proceeds to operation (CLEARANCE_READY → OPERATION_REQUESTED)',
})
async proceedToOperation(@Param('id', ParseUUIDPipe) id: string) {
const booking = await this.transitionService.requestOperation(id);
return this.transitionService.enrichBookingResponse(booking);
}
@Post(':id/clearance/review')
@BookingStaff(FREIGHT_PERMS.bookings.reviewDocuments)
@ApiOperation({ summary: 'GL reviews a clearance document (Approve | Query)' })
async reviewClearanceDocument(
@Param('id', ParseUUIDPipe) id: string,
@Body() dto: ReviewDocumentDto,
@CurrentUser() user: AuthUserPayload,
) {
const booking = await this.transitionService.reviewDocument(
id,
dto.fileKey,
dto.status,
resolveAuthUserId(user),
dto.note,
);
return this.transitionService.enrichBookingResponse(booking);
}
@Post(':id/clearance/output-documents')
@BookingStaff(FREIGHT_PERMS.bookings.uploadClearanceOutput)
@UseInterceptors(AnyFilesInterceptor())
@ApiConsumes('multipart/form-data')
@ApiOperation({ summary: 'GL uploads customs output documents (IM4/EX3/…)' })
async uploadClearanceOutput(
@Param('id', ParseUUIDPipe) id: string,
@UploadedFiles() files: Express.Multer.File[],
) {
const booking = await this.transitionService.uploadClearanceOutputDocuments(
id,
files ?? [],
);
return this.transitionService.enrichBookingResponse(booking);
}
@Post(':id/clearance/finalize')
@BookingStaff(FREIGHT_PERMS.bookings.finalizeClearance)
@ApiOperation({
summary: 'GL finalizes clearance (requires 100% approved) → CLEARANCE_READY',
})
async finalizeClearance(@Param('id', ParseUUIDPipe) id: string) {
const booking = await this.transitionService.finalizeClearance(id);
return this.transitionService.enrichBookingResponse(booking);
}
@Post(':id/staff/request-changes')
@BookingStaff(FREIGHT_PERMS.bookings.requestChanges)
@ApiOperation({ summary: 'Staff return booking for customer updates' })