import { useCallback, useRef, createContext, useContext, ReactNode } from 'react'; import { environment } from '../environments/environment.ts'; import { useHttpService } from './HttpService.tsx'; import { buildCheckoutRequestBody, classifyCheckoutAnswer, classifyCheckoutFailure, type CheckoutExpected, type CheckoutIntent, type CheckoutOutcome, } from '../utils/invoiceCheckoutAttempt'; import type { SaveTransport } from '../utils/saveOperation'; // Invoicing Phase 3, Slice B (direct patient payments, BYO processor) - the CLIENT half of the // money-moving flow. The org authors an invoice in Akashic; the patient pays the practice via the // practice's OWN processor. These two routes live behind `fhirBaseApi` (the SAME base InvoiceService // uses - /R4/payment/...), NOT apiUrl; a wrong base is a silently-swallowed 404. const API_URL = environment.fhirBaseApi; export type { CheckoutOutcome, CheckoutIntent, CheckoutExpected }; // GET /payment/session/{id} response (F08) - `settled` is the ONLY field that means "this checkout's // payment is confirmed in Akashic"; `paid` is just the provider's own say-so, and `invoicePaid` can be // true from an entirely different payment. `operationId` echoes back what was asked for, if anything. export type PaymentSessionStatus = { sessionStatus: 'paid' | 'unpaid' | 'expired' | 'unknown'; invoiceStatus: string; paid: boolean; settled: boolean; invoicePaid: boolean; operationId?: string; }; // The caller's operation admits each request after its token and proof waits, and cancels it once retired. const owned = (transport?: SaveTransport) => (transport ? { admit: transport.admit, signal: transport.signal } : {}); export const usePaymentService = () => { const { get, post } = useHttpService(); // One in-flight checkout request per operation: a same-turn double click (the button disables, but a // stray second event can still race it) is serialized here rather than opening two requests. const inFlight = useRef(new Map>()); // POST {fhirBaseApi}/payment/checkout - see invoiceCheckoutAttempt.ts for the request/typed-result // shapes. Always sends the full original request (operationId, intent, the expected balance shown // when the press was admitted); never throws - every endpoint answer, including a lost/unreadable // one, comes back as a typed CheckoutOutcome so the caller branches on `kind`, never on a thrown // status. const createCheckout = useCallback( (params: { invoiceId: string; operationId: string; intent: CheckoutIntent; amountCents?: number; expected: CheckoutExpected; successUrl: string; cancelUrl: string; }, transport?: SaveTransport): Promise => { const running = inFlight.current.get(params.operationId); if (running) return running; const call = (async () => { try { const res = await post(`${API_URL}/payment/checkout`, buildCheckoutRequestBody(params), owned(transport)); return classifyCheckoutAnswer(res); } catch (err) { return classifyCheckoutFailure(err); } })(); inFlight.current.set(params.operationId, call); void call.finally(() => { if (inFlight.current.get(params.operationId) === call) inFlight.current.delete(params.operationId); }); return call; }, [post], ); // GET {fhirBaseApi}/payment/session/{sessionId}?operationId=. `operationId` is sent whenever the // caller has one stored, so the server reads back the ORIGINAL bound operation rather than // whichever gateway/account is configured now. Throws on a non-ok response so the return poll can // decide to keep retrying; the hook treats any throw as "not yet confirmed". const getPaymentSessionStatus = useCallback( async (sessionId: string, operationId?: string, transport?: SaveTransport): Promise => { const query = operationId ? `?operationId=${encodeURIComponent(operationId)}` : ''; return get(`${API_URL}/payment/session/${encodeURIComponent(sessionId)}${query}`, owned(transport)); }, [get], ); return { createCheckout, getPaymentSessionStatus }; }; const PaymentServiceContext = createContext | undefined>(undefined); export const PaymentServiceProvider = ({ children }: { children: ReactNode }) => { const paymentService = usePaymentService(); return ( {children} ); }; export const usePaymentServiceContext = () => { const context = useContext(PaymentServiceContext); if (context === undefined) { throw new Error('usePaymentServiceContext must be used within a PaymentServiceProvider'); } return context; };