04-clinicalwave: W2filled13 citations

Medico-Legal — Compliance and Legal Records

Audiences: clinical-buyer, developer

Medico-Legal — Compliance and Legal Records

The Medico-Legal module generates official clinical summary documents (PDF or DOCX) for both patient-level and encounter-level export, stores them in MinIO via StorageService, and returns a 1-hour signed URL — enabling compliant patient record sharing with insurers, referral networks, and legal bodies.

Business Purpose

Clinics are legally required in Germany (and across the EU) to provide patients and authorized parties with a complete medical record upon request. This includes:

  • Patient summary documents: Demographics, active diagnoses, current medications, upcoming appointments — formatted as a signed, dated official document.
  • Encounter summary documents: A specific visit's clinical notes, diagnosis, and treatment plan — required for insurance reimbursement submissions and specialist referrals.

Without automated generation, this process involves manually assembling data from multiple screens, exporting PDFs, and emailing them — a process that takes 15–30 minutes per request and introduces human error.

Dudoxx HMS automates both document types through a two-endpoint API:

  1. POST /medico-legal/patients/:patientId/summary/export — patient-level summary.
  2. POST /medico-legal/visits/:visitId/encounter-summary/export — per-encounter summary.

Both endpoints aggregate live clinical data, render it to PDF (via pdfkit) or DOCX (via docx library), upload to MinIO, and return a signed URL valid for 1 hour.

Audiences

  • Investor: Automated medico-legal document generation demonstrates regulatory compliance out of the box. In the DACH healthcare market, the ability to generate DSGVO-compliant patient records on demand is a hard requirement for clinic certification. It also reduces staff time spent on record requests — a real operational cost.
  • Clinical buyer (doctor/nurse/receptionist): Clinic admins and doctors can trigger a patient summary or encounter summary from the patient chart. The generated document is stored in MinIO and accessible via a signed URL for 1 hour. Documents include the generator's name (email) and generation timestamp, creating an audit trail.
  • Developer/partner: POST /api/v1/medico-legal/patients/:patientId/summary/export and POST /api/v1/medico-legal/visits/:visitId/encounter-summary/export. Body: { format: 'PDF' | 'DOCX' }. Response: { attachmentId, url, mimeType, filename }. Requires SUPER_ADMIN, ORG_ADMIN, CLINIC_ADMIN, or DOCTOR role. url is a MinIO signed URL (TTL 3600 seconds).
  • Internal (ops/support): Generated documents are stored as Attachment Prisma rows with type: DOCUMENT (patient summary) or type: MEDICAL_RECORD (encounter summary). Tags include ['medico-legal', ...]. StorageService.uploadFile() handles MinIO bucket routing. Signed URLs are generated via StorageService.getSignedUrlForKey(bucket, path, 3600). If MinIO is unreachable, the upload step throws and the endpoint returns 500.

Architecture

diagram
MedicoLegalController  (/api/v1/medico-legal)
  └── MedicoLegalService
        ├── PatientsService                   ← getPatientSummary()
        ├── VisitsService                     ← findOne() for encounter data
        ├── StorageService                    ← MinIO upload + signed URL
        └── PrismaService                     ← Attachment row lookup post-upload

Document rendering:
  PDF   → pdfkit (renderPdf() helper — wraps PDFDocument lifecycle)
  DOCX  → docx library (Document/Paragraph/TextRun → Packer.toBuffer())

MinIO storage paths:
  Attachment.type = DOCUMENT      ← patient summary
  Attachment.type = MEDICAL_RECORD ← encounter summary
  Tags: ['medico-legal', 'patient-summary'] | ['medico-legal', 'encounter-summary', 'visit:{visitId}']

Export formats:
  MedicoLegalExportFormat.PDF  → application/pdf
  MedicoLegalExportFormat.DOCX → application/vnd.openxmlformats-officedocument.wordprocessingml.document

The renderPdf(callback) helper (medico-legal.service.ts) encapsulates the pdfkit PDFDocument lifecycle: create → pipe to buffer → call callback with the doc instance → finalize → return Buffer. This pattern ensures the PDF stream is always properly terminated.

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

Tech Stack & Choices

LayerTechnologyRationale
APINestJS 11, @Controller('medico-legal')Two export endpoints; returns DTO with signed URL (not streaming)
Auth@RequireRole(SUPER_ADMIN, ORG_ADMIN, CLINIC_ADMIN, DOCTOR)Only senior clinical/admin roles can generate official documents
PDF generationpdfkit (Node.js)Embedded Node.js PDF library; no external service dependency; synchronous generation
DOCX generationdocx library (Document, Paragraph, TextRun, Packer.toBuffer())Pure Node.js DOCX generation; output is valid .docx
StorageMinIO via StorageService.uploadFile()Documents persisted as Attachment rows; binary in MinIO bucket
Signed URLStorageService.getSignedUrlForKey(bucket, path, 3600)1-hour TTL; URL not stored in DB — caller must use it within the TTL
Patient dataPatientsService.getPatientSummary()Reuses the medical card aggregation pipeline (FHIR parallel fetch)
Encounter dataVisitsService.findOne() + FHIR Encounter extensionsClinical notes, diagnosis, treatment plan extracted from extension[] via lastExtensionValueString()

Key design decision: Clinical content for encounter summaries is read from FHIR Encounter extensions (http://dudoxx.com/fhir/StructureDefinition/clinical-notes, diagnosis, treatment-plan) using lastExtensionValueString() (medico-legal.service.ts). This mirrors how ClinicalNotesService stores SOAP fields — in FHIR extensions for programmatic access alongside the base64 HTML content attachment for display.

Data Flow

Generate patient summary document (PDF)

Business outcome: A clinic admin requests a patient's medical record for an insurance submission; within 5 seconds, a signed PDF URL is returned containing demographics, conditions, medications, and upcoming appointments.

Technical mechanism:

  1. POST /api/v1/medico-legal/patients/{patientId}/summary/export with { format: 'PDF' } and DOCTOR/ADMIN JWT.
  2. MedicoLegalController.exportPatientSummary() (medico-legal.controller.ts:47) delegates to MedicoLegalService.exportPatientSummary().
  3. PatientsService.getPatientSummary(userId, organizationId, patientId) (patients.service.ts:240) — parallel FHIR fetch: demographics, activeConditions, currentMedications, criticalAllergies, recentEncounters, upcomingAppointments.
  4. renderPdf(doc => { ... }) builds a structured PDF: Demographics → Active Conditions → Current Medications → Upcoming Appointments. Each section lists its items numerically. Generator email + ISO timestamp on the cover.
  5. StorageService.uploadFile({ buffer, filename: 'patient-summary-{id}-{date}.pdf', type: DOCUMENT, tags: ['medico-legal', 'patient-summary'] }, context).
  6. prisma.attachment.findUnique({ id: upload.id, select: { storageBucket, storagePath } }) — retrieves MinIO coordinates.
  7. StorageService.getSignedUrlForKey(storageBucket, storagePath, 3600) — 1-hour signed URL.
  8. Returns { attachmentId: upload.id, url: signedUrl, mimeType: 'application/pdf', filename }.

Generate encounter summary document (DOCX)

  1. POST /api/v1/medico-legal/visits/{visitId}/encounter-summary/export with { format: 'DOCX' }.
  2. VisitsService.findOne(visitId, organizationId) → FHIR Encounter resource.
  3. lastExtensionValueString(encounter.extension, 'http://dudoxx.com/fhir/StructureDefinition/clinical-notes') → clinical notes text.
  4. Same for diagnosis and treatment-plan extensions.
  5. new Document({ sections: [{ children: [...paragraphs] }] })Packer.toBuffer(doc).
  6. StorageService.uploadFile({ type: MEDICAL_RECORD, tags: ['medico-legal', 'encounter-summary', 'visit:{visitId}'] }, context).
  7. Returns signed DOCX URL.

Implicated Code

  • ddx-api/src/clinical-data/medico-legal/medico-legal.controller.ts:28MedicoLegalController — two endpoints: patient summary export + encounter summary export; DOCTOR/ADMIN roles only
  • ddx-api/src/clinical-data/medico-legal/medico-legal.controller.ts:37exportPatientSummary()POST /medico-legal/patients/:patientId/summary/export — builds access context from JWT user
  • ddx-api/src/clinical-data/medico-legal/medico-legal.controller.ts:79exportEncounterSummary()POST /medico-legal/visits/:visitId/encounter-summary/export
  • ddx-api/src/clinical-data/medico-legal/medico-legal.service.ts:46MedicoLegalService — injects PatientsService, VisitsService, StorageService, PrismaService
  • ddx-api/src/clinical-data/medico-legal/medico-legal.service.ts:58exportPatientSummary() — FHIR data aggregation → PDF/DOCX render → MinIO upload → signed URL
  • ddx-api/src/clinical-data/medico-legal/medico-legal.service.ts:258exportEncounterSummary() — FHIR Encounter extension extraction → PDF/DOCX render → MinIO upload → signed URL
  • ddx-api/src/clinical-data/medico-legal/medico-legal.service.ts:6lastExtensionValueString() — extracts the last matching FHIR extension valueString by URL — used for clinical-notes, diagnosis, treatment-plan fields
  • ddx-api/src/clinical-data/medico-legal/medico-legal.service.ts:16renderPdf() — pdfkit lifecycle wrapper: creates PDFDocument, pipes to buffer, calls builder callback, finalizes stream, resolves to Buffer
  • ddx-api/src/clinical-data/medico-legal/medico-legal.dto.ts:1MedicoLegalExportRequestDto (format: MedicoLegalExportFormat) + MedicoLegalExportResponseDto (attachmentId, url, mimeType, filename)
  • ddx-api/src/clinical/patients/patients.service.ts:240getPatientSummary()PatientAggregationService (reuses medical card pipeline)
  • ddx-api/src/clinical/visits/visits.service.ts:1VisitsService.findOne() — returns FHIR Encounter resource with extensions

Operational Notes

  • Signed URLs expire in 1 hour: The response url is a MinIO pre-signed URL. Callers must trigger the download within 3600 seconds. The URL is NOT stored anywhere in Prisma — if it expires before use, the caller must call the export endpoint again (which creates a new Attachment row and new signed URL).
  • MinIO dependency is synchronous: If MinIO is unreachable when StorageService.uploadFile() is called, the export fails with a 500. The generated PDF/DOCX buffer is lost. There is no retry or queue. Monitor MinIO availability before triggering large-volume exports.
  • pdfkit content escaping: The renderPdf() callback uses direct pdfkit .text() calls. If patient data contains characters that confuse pdfkit's layout engine (e.g., RTL text, unusual Unicode), the PDF may render incorrectly. HTML-special characters are NOT escaped (pdfkit handles them natively via its text engine).
  • DOCX generation is synchronous and memory-bound: Packer.toBuffer() holds the entire DOCX in memory before uploading. For patients with very long clinical histories, this could be large. Monitor heap usage if generating DOCX for patients with thousands of visits.
  • Extension field extraction uses lastExtensionValueString(): This function returns the value of the LAST matching extension URL. If FHIR Encounter extensions include duplicates (e.g., from a buggy update that appended instead of replaced), the last value wins — which may be older than the correct one. Prefer updateExtension() in ClinicalNotesService to ensure single-entry extensions.
  • DOCTOR role scoping: The controller does NOT enforce that a DOCTOR can only export summaries for their own patients — any DOCTOR can export any patient's summary. For fine-grained access control, a patient-ownership check would need to be added.
    Medico-Legal — Compliance and Legal Records — Dudoxx Docs | Dudoxx