// src/pages/DashboardPage/BillingPage/invoicePrintModel.ts // Pure assembly of one Invoice + the org Organization into the display strings PrintableInvoice renders. // Reuses the frozen invoice readers (tax never re-derived from a live rate) and the shared FHIR // formatters, so the only new logic here is composition - kept React-free and unit-tested. The logo is // returned as the raw Attachment; PrintableInvoice resolves it to a src via usePhotoUrl (org logos carry // a base64 thumbnail, so they resolve synchronously and print without a network round-trip). import type { Attachment, Invoice, Organization, PaymentReconciliation } from 'fhir/r4'; import { readInvoiceLines, invoiceSubtotalCents, invoiceTaxCents, invoiceTotalCents, lineAmountCents, } from '../../../utils/invoiceTransform'; import { formatMoney } from '../../../utils/money'; import { formatSingleAddress } from '../../../utils/fhirAddress'; import { formatFhirContactPoint } from '../../../utils/fhirContactPoint'; import { formatDate } from '../../../utils/dateUtils'; import { invoiceNumberOf, statusLabel, invoicePatientLabel } from './invoiceDisplay'; import { ORG_LOGO_EXTENSION_URL } from '../../../config/organizationExtensions'; import { paidCents, paymentHistory, remainingCents } from '../../../utils/paymentModel'; export const getOrgLogoAttachment = (organization?: Organization | null): Attachment | undefined => organization?.extension?.find((e) => e.url === ORG_LOGO_EXTENSION_URL)?.valueAttachment as Attachment | undefined; export type InvoicePrintLine = { description: string; quantity: number; unitPrice: string; amount: string }; /** The payment ledger as the detail read it for this invoice: its outcome, and the payments when it answered. */ export type InvoiceLedger = { read: 'loading' | 'ready' | 'failed' | 'incomplete'; payments: PaymentReconciliation[] }; /** * What the bill says was paid. Only a ledger that was read can say it: one that failed or had not answered yet is * said to be unknown, and nothing is worked out from the invoice's status, which a payment does not set. */ export type InvoicePrintLedger = | { kind: 'read'; rows: { date: string; amount: string }[]; paid: string; balance: string } | { kind: 'unread'; note: string }; const LEDGER_FAILED = 'Payments could not be read when this was printed, so what has been paid is not shown.'; const LEDGER_INCOMPLETE = 'Payments could not be read in full when this was printed, so what has been paid is not shown.'; const LEDGER_PENDING = 'Payments had not been read yet when this was printed, so what has been paid is not shown.'; const ledgerFor = (invoice: Invoice, ledger?: InvoiceLedger): InvoicePrintLedger => { if (!ledger || ledger.read === 'loading') return { kind: 'unread', note: LEDGER_PENDING }; if (ledger.read === 'failed') return { kind: 'unread', note: LEDGER_FAILED }; if (ledger.read === 'incomplete') return { kind: 'unread', note: LEDGER_INCOMPLETE }; return { kind: 'read', // A statement reads in the order things happened, so the oldest payment comes first. rows: paymentHistory(ledger.payments).reverse().map((entry) => ({ date: formatDate(entry.dateISO), amount: formatMoney(entry.amountCents), })), paid: formatMoney(paidCents(ledger.payments)), balance: formatMoney(remainingCents(invoice, ledger.payments)), }; }; export type InvoicePrintModel = { orgName: string; orgAddress: string; orgPhone: string; logo?: Attachment; invoiceNumber: string; dateText: string; statusLabel: string; patientName: string; lines: InvoicePrintLine[]; subtotal: string; tax?: string; total: string; ledger: InvoicePrintLedger; }; export const buildInvoicePrintModel = ( invoice: Invoice, organization?: Organization | null, ledger?: InvoiceLedger, ): InvoicePrintModel => { const taxCents = invoiceTaxCents(invoice); return { orgName: organization?.name ?? '', orgAddress: organization?.address?.[0] ? formatSingleAddress(organization.address[0]) : '', orgPhone: formatFhirContactPoint(organization?.telecom, 'phone', ', ', ''), logo: getOrgLogoAttachment(organization), invoiceNumber: invoiceNumberOf(invoice) ?? invoice.id ?? '', dateText: formatDate(invoice.date ?? invoice.meta?.lastUpdated), statusLabel: statusLabel(invoice.status), // - never a raw `Patient/` on a document a patient can be handed. patientName: invoicePatientLabel(invoice), lines: readInvoiceLines(invoice).map((line) => ({ description: line.description || 'Untitled line', quantity: line.quantity, unitPrice: formatMoney(line.unitPriceCents), amount: formatMoney(lineAmountCents(line)), })), subtotal: formatMoney(invoiceSubtotalCents(invoice)), ...(taxCents > 0 ? { tax: formatMoney(taxCents) } : {}), total: formatMoney(invoiceTotalCents(invoice)), ledger: ledgerFor(invoice, ledger), }; };