Billing (Invoices & Payments) — How it works
Overview
The billing engine issues invoices and records payments with relational financial totals (never JSONB-only), so a database or backup compromise or a JSONB parsing bug can never corrupt the money. It references patient/person/tenant/facility by id (logical, no cross-engine FK). Payment providers plug in behind a relational model; a provider failure is captured, not thrown.
Data model & ownership
Owned tables (migrations under core/src/main/resources/db/changelog/billing/):
| Table (migration) | Purpose |
|---|---|
invoice (001) | Header: relational status (`DRAFT |
invoice_line (001) | Line: line_total = quantity × unit_price − discount + tax; changing lines recomputes the invoice totals. |
payment_method (002) | Non-sensitive method config (type/provider/active), unique code per tenant. |
payment (002) | Relational status + amount, payment_reference unique per tenant, gateway snapshot in JSONB. No raw card secrets. |
Key rules & invariants
- Financial totals are relational and always match their lines — every line add/update/remove
recomputes the line total (BigDecimal, scale 4, HALF_UP) and re-derives the invoice totals; lines
are immutable once the invoice is
PAID/CANCELLED. - Payments reconcile invoices — a
SUCCEEDEDpayment moves the invoice toPARTIALLY_PAID/PAID; a provider failure is stored as aFAILEDpayment with a non-sensitive gateway snapshot, not thrown. - No raw card numbers or payment secrets are stored.
API
See the API Reference. Endpoint groups under /api/v1/billing: invoices (+ lines),
payments, payment methods.
Configuration & feature flags
None.
Related features
demographic(bill a patient by id),tenant/facility(scope),notification(send receipts).