mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-09 03:38:17 +00:00
CHANGES
This commit is contained in:
@@ -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<Booking> {
|
||||
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<Booking> {
|
||||
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
|
||||
|
||||
@@ -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
|
||||
</Button>
|
||||
) : null}
|
||||
{!finalized && !phased ? (
|
||||
{!finalized ? (
|
||||
<Button
|
||||
color="edr-green"
|
||||
radius="md"
|
||||
variant={phased ? "light" : "filled"}
|
||||
leftSection={<ShieldCheck size={16} />}
|
||||
onClick={() => finalize.mutate()}
|
||||
loading={finalize.isPending}
|
||||
disabled={!canFinalize}
|
||||
title={
|
||||
phased
|
||||
? "Closes the clearance now and marks the remaining steps as not applicable"
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
Finalize clearance
|
||||
{phased ? "Finalize without remaining steps" : "Finalize clearance"}
|
||||
</Button>
|
||||
) : null}
|
||||
</Group>
|
||||
|
||||
Reference in New Issue
Block a user