Skip to main content

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 SUCCEEDED payment moves the invoice to PARTIALLY_PAID/PAID; a provider failure is stored as a FAILED payment 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.

  • demographic (bill a patient by id), tenant/facility (scope), notification (send receipts).