Accounting — Journal Entries, Budgets and Financial Reports
Audiences: clinical-buyer, developer
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:
- Double-entry journal entries — every debit/credit line is balance-validated before save; posted entries are immutable and can only be corrected via reversal (
REVERSALentry type), satisfying GoBD §14 immutability. - GoBD-compliant fiscal period management — periods are opened/closed/reopened explicitly; posting to a
CLOSEDorFUTUREperiod raises a400 Bad Requestat the API layer. - Cost-centre and budget tracking — departmental
Kostenstellen, budget planning, approval workflow, and variance analysis in a single module. - VAT rates + price lists — configurable VAT rates per country with GL account mapping, and multi-tier pricing (GKV, PKV, VIP, employee) with quantity breaks.
- FHIR PaymentNotice projection — every payment settled in the AR ledger (
prisma:acc.Payment) is mirrored as a FHIRPaymentNoticeresource viaResourceFacade<PaymentNoticeResource>, giving the FHIR layer a canonical payment record linked to the billingInvoice.
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 inddx_api_acc— services injectPrismaAccountingService(the dedicated client), NOT the generalPrismaService. 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_acchas an independent backup schedule and migration pipeline (pnpm run prisma:migrate:accounting). ThePrismaAccountingService.executeTransaction<T>()helper wraps$transactionfor typed callbacks.
Architecture
AccountingModule (ddx-api/src/financial/accounting/) │ ├── AccountsService — Chart of Accounts (SKR03/PCG, hierarchical) ├── JournalEntriesService — Double-entry bookkeeping (DRAFT → POSTED → REVERSED/VOIDED) ├── FiscalPeriodsService — GoBD period lifecycle (OPEN → CLOSED → REOPENED) ├── CostCentersService — Departmental cost tracking (Kostenstellen) ├── BudgetsService — Budget planning + approval workflow ├── VatRatesService — Per-country VAT rates with GL account mapping ├── ProductsService — Product/service catalog with SKU, GOÄ/EBM/CCAM billing codes ├── PriceListsService — Multi-tier pricing (GKV, PKV, VIP, employee, quantity breaks) ├── PaymentsService — AR ledger (source of truth for payment records) ├── ReconciliationService — AR-clearing ledger (ArOpenItem → ArClearing; posts double-entry via JournalEntriesService) ├── DunningService — Automated dunning workflow ├── ReportsService — Trial balance, P&L, balance sheet (v6.3) └── InvoiceAccountingService — Invoice → Accounting bridge (BillingModule ↔ AccountingModule) PaymentNotice FHIR facade PAYMENT_NOTICE_FACADE token ResourceFacade<PaymentNoticeResource> ← accounting.module.ts:129 Descriptor: payment-notice.descriptor.ts Store: 'fhir' (HAPI, tenant-partitioned) + Tier-0 fhir_resource_link hooks Source of truth for payment amount: prisma: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
| Concern | Choice | Rationale |
|---|---|---|
| Storage | ddx_api_acc (dedicated PostgreSQL, ACCOUNTING_DATABASE_URL) | GoBD/HGB require financial data isolation from clinical and AI data |
| ORM | PrismaAccountingService extends @ddx/prisma-accounting | Same lifecycle pattern as other Prisma services; executeTransaction<T>() helper for typed $transaction callbacks |
| Double-entry validation | In-service balance check (validateBalance) BEFORE Prisma $transaction | Fail-fast: rejects unbalanced entries at the API layer, not the DB layer |
| Entry numbering | JE-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 locking | PeriodStatus.CLOSED check in service layer | CLOSED or FUTURE periods reject posting with 400 Bad Request |
| FHIR projection | ResourceFacade<PaymentNoticeResource> via PAYMENT_NOTICE_FACADE | Mirrors 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 codes | GOÄ, EBM (Germany), CCAM (France) mapped on Product.billingCode | Multi-country billing code support without schema divergence |
| Reporting standard | SKR03 (Germany) / PCG (France) chart of accounts | DACH-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:1—AccountingModule; 11 controllers, 13 services,forwardRef(() => BillingModule)import to break circular dep.ddx-api/src/financial/accounting/accounting.module.ts:117—PAYMENT_NOTICE_FACADEuseFactory —new ResourceFacade<PaymentNoticeResource>(makePaymentNoticeDescriptor(prisma), fhirClient).ddx-api/src/financial/accounting/accounting.module.ts:135— exports includePaymentsServiceforBillingSummaryServiceto consume AR-ledger aggregation without injectingPRISMA_ACCdirectly.
Journal entries
ddx-api/src/financial/accounting/journal-entries/journal-entries.service.ts:36—JournalEntriesService;validateBalanceat:500,validateFiscalPeriodat:536,generateEntryNumber(JE-YYYY-NNNN) at:606.ddx-api/src/financial/accounting/journal-entries/journal-entries.service.ts:320—reverse(): swaps debit/credit amounts on all lines, marks original asREVERSED, creates newREVERSAL-typed entry atomically via$transaction.
Fiscal periods
ddx-api/src/financial/accounting/fiscal-periods/fiscal-periods.service.ts:1—FiscalPeriodsService; GoBD period lifecycle management.ddx-api/src/financial/accounting/fiscal-periods/dto/close-period.dto.ts:1—ClosePeriodDtoshape.
PaymentNotice FHIR descriptor
ddx-api/src/financial/accounting/payments/payment-notice.descriptor.ts:1—makePaymentNoticeDescriptor(prisma)factory; Tier-1 FHIR-only binding withfhir_resource_linkTier-0 hooks; comment documents whyprisma:acc.Paymentis the SoT.ddx-api/src/financial/accounting/payments/payment-notice.tokens.ts:1—PAYMENT_NOTICE_FACADEDI symbol +PaymentNoticeFacadetype alias.ddx-api/src/financial/accounting/payments/payments.service.ts:1—PaymentsService; AR ledger; writes toprisma:acc.PaymentBEFORE triggering facade.ddx-api/src/financial/accounting/reconciliation/reconciliation.service.ts:70—ReconciliationService; AR-clearing ledger — when an insurerPaymentReconciliation(fromclaims-eu) or a self-payPaymentmatches anArOpenItem, reducesopenAmount, advancesstatus(OPEN → PARTIALLY_CLEARED → CLEARED), writesArClearing, and delegates the double-entry posting (debit Bank / credit Receivables + VAT) to the existingJournalEntriesService— never reinvents balance/period/account validation.
Prisma accounting client
ddx-api/src/financial/accounting/prisma-accounting/prisma-accounting.service.ts:1—PrismaAccountingService;AccountingTransactionClienttype alias at:11;executeTransaction<T>()helper at:87.ddx-api/prisma-accounting/schema.prisma:1— accounting schema; 16 models, 21 enums; envACCOUNTING_DATABASE_URL; output@ddx/prisma-accounting.
Billing integration
ddx-api/src/financial/accounting/invoice-accounting/invoice-accounting.service.ts:1—InvoiceAccountingService; bridge betweenBillingModuleinvoices and accounting journal entries.ddx-api/src/financial/accounting/accounting.module.ts:90—forwardRef(() => BillingModule)import — required becauseProductsServicesyncs to FHIRChargeItemDefinitionviaBillingModule.ChargeItemDefinitionService(F2.4).
Reports
ddx-api/src/financial/accounting/reports/reports.service.ts:1—ReportsService; trial balance, P&L, balance sheet (v6.3+ generation).ddx-api/src/financial/accounting/reports/reports.controller.ts:1—ReportsController; endpoint group for financial report generation.
Operational Notes
ACCOUNTING_DATABASE_URLis mandatory at boot —PrismaAccountingService.onModuleInit()will throw if the env var is absent. Set alongsideDATABASE_URLin every environment.pnpm run prisma:generate:accountingmust be run after any change toprisma-accounting/schema.prisma. Theprebuildhook runsprisma:generate:allautomatically, so this is only manually needed during active schema development.- Balance validation uses floating-point rounding —
journal-entries.service.ts:509rounds 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-NNNNnumber. If gap-less numbering is required (e.g. Germany§238 HGB), implement a DB sequence onddx_api_accand replacegenerateEntryNumber(). - Cross-schema reads —
PaymentsServiceoccasionally needs patient or invoice data fromddx_api_main. It injects bothPrismaAccountingServiceandPrismaServiceand assembles in-process. No Prisma-level join is possible across separate PostgreSQL instances. - FHIR hook errors are swallowed — if
PAYMENT_NOTICE_FACADE.create()fails afterprisma:acc.Payment.create(), the payment is persisted but the FHIR projection is missing. Monitorddx-apilogs for[ResourceFacade] afterCreate hook error. - Validation command —
pnpm run prisma:migrate:status:accountingto check pending migrations onddx_api_acc.
Related Topics
- Billing — Invoices, Charge Items and Pricing — patient billing;
BillingModuleandAccountingModuleare mutually dependent viaforwardRef();InvoiceAccountingServicebridges the two. - Prisma 7 Schemas —
ddx_api_acc/PrismaAccountingServicefollows the identical five-schema pattern;ACCOUNTING_DATABASE_URLenv var. - FHIR ResourceFacade —
ResourceFacade<PaymentNoticeResource>is the seventh financial resource migrated under CR-031 (2026-05-16);StoreKind = 'fhir'with Tier-0fhir_resource_linkhooks. - Audit Logging — all accounting mutations (journal entries, payments) are recorded to
ddx_api_logviaAuditInterceptorat position 8 in the global pipeline. - Insurance and Coverage — insurance claims reference invoices and coverage; settled claims trigger payment records in the accounting ledger.