Insurance and Coverage — Claims Management
Audiences: clinical-buyer, developer
Dudoxx HMS manages patient insurance coverage and claim submission through FHIR R4
CoverageandClaimresources, usingResourceFacade<ClaimResource>for tenant-isolated FHIR dispatch and thefhir_resource_linkbridge for cross-tenant indexing.
Business Purpose
In European outpatient healthcare, every billable visit must be accompanied by an insurance claim submitted to the payer — either a statutory health insurer (GKV, Krankenkasse) or a private insurer (PKV). The workflow requires:
- Coverage verification — before a visit, reception staff verify that the patient's insurance coverage (
CoverageFHIR resource) is active and determine the co-payment amount. - Claim submission — after visit completion, a
Claimresource is submitted to the payer referencing the patient'sCoverageand the billingInvoice. - Claim lifecycle tracking — claims move through
draft → active → completed/cancelledstatuses; finance staff reconcile approved claims against the accounting payment ledger.
The insurance module provides REST endpoints for all three phases, storing all data in HAPI FHIR R4 (tenant-partitioned) with the fhir_resource_link bridge keeping a fast Prisma-queryable index for audit and reporting.
Audiences
- Investor: FHIR-native claim submission removes the custom payer-integration layer that plagues legacy HMS vendors. Using the standard
Claimresource allows future integration with national claim clearinghouses (gematik TI in Germany, ANS in France) without an architectural rewrite. Coverage verification on theCoverageresource is a prerequisite for multi-payer billing. - Clinical buyer (doctor/nurse/receptionist): Reception staff use the coverage verification flow to check active insurance before a visit. After the visit, billing staff use claim creation to submit to insurers. The accountant portal shows claim status alongside invoice status.
- Developer/partner:
InsuranceModule(src/financial/insurance/) is a clean single-facade module —CLAIM_FACADEwrapsResourceFacade<ClaimResource>for all Claim CRUD, whileCoverageresources are managed via a separate coverage controller/service (coverage submodule). Patient ID resolution accepts both UUID and FHIR ID inputs viaisUuid()/isFhirId()utility functions. - Internal (ops/support): Claim statistics are aggregated from FHIR search results in
getStatistics()— no separate analytics table. For clinics with high claim volume, consider caching or aprisma:mainprojection. All claim writes are recorded inddx_api_logviaAuditService.
Architecture
InsuranceModule (ddx-api/src/financial/insurance/)
│
├── ClaimsController / ClaimsService
│ CLAIM_FACADE → ResourceFacade<ClaimResource>
│ claim.descriptor.ts (store: 'fhir', Tier-0 fhir_resource_link hooks)
│ Patient ID resolution: UUID → user.fhirPatientId via PrismaService
│
├── Coverage submodule (original endpoints, no ResourceFacade — legacy FHIR_CLIENT)
│ POST /insurance/coverages
│ GET /insurance/patient/:patientId/coverages
│ GET /insurance/coverages/:id
│ PUT /insurance/coverages/:id
│ DELETE /insurance/coverages/:id
│ GET /insurance/patient/:patientId/summary
│ POST /insurance/coverages/:id/check-eligibility
│ GET /insurance/patient/:patientId/primary
│ GET /insurance/patient/:patientId/active
│
├── Claims submodule (PMS Phase 2, ResourceFacade — FHIR path)
│ POST /insurance/claims ClaimsService.create()
│ GET /insurance/claims ClaimsService.findAll()
│ GET /insurance/claims/:id ClaimsService.findOne()
│ GET /insurance/stats ClaimsService.getStatistics()
│ POST /insurance/claims/bulk ClaimsService.bulkOperation()
│
└── Claims-EU submodule (EU billing country-pack — RELATIONAL path, prisma-accounting)
ClaimsEuController @Controller('insurance/eu') → ClaimsEuService (prisma:acc)
POST/GET/PATCH /insurance/eu/claims — Claim + nested ClaimItems (Σ item nets)
POST/GET/PATCH /insurance/eu/claim-responses — outcome COMPLETE/PARTIAL/ERROR + adjudications
POST/GET/PATCH /insurance/eu/payment-reconciliations — links Claim ↔ ClaimResponse via ReconciliationDetail
POST /insurance/eu/ar-clearings — AR-clearing → accounting ReconciliationService
forwardRef(() => PatientsModule) ← for patient FHIR ID resolution
Two claim paths coexist: the original claims/ submodule stores FHIR R4 Claim resources via ResourceFacade<ClaimResource> (interoperability). The newer claims-eu/ submodule is a relational system-of-record in ddx_api_acc (Prisma-accounting) for the EU billing chain Claim → ClaimResponse → PaymentReconciliation, with all money in Prisma.Decimal (never JS float) and organizationId scoping. This is the DE-first, pluggable country-pack model (project memory project_eu_billing_country_packs) — the relational chain is the reimbursement system-of-record; the AR-clearing/double-entry bridge lives in the accounting module's ReconciliationService. Claims-EU DTOs live in billing/dto/eu-billing.types.
InsuranceModule does NOT import BillingModule or AccountingModule directly. The FHIR server resolves Coverage references in a Claim.insurance[].coverage field by reference string — no in-process join is needed.
Tech Stack & Choices
| Concern | Choice | Rationale |
|---|---|---|
| Claim storage | HAPI FHIR R4 Claim resource (store: 'fhir') | Standard interoperability; Claim.insurance[].coverage references Coverage by FHIR reference string |
| Coverage storage | HAPI FHIR R4 Coverage resource (legacy FHIR_CLIENT path) | Coverage endpoints predate ResourceFacade (v6.0.0); migration to ResourceFacade is tracked under CR-031 |
| Bridge table | fhir_resource_link in ddx_api_main | Enables fast cross-tenant claim lookup by (resourceType='Claim', clinicId) |
| Patient ID resolution | isUuid(id) → prisma.user.fhirPatientId lookup; isFhirId(id) → use directly | Accepts both DTO patterns without separate endpoints |
| Claim status mapping | FHIR status codes (draft/active/completed/cancelled/entered-in-error) | Statistics aggregator (getStatistics()) maps FHIR statuses to business labels (pending/submitted/approved/rejected) |
| Audit logging | AuditService.log({action: 'CREATE', resource: 'Claim', ...}) | All claim writes recorded to ddx_api_log via the logging module |
| Eligibility check | POST /insurance/coverages/:id/check-eligibility | Endpoint exists; eligibility response handling is payer-specific (no universal standard yet) |
Data Flow
Creating a Claim
POST /insurance/claims
→ ClaimsController
→ ClaimsService.create(dto, userId, clinicId)
1. resolvePatientId(dto.patientId)
if UUID → prisma.user.findUnique({select: {fhirPatientId}})
if FHIR ID → use directly
2. Build FHIR Claim resource:
resourceType: 'Claim'
status: 'active'
type.coding: [{code: 'professional'}]
use: 'claim'
patient: {reference: `Patient/${patientFhirId}`}
provider: {reference: `Organization/${clinicId}`}
priority.coding: [{code: 'normal'}]
insurance: [{sequence:1, focal:true, coverage: {reference: `Coverage/${dto.coverageId}`}}]
3. CLAIM_FACADE.create(claim, clinicId, clinicId)
→ ResourceFacade dispatches to HAPI (store: 'fhir')
→ X-Clinic-ID routes to tenant partition
→ Tier-0 makeBaseLinkHooks.afterCreate upserts fhir_resource_link
(resourceType='Claim', fhirId=<HAPI id>, clinicId=clinicId)
4. AuditService.log({action:'CREATE', resource:'Claim', resourceId: created.id})
5. transformToDto(created) → ClaimResponseDto
Claim Statistics
GET /insurance/stats
→ ClaimsService.getStatistics(clinicId)
1. CLAIM_FACADE.search({_count: '1000'}, clinicId, clinicId)
2. In-process aggregation over result.value.resources:
draft → pending count
active → submitted count
completed → approved count
cancelled/entered-in-error → rejected count
created >= startOfMonth → newThisMonth count
3. return ClaimStatistics
Bulk Operation (status update / cancellation)
POST /insurance/claims/bulk
→ ClaimsService.bulkOperation(dto, clinicId)
Promise.allSettled(dto.ids.map(id =>
1. CLAIM_FACADE.read(id, clinicId, clinicId)
2. For DELETE/ARCHIVE: update status to 'cancelled'
3. For STATUS_UPDATE: update to dto.payload.status
→ CLAIM_FACADE.update(id, {...existing, status: nextStatus}, ...)
))
→ BulkResponseDto {succeeded, failed, total, successCount, failureCount}
Implicated Code
Module
ddx-api/src/financial/insurance/insurance.module.ts:1—InsuranceModule;forwardRef(() => PatientsModule)for patient FHIR ID resolution;CLAIM_FACADEuseFactory at:63.ddx-api/src/financial/insurance/insurance.module.ts:63—CLAIM_FACADEuseFactory:new ResourceFacade<ClaimResource>(makeClaimDescriptor(prisma), fhirClient). Comment references F3.2 plan + F2.1 Account wiring as the canonical shape.
Claim descriptor and tokens
ddx-api/src/financial/insurance/claims/claim.descriptor.ts:1—makeClaimDescriptor(prisma)factory; Tier-1 FHIR-only descriptor; comment notes thatCoveragereference is resolved by FHIR server at search time — noCOVERAGE_FACADEinjection needed.ddx-api/src/financial/insurance/claims/claim.tokens.ts:1—CLAIM_FACADEDI symbol +ClaimFacadetype alias.
Claims service
ddx-api/src/financial/insurance/claims/claims.service.ts:20— JSDoc: "F3.2: migrated from legacy FhirService to ResourceFacade<ClaimResource>"; canonical shape reference to F2.1 Account.ddx-api/src/financial/insurance/claims/claims.service.ts:58—ClaimsServiceconstructor: injectsPrismaService,AuditService,@Inject(CLAIM_FACADE) claimFacade.ddx-api/src/financial/insurance/claims/claims.service.ts:67—create(): dual-arg callclaimFacade.create(claim, clinicId, clinicId)—clinicIdis both the HAPI tenant slug and theorganizationIdforfhir_resource_link.ddx-api/src/financial/insurance/claims/claims.service.ts:176—getStatistics(): FHIR search with_count: 1000+ in-process aggregation; noted performance concern for high-volume clinics.ddx-api/src/financial/insurance/claims/claims.service.ts:308—resolvePatientId():isFhirId/isUuidbranching;prisma.user.findUnique({select: {fhirPatientId}})on UUID path.
Claims controller and DTOs
ddx-api/src/financial/insurance/claims/claims.controller.ts:1—ClaimsController; endpoints: POST claims, GET claims, GET claims/:id, GET stats, POST bulk.ddx-api/src/financial/insurance/claims/dto/create-claim.dto.ts:1—CreateClaimDto:patientId(UUID or FHIR ID),coverageId(FHIR Coverage ID).ddx-api/src/financial/insurance/claims/dto/claim-response.dto.ts:1—ClaimResponseDto,ClaimListResponseDto.
Claims-EU submodule (relational EU billing chain)
ddx-api/src/financial/insurance/claims-eu/claims-eu.service.ts:53—ClaimsEuService; relational system-of-record inprisma:accfor Claim → ClaimResponse → PaymentReconciliation; money inPrisma.Decimal;organizationId-scoped queries. NOT FHIR-backed.ddx-api/src/financial/insurance/claims-eu/claims-eu.controller.ts:46—ClaimsEuController@Controller('insurance/eu'); endpoints for claims, claim-responses, payment-reconciliations, ar-clearings.ddx-api/src/financial/billing/dto/eu-billing.types.ts—CreateClaimEuDto,ClaimResponseResourceResponseDto,PaymentReconciliationResponseDto, etc. (shared EU billing DTO types).
Coverage DTOs (legacy coverage endpoints)
ddx-api/src/financial/insurance/dto/create-coverage.dto.ts:1—CreateCoverageDto; coverage creation for the pre-ResourceFacade coverage endpoints.ddx-api/src/financial/insurance/dto/coverage-response.dto.ts:1—CoverageResponseDto.ddx-api/src/financial/insurance/dto/verify-coverage.dto.ts:1—VerifyCoverageDto; eligibility check payload.
Operational Notes
- Coverage endpoints are on the legacy
FhirResourceServicepath — the original coverage CRUD (9 endpoints listed ininsurance.module.tsJSDoc) was not migrated toResourceFacadein the F3.x wave. These endpoints still use@deprecated FhirResourceService. Migration is tracked under CR-031. - Claim statistics use
_count: 1000—getStatistics()fetches up to 1,000 claims from HAPI and aggregates in-process. For clinics with >1,000 historical claims, the count will be silently truncated. Consider adding a_countparam or a cached/projected approach for high-volume clinics. clinicIddoubles asorganizationIdinClaimsService.create()— the comment at:111documents this explicitly. The dual-arg callfacade.create(claim, clinicId, clinicId)passes the clinic slug as both the HAPI tenant parameter and thefhir_resource_link.organizationId. This is a simplification: for multi-org clinics, the trueorganizationId(UUID) should be passed as the second arg.- Audit logging on writes —
AuditService.log()is called after every successfulcreate(); it writes toddx_api_logvia the logging module'sAuditTrailtable. Failed FHIR writes do not produce an audit record. - FHIR Coverage reference resolution — a
ClaimreferencesCoverageby{reference: 'Coverage/{id}'}. The FHIR server resolves this reference at search time;InsuranceModuledoes not inject or fetch theCoverageresource at claim creation time. - PatientsModule circular dep —
forwardRef(() => PatientsModule)is required becausePatientsModulealso imports insurance-adjacent modules. Always useforwardRef()on both sides if a true circular dependency exists.
Related Topics
- Billing — Invoices, Charge Items and Pricing — claims reference billing
InvoiceandAccountresources; claim approval triggers payment reconciliation in the accounting ledger. - Accounting — Journal Entries, Budgets and Financial Reports — settled claims produce payment records in
ddx_api_acc; the accounting module owns the AR ledger. - FHIR ResourceFacade —
ResourceFacade<ClaimResource>is one of the 7 financial resources migrated under CR-031 (2026-05-16); Tier-0fhir_resource_linkhooks; dual-arg call signature. - FHIR Partitions and Multi-Tenancy —
X-Clinic-IDroutes claim writes to the correct HAPI tenant partition;ClinicPartitionInterceptoron the HAPI side handles the routing.