mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-09-09 04:48:18 +00:00
CHANGES
This commit is contained in:
@@ -44,6 +44,7 @@ import { Booking } from './entities/booking.entity';
|
|||||||
import { BookingsService } from './bookings.service';
|
import { BookingsService } from './bookings.service';
|
||||||
import { BookingClearanceService } from '../contracts/booking-clearance.service';
|
import { BookingClearanceService } from '../contracts/booking-clearance.service';
|
||||||
import { ClearanceMilestoneService } from '../contracts/clearance-milestone.service';
|
import { ClearanceMilestoneService } from '../contracts/clearance-milestone.service';
|
||||||
|
import { splitMilestones } from '../contracts/clearance-milestone.catalog';
|
||||||
import { ClearanceWorkflowService } from '../contracts/clearance-workflow.service';
|
import { ClearanceWorkflowService } from '../contracts/clearance-workflow.service';
|
||||||
import { ContractDocPhase } from '@edr/types';
|
import { ContractDocPhase } from '@edr/types';
|
||||||
|
|
||||||
@@ -1206,7 +1207,14 @@ export class BookingTransitionService {
|
|||||||
*/
|
*/
|
||||||
async finalizeClearance(bookingId: string, userId?: string): Promise<Booking> {
|
async finalizeClearance(bookingId: string, userId?: string): Promise<Booking> {
|
||||||
const booking = await this.bookingsService.findById(bookingId);
|
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(
|
throw new BadRequestException(
|
||||||
'General customs bookings use phased clearance — complete milestones via the phased actions instead of finalize.',
|
'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);
|
const { outputCode } = clearanceCodesForBooking(booking);
|
||||||
if (outputCode) {
|
if (outputCode) {
|
||||||
const setting =
|
const setting =
|
||||||
@@ -1271,6 +1286,59 @@ export class BookingTransitionService {
|
|||||||
return fresh;
|
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
|
* Customer proceeds to operation once clearance is ready. They pick the
|
||||||
* schedule day (the train departure day) for the shipment; the request then
|
* 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 { clearance, customerDocs, canUpload, pending, adHoc, status } = flow;
|
||||||
const docNoun = bookingDocNoun(booking);
|
const docNoun = bookingDocNoun(booking);
|
||||||
const reviewOpen = clearance.documentsOpen ?? canUpload;
|
const reviewOpen = clearance.documentsOpen ?? canUpload;
|
||||||
// A phased (agent-cleared) booking advances through the clearance wizard —
|
// An agent-cleared booking normally advances through the clearance wizard,
|
||||||
// approving the last document moves it on by itself, and the server refuses
|
// but finalizing is still allowed once every document is approved: the
|
||||||
// the plain finalize. Only a non-phased booking finalizes from here.
|
// 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 phased = Boolean(clearance.phase);
|
||||||
const canFinalize =
|
const canFinalize = status === "DOCUMENTS_UNDER_REVIEW" && clearance.allApproved;
|
||||||
!phased && status === "DOCUMENTS_UNDER_REVIEW" && clearance.allApproved;
|
|
||||||
const finalized = [
|
const finalized = [
|
||||||
"CLEARANCE_READY",
|
"CLEARANCE_READY",
|
||||||
"OPERATION_REQUEST_PENDING",
|
"OPERATION_REQUEST_PENDING",
|
||||||
@@ -449,16 +449,22 @@ export function ForwarderDocumentReview({
|
|||||||
Submit documents
|
Submit documents
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
{!finalized && !phased ? (
|
{!finalized ? (
|
||||||
<Button
|
<Button
|
||||||
color="edr-green"
|
color="edr-green"
|
||||||
radius="md"
|
radius="md"
|
||||||
|
variant={phased ? "light" : "filled"}
|
||||||
leftSection={<ShieldCheck size={16} />}
|
leftSection={<ShieldCheck size={16} />}
|
||||||
onClick={() => finalize.mutate()}
|
onClick={() => finalize.mutate()}
|
||||||
loading={finalize.isPending}
|
loading={finalize.isPending}
|
||||||
disabled={!canFinalize}
|
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>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
</Group>
|
</Group>
|
||||||
|
|||||||
Reference in New Issue
Block a user