10-cross-cuttingwave: W6filled17 citations

Billing — Invoices, Charge Items and Pricing

Audiences: clinical-buyer, developer, investor

Billing — Invoices, Charge Items and Pricing

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:

  1. FHIR-native invoice storageInvoice, Account, ChargeItem, and ChargeItemDefinition are stored in HAPI FHIR R4, giving every billing record a globally unique FHIR ID and enabling FHIR-native exchange with payers and EHRs.
  2. Gapless invoice numberingInvoiceSequenceService allocates numbers inside the FHIR create transaction; a rollback-on-failure mechanism ensures no gap is silently emitted (invoices.service.ts rollback logic).
  3. Pricing catalogChargeItemDefinition models the service catalog (GOÄ/EBM/CCAM billing codes) that ChargeItemsService references when creating billable service records per visit.
  4. Billing summary aggregationBillingSummaryService joins FHIR invoice data with AR-ledger payment records from ddx_api_acc to 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-feature useFactory providers. Adding a new FHIR billing resource means writing a ResourceDescriptor<R> and a useFactory block in billing.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). The fhir_resource_link table in ddx_api_main provides a fast cross-tenant index (resourceType='Invoice', clinicId=...). Payment records live in ddx_api_acc — see Accounting.

Architecture

BillingModule (ddx-api/src/financial/billing/) owns:

Controller / ServiceFacade token → typeDescriptor + notes
InvoicesController / InvoicesServiceINVOICE_FACADEResourceFacade<InvoiceResource>invoice.descriptor.ts (store: 'fhir', Tier-0 link hooks + Tier-1 sequence hook). InvoiceSequenceService ← gap-less number allocator (prisma:main.invoiceSequence)
AccountsController / AccountsServiceACCOUNT_FACADEResourceFacade<AccountResource>account.descriptor.ts (store: 'fhir', Tier-0 link hooks)
ChargeItemsController / ChargeItemsServiceCHARGE_ITEM_FACADEResourceFacade<ChargeItemResource>charge-item.descriptor.ts (store: 'fhir', Tier-1 split-storage TODO)
PricingController / ChargeItemDefinitionServiceCHARGE_ITEM_DEFINITION_FACADEResourceFacade<ChargeItemDefinitionResource>Merged from legacy PricingService + ProductFhirSyncService (F2.4). Syncs AccountingModule.ProductsService catalog to FHIR
BillingSummaryModuleBillingSummaryService ← 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

ConcernChoiceRationale
Primary storageHAPI FHIR R4 (store: 'fhir')FHIR-native interoperability; canonical Invoice/Account/ChargeItem/ChargeItemDefinition resources
Bridge tablefhir_resource_link in ddx_api_mainFast cross-tenant invoice lookup without FHIR search; keyed (resourceType, fhirId, clinicId)
Invoice numberingInvoiceSequenceService + Prisma $transactionGap-less numbering per §238 HGB; rollback-safe: sequence is allocated before HAPI write and released on failure
Pricing catalogChargeItemDefinition (FHIR) + Product/PriceList (Prisma acc)Two-layer: Prisma acc is editable source of truth; FHIR is the interoperability projection via F2.4 sync
Billing codesGOÄ, EBM, CCAM on ChargeItemDefinition.codeMulti-country support without schema forking
Billing summaryBillingSummaryService (in-process join)Avoids N+1 FHIR searches for dashboard widgets; joins FHIR invoice list with AR-ledger aggregates
Cross-module depforwardRef(() => 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:85INVOICE_FACADE useFactory; new ResourceFacade<InvoiceResource>(makeInvoiceDescriptor(prisma), fhirClient).
  • ddx-api/src/financial/billing/billing.module.ts:103ACCOUNT_FACADE useFactory; mirrors F2.1 wiring; comment explains Option A rationale (feature module owns descriptor + facade instantiation).
  • ddx-api/src/financial/billing/billing.module.ts:119CHARGE_ITEM_FACADE useFactory; descriptor forward-shaped for split storage (TODO[F2.3-split]).
  • ddx-api/src/financial/billing/billing.module.ts:131CHARGE_ITEM_DEFINITION_FACADE useFactory; merged from legacy PricingService + ProductFhirSyncService (F2.4).

Invoice service

  • ddx-api/src/financial/billing/invoices/invoices.service.ts:1InvoicesService; gap-less sequence allocation and release; rollback pattern documented in JSDoc comment at class level.
  • ddx-api/src/financial/billing/invoices/invoice.descriptor.ts:1makeInvoiceDescriptor(prisma) factory; Tier-1 invoiceNumberAllocatorHook bumps the gap-less sequence in prisma:main after successful HAPI write.
  • ddx-api/src/financial/billing/invoices/invoice-sequence.service.ts:1InvoiceSequenceService; allocates/releases gap-less invoice numbers via Prisma $transaction on ddx_api_main.
  • ddx-api/src/financial/billing/invoices/invoices.tokens.ts:1INVOICE_FACADE DI symbol + InvoiceFacade type alias.

Account (billing account for patient)

  • ddx-api/src/financial/billing/accounts/account.descriptor.ts:1makeAccountDescriptor(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:1ACCOUNT_FACADE DI symbol.

Charge items

  • ddx-api/src/financial/billing/charge-items/charge-item.descriptor.ts:1makeChargeItemDescriptor(prisma) factory; TODO[F2.3-split] comment marks the planned split between FHIR and prisma:main for high-volume charge-item queries.
  • ddx-api/src/financial/billing/charge-items/charge-items.service.ts:1ChargeItemsService; maps clinical visit service codes to FHIR ChargeItem.code.

Pricing

  • ddx-api/src/financial/billing/pricing/charge-item-definition.service.ts:1ChargeItemDefinitionService; merged replacement for legacy PricingService + ProductFhirSyncService; syncs AccountingModule.ProductsService catalog to FHIR ChargeItemDefinition.
  • ddx-api/src/financial/billing/pricing/charge-item-definition.descriptor.ts:1makeChargeItemDefinitionDescriptor(prisma) factory.
  • ddx-api/src/financial/billing/pricing/pricing.controller.ts:1PricingController; service pricing catalog endpoints.

Billing summary

  • ddx-api/src/financial/billing/billing-summary/billing-summary.service.ts:1BillingSummaryService; in-process join of FHIR invoice search and PaymentsService.findByInvoiceIds() from AccountingModule.
  • ddx-api/src/financial/billing/billing-summary/billing-summary.module.ts:1BillingSummaryModule; 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 direct FhirResourceService calls for billing were retired in the F2.x migration wave.
  • Gap-less sequence gotcha — if INVOICE_FACADE.create() throws AFTER InvoiceSequenceService.allocate() but BEFORE the Tier-1 hook confirms, the service calls release() to void the number. Monitor for [InvoicesService] Invoice allocation rollback log lines indicating HAPI write failures.
  • ChargeItem split-storage deferred — the descriptor's TODO[F2.3-split] marks a planned future split where high-frequency charge queries bypass HAPI and go directly to prisma:main. Until then, all ChargeItem reads hit HAPI.
  • Billing summary uses PaymentsService — exported from AccountingModule via forwardRef(); the dependency is across module boundaries. If AccountingModule is removed or the export dropped, BillingSummaryService compilation 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), and InsuranceModule (FHIR Claim). Compose in a service layer, not in a controller.