04-clinicalwave: W2filled12 citations

Visits — Clinical Encounter Management

Audiences: doctor, nurse, receptionist, clinical-buyer, developer

Visits — Clinical Encounter Management

The Visit (FHIR Encounter) is the central clinical object in Dudoxx HMS — every medication, vital, diagnosis, recording, and document attaches to a visit, making it the transactional anchor for all patient-facing clinical work.

Business Purpose

A "visit" represents a discrete clinical encounter between a patient and a practitioner — in person, virtual, or emergency. Clinics need a reliable, auditable record of every consultation: who saw whom, when, why, what happened, and what follow-up is required.

Dudoxx HMS models each visit as a FHIR R4 Encounter resource on HAPI FHIR, enriched with Prisma-side metadata (recordings, links to appointments, treatment plans). This dual-store design means:

  • Interoperability with any FHIR-aware system out of the box (labs, insurance, referral networks).
  • Rich operational data (audio recordings, AI agent context) in Prisma for fast access.
  • A single UUID is the public API surface; internal FHIR Encounter IDs are resolved transparently.

Clinical buyers (practice managers, compliance officers) gain an auditable encounter log with status history; doctors get an AI-enriched context endpoint (/agent-context) that feeds the TUCAN AI with patient recordings and transcriptions.

Audiences

  • Investor: Every consultation generates a visit record — the volume of visits is a leading indicator of platform adoption. Encounters are stored as FHIR, satisfying regulators and enabling future payer integrations.
  • Clinical buyer (doctor/nurse/receptionist): Doctors open a visit when a patient arrives; nurses record vitals against it; receptionists link it to an appointment. A visit cannot close until status transitions to finished.
  • Developer/partner: REST CRUD at GET|POST|PATCH|DELETE /api/v1/visits. Encounters keyed by UUID (not numeric FHIR IDs). Agent-context endpoint (GET /visits/:id/agent-context) returns structured text for LLM consumption.
  • Internal (ops/support): All create/update/delete operations emit audit log rows via AuditService. Status changes are tracked in FHIR statusHistory. Recordings and attachments are counted on active-visit queries.

Architecture

The Visit subsystem sits in ddx-api/src/clinical/visits/ and follows the Orchestrator Pattern: VisitsControllerVisitsService (orchestrator) → five specialized sub-services:

diagram
VisitsController
  └── VisitsService (orchestrator)
        ├── VisitCrudService              — CRUD facade (delegates create to VisitLifecycleService)
        ├── VisitFhirService              — FHIR Encounter create/search/delete via facade
        ├── VisitValidationService        — create-time business-rule guards (conflict, availability)
        ├── VisitResolverService          — UUID → FHIR Encounter ID resolution (also resolves Appointment UUID → visit)
        ├── VisitLifecycleService         — createVisit: FHIR Encounter + Prisma visit row + patient-activity bump
        ├── VisitDischargeService         — discharge: FHIR Condition + MedicationStatement from treatment text (best-effort)
        └── VisitTreatmentPlanFhirService — DocumentReference for generated treatment-plan PDFs

VisitsService.create() is get-or-create: it enforces "one open visit per patient per practitioner" (open = status ∈ {planned, arrived, in-progress}) — an existing open visit is RESUMED rather than duplicated. The response data is a discriminated union on resolution ('created' | 'resumed'). A VisitRaceError from a concurrent create triggers an orphan-Encounter cleanup (deleteFhirEncounter).

The EncounterResource interface (NF2.4 facade) lives in encounter.descriptor.ts and declares the complete FHIR R4 Encounter field set. All encounter operations go through the ResourceFacade<EncounterResource>, which upserts a fhir_resource_link Prisma row on each write — providing a cross-store audit trail without duplicating the payload.

Visit-appointment linking is a side-car service (VisitAppointmentLinkingService) that maintains a bidirectional association between FHIR Encounters and appointment records.

Cross-cutting concerns: AuditService logs every CREATE/UPDATE/DELETE; TenantIsolationInterceptor enforces organizationId scoping on every request; RbacEnforcementInterceptor enforces role gates declared via @RequireRole().

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

Tech Stack & Choices

LayerTechnologyRationale
APINestJS 11, @Controller('visits')Uniform REST surface with Swagger auto-docs
Auth@RequireRole(UserRole.*) + GatewayAuthGuardTrusted Gateway v2.1 — roles resolved from JWT at Next.js, forwarded as headers
FHIR storageHAPI FHIR R4 (port 18080) via IFhirClientEncounters are authoritative in FHIR; fhir_resource_link Prisma row provides fast UUID lookup
PrismaPrismaService (ddx_api_main)Stores Recording, VisitAppointmentLink, and fhir_resource_link rows
DescriptormakeEncounterDescriptor() + ResourceFacadeNF2.4 pattern — Tier-1 FHIR-only resource; hooks upsert fhir_resource_link after writes
AuditAuditService.log()Structured JSON rows in audit log DB
Rate-limitingThrottlerModule 3-tierProtects batch visit listing in large clinics

Key design decision: Visit UUIDs (Prisma) are the stable public API identifiers. Numeric FHIR Encounter IDs are an internal implementation detail — VisitResolverService.resolveFhirId() enforces this boundary. Passing a non-UUID to any visit endpoint returns HTTP 400, preventing FHIR ID leakage.

Data Flow

Doctor opens a visit (happy path)

Business outcome: A doctor clicks "Start Visit" for an arriving patient; within 1 second the visit appears as in-progress on all clinical staff dashboards.

Technical mechanism:

  1. ddx-web sends POST /api/v1/visits with { patientId, practitionerId, encounterClass: "AMB" } via Next.js proxy (ddx-web/src/app/api/proxy/[...path]/route.ts).
  2. VisitsController.create() (ddx-api/src/clinical/visits/visits.controller.ts:96) resolves effectiveOrgId from X-Clinic-ID header, auto-populates practitionerId from gateway user if empty.
  3. VisitsService.create() calls VisitValidationService.validateCreateVisit(), then checks for an existing OPEN visit (get-or-create); if none, delegates to VisitCrudService.createVisit()VisitLifecycleService.createVisit().
  4. VisitLifecycleService issues a FHIR POST /Encounter (via VisitFhirService.createFhirEncounter() → facade) to HAPI FHIR 18080, then writes the Prisma visit row and bumps patient-activity. On success the facade upserts a fhir_resource_link Prisma row mapping UUID → FHIR Encounter ID.
  5. AuditService.log() records action: 'CREATE', resource: 'Visit' with patient/practitioner context (visits.service.ts:72).
  6. Response is wrapped by ResponseTransformInterceptor into { data, meta } envelope.

Doctor checks for active visit (TUCAN integration)

GET /api/v1/visits/active?patientId=xxx (visits.controller.ts:369) returns { hasActiveVisit: true, visit: { visitId, status, recordingCount, ... } }. Used by TUCAN AI agent to determine if a consultation is live before running clinical tools.

Patient views own visit history

GET /api/v1/visits/me (visits.controller.ts:163) — PATIENT role only; auto-creates FHIR Patient record if missing via PatientsService.ensureFhirPatient(). Returns paginated bundle.

AI agent context

GET /api/v1/visits/:id/agent-context (visits.controller.ts:688) assembles a structured text block with visit metadata + per-recording transcription timelines. Used by TUCAN to inject clinical context into LLM prompts.

Implicated Code

  • ddx-api/src/clinical/visits/visits.controller.ts:96POST /visits — creates encounter, resolves effective org, auto-fills practitionerId
  • ddx-api/src/clinical/visits/visits.controller.ts:163GET /visits/me — patient self-service visit list with FHIR auto-sync
  • ddx-api/src/clinical/visits/visits.controller.ts:369GET /visits/active — TUCAN agent check for in-progress consultation
  • ddx-api/src/clinical/visits/visits.controller.ts:688GET /visits/:id/agent-context — structured LLM context with transcription timelines
  • ddx-api/src/clinical/visits/visits.controller.ts:803POST /visits/:visitId/link-appointment/:appointmentId — bidirectional appointment link
  • ddx-api/src/clinical/visits/visits.service.tsVisitsService — orchestrator; delegates to sub-services
  • ddx-api/src/clinical/visits/visits.service.tscreate() — get-or-create; validate → find open visit (resume) OR FHIR create → audit; discriminated resolution union
  • ddx-api/src/clinical/visits/visits.service.tsresolveVisitId() — strict UUID-only; rejects numeric FHIR IDs with HTTP 400
  • ddx-api/src/clinical/visits/visits.service.tsfindActiveVisit() — FHIR search for status=in-progress, returns recording/attachment counts from Prisma
  • ddx-api/src/clinical/visits/services/visit-lifecycle.service.tscreateVisit() — FHIR Encounter + Prisma visit row + patient-activity bump
  • ddx-api/src/clinical/visits/services/visit-fhir.service.tscreateFhirEncounter() — builds FHIR R4 Encounter (participants, patient/practitioner display) via facade
  • ddx-api/src/clinical/visits/encounter.descriptor.tsEncounterResource interface + makeEncounterDescriptor(); hooks upsert fhir_resource_link after FHIR writes (NF2.4)
  • ddx-api/src/clinical/visits/visit-appointment-linking.service.tsgetOrCreateVisitForAppointment() — unified visit resolution for appointment start / check-in / video-join
  • ddx-api/src/clinical/visits/services/visit-resolver.service.tsresolveVisitId() / getOrCreateVisit() — resolves Visit UUID or Appointment UUID → real visits.id

Operational Notes

  • UUID boundary (critical): Passing a numeric FHIR Encounter ID to any /visits/:id endpoint returns HTTP 400. The resolveVisitId() method enforces this (visits.service.ts:110). Older code paths that passed numeric IDs directly are a known source of silent write misses.
  • PATIENT role auto-filter: GET /visits with a PATIENT-role JWT automatically scopes results to the authenticated patient's FHIR ID; the patientId query param is ignored. No explicit guard needed — implemented in the controller at line 323.
  • FHIR soft-delete: DELETE /visits/:id does NOT delete the FHIR Encounter — it sets status to cancelled. Encounters are never destroyed, preserving audit integrity.
  • TUCAN save_treatment_plan: Visit UUID is the authoritative identifier for treatment plan saves. Passing a FHIR Encounter numeric ID to the TUCAN tool caused silent failures (reference: plans/visit-uuid-boundary).
  • practitionerId auto-population: If practitionerId is empty in the create DTO, it is auto-populated from user.fhirPractitionerId || user.id (visits.controller.ts:136). Warning is logged.
  • Response envelope: All responses go through ResponseTransformInterceptor{ data, meta }. API consumers must unwrap .data.
  • Rate limits: ThrottlerModule applies 10/1s burst, 100/60s medium, 1000/3600s hourly — large visit list calls can hit medium tier.
    Visits — Clinical Encounter Management — Dudoxx Docs | Dudoxx