mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-29 14:08:11 +00:00
fix(portal): show declaration, T1 and Djibouti clearance documents to the customer
This commit is contained in:
@@ -47,6 +47,7 @@ export const AUDIT_ENDPOINTS: Readonly<Record<string, AuditEndpointMeta>> = {
|
||||
"POST /api/bookings/:id/clearance/proceed": ["Customer requests operation with a schedule day", "POST", "Booking"],
|
||||
"POST /api/bookings/:id/clearance/release-order": ["Upload Booking Release Order", "POST", "Booking"],
|
||||
"POST /api/bookings/:id/clearance/review": ["GL reviews a clearance document (Approve | Query)", "POST", "Booking"],
|
||||
"POST /api/bookings/:id/clearance/doc-requests": ["GL asks the customer for additional clearance documents", "POST", "Booking"],
|
||||
"POST /api/bookings/:id/clearance/charges/port-document": ["GL Djibouti uploads the port-charges document", "POST", "Booking"],
|
||||
"PATCH /api/bookings/:id/clearance/charges/:chargeId/bill": ["GL Ethiopia sets or revises a clearance charge's amount + currency", "PATCH", "Booking"],
|
||||
"POST /api/bookings/:id/clearance/charges/:chargeId/send": ["GL Ethiopia issues the clearance charge invoice to the customer", "POST", "Booking"],
|
||||
|
||||
@@ -202,6 +202,17 @@ export class BookingLifecycleNotifierService {
|
||||
}
|
||||
|
||||
/** A clearance document was queried and needs the customer to re-upload. */
|
||||
/** GL asked the customer for additional clearance document(s). */
|
||||
additionalDocsRequested(b: Booking, note: string): void {
|
||||
const msg =
|
||||
`Additional document(s) requested on booking ${b.reference}: ` +
|
||||
`${note} Please upload them from the portal.`;
|
||||
void this.notifyContact(b, msg, 'ADDITIONAL DOCUMENTS REQUESTED');
|
||||
this.inApp(b, 'Additional documents requested', msg, {
|
||||
type: NotificationType.DOCUMENT_ACTION,
|
||||
});
|
||||
}
|
||||
|
||||
documentQueried(b: Booking, fileKey: string, note: string): void {
|
||||
const msg =
|
||||
`A clearance document on booking ${b.reference} needs attention: "${fileKey}". ` +
|
||||
|
||||
@@ -643,6 +643,12 @@ export class BookingTransitionService {
|
||||
}>;
|
||||
allApproved: boolean;
|
||||
documentsOpen: boolean;
|
||||
docRequests: Array<{
|
||||
id: string;
|
||||
note: string;
|
||||
byName: string | null;
|
||||
at: string;
|
||||
}>;
|
||||
phase?: string | null;
|
||||
milestones?: unknown[];
|
||||
nextAction?: unknown;
|
||||
@@ -674,9 +680,14 @@ export class BookingTransitionService {
|
||||
bookingId,
|
||||
"CHANGES_REQUESTED",
|
||||
);
|
||||
const docRequestNotes = await this.bookingsRepository.findReviewNotes(
|
||||
bookingId,
|
||||
"ADDITIONAL_DOC_REQUEST",
|
||||
);
|
||||
const reviewerNames = await this.bookingsRepository.resolveStaffNames([
|
||||
...reviews.map((r) => r.reviewedByStaffId),
|
||||
...queryNotes.map((n) => n.authorId),
|
||||
...docRequestNotes.map((n) => n.authorId),
|
||||
]);
|
||||
|
||||
const documents: Awaited<
|
||||
@@ -763,9 +774,51 @@ export class BookingTransitionService {
|
||||
documents,
|
||||
allApproved,
|
||||
documentsOpen: clearanceDocumentsOpen(booking),
|
||||
docRequests: docRequestNotes.map((n) => ({
|
||||
id: n.id,
|
||||
note: n.note,
|
||||
byName: n.authorId ? (reviewerNames.get(n.authorId) ?? null) : null,
|
||||
at: n.createdAt.toISOString(),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* GL asks the customer for additional clearance document(s). Stored as a
|
||||
* review-note thread shown on both the GL clearance page and the customer's
|
||||
* portal; the customer answers with an ad-hoc upload. Allowed for as long as
|
||||
* documents are open (until the shipment is paid).
|
||||
*/
|
||||
async requestAdditionalDocuments(
|
||||
bookingId: string,
|
||||
note: string,
|
||||
staffId: string,
|
||||
): Promise<void> {
|
||||
const booking = await this.bookingsService.findById(bookingId);
|
||||
if (!clearanceDocumentsOpen(booking)) {
|
||||
throw new ConflictException(
|
||||
`Clearance documents are closed for this booking (status "${booking.status}").`,
|
||||
);
|
||||
}
|
||||
if (!note?.trim()) {
|
||||
throw new BadRequestException("Describe the document(s) you need.");
|
||||
}
|
||||
await this.bookingsRepository.createReviewNote(
|
||||
bookingId,
|
||||
note.trim(),
|
||||
"ADDITIONAL_DOC_REQUEST",
|
||||
staffId,
|
||||
);
|
||||
await this.clearanceEvents.record({
|
||||
bookingId,
|
||||
action: "ADDITIONAL_DOCS_REQUESTED",
|
||||
label: "Requested additional document(s) from the customer",
|
||||
actorId: staffId,
|
||||
metadata: { note: note.trim() },
|
||||
});
|
||||
this.notifier.additionalDocsRequested(booking, note.trim());
|
||||
}
|
||||
|
||||
/**
|
||||
* True when every REQUIRED field of the booking's customer-input clearance set
|
||||
* has an APPROVED review row. The 100% gate before clearance can be finalized.
|
||||
|
||||
@@ -1083,6 +1083,25 @@ export class BookingsController {
|
||||
return this.transitionService.enrichBookingResponse(booking);
|
||||
}
|
||||
|
||||
@Post(":id/clearance/doc-requests")
|
||||
@BookingStaff(FREIGHT_PERMS.contracts.clearanceEtActions)
|
||||
@ApiOperation({
|
||||
summary:
|
||||
"GL asks the customer for additional clearance document(s) — shown on the portal with author and time",
|
||||
})
|
||||
async requestAdditionalDocuments(
|
||||
@Param("id", ParseUUIDPipe) id: string,
|
||||
@Body("note") note: string,
|
||||
@CurrentUser() user: AuthUserPayload,
|
||||
) {
|
||||
await this.transitionService.requestAdditionalDocuments(
|
||||
id,
|
||||
note,
|
||||
resolveAuthUserId(user),
|
||||
);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
@Get(":id/clearance/history")
|
||||
@BookingStaff([
|
||||
FREIGHT_PERMS.contracts.clearanceEtActions,
|
||||
|
||||
@@ -11,6 +11,12 @@ export const REVIEW_NOTE_TYPES = [
|
||||
* (price/files). One row per round — the draft/change-request loop can repeat.
|
||||
*/
|
||||
'DRAFT_DECL_CHANGE_REQUEST',
|
||||
/**
|
||||
* GL asked the customer for additional clearance document(s). Shown as a
|
||||
* thread on both the GL clearance page and the customer's portal — the
|
||||
* customer answers by uploading an ad-hoc document.
|
||||
*/
|
||||
'ADDITIONAL_DOC_REQUEST',
|
||||
] as const;
|
||||
export type ReviewNoteType = (typeof REVIEW_NOTE_TYPES)[number];
|
||||
|
||||
|
||||
@@ -66,6 +66,12 @@ export interface BookingClearanceView {
|
||||
}>;
|
||||
allApproved: boolean;
|
||||
documentsOpen: boolean;
|
||||
docRequests: Array<{
|
||||
id: string;
|
||||
note: string;
|
||||
byName: string | null;
|
||||
at: string;
|
||||
}>;
|
||||
phase?: string | null;
|
||||
milestones?: Array<{
|
||||
id: string;
|
||||
@@ -206,9 +212,14 @@ export class BookingClearanceService {
|
||||
bookingId,
|
||||
'CHANGES_REQUESTED',
|
||||
);
|
||||
const docRequestNotes = await this.bookingsRepository.findReviewNotes(
|
||||
bookingId,
|
||||
'ADDITIONAL_DOC_REQUEST',
|
||||
);
|
||||
const reviewerNames = await this.bookingsRepository.resolveStaffNames([
|
||||
...reviews.map((r) => r.reviewedByStaffId),
|
||||
...queryNotes.map((n) => n.authorId),
|
||||
...docRequestNotes.map((n) => n.authorId),
|
||||
]);
|
||||
|
||||
const documents: BookingClearanceView['documents'] = [];
|
||||
@@ -357,6 +368,12 @@ export class BookingClearanceService {
|
||||
documents,
|
||||
allApproved,
|
||||
documentsOpen: clearanceDocumentsOpen(booking),
|
||||
docRequests: docRequestNotes.map((n) => ({
|
||||
id: n.id,
|
||||
note: n.note,
|
||||
byName: n.authorId ? (reviewerNames.get(n.authorId) ?? null) : null,
|
||||
at: n.createdAt.toISOString(),
|
||||
})),
|
||||
phase,
|
||||
milestones: milestones.map((m) => ({
|
||||
id: m.id,
|
||||
|
||||
Reference in New Issue
Block a user