From 54f76d47798503d7fea8412e41ee69e251ff3cea Mon Sep 17 00:00:00 2001 From: marshal Date: Tue, 8 Sep 2026 22:05:09 +0000 Subject: [PATCH] CHANGES --- .../bookings/booking-transition.service.ts | 70 ++++++++++++++++++- .../forwarder/ForwarderDocumentReview.tsx | 20 ++++-- 2 files changed, 82 insertions(+), 8 deletions(-) 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 69da49299..bdccd38ee 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 @@ -44,6 +44,7 @@ import { Booking } from './entities/booking.entity'; import { BookingsService } from './bookings.service'; import { BookingClearanceService } from '../contracts/booking-clearance.service'; import { ClearanceMilestoneService } from '../contracts/clearance-milestone.service'; +import { splitMilestones } from '../contracts/clearance-milestone.catalog'; import { ClearanceWorkflowService } from '../contracts/clearance-workflow.service'; import { ContractDocPhase } from '@edr/types'; @@ -1206,7 +1207,14 @@ export class BookingTransitionService { */ async finalizeClearance(bookingId: string, userId?: string): Promise { const booking = await this.bookingsService.findById(bookingId); - if (this.isPhasedCustoms(booking)) { + // A GL-cleared customs booking runs the phased ET/DJ workflow end to end, + // and finalize would jump the desks' own steps — it stays refused there. + // An agent-cleared booking is different: the clearing agent does its + // paperwork with customs off the platform, so Operations may close the + // clearance once every customer document is approved. The steps that were + // never taken are SKIPPED below rather than left pending, or the timeline + // would stall behind milestones nobody is going to complete. + if (this.isPhasedCustoms(booking) && !booking.clearedByAgent) { throw new BadRequestException( 'General customs bookings use phased clearance — complete milestones via the phased actions instead of finalize.', ); @@ -1220,6 +1228,13 @@ export class BookingTransitionService { ); } + // Agent-cleared: close out the pre-booking steps the agent did not take, + // then hand the booking to the customer to complete. Everything below this + // point is the non-phased path and does not apply. + if (booking.clearedByAgent) { + return this.finalizeAgentClearedBooking(booking, userId); + } + const { outputCode } = clearanceCodesForBooking(booking); if (outputCode) { const setting = @@ -1271,6 +1286,59 @@ export class BookingTransitionService { return fresh; } + /** + * Operations closes the clearance on a booking the customer handed to a + * registered clearing agent. + * + * The agent files with customs off the platform, so the steps it never + * recorded here (declaration, transit permit, pre-clearance handoff, + * Delivery Order / Release Order) are marked SKIPPED — a PENDING milestone + * would stall the phase derivation and leave the wizard mid-flow forever. + * Post-booking milestones are untouched: payment, wagons and the rail leg + * still happen, and the customer completes the booking from here. + */ + private async finalizeAgentClearedBooking( + booking: Booking, + userId?: string, + ): Promise { + const bookingId = booking.id; + const tradeDirection = booking.tradeDirection ?? 'IMPORT'; + const milestones = + await this.workflowService.listMilestonesForBooking(bookingId); + // Only the pre-booking half: anything from the boundary onward belongs to + // the shipment itself, not to clearance. + const { preBooking } = splitMilestones(tradeDirection); + const preBookingCodes = new Set(preBooking.map((d) => d.code)); + const pending = milestones.filter( + (m) => preBookingCodes.has(m.milestoneCode) && m.status === 'PENDING', + ); + if (pending.length > 0) { + await this.workflowService.skipMilestonesForBooking( + bookingId, + pending.map((m) => m.milestoneCode), + ); + } + + await this.bookingsRepository.update(bookingId, { + status: 'CLEARANCE_READY', + clearanceCurrentPhase: ContractDocPhase.GlEtPostClearance, + preClearanceFinalizedAt: booking.preClearanceFinalizedAt ?? new Date(), + } as never); + await this.clearanceEvents.record({ + bookingId, + action: 'CLEARANCE_FINALIZED', + label: + pending.length > 0 + ? `Finalized clearance — ${pending.length} clearing-agent step(s) closed as not applicable` + : 'Finalized document approval — clearance ready', + actorId: userId ?? null, + metadata: { skipped: pending.map((m) => m.milestoneCode) }, + }); + const fresh = await this.bookingsService.findById(bookingId); + this.notifier.clearanceReady(fresh); + return fresh; + } + /** * Customer proceeds to operation once clearance is ready. They pick the * schedule day (the train departure day) for the shipment; the request then diff --git a/apps/edr-freight-web/portal/src/pages/forwarder/ForwarderDocumentReview.tsx b/apps/edr-freight-web/portal/src/pages/forwarder/ForwarderDocumentReview.tsx index f2a3a254d..8e1ab00b9 100644 --- a/apps/edr-freight-web/portal/src/pages/forwarder/ForwarderDocumentReview.tsx +++ b/apps/edr-freight-web/portal/src/pages/forwarder/ForwarderDocumentReview.tsx @@ -131,12 +131,12 @@ export function ForwarderDocumentReview({ const { clearance, customerDocs, canUpload, pending, adHoc, status } = flow; const docNoun = bookingDocNoun(booking); const reviewOpen = clearance.documentsOpen ?? canUpload; - // A phased (agent-cleared) booking advances through the clearance wizard — - // approving the last document moves it on by itself, and the server refuses - // the plain finalize. Only a non-phased booking finalizes from here. + // An agent-cleared booking normally advances through the clearance wizard, + // but finalizing is still allowed once every document is approved: the + // remaining steps are closed as not applicable, for an agent that files + // with customs off the platform. The wizard stays the expected path. const phased = Boolean(clearance.phase); - const canFinalize = - !phased && status === "DOCUMENTS_UNDER_REVIEW" && clearance.allApproved; + const canFinalize = status === "DOCUMENTS_UNDER_REVIEW" && clearance.allApproved; const finalized = [ "CLEARANCE_READY", "OPERATION_REQUEST_PENDING", @@ -449,16 +449,22 @@ export function ForwarderDocumentReview({ Submit documents ) : null} - {!finalized && !phased ? ( + {!finalized ? ( ) : null}