$patient * @param array $encounter * @param array> $medications * @param array> $conditions * * @return array */ public function buildPrescriptionBundle(array $patient, array $encounter, array $medications, array $conditions = [], array $context = []): array { $issuedAt = $this->isoTimestamp(); $practitioner = is_array($context['practitioner'] ?? null) ? (array) $context['practitioner'] : []; $organization = is_array($context['organization'] ?? null) ? (array) $context['organization'] : []; $observations = is_array($context['observations'] ?? null) ? (array) $context['observations'] : []; $allergies = is_array($context['allergies'] ?? null) ? (array) $context['allergies'] : []; $complaints = is_array($context['complaints'] ?? null) ? (array) $context['complaints'] : []; $serviceRequests = is_array($context['service_requests'] ?? null) ? (array) $context['service_requests'] : []; $appointments = is_array($context['appointments'] ?? null) ? (array) $context['appointments'] : []; // UUID-based identity for every resource (ABDM IG v6.5.0 requirement) $bundleUuid = $this->generateUuid(); $compositionUuid = $this->generateUuid(); $patientUuid = $this->generateUuid(); $encounterUuid = $this->generateUuid(); $hasPractitioner = trim((string) ($practitioner['id'] ?? '')) !== '' || trim((string) ($practitioner['name'] ?? '')) !== ''; $hasOrganization = trim((string) ($organization['id'] ?? '')) !== '' || trim((string) ($organization['name'] ?? '')) !== ''; $practitionerUuid = $hasPractitioner ? $this->generateUuid() : ''; $organizationUuid = $hasOrganization ? $this->generateUuid() : ''; $patientRef = 'urn:uuid:' . $patientUuid; $encounterRef = 'urn:uuid:' . $encounterUuid; $practitionerRef = $hasPractitioner ? ('urn:uuid:' . $practitionerUuid) : ''; $organizationRef = $hasOrganization ? ('urn:uuid:' . $organizationUuid) : ''; // ── Tracking arrays ────────────────────────────────────────────────── $encounterDiagnosisRefs = []; $complaintRefs = []; $observationRefs = []; $allergyRefs = []; $medicationRefs = []; $serviceRequestRefs = []; $appointmentRefs = []; // Resource entries collected here; Composition prepended at the end. $resourceEntries = []; // ── Practitioner ───────────────────────────────────────────────────── if ($hasPractitioner) { $practRawName = trim((string) ($practitioner['name'] ?? '')); $practNameEntry = ['text' => $practRawName]; // Split "Dr. Ramesh Sharma" → prefix=["Dr."], given=["Ramesh"], family="Sharma" $practParts = preg_split('/\s+/', $practRawName, -1, PREG_SPLIT_NO_EMPTY) ?: []; if (count($practParts) >= 2) { if (preg_match('/^(Dr\.?|Prof\.?|Mr\.?|Mrs\.?|Ms\.?)$/i', $practParts[0])) { $practNameEntry['prefix'] = [array_shift($practParts)]; } if (count($practParts) >= 1) { $practNameEntry['family'] = (string) array_pop($practParts); } if (! empty($practParts)) { $practNameEntry['given'] = $practParts; } } $practResource = [ 'resourceType' => 'Practitioner', 'id' => $practitionerUuid, 'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/Practitioner']], 'name' => [$practNameEntry], ]; $regNumber = trim((string) ($practitioner['registration_number'] ?? '')); if ($regNumber !== '') { $practResource['identifier'] = [[ 'type' => ['coding' => [[ 'system' => 'http://terminology.hl7.org/CodeSystem/v2-0203', 'code' => 'MD', 'display' => 'Medical License number', ]]], 'system' => 'https://doctor.ndhm.gov.in', 'value' => $regNumber, ]]; } $resourceEntries[] = ['fullUrl' => 'urn:uuid:' . $practitionerUuid, 'resource' => $practResource]; } // ── Organization ───────────────────────────────────────────────────── if ($hasOrganization) { $orgResource = [ 'resourceType' => 'Organization', 'id' => $organizationUuid, 'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/Organization']], 'name' => trim((string) ($organization['name'] ?? '')), ]; $hfrId = trim((string) ($organization['hfr_id'] ?? '')); if ($hfrId !== '') { $orgResource['identifier'] = [[ 'type' => ['coding' => [[ 'system' => 'http://terminology.hl7.org/CodeSystem/v2-0203', 'code' => 'PRN', 'display' => 'Provider number', ]]], 'system' => 'https://facility.ndhm.gov.in', 'value' => $hfrId, ]]; } $resourceEntries[] = ['fullUrl' => 'urn:uuid:' . $organizationUuid, 'resource' => $orgResource]; } // ── Patient ─────────────────────────────────────────────────────────── $patientResource = $this->buildPatientResource($patient); $patientResource['id'] = $patientUuid; // override DB id with UUID $resourceEntries[] = ['fullUrl' => $patientRef, 'resource' => $patientResource]; // ── Encounter (built here, appended after conditions loop) ────────── $encounterResource = [ 'resourceType' => 'Encounter', 'id' => $encounterUuid, 'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/Encounter']], 'identifier' => [['system' => 'https://ndhm.in', 'value' => (string) ($encounter['id'] ?? $encounterUuid)]], 'status' => (string) ($encounter['status'] ?? 'finished'), 'class' => [ 'system' => 'http://terminology.hl7.org/CodeSystem/v3-ActCode', 'code' => 'AMB', 'display' => 'Ambulatory', ], 'subject' => ['reference' => $patientRef, 'display' => 'Patient'], 'period' => ['start' => trim((string) ($encounter['period_start'] ?? '')) !== '' ? (string) $encounter['period_start'] : $issuedAt], ]; if ($practitionerRef !== '') { $encounterResource['participant'] = [['individual' => ['reference' => $practitionerRef]]]; } if ($organizationRef !== '') { $encounterResource['serviceProvider'] = ['reference' => $organizationRef]; } // ── Conditions (diagnoses) ──────────────────────────────────────────── foreach ($conditions as $index => $condition) { $text = trim((string) ($condition['text'] ?? '')); if ($text === '') { continue; } $condUuid = $this->generateUuid(); $condRef = 'urn:uuid:' . $condUuid; $verification = trim((string) ($condition['verification_status'] ?? 'provisional')); $verificationCode = strtolower($verification) === 'confirmed' ? 'confirmed' : 'provisional'; $useSnomedCode = $verificationCode === 'confirmed' ? '39154008' : '148006'; $useSnomedDisplay = $verificationCode === 'confirmed' ? 'Clinical diagnosis' : 'Preliminary diagnosis'; $encounterDiagnosisRefs[] = [ 'condition' => ['reference' => $condRef, 'display' => 'Condition'], 'use' => ['coding' => [[ 'system' => 'http://snomed.info/sct', 'code' => $useSnomedCode, 'display' => $useSnomedDisplay, ]]], ]; $code = ['text' => $text]; $snomedCode = trim((string) ($condition['snomed_code'] ?? '')); if ($snomedCode !== '') { $code['coding'] = [[ 'system' => 'http://snomed.info/sct', 'code' => $snomedCode, 'display' => trim((string) ($condition['snomed_display'] ?? $text)), ]]; } $resourceEntries[] = ['fullUrl' => $condRef, 'resource' => [ 'resourceType' => 'Condition', 'id' => $condUuid, 'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/Condition']], 'clinicalStatus' => ['coding' => [[ 'system' => 'http://terminology.hl7.org/CodeSystem/condition-clinical', 'code' => 'active', 'display' => 'Active', ]]], 'verificationStatus' => ['coding' => [[ 'system' => 'http://terminology.hl7.org/CodeSystem/condition-ver-status', 'code' => $verificationCode, 'display' => ucfirst($verificationCode), ]]], 'code' => $code, 'subject' => ['reference' => $patientRef, 'display' => 'Patient'], ]]; } // Append Encounter (now has diagnosis refs) if (! empty($encounterDiagnosisRefs)) { $encounterResource['diagnosis'] = $encounterDiagnosisRefs; } $resourceEntries[] = ['fullUrl' => $encounterRef, 'resource' => $encounterResource]; // ── Complaints (Chief Complaints) ───────────────────────────────── foreach ($complaints as $index => $complaint) { $text = trim((string) ($complaint['text'] ?? '')); if ($text === '') { continue; } $complaintUuid = $this->generateUuid(); $complaintRef = 'urn:uuid:' . $complaintUuid; $complaintRefs[] = ['reference' => $complaintRef, 'display' => 'Condition']; $code = ['text' => $text]; $snomedCode = trim((string) ($complaint['snomed_code'] ?? '')); if ($snomedCode !== '') { $code['coding'] = [[ 'system' => 'http://snomed.info/sct', 'code' => $snomedCode, 'display' => trim((string) ($complaint['snomed_display'] ?? $text)), ]]; } $complaintResource = [ 'resourceType' => 'Condition', 'id' => $complaintUuid, 'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/Condition']], 'clinicalStatus' => ['coding' => [[ 'system' => 'http://terminology.hl7.org/CodeSystem/condition-clinical', 'code' => 'active', 'display' => 'Active', ]]], 'code' => $code, 'subject' => ['reference' => $patientRef, 'display' => 'Patient'], ]; // Severity (High/Moderate/Low → SNOMED coded) $severityText = trim((string) ($complaint['severity'] ?? '')); if ($severityText !== '') { $severityMap = [ 'high' => ['24484000', 'Severe'], 'severe' => ['24484000', 'Severe'], 'moderate' => ['6736007', 'Moderate'], 'mild' => ['255604002', 'Mild'], 'low' => ['255604002', 'Mild'], ]; $severityKey = strtolower($severityText); [$sevCode, $sevDisplay] = $severityMap[$severityKey] ?? ['', $severityText]; $severityCoding = ['text' => ucfirst($severityText)]; if ($sevCode !== '') { $severityCoding['coding'] = [[ 'system' => 'http://snomed.info/sct', 'code' => $sevCode, 'display' => $sevDisplay, ]]; } $complaintResource['severity'] = $severityCoding; } // Duration → note $durationText = trim((string) ($complaint['duration'] ?? '')); $frequencyText = trim((string) ($complaint['frequency'] ?? '')); $noteParts = array_filter([$durationText, $frequencyText]); if (! empty($noteParts)) { $complaintResource['note'] = [['text' => implode(' | ', $noteParts)]]; } $resourceEntries[] = ['fullUrl' => $complaintRef, 'resource' => $complaintResource]; } // ── Observations (Vitals / Physical Examination) ────────────────────── foreach ($observations as $index => $observation) { $value = $observation['value'] ?? null; if (! is_numeric($value)) { continue; } $obsUuid = $this->generateUuid(); $observationRefs[] = ['reference' => 'urn:uuid:' . $obsUuid, 'display' => 'Observation']; $resourceEntries[] = ['fullUrl' => 'urn:uuid:' . $obsUuid, 'resource' => [ 'resourceType' => 'Observation', 'id' => $obsUuid, 'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/ObservationVitalSigns']], 'status' => 'final', 'category' => [['coding' => [[ 'system' => 'http://terminology.hl7.org/CodeSystem/observation-category', 'code' => 'vital-signs', ]]]], 'code' => [ 'coding' => [[ 'system' => 'http://loinc.org', 'code' => (string) ($observation['loinc'] ?? ''), 'display' => (string) ($observation['display'] ?? ''), ]], 'text' => (string) ($observation['display'] ?? ''), ], 'subject' => ['reference' => $patientRef, 'display' => 'Patient'], 'encounter' => ['reference' => $encounterRef], 'effectiveDateTime' => $issuedAt, 'valueQuantity' => [ 'value' => (float) $value, 'unit' => (string) ($observation['unit'] ?? ''), 'system' => 'http://unitsofmeasure.org', 'code' => (string) ($observation['ucum'] ?? ''), ], ]]; } foreach ($allergies as $index => $allergy) { $codeText = trim((string) ($allergy['code_text'] ?? '')); if ($codeText === '') { continue; } $allergyUuid = $this->generateUuid(); $allergyRefs[] = ['reference' => 'urn:uuid:' . $allergyUuid, 'display' => 'AllergyIntolerance']; $allergyResource = [ 'resourceType' => 'AllergyIntolerance', 'id' => $allergyUuid, 'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/AllergyIntolerance']], 'clinicalStatus' => ['coding' => [[ 'system' => 'http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical', 'code' => (string) ($allergy['clinical_status'] ?? 'active'), ]]], 'verificationStatus' => ['coding' => [[ 'system' => 'http://terminology.hl7.org/CodeSystem/allergyintolerance-verification', 'code' => (string) ($allergy['verification_status'] ?? 'confirmed'), ]]], 'code' => ['text' => $codeText], 'patient' => ['reference' => $patientRef, 'display' => 'Patient'], 'recordedDate' => $issuedAt, ]; $reaction = trim((string) ($allergy['reaction_text'] ?? '')); if ($reaction !== '') { $allergyResource['reaction'] = [['description' => $reaction]]; } $resourceEntries[] = ['fullUrl' => 'urn:uuid:' . $allergyUuid, 'resource' => $allergyResource]; } // ── Medications ─────────────────────────────────────────────────────── foreach ($medications as $index => $medication) { $medUuid = $this->generateUuid(); $medicationRefs[] = ['reference' => 'urn:uuid:' . $medUuid, 'display' => 'MedicationRequest']; $drugName = trim((string) ($medication['drug_name'] ?? '')); $genericName = trim((string) ($medication['generic_name'] ?? '')); $medType = trim((string) ($medication['med_type'] ?? '')); $displayText = $drugName; if ($genericName !== '' && stripos($drugName, $genericName) === false) { $displayText = trim($drugName . ' (' . $genericName . ')'); } $medicationCodeableConcept = ['text' => $displayText !== '' ? $displayText : $drugName]; $snomedCode = trim((string) ($medication['snomed_code'] ?? '')); $atcCode = strtoupper(trim((string) ($medication['atc_code'] ?? ''))); if ($snomedCode !== '') { $medicationCodeableConcept['coding'] = [[ 'system' => 'http://snomed.info/sct', 'code' => $snomedCode, 'display' => $displayText !== '' ? $displayText : $drugName, ]]; } elseif ($atcCode !== '') { $medicationCodeableConcept['coding'] = [[ 'system' => 'http://www.whocc.no/atc', 'code' => $atcCode, 'display' => $displayText !== '' ? $displayText : $drugName, ]]; } $dosageInstruction = ['text' => (string) ($medication['dosage'] ?? '')]; $routeText = trim((string) ($medication['route_text'] ?? '')); if ($routeText !== '') { $routeEntry = ['text' => $routeText]; // Add SNOMED route code when we can resolve it from text or a pre-supplied code $routeCode = trim((string) ($medication['route_code'] ?? '')); if ($routeCode === '') { $routeCode = $this->resolveRouteSnomedCode($routeText); } if ($routeCode !== '') { $routeEntry['coding'] = [[ 'system' => 'http://snomed.info/sct', 'code' => $routeCode, 'display' => $routeText, ]]; } $dosageInstruction['route'] = $routeEntry; } $methodText = trim((string) ($medication['method_text'] ?? $medType)); $methodCode = trim((string) ($medication['method_code'] ?? '')); if ($methodText !== '') { $methodEntry = ['text' => $methodText]; if ($methodCode !== '') { $methodEntry['coding'] = [[ 'system' => 'http://snomed.info/sct', 'code' => $methodCode, 'display' => $methodText, ]]; } $dosageInstruction['method'] = $methodEntry; } $medRes = [ 'resourceType' => 'MedicationRequest', 'id' => $medUuid, 'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/MedicationRequest']], 'status' => (string) ($medication['status'] ?? 'active'), 'intent' => 'order', 'medicationCodeableConcept' => $medicationCodeableConcept, 'subject' => ['reference' => $patientRef, 'display' => 'Patient'], 'encounter' => ['reference' => $encounterRef], 'authoredOn' => $issuedAt, 'dosageInstruction' => [$dosageInstruction], ]; if ($practitionerRef !== '') { $medRes['requester'] = ['reference' => $practitionerRef]; } $resourceEntries[] = ['fullUrl' => 'urn:uuid:' . $medUuid, 'resource' => $medRes]; } // ── ServiceRequests (Investigation Advice) ──────────────────────────── foreach ($serviceRequests as $index => $serviceRequest) { $codeText = trim((string) ($serviceRequest['code_text'] ?? '')); if ($codeText === '') { continue; } $svcUuid = $this->generateUuid(); $serviceRequestRefs[] = ['reference' => 'urn:uuid:' . $svcUuid, 'display' => 'ServiceRequest']; $svcRes = [ 'resourceType' => 'ServiceRequest', 'id' => $svcUuid, 'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/ServiceRequest']], 'status' => (string) ($serviceRequest['status'] ?? 'active'), 'intent' => (string) ($serviceRequest['intent'] ?? 'order'), 'code' => ['text' => $codeText], 'subject' => ['reference' => $patientRef, 'display' => 'Patient'], 'encounter' => ['reference' => $encounterRef], 'authoredOn' => $issuedAt, ]; if ($practitionerRef !== '') { $svcRes['requester'] = ['reference' => $practitionerRef]; } $resourceEntries[] = ['fullUrl' => 'urn:uuid:' . $svcUuid, 'resource' => $svcRes]; } // ── Appointments (Follow Up) ────────────────────────────────────────── foreach ($appointments as $index => $appointment) { $description = trim((string) ($appointment['description'] ?? '')); if ($description === '') { continue; } $apptUuid = $this->generateUuid(); $appointmentRefs[] = ['reference' => 'urn:uuid:' . $apptUuid, 'display' => 'Appointment']; $apptRes = [ 'resourceType' => 'Appointment', 'id' => $apptUuid, 'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/Appointment']], 'status' => (string) ($appointment['status'] ?? 'proposed'), 'description' => $description, 'participant' => [[ 'actor' => ['reference' => $patientRef, 'display' => 'Patient'], 'status' => 'accepted', ]], ]; if ($practitionerRef !== '') { $apptRes['participant'][] = [ 'actor' => ['reference' => $practitionerRef], 'status' => 'accepted', ]; } $resourceEntries[] = ['fullUrl' => 'urn:uuid:' . $apptUuid, 'resource' => $apptRes]; } // ── Composition sections (ABDM SNOMED section codes) ───────────────── $compositionSections = []; if (! empty($complaintRefs)) { $compositionSections[] = [ 'title' => 'Chief complaints', 'code' => ['coding' => [[ 'system' => 'http://snomed.info/sct', 'code' => '422843007', 'display' => 'Chief complaint section', ]]], 'entry' => $complaintRefs, ]; } if (! empty($allergyRefs)) { $compositionSections[] = [ 'title' => 'Allergies', 'code' => ['coding' => [[ 'system' => 'http://snomed.info/sct', 'code' => '722446000', 'display' => 'Allergy record', ]]], 'entry' => $allergyRefs, ]; } if (! empty($observationRefs)) { $compositionSections[] = [ 'title' => 'Physical Examination', 'code' => ['coding' => [[ 'system' => 'http://snomed.info/sct', 'code' => '425044008', 'display' => 'Physical exam section', ]]], 'entry' => $observationRefs, ]; } if (! empty($encounterDiagnosisRefs)) { // Diagnoses section — conditions referenced from Encounter.diagnosis $diagSectionEntries = array_map(static fn ($d) => $d['condition'], $encounterDiagnosisRefs); $compositionSections[] = [ 'title' => 'Problems and Diagnoses', 'code' => ['coding' => [[ 'system' => 'http://snomed.info/sct', 'code' => '439401001', 'display' => 'Diagnosis', ]]], 'entry' => $diagSectionEntries, ]; } if (! empty($medicationRefs)) { $compositionSections[] = [ 'title' => 'Medications', 'code' => ['coding' => [[ 'system' => 'http://snomed.info/sct', 'code' => '721912009', 'display' => 'Medication summary document', ]]], 'entry' => $medicationRefs, ]; } if (! empty($serviceRequestRefs)) { $compositionSections[] = [ 'title' => 'Investigation Advice', 'code' => ['coding' => [[ 'system' => 'http://snomed.info/sct', 'code' => '721963009', 'display' => 'Order document', ]]], 'entry' => $serviceRequestRefs, ]; } if (! empty($appointmentRefs)) { $compositionSections[] = [ 'title' => 'Follow Up', 'code' => ['coding' => [[ 'system' => 'http://snomed.info/sct', 'code' => '736271009', 'display' => 'Outpatient care plan', ]]], 'entry' => $appointmentRefs, ]; } // ── Composition (first entry per ABDM spec) ─────────────────────────── $composition = [ 'resourceType' => 'Composition', 'id' => $compositionUuid, 'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/OPConsultRecord']], 'language' => 'en-IN', 'identifier' => ['system' => 'https://ndhm.in/phr', 'value' => $compositionUuid], 'status' => 'final', 'type' => [ 'coding' => [[ 'system' => 'http://snomed.info/sct', 'code' => '371530004', 'display' => 'Clinical consultation report', ]], 'text' => 'Clinical Consultation report', ], 'subject' => ['reference' => $patientRef, 'display' => 'Patient'], 'encounter' => ['reference' => $encounterRef, 'display' => 'Encounter'], 'date' => $issuedAt, 'author' => $practitionerRef !== '' ? [['reference' => $practitionerRef, 'display' => 'Practitioner']] : [['display' => 'Unknown']], 'title' => 'Consultation Report', 'section' => $compositionSections, ]; if ($organizationRef !== '') { $composition['custodian'] = ['reference' => $organizationRef, 'display' => 'Organization']; } // Composition first, then all resource entries $allEntries = array_merge( [['fullUrl' => 'urn:uuid:' . $compositionUuid, 'resource' => $composition]], $resourceEntries ); // ── Bundle ──────────────────────────────────────────────────────────── $hfrId = trim((string) ($organization['hfr_id'] ?? '')); $bundleIdSystem = $hfrId !== '' ? 'https://' . strtolower(preg_replace('/[^A-Za-z0-9]/', '', $hfrId)) . '.hfr.abdm.gov.in' : 'https://hfr.abdm.gov.in'; $bundleIdValue = $hfrId !== '' ? 'OPD-' . ($encounter['id'] ?? $bundleUuid) . '-' . date('Y-m-d') : $bundleUuid; return [ 'resourceType' => 'Bundle', 'id' => $bundleUuid, 'meta' => [ 'profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/DocumentBundle'], 'security' => [[ 'system' => 'http://terminology.hl7.org/CodeSystem/v3-Confidentiality', 'code' => 'V', 'display' => 'very restricted', ]], ], 'identifier' => ['system' => $bundleIdSystem, 'value' => $bundleIdValue], 'type' => 'document', 'timestamp' => $issuedAt, 'entry' => $allEntries, ]; } /** * Build a ABDM-compliant DiagnosticReportRecord FHIR DocumentBundle. * * Supports: * - LOINC-coded observations (valueQuantity) for structured lab results * - HTML report as presentedForm via Binary + DocumentReference * - Optional Practitioner, Organization, Encounter * * @param array $patient {id, name, given_name, family_name, gender, birthDate, abhaAddress, phone} * @param array $diagnosticReport {id, title, category_snomed_code, category_snomed_display, * loinc_code, loinc_display, status, conclusion, * reported_at, report_html, report_content_type} * @param array> $observations Each: {test_name, loinc_code, value, value_type (quantity|string), * unit, ucum_code, ref_low, ref_high, interpretation, status} * @param array|null $practitioner {name, registration_number} * @param array|null $organization {name, hfr_id} * @param array|null $encounter {id, status, class_code (AMB|IMP|EMER), period_start, period_end} * @param array|null $attachment Optional PDF attachment {content_type, data_base64, title} * * @return array */ public function buildLabReportBundle( array $patient, array $diagnosticReport, array $observations = [], ?array $practitioner = null, ?array $organization = null, ?array $encounter = null, ?array $attachment = null ): array { $issuedAt = $this->isoTimestamp(); $bundleUuid = $this->generateUuid(); $compositionUuid = $this->generateUuid(); $patientUuid = $this->generateUuid(); $reportUuid = $this->generateUuid(); $patientRef = 'urn:uuid:' . $patientUuid; $reportRef = 'urn:uuid:' . $reportUuid; $reportedAt = trim((string) ($diagnosticReport['reported_at'] ?? '')) ?: $issuedAt; $reportTitle = trim((string) ($diagnosticReport['title'] ?? 'Diagnostic Report')); $reportStatus = trim((string) ($diagnosticReport['status'] ?? 'final')); $isImaging = (bool) ($diagnosticReport['is_imaging'] ?? false) || strtolower(trim((string) ($diagnosticReport['report_domain'] ?? ''))) === 'imaging'; $hasPractitioner = $practitioner !== null && trim((string) ($practitioner['name'] ?? '')) !== ''; $hasOrganization = $organization !== null && trim((string) ($organization['name'] ?? '')) !== ''; $hasEncounter = $encounter !== null; $practitionerUuid = $hasPractitioner ? $this->generateUuid() : ''; $organizationUuid = $hasOrganization ? $this->generateUuid() : ''; $encounterUuid = $hasEncounter ? $this->generateUuid() : ''; $practitionerRef = $hasPractitioner ? ('urn:uuid:' . $practitionerUuid) : ''; $organizationRef = $hasOrganization ? ('urn:uuid:' . $organizationUuid) : ''; $encounterRef = $hasEncounter ? ('urn:uuid:' . $encounterUuid) : ''; $resourceEntries = []; // ── Practitioner ────────────────────────────────────────────────────── if ($hasPractitioner) { $practRes = [ 'resourceType' => 'Practitioner', 'id' => $practitionerUuid, 'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/Practitioner']], 'name' => [['text' => trim((string) ($practitioner['name'] ?? ''))]], ]; $regNo = trim((string) ($practitioner['registration_number'] ?? '')); if ($regNo !== '') { $practRes['identifier'] = [[ 'type' => ['coding' => [['system' => 'http://terminology.hl7.org/CodeSystem/v2-0203', 'code' => 'MD', 'display' => 'Medical License number']]], 'system' => 'https://doctor.ndhm.gov.in', 'value' => $regNo, ]]; } $resourceEntries[] = ['fullUrl' => $practitionerRef, 'resource' => $practRes]; } // ── Organization ────────────────────────────────────────────────────── if ($hasOrganization) { $orgRes = [ 'resourceType' => 'Organization', 'id' => $organizationUuid, 'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/Organization']], 'name' => trim((string) ($organization['name'] ?? '')), ]; $hfrId = trim((string) ($organization['hfr_id'] ?? '')); if ($hfrId !== '') { $orgRes['identifier'] = [[ 'type' => ['coding' => [['system' => 'http://terminology.hl7.org/CodeSystem/v2-0203', 'code' => 'PRN', 'display' => 'Provider number']]], 'system' => 'https://facility.ndhm.gov.in', 'value' => $hfrId, ]]; } $resourceEntries[] = ['fullUrl' => $organizationRef, 'resource' => $orgRes]; } // ── Patient ─────────────────────────────────────────────────────────── $patientResource = $this->buildPatientResource($patient); $patientResource['id'] = $patientUuid; $resourceEntries[] = ['fullUrl' => $patientRef, 'resource' => $patientResource]; // ── Encounter ───────────────────────────────────────────────────────── if ($hasEncounter) { $encounterRes = [ 'resourceType' => 'Encounter', 'id' => $encounterUuid, 'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/Encounter']], 'identifier' => [['system' => 'https://ndhm.in', 'value' => (string) ($encounter['id'] ?? $encounterUuid)]], 'status' => (string) ($encounter['status'] ?? 'finished'), 'class' => [ 'system' => 'http://terminology.hl7.org/CodeSystem/v3-ActCode', 'code' => (string) ($encounter['class_code'] ?? 'AMB'), 'display' => (string) ($encounter['class_code'] ?? 'AMB') === 'IMP' ? 'inpatient encounter' : 'Ambulatory', ], 'subject' => ['reference' => $patientRef, 'display' => 'Patient'], ]; $pStart = trim((string) ($encounter['period_start'] ?? '')); if ($pStart !== '') { $encounterRes['period'] = ['start' => $pStart]; $pEnd = trim((string) ($encounter['period_end'] ?? '')); if ($pEnd !== '') { $encounterRes['period']['end'] = $pEnd; } } if ($practitionerRef !== '') { $encounterRes['participant'] = [['individual' => ['reference' => $practitionerRef]]]; } if ($organizationRef !== '') { $encounterRes['serviceProvider'] = ['reference' => $organizationRef]; } $resourceEntries[] = ['fullUrl' => $encounterRef, 'resource' => $encounterRes]; } // ── Observations (structured lab results) ───────────────────────────── $observationEntries = []; $observationRefs = []; foreach ($observations as $obs) { $obsUuid = $this->generateUuid(); $obsRef = 'urn:uuid:' . $obsUuid; $observationRefs[] = ['reference' => $obsRef]; $testName = trim((string) ($obs['test_name'] ?? '')); $loincCode = trim((string) ($obs['loinc_code'] ?? '')); $codeEntry = ['text' => $testName]; if ($loincCode !== '') { $codeEntry['coding'] = [['system' => 'http://loinc.org', 'code' => $loincCode, 'display' => $testName]]; } $obsResource = [ 'resourceType' => 'Observation', 'id' => $obsUuid, 'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/Observation']], 'status' => (string) ($obs['status'] ?? 'final'), 'code' => $codeEntry, 'subject' => ['reference' => $patientRef], 'effectiveDateTime' => $reportedAt, ]; if ($encounterRef !== '') { $obsResource['encounter'] = ['reference' => $encounterRef]; } if ($practitionerRef !== '') { $obsResource['performer'] = [['reference' => $practitionerRef]]; } elseif ($organizationRef !== '') { $obsResource['performer'] = [['reference' => $organizationRef]]; } // Value: quantity or string $valueType = trim((string) ($obs['value_type'] ?? 'string')); $rawValue = $obs['value'] ?? ''; if ($valueType === 'quantity' && is_numeric($rawValue)) { $ucum = trim((string) ($obs['ucum_code'] ?? '')); $unit = trim((string) ($obs['unit'] ?? '')); $obsResource['valueQuantity'] = [ 'value' => (float) $rawValue, 'unit' => $unit !== '' ? $unit : $ucum, 'system' => 'http://unitsofmeasure.org', 'code' => $ucum, ]; } else { $obsResource['valueString'] = (string) $rawValue; } // Reference range $refLow = $obs['ref_low'] ?? null; $refHigh = $obs['ref_high'] ?? null; if ($refLow !== null || $refHigh !== null) { $rr = []; $ucum = trim((string) ($obs['ucum_code'] ?? '')); $unit = trim((string) ($obs['unit'] ?? '')); if ($refLow !== null && is_numeric($refLow)) { $rr['low'] = ['value' => (float) $refLow, 'unit' => $unit ?: $ucum, 'system' => 'http://unitsofmeasure.org', 'code' => $ucum]; } if ($refHigh !== null && is_numeric($refHigh)) { $rr['high'] = ['value' => (float) $refHigh, 'unit' => $unit ?: $ucum, 'system' => 'http://unitsofmeasure.org', 'code' => $ucum]; } $obsResource['referenceRange'] = [$rr]; } // Interpretation (H/L/N) $interp = trim((string) ($obs['interpretation'] ?? '')); if ($interp !== '') { $interpCode = strtoupper($interp); $interpCodeMap = ['H' => 'H', 'HIGH' => 'H', 'L' => 'L', 'LOW' => 'L', 'N' => 'N', 'NORMAL' => 'N']; $obsResource['interpretation'] = [[ 'coding' => [[ 'system' => 'http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation', 'code' => $interpCodeMap[$interpCode] ?? $interpCode, 'display' => $interp, ]], 'text' => $interp, ]]; } $observationEntries[] = ['fullUrl' => $obsRef, 'resource' => $obsResource]; } // ── Binary + DocumentReference for HTML report ───────────────────────── $docRefEntry = null; $docRefRef = null; $pdfDocRefRef = null; $mediaRef = null; $reportHtml = trim((string) ($diagnosticReport['report_html'] ?? '')); if ($reportHtml !== '') { $binaryUuid = $this->generateUuid(); $docRefUuid = $this->generateUuid(); $docRefRef = 'urn:uuid:' . $docRefUuid; $contentType = trim((string) ($diagnosticReport['report_content_type'] ?? 'text/html; charset=utf-8')); $resourceEntries[] = ['fullUrl' => 'urn:uuid:' . $binaryUuid, 'resource' => [ 'resourceType' => 'Binary', 'id' => $binaryUuid, 'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/Binary']], 'contentType' => $contentType, 'data' => base64_encode($reportHtml), ]]; $docRefEntry = [ 'resourceType' => 'DocumentReference', 'id' => $docRefUuid, 'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/DocumentReference']], 'status' => 'current', 'type' => ['coding' => [['system' => 'http://snomed.info/sct', 'code' => '721981007', 'display' => 'Diagnostic studies report']]], 'subject' => ['reference' => $patientRef], 'content' => [['attachment' => ['contentType' => $contentType, 'url' => 'urn:uuid:' . $binaryUuid, 'data' => base64_encode($reportHtml), 'title' => $reportTitle]]], ]; $resourceEntries[] = ['fullUrl' => $docRefRef, 'resource' => $docRefEntry]; } // ── Optional PDF attachment as Binary + DocumentReference ─────────── if ($attachment !== null && ! empty($attachment['data_base64'])) { $pdfData = trim((string) ($attachment['data_base64'] ?? '')); if ($pdfData !== '') { $pdfBinaryUuid = $this->generateUuid(); $pdfDocRefUuid = $this->generateUuid(); $pdfDocRefRef = 'urn:uuid:' . $pdfDocRefUuid; $pdfContentType = trim((string) ($attachment['content_type'] ?? 'application/pdf')) ?: 'application/pdf'; $pdfTitle = trim((string) ($attachment['title'] ?? 'Lab Report PDF')) ?: 'Lab Report PDF'; $resourceEntries[] = ['fullUrl' => 'urn:uuid:' . $pdfBinaryUuid, 'resource' => [ 'resourceType' => 'Binary', 'id' => $pdfBinaryUuid, 'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/Binary']], 'contentType' => $pdfContentType, 'data' => $pdfData, ]]; $resourceEntries[] = ['fullUrl' => $pdfDocRefRef, 'resource' => [ 'resourceType' => 'DocumentReference', 'id' => $pdfDocRefUuid, 'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/DocumentReference']], 'status' => 'current', 'type' => ['coding' => [['system' => 'http://snomed.info/sct', 'code' => '721981007', 'display' => 'Diagnostic studies report']]], 'subject' => ['reference' => $patientRef], 'content' => [[ 'attachment' => [ 'contentType' => $pdfContentType, 'url' => 'urn:uuid:' . $pdfBinaryUuid, 'data' => $pdfData, 'title' => $pdfTitle, ], ]], ]]; if ($isImaging) { $mediaUuid = $this->generateUuid(); $mediaRef = 'urn:uuid:' . $mediaUuid; $resourceEntries[] = ['fullUrl' => $mediaRef, 'resource' => [ 'resourceType' => 'Media', 'id' => $mediaUuid, 'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/Media']], 'status' => 'completed', 'type' => 'photo', 'subject' => ['reference' => $patientRef], 'createdDateTime' => $reportedAt, 'content' => [ 'contentType' => $pdfContentType, 'url' => 'urn:uuid:' . $pdfBinaryUuid, 'data' => $pdfData, 'title' => $pdfTitle, ], ]]; } } } // Pick a single DocumentReference for Composition.section slicing (0..1). $effectiveDocRefRef = $pdfDocRefRef !== null ? $pdfDocRefRef : $docRefRef; // ── DiagnosticReport ────────────────────────────────────────────────── $reportRes = [ 'resourceType' => 'DiagnosticReport', 'id' => $reportUuid, 'meta' => ['profile' => [$isImaging ? 'https://nrces.in/ndhm/fhir/r4/StructureDefinition/DiagnosticReportImaging' : 'https://nrces.in/ndhm/fhir/r4/StructureDefinition/DiagnosticReportLab' ]], 'status' => $reportStatus, 'code' => array_filter([ 'coding' => trim((string) ($diagnosticReport['loinc_code'] ?? '')) !== '' ? [['system' => 'http://loinc.org', 'code' => (string) $diagnosticReport['loinc_code'], 'display' => $reportTitle]] : null, 'text' => $reportTitle, ]), 'subject' => ['reference' => $patientRef, 'display' => 'Patient'], 'issued' => $reportedAt, ]; if (!empty($observationRefs)) { $reportRes['result'] = $observationRefs; } if ($organizationRef !== '') { $reportRes['performer'] = [['reference' => $organizationRef]]; } elseif ($practitionerRef !== '') { $reportRes['performer'] = [['reference' => $practitionerRef]]; } if ($practitionerRef !== '') { $reportRes['resultsInterpreter'] = [['reference' => $practitionerRef]]; } elseif ($organizationRef !== '') { $reportRes['resultsInterpreter'] = [['reference' => $organizationRef]]; } if ($encounterRef !== '') { $reportRes['encounter'] = ['reference' => $encounterRef]; } $catSnomedCode = trim((string) ($diagnosticReport['category_snomed_code'] ?? '')); if ($catSnomedCode !== '') { $reportRes['category'] = [['coding' => [['system' => 'http://snomed.info/sct', 'code' => $catSnomedCode, 'display' => trim((string) ($diagnosticReport['category_snomed_display'] ?? $catSnomedCode))]]]]; } $conclusion = trim((string) ($diagnosticReport['conclusion'] ?? '')); $reportRes['conclusion'] = $conclusion !== '' ? $conclusion : 'Laboratory report generated'; if ($isImaging && $mediaRef !== null) { $reportRes['media'] = [[ 'link' => ['reference' => $mediaRef], ]]; } if ($attachment !== null && ! empty($attachment['data_base64'])) { $pdfPresentedFormUrl = $pdfDocRefRef !== null ? $pdfDocRefRef : null; $reportRes['presentedForm'] = [[ 'contentType' => trim((string) ($attachment['content_type'] ?? 'application/pdf')) ?: 'application/pdf', 'title' => trim((string) ($attachment['title'] ?? 'Lab Report PDF')) ?: 'Lab Report PDF', 'url' => $pdfPresentedFormUrl, ]]; } $resourceEntries[] = ['fullUrl' => $reportRef, 'resource' => $reportRes]; // ── Composition section entries ──────────────────────────────────────── $sectionEntries = [[ 'reference' => $reportRef, 'type' => 'DiagnosticReport', ]]; if ($effectiveDocRefRef !== null) { $sectionEntries[] = [ 'reference' => $effectiveDocRefRef, 'type' => 'DocumentReference', ]; } $sectionTitle = trim((string) ($diagnosticReport['section_title'] ?? ($isImaging ? 'Computed tomography imaging report' : 'Hematology report'))); $sectionCode = trim((string) ($diagnosticReport['section_snomed_code'] ?? ($isImaging ? '371531008' : '4321000179101'))); $sectionDisplay = trim((string) ($diagnosticReport['section_snomed_display'] ?? $sectionTitle)); // ── Composition ─────────────────────────────────────────────────────── $authorEntry = $practitionerRef !== '' ? ['reference' => $practitionerRef] : ($organizationRef !== '' ? ['reference' => $organizationRef] : ['reference' => $patientRef]); $composition = [ 'resourceType' => 'Composition', 'id' => $compositionUuid, 'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/DiagnosticReportRecord']], 'language' => 'en-IN', 'identifier' => ['system' => 'https://ndhm.in/phr', 'value' => $compositionUuid], 'status' => 'final', 'type' => [ 'coding' => [[ 'system' => 'http://snomed.info/sct', 'code' => $isImaging ? '721979004' : '721981007', 'display' => $isImaging ? 'Diagnostic Report- Imaging' : 'Diagnostic Report- Lab', ]], 'text' => $isImaging ? 'Diagnostic Report- Imaging' : 'Diagnostic Report- Lab', ], 'subject' => ['reference' => $patientRef, 'display' => 'Patient'], 'date' => $issuedAt, 'author' => [$authorEntry], 'title' => $reportTitle, 'section' => [[ 'title' => $sectionTitle, 'code' => ['coding' => [['system' => 'http://snomed.info/sct', 'code' => $sectionCode, 'display' => $sectionDisplay]]], 'entry' => $sectionEntries, ]], ]; if ($encounterRef !== '') { $composition['encounter'] = ['reference' => $encounterRef]; } if ($organizationRef !== '') { $composition['custodian'] = ['reference' => $organizationRef]; } // ── Bundle identifier system: HFR ID based or fallback ──────────────── $hfrId = trim((string) ($organization['hfr_id'] ?? '')); $bundleIdSystem = $hfrId !== '' ? 'https://' . strtolower(preg_replace('/[^A-Za-z0-9]/', '', $hfrId)) . '.hfr.abdm.gov.in' : 'https://hfr.abdm.gov.in'; $bundleIdValue = $hfrId !== '' ? 'LAB-' . (string) ($diagnosticReport['id'] ?? $bundleUuid) : $bundleUuid; return [ 'resourceType' => 'Bundle', 'id' => $bundleUuid, 'meta' => [ 'profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/DocumentBundle'], 'security' => [['system' => 'http://terminology.hl7.org/CodeSystem/v3-Confidentiality', 'code' => 'V', 'display' => 'very restricted']], ], 'identifier' => ['system' => $bundleIdSystem, 'value' => $bundleIdValue], 'type' => 'document', 'timestamp' => $issuedAt, 'entry' => array_merge( [['fullUrl' => 'urn:uuid:' . $compositionUuid, 'resource' => $composition]], $resourceEntries, $observationEntries ), ]; } /** * Build an ABDM-compliant DischargeSummaryRecord FHIR DocumentBundle. * * @param array $patient {id, name, given_name, family_name, gender, birthDate, abhaAddress, phone} * @param array $encounter {id, admission_date, discharge_date, ipd_code, discharge_disposition} * @param array $summary {title, clinical_summary_html, chief_complaints, diagnosis_text, * investigations_text, procedures_text, medications_text, follow_up} * @param array|null $practitioner {name, registration_number} * @param array|null $organization {name, hfr_id} * * @return array */ public function buildDischargeSummaryBundle( array $patient, array $encounter, array $summary, ?array $practitioner = null, ?array $organization = null ): array { $issuedAt = $this->isoTimestamp(); $bundleUuid = $this->generateUuid(); $compositionUuid = $this->generateUuid(); $patientUuid = $this->generateUuid(); $encounterUuid = $this->generateUuid(); $patientRef = 'urn:uuid:' . $patientUuid; $encounterRef = 'urn:uuid:' . $encounterUuid; $hasPractitioner = $practitioner !== null && trim((string) ($practitioner['name'] ?? '')) !== ''; $hasOrganization = $organization !== null && trim((string) ($organization['name'] ?? '')) !== ''; $practitionerUuid = $hasPractitioner ? $this->generateUuid() : ''; $organizationUuid = $hasOrganization ? $this->generateUuid() : ''; $practitionerRef = $hasPractitioner ? ('urn:uuid:' . $practitionerUuid) : ''; $organizationRef = $hasOrganization ? ('urn:uuid:' . $organizationUuid) : ''; $resourceEntries = []; // ── Practitioner ────────────────────────────────────────────────────── if ($hasPractitioner) { $practRes = [ 'resourceType' => 'Practitioner', 'id' => $practitionerUuid, 'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/Practitioner']], 'name' => [['text' => trim((string) ($practitioner['name'] ?? ''))]], ]; $regNo = trim((string) ($practitioner['registration_number'] ?? '')); if ($regNo !== '') { $practRes['identifier'] = [[ 'type' => ['coding' => [['system' => 'http://terminology.hl7.org/CodeSystem/v2-0203', 'code' => 'MD', 'display' => 'Medical License number']]], 'system' => 'https://doctor.ndhm.gov.in', 'value' => $regNo, ]]; } $resourceEntries[] = ['fullUrl' => $practitionerRef, 'resource' => $practRes]; } // ── Organization ────────────────────────────────────────────────────── if ($hasOrganization) { $orgRes = [ 'resourceType' => 'Organization', 'id' => $organizationUuid, 'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/Organization']], 'name' => trim((string) ($organization['name'] ?? '')), ]; $hfrId = trim((string) ($organization['hfr_id'] ?? '')); if ($hfrId !== '') { $orgRes['identifier'] = [[ 'type' => ['coding' => [['system' => 'http://terminology.hl7.org/CodeSystem/v2-0203', 'code' => 'PRN', 'display' => 'Provider number']]], 'system' => 'https://facility.ndhm.gov.in', 'value' => $hfrId, ]]; } $resourceEntries[] = ['fullUrl' => $organizationRef, 'resource' => $orgRes]; } // ── Patient ─────────────────────────────────────────────────────────── $patientResource = $this->buildPatientResource($patient); $patientResource['id'] = $patientUuid; $resourceEntries[] = ['fullUrl' => $patientRef, 'resource' => $patientResource]; // ── Encounter (inpatient) ───────────────────────────────────────────── $admissionDate = trim((string) ($encounter['admission_date'] ?? '')) ?: $issuedAt; $dischargeDate = trim((string) ($encounter['discharge_date'] ?? '')) ?: $issuedAt; $encounterResource = [ 'resourceType' => 'Encounter', 'id' => $encounterUuid, 'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/Encounter']], 'identifier' => [['system' => 'https://ndhm.in', 'value' => trim((string) ($encounter['ipd_code'] ?? $encounterUuid))]], 'status' => 'finished', 'class' => [ 'system' => 'http://terminology.hl7.org/CodeSystem/v3-ActCode', 'code' => 'IMP', 'display' => 'inpatient encounter', ], 'subject' => ['reference' => $patientRef, 'display' => 'Patient'], 'period' => ['start' => $admissionDate, 'end' => $dischargeDate], ]; $dispText = trim((string) ($encounter['discharge_disposition'] ?? '')); if ($dispText !== '') { $encounterResource['hospitalization'] = [ 'dischargeDisposition' => [ 'coding' => [['system' => 'http://terminology.hl7.org/CodeSystem/discharge-disposition', 'code' => 'home', 'display' => 'Home']], 'text' => $dispText, ], ]; } if ($practitionerRef !== '') { $encounterResource['participant'] = [['individual' => ['reference' => $practitionerRef]]]; } if ($organizationRef !== '') { $encounterResource['serviceProvider'] = ['reference' => $organizationRef]; } $resourceEntries[] = ['fullUrl' => $encounterRef, 'resource' => $encounterResource]; // ── Composition sections per DischargeSummaryRecord spec ────────────── $sections = []; $summaryHtml = trim((string) ($summary['clinical_summary_html'] ?? '')); // Chief complaints section (SNOMED 422843007) $chiefComplaints = trim((string) ($summary['chief_complaints'] ?? '')); if ($chiefComplaints !== '' || $summaryHtml !== '') { $ccSection = [ 'title' => 'Chief complaints', 'code' => ['coding' => [['system' => 'http://snomed.info/sct', 'code' => '422843007', 'display' => 'Chief complaint section']]], ]; if ($chiefComplaints !== '') { $ccSection['text'] = ['status' => 'generated', 'div' => '
' . htmlspecialchars($chiefComplaints) . '
']; } $sections[] = $ccSection; } // Medical History section (SNOMED 1003642006) $diagnosisText = trim((string) ($summary['diagnosis_text'] ?? '')); if ($diagnosisText !== '') { $sections[] = [ 'title' => 'Medical History', 'code' => ['coding' => [['system' => 'http://snomed.info/sct', 'code' => '1003642006', 'display' => 'Past medical history section']]], 'text' => ['status' => 'generated', 'div' => '
' . htmlspecialchars($diagnosisText) . '
'], ]; } // Investigations section (SNOMED 721981007) $investigationsText = trim((string) ($summary['investigations_text'] ?? '')); if ($investigationsText !== '') { $sections[] = [ 'title' => 'Investigations', 'code' => ['coding' => [['system' => 'http://snomed.info/sct', 'code' => '721981007', 'display' => 'Diagnostic studies report']]], 'text' => ['status' => 'generated', 'div' => '
' . htmlspecialchars($investigationsText) . '
'], ]; } // Procedures section (SNOMED 1003640003) $proceduresText = trim((string) ($summary['procedures_text'] ?? '')); if ($proceduresText !== '') { $sections[] = [ 'title' => 'Procedures', 'code' => ['coding' => [['system' => 'http://snomed.info/sct', 'code' => '1003640003', 'display' => 'Procedure report']]], 'text' => ['status' => 'generated', 'div' => '
' . htmlspecialchars($proceduresText) . '
'], ]; } // Medications section (SNOMED 721912009) $medicationsText = trim((string) ($summary['medications_text'] ?? '')); if ($medicationsText !== '') { $sections[] = [ 'title' => 'Medications', 'code' => ['coding' => [['system' => 'http://snomed.info/sct', 'code' => '721912009', 'display' => 'Medication summary document']]], 'text' => ['status' => 'generated', 'div' => '
' . htmlspecialchars($medicationsText) . '
'], ]; } // Follow-up section (SNOMED 736271009) $followUp = trim((string) ($summary['follow_up'] ?? '')); if ($followUp !== '') { $sections[] = [ 'title' => 'Follow Up', 'code' => ['coding' => [['system' => 'http://snomed.info/sct', 'code' => '736271009', 'display' => 'Outpatient care plan']]], 'text' => ['status' => 'generated', 'div' => '
' . htmlspecialchars($followUp) . '
'], ]; } // Fallback: one summary section if nothing else provided if (empty($sections) && $summaryHtml !== '') { $sections[] = [ 'title' => 'Discharge Summary', 'code' => ['coding' => [['system' => 'http://snomed.info/sct', 'code' => '373942005', 'display' => 'Discharge summary']]], 'text' => ['status' => 'generated', 'div' => $summaryHtml], ]; } // ── Composition ─────────────────────────────────────────────────────── $authorEntry = $practitionerRef !== '' ? ['reference' => $practitionerRef] : ['display' => 'Unknown']; $composition = [ 'resourceType' => 'Composition', 'id' => $compositionUuid, 'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/DischargeSummaryRecord']], 'language' => 'en-IN', 'identifier' => ['system' => 'https://ndhm.in/phr', 'value' => $compositionUuid], 'status' => 'final', 'type' => [ 'coding' => [['system' => 'http://snomed.info/sct', 'code' => '373942005', 'display' => 'Discharge summary']], 'text' => 'Discharge Summary', ], 'subject' => ['reference' => $patientRef, 'display' => 'Patient'], 'encounter' => ['reference' => $encounterRef, 'display' => 'Encounter'], 'date' => $issuedAt, 'author' => [[$authorEntry]], 'title' => (string) ($summary['title'] ?? 'Discharge Summary'), 'section' => $sections, ]; if ($organizationRef !== '') { $composition['custodian'] = ['reference' => $organizationRef]; } // ── Bundle identifier system: HFR ID based or fallback ──────────────── $hfrId = trim((string) ($organization['hfr_id'] ?? '')); $bundleIdSystem = $hfrId !== '' ? 'https://' . strtolower(preg_replace('/[^A-Za-z0-9]/', '', $hfrId)) . '.hfr.abdm.gov.in' : 'https://hfr.abdm.gov.in'; $ipdCode = trim((string) ($encounter['ipd_code'] ?? '')); $bundleIdValue = $hfrId !== '' ? 'IPD-' . ($ipdCode !== '' ? $ipdCode : $bundleUuid) : $bundleUuid; return [ 'resourceType' => 'Bundle', 'id' => $bundleUuid, 'meta' => [ 'profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/DocumentBundle'], 'security' => [['system' => 'http://terminology.hl7.org/CodeSystem/v3-Confidentiality', 'code' => 'V', 'display' => 'very restricted']], ], 'identifier' => ['system' => $bundleIdSystem, 'value' => $bundleIdValue], 'type' => 'document', 'timestamp' => $issuedAt, 'entry' => array_merge( [['fullUrl' => 'urn:uuid:' . $compositionUuid, 'resource' => $composition]], $resourceEntries ), ]; } /** * @param array $patient * @param array $encounter * @param array $claim * * @return array */ public function buildClaimBundle(array $patient, array $encounter, array $claim): array { $issuedAt = Time::now('Asia/Kolkata')->toDateTimeString(); $patientRef = 'Patient/' . (string) ($patient['id'] ?? 'unknown'); $encounterRef = 'Encounter/' . (string) ($encounter['id'] ?? 'unknown'); $claimId = (string) ($claim['id'] ?? ('claim-' . time())); $items = is_array($claim['items'] ?? null) ? ($claim['items'] ?? []) : []; $claimItems = []; foreach ($items as $index => $item) { if (! is_array($item)) { continue; } $claimItems[] = [ 'sequence' => $index + 1, 'productOrService' => [ 'text' => (string) ($item['name'] ?? $item['description'] ?? ''), ], 'quantity' => [ 'value' => (float) ($item['qty'] ?? 1), ], 'unitPrice' => [ 'value' => (float) ($item['unit_price'] ?? 0), 'currency' => (string) ($item['currency'] ?? 'INR'), ], 'net' => [ 'value' => (float) ($item['amount'] ?? 0), 'currency' => (string) ($item['currency'] ?? 'INR'), ], ]; } $claimResource = [ 'resourceType' => 'Claim', 'id' => $claimId, 'status' => (string) ($claim['status'] ?? 'active'), 'use' => (string) ($claim['use'] ?? 'claim'), 'type' => [ 'text' => (string) ($claim['type'] ?? 'institutional'), ], 'patient' => ['reference' => $patientRef], 'created' => $issuedAt, 'provider' => [ 'display' => (string) ($claim['provider'] ?? ''), ], 'insurer' => [ 'display' => (string) ($claim['insurer'] ?? ''), ], 'priority' => [ 'text' => (string) ($claim['priority'] ?? 'normal'), ], 'item' => $claimItems, 'total' => [ 'value' => (float) ($claim['total'] ?? 0), 'currency' => (string) ($claim['currency'] ?? 'INR'), ], 'encounter' => [[ 'reference' => $encounterRef, ]], ]; return [ 'resourceType' => 'Bundle', 'type' => 'collection', 'timestamp' => $issuedAt, 'entry' => [[ 'resource' => $this->buildPatientResource($patient), ], [ 'resource' => [ 'resourceType' => 'Encounter', 'id' => (string) ($encounter['id'] ?? 'unknown'), 'status' => (string) ($encounter['status'] ?? 'finished'), 'subject' => ['reference' => $patientRef], ], ], [ 'resource' => $claimResource, ]], ]; } /** Generate a cryptographically random UUID v4. */ private function generateUuid(): string { $data = random_bytes(16); $data[6] = chr(ord($data[6]) & 0x0f | 0x40); $data[8] = chr(ord($data[8]) & 0x3f | 0x80); return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($data), 4)); } /** ISO 8601 timestamp with India Standard Time offset (+05:30). */ private function isoTimestamp(): string { return (new \DateTime('now', new \DateTimeZone('Asia/Kolkata')))->format('Y-m-d\TH:i:sP'); } /** * @param array $patient * * @return array */ private function buildPatientResource(array $patient): array { // Build structured name: prefer explicit given_name/family_name fields; // fall back to splitting the full 'name' string. $fullText = trim((string) ($patient['name'] ?? '')); $givenName = trim((string) ($patient['given_name'] ?? '')); $familyName = trim((string) ($patient['family_name'] ?? '')); if ($givenName === '' && $familyName === '' && $fullText !== '') { // Try splitting: last word → family, rest → given $parts = preg_split('/\s+/', $fullText, -1, PREG_SPLIT_NO_EMPTY) ?: [$fullText]; $familyName = count($parts) > 1 ? (string) array_pop($parts) : ''; $givenName = implode(' ', $parts); } $nameText = $fullText !== '' ? $fullText : trim($givenName . ' ' . $familyName); $nameEntry = ['text' => $nameText]; if ($givenName !== '') { $nameEntry['given'] = [$givenName]; } if ($familyName !== '') { $nameEntry['family'] = $familyName; } $resource = [ 'resourceType' => 'Patient', 'id' => (string) ($patient['id'] ?? 'unknown'), 'name' => [$nameEntry], ]; if (! empty($patient['gender'])) { $resource['gender'] = (string) $patient['gender']; } if (! empty($patient['birthDate'])) { $resource['birthDate'] = (string) $patient['birthDate']; } $abhaAddress = trim((string) ($patient['abhaAddress'] ?? '')); if ($abhaAddress !== '') { $resource['meta'] = ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/Patient']]; // NI = National unique individual identifier (ABHA number / address) $resource['identifier'] = [[ 'type' => ['coding' => [[ 'system' => 'http://terminology.hl7.org/CodeSystem/v2-0203', 'code' => 'NI', 'display' => 'National unique individual identifier', ]]], 'system' => 'https://healthid.ndhm.gov.in', 'value' => $abhaAddress, ]]; } // Phone / mobile telecom $phone = trim((string) ($patient['phone'] ?? '')); if ($phone !== '') { $resource['telecom'] = [[ 'system' => 'phone', 'value' => $phone, 'use' => 'mobile', ]]; } return $resource; } /** * Map common route-of-administration text to SNOMED CT codes. * Returns empty string when no mapping is found. */ private function resolveRouteSnomedCode(string $routeText): string { static $map = [ // Oral 'oral' => '26643006', 'by mouth' => '26643006', 'po' => '26643006', 'per oral' => '26643006', // Intravenous 'intravenous' => '47625008', 'iv' => '47625008', 'i.v.' => '47625008', 'intravenously' => '47625008', // Intramuscular 'intramuscular' => '78421000', 'im' => '78421000', 'i.m.' => '78421000', // Subcutaneous 'subcutaneous' => '34206005', 'sc' => '34206005', 's.c.' => '34206005', 'subcut' => '34206005', // Topical 'topical' => '6064005', 'locally' => '6064005', // Inhaled / nebulisation 'inhaled' => '18679011000001101', 'inhalation' => '18679011000001101', 'nebulisation' => '18679011000001101', 'nebulizer' => '18679011000001101', // Sublingual 'sublingual' => '37839007', 'sl' => '37839007', // Rectal 'rectal' => '37161004', 'per rectal' => '37161004', 'pr' => '37161004', // Nasal 'nasal' => '46713006', 'intranasal' => '46713006', // Ophthalmic / eye 'ophthalmic' => '54485002', 'eye' => '54485002', 'otic' => '10547007', 'ear' => '10547007', ]; $key = strtolower(trim($routeText)); return $map[$key] ?? ''; } // ========================================================================= // Enhanced FHIR Bundle Builders (ABDM M3 Compliant) // ========================================================================= /** * Build a full DiagnosticReport FHIR Bundle (ABDM M3 — DiagnosticReportRecord). * * Supports LOINC-coded observations, specimen metadata, Practitioner performer, * and an optional scanned-document attachment via DocumentReference + Binary. * * @param array $patient Patient demographics * @param array $diagnosticReport {id, title, category_loinc, * category_display, status, conclusion, reported_at} * @param array> $observations Each: {test_name, loinc_code, value, * value_type (quantity|string), unit, ucum_code, * reference_range_low, reference_range_high, * interpretation, status} * @param array|null $specimen {id, type_text, collection_date} * @param array|null $attachment {content_type, data_base64, title} * @param array|null $practitioner {id, name, registration_number} * @param array|null $organization {id, name, hfr_id} * * @return array */ public function buildEnhancedLabReportBundle( array $patient, array $diagnosticReport, array $observations = [], ?array $specimen = null, ?array $attachment = null, ?array $practitioner = null, ?array $organization = null ): array { $issuedAt = Time::now('Asia/Kolkata')->toDateTimeString(); $patientRef = 'Patient/' . (string) ($patient['id'] ?? 'unknown'); $reportId = 'lab-rpt-' . (string) ($diagnosticReport['id'] ?? date('YmdHis')); $entries = [['resource' => $this->buildPatientResource($patient)]]; $practitionerRef = ''; $organizationRef = ''; if ($practitioner !== null) { $practId = trim((string) ($practitioner['id'] ?? '')); $practitionerRef = $practId !== '' ? ('Practitioner/' . $practId) : ''; if ($practitionerRef !== '') { $practRes = [ 'resourceType' => 'Practitioner', 'id' => $practId, 'name' => [['text' => trim((string) ($practitioner['name'] ?? ''))]], ]; $regNo = trim((string) ($practitioner['registration_number'] ?? '')); if ($regNo !== '') { $practRes['identifier'] = [[ 'system' => 'https://hpr.abdm.gov.in/hpr-id', 'value' => $regNo, ]]; } $entries[] = ['resource' => $practRes]; } } if ($organization !== null) { $orgId = trim((string) ($organization['id'] ?? '')); $organizationRef = $orgId !== '' ? ('Organization/' . $orgId) : ''; if ($organizationRef !== '') { $orgRes = [ 'resourceType' => 'Organization', 'id' => $orgId, 'name' => trim((string) ($organization['name'] ?? '')), ]; $hfrId = trim((string) ($organization['hfr_id'] ?? '')); if ($hfrId !== '') { $orgRes['identifier'] = [[ 'system' => 'https://facility.abdm.gov.in/hfr', 'value' => $hfrId, ]]; } $entries[] = ['resource' => $orgRes]; } } // Specimen (optional) $specimenRef = ''; if ($specimen !== null) { $specimenId = 'specimen-' . (string) ($specimen['id'] ?? '1'); $specimenRef = 'Specimen/' . $specimenId; $specimenRes = [ 'resourceType' => 'Specimen', 'id' => $specimenId, 'subject' => ['reference' => $patientRef], 'type' => ['text' => (string) ($specimen['type_text'] ?? 'Blood')], ]; $collDate = trim((string) ($specimen['collection_date'] ?? '')); if ($collDate !== '') { $specimenRes['collection'] = ['collectedDateTime' => $collDate]; } $entries[] = ['resource' => $specimenRes]; } // Observations $observationRefs = []; foreach ($observations as $index => $obs) { $obsId = 'obs-' . ($index + 1); $observationRefs[] = ['reference' => 'Observation/' . $obsId]; $loincCode = trim((string) ($obs['loinc_code'] ?? '')); $testName = (string) ($obs['test_name'] ?? ''); $valueType = trim((string) ($obs['value_type'] ?? 'string')); $rawValue = $obs['value'] ?? ''; $unit = (string) ($obs['unit'] ?? ''); $ucumCode = (string) ($obs['ucum_code'] ?? $unit); $interpText = (string) ($obs['interpretation'] ?? ''); $code = ['text' => $testName]; if ($loincCode !== '') { $code['coding'] = [[ 'system' => 'http://loinc.org', 'code' => $loincCode, 'display' => $testName, ]]; } $obsResource = [ 'resourceType' => 'Observation', 'id' => $obsId, 'status' => (string) ($obs['status'] ?? 'final'), 'category' => [[ 'coding' => [[ 'system' => 'http://terminology.hl7.org/CodeSystem/observation-category', 'code' => 'laboratory', 'display' => 'Laboratory', ]], ]], 'code' => $code, 'subject' => ['reference' => $patientRef], 'issued' => $issuedAt, ]; if ($specimenRef !== '') { $obsResource['specimen'] = ['reference' => $specimenRef]; } if ($practitionerRef !== '') { $obsResource['performer'] = [['reference' => $practitionerRef]]; } if ($valueType === 'quantity' && is_numeric($rawValue)) { $obsResource['valueQuantity'] = [ 'value' => (float) $rawValue, 'unit' => $unit, 'system' => 'http://unitsofmeasure.org', 'code' => $ucumCode !== '' ? $ucumCode : $unit, ]; } else { $obsResource['valueString'] = (string) $rawValue; } $refLow = $obs['reference_range_low'] ?? null; $refHigh = $obs['reference_range_high'] ?? null; if ($refLow !== null || $refHigh !== null) { $refRange = []; if (is_numeric($refLow)) { $refRange['low'] = ['value' => (float) $refLow, 'unit' => $unit]; } if (is_numeric($refHigh)) { $refRange['high'] = ['value' => (float) $refHigh, 'unit' => $unit]; } if (! empty($refRange)) { $obsResource['referenceRange'] = [$refRange]; } } if ($interpText !== '') { $obsResource['interpretation'] = [['text' => $interpText]]; } $entries[] = ['resource' => $obsResource]; } // Optional scanned attachment $docRefRef = ''; if ($attachment !== null && ! empty($attachment['data_base64'])) { $docEntries = $this->buildDocumentReferenceEntry( 'doc-lab-' . $reportId, (string) ($attachment['title'] ?? 'Lab Report Document'), (string) ($attachment['content_type'] ?? 'application/pdf'), (string) $attachment['data_base64'], $patientRef, $issuedAt ); $docRefRef = 'DocumentReference/doc-lab-' . $reportId; foreach ($docEntries as $entry) { $entries[] = $entry; } } $performers = []; if ($practitionerRef !== '') { $performers[] = ['reference' => $practitionerRef]; } if ($organizationRef !== '') { $performers[] = ['reference' => $organizationRef]; } $categoryLoinc = trim((string) ($diagnosticReport['category_loinc'] ?? '26436-6')); $categoryDisplay = trim((string) ($diagnosticReport['category_display'] ?? 'Laboratory studies (procedure)')); $drResource = [ 'resourceType' => 'DiagnosticReport', 'id' => $reportId, 'status' => (string) ($diagnosticReport['status'] ?? 'final'), 'category' => [[ 'coding' => [[ 'system' => 'http://loinc.org', 'code' => $categoryLoinc, 'display' => $categoryDisplay, ]], ]], 'code' => ['text' => (string) ($diagnosticReport['title'] ?? 'Laboratory Report')], 'subject' => ['reference' => $patientRef], 'issued' => (string) ($diagnosticReport['reported_at'] ?? $issuedAt), 'result' => $observationRefs, 'conclusion' => (string) ($diagnosticReport['conclusion'] ?? ''), ]; if ($specimenRef !== '') { $drResource['specimen'] = [['reference' => $specimenRef]]; } if (! empty($performers)) { $drResource['performer'] = $performers; } if ($docRefRef !== '') { $drResource['presentedForm'] = [[ 'contentType' => (string) ($attachment['content_type'] ?? 'application/pdf'), 'title' => (string) ($attachment['title'] ?? 'Lab Report'), ]]; } $entries[] = ['resource' => $drResource]; // Composition (LOINC 11502-2 = Laboratory report) $compositionSections = [ ['title' => 'Lab Results', 'entry' => $observationRefs], ]; if ($docRefRef !== '') { $compositionSections[] = [ 'title' => 'Report Document', 'entry' => [['reference' => $docRefRef]], ]; } $composition = [ 'resourceType' => 'Composition', 'id' => 'comp-' . $reportId, 'status' => 'final', 'type' => [ 'coding' => [[ 'system' => 'http://loinc.org', 'code' => '11502-2', 'display' => 'Laboratory report', ]], 'text' => 'Diagnostic Report - Lab', ], 'subject' => ['reference' => $patientRef], 'date' => $issuedAt, 'title' => (string) ($diagnosticReport['title'] ?? 'Laboratory Report'), 'section' => $compositionSections, ]; if ($practitionerRef !== '') { $composition['author'] = [['reference' => $practitionerRef]]; } $entries[] = ['resource' => $composition]; return [ 'resourceType' => 'Bundle', 'identifier' => [ 'system' => 'urn:ietf:rfc:3986', 'value' => 'urn:uuid:bundle-lab-' . $reportId . '-' . date('YmdHis'), ], 'type' => 'document', 'timestamp' => $issuedAt, 'entry' => $entries, ]; } // ------------------------------------------------------------------------- /** * Build a full Discharge Summary FHIR Bundle (ABDM M3 — DischargeSummaryRecord). * * Covers all mandatory composition sections: chief complaints, diagnosis, * allergies, procedures, medications, investigations, care plan, and a * free-text clinical notes narrative. * * @param array $patient Patient demographics * @param array $practitioner {id, name, registration_number} * @param array $organization {id, name, hfr_id} * @param array $encounter {id, admission_date, discharge_date} * @param array $summary { * title, chief_complaints[], conditions[], allergies[], * procedures[], medications[], investigations[], care_plan, * follow_up_date, clinical_notes_html * } * * @return array */ public function buildEnhancedDischargeSummaryBundle( array $patient, array $practitioner, array $organization, array $encounter, array $summary ): array { $issuedAt = Time::now('Asia/Kolkata')->toDateTimeString(); $encounterId = (string) ($encounter['id'] ?? 'enc-' . date('YmdHis')); $patientRef = 'Patient/' . (string) ($patient['id'] ?? 'unknown'); $encounterRef = 'Encounter/' . $encounterId; $practId = trim((string) ($practitioner['id'] ?? '')); $orgId = trim((string) ($organization['id'] ?? '')); $practitionerRef = $practId !== '' ? ('Practitioner/' . $practId) : ''; $organizationRef = $orgId !== '' ? ('Organization/' . $orgId) : ''; $entries = [['resource' => $this->buildPatientResource($patient)]]; // Practitioner if ($practitionerRef !== '') { $practRes = [ 'resourceType' => 'Practitioner', 'id' => $practId, 'name' => [['text' => (string) ($practitioner['name'] ?? '')]], ]; $regNo = trim((string) ($practitioner['registration_number'] ?? '')); if ($regNo !== '') { $practRes['identifier'] = [[ 'system' => 'https://hpr.abdm.gov.in/hpr-id', 'value' => $regNo, ]]; } $entries[] = ['resource' => $practRes]; } // Organization if ($organizationRef !== '') { $orgRes = [ 'resourceType' => 'Organization', 'id' => $orgId, 'name' => (string) ($organization['name'] ?? ''), ]; $hfrId = trim((string) ($organization['hfr_id'] ?? '')); if ($hfrId !== '') { $orgRes['identifier'] = [[ 'system' => 'https://facility.abdm.gov.in/hfr', 'value' => $hfrId, ]]; } $entries[] = ['resource' => $orgRes]; } // Encounter (inpatient) $encounterResource = [ 'resourceType' => 'Encounter', 'id' => $encounterId, 'status' => 'finished', 'class' => [ 'system' => 'http://terminology.hl7.org/CodeSystem/v3-ActCode', 'code' => 'IMP', 'display' => 'inpatient encounter', ], 'subject' => ['reference' => $patientRef], 'period' => [ 'start' => (string) ($encounter['admission_date'] ?? $encounter['period_start'] ?? $issuedAt), 'end' => (string) ($encounter['discharge_date'] ?? $encounter['period_end'] ?? $issuedAt), ], ]; if ($practitionerRef !== '') { $encounterResource['participant'] = [['individual' => ['reference' => $practitionerRef]]]; } if ($organizationRef !== '') { $encounterResource['serviceProvider'] = ['reference' => $organizationRef]; } $entries[] = ['resource' => $encounterResource]; $sections = []; // --- Chief Complaints --- $complaintRefs = []; foreach ((array) ($summary['chief_complaints'] ?? []) as $idx => $complaint) { $text = is_array($complaint) ? trim((string) ($complaint['text'] ?? '')) : trim((string) $complaint); if ($text === '') { continue; } $condId = 'cc-' . ($idx + 1); $code = ['text' => $text]; $snomedCode = is_array($complaint) ? trim((string) ($complaint['snomed_code'] ?? '')) : ''; if ($snomedCode !== '') { $code['coding'] = [['system' => 'http://snomed.info/sct', 'code' => $snomedCode, 'display' => $text]]; } $complaintRefs[] = ['reference' => 'Condition/' . $condId]; $entries[] = ['resource' => [ 'resourceType' => 'Condition', 'id' => $condId, 'clinicalStatus' => ['coding' => [['system' => 'http://terminology.hl7.org/CodeSystem/condition-clinical', 'code' => 'active']]], 'category' => [['coding' => [['system' => 'http://terminology.hl7.org/CodeSystem/condition-category', 'code' => 'problem-list-item']], 'text' => 'Chief Complaint']], 'code' => $code, 'subject' => ['reference' => $patientRef], 'encounter' => ['reference' => $encounterRef], ]]; } if (! empty($complaintRefs)) { $sections[] = ['title' => 'Chief Complaints', 'entry' => $complaintRefs]; } // --- Diagnosis / Conditions --- $conditionRefs = []; $encounterDiagnosis = []; foreach ((array) ($summary['conditions'] ?? []) as $idx => $cond) { $text = is_array($cond) ? trim((string) ($cond['text'] ?? '')) : trim((string) $cond); if ($text === '') { continue; } $condId = 'diag-' . ($idx + 1); $code = ['text' => $text]; $snomedCode = is_array($cond) ? trim((string) ($cond['snomed_code'] ?? '')) : ''; if ($snomedCode !== '') { $code['coding'] = [[ 'system' => 'http://snomed.info/sct', 'code' => $snomedCode, 'display' => is_array($cond) ? (string) ($cond['snomed_display'] ?? $text) : $text, ]]; } $verStatus = is_array($cond) && strtolower((string) ($cond['verification_status'] ?? '')) === 'confirmed' ? 'confirmed' : 'provisional'; $conditionRefs[] = ['reference' => 'Condition/' . $condId]; $encounterDiagnosis[] = ['condition' => ['reference' => 'Condition/' . $condId]]; $entries[] = ['resource' => [ 'resourceType' => 'Condition', 'id' => $condId, 'clinicalStatus' => ['coding' => [['system' => 'http://terminology.hl7.org/CodeSystem/condition-clinical', 'code' => 'active']]], 'verificationStatus' => ['coding' => [['system' => 'http://terminology.hl7.org/CodeSystem/condition-ver-status', 'code' => $verStatus]]], 'category' => [['coding' => [['system' => 'http://terminology.hl7.org/CodeSystem/condition-category', 'code' => 'encounter-diagnosis']]]], 'code' => $code, 'subject' => ['reference' => $patientRef], 'encounter' => ['reference' => $encounterRef], ]]; } if (! empty($conditionRefs)) { $sections[] = ['title' => 'Diagnosis', 'entry' => $conditionRefs]; // Back-patch encounter diagnosis references $encounterResource['diagnosis'] = $encounterDiagnosis; } // --- Allergies --- $allergyRefs = []; foreach ((array) ($summary['allergies'] ?? []) as $idx => $allergy) { $codeText = is_array($allergy) ? trim((string) ($allergy['code_text'] ?? '')) : trim((string) $allergy); if ($codeText === '') { continue; } $allergyId = 'allergy-ds-' . ($idx + 1); $allergyRefs[] = ['reference' => 'AllergyIntolerance/' . $allergyId]; $allergyRes = [ 'resourceType' => 'AllergyIntolerance', 'id' => $allergyId, 'clinicalStatus' => ['coding' => [['system' => 'http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical', 'code' => 'active']]], 'verificationStatus' => ['coding' => [['system' => 'http://terminology.hl7.org/CodeSystem/allergyintolerance-verification', 'code' => 'confirmed']]], 'patient' => ['reference' => $patientRef], 'code' => ['text' => $codeText], ]; $reaction = is_array($allergy) ? trim((string) ($allergy['reaction_text'] ?? '')) : ''; if ($reaction !== '') { $allergyRes['reaction'] = [['description' => $reaction]]; } $entries[] = ['resource' => $allergyRes]; } if (! empty($allergyRefs)) { $sections[] = ['title' => 'Allergies', 'entry' => $allergyRefs]; } // --- Procedures --- $procedureRefs = []; foreach ((array) ($summary['procedures'] ?? []) as $idx => $procedure) { $text = is_array($procedure) ? trim((string) ($procedure['text'] ?? '')) : trim((string) $procedure); if ($text === '') { continue; } $procId = 'proc-' . ($idx + 1); $code = ['text' => $text]; $snomedCode = is_array($procedure) ? trim((string) ($procedure['snomed_code'] ?? '')) : ''; if ($snomedCode !== '') { $code['coding'] = [['system' => 'http://snomed.info/sct', 'code' => $snomedCode, 'display' => $text]]; } $procedureRefs[] = ['reference' => 'Procedure/' . $procId]; $entries[] = ['resource' => [ 'resourceType' => 'Procedure', 'id' => $procId, 'status' => is_array($procedure) ? (string) ($procedure['status'] ?? 'completed') : 'completed', 'code' => $code, 'subject' => ['reference' => $patientRef], 'encounter' => ['reference' => $encounterRef], ]]; } if (! empty($procedureRefs)) { $sections[] = ['title' => 'Procedures', 'entry' => $procedureRefs]; } // --- Medications --- $medicationRefs = []; foreach ((array) ($summary['medications'] ?? []) as $idx => $med) { $drugName = is_array($med) ? trim((string) ($med['drug_name'] ?? '')) : trim((string) $med); if ($drugName === '') { continue; } $medId = 'medreq-ds-' . ($idx + 1); $mc = ['text' => $drugName]; $snomedCode = is_array($med) ? trim((string) ($med['snomed_code'] ?? '')) : ''; $atcCode = is_array($med) ? strtoupper(trim((string) ($med['atc_code'] ?? ''))) : ''; if ($snomedCode !== '') { $mc['coding'] = [['system' => 'http://snomed.info/sct', 'code' => $snomedCode, 'display' => $drugName]]; } elseif ($atcCode !== '') { $mc['coding'] = [['system' => 'http://www.whocc.no/atc', 'code' => $atcCode, 'display' => $drugName]]; } $medicationRefs[] = ['reference' => 'MedicationRequest/' . $medId]; $entries[] = ['resource' => [ 'resourceType' => 'MedicationRequest', 'id' => $medId, 'status' => 'active', 'intent' => 'order', 'subject' => ['reference' => $patientRef], 'encounter' => ['reference' => $encounterRef], 'medicationCodeableConcept' => $mc, 'dosageInstruction' => [['text' => is_array($med) ? (string) ($med['dosage'] ?? '') : '']], ]]; } if (! empty($medicationRefs)) { $sections[] = ['title' => 'Medications', 'entry' => $medicationRefs]; } // --- Investigations --- $investigationRefs = []; foreach ((array) ($summary['investigations'] ?? []) as $idx => $inv) { $text = is_array($inv) ? trim((string) ($inv['text'] ?? '')) : trim((string) $inv); if ($text === '') { continue; } $svcId = 'svc-ds-' . ($idx + 1); $investigationRefs[] = ['reference' => 'ServiceRequest/' . $svcId]; $entries[] = ['resource' => [ 'resourceType' => 'ServiceRequest', 'id' => $svcId, 'status' => 'completed', 'intent' => 'order', 'code' => ['text' => $text], 'subject' => ['reference' => $patientRef], 'encounter' => ['reference' => $encounterRef], ]]; } if (! empty($investigationRefs)) { $sections[] = ['title' => 'Investigations', 'entry' => $investigationRefs]; } // --- Care Plan --- $carePlanRefs = []; $carePlanText = trim((string) ($summary['care_plan'] ?? '')); if ($carePlanText !== '') { $cpId = 'careplan-1'; $carePlanRefs[] = ['reference' => 'CarePlan/' . $cpId]; $cpResource = [ 'resourceType' => 'CarePlan', 'id' => $cpId, 'status' => 'active', 'intent' => 'plan', 'subject' => ['reference' => $patientRef], 'encounter' => ['reference' => $encounterRef], 'description' => $carePlanText, ]; $followUpDate = trim((string) ($summary['follow_up_date'] ?? '')); if ($followUpDate !== '') { $cpResource['period'] = ['start' => $issuedAt, 'end' => $followUpDate]; } $entries[] = ['resource' => $cpResource]; $sections[] = ['title' => 'Care Plan', 'entry' => $carePlanRefs]; } // Clinical notes narrative (unstructured) $clinicalNotesHtml = (string) ($summary['clinical_notes_html'] ?? ''); // Composition (LOINC 18842-5 = Discharge summary) $composition = [ 'resourceType' => 'Composition', 'id' => 'comp-discharge-' . $encounterId, 'status' => 'final', 'type' => [ 'coding' => [[ 'system' => 'http://loinc.org', 'code' => '18842-5', 'display' => 'Discharge summary', ]], 'text' => 'Discharge Summary', ], 'subject' => ['reference' => $patientRef], 'encounter' => ['reference' => $encounterRef], 'date' => $issuedAt, 'title' => (string) ($summary['title'] ?? 'Discharge Summary'), 'section' => $sections, ]; if ($practitionerRef !== '') { $composition['author'] = [['reference' => $practitionerRef]]; } if ($clinicalNotesHtml !== '') { $composition['section'][] = [ 'title' => 'Clinical Summary Notes', 'text' => ['status' => 'generated', 'div' => $clinicalNotesHtml], ]; } $entries[] = ['resource' => $composition]; return [ 'resourceType' => 'Bundle', 'identifier' => [ 'system' => 'urn:ietf:rfc:3986', 'value' => 'urn:uuid:bundle-discharge-' . $encounterId . '-' . date('YmdHis'), ], 'type' => 'document', 'timestamp' => $issuedAt, 'entry' => $entries, ]; } // ------------------------------------------------------------------------- /** * Build a WellnessRecord FHIR Bundle (ABDM M3 — WellnessRecord). * * Contains vital-sign / body-measurement Observations and * social-history lifestyle Observations. * * @param array $patient Patient demographics * @param array> $vitals Each: {loinc_code, display, value, * value_type (quantity|string), unit, ucum_code} * @param array> $lifestyle Each: {code, display, value, system} * @param array|null $practitioner {id, name} * * @return array */ public function buildWellnessBundle( array $patient, array $vitals = [], array $lifestyle = [], ?array $practitioner = null ): array { $issuedAt = Time::now('Asia/Kolkata')->toDateTimeString(); $patientRef = 'Patient/' . (string) ($patient['id'] ?? 'unknown'); $entries = [['resource' => $this->buildPatientResource($patient)]]; $practitionerRef = ''; if ($practitioner !== null) { $practId = trim((string) ($practitioner['id'] ?? '')); $practitionerRef = $practId !== '' ? ('Practitioner/' . $practId) : ''; if ($practitionerRef !== '') { $entries[] = ['resource' => [ 'resourceType' => 'Practitioner', 'id' => $practId, 'name' => [['text' => (string) ($practitioner['name'] ?? '')]], ]]; } } $observationRefs = []; // Vital signs / body measurements foreach ($vitals as $idx => $vital) { $loincCode = trim((string) ($vital['loinc_code'] ?? '')); $display = (string) ($vital['display'] ?? ''); $value = $vital['value'] ?? ''; $unit = (string) ($vital['unit'] ?? ''); $ucumCode = (string) ($vital['ucum_code'] ?? $unit); $valueType = (string) ($vital['value_type'] ?? 'quantity'); if ($valueType === 'quantity' && ! is_numeric($value)) { continue; } if (trim((string) $value) === '') { continue; } $obsId = 'wellness-obs-' . ($idx + 1); $observationRefs[] = ['reference' => 'Observation/' . $obsId]; $code = ['text' => $display]; if ($loincCode !== '') { $code['coding'] = [['system' => 'http://loinc.org', 'code' => $loincCode, 'display' => $display]]; } $obsResource = [ 'resourceType' => 'Observation', 'id' => $obsId, 'status' => 'final', 'category' => [[ 'coding' => [[ 'system' => 'http://terminology.hl7.org/CodeSystem/observation-category', 'code' => 'vital-signs', 'display' => 'Vital Signs', ]], ]], 'code' => $code, 'subject' => ['reference' => $patientRef], 'effectiveDateTime' => $issuedAt, ]; if ($practitionerRef !== '') { $obsResource['performer'] = [['reference' => $practitionerRef]]; } if ($valueType === 'quantity' && is_numeric($value)) { $obsResource['valueQuantity'] = [ 'value' => (float) $value, 'unit' => $unit, 'system' => 'http://unitsofmeasure.org', 'code' => $ucumCode, ]; } else { $obsResource['valueString'] = (string) $value; } $entries[] = ['resource' => $obsResource]; } // Lifestyle / social-history observations foreach ($lifestyle as $idx => $ls) { $code = trim((string) ($ls['code'] ?? '')); $display = (string) ($ls['display'] ?? ''); $value = (string) ($ls['value'] ?? ''); $system = (string) ($ls['system'] ?? 'http://snomed.info/sct'); if ($value === '' || $display === '') { continue; } $obsId = 'wellness-ls-' . ($idx + 1); $observationRefs[] = ['reference' => 'Observation/' . $obsId]; $obsCode = ['text' => $display]; if ($code !== '') { $obsCode['coding'] = [['system' => $system, 'code' => $code, 'display' => $display]]; } $entries[] = ['resource' => [ 'resourceType' => 'Observation', 'id' => $obsId, 'status' => 'final', 'category' => [[ 'coding' => [[ 'system' => 'http://terminology.hl7.org/CodeSystem/observation-category', 'code' => 'social-history', 'display' => 'Social History', ]], ]], 'code' => $obsCode, 'subject' => ['reference' => $patientRef], 'effectiveDateTime' => $issuedAt, 'valueString' => $value, ]]; } $composition = [ 'resourceType' => 'Composition', 'id' => 'comp-wellness-' . date('YmdHis'), 'status' => 'final', 'type' => [ 'coding' => [[ 'system' => 'http://snomed.info/sct', 'code' => '371529009', 'display' => 'Health maintenance report (record artifact)', ]], 'text' => 'Wellness Record', ], 'subject' => ['reference' => $patientRef], 'date' => $issuedAt, 'title' => 'Wellness Record', 'section' => [ ['title' => 'Vital Signs and Body Measurements', 'entry' => $observationRefs], ], ]; if ($practitionerRef !== '') { $composition['author'] = [['reference' => $practitionerRef]]; } $entries[] = ['resource' => $composition]; return [ 'resourceType' => 'Bundle', 'identifier' => [ 'system' => 'urn:ietf:rfc:3986', 'value' => 'urn:uuid:bundle-wellness-' . date('YmdHis'), ], 'type' => 'document', 'timestamp' => $issuedAt, 'entry' => $entries, ]; } // ------------------------------------------------------------------------- /** * Build DocumentReference + Binary resource entries for a scanned attachment. * * Usage: append the returned entries directly to an existing Bundle's entry array. * * @param string $id Resource ID stem (without "DocumentReference/") * @param string $title Document title (human-readable) * @param string $contentType MIME type — application/pdf | image/jpeg | image/png * @param string $dataBase64 Base64-encoded raw file content * @param string $subjectRef Patient reference string (e.g. "Patient/123") * @param string $date ISO 8601 date/datetime string * * @return array> [ [resource => Binary], [resource => DocumentReference] ] */ public function buildDocumentReferenceEntry( string $id, string $title, string $contentType, string $dataBase64, string $subjectRef, string $date ): array { $binaryId = 'binary-' . $id; $binaryRef = 'Binary/' . $binaryId; $binaryResource = [ 'resourceType' => 'Binary', 'id' => $binaryId, 'contentType' => $contentType, 'data' => $dataBase64, ]; $docRefResource = [ 'resourceType' => 'DocumentReference', 'id' => $id, 'status' => 'current', 'type' => ['text' => $title], 'subject' => ['reference' => $subjectRef], 'date' => $date, 'content' => [[ 'attachment' => [ 'contentType' => $contentType, 'url' => $binaryRef, 'title' => $title, ], ]], ]; return [ ['resource' => $binaryResource], ['resource' => $docRefResource], ]; } /** * Read a file from disk and return its base64-encoded content. * Returns an empty string when the file is absent or unreadable. */ public function encodeFileAsBase64(string $filePath): string { if ($filePath === '' || ! is_file($filePath) || ! is_readable($filePath)) { return ''; } $content = file_get_contents($filePath); return $content !== false ? base64_encode($content) : ''; } }