mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
CAC Bank is an OTP debit with no redirect and no webhook: initiate SMSes a code to the payer's mobile, and the charge only settles when that code is confirmed. The payment service already spoke it (passenger uses it); the freight side had the enum values but none of the flow. API: - PaymentClientService.confirmOtp forwards the code to POST /payments/intents/:id/confirm, mapping 400/404 to BadRequest so a mistyped code stays retryable instead of surfacing as a gateway failure. - PaymentService.confirmOtp is keyed by the LOCAL intent id (the invoice's paymentId) rather than the domain reference, so the right invoice settles when several share a booking. On success billing settles the invoice. - payInvoice rejects CAC_BANK without payerAccount before calling the gateway, and no longer runs the demo auto-settle for a COLLECT_OTP intent (it is not paid until the payer confirms). - POST /billing/my-invoices/:id/confirm — ownership-checked, and since warehouse fee invoices are central invoices it covers those too. Portal: - useInvoicePayment owns the whole flow (initiate, redirect-or-OTP, confirm) and replaces the five near-identical pay mutations at the call sites. - PaymentMethodModal gains the CAC Bank option, the payer mobile field, and the OTP step. Click-outside is disabled there so a stray click cannot drop the payer out of a live OTP window. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
83 lines
2.5 KiB
TypeScript
83 lines
2.5 KiB
TypeScript
import type { Freight } from "@edr/types";
|
|
|
|
import { URL_CONSTANTS } from "@/constants/URLS";
|
|
import { client } from "../utils/api";
|
|
import type { InitiateResponse } from "./payments.service";
|
|
|
|
const B = URL_CONSTANTS.BILLING;
|
|
|
|
/** A customer-facing invoice row, as returned by `GET /billing/my-invoices`. */
|
|
export type PortalInvoice = Freight.IInvoice;
|
|
|
|
/** An invoice plus its line items, as returned by `GET /billing/my-invoices/:id`. */
|
|
export type PortalInvoiceDetail = Freight.IInvoice & {
|
|
lines: Freight.IInvoiceLine[];
|
|
};
|
|
|
|
export interface PayInvoicePayload {
|
|
method?: string;
|
|
platform?: "web" | "mobile";
|
|
payerAccount?: string;
|
|
returnUrl?: string;
|
|
failureUrl?: string;
|
|
}
|
|
|
|
export const invoicesService = {
|
|
/** Every invoice billed to the signed-in customer's company, newest first. */
|
|
listMy: async (): Promise<PortalInvoice[]> => {
|
|
const { data } = await client.get(B.MY_INVOICES);
|
|
return data.data ?? data;
|
|
},
|
|
|
|
/** The customer's invoices for one source record (e.g. a booking). */
|
|
listForSource: async (
|
|
source: string,
|
|
sourceId: string,
|
|
): Promise<PortalInvoice[]> => {
|
|
const { data } = await client.get(B.MY_INVOICES, {
|
|
params: { source, sourceId },
|
|
});
|
|
return data.data ?? data;
|
|
},
|
|
|
|
/** One of the customer's invoices, with its line items. */
|
|
get: async (id: string): Promise<PortalInvoiceDetail> => {
|
|
const { data } = await client.get(B.MY_INVOICE_BY_ID(id));
|
|
return data.data ?? data;
|
|
},
|
|
|
|
/** The sealed invoice PDF for one of the customer's invoices. */
|
|
downloadDocument: async (id: string): Promise<Blob> => {
|
|
const { data } = await client.get(B.MY_INVOICE_DOCUMENT(id), {
|
|
responseType: "blob",
|
|
});
|
|
return data;
|
|
},
|
|
|
|
/** The sealed payment-receipt PDF (available once paid). */
|
|
downloadReceipt: async (id: string): Promise<Blob> => {
|
|
const { data } = await client.get(B.MY_INVOICE_RECEIPT(id), {
|
|
responseType: "blob",
|
|
});
|
|
return data;
|
|
},
|
|
|
|
/** Initiate gateway payment for an open invoice; returns the client action. */
|
|
pay: async (
|
|
id: string,
|
|
payload: PayInvoicePayload = {},
|
|
): Promise<InitiateResponse> => {
|
|
const { data } = await client.post(B.PAY_INVOICE(id), {
|
|
platform: "web",
|
|
...payload,
|
|
});
|
|
return data.data ?? data;
|
|
},
|
|
|
|
/** Submit the CAC Bank OTP for an invoice whose intent is awaiting confirmation. */
|
|
confirmOtp: async (id: string, otp: string): Promise<InitiateResponse> => {
|
|
const { data } = await client.post(B.CONFIRM_INVOICE_OTP(id), { otp });
|
|
return data.data ?? data;
|
|
},
|
|
};
|