New Transit Agents admin table (name, valid-from/to, active/suspended, validity badge) — DJ assign step now picks from it, active+valid only.

This commit is contained in:
Marshal
2026-07-29 05:38:35 +00:00
parent 891311d861
commit b17a72c280
59 changed files with 2903 additions and 451 deletions

View File

@@ -317,11 +317,11 @@ export class BookingLifecycleNotifierService {
});
}
/** GL raised the final (post-offload) invoice — customer pays + uploads slip. */
/** GL raised the final (post-offload) invoice — customer approves, pays, uploads slip. */
finalInvoiceCreated(b: Booking, amount: number, currency: string): void {
const msg =
`A final invoice of ${amount} ${currency} has been issued for booking ${b.reference}. ` +
`Please pay and upload the payment slip from the portal.`;
`A final invoice of ${amount} ${currency} has been raised for booking ${b.reference}. ` +
`Please review and approve it in the portal, then pay and upload the payment slip.`;
void this.notifyContact(b, msg, 'FINAL INVOICE');
this.inApp(b, 'Final invoice issued', msg, {
type: NotificationType.INVOICE_ISSUED,
@@ -361,6 +361,15 @@ export class BookingLifecycleNotifierService {
);
}
/** Customer approved the GL Djibouti final invoice — payment slip can follow. */
finalInvoiceApprovedToStaff(b: Booking): void {
this.inAppStaff(
b,
'Final invoice approved',
`The customer approved the final invoice for booking ${this.ref(b)} — awaiting payment slip.`,
);
}
/** Customer signed the booking contract. */
customerSignedToStaff(b: Booking): void {
this.inAppStaff(
@@ -392,16 +401,27 @@ export class BookingLifecycleNotifierService {
);
}
/**
* The customer disputed the advised duty & tax. This goes to STAFF, not the
* customer: GL Ethiopia is the one who has to re-advise, and the clearance
* page is where they do it.
*/
dutyDisputed(b: Booking, note: string): void {
/** GL Ethiopia sent a draft customs declaration — the customer must accept or request a change. */
draftDeclarationReady(b: Booking, price: number, currency: string): void {
const msg =
`The customer disputed the duty & tax advised on booking ${this.ref(b)}: ` +
`"${note}". Review and re-advise the amount on the clearance page.`;
this.inAppStaff(b, `Duty disputed on ${this.ref(b)}`, msg, {
`A draft customs declaration for booking ${b.reference} is ready for your review — ` +
`estimated price ${price} ${currency}. Please accept it or request a change from the portal.`;
void this.notifyContact(b, msg, 'DRAFT DECLARATION READY');
this.inApp(b, 'Draft declaration ready for review', msg, {
type: NotificationType.DOCUMENT_ACTION,
});
}
/**
* The customer asked for a change on the draft declaration. This goes to
* STAFF, not the customer: GL Ethiopia is the one who has to send a
* corrected draft, and the clearance page is where they do it.
*/
draftDeclarationChangeRequested(b: Booking, note: string): void {
const msg =
`The customer requested a change to the draft declaration on booking ${this.ref(b)}: ` +
`"${note}". Send a corrected draft from the clearance page.`;
this.inAppStaff(b, `Draft declaration change requested — ${this.ref(b)}`, msg, {
type: NotificationType.CLEARANCE_REVIEW,
link: `/dashboard/bookings/${b.id}/clearance`,
});

View File

@@ -841,13 +841,13 @@ export class BookingsController {
@BookingStaff(FREIGHT_PERMS.contracts.clearanceDjActions)
@ApiOperation({
summary:
'GL Djibouti names the transit officer (free text) — unblocks the customs declaration; calling again reassigns',
'GL Djibouti picks the transit officer from the roster — unblocks the customs declaration; calling again reassigns',
})
async assignBookingTransitAssignee(
@Param('id', ParseUUIDPipe) id: string,
@Body('assignee') assignee: string,
@Body('transitAgentId', ParseUUIDPipe) transitAgentId: string,
) {
const booking = await this.bookingClearanceService.assignTransitAssignee(id, assignee);
const booking = await this.bookingClearanceService.assignTransitAssignee(id, transitAgentId);
return this.transitionService.enrichBookingResponse(booking);
}
@@ -900,17 +900,52 @@ export class BookingsController {
return this.transitionService.enrichBookingResponse(booking);
}
@Post(':id/clearance/duty/dispute')
@Post(':id/clearance/draft-declaration')
@BookingStaff(FREIGHT_PERMS.contracts.clearanceEtActions)
@UseInterceptors(AnyFilesInterceptor())
@ApiConsumes('multipart/form-data')
@ApiOperation({
summary:
'Customer disputes the advised duty/tax with a reason — reopens the step so GL Ethiopia can re-advise (repeatable)',
'GL ET sends a draft customs declaration (multi-file) with an estimated price for the customer to review',
})
async disputeBookingDuty(
async uploadBookingDraftDeclaration(
@Param('id', ParseUUIDPipe) id: string,
@Body('price') priceRaw: string,
@Body('currency') currency: string | undefined,
@UploadedFiles() files: Express.Multer.File[],
@CurrentUser() user: TCurrentUser,
) {
const booking = await this.bookingClearanceService.uploadDraftDeclaration(
id,
files ?? [],
Number(priceRaw),
currency ?? 'ETB',
resolveAuthUserId(user),
);
return this.transitionService.enrichBookingResponse(booking);
}
@Post(':id/clearance/draft-declaration/accept')
@ApiOperation({
summary:
'Customer accepts the draft customs declaration — unlocks the real customs declaration step for GL Ethiopia',
})
async acceptBookingDraftDeclaration(@Param('id', ParseUUIDPipe) id: string) {
const booking = await this.bookingClearanceService.acceptDraftDeclaration(id);
return this.transitionService.enrichBookingResponse(booking);
}
@Post(':id/clearance/draft-declaration/change')
@ApiOperation({
summary:
'Customer requests a change to the draft customs declaration with a reason — GL Ethiopia sends a corrected draft (repeatable)',
})
async requestBookingDraftDeclarationChange(
@Param('id', ParseUUIDPipe) id: string,
@Body('note') note: string,
@CurrentUser() user: TCurrentUser,
) {
const booking = await this.bookingClearanceService.disputeDuty(
const booking = await this.bookingClearanceService.requestDraftDeclarationChange(
id,
note,
resolveAuthUserId(user),

View File

@@ -7,10 +7,10 @@ export const REVIEW_NOTE_TYPES = [
'REJECTION',
'STAFF_NOTE',
/**
* The customer disputed the advised duty & tax and asked GL Ethiopia to
* correct it. One row per round — the advice/dispute loop can repeat.
* The customer asked GL Ethiopia to correct the draft customs declaration
* (price/files). One row per round — the draft/change-request loop can repeat.
*/
'DUTY_DISPUTE',
'DRAFT_DECL_CHANGE_REQUEST',
] as const;
export type ReviewNoteType = (typeof REVIEW_NOTE_TYPES)[number];