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

PatientsCrudController (/api/v1/patients) → PatientsService (orchestrator — delegates to 11 sub-services):

#Sub-serviceResponsibility
1PatientCreationServicetransactional Prisma+FHIR+MRN creation
2PatientCrudServiceFHIR read/update/delete; IFhirClient
3PatientSearchServicefuzzy search across Prisma + FHIR
4PatientAggregationServiceparallel FHIR fetch → PatientSummaryResponseDto
5PatientIdResolverServiceUUID ↔ FHIR Patient numeric ID resolution
6PatientIdResolver (sync)fast local cache for ID resolution
7PatientMedicationServicepatient-level medication management
8PatientAttachmentServicedocument/file attachments
9PatientCareTeamServiceFHIR CareTeam resources
10PatientFavoritesActivityfavorite/recently-viewed flags
11PatientEverythingServiceFHIR $everything export

PatientCreationService creation steps (9 phases, in order):

#StepDetail
1findExistingPatient()dedup check by email hash
2generateMrn()MrnService.assignMrn()
3createFhirPatient()POST Patient to HAPI
4createFhirPerson()POST Person (identity link for MDM)
5buildPersonLink()link PersonPatient
6persistDbState()Prisma User + OrganizationMember + fhir_resource_link
7createRelatedPersons()FHIR RelatedPerson for emergency contacts (with retry backoff)
8toEmergencyContactCreate()Prisma EmergencyContact rows
9toInsuranceCoverageCreate()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