10-cross-cuttingwave: W6filled17 citations

Insurance and Coverage — Claims Management

Audiences: clinical-buyer, developer

Insurance and Coverage — Claims Management

Dudoxx HMS manages patient insurance coverage and claim submission through FHIR R4 Coverage and Claim resources, using ResourceFacade<ClaimResource> for tenant-isolated FHIR dispatch and the fhir_resource_link bridge 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:

  1. Coverage verification — before a visit, reception staff verify that the patient's insurance coverage (Coverage FHIR resource) is active and determine the co-payment amount.
  2. Claim submission — after visit completion, a Claim resource is submitted to the payer referencing the patient's Coverage and the billing Invoice.
  3. Claim lifecycle tracking — claims move through draft → active → completed/cancelled statuses; 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 Claim resource allows future integration with national claim clearinghouses (gematik TI in Germany, ANS in France) without an architectural rewrite. Coverage verification on the Coverage resource 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_FACADE wraps ResourceFacade<ClaimResource> for all Claim CRUD, while Coverage resources are managed via a separate coverage controller/service (coverage submodule). Patient ID resolution accepts both UUID and FHIR ID inputs via isUuid()/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 a prisma:main projection. All claim writes are recorded in ddx_api_log via AuditService.

Architecture

InsuranceModule (ddx-api/src/financial/insurance/) owns:

SubmoduleStorage pathDetail
ClaimsController / ClaimsServiceFHIRCLAIM_FACADEResourceFacade<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)

MethodRoute
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)

MethodRouteHandler
POST/insurance/claimsClaimsService.create()
GET/insurance/claimsClaimsService.findAll()
GET/insurance/claims/:idClaimsService.findOne()
GET/insurance/statsClaimsService.getStatistics()
POST/insurance/claims/bulkClaimsService.bulkOperation()

Claims-EU submodule — EU billing country-pack, RELATIONAL path (prisma-accounting). ClaimsEuController @Controller('insurance/eu')ClaimsEuService (prisma:acc)

MethodRouteDetail
POST/GET/PATCH/insurance/eu/claimsClaim + nested ClaimItems (Σ item nets)
POST/GET/PATCH/insurance/eu/claim-responsesoutcome COMPLETE/PARTIAL/ERROR + adjudications
POST/GET/PATCH/insurance/eu/payment-reconciliationslinks ClaimClaimResponse via ReconciliationDetail
POST/insurance/eu/ar-clearingsAR-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

ConcernChoiceRationale
Claim storageHAPI FHIR R4 Claim resource (store: 'fhir')Standard interoperability; Claim.insurance[].coverage references Coverage by FHIR reference string
Coverage storageHAPI FHIR R4 Coverage resource (legacy FHIR_CLIENT path)Coverage endpoints predate ResourceFacade (v6.0.0); migration to ResourceFacade is tracked under CR-031
Bridge tablefhir_resource_link in ddx_api_mainEnables fast cross-tenant claim lookup by (resourceType='Claim', clinicId)
Patient ID resolutionisUuid(id)prisma.user.fhirPatientId lookup; isFhirId(id) → use directlyAccepts both DTO patterns without separate endpoints
Claim status mappingFHIR status codes (draft/active/completed/cancelled/entered-in-error)Statistics aggregator (getStatistics()) maps FHIR statuses to business labels (pending/submitted/approved/rejected)
Audit loggingAuditService.log({action: 'CREATE', resource: 'Claim', ...})All claim writes recorded to ddx_api_log via the logging module
Eligibility checkPOST /insurance/coverages/:id/check-eligibilityEndpoint 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:1InsuranceModule; forwardRef(() => PatientsModule) for patient FHIR ID resolution; CLAIM_FACADE useFactory at :63.
  • ddx-api/src/financial/insurance/insurance.module.ts:63CLAIM_FACADE useFactory: 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:1makeClaimDescriptor(prisma) factory; Tier-1 FHIR-only descriptor; comment notes that Coverage reference is resolved by FHIR server at search time — no COVERAGE_FACADE injection needed.
  • ddx-api/src/financial/insurance/claims/claim.tokens.ts:1CLAIM_FACADE DI symbol + ClaimFacade type 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:58ClaimsService constructor: injects PrismaService, AuditService, @Inject(CLAIM_FACADE) claimFacade.
  • ddx-api/src/financial/insurance/claims/claims.service.ts:67create(): dual-arg call claimFacade.create(claim, clinicId, clinicId)clinicId is both the HAPI tenant slug and the organizationId for fhir_resource_link.
  • ddx-api/src/financial/insurance/claims/claims.service.ts:176getStatistics(): FHIR search with _count: 1000 + in-process aggregation; noted performance concern for high-volume clinics.
  • ddx-api/src/financial/insurance/claims/claims.service.ts:308resolvePatientId(): isFhirId / isUuid branching; prisma.user.findUnique({select: {fhirPatientId}}) on UUID path.

Claims controller and DTOs

  • ddx-api/src/financial/insurance/claims/claims.controller.ts:1ClaimsController; endpoints: POST claims, GET claims, GET claims/:id, GET stats, POST bulk.
  • ddx-api/src/financial/insurance/claims/dto/create-claim.dto.ts:1CreateClaimDto: patientId (UUID or FHIR ID), coverageId (FHIR Coverage ID).
  • ddx-api/src/financial/insurance/claims/dto/claim-response.dto.ts:1ClaimResponseDto, ClaimListResponseDto.

Claims-EU submodule (relational EU billing chain)

  • ddx-api/src/financial/insurance/claims-eu/claims-eu.service.ts:53ClaimsEuService; relational system-of-record in prisma:acc for Claim → ClaimResponse → PaymentReconciliation; money in Prisma.Decimal; organizationId-scoped queries. NOT FHIR-backed.
  • ddx-api/src/financial/insurance/claims-eu/claims-eu.controller.ts:46ClaimsEuController @Controller('insurance/eu'); endpoints for claims, claim-responses, payment-reconciliations, ar-clearings.
  • ddx-api/src/financial/billing/dto/eu-billing.types.tsCreateClaimEuDto, ClaimResponseResourceResponseDto, PaymentReconciliationResponseDto, etc. (shared EU billing DTO types).

Coverage DTOs (legacy coverage endpoints)

  • ddx-api/src/financial/insurance/dto/create-coverage.dto.ts:1CreateCoverageDto; coverage creation for the pre-ResourceFacade coverage endpoints.
  • ddx-api/src/financial/insurance/dto/coverage-response.dto.ts:1CoverageResponseDto.
  • ddx-api/src/financial/insurance/dto/verify-coverage.dto.ts:1VerifyCoverageDto; eligibility check payload.

Operational Notes

  • Coverage endpoints are on the legacy FhirResourceService path — the original coverage CRUD (9 endpoints listed in insurance.module.ts JSDoc) was not migrated to ResourceFacade in the F3.x wave. These endpoints still use @deprecated FhirResourceService. Migration is tracked under CR-031.
  • Claim statistics use _count: 1000getStatistics() 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 _count param or a cached/projected approach for high-volume clinics.
  • clinicId doubles as organizationId in ClaimsService.create() — the comment at :111 documents this explicitly. The dual-arg call facade.create(claim, clinicId, clinicId) passes the clinic slug as both the HAPI tenant parameter and the fhir_resource_link.organizationId. This is a simplification: for multi-org clinics, the true organizationId (UUID) should be passed as the second arg.
  • Audit logging on writesAuditService.log() is called after every successful create(); it writes to ddx_api_log via the logging module's AuditTrail table. Failed FHIR writes do not produce an audit record.
  • FHIR Coverage reference resolution — a Claim references Coverage by {reference: 'Coverage/{id}'}. The FHIR server resolves this reference at search time; InsuranceModule does not inject or fetch the Coverage resource at claim creation time.
  • PatientsModule circular depforwardRef(() => PatientsModule) is required because PatientsModule also imports insurance-adjacent modules. Always use forwardRef() on both sides if a true circular dependency exists.