mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
View manual (offline) payment channel settings
Confirm offline (bank transfer) invoice payment
This commit is contained in:
@@ -27,7 +27,10 @@ import { BookingPricingService } from './booking-pricing.service';
|
|||||||
import { ContainerValidationService } from './container-validation.service';
|
import { ContainerValidationService } from './container-validation.service';
|
||||||
import { BookingsRepository } from './bookings.repository';
|
import { BookingsRepository } from './bookings.repository';
|
||||||
import { assertBookingStatus } from './booking-status.util';
|
import { assertBookingStatus } from './booking-status.util';
|
||||||
import { clearanceCodesForBooking } from './clearance.util';
|
import {
|
||||||
|
clearanceCodesForBooking,
|
||||||
|
clearanceDocumentsOpen,
|
||||||
|
} from './clearance.util';
|
||||||
import {
|
import {
|
||||||
buildClearanceDocHistory,
|
buildClearanceDocHistory,
|
||||||
type ClearanceDocEvent,
|
type ClearanceDocEvent,
|
||||||
@@ -639,6 +642,7 @@ export class BookingTransitionService {
|
|||||||
history: ClearanceDocEvent[];
|
history: ClearanceDocEvent[];
|
||||||
}>;
|
}>;
|
||||||
allApproved: boolean;
|
allApproved: boolean;
|
||||||
|
documentsOpen: boolean;
|
||||||
phase?: string | null;
|
phase?: string | null;
|
||||||
milestones?: unknown[];
|
milestones?: unknown[];
|
||||||
nextAction?: unknown;
|
nextAction?: unknown;
|
||||||
@@ -758,6 +762,7 @@ export class BookingTransitionService {
|
|||||||
outputCode,
|
outputCode,
|
||||||
documents,
|
documents,
|
||||||
allApproved,
|
allApproved,
|
||||||
|
documentsOpen: clearanceDocumentsOpen(booking),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -800,10 +805,14 @@ export class BookingTransitionService {
|
|||||||
userId?: string,
|
userId?: string,
|
||||||
): Promise<Booking> {
|
): Promise<Booking> {
|
||||||
const booking = await this.bookingsService.findById(bookingId);
|
const booking = await this.bookingsService.findById(bookingId);
|
||||||
assertBookingStatus(booking, [
|
// Documents stay open until the shipment is paid — a customs shipment keeps
|
||||||
"AWAITING_DOCUMENTS",
|
// collecting paperwork (amended invoices, port documents) well past
|
||||||
"DOCUMENTS_UNDER_REVIEW",
|
// clearance finalization. See {@link clearanceDocumentsOpen}.
|
||||||
]);
|
if (!clearanceDocumentsOpen(booking)) {
|
||||||
|
throw new ConflictException(
|
||||||
|
`Clearance documents are closed for this booking (status "${booking.status}").`,
|
||||||
|
);
|
||||||
|
}
|
||||||
const { inputCode } = clearanceCodesForBooking(booking);
|
const { inputCode } = clearanceCodesForBooking(booking);
|
||||||
if (!inputCode) {
|
if (!inputCode) {
|
||||||
throw new BadRequestException(
|
throw new BadRequestException(
|
||||||
@@ -841,19 +850,29 @@ export class BookingTransitionService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.bookingsRepository.update(bookingId, {
|
// Only the pre-finalization submission drives the booking into review.
|
||||||
status: "DOCUMENTS_UNDER_REVIEW",
|
// A later addition (an amended invoice while the shipment is already
|
||||||
} as never);
|
// scheduled) must never rewind the status or reopen the phased workflow —
|
||||||
|
// it lands as a new PENDING document for GL to approve where it stands.
|
||||||
|
const inDocumentPhase =
|
||||||
|
booking.status === "AWAITING_DOCUMENTS" ||
|
||||||
|
booking.status === "DOCUMENTS_UNDER_REVIEW";
|
||||||
|
|
||||||
if (this.isPhasedCustoms(booking)) {
|
if (inDocumentPhase) {
|
||||||
await this.workflowService.onCustomerDocsUploadedForBooking(
|
|
||||||
bookingId,
|
|
||||||
booking.tradeDirection ?? 'IMPORT',
|
|
||||||
);
|
|
||||||
await this.workflowService.onDocumentReviewReopenedForBooking(bookingId);
|
|
||||||
await this.bookingsRepository.update(bookingId, {
|
await this.bookingsRepository.update(bookingId, {
|
||||||
clearanceCurrentPhase: ContractDocPhase.GlEtReview,
|
status: "DOCUMENTS_UNDER_REVIEW",
|
||||||
} as never);
|
} as never);
|
||||||
|
|
||||||
|
if (this.isPhasedCustoms(booking)) {
|
||||||
|
await this.workflowService.onCustomerDocsUploadedForBooking(
|
||||||
|
bookingId,
|
||||||
|
booking.tradeDirection ?? 'IMPORT',
|
||||||
|
);
|
||||||
|
await this.workflowService.onDocumentReviewReopenedForBooking(bookingId);
|
||||||
|
await this.bookingsRepository.update(bookingId, {
|
||||||
|
clearanceCurrentPhase: ContractDocPhase.GlEtReview,
|
||||||
|
} as never);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileKeys = files.map((f) => f.fieldname);
|
const fileKeys = files.map((f) => f.fieldname);
|
||||||
@@ -918,7 +937,14 @@ export class BookingTransitionService {
|
|||||||
note?: string,
|
note?: string,
|
||||||
): Promise<Booking> {
|
): Promise<Booking> {
|
||||||
const booking = await this.bookingsService.findById(bookingId);
|
const booking = await this.bookingsService.findById(bookingId);
|
||||||
assertBookingStatus(booking, ["DOCUMENTS_UNDER_REVIEW"]);
|
// GL keeps reviewing for as long as the customer can still submit — the
|
||||||
|
// two sides share one predicate so they can never drift apart. Documents
|
||||||
|
// added after clearance was finalized still need approving/querying.
|
||||||
|
if (!clearanceDocumentsOpen(booking)) {
|
||||||
|
throw new ConflictException(
|
||||||
|
`Clearance documents are closed for this booking (status "${booking.status}").`,
|
||||||
|
);
|
||||||
|
}
|
||||||
const { inputCode, outputCode } = clearanceCodesForBooking(booking);
|
const { inputCode, outputCode } = clearanceCodesForBooking(booking);
|
||||||
|
|
||||||
const existing =
|
const existing =
|
||||||
@@ -935,15 +961,6 @@ export class BookingTransitionService {
|
|||||||
"A note is required when querying a document",
|
"A note is required when querying a document",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (
|
|
||||||
status === 'QUERIED' &&
|
|
||||||
this.isPhasedCustoms(booking) &&
|
|
||||||
booking.preClearanceFinalizedAt
|
|
||||||
) {
|
|
||||||
throw new BadRequestException(
|
|
||||||
'Customer documents cannot be queried after pre-clearance is finalized.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await this.bookingsRepository.setDocumentReviewStatus(
|
await this.bookingsRepository.setDocumentReviewStatus(
|
||||||
bookingId,
|
bookingId,
|
||||||
@@ -970,7 +987,10 @@ export class BookingTransitionService {
|
|||||||
"CHANGES_REQUESTED",
|
"CHANGES_REQUESTED",
|
||||||
staffId,
|
staffId,
|
||||||
);
|
);
|
||||||
if (this.isPhasedCustoms(booking)) {
|
// Reopening the review phase only makes sense while clearance is still
|
||||||
|
// being decided. Querying a document that arrived afterwards must not
|
||||||
|
// drag a finalized shipment back into the GL review phase.
|
||||||
|
if (this.isPhasedCustoms(booking) && !booking.preClearanceFinalizedAt) {
|
||||||
await this.workflowService.onDocumentReviewReopenedForBooking(bookingId);
|
await this.workflowService.onDocumentReviewReopenedForBooking(bookingId);
|
||||||
await this.bookingsRepository.update(bookingId, {
|
await this.bookingsRepository.update(bookingId, {
|
||||||
clearanceCurrentPhase: ContractDocPhase.GlEtReview,
|
clearanceCurrentPhase: ContractDocPhase.GlEtReview,
|
||||||
@@ -982,7 +1002,10 @@ export class BookingTransitionService {
|
|||||||
if (status === "QUERIED") {
|
if (status === "QUERIED") {
|
||||||
this.notifier.documentQueried(updated, fileKey, note ?? '');
|
this.notifier.documentQueried(updated, fileKey, note ?? '');
|
||||||
}
|
}
|
||||||
if (this.isPhasedCustoms(updated)) {
|
// Same reasoning as the query branch: advance the workflow only while
|
||||||
|
// clearance is still open. Approving a late-added document leaves an
|
||||||
|
// already-finalized shipment's phase exactly where it is.
|
||||||
|
if (this.isPhasedCustoms(updated) && !updated.preClearanceFinalizedAt) {
|
||||||
const allApproved = await this.isClearanceFullyApproved(updated);
|
const allApproved = await this.isClearanceFullyApproved(updated);
|
||||||
if (allApproved) {
|
if (allApproved) {
|
||||||
await this.workflowService.onAllDocsApprovedForBooking(bookingId);
|
await this.workflowService.onAllDocsApprovedForBooking(bookingId);
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { Booking } from './entities/booking.entity';
|
||||||
|
import { clearanceDocumentsOpen } from './clearance.util';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The customer may attach clearance documents — and GL may review them — right
|
||||||
|
* up to payment, not merely until clearance is finalized. Both the upload and
|
||||||
|
* the review endpoint gate on this one predicate, so a drift here silently
|
||||||
|
* desynchronizes the two sides.
|
||||||
|
*/
|
||||||
|
const booking = (patch: Partial<Booking>): Booking =>
|
||||||
|
({ status: 'CLEARANCE_READY', paymentStatus: 'PENDING', ...patch }) as Booking;
|
||||||
|
|
||||||
|
describe('clearanceDocumentsOpen', () => {
|
||||||
|
it('stays open across the whole pre-payment flow', () => {
|
||||||
|
for (const status of [
|
||||||
|
'AWAITING_DOCUMENTS',
|
||||||
|
'DOCUMENTS_UNDER_REVIEW',
|
||||||
|
'CLEARANCE_READY',
|
||||||
|
'OPERATION_REQUEST_PENDING',
|
||||||
|
'SELECTED_FOR_BATCH',
|
||||||
|
'PNR_GENERATED',
|
||||||
|
'PAYMENT_VERIFICATION_IN_PROGRESS',
|
||||||
|
]) {
|
||||||
|
expect(clearanceDocumentsOpen(booking({ status }))).toBe(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('closes once the shipment is paid or finished', () => {
|
||||||
|
for (const status of ['PAID', 'IN_TRANSIT', 'ARRIVED', 'COMPLETED']) {
|
||||||
|
expect(clearanceDocumentsOpen(booking({ status }))).toBe(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('closes on a dead booking', () => {
|
||||||
|
for (const status of ['REJECTED', 'CANCELLED', 'EXPIRED']) {
|
||||||
|
expect(clearanceDocumentsOpen(booking({ status }))).toBe(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('closes when payment settled before the status caught up', () => {
|
||||||
|
expect(
|
||||||
|
clearanceDocumentsOpen(
|
||||||
|
booking({ status: 'PNR_GENERATED', paymentStatus: 'PAID' }),
|
||||||
|
),
|
||||||
|
).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -113,3 +113,36 @@ export function clearanceCodesForBooking(booking: Booking): {
|
|||||||
includesCustoms,
|
includesCustoms,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Statuses after which clearance documents are closed: the shipment is paid
|
||||||
|
* and moving. Everything before that — review, clearance ready, operation
|
||||||
|
* request, batch selection, PNR, payment verification — still accepts new
|
||||||
|
* customer documents and still lets GL review them.
|
||||||
|
*/
|
||||||
|
const CLEARANCE_DOCS_CLOSED_STATUSES = new Set<string>([
|
||||||
|
'PAID',
|
||||||
|
'IN_TRANSIT',
|
||||||
|
'ARRIVED',
|
||||||
|
'COMPLETED',
|
||||||
|
'REJECTED',
|
||||||
|
'CANCELLED',
|
||||||
|
'EXPIRED',
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True while the customer may still attach clearance documents and GL may
|
||||||
|
* still approve or query them.
|
||||||
|
*
|
||||||
|
* Clearance finalization is NOT the cut-off: a customs shipment keeps
|
||||||
|
* collecting paperwork (amended invoices, revised packing lists, port
|
||||||
|
* documents) right up to the final invoice being settled. Both the customer's
|
||||||
|
* upload endpoint and GL's review endpoint gate on this one predicate, so the
|
||||||
|
* two sides can never drift apart.
|
||||||
|
*/
|
||||||
|
export function clearanceDocumentsOpen(booking: Booking): boolean {
|
||||||
|
if (CLEARANCE_DOCS_CLOSED_STATUSES.has(booking.status)) return false;
|
||||||
|
// Payment settled ahead of the status transition (webhook ordering).
|
||||||
|
if (booking.paymentStatus === 'PAID') return false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ import {
|
|||||||
type ClearanceDocEvent,
|
type ClearanceDocEvent,
|
||||||
} from '../bookings/clearance-doc-history.util';
|
} from '../bookings/clearance-doc-history.util';
|
||||||
import { ClearanceEventService } from '../bookings/clearance-event.service';
|
import { ClearanceEventService } from '../bookings/clearance-event.service';
|
||||||
|
import { clearanceDocumentsOpen } from '../bookings/clearance.util';
|
||||||
|
|
||||||
const RO_VESSEL_MIN_DAYS_CODE = 'ro_vessel_min_days';
|
const RO_VESSEL_MIN_DAYS_CODE = 'ro_vessel_min_days';
|
||||||
|
|
||||||
@@ -64,6 +65,7 @@ export interface BookingClearanceView {
|
|||||||
history: ClearanceDocEvent[];
|
history: ClearanceDocEvent[];
|
||||||
}>;
|
}>;
|
||||||
allApproved: boolean;
|
allApproved: boolean;
|
||||||
|
documentsOpen: boolean;
|
||||||
phase?: string | null;
|
phase?: string | null;
|
||||||
milestones?: Array<{
|
milestones?: Array<{
|
||||||
id: string;
|
id: string;
|
||||||
@@ -354,6 +356,7 @@ export class BookingClearanceService {
|
|||||||
outputCode,
|
outputCode,
|
||||||
documents,
|
documents,
|
||||||
allApproved,
|
allApproved,
|
||||||
|
documentsOpen: clearanceDocumentsOpen(booking),
|
||||||
phase,
|
phase,
|
||||||
milestones: milestones.map((m) => ({
|
milestones: milestones.map((m) => ({
|
||||||
id: m.id,
|
id: m.id,
|
||||||
@@ -1161,7 +1164,16 @@ export class BookingClearanceService {
|
|||||||
for (const b of candidates) {
|
for (const b of candidates) {
|
||||||
if (!this.isPhasedCustomsBooking(b)) continue;
|
if (!this.isPhasedCustomsBooking(b)) continue;
|
||||||
const milestones = await this.workflowService.listMilestonesForBooking(b.id);
|
const milestones = await this.workflowService.listMilestonesForBooking(b.id);
|
||||||
if (belongsOnEtClearanceQueue(milestones)) filtered.push(b);
|
if (!belongsOnEtClearanceQueue(milestones)) continue;
|
||||||
|
// Surfaced on the queue row: every required document is approved even
|
||||||
|
// though the booking status stays DOCUMENTS_UNDER_REVIEW until finalize.
|
||||||
|
(b as Booking & { allDocsApproved?: boolean }).allDocsApproved =
|
||||||
|
milestones.some(
|
||||||
|
(m) =>
|
||||||
|
m.milestoneCode === 'DOCUMENTS_APPROVED' &&
|
||||||
|
(m.status === 'COMPLETED' || m.status === 'SKIPPED'),
|
||||||
|
);
|
||||||
|
filtered.push(b);
|
||||||
}
|
}
|
||||||
const rows = await this.attachContractSummary(filtered);
|
const rows = await this.attachContractSummary(filtered);
|
||||||
return this.narrowToYardScope(rows, user);
|
return this.narrowToYardScope(rows, user);
|
||||||
|
|||||||
@@ -149,24 +149,12 @@ export default function DocumentClearanceDetailPage() {
|
|||||||
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
|
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
|
||||||
)[0]?.note ?? null;
|
)[0]?.note ?? null;
|
||||||
|
|
||||||
const docsPhaseComplete =
|
// Documents stay reviewable for as long as the customer can still submit
|
||||||
clearance?.milestones?.some(
|
// them — until the shipment is paid, not merely until clearance is
|
||||||
(m) => m.milestoneCode === "DOCUMENTS_APPROVED" && m.status === "COMPLETED",
|
// finalized. `documentsOpen` is the server's own predicate (the same one
|
||||||
) ?? false;
|
// both the upload and review endpoints gate on), so the buttons are shown
|
||||||
// Querying a document is only possible while the booking is actually in
|
// exactly when the API would accept them.
|
||||||
// review — the server enforces exactly that (reviewDocument asserts
|
const documentsClosed = clearance?.documentsOpen === false;
|
||||||
// DOCUMENTS_UNDER_REVIEW), so once clearance is finalized the button could
|
|
||||||
// only ever produce a 400.
|
|
||||||
//
|
|
||||||
// `preClearanceFinalized` alone was not enough: it is a phased-customs field,
|
|
||||||
// so a non-customs booking (self-clearance, and every shipping-line booking)
|
|
||||||
// never sets it and kept offering Query after Operations had finalized.
|
|
||||||
const queriesLocked =
|
|
||||||
Boolean(
|
|
||||||
(clearance as Freight.ContractClearanceView | undefined)
|
|
||||||
?.preClearanceFinalized,
|
|
||||||
) ||
|
|
||||||
(booking?.status != null && booking.status !== "DOCUMENTS_UNDER_REVIEW");
|
|
||||||
const workflowFiles =
|
const workflowFiles =
|
||||||
(clearance as Freight.ContractClearanceView | undefined)?.workflowFiles ?? [];
|
(clearance as Freight.ContractClearanceView | undefined)?.workflowFiles ?? [];
|
||||||
|
|
||||||
@@ -346,8 +334,8 @@ export default function DocumentClearanceDetailPage() {
|
|||||||
<ClearanceReviewSection
|
<ClearanceReviewSection
|
||||||
bookingId={id!}
|
bookingId={id!}
|
||||||
hideSummary
|
hideSummary
|
||||||
approvalsLocked={isPhasedGeneral && docsPhaseComplete}
|
approvalsLocked={documentsClosed}
|
||||||
queriesLocked={queriesLocked}
|
queriesLocked={documentsClosed}
|
||||||
phasedCustoms={isPhasedGeneral}
|
phasedCustoms={isPhasedGeneral}
|
||||||
onChanged={() => void refetch()}
|
onChanged={() => void refetch()}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -164,6 +164,7 @@ export default function ContractClearanceListPage() {
|
|||||||
tradeDirection: b.tradeDirection ?? "—",
|
tradeDirection: b.tradeDirection ?? "—",
|
||||||
freightType: b.freightType ?? "—",
|
freightType: b.freightType ?? "—",
|
||||||
status: b.status,
|
status: b.status,
|
||||||
|
allDocsApproved: Boolean(b.allDocsApproved),
|
||||||
requested: requestedByBooking.get(b.id) ?? null,
|
requested: requestedByBooking.get(b.id) ?? null,
|
||||||
contractId: b.contractId ?? null,
|
contractId: b.contractId ?? null,
|
||||||
contractReference: b.contractReference ?? null,
|
contractReference: b.contractReference ?? null,
|
||||||
@@ -344,6 +345,8 @@ interface ShipmentBookingRow {
|
|||||||
tradeDirection: string;
|
tradeDirection: string;
|
||||||
freightType: string;
|
freightType: string;
|
||||||
status: string;
|
status: string;
|
||||||
|
/** Every required document approved, even before clearance is finalized. */
|
||||||
|
allDocsApproved: boolean;
|
||||||
/** Requested quantities from the originating shipment request. */
|
/** Requested quantities from the originating shipment request. */
|
||||||
requested: Freight.RequestedShipmentLines | null;
|
requested: Freight.RequestedShipmentLines | null;
|
||||||
/** Contract this shipment booking was created under. */
|
/** Contract this shipment booking was created under. */
|
||||||
@@ -518,13 +521,22 @@ function ShipmentBookingsTable({
|
|||||||
header: () => <span className={bookingTable.headerCell}>Status</span>,
|
header: () => <span className={bookingTable.headerCell}>Status</span>,
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => (
|
||||||
<Group gap={6} wrap="nowrap">
|
<Group gap={6} wrap="nowrap">
|
||||||
<Badge
|
{/* All docs approved but not yet finalized: the booking status is
|
||||||
variant="light"
|
still DOCUMENTS_UNDER_REVIEW — show the real review state. */}
|
||||||
color={shipmentStatusColor(row.original.status)}
|
{row.original.status === "DOCUMENTS_UNDER_REVIEW" &&
|
||||||
radius="sm"
|
row.original.allDocsApproved ? (
|
||||||
>
|
<Badge variant="light" color="edr-green" radius="sm">
|
||||||
{prettyStatus(row.original.status)}
|
Documents approved
|
||||||
</Badge>
|
</Badge>
|
||||||
|
) : (
|
||||||
|
<Badge
|
||||||
|
variant="light"
|
||||||
|
color={shipmentStatusColor(row.original.status)}
|
||||||
|
radius="sm"
|
||||||
|
>
|
||||||
|
{prettyStatus(row.original.status)}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
{row.original.bookingCreated ? (
|
{row.original.bookingCreated ? (
|
||||||
<Tooltip label="Booking created by GL Ethiopia" withArrow>
|
<Tooltip label="Booking created by GL Ethiopia" withArrow>
|
||||||
<Badge
|
<Badge
|
||||||
|
|||||||
@@ -234,6 +234,8 @@ export interface BookingDetail {
|
|||||||
equipmentReturn?: string;
|
equipmentReturn?: string;
|
||||||
customsClearingEnabled?: boolean;
|
customsClearingEnabled?: boolean;
|
||||||
customsClearingAgent?: string | null;
|
customsClearingAgent?: string | null;
|
||||||
|
/** ET clearance queue: every required document approved (pre-finalize). */
|
||||||
|
allDocsApproved?: boolean;
|
||||||
contractKind?: "ONE_TIME" | "GENERAL" | null;
|
contractKind?: "ONE_TIME" | "GENERAL" | null;
|
||||||
contractId?: string | null;
|
contractId?: string | null;
|
||||||
/** Reference of the contract this booking was created under (list column + search). */
|
/** Reference of the contract this booking was created under (list column + search). */
|
||||||
|
|||||||
@@ -209,6 +209,17 @@ export function ClearanceFlow({ booking, flow, footer }: ClearanceFlowProps) {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Clearance is done but the shipment is not paid yet: documents stay
|
||||||
|
open, so say so — otherwise the upload box below reads as leftover UI. */}
|
||||||
|
{canUpload && !isInitialUpload && status !== "DOCUMENTS_UNDER_REVIEW" ? (
|
||||||
|
<Alert color="blue" variant="light" radius="md" mt="md" p="xs">
|
||||||
|
<Text fz="12px">
|
||||||
|
Need to send something else? You can still add documents to this
|
||||||
|
shipment until it is paid.
|
||||||
|
</Text>
|
||||||
|
</Alert>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{canUpload ? (
|
{canUpload ? (
|
||||||
<ClearanceAdHocUploadSection
|
<ClearanceAdHocUploadSection
|
||||||
rows={adHoc}
|
rows={adHoc}
|
||||||
|
|||||||
@@ -116,8 +116,14 @@ export function useClearanceFlow(booking: Freight.IBooking) {
|
|||||||
// just sees that clearance is done and GL is preparing the booking.
|
// just sees that clearance is done and GL is preparing the booking.
|
||||||
const awaitingGlCompletion =
|
const awaitingGlCompletion =
|
||||||
isReady && isBareInstance && Boolean(clearance?.includesCustoms);
|
isReady && isBareInstance && Boolean(clearance?.includesCustoms);
|
||||||
|
// Documents stay open until the shipment is paid: a customs shipment keeps
|
||||||
|
// collecting paperwork (amended invoices, revised packing lists) well past
|
||||||
|
// clearance finalization. `documentsOpen` is the server's own predicate, so
|
||||||
|
// the upload control appears exactly when the API would accept a file.
|
||||||
|
// Older API builds omit the field — fall back to the previous rule there.
|
||||||
const canUpload =
|
const canUpload =
|
||||||
status === "AWAITING_DOCUMENTS" || status === "DOCUMENTS_UNDER_REVIEW";
|
clearance?.documentsOpen ??
|
||||||
|
(status === "AWAITING_DOCUMENTS" || status === "DOCUMENTS_UNDER_REVIEW");
|
||||||
// The very first upload (nothing in review yet). Here every required document
|
// The very first upload (nothing in review yet). Here every required document
|
||||||
// must be provided. Once GL has started reviewing (DOCUMENTS_UNDER_REVIEW) the
|
// must be provided. Once GL has started reviewing (DOCUMENTS_UNDER_REVIEW) the
|
||||||
// customer is only re-uploading queried/pending docs, so we don't re-gate on
|
// customer is only re-uploading queried/pending docs, so we don't re-gate on
|
||||||
|
|||||||
@@ -924,6 +924,12 @@ export interface ClearanceView {
|
|||||||
documents: ClearanceDocument[];
|
documents: ClearanceDocument[];
|
||||||
/** True once every required customer document is APPROVED (the 100% gate). */
|
/** True once every required customer document is APPROVED (the 100% gate). */
|
||||||
allApproved: boolean;
|
allApproved: boolean;
|
||||||
|
/**
|
||||||
|
* True while the customer may still attach documents and GL may still
|
||||||
|
* approve or query them — open until the shipment is paid, not merely until
|
||||||
|
* clearance is finalized.
|
||||||
|
*/
|
||||||
|
documentsOpen?: boolean;
|
||||||
/** Phased clearance (GENERAL + customs per-booking). */
|
/** Phased clearance (GENERAL + customs per-booking). */
|
||||||
phase?: ContractDocPhase | null;
|
phase?: ContractDocPhase | null;
|
||||||
milestones?: IClearanceMilestone[];
|
milestones?: IClearanceMilestone[];
|
||||||
|
|||||||
Reference in New Issue
Block a user