import { formatMoney } from '../../utils/money'; // - how a receipt reads on the subscription page. // // The settled design's rule 11: receipts live in the product, and Stripe's portal keeps the payment // method and cancellation only. The rendering decisions live here rather than in the page so they can be // argued about - and mutation-tested - without a DOM. // // ONLY EXCEPTIONS EARN A WORD. Almost every receipt is paid, so a "Paid" chip on every row is a column // of noise that trains the reader to skip the exact place an unpaid one would appear. Same rule the // receptionist board settled on for its own queues. /** The server's projection (`GET /R4/Stripe/invoices`). Deliberately the same shape, field for field. */ export interface Receipt { id: string; number?: string; /** ISO instant. */ date: string; status: string; amountCents?: number; currency?: string; hostedUrl?: string; pdfUrl?: string; } export interface DescribedReceipt { label: string; /** Absent when the date could not be read - never the string "Invalid Date". */ dateLabel?: string; /** Absent when the amount is unknown. Never "$0.00" for an amount we could not read. */ amountLabel?: string; /** Absent for the ordinary paid case. Present only when the reader needs to act or know. */ statusLabel?: string; documentUrl?: string; } /** * Our words for the Stripe states we understand. An unrecognised code returns nothing rather than * being shown raw - the same rule the FHIR status vocabulary follows. "uncollectible_retry" on a * practice's billing page reads as a system talking to itself. */ const STATUS_WORDS: Record = { open: 'Due', void: 'Cancelled', uncollectible: 'Unpaid', }; export function describeReceipt(receipt: Receipt): DescribedReceipt { const parsed = new Date(receipt.date); const dateLabel = Number.isNaN(parsed.getTime()) ? undefined : parsed.toLocaleDateString(undefined, { day: 'numeric', month: 'long', year: 'numeric' }); return { // Never the Stripe id as a fallback: `in_1KcZ...` is our support identifier, not something a // practice can reconcile against a bank statement. The date is what they would look for. label: receipt.number ?? (dateLabel ? `Receipt — ${dateLabel}` : 'Receipt'), dateLabel, amountLabel: typeof receipt.amountCents === 'number' ? formatMoney(receipt.amountCents, (receipt.currency ?? 'usd').toUpperCase()) : undefined, statusLabel: STATUS_WORDS[receipt.status], // Stripe's invoice PDF is the bill: once paid it still reads "Amount due" and "Pay online". A paid // receipt opens the hosted page instead, which says it was paid and offers the receipt to download. Without // one it offers no document at all, never that PDF. documentUrl: receipt.status === 'paid' ? receipt.hostedUrl : receipt.pdfUrl ?? receipt.hostedUrl, }; }