Medico-Legal — Compliance and Legal Records
Audiences: clinical-buyer, developer
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:
POST /medico-legal/patients/:patientId/summary/export— patient-level summary.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/exportandPOST /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.urlis a MinIO signed URL (TTL 3600 seconds). - Internal (ops/support): Generated documents are stored as
AttachmentPrisma rows withtype: DOCUMENT(patient summary) ortype: MEDICAL_RECORD(encounter summary). Tags include['medico-legal', ...].StorageService.uploadFile()handles MinIO bucket routing. Signed URLs are generated viaStorageService.getSignedUrlForKey(bucket, path, 3600). If MinIO is unreachable, the upload step throws and the endpoint returns 500.
Architecture
Document rendering:
| Format | Renderer |
|---|---|
pdfkit (renderPdf() helper — wraps PDFDocument lifecycle) | |
| DOCX | docx library (Document/Paragraph/TextRun → Packer.toBuffer()) |
MinIO storage paths:
Attachment.type | Content | Tags |
|---|---|---|
DOCUMENT | patient summary | ['medico-legal', 'patient-summary'] |
MEDICAL_RECORD | encounter summary | ['medico-legal', 'encounter-summary', 'visit:{visitId}'] |
Export formats:
| Enum value | MIME type |
|---|---|
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
| Layer | Technology | Rationale |
|---|---|---|
| API | NestJS 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 generation | pdfkit (Node.js) | Embedded Node.js PDF library; no external service dependency; synchronous generation |
| DOCX generation | docx library (Document, Paragraph, TextRun, Packer.toBuffer()) | Pure Node.js DOCX generation; output is valid .docx |
| Storage | MinIO via StorageService.uploadFile() | Documents persisted as Attachment rows; binary in MinIO bucket |
| Signed URL | StorageService.getSignedUrlForKey(bucket, path, 3600) | 1-hour TTL; URL not stored in DB — caller must use it within the TTL |
| Patient data | PatientsService.getPatientSummary() | Reuses the medical card aggregation pipeline (FHIR parallel fetch) |
| Encounter data | VisitsService.findOne() + FHIR Encounter extensions | Clinical 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:
POST /api/v1/medico-legal/patients/{patientId}/summary/exportwith{ format: 'PDF' }and DOCTOR/ADMIN JWT.MedicoLegalController.exportPatientSummary()(medico-legal.controller.ts:47) delegates toMedicoLegalService.exportPatientSummary().PatientsService.getPatientSummary(userId, organizationId, patientId)(patients.service.ts:240) — parallel FHIR fetch: demographics, activeConditions, currentMedications, criticalAllergies, recentEncounters, upcomingAppointments.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.StorageService.uploadFile({ buffer, filename: 'patient-summary-{id}-{date}.pdf', type: DOCUMENT, tags: ['medico-legal', 'patient-summary'] }, context).prisma.attachment.findUnique({ id: upload.id, select: { storageBucket, storagePath } })— retrieves MinIO coordinates.StorageService.getSignedUrlForKey(storageBucket, storagePath, 3600)— 1-hour signed URL.- Returns
{ attachmentId: upload.id, url: signedUrl, mimeType: 'application/pdf', filename }.
Generate encounter summary document (DOCX)
POST /api/v1/medico-legal/visits/{visitId}/encounter-summary/exportwith{ format: 'DOCX' }.VisitsService.findOne(visitId, organizationId)→ FHIR Encounter resource.lastExtensionValueString(encounter.extension, 'http://dudoxx.com/fhir/StructureDefinition/clinical-notes')→ clinical notes text.- Same for
diagnosisandtreatment-planextensions. new Document({ sections: [{ children: [...paragraphs] }] })→Packer.toBuffer(doc).StorageService.uploadFile({ type: MEDICAL_RECORD, tags: ['medico-legal', 'encounter-summary', 'visit:{visitId}'] }, context).- Returns signed DOCX URL.
Implicated Code
ddx-api/src/clinical-data/medico-legal/medico-legal.controller.ts:28—MedicoLegalController— two endpoints: patient summary export + encounter summary export; DOCTOR/ADMIN roles onlyddx-api/src/clinical-data/medico-legal/medico-legal.controller.ts:37—exportPatientSummary()—POST /medico-legal/patients/:patientId/summary/export— builds access context from JWT userddx-api/src/clinical-data/medico-legal/medico-legal.controller.ts:79—exportEncounterSummary()—POST /medico-legal/visits/:visitId/encounter-summary/exportddx-api/src/clinical-data/medico-legal/medico-legal.service.ts:46—MedicoLegalService— injectsPatientsService,VisitsService,StorageService,PrismaServiceddx-api/src/clinical-data/medico-legal/medico-legal.service.ts:58—exportPatientSummary()— FHIR data aggregation → PDF/DOCX render → MinIO upload → signed URLddx-api/src/clinical-data/medico-legal/medico-legal.service.ts:258—exportEncounterSummary()— FHIR Encounter extension extraction → PDF/DOCX render → MinIO upload → signed URLddx-api/src/clinical-data/medico-legal/medico-legal.service.ts:6—lastExtensionValueString()— extracts the last matching FHIR extensionvalueStringby URL — used for clinical-notes, diagnosis, treatment-plan fieldsddx-api/src/clinical-data/medico-legal/medico-legal.service.ts:16—renderPdf()— pdfkit lifecycle wrapper: createsPDFDocument, pipes to buffer, calls builder callback, finalizes stream, resolves toBufferddx-api/src/clinical-data/medico-legal/medico-legal.dto.ts:1—MedicoLegalExportRequestDto(format: MedicoLegalExportFormat) +MedicoLegalExportResponseDto(attachmentId, url, mimeType, filename)ddx-api/src/clinical/patients/patients.service.ts:240—getPatientSummary()→PatientAggregationService(reuses medical card pipeline)ddx-api/src/clinical/visits/visits.service.ts:1—VisitsService.findOne()— returns FHIR Encounter resource with extensions
Operational Notes
- Signed URLs expire in 1 hour: The response
urlis 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 newAttachmentrow 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. PreferupdateExtension()inClinicalNotesServiceto 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.
Related Topics
- Patients — Registration and Demographics —
getPatientSummary()aggregation pipeline used for patient-level export - Clinical Visits —
VisitsService.findOne()provides FHIR Encounter data for encounter-level export - Clinical Notes — Visit Notes with Audio Transcription — FHIR Encounter extensions (clinical-notes, diagnosis, treatment-plan) populated by ClinicalNotesService
- Medical Cards — Patient Health Summary —
PatientSummaryResponseDtois the data model for patient summary PDF content - HAPI FHIR R4 Server — FHIR Patient + Encounter reads underpin both export types