feat(bookings): add Additional Payments tab, add-charge modal, portal pay

Backoffice: new tab on the booking detail page (?tab=additional-charges,
already linked from the row menu) with an Add Charge modal — Save draft or
Send to customer.

Portal: charges panel on the booking detail page, Pay now per charge via
the existing OTP/CBE-bill payment flow.

Also fixes: GET /bookings/:id/additional-charges was returning DRAFT
charges to the customer (hidden client-side only) — now filtered
server-side per caller.
This commit is contained in:
Hagernesh
2026-08-20 12:54:37 +00:00
parent 0ceb595714
commit 6e6d6329b6
7 changed files with 613 additions and 3 deletions

View File

@@ -508,6 +508,53 @@ export const bookingsService = {
return unwrap(response.data) as Freight.ClearanceCharge[];
},
// ── Additional charges (ad-hoc finance billing) ──
getAdditionalCharges: async (id: string): Promise<Freight.AdditionalCharge[]> => {
const response = await client.get(`/bookings/${id}/additional-charges`);
return unwrap(response.data) as Freight.AdditionalCharge[];
},
/** Finance raises a new charge — 'draft' just saves it, 'send' also issues the invoice and notifies the customer. */
createAdditionalCharge: async (
id: string,
payload: { reason: string; amount: number; currency: string; action: "draft" | "send"; file?: File | null },
): Promise<Freight.AdditionalCharge[]> => {
const form = new FormData();
form.append("reason", payload.reason);
form.append("amount", String(payload.amount));
form.append("currency", payload.currency);
form.append("action", payload.action);
if (payload.file) form.append("file", payload.file);
const response = await client.post(`/bookings/${id}/additional-charges`, form, {
headers: { "Content-Type": "multipart/form-data" },
});
return unwrap(response.data) as Freight.AdditionalCharge[];
},
/** Issues the draft charge's payable invoice and notifies the customer. */
sendAdditionalCharge: async (
id: string,
chargeId: string,
): Promise<Freight.AdditionalCharge[]> => {
const response = await client.post(
`/bookings/${id}/additional-charges/${chargeId}/send`,
);
return unwrap(response.data) as Freight.AdditionalCharge[];
},
/** Withdraws a draft or unpaid additional charge. */
cancelAdditionalCharge: async (
id: string,
chargeId: string,
reason?: string,
): Promise<Freight.AdditionalCharge[]> => {
const response = await client.post(
`/bookings/${id}/additional-charges/${chargeId}/cancel`,
{ reason },
);
return unwrap(response.data) as Freight.AdditionalCharge[];
},
/** GL ET asks Djibouti to name the officer handling the shipment in transit. */
requestTransitAssignee: (id: string, note?: string) =>
postBooking<BookingDetail>(B.CLEARANCE_TRANSIT_ASSIGNEE_REQUEST(id), {