From d7752f438629c9449851a9507f0606ffa56c276c Mon Sep 17 00:00:00 2001 From: Marshal Date: Thu, 20 Aug 2026 08:36:02 +0000 Subject: [PATCH 1/2] fix(portal): show declaration, T1 and Djibouti clearance documents to the customer --- .../src/modules/audit/audit-endpoints.ts | 1 + .../booking-lifecycle-notifier.service.ts | 11 ++ .../bookings/booking-transition.service.ts | 53 ++++++++ .../modules/bookings/bookings.controller.ts | 19 +++ .../entities/booking-review-note.entity.ts | 6 + .../contracts/booking-clearance.service.ts | 17 +++ .../detail/AdditionalDocsRequestCard.tsx | 113 ++++++++++++++++++ .../bookings/DocumentClearanceDetailPage.tsx | 9 ++ .../src/services/bookings.service.ts | 5 + .../bookings/clearance/ClearanceFlow.tsx | 83 +++++++++++++ .../bookings/clearance/useClearanceFlow.ts | 13 ++ packages/types/src/freight/index.ts | 11 ++ 12 files changed, 341 insertions(+) create mode 100644 apps/edr-freight-web/backoffice/src/components/bookings/detail/AdditionalDocsRequestCard.tsx diff --git a/apps/edr-freight-api/src/modules/audit/audit-endpoints.ts b/apps/edr-freight-api/src/modules/audit/audit-endpoints.ts index deda35e1c..019d99920 100644 --- a/apps/edr-freight-api/src/modules/audit/audit-endpoints.ts +++ b/apps/edr-freight-api/src/modules/audit/audit-endpoints.ts @@ -47,6 +47,7 @@ export const AUDIT_ENDPOINTS: Readonly> = { "POST /api/bookings/:id/clearance/proceed": ["Customer requests operation with a schedule day", "POST", "Booking"], "POST /api/bookings/:id/clearance/release-order": ["Upload Booking Release Order", "POST", "Booking"], "POST /api/bookings/:id/clearance/review": ["GL reviews a clearance document (Approve | Query)", "POST", "Booking"], + "POST /api/bookings/:id/clearance/doc-requests": ["GL asks the customer for additional clearance documents", "POST", "Booking"], "POST /api/bookings/:id/clearance/charges/port-document": ["GL Djibouti uploads the port-charges document", "POST", "Booking"], "PATCH /api/bookings/:id/clearance/charges/:chargeId/bill": ["GL Ethiopia sets or revises a clearance charge's amount + currency", "PATCH", "Booking"], "POST /api/bookings/:id/clearance/charges/:chargeId/send": ["GL Ethiopia issues the clearance charge invoice to the customer", "POST", "Booking"], diff --git a/apps/edr-freight-api/src/modules/bookings/booking-lifecycle-notifier.service.ts b/apps/edr-freight-api/src/modules/bookings/booking-lifecycle-notifier.service.ts index 78c07bd22..f457e782a 100644 --- a/apps/edr-freight-api/src/modules/bookings/booking-lifecycle-notifier.service.ts +++ b/apps/edr-freight-api/src/modules/bookings/booking-lifecycle-notifier.service.ts @@ -202,6 +202,17 @@ export class BookingLifecycleNotifierService { } /** A clearance document was queried and needs the customer to re-upload. */ + /** GL asked the customer for additional clearance document(s). */ + additionalDocsRequested(b: Booking, note: string): void { + const msg = + `Additional document(s) requested on booking ${b.reference}: ` + + `${note} Please upload them from the portal.`; + void this.notifyContact(b, msg, 'ADDITIONAL DOCUMENTS REQUESTED'); + this.inApp(b, 'Additional documents requested', msg, { + type: NotificationType.DOCUMENT_ACTION, + }); + } + documentQueried(b: Booking, fileKey: string, note: string): void { const msg = `A clearance document on booking ${b.reference} needs attention: "${fileKey}". ` + diff --git a/apps/edr-freight-api/src/modules/bookings/booking-transition.service.ts b/apps/edr-freight-api/src/modules/bookings/booking-transition.service.ts index 805b2216f..6d4d9073b 100644 --- a/apps/edr-freight-api/src/modules/bookings/booking-transition.service.ts +++ b/apps/edr-freight-api/src/modules/bookings/booking-transition.service.ts @@ -643,6 +643,12 @@ export class BookingTransitionService { }>; allApproved: boolean; documentsOpen: boolean; + docRequests: Array<{ + id: string; + note: string; + byName: string | null; + at: string; + }>; phase?: string | null; milestones?: unknown[]; nextAction?: unknown; @@ -674,9 +680,14 @@ export class BookingTransitionService { bookingId, "CHANGES_REQUESTED", ); + const docRequestNotes = await this.bookingsRepository.findReviewNotes( + bookingId, + "ADDITIONAL_DOC_REQUEST", + ); const reviewerNames = await this.bookingsRepository.resolveStaffNames([ ...reviews.map((r) => r.reviewedByStaffId), ...queryNotes.map((n) => n.authorId), + ...docRequestNotes.map((n) => n.authorId), ]); const documents: Awaited< @@ -763,9 +774,51 @@ export class BookingTransitionService { documents, allApproved, documentsOpen: clearanceDocumentsOpen(booking), + docRequests: docRequestNotes.map((n) => ({ + id: n.id, + note: n.note, + byName: n.authorId ? (reviewerNames.get(n.authorId) ?? null) : null, + at: n.createdAt.toISOString(), + })), }; } + /** + * GL asks the customer for additional clearance document(s). Stored as a + * review-note thread shown on both the GL clearance page and the customer's + * portal; the customer answers with an ad-hoc upload. Allowed for as long as + * documents are open (until the shipment is paid). + */ + async requestAdditionalDocuments( + bookingId: string, + note: string, + staffId: string, + ): Promise { + const booking = await this.bookingsService.findById(bookingId); + if (!clearanceDocumentsOpen(booking)) { + throw new ConflictException( + `Clearance documents are closed for this booking (status "${booking.status}").`, + ); + } + if (!note?.trim()) { + throw new BadRequestException("Describe the document(s) you need."); + } + await this.bookingsRepository.createReviewNote( + bookingId, + note.trim(), + "ADDITIONAL_DOC_REQUEST", + staffId, + ); + await this.clearanceEvents.record({ + bookingId, + action: "ADDITIONAL_DOCS_REQUESTED", + label: "Requested additional document(s) from the customer", + actorId: staffId, + metadata: { note: note.trim() }, + }); + this.notifier.additionalDocsRequested(booking, note.trim()); + } + /** * True when every REQUIRED field of the booking's customer-input clearance set * has an APPROVED review row. The 100% gate before clearance can be finalized. diff --git a/apps/edr-freight-api/src/modules/bookings/bookings.controller.ts b/apps/edr-freight-api/src/modules/bookings/bookings.controller.ts index b1be94016..05d312384 100644 --- a/apps/edr-freight-api/src/modules/bookings/bookings.controller.ts +++ b/apps/edr-freight-api/src/modules/bookings/bookings.controller.ts @@ -1083,6 +1083,25 @@ export class BookingsController { return this.transitionService.enrichBookingResponse(booking); } + @Post(":id/clearance/doc-requests") + @BookingStaff(FREIGHT_PERMS.contracts.clearanceEtActions) + @ApiOperation({ + summary: + "GL asks the customer for additional clearance document(s) — shown on the portal with author and time", + }) + async requestAdditionalDocuments( + @Param("id", ParseUUIDPipe) id: string, + @Body("note") note: string, + @CurrentUser() user: AuthUserPayload, + ) { + await this.transitionService.requestAdditionalDocuments( + id, + note, + resolveAuthUserId(user), + ); + return { success: true }; + } + @Get(":id/clearance/history") @BookingStaff([ FREIGHT_PERMS.contracts.clearanceEtActions, diff --git a/apps/edr-freight-api/src/modules/bookings/entities/booking-review-note.entity.ts b/apps/edr-freight-api/src/modules/bookings/entities/booking-review-note.entity.ts index 969098196..8bac3ea2b 100644 --- a/apps/edr-freight-api/src/modules/bookings/entities/booking-review-note.entity.ts +++ b/apps/edr-freight-api/src/modules/bookings/entities/booking-review-note.entity.ts @@ -11,6 +11,12 @@ export const REVIEW_NOTE_TYPES = [ * (price/files). One row per round — the draft/change-request loop can repeat. */ 'DRAFT_DECL_CHANGE_REQUEST', + /** + * GL asked the customer for additional clearance document(s). Shown as a + * thread on both the GL clearance page and the customer's portal — the + * customer answers by uploading an ad-hoc document. + */ + 'ADDITIONAL_DOC_REQUEST', ] as const; export type ReviewNoteType = (typeof REVIEW_NOTE_TYPES)[number]; diff --git a/apps/edr-freight-api/src/modules/contracts/booking-clearance.service.ts b/apps/edr-freight-api/src/modules/contracts/booking-clearance.service.ts index d9fe1825f..d393c011d 100644 --- a/apps/edr-freight-api/src/modules/contracts/booking-clearance.service.ts +++ b/apps/edr-freight-api/src/modules/contracts/booking-clearance.service.ts @@ -66,6 +66,12 @@ export interface BookingClearanceView { }>; allApproved: boolean; documentsOpen: boolean; + docRequests: Array<{ + id: string; + note: string; + byName: string | null; + at: string; + }>; phase?: string | null; milestones?: Array<{ id: string; @@ -206,9 +212,14 @@ export class BookingClearanceService { bookingId, 'CHANGES_REQUESTED', ); + const docRequestNotes = await this.bookingsRepository.findReviewNotes( + bookingId, + 'ADDITIONAL_DOC_REQUEST', + ); const reviewerNames = await this.bookingsRepository.resolveStaffNames([ ...reviews.map((r) => r.reviewedByStaffId), ...queryNotes.map((n) => n.authorId), + ...docRequestNotes.map((n) => n.authorId), ]); const documents: BookingClearanceView['documents'] = []; @@ -357,6 +368,12 @@ export class BookingClearanceService { documents, allApproved, documentsOpen: clearanceDocumentsOpen(booking), + docRequests: docRequestNotes.map((n) => ({ + id: n.id, + note: n.note, + byName: n.authorId ? (reviewerNames.get(n.authorId) ?? null) : null, + at: n.createdAt.toISOString(), + })), phase, milestones: milestones.map((m) => ({ id: m.id, diff --git a/apps/edr-freight-web/backoffice/src/components/bookings/detail/AdditionalDocsRequestCard.tsx b/apps/edr-freight-web/backoffice/src/components/bookings/detail/AdditionalDocsRequestCard.tsx new file mode 100644 index 000000000..fd9abe744 --- /dev/null +++ b/apps/edr-freight-web/backoffice/src/components/bookings/detail/AdditionalDocsRequestCard.tsx @@ -0,0 +1,113 @@ +import { useState } from "react"; +import { useMutation } from "@tanstack/react-query"; +import { + Box, + Button, + Group, + Paper, + Stack, + Text, + Textarea, +} from "@mantine/core"; +import { MessageSquarePlus, Send } from "lucide-react"; +import toast from "react-hot-toast"; +import type { Freight } from "@edr/types"; + +import { SectionCard } from "./SectionCard"; +import { bookingsService } from "@/services/bookings.service"; +import { formatDateTime } from "@/lib/format"; +import { extractErrorMessage } from "@/utils/errorExtractor"; + +export interface AdditionalDocsRequestCardProps { + bookingId: string; + /** Past requests, newest first. */ + requests: Freight.ClearanceDocRequest[]; + /** False once the shipment is paid — documents (and requests) are closed. */ + canRequest: boolean; + onSent?: () => void; +} + +/** + * GL asks the customer for additional clearance document(s) in plain words. + * The message, its author and its time show on the customer's portal beside + * the upload box, so the customer knows exactly what to send and who asked. + */ +export function AdditionalDocsRequestCard({ + bookingId, + requests, + canRequest, + onSent, +}: AdditionalDocsRequestCardProps) { + const [note, setNote] = useState(""); + + const send = useMutation({ + mutationFn: () => bookingsService.requestAdditionalDocuments(bookingId, note), + onSuccess: () => { + toast.success("Request sent to the customer"); + setNote(""); + onSent?.(); + }, + onError: (e) => + toast.error(extractErrorMessage(e, "Could not send the request")), + }); + + return ( + + + {canRequest ? ( + +