10-cross-cuttingwave: W6filled19 citations

Accounting — Journal Entries, Budgets and Financial Reports

Audiences: clinical-buyer, developer

Accounting — Journal Entries, Budgets and Financial Reports

Dudoxx HMS embeds a SKR03/PCG-compliant double-entry accounting engine in a dedicated PostgreSQL database (ddx_api_acc), covering the full ledger lifecycle — chart of accounts, journal entries, fiscal period management, budgets, VAT rates, price lists, and financial reporting — with FHIR PaymentNotice projections for each settled payment.

Business Purpose

Healthcare organisations in the DACH/EU region are subject to GoBD (Grundsätze zur ordnungsmäßigen Führung und Aufbewahrung) and national accounting standards (SKR03 in Germany, PCG in France) that require immutable, period-locked double-entry bookkeeping. Without an embedded accounting layer, clinics must export billing data to a separate ERP — introducing reconciliation lag, audit risk, and GDPR data-transfer overhead.

The accounting module eliminates the ERP gap by providing:

  1. Double-entry journal entries — every debit/credit line is balance-validated before save; posted entries are immutable and can only be corrected via reversal (REVERSAL entry type), satisfying GoBD §14 immutability.
  2. GoBD-compliant fiscal period management — periods are opened/closed/reopened explicitly; posting to a CLOSED or FUTURE period raises a 400 Bad Request at the API layer.
  3. Cost-centre and budget tracking — departmental Kostenstellen, budget planning, approval workflow, and variance analysis in a single module.
  4. VAT rates + price lists — configurable VAT rates per country with GL account mapping, and multi-tier pricing (GKV, PKV, VIP, employee) with quantity breaks.
  5. FHIR PaymentNotice projection — every payment settled in the AR ledger (prisma:acc.Payment) is mirrored as a FHIR PaymentNotice resource via ResourceFacade<PaymentNoticeResource>, giving the FHIR layer a canonical payment record linked to the billing Invoice.

Audiences

  • Investor: The embedded accounting module removes the ERP dependency that typically blocks mid-market hospital groups from adopting cloud HMS. The SKR03/GoBD compliance posture is a prerequisite for DACH sales. The FHIR PaymentNotice bridge positions Dudoxx for FHIR-native financial exchange with payers.
  • Clinical buyer (doctor/nurse/receptionist): Accountants and finance staff interact with the Accountant Portal (see 08-portals-and-i18n/accountant-portal.md), which surfaces journals, period reports, and payment reconciliation. Clinical staff see only billing summaries.
  • Developer/partner: The accounting module lives under ddx-api/src/financial/accounting/ and is isolated in ddx_api_acc — services inject PrismaAccountingService (the dedicated client), NOT the general PrismaService. Cross-schema reads (e.g. patient name for a payment record) require two injected services and in-process assembly; no Prisma-level join is possible across separate PostgreSQL instances.
  • Internal (ops/support): ddx_api_acc has an independent backup schedule and migration pipeline (pnpm run prisma:migrate:accounting). The PrismaAccountingService.executeTransaction<T>() helper wraps $transaction for typed callbacks.

Architecture

AccountingModule (ddx-api/src/financial/accounting/) owns:

ServiceResponsibility
AccountsServiceChart of Accounts (SKR03/PCG, hierarchical)
JournalEntriesServiceDouble-entry bookkeeping (DRAFTPOSTEDREVERSED/VOIDED)
FiscalPeriodsServiceGoBD period lifecycle (OPENCLOSEDREOPENED)
CostCentersServiceDepartmental cost tracking (Kostenstellen)
BudgetsServiceBudget planning + approval workflow
VatRatesServicePer-country VAT rates with GL account mapping
ProductsServiceProduct/service catalog with SKU, GOÄ/EBM/CCAM billing codes
PriceListsServiceMulti-tier pricing (GKV, PKV, VIP, employee, quantity breaks)
PaymentsServiceAR ledger (source of truth for payment records)
ReconciliationServiceAR-clearing ledger (ArOpenItemArClearing; posts double-entry via JournalEntriesService)
DunningServiceAutomated dunning workflow
ReportsServiceTrial balance, P&L, balance sheet (v6.3)
InvoiceAccountingServiceInvoice → Accounting bridge (BillingModuleAccountingModule)

PaymentNotice FHIR facade

AspectValue
tokenPAYMENT_NOTICE_FACADE
facadeResourceFacade<PaymentNoticeResource>accounting.module.ts:129
Descriptorpayment-notice.descriptor.ts
Store'fhir' (HAPI, tenant-partitioned) + Tier-0 fhir_resource_link hooks
Source of truth for payment amountprisma:acc.Payment (written BEFORE facade call)

AccountingModule imports forwardRef(() => BillingModule) to break the circular dependency: Billing needs AccountingService (for pricing sync), Accounting needs BillingModule (for ChargeItemDefinitionService). Products ↔ FHIR sync lives in BillingModule via ChargeItemDefinitionService (F2.4).

See Prisma 7 Schemas for the ddx_api_acc / PrismaAccountingService pattern.

Tech Stack & Choices

ConcernChoiceRationale
Storageddx_api_acc (dedicated PostgreSQL, ACCOUNTING_DATABASE_URL)GoBD/HGB require financial data isolation from clinical and AI data
ORMPrismaAccountingService extends @ddx/prisma-accountingSame lifecycle pattern as other Prisma services; executeTransaction<T>() helper for typed $transaction callbacks
Double-entry validationIn-service balance check (validateBalance) BEFORE Prisma $transactionFail-fast: rejects unbalanced entries at the API layer, not the DB layer
Entry numberingJE-YYYY-NNNN sequential, last-entry lookup (not a DB sequence)Works across PostgreSQL without advisory locks; not gapless by design (voided entries consume a number)
Fiscal period lockingPeriodStatus.CLOSED check in service layerCLOSED or FUTURE periods reject posting with 400 Bad Request
FHIR projectionResourceFacade<PaymentNoticeResource> via PAYMENT_NOTICE_FACADEMirrors the F2.1/F2.2 billing facade pattern; PaymentNotice is a Tier-1 FHIR-only projection — prisma:acc.Payment is the source of truth
Billing codesGOÄ, EBM (Germany), CCAM (France) mapped on Product.billingCodeMulti-country billing code support without schema divergence
Reporting standardSKR03 (Germany) / PCG (France) chart of accountsDACH-first, extensible to other EU frameworks

Data Flow

Journal Entry lifecycle

POST /accounting/journal-entries
  → JournalEntriesController
    → JournalEntriesService.create(dto, userId, organizationId)
      1. validateBalance(lines)             ← debits must equal credits (rounded to 2dp)
      2. validateFiscalPeriod(...)          ← OPEN or REOPENED period required
      3. validateAccounts(...)             ← accounts must exist, active, not control accounts
      4. generateEntryNumber(...)          ← JE-YYYY-NNNN sequential
      5. prisma.$transaction([
           journalEntry.create(status: DRAFT),
           journalEntryLine.create × N
         ])
      → return entry + lines

POST /accounting/journal-entries/:id/post
  → JournalEntriesService.post(id, dto, userId, organizationId)
      1. entry must be in DRAFT
      2. period must be OPEN
      3. journalEntry.update(status: POSTED, postingDate, approvedBy)

POST /accounting/journal-entries/:id/reverse
  → JournalEntriesService.reverse(id, userId, organizationId)
      1. original must be POSTED
      2. $transaction:
           original → REVERSED
           new entry (REVERSAL type, lines with debit/credit swapped, status: POSTED)

Payment → FHIR PaymentNotice

POST /accounting/payments
  → PaymentsService.create(dto, userId, organizationId)
      1. prisma:acc.Payment.create(...)    ← source of truth in ddx_api_acc
      2. PAYMENT_NOTICE_FACADE.create(...)
           → ResourceFacade dispatches to HAPI (store: 'fhir')
           → Tier-0 makeBaseLinkHooks.afterCreate upserts fhir_resource_link
             (resourceType='PaymentNotice', fhirId=<HAPI id>, clinicId=...)

Implicated Code

Core module wiring

  • ddx-api/src/financial/accounting/accounting.module.ts:1AccountingModule; 11 controllers, 13 services, forwardRef(() => BillingModule) import to break circular dep.
  • ddx-api/src/financial/accounting/accounting.module.ts:117PAYMENT_NOTICE_FACADE useFactory — new ResourceFacade<PaymentNoticeResource>(makePaymentNoticeDescriptor(prisma), fhirClient).
  • ddx-api/src/financial/accounting/accounting.module.ts:135 — exports include PaymentsService for BillingSummaryService to consume AR-ledger aggregation without injecting PRISMA_ACC directly.

Journal entries

  • ddx-api/src/financial/accounting/journal-entries/journal-entries.service.ts:36JournalEntriesService; validateBalance at :500, validateFiscalPeriod at :536, generateEntryNumber (JE-YYYY-NNNN) at :606.
  • ddx-api/src/financial/accounting/journal-entries/journal-entries.service.ts:320reverse(): swaps debit/credit amounts on all lines, marks original as REVERSED, creates new REVERSAL-typed entry atomically via $transaction.

Fiscal periods

  • ddx-api/src/financial/accounting/fiscal-periods/fiscal-periods.service.ts:1FiscalPeriodsService; GoBD period lifecycle management.
  • ddx-api/src/financial/accounting/fiscal-periods/dto/close-period.dto.ts:1ClosePeriodDto shape.

PaymentNotice FHIR descriptor

  • ddx-api/src/financial/accounting/payments/payment-notice.descriptor.ts:1makePaymentNoticeDescriptor(prisma) factory; Tier-1 FHIR-only binding with fhir_resource_link Tier-0 hooks; comment documents why prisma:acc.Payment is the SoT.
  • ddx-api/src/financial/accounting/payments/payment-notice.tokens.ts:1PAYMENT_NOTICE_FACADE DI symbol + PaymentNoticeFacade type alias.
  • ddx-api/src/financial/accounting/payments/payments.service.ts:1PaymentsService; AR ledger; writes to prisma:acc.Payment BEFORE triggering facade.
  • ddx-api/src/financial/accounting/reconciliation/reconciliation.service.ts:70ReconciliationService; AR-clearing ledger — when an insurer PaymentReconciliation (from claims-eu) or a self-pay Payment matches an ArOpenItem, reduces openAmount, advances status (OPEN → PARTIALLY_CLEARED → CLEARED), writes ArClearing, and delegates the double-entry posting (debit Bank / credit Receivables + VAT) to the existing JournalEntriesService — never reinvents balance/period/account validation.

Prisma accounting client

  • ddx-api/src/financial/accounting/prisma-accounting/prisma-accounting.service.ts:1PrismaAccountingService; AccountingTransactionClient type alias at :11; executeTransaction<T>() helper at :87.
  • ddx-api/prisma-accounting/schema.prisma:1 — accounting schema; 16 models, 21 enums; env ACCOUNTING_DATABASE_URL; output @ddx/prisma-accounting.

Billing integration

  • ddx-api/src/financial/accounting/invoice-accounting/invoice-accounting.service.ts:1InvoiceAccountingService; bridge between BillingModule invoices and accounting journal entries.
  • ddx-api/src/financial/accounting/accounting.module.ts:90forwardRef(() => BillingModule) import — required because ProductsService syncs to FHIR ChargeItemDefinition via BillingModule.ChargeItemDefinitionService (F2.4).

Reports

  • ddx-api/src/financial/accounting/reports/reports.service.ts:1ReportsService; trial balance, P&L, balance sheet (v6.3+ generation).
  • ddx-api/src/financial/accounting/reports/reports.controller.ts:1ReportsController; endpoint group for financial report generation.

Operational Notes

  • ACCOUNTING_DATABASE_URL is mandatory at boot — PrismaAccountingService.onModuleInit() will throw if the env var is absent. Set alongside DATABASE_URL in every environment.
  • pnpm run prisma:generate:accounting must be run after any change to prisma-accounting/schema.prisma. The prebuild hook runs prisma:generate:all automatically, so this is only manually needed during active schema development.
  • Balance validation uses floating-point roundingjournal-entries.service.ts:509 rounds debit and credit totals to 2 decimal places before comparing. Amounts in the DTO should be passed as numbers; very large currencies with >2 decimal precision (e.g. JPY) are not currently supported.
  • Entry numbering is NOT gapless — voided DRAFT entries consume a JE-YYYY-NNNN number. If gap-less numbering is required (e.g. Germany §238 HGB), implement a DB sequence on ddx_api_acc and replace generateEntryNumber().
  • Cross-schema readsPaymentsService occasionally needs patient or invoice data from ddx_api_main. It injects both PrismaAccountingService and PrismaService and assembles in-process. No Prisma-level join is possible across separate PostgreSQL instances.
  • FHIR hook errors are swallowed — if PAYMENT_NOTICE_FACADE.create() fails after prisma:acc.Payment.create(), the payment is persisted but the FHIR projection is missing. Monitor ddx-api logs for [ResourceFacade] afterCreate hook error.
  • Validation commandpnpm run prisma:migrate:status:accounting to check pending migrations on ddx_api_acc.
  • Billing — Invoices, Charge Items and Pricing — patient billing; BillingModule and AccountingModule are mutually dependent via forwardRef(); InvoiceAccountingService bridges the two.
  • Prisma 7 Schemasddx_api_acc / PrismaAccountingService follows the identical five-schema pattern; ACCOUNTING_DATABASE_URL env var.
  • FHIR ResourceFacadeResourceFacade<PaymentNoticeResource> is the seventh financial resource migrated under CR-031 (2026-05-16); StoreKind = 'fhir' with Tier-0 fhir_resource_link hooks.
  • Audit Logging — all accounting mutations (journal entries, payments) are recorded to ddx_api_log via AuditInterceptor at position 8 in the global pipeline.
  • Insurance and Coverage — insurance claims reference invoices and coverage; settled claims trigger payment records in the accounting ledger.