mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
refactor T1 document handling and update related logic for import/export processes
This commit is contained in:
@@ -244,9 +244,10 @@ export class GlOperationsService {
|
||||
}
|
||||
|
||||
/**
|
||||
* T1 transit-document lifecycle state for an import shipment booking. Wagon
|
||||
* allocation opens the upload window; train departure locks it; train arrival
|
||||
* lets GL Ethiopia close (accept) the T1 set.
|
||||
* T1 transit-document lifecycle state for an import shipment booking. The
|
||||
* gate pass (secured on the train schedule after wagon allocation) opens the
|
||||
* upload window; train departure locks it; train arrival lets GL Ethiopia
|
||||
* close (accept) the T1 set.
|
||||
*/
|
||||
async t1State(bookingId: string): Promise<Freight.ClearanceT1State> {
|
||||
const train = await this.trainState(bookingId);
|
||||
@@ -269,7 +270,8 @@ export class GlOperationsService {
|
||||
}
|
||||
|
||||
/**
|
||||
* GL Djibouti uploads T1 transport documents (multi-file) after wagon allocation.
|
||||
* GL Djibouti uploads T1 transport documents (multi-file) once the gate pass
|
||||
* is secured on the train schedule (which itself follows wagon allocation).
|
||||
* Replaces the previous batch; locked once the train departs or T1 is closed.
|
||||
*/
|
||||
async uploadT1Documents(
|
||||
@@ -287,6 +289,12 @@ export class GlOperationsService {
|
||||
'Wagons must be allocated before T1 transport documents can be uploaded.',
|
||||
);
|
||||
}
|
||||
const gatepass = await this.gatepassForBooking(bookingId);
|
||||
if (!gatepass.granted) {
|
||||
throw new BadRequestException(
|
||||
'Secure the Djibouti gate pass on the train schedule before uploading T1 transport documents.',
|
||||
);
|
||||
}
|
||||
if (state.closed) {
|
||||
throw new BadRequestException('T1 has been closed by GL Ethiopia — documents are final.');
|
||||
}
|
||||
@@ -303,7 +311,8 @@ export class GlOperationsService {
|
||||
/**
|
||||
* Close (accept) the T1/transport document set.
|
||||
* Import: GL Ethiopia closes once the train has arrived (T1 files required).
|
||||
* Export: GL Djibouti closes after the gate pass (transport document required).
|
||||
* Export: GL Djibouti closes once the train arrives at Djibouti (transport
|
||||
* document required).
|
||||
*/
|
||||
async closeT1(
|
||||
bookingId: string,
|
||||
@@ -337,10 +346,9 @@ export class GlOperationsService {
|
||||
'The transport document must be uploaded before T1 can be closed.',
|
||||
);
|
||||
}
|
||||
const gatepass = await this.gatepassForBooking(bookingId);
|
||||
if (!gatepass.granted) {
|
||||
if (!state.trainArrivedAt) {
|
||||
throw new BadRequestException(
|
||||
'Secure the Djibouti gate pass on the train schedule before closing T1.',
|
||||
'The train has not arrived at Djibouti yet — T1 can be closed only after arrival.',
|
||||
);
|
||||
}
|
||||
// Export bookings seeded before T1_CLOSED joined the catalog lack the row.
|
||||
|
||||
@@ -53,8 +53,9 @@ import { bookingsService } from "@/services/bookings.service";
|
||||
/**
|
||||
* Export customs flow, ordered per the stakeholder process:
|
||||
* customer docs → RO (DJ) → declaration (ET, auto-releases) → create booking (ET)
|
||||
* → payment + wagons → transport document (ET) → train to Djibouti → gate pass (DJ)
|
||||
* → accept T1 (DJ) → final invoice (DJ) + customer slip + GL confirm.
|
||||
* → payment + wagons → transport document / T1 (ET) → train to Djibouti
|
||||
* → accept T1 (DJ, one button after arrival) → gate pass (DJ)
|
||||
* → final invoice (DJ) + customer slip + GL confirm.
|
||||
*/
|
||||
export function computeExportActiveStep(
|
||||
clearance: ClearanceViewLike,
|
||||
@@ -77,8 +78,8 @@ export function computeExportActiveStep(
|
||||
}
|
||||
if (!isBookingMilestoneDone(bookingMilestones, "EXPORT_TRANSPORT_ISSUED")) return 5;
|
||||
if (!clearance.train?.arrivedAt) return 6;
|
||||
if (!clearance.gatepassGranted) return 7;
|
||||
if (!clearance.t1Closed) return 8;
|
||||
if (!clearance.t1Closed) return 7;
|
||||
if (!clearance.gatepassGranted) return 8;
|
||||
if (clearance.finalInvoice?.status !== "PAID") return 9;
|
||||
return 10;
|
||||
}
|
||||
@@ -395,17 +396,9 @@ export function ExportClearanceStepper({
|
||||
</Stack>
|
||||
</Stepper.Step>
|
||||
|
||||
<Stepper.Step
|
||||
label="Gate pass"
|
||||
description="Secured on the train schedule after arrival"
|
||||
icon={clearance.gatepassGranted ? <CheckCircle2 size={14} /> : <Truck size={14} />}
|
||||
>
|
||||
<GatepassStep clearance={clearance} />
|
||||
</Stepper.Step>
|
||||
|
||||
<Stepper.Step
|
||||
label="Accept T1"
|
||||
description="GL Djibouti closes the transport document"
|
||||
description="GL Djibouti closes once the train arrives"
|
||||
icon={clearance.t1Closed ? <CheckCircle2 size={14} /> : <PackageCheck size={14} />}
|
||||
>
|
||||
<AcceptT1Step
|
||||
@@ -420,6 +413,14 @@ export function ExportClearanceStepper({
|
||||
/>
|
||||
</Stepper.Step>
|
||||
|
||||
<Stepper.Step
|
||||
label="Gate pass"
|
||||
description="Secured on the train schedule after arrival"
|
||||
icon={clearance.gatepassGranted ? <CheckCircle2 size={14} /> : <Truck size={14} />}
|
||||
>
|
||||
<GatepassStep clearance={clearance} />
|
||||
</Stepper.Step>
|
||||
|
||||
<Stepper.Step
|
||||
label="Final invoice & payment"
|
||||
description="GL Djibouti invoices after offload; customer pays"
|
||||
@@ -554,6 +555,7 @@ function AcceptT1Step({
|
||||
}) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const files = exportTransitFilesFromWorkflow(workflowFiles);
|
||||
const arrived = Boolean(clearance.train?.arrivedAt);
|
||||
|
||||
return (
|
||||
<Stack gap="sm">
|
||||
@@ -582,9 +584,9 @@ function AcceptT1Step({
|
||||
pendingLabel={
|
||||
!transportIssued
|
||||
? "Waiting for the transport document."
|
||||
: clearance.gatepassGranted
|
||||
? "Gate pass granted — GL Djibouti accepts (closes) the T1."
|
||||
: "Available after the gate pass is granted."
|
||||
: arrived
|
||||
? "Train arrived — GL Djibouti accepts (closes) the T1."
|
||||
: "Available once the train arrives at Djibouti."
|
||||
}
|
||||
doneLabel=""
|
||||
/>
|
||||
@@ -592,7 +594,7 @@ function AcceptT1Step({
|
||||
<Button
|
||||
color="edr-green"
|
||||
loading={loading}
|
||||
disabled={!transportIssued || !clearance.gatepassGranted}
|
||||
disabled={!transportIssued || !arrived}
|
||||
leftSection={<PackageCheck size={16} />}
|
||||
onClick={async () => {
|
||||
setLoading(true);
|
||||
|
||||
@@ -98,6 +98,7 @@ function computeImportActiveStep(
|
||||
clearance: ClearanceViewLike,
|
||||
bookingCreated: boolean,
|
||||
bookingMilestones: MilestoneRow[],
|
||||
t1Uploaded: boolean,
|
||||
): number {
|
||||
if (!isMilestoneDone(clearance.milestones, "DOCUMENTS_APPROVED")) return 0;
|
||||
if (!isMilestoneDone(clearance.milestones, "DECLARED")) return 1;
|
||||
@@ -119,16 +120,17 @@ function computeImportActiveStep(
|
||||
if (!isMilestoneDone(clearance.milestones, "DO_COLLECTED")) return 6;
|
||||
if (!bookingCreated) return 7;
|
||||
if (!clearance.gatepassGranted) return 8;
|
||||
if (!clearance.t1?.closed) return 9;
|
||||
if (!isBookingMilestoneDone(bookingMilestones, "RISK_ASSIGNED")) return 10;
|
||||
if (!t1Uploaded && !clearance.t1?.closed) return 9;
|
||||
if (!clearance.t1?.closed) return 10;
|
||||
if (!isBookingMilestoneDone(bookingMilestones, "RISK_ASSIGNED")) return 11;
|
||||
// Additional duty round is optional — resolved once skipped or paid.
|
||||
const secondDutyResolved =
|
||||
clearance.secondDuty?.skipped ||
|
||||
clearance.secondDuty?.paid ||
|
||||
isBookingMilestoneDone(bookingMilestones, "SECOND_DUTY_PAID");
|
||||
if (!secondDutyResolved) return 11;
|
||||
if (!clearance.importReleaseGranted) return 12;
|
||||
return 13;
|
||||
if (!secondDutyResolved) return 12;
|
||||
if (!clearance.importReleaseGranted) return 13;
|
||||
return 14;
|
||||
}
|
||||
|
||||
function t1FilesFromWorkflow(
|
||||
@@ -226,12 +228,18 @@ export function PhasedClearanceActionPanel({
|
||||
// The booking that carries the post-booking steps (gate pass, risk, duty, release).
|
||||
const actionBookingId =
|
||||
clearance.t1?.bookingId ?? clearance.linkedBookingId ?? bookingId ?? null;
|
||||
const t1Uploaded = t1FilesFromWorkflow(workflowFiles).length > 0;
|
||||
const activeStep = useMemo(
|
||||
() =>
|
||||
isImport
|
||||
? computeImportActiveStep(clearance, effectiveBookingCreated, bookingMilestones)
|
||||
? computeImportActiveStep(
|
||||
clearance,
|
||||
effectiveBookingCreated,
|
||||
bookingMilestones,
|
||||
t1Uploaded,
|
||||
)
|
||||
: 0,
|
||||
[clearance, isImport, effectiveBookingCreated, bookingMilestones],
|
||||
[clearance, isImport, effectiveBookingCreated, bookingMilestones, t1Uploaded],
|
||||
);
|
||||
|
||||
if (isImport) {
|
||||
@@ -544,22 +552,45 @@ export function PhasedClearanceActionPanel({
|
||||
|
||||
<Stepper.Step
|
||||
label="T1 transport documents"
|
||||
description="GL Djibouti uploads after wagon allocation; GL Ethiopia closes on arrival"
|
||||
description="GL Djibouti uploads after the gate pass is secured"
|
||||
icon={
|
||||
clearance.t1?.closed ? <CheckCircle2 size={14} /> : <Truck size={14} />
|
||||
t1Uploaded || clearance.t1?.closed ? (
|
||||
<CheckCircle2 size={14} />
|
||||
) : (
|
||||
<Truck size={14} />
|
||||
)
|
||||
}
|
||||
>
|
||||
<ImportT1Section
|
||||
<ImportT1UploadStep
|
||||
t1={clearance.t1 ?? null}
|
||||
gatepassGranted={Boolean(clearance.gatepassGranted)}
|
||||
workflowFiles={workflowFiles}
|
||||
canDjAct={showDj && canDj}
|
||||
canEtAct={showEt && canEt}
|
||||
onChanged={onChanged}
|
||||
onViewFile={onViewFile}
|
||||
onDownloadFile={onDownloadFile}
|
||||
/>
|
||||
</Stepper.Step>
|
||||
|
||||
<Stepper.Step
|
||||
label="Close T1"
|
||||
description="GL Ethiopia closes once the train arrives"
|
||||
icon={
|
||||
clearance.t1?.closed ? (
|
||||
<CheckCircle2 size={14} />
|
||||
) : (
|
||||
<PackageCheck size={14} />
|
||||
)
|
||||
}
|
||||
>
|
||||
<ImportT1CloseStep
|
||||
t1={clearance.t1 ?? null}
|
||||
t1Uploaded={t1Uploaded}
|
||||
canEtAct={showEt && canEt}
|
||||
onChanged={onChanged}
|
||||
/>
|
||||
</Stepper.Step>
|
||||
|
||||
<Stepper.Step
|
||||
label="Customs risk"
|
||||
description="GL Ethiopia assigns Green / Yellow / Red"
|
||||
@@ -645,26 +676,29 @@ export function PhasedClearanceActionPanel({
|
||||
);
|
||||
}
|
||||
|
||||
function ImportT1Section({
|
||||
/**
|
||||
* GL Djibouti uploads the T1 transport documents (multi-file) once the gate
|
||||
* pass is secured on the train schedule; locked on departure or T1 close.
|
||||
*/
|
||||
function ImportT1UploadStep({
|
||||
t1,
|
||||
gatepassGranted,
|
||||
workflowFiles = [],
|
||||
canDjAct,
|
||||
canEtAct,
|
||||
onChanged,
|
||||
onViewFile,
|
||||
onDownloadFile,
|
||||
}: {
|
||||
t1: Freight.ClearanceT1State | null;
|
||||
gatepassGranted: boolean;
|
||||
workflowFiles?: Freight.ClearanceWorkflowFile[];
|
||||
canDjAct: boolean;
|
||||
canEtAct: boolean;
|
||||
onChanged?: () => void;
|
||||
onViewFile?: (file: { name: string; url: string }) => void;
|
||||
onDownloadFile?: (file: { id: string; name: string }) => void;
|
||||
}) {
|
||||
const [files, setFiles] = useState<File[]>([]);
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [closing, setClosing] = useState(false);
|
||||
|
||||
const uploaded = t1FilesFromWorkflow(workflowFiles);
|
||||
const replaceMode = uploaded.length > 0;
|
||||
@@ -680,8 +714,8 @@ function ImportT1Section({
|
||||
}
|
||||
|
||||
const departed = Boolean(t1.trainDepartedAt);
|
||||
const arrived = Boolean(t1.trainArrivedAt);
|
||||
const canUpload = canDjAct && t1.wagonAllocated && !departed && !t1.closed;
|
||||
const canUpload =
|
||||
canDjAct && t1.wagonAllocated && gatepassGranted && !departed && !t1.closed;
|
||||
|
||||
return (
|
||||
<Stack gap="sm">
|
||||
@@ -707,7 +741,7 @@ function ImportT1Section({
|
||||
<StepStatus
|
||||
done
|
||||
pendingLabel=""
|
||||
doneLabel="T1 accepted and closed by GL Ethiopia — documents are final."
|
||||
doneLabel="T1 documents are final — closed by GL Ethiopia."
|
||||
/>
|
||||
) : !t1.wagonAllocated ? (
|
||||
<StepStatus
|
||||
@@ -715,6 +749,12 @@ function ImportT1Section({
|
||||
pendingLabel="Waiting for operations to allocate wagons."
|
||||
doneLabel=""
|
||||
/>
|
||||
) : !gatepassGranted ? (
|
||||
<StepStatus
|
||||
done={false}
|
||||
pendingLabel="Waiting for the gate pass to be secured on the train schedule."
|
||||
doneLabel=""
|
||||
/>
|
||||
) : departed ? (
|
||||
<Alert color="orange" variant="light" icon={<AlertTriangle size={16} />}>
|
||||
The train has departed — T1 documents are locked and can no longer be changed.
|
||||
@@ -725,6 +765,8 @@ function ImportT1Section({
|
||||
pendingLabel="Waiting for GL Djibouti to upload T1 transport documents."
|
||||
doneLabel=""
|
||||
/>
|
||||
) : uploaded.length > 0 && !canUpload ? (
|
||||
<StepStatus done pendingLabel="" doneLabel="T1 documents uploaded." />
|
||||
) : null}
|
||||
|
||||
{canUpload ? (
|
||||
@@ -771,40 +813,103 @@ function ImportT1Section({
|
||||
</Button>
|
||||
</>
|
||||
) : null}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
{canEtAct && !t1.closed ? (
|
||||
arrived ? (
|
||||
<Stack gap="xs">
|
||||
<Text size="sm" c="dimmed">
|
||||
The train has arrived — review the T1 documents and close (accept) them.
|
||||
</Text>
|
||||
<Button
|
||||
color="edr-green"
|
||||
loading={closing}
|
||||
disabled={uploaded.length === 0}
|
||||
leftSection={<PackageCheck size={16} />}
|
||||
onClick={async () => {
|
||||
setClosing(true);
|
||||
try {
|
||||
await contractsService.closeT1(t1.bookingId);
|
||||
toast.success("T1 closed");
|
||||
onChanged?.();
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : "Failed");
|
||||
} finally {
|
||||
setClosing(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Accept & close T1
|
||||
</Button>
|
||||
</Stack>
|
||||
) : departed ? (
|
||||
<Alert color="blue" variant="light" icon={<Clock size={16} />}>
|
||||
Train en route — T1 can be closed once it arrives in Ethiopia.
|
||||
</Alert>
|
||||
) : null
|
||||
) : null}
|
||||
/**
|
||||
* GL Ethiopia closes (accepts) the T1 set with one click once the train has
|
||||
* arrived. Separate step from the GL Djibouti upload.
|
||||
*/
|
||||
function ImportT1CloseStep({
|
||||
t1,
|
||||
t1Uploaded,
|
||||
canEtAct,
|
||||
onChanged,
|
||||
}: {
|
||||
t1: Freight.ClearanceT1State | null;
|
||||
t1Uploaded: boolean;
|
||||
canEtAct: boolean;
|
||||
onChanged?: () => void;
|
||||
}) {
|
||||
const [closing, setClosing] = useState(false);
|
||||
|
||||
if (!t1) {
|
||||
return (
|
||||
<StepStatus
|
||||
done={false}
|
||||
pendingLabel="Available once the shipment booking is created."
|
||||
doneLabel=""
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (t1.closed) {
|
||||
return (
|
||||
<StepStatus
|
||||
done
|
||||
pendingLabel=""
|
||||
doneLabel={`T1 accepted and closed by GL Ethiopia${
|
||||
t1.closedAt ? ` · ${new Date(t1.closedAt).toLocaleString()}` : ""
|
||||
}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!t1Uploaded) {
|
||||
return (
|
||||
<StepStatus
|
||||
done={false}
|
||||
pendingLabel="Waiting for GL Djibouti to upload T1 transport documents."
|
||||
doneLabel=""
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!t1.trainArrivedAt) {
|
||||
return (
|
||||
<Alert color="blue" variant="light" icon={<Clock size={16} />}>
|
||||
{t1.trainDepartedAt
|
||||
? "Train en route — T1 can be closed once it arrives in Ethiopia."
|
||||
: "T1 can be closed once the train arrives in Ethiopia."}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
if (!canEtAct) {
|
||||
return (
|
||||
<StepStatus
|
||||
done={false}
|
||||
pendingLabel="Train arrived — waiting for GL Ethiopia to close the T1."
|
||||
doneLabel=""
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Stack gap="xs">
|
||||
<Text size="sm" c="dimmed">
|
||||
The train has arrived — review the T1 documents and close (accept) them.
|
||||
</Text>
|
||||
<Button
|
||||
color="edr-green"
|
||||
loading={closing}
|
||||
leftSection={<PackageCheck size={16} />}
|
||||
onClick={async () => {
|
||||
setClosing(true);
|
||||
try {
|
||||
await contractsService.closeT1(t1.bookingId);
|
||||
toast.success("T1 closed");
|
||||
onChanged?.();
|
||||
} catch (e) {
|
||||
toast.error(e instanceof Error ? e.message : "Failed");
|
||||
} finally {
|
||||
setClosing(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
Accept & close T1
|
||||
</Button>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user