// Reads each issued bill's payment ledger for the bills list, a few at a time. import { useEffect, useState } from 'react'; import type { Invoice, PaymentReconciliation } from 'fhir/r4'; import { useInvoiceServiceContext } from '../../../services'; import { ownedRead } from '../../../utils/ownedRead'; import { PRACTICE_READ_DEADLINE_MS } from './usePatientBills'; export type RowLedger = { read: 'ready'; payments: PaymentReconciliation[] } | { read: 'failed' }; const LEDGER_READS_IN_FLIGHT = 4; // Only an issued bill can be part paid, so only those are read. const ledgerKey = (invoices: Invoice[]): string => invoices .filter((invoice) => invoice.status === 'issued' && invoice.id) .map((invoice) => `${invoice.id}@${invoice.meta?.versionId ?? invoice.meta?.lastUpdated ?? ''}`) .join('|'); export const useInvoiceLedgers = ( invoices: Invoice[], enabled: boolean, refreshKey: number, // On the patients host each bill's payments are read through the practice that owns it. A bill with no known // practice, or a ledger that stopped short, is unknown: no remaining balance is stated from it. practiceOf?: ReadonlyMap, ): ReadonlyMap => { const { fetchInvoicePayments, fetchInvoicePaymentsAtPractice } = useInvoiceServiceContext(); const [ledgers, setLedgers] = useState>(new Map()); const key = enabled ? ledgerKey(invoices) : ''; useEffect(() => { setLedgers(new Map()); if (!key) return; let active = true; const owned = { deadlineMs: PRACTICE_READ_DEADLINE_MS, owns: () => active, track: new Set() }; const ids = key.split('|').map((entry) => entry.slice(0, entry.lastIndexOf('@'))); let cursor = 0; const record = (id: string, ledger: RowLedger) => { if (!active) return; setLedgers((current) => new Map(current).set(id, ledger)); }; const runNext = async (): Promise => { while (active && cursor < ids.length) { const id = ids[cursor++]; try { if (practiceOf) { const practiceId = practiceOf.get(id); // Bounded and owned: one that never answers frees its worker and reads as unknown. const ledger = practiceId ? await ownedRead((request) => fetchInvoicePaymentsAtPractice(id, practiceId, request), owned) : undefined; record(id, ledger && !ledger.truncated ? { read: 'ready', payments: ledger.payments } : { read: 'failed' }); continue; } record(id, { read: 'ready', payments: await fetchInvoicePayments(id) }); } catch (ledgerError) { console.error('Failed to load invoice payments:', ledgerError); record(id, { read: 'failed' }); } } }; void Promise.all( Array.from({ length: Math.min(LEDGER_READS_IN_FLIGHT, ids.length) }, () => runNext()), ); return () => { active = false; owned.track.forEach((controller) => controller.abort()); }; // The service identity churns with its shared cache; the key covers what changes the answer. // eslint-disable-next-line react-hooks/exhaustive-deps }, [key, refreshKey]); return ledgers; };