04-clinicalwave: W2filled19 citations

Patients — Registration and Demographics

Audiences: doctor, clinical-buyer, developer

Patients — Registration and Demographics

A patient in Dudoxx HMS is a dual-store entity: a User row in Prisma (ddx_api_main) for operational identity (auth, MRN, insurance), and a FHIR Patient resource in HAPI for clinical identity (demographics, consent, family relationships) — linked by UUID with a fhirPatientId field on the Prisma User row.

Business Purpose

Patient registration is the entry point for every clinical workflow. A patient who is not yet registered cannot have appointments booked, visits opened, or clinical data recorded. The registration process must:

  1. Create a stable identity that persists across visits and practitioners.
  2. Assign a Medical Record Number (MRN) that serves as the legal identifier for all clinical documents.
  3. Sync demographics to FHIR so the patient can be found by any FHIR-aware component (lab system, insurance gateway, AI agent).
  4. Optionally capture family/emergency contacts and insurance coverage at registration time.

Dudoxx HMS satisfies all four requirements through a transactional PatientCreationService that orchestrates Prisma and HAPI writes atomically, with compensation logic if either store fails.

Audiences

  • Investor: Patient count is the primary adoption metric. The dual Prisma+FHIR registration enables both operational scalability (Prisma fast-path for appointments) and regulatory compliance (FHIR for insurance, referral, and ePA integration) without maintaining a synchronization job.
  • Clinical buyer (doctor/nurse/receptionist): Receptionists register patients through the patient portal form. The system auto-assigns an MRN on creation. Existing patients can be found by name fuzzy search, phone, or MRN. Doctors see full demographics + active conditions + medications in the medical card on the patient detail page.
  • Developer/partner: GET|POST|PUT|PATCH|DELETE /api/v1/patients. Patient UUID (Prisma user.id) is the stable API identifier. FHIR Patient numeric ID is an internal implementation detail resolved by PatientIdResolverService. Fuzzy patient search available via GET /api/v1/patients?search={term}. Batch info lookup at POST /api/v1/patients/batch-info.
  • Internal (ops/support): Patient creation is a 9-step transactional process with state tracking in Prisma. If creation fails mid-flight, the compensateFhirCreate() path deletes any partially-created FHIR resources. MRN format is controlled by MrnService and configured per organization.

Architecture

diagram
PatientsCrudController  (/api/v1/patients)
  └── PatientsService (orchestrator — delegates to 11 sub-services)
        ├── PatientCreationService      — transactional Prisma+FHIR+MRN creation
        ├── PatientCrudService          — FHIR read/update/delete; IFhirClient
        ├── PatientSearchService        — fuzzy search across Prisma + FHIR
        ├── PatientAggregationService   — parallel FHIR fetch → PatientSummaryResponseDto
        ├── PatientIdResolverService    — UUID ↔ FHIR Patient numeric ID resolution
        ├── PatientIdResolver (sync)    — fast local cache for ID resolution
        ├── PatientMedicationService    — patient-level medication management
        ├── PatientAttachmentService    — document/file attachments
        ├── PatientCareTeamService      — FHIR CareTeam resources
        ├── PatientFavoritesActivity    — favorite/recently-viewed flags
        └── PatientEverythingService    — FHIR $everything export

PatientCreationService creation steps (9 phases):
  1. findExistingPatient()      — dedup check by email hash
  2. generateMrn()              — MrnService.assignMrn()
  3. createFhirPatient()        — POST Patient to HAPI
  4. createFhirPerson()         — POST Person (identity link for MDM)
  5. buildPersonLink()          — link Person → Patient
  6. persistDbState()           — Prisma User + OrganizationMember + fhir_resource_link
  7. createRelatedPersons()     — FHIR RelatedPerson for emergency contacts (with retry backoff)
  8. toEmergencyContactCreate() — Prisma EmergencyContact rows
  9. toInsuranceCoverageCreate()— Prisma InsuranceCoverage rows

If any HAPI write fails, compensateFhirCreate() deletes the Patient and Person resources to prevent orphaned FHIR data.

See coding_context/ddx-hms-context.md §Clinical for the module map.

Tech Stack & Choices

LayerTechnologyRationale
APINestJS 11, multi-controller split (patients-crud, patients-documents, patients-medical, etc.)Split by concern to keep controller files under 700 lines
AuthRole-based: DOCTOR/NURSE for clinical read; RECEPTIONIST/CLINIC_ADMIN for registration; PATIENT for self-serviceFine-grained role gating per endpoint
FHIR identityPatient + Person (MDM)Person links allows future MDM merge/unmerge without deleting Patient resources
MRNMrnService.assignMrn()MRN_SYSTEM_PREFIX constantFormat {prefix}-{orgSlug}-{seq} configured per organization
Prisma identityUser table (ddx_api_main)Stores email hash for dedup, fhirPatientId for cross-store resolution, auth credentials
ID resolutionPatientIdResolverService — cache + Prisma lookupUUID → FHIR ID in ~1ms (cache hit) or ~5ms (DB lookup); critical for all FHIR operations
DemographicsFHIR Patient.name, .telecom, .address, .gender, .birthDateNormalized via normalizeGender() + formatFhirAddress() helper functions
Fuzzy searchPatientSearchService — Prisma fulltext + FHIR text searchReturns ranked results across both stores; used by receptionist appointment booking
Text aggregationPatientTextAggregationService — builds LLM-ready patient textUsed by TUCAN AI to inject patient context without a separate API call

Key design decision: The Prisma User UUID is the stable public API identifier — NOT the FHIR Patient numeric ID. All endpoint routes use UUIDs (/patients/:id). The PatientIdResolverService maintains a cache of UUID → FHIR numeric ID mappings to avoid per-request HAPI lookups. Passing a FHIR numeric ID to any patient endpoint returns 404.

Data Flow

New patient registration

Business outcome: Receptionist registers a new patient; within 3 seconds, the patient appears in the system with an MRN and a FHIR Patient record.

Technical mechanism:

  1. POST /api/v1/patients with { firstName, lastName, email, birthDate, gender, phone, address, emergencyContacts[], insuranceCoverages[] } and X-Clinic-ID.
  2. PatientsCrudController.createPatient() (patients-crud.controller.ts:322) delegates to PatientsService.createPatient().
  3. PatientCreationService.createPatientTransactional() (patient-creation.service.ts) executes 9 phases:
    • Phase 1: findExistingPatient() by emailHash — throws ConflictException if duplicate.
    • Phase 2: generateMrn()MrnService.assignMrn(orgId) → MRN string.
    • Phase 3: createFhirPatient()fhirClient.create('Patient', { name, telecom, address, gender, birthDate }, clinicSlug).
    • Phase 4: createFhirPerson()fhirClient.create('Person', { ... }, clinicSlug) for MDM identity linkage.
    • Phase 6: persistDbState()prisma.user.create(...) with fhirPatientId from phase 3 result; prisma.organizationMember.create(...).
    • Phases 7-9: Create related persons, emergency contacts, insurance coverages.
  4. On any HAPI error: compensateFhirCreate() deletes Patient + Person resources.
  5. Returns PatientResponseDto with UUID, MRN, FHIR Patient ID.

Find patient by fuzzy name

GET /api/v1/patients?search={term}&limit=20PatientsService.listPatientsWithFuzzySearch() (patients.service.ts:161) → PatientSearchService queries Prisma fulltext index on firstName + lastName + email, then enriches with FHIR demographics. Returns { data, total, page }.

Get patient detail (medical card)

GET /api/v1/patients/:idPatientsService.getPatientDetail() (patients.service.ts:217) → PatientAggregationService.getPatientDetail() → parallel FHIR fetch for demographics, conditions, medications, allergies, encounters, appointments → returns PatientDetailResponseDto.

FHIR $everything export

GET /api/v1/patients/:id/everythingPatientsService.getPatientEverything() (patients.service.ts:659) → PatientEverythingServicefhirClient.everything(fhirPatientId, clinicSlug) → FHIR Bundle with all linked resources.

Implicated Code

  • ddx-api/src/clinical/patients/controllers/patients-crud.controller.tsPatientsCrudController@ResourceMeta decorated; handles list/get/create/update/patch/delete/export/bulk/aggregate + getAdjacentPatients, getBatchInfo, backfillLastActivity, regeneratePatientAggregate
  • ddx-api/src/clinical/patients/controllers/patients-crud.controller.ts:147listPatients()GET /patients — pagination, filters, fuzzy search dispatch
  • ddx-api/src/clinical/patients/controllers/patients-crud.controller.ts:302getPatientDetail()GET /patients/:id — aggregated medical-card detail
  • ddx-api/src/clinical/patients/controllers/patients-crud.controller.ts:347createPatient()POST /patients — transactional creation with emergency contacts + insurance
  • ddx-api/src/clinical/patients/controllers/patients-crud.controller.ts:462patchPatient()PATCH /patients/:id — partial Prisma + FHIR update; updatePatient() (:420) = full PUT replace
  • ddx-api/src/clinical/patients/controllers/patients-medical.controller.ts:66getPatientSummary()GET /patients/:id/summaryPatientSummaryResponseDto (demographics incl. maritalStatus/preferredLanguage/emergencyContacts, conditions, medications, allergies, encounters, appointments)
  • ddx-api/src/clinical/patients/patients.service.ts:93PatientsService — orchestrator delegating to 11 sub-services
  • ddx-api/src/clinical/patients/patients.service.ts:161listPatientsWithFuzzySearch() — fuzzy name/email search across Prisma + FHIR
  • ddx-api/src/clinical/patients/patients.service.ts:411ensureFhirPatient() — lazy FHIR Patient creation for patients without fhirPatientId (pre-migration records)
  • ddx-api/src/clinical/patients/services/patient-creation.service.ts:1PatientCreationService — 9-phase transactional creation with FHIR compensation; MRN assignment; RelatedPerson retry backoff (RELATED_PERSON_BACKOFF_MS, RELATED_PERSON_MAX_ATTEMPTS)
  • ddx-api/src/clinical/patients/services/patient-id-resolver.service.ts:1PatientIdResolverService — UUID ↔ FHIR Patient ID resolution with sync throttle (SYNC_THROTTLE_MS); resolveFhirPatientId() used by every FHIR-writing service
  • ddx-api/src/clinical/patients/services/patient-aggregation.service.ts:72PatientAggregationService — parallel FHIR fetch for medical card; see Medical Cards
  • ddx-api/src/clinical/patients/services/patient-search.service.ts:1PatientSearchService — Prisma fulltext + FHIR text search; ranked merge
  • ddx-api/src/clinical/patients/dto/patient-summary-response.dto.ts:172PatientSummaryResponseDto — 6-component aggregate DTO (demographics, conditions, medications, allergies, encounters, appointments)
  • ddx-api/src/platform/mrn/mrn.service.ts:1MrnService — MRN generation with MRN_SYSTEM_PREFIX; org-scoped sequential assignment

Operational Notes

  • ensureFhirPatient() is critical: Any patient created before the FHIR sync (or via seeder without FHIR write) will have fhirPatientId = null. PatientsService.ensureFhirPatient() (patients.service.ts:411) creates the FHIR Patient lazily on the next read. This is called by GET /visits/me before returning visit history. Services that assume fhirPatientId is non-null will throw NotFoundException for pre-migration patients.
  • UUID is the stable identifier: FHIR Patient numeric IDs change if HAPI is wiped and re-seeded. The Prisma UUID (user.id) never changes. External systems must use UUIDs in API calls.
  • Email hash dedup: PatientCreationService.findExistingPatient() checks emailHash (SHA-256 of lowercase email) — not raw email — to avoid storing PII in the lookup index. Registering the same patient email twice throws ConflictException.
  • RelatedPerson retry: HAPI sometimes returns a transient error when creating RelatedPerson resources immediately after Person creation. createOneRelatedPersonWithRetry() uses exponential backoff (RELATED_PERSON_BACKOFF_MS, RELATED_PERSON_MAX_ATTEMPTS) before giving up (non-fatal — patient is still created).
  • Patch vs PUT semantics: PATCH /patients/:id is a partial update (only supplied fields are changed in Prisma + FHIR). PUT /patients/:id is a full replace. Use PATCH for UI edit forms; PUT for bulk import.
  • FHIR patient ID resolution cache: PatientIdResolverService caches UUID→FHIR ID mappings. If a FHIR Patient is deleted and re-created (e.g., after a HAPI reset in dev), the cache must be cleared or the service restarted to pick up the new FHIR IDs.
    Patients — Registration and Demographics — Dudoxx Docs | Dudoxx