Billing — Invoices, Charge Items and Pricing
Audiences: clinical-buyer, developer, investor
Dudoxx HMS manages patient billing entirely through FHIR R4 resources — Invoice, Account, ChargeItem, and ChargeItemDefinition — stored in HAPI FHIR and synchronised to Prisma via the
ResourceFacade<R>pattern, delivering tenant-isolated, gapless invoice numbers and a full pricing catalog.
Business Purpose
Patient billing in a multi-tenant clinic HMS must satisfy several competing constraints simultaneously: invoices must carry sequential, gap-less numbers (required for German §238 HGB), the same invoice must be queryable both as a FHIR resource (for interoperability with health information exchanges) and via Prisma (for fast reporting queries), and pricing must support multiple tier structures (statutory GKV, private PKV, VIP, employee rates) without schema divergence per country.
The billing module achieves this by:
- FHIR-native invoice storage —
Invoice,Account,ChargeItem, andChargeItemDefinitionare stored in HAPI FHIR R4, giving every billing record a globally unique FHIR ID and enabling FHIR-native exchange with payers and EHRs. - Gapless invoice numbering —
InvoiceSequenceServiceallocates numbers inside the FHIRcreatetransaction; a rollback-on-failure mechanism ensures no gap is silently emitted (invoices.service.tsrollback logic). - Pricing catalog —
ChargeItemDefinitionmodels the service catalog (GOÄ/EBM/CCAM billing codes) thatChargeItemsServicereferences when creating billable service records per visit. - Billing summary aggregation —
BillingSummaryServicejoins FHIR invoice data with AR-ledger payment records fromddx_api_accto produce clinic-level financial dashboards without FHIR round-trips for every widget.
Audiences
- Investor: FHIR-native billing is a prerequisite for connecting to national health information exchanges (e.g. gematik TI, French ANS). Storing billing as FHIR resources positions Dudoxx for electronic claim submission without a separate billing gateway. The gap-less sequence satisfies the German §238 HGB requirement that eliminates one common compliance blocker for DACH sales.
- Clinical buyer (doctor/nurse/receptionist): Reception staff and doctors see billing through the Accountant Portal — invoice creation, charge-item attachment per visit, and status tracking (draft → issued → paid). The billing UI never surfaces FHIR internals.
- Developer/partner: All four billing resource types use
ResourceFacade<R>via per-featureuseFactoryproviders. Adding a new FHIR billing resource means writing aResourceDescriptor<R>and auseFactoryblock inbilling.module.ts; no new service class is required for the FHIR dispatch layer. - Internal (ops/support): FHIR billing data lives in HAPI FHIR (port 18080,
ddx_fhir_core). Thefhir_resource_linktable inddx_api_mainprovides a fast cross-tenant index (resourceType='Invoice', clinicId=...). Payment records live inddx_api_acc— see Accounting.
Architecture
BillingModule (ddx-api/src/financial/billing/) owns:
| Controller / Service | Facade token → type | Descriptor + notes |
|---|---|---|
InvoicesController / InvoicesService | INVOICE_FACADE → ResourceFacade<InvoiceResource> | invoice.descriptor.ts (store: 'fhir', Tier-0 link hooks + Tier-1 sequence hook). InvoiceSequenceService ← gap-less number allocator (prisma:main.invoiceSequence) |
AccountsController / AccountsService | ACCOUNT_FACADE → ResourceFacade<AccountResource> | account.descriptor.ts (store: 'fhir', Tier-0 link hooks) |
ChargeItemsController / ChargeItemsService | CHARGE_ITEM_FACADE → ResourceFacade<ChargeItemResource> | charge-item.descriptor.ts (store: 'fhir', Tier-1 split-storage TODO) |
PricingController / ChargeItemDefinitionService | CHARGE_ITEM_DEFINITION_FACADE → ResourceFacade<ChargeItemDefinitionResource> | Merged from legacy PricingService + ProductFhirSyncService (F2.4). Syncs AccountingModule.ProductsService catalog to FHIR |
BillingSummaryModule | — | BillingSummaryService ← aggregates Invoice FHIR data + PaymentsService AR ledger |
forwardRef(() => AccountingModule) ← BillingModule needs AccountsService,
InvoiceAccountingService.
All four ResourceFacade<R> instances are wired as useFactory providers in billing.module.ts — they cannot be global singletons because the generic parameter differs per resource type (see FHIR ResourceFacade).
A second, RELATIONAL billing path coexists under billing/eu/ + billing/country-packs/ + billing/catalog/ — a full EU billing system-of-record (Coverage / ChargeItem / Invoice) stored in ddx_api_acc (Prisma-accounting), keyed to jurisdiction purely by data through CountryPackService (the pluggable country-pack seam), with GOÄ/EBM/ICD-10-GM catalogs. This is documented separately in EU Billing Country-Packs; the FHIR facade path above is the interoperability projection, the relational path is the reimbursement system-of-record.
Tech Stack & Choices
| Concern | Choice | Rationale |
|---|---|---|
| Primary storage | HAPI FHIR R4 (store: 'fhir') | FHIR-native interoperability; canonical Invoice/Account/ChargeItem/ChargeItemDefinition resources |
| Bridge table | fhir_resource_link in ddx_api_main | Fast cross-tenant invoice lookup without FHIR search; keyed (resourceType, fhirId, clinicId) |
| Invoice numbering | InvoiceSequenceService + Prisma $transaction | Gap-less numbering per §238 HGB; rollback-safe: sequence is allocated before HAPI write and released on failure |
| Pricing catalog | ChargeItemDefinition (FHIR) + Product/PriceList (Prisma acc) | Two-layer: Prisma acc is editable source of truth; FHIR is the interoperability projection via F2.4 sync |
| Billing codes | GOÄ, EBM, CCAM on ChargeItemDefinition.code | Multi-country support without schema forking |
| Billing summary | BillingSummaryService (in-process join) | Avoids N+1 FHIR searches for dashboard widgets; joins FHIR invoice list with AR-ledger aggregates |
| Cross-module dep | forwardRef(() => AccountingModule) | Breaks circular import: BillingModule needs AccountingService prices; AccountingModule needs ChargeItemDefinitionService for product↔FHIR sync |
Data Flow
Creating an Invoice
POST /billing/invoices
→ InvoicesController
→ InvoicesService.create(dto, clinicId, orgId)
1. InvoiceSequenceService.allocate(clinicId) ← prisma:main $transaction, gap-less
2. Build FHIR Invoice resource (status: draft, identifier: [allocated number])
3. INVOICE_FACADE.create(invoice, clinicId, orgId)
→ ResourceFacade dispatches to HAPI (store: 'fhir')
→ HAPI routes to tenant partition (X-Clinic-ID)
→ Tier-0 makeBaseLinkHooks.afterCreate upserts fhir_resource_link
→ Tier-1 invoiceNumberAllocatorHook confirms sequence in prisma:main
4. On facade failure: InvoiceSequenceService.release(number) ← rollback
5. Return InvoiceResponseDto
Attaching a Charge Item
POST /billing/charge-items
→ ChargeItemsService.create(dto, clinicId, orgId)
1. Build FHIR ChargeItem resource
(subject: Patient/..., service: Encounter/..., code: GOÄ/EBM code)
2. CHARGE_ITEM_FACADE.create(chargeItem, clinicId, orgId)
→ ResourceFacade → HAPI → fhir_resource_link upsert
Billing Summary
GET /billing/summary
→ BillingSummaryController
→ BillingSummaryService.getSummary(orgId, clinicId)
1. INVOICE_FACADE.search({status: 'issued'}, clinicId, orgId)
2. AccountingModule.PaymentsService.findByInvoiceIds(invoiceIds)
← PaymentsService exported from AccountingModule via forwardRef
3. In-process join → BillingSummaryDto
Implicated Code
Module wiring — 4 ResourceFacade instances
ddx-api/src/financial/billing/billing.module.ts:85—INVOICE_FACADEuseFactory;new ResourceFacade<InvoiceResource>(makeInvoiceDescriptor(prisma), fhirClient).ddx-api/src/financial/billing/billing.module.ts:103—ACCOUNT_FACADEuseFactory; mirrors F2.1 wiring; comment explains Option A rationale (feature module owns descriptor + facade instantiation).ddx-api/src/financial/billing/billing.module.ts:119—CHARGE_ITEM_FACADEuseFactory; descriptor forward-shaped for split storage (TODO[F2.3-split]).ddx-api/src/financial/billing/billing.module.ts:131—CHARGE_ITEM_DEFINITION_FACADEuseFactory; merged from legacyPricingService+ProductFhirSyncService(F2.4).
Invoice service
ddx-api/src/financial/billing/invoices/invoices.service.ts:1—InvoicesService; gap-less sequence allocation and release; rollback pattern documented in JSDoc comment at class level.ddx-api/src/financial/billing/invoices/invoice.descriptor.ts:1—makeInvoiceDescriptor(prisma)factory; Tier-1invoiceNumberAllocatorHookbumps the gap-less sequence inprisma:mainafter successful HAPI write.ddx-api/src/financial/billing/invoices/invoice-sequence.service.ts:1—InvoiceSequenceService; allocates/releases gap-less invoice numbers via Prisma$transactiononddx_api_main.ddx-api/src/financial/billing/invoices/invoices.tokens.ts:1—INVOICE_FACADEDI symbol +InvoiceFacadetype alias.
Account (billing account for patient)
ddx-api/src/financial/billing/accounts/account.descriptor.ts:1—makeAccountDescriptor(prisma)factory; Tier-1 FHIR-only; mirrors F2.1 wiring as the canonical example for subsequent facades.ddx-api/src/financial/billing/accounts/accounts.tokens.ts:1—ACCOUNT_FACADEDI symbol.
Charge items
ddx-api/src/financial/billing/charge-items/charge-item.descriptor.ts:1—makeChargeItemDescriptor(prisma)factory;TODO[F2.3-split]comment marks the planned split between FHIR andprisma:mainfor high-volume charge-item queries.ddx-api/src/financial/billing/charge-items/charge-items.service.ts:1—ChargeItemsService; maps clinical visit service codes to FHIRChargeItem.code.
Pricing
ddx-api/src/financial/billing/pricing/charge-item-definition.service.ts:1—ChargeItemDefinitionService; merged replacement for legacyPricingService+ProductFhirSyncService; syncsAccountingModule.ProductsServicecatalog to FHIRChargeItemDefinition.ddx-api/src/financial/billing/pricing/charge-item-definition.descriptor.ts:1—makeChargeItemDefinitionDescriptor(prisma)factory.ddx-api/src/financial/billing/pricing/pricing.controller.ts:1—PricingController; service pricing catalog endpoints.
Billing summary
ddx-api/src/financial/billing/billing-summary/billing-summary.service.ts:1—BillingSummaryService; in-process join of FHIR invoice search andPaymentsService.findByInvoiceIds()from AccountingModule.ddx-api/src/financial/billing/billing-summary/billing-summary.module.ts:1—BillingSummaryModule; thin submodule wiring.
Operational Notes
- Four FHIR resources migrated to ResourceFacade as of 2026-05-16 (
Invoice,Account,ChargeItem,ChargeItemDefinition) — all under CR-031. The older directFhirResourceServicecalls for billing were retired in the F2.x migration wave. - Gap-less sequence gotcha — if
INVOICE_FACADE.create()throws AFTERInvoiceSequenceService.allocate()but BEFORE the Tier-1 hook confirms, the service callsrelease()to void the number. Monitor for[InvoicesService] Invoice allocation rollbacklog lines indicating HAPI write failures. ChargeItemsplit-storage deferred — the descriptor'sTODO[F2.3-split]marks a planned future split where high-frequency charge queries bypass HAPI and go directly toprisma:main. Until then, all ChargeItem reads hit HAPI.- Billing summary uses
PaymentsService— exported fromAccountingModuleviaforwardRef(); the dependency is across module boundaries. IfAccountingModuleis removed or the export dropped,BillingSummaryServicecompilation fails. - Cross-schema billing report — a complete billing report for a patient (invoice + payment + insurance claim) requires three injected services across
BillingModule(FHIR),AccountingModule(prisma:acc), andInsuranceModule(FHIR Claim). Compose in a service layer, not in a controller.
Related Topics
- Accounting — Journal Entries, Budgets and Financial Reports —
AccountingModuleis the AR ledger counterpart;BillingModuleandAccountingModuleshare aforwardRef()circular import. - Insurance and Coverage — Claims Management —
ClaimResourcereferencesCoverage(patient insurance) and is submitted after invoice creation. - FHIR ResourceFacade — canonical pattern used by all four billing facades;
fhir_resource_linkbridge table; CR-031 migration tracker. - Prisma 7 Schemas —
ddx_api_mainholdsfhir_resource_linkand the invoice sequence table;ddx_api_accholds payment records.