import { useMutation } from "@tanstack/react-query"; import type { AxiosError } from "axios"; import { useState } from "react"; import { invoicesService } from "@/services/invoices.service"; import { paymentsService, type InitiateResponse, type PaymentMethod, } from "@/services/payments.service"; /** How the invoice is charged — overridable for warehouse fee invoices. */ type InitiateFn = ( invoiceId: string, method: PaymentMethod, payerAccount?: string, ) => Promise; const payViaBilling: InitiateFn = (invoiceId, method, payerAccount) => invoicesService.pay(invoiceId, { method, platform: "web", payerAccount }); /** The server's message (`{ message }` / `{ message: [] }`), or a fallback. */ function apiMessage(err: unknown, fallback: string): string { const message = (err as AxiosError<{ message?: string | string[] }>)?.response ?.data?.message; const first = Array.isArray(message) ? message[0] : message; return first || fallback; } /** * One payment flow for every "pay this invoice" entry point: initiate, then * either redirect to the provider or — for CAC Bank, an OTP debit with no * redirect — collect the SMS'd code and confirm it in-app. Pass `initiate` to * charge through a different endpoint (warehouse fee invoices); OTP * confirmation always goes through billing, which owns the intent either way. */ export function useInvoicePayment(initiate: InitiateFn = payViaBilling) { const [otpInvoiceId, setOtpInvoiceId] = useState(null); const [otpMessage, setOtpMessage] = useState(); const payMutation = useMutation({ mutationFn: (vars: { invoiceId: string; method: PaymentMethod; payerAccount?: string; }) => initiate(vars.invoiceId, vars.method, vars.payerAccount), onSuccess: (data, vars) => { if (data?.clientAction?.type === "COLLECT_OTP") { setOtpMessage( data.clientAction.message ?? "Enter the OTP sent to your phone", ); setOtpInvoiceId(vars.invoiceId); return; } window.location.href = data?.clientAction?.type === "REDIRECT" && data.clientAction.url ? data.clientAction.url : paymentsService.checkoutUrlForInvoice({ invoiceId: vars.invoiceId, method: vars.method, }); }, }); const otpMutation = useMutation({ mutationFn: (otp: string) => invoicesService.confirmOtp(otpInvoiceId as string, otp), // Settled — reload so the invoice/booking re-reads its now-paid state. onSuccess: () => { setOtpInvoiceId(null); window.location.reload(); }, }); const reset = () => { payMutation.reset(); otpMutation.reset(); setOtpInvoiceId(null); }; return { processing: payMutation.isPending, error: payMutation.isError ? apiMessage( payMutation.error, payMutation.error instanceof Error ? payMutation.error.message : "Could not start payment. Please try again.", ) : null, pay: (invoiceId: string, method: PaymentMethod, payerAccount?: string) => payMutation.mutate({ invoiceId, method, payerAccount }), reset, /** Drives the modal's OTP step; `open` only for CAC Bank. */ otp: { open: otpInvoiceId !== null, message: otpMessage, submitting: otpMutation.isPending, // A wrong/expired OTP is a 400 — keep the step open so the payer retries. error: otpMutation.isError ? apiMessage( otpMutation.error, "Invalid or expired OTP. Please try again.", ) : null, submit: (otp: string) => otpMutation.mutate(otp), cancel: () => { otpMutation.reset(); setOtpInvoiceId(null); }, }, }; } export type InvoicePaymentFlow = ReturnType;