$patient * @param array $encounter * @param array> $medications * @param array> $conditions * @param array $context * * @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'] : []; $attachments = is_array($context['attachments'] ?? null) ? (array) $context['attachments'] : []; // 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 = []; $documentRefs = []; // 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) { $resourceEntries[] = ['fullUrl' => 'urn:uuid:' . $organizationUuid, 'resource' => $this->buildOrganizationResource($organization, $organizationUuid)]; } // ── 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' => $this->normalizeIsoDateTime((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'], 'recordedDate' => $issuedAt, ]]; } // 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'], 'recordedDate' => $issuedAt, ]; // 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; } $num = (float) $value; $loinc = (string) ($observation['loinc'] ?? ''); $display = (string) ($observation['display'] ?? ''); $unit = (string) ($observation['unit'] ?? ''); if ($loinc === '8310-5' || strcasecmp($display, 'Body temperature') === 0 || in_array($unit, ['Cel', 'degF', '[degF]'], true)) { $num = round($num, 1); } elseif ($loinc === '39156-5' || strcasecmp($display, 'Body mass index') === 0) { $num = round($num, 1); } elseif ($loinc === '29463-7' || strcasecmp($display, 'Body weight') === 0) { $num = round($num, 2); } elseif ($loinc === '8302-2' || strcasecmp($display, 'Body height') === 0) { $num = round($num, 1); } elseif (in_array($loinc, ['8867-4', '59408-5', '8480-6', '8462-4', '9279-1'], true)) { $num = (float) round($num); } else { $num = round($num, 2); } $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' => $loinc, 'display' => $display, ]], 'text' => $display, ], 'subject' => ['reference' => $patientRef, 'display' => 'Patient'], 'encounter' => ['reference' => $encounterRef], 'effectiveDateTime' => $issuedAt, 'valueQuantity' => [ 'value' => $num, 'unit' => $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 ($this->isMeaningfulValue($snomedCode) && $snomedCode !== '371530004') { $medicationCodeableConcept['coding'] = [[ 'system' => 'http://snomed.info/sct', 'code' => $snomedCode, 'display' => $displayText !== '' ? $displayText : $drugName, ]]; } elseif ($this->isMeaningfulValue($atcCode)) { $medicationCodeableConcept['coding'] = [[ 'system' => 'http://www.whocc.no/atc', 'code' => $atcCode, 'display' => $displayText !== '' ? $displayText : $drugName, ]]; } else { $medicationCodeableConcept['coding'] = [[ 'system' => 'http://snomed.info/sct', 'code' => '105904009', 'display' => $displayText !== '' ? $displayText : $drugName, ]]; } $dosageInstruction = ['text' => (string) ($medication['dosage'] ?? '')]; $routeText = trim((string) ($medication['route_text'] ?? '')); $routeCode = trim((string) ($medication['route_code'] ?? '')); if (! $this->isMeaningfulValue($routeText)) { $medTypeUpper = strtoupper($medType); if (in_array($medTypeUpper, ['INJ', 'INJECTION', 'IV', 'IM'], true)) { $routeText = 'Injection'; $routeCode = '47625008'; } elseif (in_array($medTypeUpper, ['CREAM', 'OINT', 'OINTMENT', 'GEL', 'LOTION'], true)) { $routeText = 'Topical'; $routeCode = '6064005'; } else { $routeText = 'Oral'; $routeCode = '260548002'; } } elseif ($routeCode === '') { $routeCode = $this->resolveRouteSnomedCode($routeText); } $routeEntry = ['text' => $routeText]; if ($this->isMeaningfulValue($routeCode) && $routeCode !== '371530004') { $routeEntry['coding'] = [[ 'system' => 'http://snomed.info/sct', 'code' => $routeCode, 'display' => $routeText, ]]; } else { $routeEntry['coding'] = [[ 'system' => 'http://snomed.info/sct', 'code' => '260548002', 'display' => $routeText, ]]; } $dosageInstruction['route'] = $routeEntry; $methodText = trim((string) ($medication['method_text'] ?? $medType)); $methodCode = trim((string) ($medication['method_code'] ?? '')); if ($this->isMeaningfulValue($methodText)) { $methodEntry = ['text' => $methodText]; if ($this->isMeaningfulValue($methodCode) && $methodCode !== '371530004') { $methodEntry['coding'] = [[ 'system' => 'http://snomed.info/sct', 'code' => $methodCode, 'display' => $methodText, ]]; } $dosageInstruction['method'] = $methodEntry; } $freqText = trim((string) ($medication['frequency_text'] ?? '')); $dosageText = (string) ($medication['dosage'] ?? ''); $timingResult = $this->buildFhirTimingStructure($freqText, $dosageText); if ($timingResult['timing'] !== null) { $dosageInstruction['timing'] = $timingResult['timing']; } $medRes = [ 'resourceType' => 'MedicationRequest', 'id' => $medUuid, 'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/MedicationRequest']], 'status' => (string) ($medication['status'] ?? 'active'), 'intent' => 'order', 'category' => [[ 'coding' => [[ 'system' => 'http://terminology.hl7.org/CodeSystem/medicationrequest-category', 'code' => 'outpatient', 'display' => 'Outpatient', ]], ]], 'medicationCodeableConcept' => $medicationCodeableConcept, 'subject' => ['reference' => $patientRef, 'display' => 'Patient'], 'encounter' => ['reference' => $encounterRef], 'authoredOn' => $issuedAt, 'dosageInstruction' => [$dosageInstruction], ]; if ($timingResult['asNeeded']) { $medRes['asNeededBoolean'] = true; } if ($practitionerRef !== '') { $medRes['requester'] = ['reference' => $practitionerRef, 'display' => $practRawName !== '' ? $practRawName : 'Practitioner']; } $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']; $apptStartIso = $issuedAt; if (preg_match('/(\d{2}-\d{2}-\d{4})/', $description, $m) === 1) { $parsed = \DateTimeImmutable::createFromFormat('d-m-Y', $m[1]); if ($parsed !== false) { $apptStartIso = $parsed->setTime(10, 0, 0)->format(DATE_ATOM); } } elseif (preg_match('/(\d{4}-\d{2}-\d{2})/', $description, $m) === 1) { $parsed = \DateTimeImmutable::createFromFormat('Y-m-d', $m[1]); if ($parsed !== false) { $apptStartIso = $parsed->setTime(10, 0, 0)->format(DATE_ATOM); } } $apptRes = [ 'resourceType' => 'Appointment', 'id' => $apptUuid, 'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/Appointment']], 'status' => (string) ($appointment['status'] ?? 'proposed'), 'description' => $description, 'start' => (string) ($appointment['start'] ?? $apptStartIso), 'end' => (string) ($appointment['end'] ?? $apptStartIso), 'participant' => [[ 'actor' => ['reference' => $patientRef, 'display' => 'Patient'], 'status' => 'accepted', ]], ]; if ($practitionerRef !== '') { $apptRes['participant'][] = [ 'actor' => ['reference' => $practitionerRef, 'display' => $practRawName !== '' ? $practRawName : 'Practitioner'], 'status' => 'accepted', ]; } $resourceEntries[] = ['fullUrl' => 'urn:uuid:' . $apptUuid, 'resource' => $apptRes]; } // ── OPD scan/upload attachments (DocumentReference + Binary) ───────── foreach ($attachments as $index => $attachment) { if (! is_array($attachment)) { continue; } $dataBase64 = trim((string) ($attachment['data_base64'] ?? '')); if ($dataBase64 === '') { continue; } $contentType = trim((string) ($attachment['content_type'] ?? 'application/octet-stream')); $title = trim((string) ($attachment['title'] ?? 'OPD scanned document')); $date = $this->normalizeIsoDateTime((string) ($attachment['date'] ?? ''), $issuedAt); $attachmentId = preg_replace('/[^A-Za-z0-9\-\.]/', '-', trim((string) ($attachment['id'] ?? ''))); if ($attachmentId === '' || $attachmentId === null) { $attachmentId = (string) ($index + 1); } if (str_starts_with($attachmentId, 'prescription-') || str_starts_with($attachmentId, 'doc-') || $contentType === 'application/pdf') { $docUuid = str_starts_with($attachmentId, 'doc-') ? $attachmentId : 'doc-' . $attachmentId; } else { $docUuid = 'opd-scan-' . $attachmentId . '-' . $this->generateUuid(); } $binaryUuid = 'binary-' . $docUuid; $docRef = 'urn:uuid:' . $docUuid; $binaryRef = 'urn:uuid:' . $binaryUuid; $documentRefs[] = ['reference' => $docRef, 'display' => 'DocumentReference']; $resourceEntries[] = ['fullUrl' => $binaryRef, 'resource' => [ 'resourceType' => 'Binary', 'id' => $binaryUuid, 'contentType' => $contentType, 'data' => $dataBase64, ]]; $resourceEntries[] = ['fullUrl' => $docRef, 'resource' => [ 'resourceType' => 'DocumentReference', 'id' => $docUuid, 'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/DocumentReference']], 'status' => 'current', 'type' => ['text' => $title], 'subject' => ['reference' => $patientRef, 'display' => 'Patient'], 'date' => $date, 'content' => [[ 'attachment' => [ 'contentType' => $contentType, 'url' => $binaryRef, 'data' => $dataBase64, 'title' => $title, ], ]], ]]; } // ── 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, ]; } if (! empty($documentRefs)) { $compositionSections[] = [ 'title' => 'Document Reference', 'code' => ['coding' => [[ 'system' => 'http://snomed.info/sct', 'code' => '371530004', 'display' => 'Clinical consultation report', ]]], 'entry' => $documentRefs, ]; } // ── Composition (first entry per ABDM spec) ─────────────────────────── $composition = [ 'resourceType' => 'Composition', 'id' => $compositionUuid, 'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/PrescriptionRecord']], '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' => 'Prescription Record', '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 !== '' ? 'PRESCRIPTION-' . ($encounter['id'] ?? $bundleUuid) . '-' . date('Y-m-d') : $bundleUuid; return $this->sanitizeBundle([ '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 = $this->normalizeIsoDateTime((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) { $resourceEntries[] = ['fullUrl' => $organizationRef, 'resource' => $this->buildOrganizationResource($organization, $organizationUuid)]; } // ── 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'], ]; $pStartRaw = trim((string) ($encounter['period_start'] ?? '')); if ($pStartRaw !== '') { $encounterRes['period'] = ['start' => $this->normalizeIsoDateTime($pStartRaw, $issuedAt)]; $pEndRaw = trim((string) ($encounter['period_end'] ?? '')); if ($pEndRaw !== '') { $pEndIso = $this->normalizeIsoDateTime($pEndRaw); if ($pEndIso !== '') { $encounterRes['period']['end'] = $pEndIso; } } } 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]; } if (! empty($rr)) { $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'; $pdfBytes = base64_decode($pdfData, true); $pdfSize = (int) ($attachment['size'] ?? (is_string($pdfBytes) ? strlen($pdfBytes) : 0)); $pdfHash = trim((string) ($attachment['hash'] ?? (is_string($pdfBytes) ? base64_encode(sha1($pdfBytes, true)) : ''))); $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' => array_filter([ 'contentType' => $pdfContentType, 'url' => 'urn:uuid:' . $pdfBinaryUuid, 'data' => $pdfData, 'title' => $pdfTitle, 'size' => $pdfSize > 0 ? $pdfSize : null, 'hash' => $pdfHash !== '' ? $pdfHash : null, ], static fn ($value): bool => $value !== null), ]], ]]; 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 $this->sanitizeBundle([ '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) { $resourceEntries[] = ['fullUrl' => $organizationRef, 'resource' => $this->buildOrganizationResource($organization, $organizationUuid)]; } // ── 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]; } // ABDM rejects a DischargeSummaryRecord without at least one Condition or // Procedure resource, so the diagnosis must be a resource and not just narrative. $diagnosisResourceText = trim((string) ( $summary['diagnosis_text'] ?? $summary['final_diagnosis'] ?? $summary['chief_complaints'] ?? '' )); $conditionRef = ''; if ($diagnosisResourceText !== '') { $conditionUuid = $this->generateUuid(); $conditionRef = 'urn:uuid:' . $conditionUuid; $encounterResource['diagnosis'] = [[ 'condition' => ['reference' => $conditionRef, 'display' => 'Condition'], ]]; $resourceEntries[] = ['fullUrl' => $conditionRef, 'resource' => [ 'resourceType' => 'Condition', 'id' => $conditionUuid, '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' => 'confirmed', 'display' => 'Confirmed']]], 'category' => [['coding' => [['system' => 'http://terminology.hl7.org/CodeSystem/condition-category', 'code' => 'encounter-diagnosis', 'display' => 'Encounter Diagnosis']]]], 'code' => ['text' => $diagnosisResourceText], 'subject' => ['reference' => $patientRef, 'display' => 'Patient'], 'encounter' => ['reference' => $encounterRef], ]]; } $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) . '
'], ]; } if ($conditionRef !== '') { $sections[] = [ 'title' => 'Diagnosis', 'code' => ['coding' => [['system' => 'http://snomed.info/sct', 'code' => '721981007', 'display' => 'Diagnosis']]], 'entry' => [['reference' => $conditionRef, 'display' => 'Condition']], 'text' => ['status' => 'generated', 'div' => '
' . htmlspecialchars($diagnosisResourceText) . '
'], ]; } // 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 $this->sanitizeBundle([ '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, ]], ]; // ABDM validates InvoiceRecord on a FHIR Invoice resource; Claim alone is rejected. $invoiceLineItems = []; foreach ($claimItems as $index => $claimItem) { $invoiceLineItems[] = [ 'sequence' => $index + 1, 'chargeItemCodeableConcept' => $claimItem['productOrService'], 'priceComponent' => [[ 'type' => 'base', 'amount' => $claimItem['net'], ]], ]; } $invoiceResource = [ 'resourceType' => 'Invoice', 'id' => 'invoice-' . $claimId, 'status' => (string) ($claim['invoice_status'] ?? 'issued'), 'subject' => ['reference' => $patientRef], 'date' => $issuedAt, 'totalGross' => [ 'value' => (float) ($claim['total'] ?? 0), 'currency' => (string) ($claim['currency'] ?? 'INR'), ], 'totalNet' => [ 'value' => (float) ($claim['total'] ?? 0), 'currency' => (string) ($claim['currency'] ?? 'INR'), ], ]; $invoiceNo = trim((string) ($claim['invoice_no'] ?? '')); if ($invoiceNo !== '') { $invoiceResource['identifier'] = [['system' => 'https://ndhm.in/invoice', 'value' => $invoiceNo]]; } if ($invoiceLineItems !== []) { $invoiceResource['lineItem'] = $invoiceLineItems; } // ABDM InvoiceRecord must be a document bundle led by a Composition. $composition = [ 'resourceType' => 'Composition', 'id' => 'comp-invoice-' . $claimId, 'status' => 'final', 'type' => [ 'coding' => [[ 'system' => 'http://snomed.info/sct', 'code' => '736762002', 'display' => 'Invoice record', ]], 'text' => 'Invoice', ], 'subject' => ['reference' => $patientRef], 'date' => $issuedAt, 'title' => 'Invoice', 'encounter' => ['reference' => $encounterRef], 'section' => [[ 'title' => 'Invoice', 'entry' => [ ['reference' => 'Invoice/invoice-' . $claimId], ['reference' => 'Claim/' . $claimId], ], ]], ]; $provider = trim((string) ($claim['provider'] ?? '')); if ($provider !== '') { $composition['author'] = [['display' => $provider]]; } return $this->sanitizeBundle([ 'resourceType' => 'Bundle', 'type' => 'document', 'timestamp' => $issuedAt, 'entry' => [[ 'resource' => $composition, ], [ 'resource' => $this->buildPatientResource($patient), ], [ 'resource' => [ 'resourceType' => 'Encounter', 'id' => (string) ($encounter['id'] ?? 'unknown'), 'status' => (string) ($encounter['status'] ?? 'finished'), 'subject' => ['reference' => $patientRef], ], ], [ 'resource' => $invoiceResource, ], [ 'resource' => $claimResource, ]], ]); } /** * Build an ABDM ImmunizationRecord FHIR DocumentBundle. * * @param array $patient * @param array> $immunizations * @param array $context practitioner, organization, encounter, care_context_reference * * @return array */ public function buildImmunizationRecordBundle(array $patient, array $immunizations, array $context = []): array { $issuedAt = $this->isoTimestamp(); $bundleUuid = $this->generateUuid(); $compositionUuid = $this->generateUuid(); $patientUuid = $this->generateUuid(); $patientRef = 'urn:uuid:' . $patientUuid; $practitioner = is_array($context['practitioner'] ?? null) ? (array) $context['practitioner'] : []; $organization = is_array($context['organization'] ?? null) ? (array) $context['organization'] : []; $recommendations = is_array($context['recommendations'] ?? null) ? (array) $context['recommendations'] : []; if ($recommendations === [] && isset($context['recommendation']) && is_array($context['recommendation'])) { $recommendations = [$context['recommendation']]; } $hasPractitioner = trim((string) ($practitioner['id'] ?? '')) !== '' || trim((string) ($practitioner['name'] ?? '')) !== ''; $hasOrganization = trim((string) ($organization['id'] ?? $organization['hfr_id'] ?? '')) !== '' || trim((string) ($organization['name'] ?? '')) !== ''; $practitionerUuid = $hasPractitioner ? $this->generateUuid() : ''; $organizationUuid = $hasOrganization ? $this->generateUuid() : ''; $practitionerRef = $hasPractitioner ? ('urn:uuid:' . $practitionerUuid) : ''; $organizationRef = $hasOrganization ? ('urn:uuid:' . $organizationUuid) : ''; $patientDisplayName = trim((string) ($patient['name'] ?? '')); if ($patientDisplayName === '') { $patientDisplayName = 'Patient'; } $patientResource = $this->buildPatientResource($patient); $patientResource['id'] = $patientUuid; $patientResource['meta'] = ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/Patient']]; $patientEntry = ['fullUrl' => $patientRef, 'resource' => $patientResource]; $careContextReference = trim((string) ($context['care_context_reference'] ?? '')); if ($careContextReference === '') { $sourceIds = []; foreach ($immunizations as $immunization) { $sourceId = trim((string) ($immunization['id'] ?? '')); if ($sourceId !== '') { $sourceIds[] = $sourceId; } } $sourceIds = array_values(array_unique($sourceIds)); sort($sourceIds, SORT_STRING); if (count($sourceIds) === 1) { $careContextReference = 'IMM-' . $sourceIds[0]; } elseif ($sourceIds !== []) { $careContextReference = 'IMM-SET-' . substr(hash('sha256', implode(',', $sourceIds)), 0, 20); } else { $careContextReference = 'IMM-PAT-' . trim((string) ($patient['id'] ?? 'unknown')); } } $encounterUuid = $this->generateUuid(); $encounterRef = 'urn:uuid:' . $encounterUuid; $encounterContext = is_array($context['encounter'] ?? null) ? (array) $context['encounter'] : []; $encounterId = trim((string) ($encounterContext['id'] ?? '')); if ($encounterId === '') { $encounterId = $careContextReference !== '' ? $careContextReference : ('IMM-' . $encounterUuid); } $encounterStatus = trim((string) ($encounterContext['status'] ?? 'finished')); $classCode = strtoupper(trim((string) ($encounterContext['class_code'] ?? 'AMB'))); $classDisplay = trim((string) ($encounterContext['class_display'] ?? ($classCode === 'IMP' ? 'inpatient encounter' : 'ambulatory'))); $encounterStartRaw = trim((string) ($encounterContext['period_start'] ?? '')); if ($encounterStartRaw === '') { foreach ($immunizations as $imm) { $d = trim((string) ($imm['given_date'] ?? $imm['occurrenceDateTime'] ?? '')); if ($d !== '') { $encounterStartRaw = $d; break; } } } $encounterStart = $this->normalizeIsoDateTime($encounterStartRaw, $issuedAt); $encounterEndRaw = trim((string) ($encounterContext['period_end'] ?? '')); $encounterEnd = $encounterEndRaw !== '' ? $this->normalizeIsoDateTime($encounterEndRaw, $issuedAt) : ''; $encounterPeriod = ['start' => $encounterStart]; if ($encounterEnd !== '') { $encounterPeriod['end'] = $encounterEnd; } $encounterResource = [ 'resourceType' => 'Encounter', 'id' => $encounterUuid, 'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/Encounter']], 'identifier' => [[ 'system' => 'https://ndhm.in', 'value' => $encounterId, ]], 'status' => $encounterStatus, 'class' => [ 'system' => 'http://terminology.hl7.org/CodeSystem/v3-ActCode', 'code' => $classCode, 'display' => $classDisplay, ], 'subject' => ['reference' => $patientRef, 'display' => $patientDisplayName], 'period' => $encounterPeriod, ]; $practitionerName = ''; $practitionerEntry = null; if ($hasPractitioner) { $practitionerName = trim((string) ($practitioner['name'] ?? '')); if ($practitionerName !== '' && stripos($practitionerName, 'Dr') === false) { $practitionerName = 'Dr. ' . $practitionerName; } $practitionerResource = [ 'resourceType' => 'Practitioner', 'id' => $practitionerUuid, 'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/Practitioner']], 'name' => [['text' => $practitionerName]], ]; $registrationNumber = trim((string) ($practitioner['registration_number'] ?? '')); $hprId = trim((string) ($practitioner['hpr_id'] ?? '')); $practitionerResource['identifier'] = []; if ($registrationNumber !== '') { $practitionerResource['identifier'][] = [ 'type' => ['coding' => [[ 'system' => 'http://terminology.hl7.org/CodeSystem/v2-0203', 'code' => 'MD', 'display' => 'Medical License number', ]]], 'system' => 'https://doctor.ndhm.gov.in', 'value' => $registrationNumber, ]; } if ($hprId !== '') { $practitionerResource['identifier'][] = [ 'type' => ['coding' => [[ 'system' => 'http://terminology.hl7.org/CodeSystem/v2-0203', 'code' => 'HPR', 'display' => 'Healthcare Professional Registry ID', ]]], 'system' => 'https://hpr.ndhm.gov.in', 'value' => $hprId, ]; } if ($practitionerResource['identifier'] === []) { $practitionerResource['identifier'][] = [ 'type' => ['coding' => [[ 'system' => 'http://terminology.hl7.org/CodeSystem/v2-0203', 'code' => 'MD', 'display' => 'Medical License number', ]]], 'system' => 'https://doctor.ndhm.gov.in', 'value' => (string) ($practitioner['id'] ?? 'DOC-1'), ]; } $practitionerEntry = ['fullUrl' => $practitionerRef, 'resource' => $practitionerResource]; } if ($practitionerRef !== '') { $encounterResource['participant'] = [[ 'individual' => [ 'reference' => $practitionerRef, 'display' => $practitionerName ?: 'Doctor', ], ]]; } if ($organizationRef !== '') { $encounterResource['serviceProvider'] = [ 'reference' => $organizationRef, 'display' => trim((string) ($organization['name'] ?? '')), ]; } $encounterEntry = ['fullUrl' => $encounterRef, 'resource' => $encounterResource]; $organizationEntry = null; if ($hasOrganization) { $organizationEntry = ['fullUrl' => $organizationRef, 'resource' => $this->buildOrganizationResource($organization, $organizationUuid)]; } $immunizationRefs = []; $immunizationEntries = []; $firstImmunizationRef = ''; foreach ($immunizations as $immunization) { $vaccineName = trim((string) ($immunization['vaccine_name'] ?? $immunization['vaccine_display'] ?? '')); if ($vaccineName === '') { continue; } $immunizationUuid = $this->generateUuid(); $immunizationRef = 'urn:uuid:' . $immunizationUuid; if ($firstImmunizationRef === '') { $firstImmunizationRef = $immunizationRef; } $status = $this->normalizeImmunizationStatus((string) ($immunization['status'] ?? 'completed')); $occurrence = $this->normalizeIsoDateTime( (string) ($immunization['given_date'] ?? $immunization['occurrenceDateTime'] ?? $immunization['due_date'] ?? ''), $issuedAt ); $vaccineCoding = $this->resolveVaccineStandardCoding( (string) ($immunization['vaccine_code_system'] ?? ''), (string) ($immunization['vaccine_code'] ?? ''), $vaccineName ); $resource = [ 'resourceType' => 'Immunization', 'id' => $immunizationUuid, 'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/Immunization']], 'identifier' => [[ 'system' => 'https://ndhm.in/immunization', 'value' => (string) ($immunization['id'] ?? $immunizationUuid), ]], 'status' => $status, 'vaccineCode' => $vaccineCoding, 'patient' => ['reference' => $patientRef, 'display' => $patientDisplayName], 'encounter' => ['reference' => $encounterRef], 'occurrenceDateTime' => $occurrence, 'recorded' => $issuedAt, 'primarySource' => true, ]; $manufacturer = trim((string) ($immunization['manufacturer'] ?? '')); if ($manufacturer !== '') { $resource['manufacturer'] = ['display' => $manufacturer]; } $lotNumber = trim((string) ($immunization['lot_number'] ?? '')); if ($lotNumber !== '') { $resource['lotNumber'] = $lotNumber; } $expiryDate = trim((string) ($immunization['expiry_date'] ?? $immunization['expirationDate'] ?? '')); if ($expiryDate !== '' && strtotime($expiryDate) !== false) { $resource['expirationDate'] = date('Y-m-d', strtotime($expiryDate)); } $site = $this->resolveSiteCoding((string) ($immunization['site_code'] ?? ''), (string) ($immunization['site_name'] ?? '')); if (! empty($site)) { $resource['site'] = $site; } $route = $this->resolveRouteCoding((string) ($immunization['route_code'] ?? ''), (string) ($immunization['route_name'] ?? '')); if (! empty($route)) { $resource['route'] = $route; } if ($practitionerRef !== '') { $resource['performer'] = [[ 'function' => [ 'coding' => [[ 'system' => 'http://terminology.hl7.org/CodeSystem/v2-0443', 'code' => 'AP', 'display' => 'Administering Provider', ]], 'text' => 'Administering provider', ], 'actor' => ['reference' => $practitionerRef, 'display' => $practitionerName ?? trim((string) ($practitioner['name'] ?? 'Doctor'))], ]]; } elseif ($organizationRef !== '') { $resource['performer'] = [[ 'function' => [ 'text' => 'Administering organization', ], 'actor' => ['reference' => $organizationRef, 'display' => trim((string) ($organization['name'] ?? ''))], ]]; } $protocol = []; $series = trim((string) ($immunization['series_name'] ?? '')); if ($series === '') { $series = $vaccineName; } $protocol['series'] = $series; $dose = $this->buildDoseNumberElement((string) ($immunization['dose_number'] ?? '1')); if (! empty($dose)) { $protocol += $dose; } $seriesDoses = $this->buildDoseNumberElement((string) ($immunization['series_doses'] ?? ''), 'seriesDoses'); if (! empty($seriesDoses)) { $protocol += $seriesDoses; } $targetDisease = $this->resolveTargetDiseaseCoding( (string) ($immunization['target_disease_code'] ?? ''), (string) ($immunization['target_disease_name'] ?? ''), $vaccineName ); if (! empty($targetDisease)) { $protocol['targetDisease'] = [$targetDisease]; } if ($organizationRef !== '') { $protocol['authority'] = ['reference' => $organizationRef]; } $resource['protocolApplied'] = [$protocol]; $note = trim((string) ($immunization['notes'] ?? '')); if ($note !== '') { $resource['note'] = [['text' => $note]]; } $immunizationRefs[] = ['reference' => $immunizationRef, 'type' => 'Immunization']; $immunizationEntries[] = ['fullUrl' => $immunizationRef, 'resource' => $resource]; } // --- Build ImmunizationRecommendation resource (ABDM Section 3, slice max: 1) --- $recommendationRefs = []; $recommendationEntries = []; $recItems = []; foreach ($recommendations as $rec) { $recVaccineName = trim((string) ($rec['vaccine_name'] ?? $rec['vaccine_display'] ?? '')); if ($recVaccineName === '') { continue; } $recCoding = $this->resolveVaccineStandardCoding( (string) ($rec['vaccine_code_system'] ?? ''), (string) ($rec['vaccine_code'] ?? ''), $recVaccineName ); $dueDate = trim((string) ($rec['due_date'] ?? '')); if ($dueDate !== '') { $dueDate = $this->normalizeIsoDateTime($dueDate, ''); } $recDoseNum = (int) ($rec['dose_number'] ?? 0); $recSeriesDoses = (int) ($rec['series_doses'] ?? 0); $recSeries = trim((string) ($rec['series_name'] ?? $recVaccineName)); $recItem = [ 'vaccineCode' => [$recCoding], 'forecastStatus' => [ 'coding' => [[ 'system' => 'http://terminology.hl7.org/CodeSystem/immunization-recommendation-status', 'code' => 'due', 'display' => 'Due', ]], ], ]; if ($dueDate !== '') { $recItem['dateCriterion'] = [[ 'code' => [ 'coding' => [[ 'system' => 'http://loinc.org', 'code' => '30980-7', 'display' => 'Date vaccine due', ]], ], 'value' => $dueDate, ]]; } if ($recDoseNum > 0) { $recItem['doseNumberPositiveInt'] = $recDoseNum; $recItem['description'] = 'Dose ' . $recDoseNum . ' in ' . $recSeries; } if ($recSeriesDoses > 0) { $recItem['seriesDosesPositiveInt'] = $recSeriesDoses; } if ($recSeries !== '') { $recItem['series'] = $recSeries; } if ($firstImmunizationRef !== '') { $recItem['supportingImmunization'] = [[ 'reference' => $firstImmunizationRef, 'display' => 'Immunization', ]]; } $recItems[] = $recItem; } if ($recItems !== []) { $recUuid = $this->generateUuid(); $recRef = 'urn:uuid:' . $recUuid; $recResource = [ 'resourceType' => 'ImmunizationRecommendation', 'id' => $recUuid, 'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/ImmunizationRecommendation']], 'patient' => ['reference' => $patientRef, 'display' => 'Patient'], 'date' => $issuedAt, 'recommendation' => $recItems, ]; if ($organizationRef !== '') { $recResource['authority'] = ['reference' => $organizationRef]; } $recommendationRefs[] = [ 'reference' => $recRef, 'type' => 'ImmunizationRecommendation', ]; $recommendationEntries[] = ['fullUrl' => $recRef, 'resource' => $recResource]; } $allSectionEntries = array_merge($immunizationRefs, $recommendationRefs); $patientDisplayName = trim((string) ($patient['name'] ?? '')); if ($patientDisplayName === '') { $patientDisplayName = 'Patient'; } $composition = [ 'resourceType' => 'Composition', 'id' => $compositionUuid, 'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/ImmunizationRecord']], 'language' => 'en-IN', 'text' => [ 'status' => 'generated', 'div' => '
Immunization record
', ], 'identifier' => [ 'system' => 'https://ndhm.in/phr', 'value' => $compositionUuid, ], 'status' => 'final', 'type' => [ 'coding' => [[ 'system' => 'http://snomed.info/sct', 'code' => '41000179103', 'display' => 'Immunization record', ]], 'text' => 'Immunization record', ], 'subject' => ['reference' => $patientRef, 'display' => $patientDisplayName], 'encounter' => ['reference' => $encounterRef, 'display' => 'Encounter'], 'date' => $issuedAt, 'title' => 'Immunization record', 'section' => [[ 'title' => 'Immunization record', 'code' => ['coding' => [[ 'system' => 'http://snomed.info/sct', 'code' => '41000179103', 'display' => 'Immunization record', ]]], 'entry' => $allSectionEntries, ]], ]; if ($practitionerRef !== '') { $composition['author'] = [[ 'reference' => $practitionerRef, 'display' => $practitionerName ?? trim((string) ($practitioner['name'] ?? 'Doctor')), ]]; } elseif ($organizationRef !== '') { $composition['author'] = [[ 'reference' => $organizationRef, 'display' => trim((string) ($organization['name'] ?? '')), ]]; } if ($organizationRef !== '') { $composition['custodian'] = [ 'reference' => $organizationRef, 'display' => trim((string) ($organization['name'] ?? '')), ]; } $hfrId = trim((string) ($organization['hfr_id'] ?? $organization['id'] ?? '')); // Order entries in canonical NRCES order: // 1. Composition // 2. Practitioner (if present) // 3. Organization (if present) // 4. Patient // 5. Encounter // 6. Immunization(s) // 7. ImmunizationRecommendation(s) $bundleEntries = [ ['fullUrl' => 'urn:uuid:' . $compositionUuid, 'resource' => $composition], ]; if ($practitionerEntry !== null) { $bundleEntries[] = $practitionerEntry; } if ($organizationEntry !== null) { $bundleEntries[] = $organizationEntry; } $bundleEntries[] = $patientEntry; $bundleEntries[] = $encounterEntry; foreach ($immunizationEntries as $entry) { $bundleEntries[] = $entry; } foreach ($recommendationEntries as $entry) { $bundleEntries[] = $entry; } return $this->sanitizeBundle([ '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' => $hfrId !== '' ? ('https://' . $hfrId . '.hfr.abdm.gov.in') : 'urn:ietf:rfc:3986', 'value' => $careContextReference, ], 'type' => 'document', 'timestamp' => $issuedAt, 'entry' => $bundleEntries, ]); } private function normalizeImmunizationStatus(string $status): string { $status = strtolower(trim($status)); return match ($status) { 'completed', 'done', 'given' => 'completed', 'entered-in-error', 'entered_in_error', 'error' => 'entered-in-error', default => 'not-done', }; } /** @return array{0:string,1:string} */ private function normalizeVaccineCoding(string $system, string $code): array { $system = trim($system); $code = trim($code); if ($system !== 'https://hms.local/immunization/uip') { return [$system, $code]; } $cvxCodes = [ 'UIP-BCG' => '19', 'UIP-OPV' => '02', 'UIP-HEPB' => '45', 'UIP-ROTA' => '122', 'UIP-FIPV' => '10', 'UIP-PENTA' => '198', 'UIP-PENTAVALENT' => '198', 'UIP-PCV' => '152', 'UIP-MR' => '04', 'UIP-JE' => '39', 'UIP-DPT' => '01', 'UIP-TD' => '09', 'UIP-TT' => '09', ]; if (! isset($cvxCodes[$code])) { return [$system, $code]; } return ['http://hl7.org/fhir/sid/cvx', $cvxCodes[$code]]; } /** * @return array */ private function resolveVaccineStandardCoding(string $system, string $code, string $name): array { $concept = []; $name = trim($name); if ($name !== '') { $concept['text'] = $name; } [$normSystem, $normCode] = $this->normalizeVaccineCoding($system, $code); if ($normSystem !== '' && $normCode !== '' && $normSystem !== 'https://hms.local/immunization/uip') { $display = $name; if ($normSystem === 'http://hl7.org/fhir/sid/cvx') { $display = match ($normCode) { '19' => 'BCG', '02' => 'OPV', '45' => 'Hep B', '122' => 'rotavirus, unspecified', '10' => 'IPV', '198' => 'Pentavalent', '152' => 'Pneumococcal Conjugate', '04' => 'M/R', '39' => 'Japanese Encephalitis', '01' => 'DTP', '09' => 'Td', default => $name, }; } $concept['coding'] = [[ 'system' => $normSystem, 'code' => $normCode, 'display' => $display, ]]; return $concept; } $lookupKey = strtoupper(trim($code !== '' ? $code : $name)); $knownCodes = [ 'BCG' => ['system' => 'http://hl7.org/fhir/sid/cvx', 'code' => '19', 'display' => 'BCG'], 'OPV' => ['system' => 'http://hl7.org/fhir/sid/cvx', 'code' => '02', 'display' => 'OPV'], 'HEPATITIS B' => ['system' => 'http://hl7.org/fhir/sid/cvx', 'code' => '45', 'display' => 'Hep B'], 'HEP B' => ['system' => 'http://hl7.org/fhir/sid/cvx', 'code' => '45', 'display' => 'Hep B'], 'HEPB' => ['system' => 'http://hl7.org/fhir/sid/cvx', 'code' => '45', 'display' => 'Hep B'], 'ROTAVIRUS' => ['system' => 'http://hl7.org/fhir/sid/cvx', 'code' => '122', 'display' => 'rotavirus, unspecified'], 'ROTA' => ['system' => 'http://hl7.org/fhir/sid/cvx', 'code' => '122', 'display' => 'rotavirus, unspecified'], 'FRACTIONAL IPV' => ['system' => 'http://hl7.org/fhir/sid/cvx', 'code' => '10', 'display' => 'IPV'], 'FIPV' => ['system' => 'http://hl7.org/fhir/sid/cvx', 'code' => '10', 'display' => 'IPV'], 'IPV' => ['system' => 'http://hl7.org/fhir/sid/cvx', 'code' => '10', 'display' => 'IPV'], 'PENTAVALENT' => ['system' => 'http://hl7.org/fhir/sid/cvx', 'code' => '198', 'display' => 'Pentavalent'], 'PENTA' => ['system' => 'http://hl7.org/fhir/sid/cvx', 'code' => '198', 'display' => 'Pentavalent'], 'PCV' => ['system' => 'http://hl7.org/fhir/sid/cvx', 'code' => '152', 'display' => 'Pneumococcal Conjugate'], 'MR' => ['system' => 'http://hl7.org/fhir/sid/cvx', 'code' => '04', 'display' => 'M/R'], 'MEASLES' => ['system' => 'http://hl7.org/fhir/sid/cvx', 'code' => '05', 'display' => 'measles'], 'JE' => ['system' => 'http://hl7.org/fhir/sid/cvx', 'code' => '39', 'display' => 'Japanese Encephalitis'], 'DPT' => ['system' => 'http://hl7.org/fhir/sid/cvx', 'code' => '01', 'display' => 'DTP'], 'TD' => ['system' => 'http://hl7.org/fhir/sid/cvx', 'code' => '09', 'display' => 'Td'], 'TT' => ['system' => 'http://hl7.org/fhir/sid/cvx', 'code' => '09', 'display' => 'tetanus toxoid'], 'COVID' => ['system' => 'http://snomed.info/sct', 'code' => '1119305005', 'display' => 'COVID-19 vaccine'], ]; foreach ($knownCodes as $prefix => $target) { if (str_contains($lookupKey, $prefix)) { $concept['coding'] = [$target]; return $concept; } } if ($normSystem !== '' && $normCode !== '') { $concept['coding'] = [[ 'system' => $normSystem, 'code' => $normCode, 'display' => $name, ]]; return $concept; } if (empty($concept['coding']) && $name !== '') { $concept['coding'] = [[ 'system' => 'http://snomed.info/sct', 'code' => '774203006', 'display' => $name, ]]; } return $concept; } /** * @return array */ private function resolveSiteCoding(string $code, string $name): array { $code = trim($code); $name = trim($name); if ($code === '' && $name === '') { return []; } $search = strtolower($code . ' ' . $name); $snomedCode = ''; $display = ''; if (str_contains($search, 'right upper arm') || str_contains($search, 'right arm') || str_contains($search, 'right deltoid')) { $snomedCode = '368209003'; $display = 'Right upper arm structure'; } elseif (str_contains($search, 'left upper arm') || str_contains($search, 'left arm') || str_contains($search, 'left deltoid')) { $snomedCode = '368208006'; $display = 'Left upper arm structure'; } elseif (str_contains($search, 'left thigh') || str_contains($search, 'anterolateral aspect of left thigh')) { $snomedCode = '61396006'; $display = 'Left thigh structure'; } elseif (str_contains($search, 'right thigh') || str_contains($search, 'anterolateral aspect of right thigh')) { $snomedCode = '11207009'; $display = 'Right thigh structure'; } elseif (str_contains($search, 'mouth') || str_contains($search, 'oral')) { $snomedCode = '123851003'; $display = 'Mouth region structure'; } elseif ($code !== '' && preg_match('/^\d+$/', $code) === 1) { $snomedCode = $code; $display = $name !== '' ? $name : 'Anatomical site'; } $res = []; if ($name !== '') { $res['text'] = $name; } if ($snomedCode !== '') { $res['coding'] = [[ 'system' => 'http://snomed.info/sct', 'code' => $snomedCode, 'display' => $display, ]]; if (empty($res['text'])) { $res['text'] = $display; } } return $res; } /** * @return array */ private function resolveRouteCoding(string $code, string $name): array { $code = trim($code); $name = trim($name); if ($code === '' && $name === '') { return []; } $search = strtolower($code . ' ' . $name); $snomedCode = ''; $display = ''; if (str_contains($search, 'intradermal') || preg_match('/\b(id)\b/i', $search) === 1) { $snomedCode = '372464004'; $display = 'Intradermal use'; } elseif (str_contains($search, 'intramuscular') || preg_match('/\b(im)\b/i', $search) === 1) { $snomedCode = '78421000'; $display = 'Intramuscular route'; } elseif (str_contains($search, 'subcutaneous') || preg_match('/\b(sc|subq)\b/i', $search) === 1) { $snomedCode = '34206005'; $display = 'Subcutaneous route'; } elseif (str_contains($search, 'oral') || preg_match('/\b(po)\b/i', $search) === 1) { $snomedCode = '260548002'; $display = 'Oral route'; } elseif (str_contains($search, 'intranasal') || str_contains($search, 'nasal')) { $snomedCode = '46713006'; $display = 'Nasal route'; } elseif ($code !== '' && preg_match('/^\d+$/', $code) === 1) { $snomedCode = $code; $display = $name !== '' ? $name : 'Route'; } $res = []; if ($name !== '') { $res['text'] = $name; } if ($snomedCode !== '') { $res['coding'] = [[ 'system' => 'http://snomed.info/sct', 'code' => $snomedCode, 'display' => $display, ]]; if (empty($res['text'])) { $res['text'] = $display; } } return $res; } /** * @return array */ private function resolveTargetDiseaseCoding(string $code, string $name, string $vaccineName): array { $code = trim($code); $name = trim($name); $vaccineName = trim($vaccineName); $search = strtolower($code . ' ' . $name . ' ' . $vaccineName); $snomedCode = ''; $display = ''; if ($code !== '' && preg_match('/^\d+$/', $code) === 1) { $snomedCode = $code; $display = $name !== '' ? $name : 'Target Disease'; } elseif (str_contains($search, 'polio') || str_contains($search, 'fipv') || str_contains($search, 'ipv') || str_contains($search, 'opv')) { $snomedCode = '398102009'; $display = 'Poliomyelitis'; } elseif (str_contains($search, 'tuberculosis') || str_contains($search, 'bcg')) { $snomedCode = '56717001'; $display = 'Tuberculosis'; } elseif (str_contains($search, 'hepb') || str_contains($search, 'hepatitis b')) { $snomedCode = '66071002'; $display = 'Type B viral hepatitis'; } elseif (str_contains($search, 'rotavirus') || str_contains($search, 'rota')) { $snomedCode = '186358004'; $display = 'Rotavirus infection'; } elseif (str_contains($search, 'diphtheria')) { $snomedCode = '397430003'; $display = 'Diphtheria'; } elseif (str_contains($search, 'tetanus') || str_contains($search, 'tt') || str_contains($search, 'td')) { $snomedCode = '76902006'; $display = 'Tetanus'; } elseif (str_contains($search, 'pertussis') || str_contains($search, 'whooping')) { $snomedCode = '27836007'; $display = 'Pertussis'; } elseif (str_contains($search, 'measles')) { $snomedCode = '14189004'; $display = 'Measles'; } elseif (str_contains($search, 'rubella')) { $snomedCode = '36653000'; $display = 'Rubella'; } elseif (str_contains($search, 'japanese encephalitis') || str_contains($search, 'je')) { $snomedCode = '52947006'; $display = 'Japanese encephalitis'; } elseif (str_contains($search, 'pneumococcal') || str_contains($search, 'pcv')) { $snomedCode = '16814004'; $display = 'Pneumococcal infectious disease'; } $res = []; $text = $name !== '' ? $name : $display; if ($text !== '') { $res['text'] = $text; } if ($snomedCode !== '') { $res['coding'] = [[ 'system' => 'http://snomed.info/sct', 'code' => $snomedCode, 'display' => $display !== '' ? $display : $text, ]]; } return $res; } /** * @return array */ private function buildSimpleCodeableConcept(string $system, string $code, string $display, string $text): array { $concept = []; $text = trim($text); if ($text !== '') { $concept['text'] = $text; } $system = trim($system); $code = trim($code); $display = trim($display); if ($system !== '' && $code !== '' && $display !== '') { $concept['coding'] = [[ 'system' => $system, 'code' => $code, 'display' => $display, ]]; } return $concept; } /** * @return array */ private function buildDoseNumberElement(string $value, string $fieldPrefix = 'doseNumber'): array { $value = trim($value); if ($value === '') { return []; } if (preg_match('/^[1-9]\d*$/', $value) === 1) { return [$fieldPrefix . 'PositiveInt' => (int) $value]; } return [$fieldPrefix . 'String' => $value]; } /** 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)); } /** @param array $bundle * @return array */ private function sanitizeBundle(array $bundle): array { return $this->sanitizeFhirValue($bundle); } /** @param mixed $value * @return mixed */ private function sanitizeFhirValue($value) { if (is_string($value)) { $trimmed = trim($value); if (in_array(strtoupper($trimmed), ['NA', 'N/A', 'NULL', 'NOT AVAILABLE'], true)) { return null; } if (preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}(:\d{2})?$/', $trimmed) === 1) { return $this->normalizeIsoDateTime($trimmed); } return $value; } if (! is_array($value)) { return $value; } $clean = []; foreach ($value as $key => $nested) { $sanitized = $this->sanitizeFhirValue($nested); if ($sanitized === null || $sanitized === '' || $sanitized === []) { continue; } $clean[$key] = $sanitized; } return $clean; } /** 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'); } /** Excludes blank, 0, or NA placeholder values. */ public function isMeaningfulValue(string $value): bool { $v = strtoupper(trim(strip_tags($value))); if ($v === '') { return false; } return ! in_array($v, ['0', 'NA', 'N/A', 'N / A', 'N/ A', 'NONE', 'NIL', 'NULL', 'UNDEFINED', 'UNSPECIFIED'], true); } /** * Normalize any datetime-like string into ISO 8601 with IST offset. */ private function normalizeIsoDateTime(string $value, string $fallback = ''): string { $raw = trim($value); if ($raw === '') { return trim($fallback); } try { $hasTimezone = preg_match('/([+-]\d{2}:\d{2}|Z)$/', $raw) === 1; $dt = $hasTimezone && strpos($raw, 'T') !== false ? new \DateTime($raw) : new \DateTime($raw, new \DateTimeZone('Asia/Kolkata')); return $dt->setTimezone(new \DateTimeZone('Asia/Kolkata'))->format('Y-m-d\TH:i:sP'); } catch (\Throwable $e) { return trim($fallback); } } /** * Build NRCES-compliant Organization resource with MustSupport telecom and address. * * @param array $organization * @return array */ private function buildOrganizationResource(array $organization, string $organizationUuid): array { $hfrId = trim((string) ($organization['hfr_id'] ?? $organization['id'] ?? '')); $name = trim((string) ($organization['name'] ?? '')); $resource = [ 'resourceType' => 'Organization', 'id' => $organizationUuid, 'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/Organization']], 'name' => $name, ]; if ($hfrId !== '') { $resource['identifier'] = [[ 'type' => ['coding' => [[ 'system' => 'http://terminology.hl7.org/CodeSystem/v2-0203', 'code' => 'PRN', 'display' => 'Provider number', ]]], 'system' => 'https://facility.ndhm.gov.in', 'value' => $hfrId, ]]; } $telecom = []; if (isset($organization['telecom']) && is_array($organization['telecom'])) { $telecom = $organization['telecom']; } else { $phone = trim((string) ($organization['phone'] ?? $organization['phone_no'] ?? '')); if ($phone !== '') { $telecom[] = [ 'system' => 'phone', 'value' => $phone, 'use' => 'work', ]; } $email = trim((string) ($organization['email'] ?? '')); if ($email !== '') { $telecom[] = [ 'system' => 'email', 'value' => $email, 'use' => 'work', ]; } } if (! empty($telecom)) { $resource['telecom'] = $telecom; } if (isset($organization['address']) && is_array($organization['address'])) { $resource['address'] = $organization['address']; } else { $addr = $this->buildOrganizationAddress($organization); if (! empty($addr)) { $resource['address'] = [$addr]; } } return $resource; } /** * Construct structured Organization address from database hospital_setting. * * @param array $organization * @return array */ private function buildOrganizationAddress(array $organization): array { $addressText = trim((string) ($organization['address'] ?? '')); $line1 = trim((string) ($organization['address_1'] ?? $organization['line1'] ?? '')); $line2 = trim((string) ($organization['address_2'] ?? $organization['line2'] ?? '')); $city = trim((string) ($organization['city'] ?? '')); $state = trim((string) ($organization['state'] ?? '')); $pincode = trim((string) ($organization['pincode'] ?? $organization['postalCode'] ?? $organization['pin'] ?? '')); if ($addressText !== '' && ($city === '' || $state === '' || $pincode === '')) { if ($pincode === '' && preg_match('/\b(\d{6})\b/', $addressText, $pm)) { $pincode = $pm[1]; } if ($state === '' && preg_match('/\b(Uttarakhand|Uttar Pradesh|Delhi|Haryana|Punjab|Maharashtra|Karnataka|Tamil Nadu|Gujarat|Rajasthan|Bihar|West Bengal|Madhya Pradesh|Kerala|Odisha|Telangana|Andhra Pradesh|Assam|Jharkhand)\b/i', $addressText, $sm)) { $state = $sm[1]; } if ($city === '' && preg_match('/(?:,\s*|\b)([A-Z][a-zA-Z\s]+?)(?:\s*-\s*\d{6}|\s*,\s*(?:Uttarakhand|Uttar Pradesh|Delhi|Haryana|Punjab|India))/i', $addressText, $cm)) { $city = trim($cm[1]); } } $lines = []; if ($line1 !== '') { $lines[] = $line1; } if ($line2 !== '' && ($line1 === '' || ($city === '' && $state === ''))) { $lines[] = $line2; } if (empty($lines) && $addressText === '' && $city === '' && $state === '' && $pincode === '') { return []; } $address = [ 'line' => ! empty($lines) ? $lines : [$addressText !== '' ? $addressText : 'Hospital Facility'], ]; if ($city !== '') { $address['city'] = $city; } if ($state !== '') { $address['state'] = $state; } if ($pincode !== '') { $address['postalCode'] = $pincode; } $address['country'] = 'IND'; $address['text'] = $addressText !== '' ? $addressText : implode(', ', array_filter([implode(', ', $lines), $city, $state, $pincode, 'IND'])); return $address; } /** * @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], ]; $gender = $this->normalizeAdministrativeGender($patient['gender'] ?? null); if ($gender !== '') { $resource['gender'] = $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, ]]; } $uhid = trim((string) ($patient['uhid'] ?? $patient['patient_code'] ?? $patient['p_code'] ?? $patient['hospital_patient_id'] ?? '')); if ($this->isMeaningfulValue($uhid)) { if (! isset($resource['identifier']) || ! is_array($resource['identifier'])) { $resource['identifier'] = []; } $resource['identifier'][] = [ 'type' => ['coding' => [[ 'system' => 'http://terminology.hl7.org/CodeSystem/v2-0203', 'code' => 'MR', 'display' => 'Medical record number', ]], 'text' => 'UHID'], 'system' => 'https://hms.local/uhid', 'value' => $uhid, ]; } // Phone / mobile telecom $phone = trim((string) ($patient['phone'] ?? '')); if ($phone !== '') { $resource['telecom'] = [[ 'system' => 'phone', 'value' => $phone, 'use' => 'mobile', ]]; } return $resource; } /** * Convert legacy/local gender values to FHIR AdministrativeGender. */ private function normalizeAdministrativeGender($value): string { $raw = strtolower(trim((string) $value)); if ($raw === '') { return ''; } return match ($raw) { 'm', 'male', '1' => 'male', 'f', 'female', '2' => 'female', 'o', 'other', '3', 'transgender', 'trans' => 'other', 'u', 'unknown', '0', 'na', 'n/a', 'not known' => 'unknown', default => in_array($raw, ['male', 'female', 'other', 'unknown'], true) ? $raw : 'unknown', }; } /** * 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)) { $num = (float) $value; if ($loincCode === '8310-5' || strcasecmp($display, 'Body temperature') === 0 || in_array($unit, ['Cel', 'degF', '[degF]'], true)) { $num = round($num, 1); } elseif ($loincCode === '39156-5' || strcasecmp($display, 'Body mass index') === 0) { $num = round($num, 1); } elseif ($loincCode === '29463-7' || strcasecmp($display, 'Body weight') === 0) { $num = round($num, 2); } elseif ($loincCode === '8302-2' || strcasecmp($display, 'Body height') === 0) { $num = round($num, 1); } elseif (in_array($loincCode, ['8867-4', '59408-5', '8480-6', '8462-4', '9279-1'], true)) { $num = (float) round($num); } else { $num = round($num, 2); } $obsResource['valueQuantity'] = [ 'value' => $num, '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]]; } // ABDM requires the Composition to be the first bundle entry. array_unshift($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) : ''; } /** * Build FHIR Timing.repeat & timing code per ABDM specification * * @return array{timing: array|null, asNeeded: bool} */ private function buildFhirTimingStructure(string $freqText, string $dosageText): array { $text = strtoupper(trim($freqText !== '' ? $freqText : $dosageText)); if ($text === '') { return ['timing' => null, 'asNeeded' => false]; } if (str_contains($text, 'SOS') || str_contains($text, 'AS NEEDED') || str_contains($text, 'PRN')) { return [ 'timing' => [ 'code' => [ 'coding' => [[ 'system' => 'http://terminology.hl7.org/CodeSystem/v3-GTSAbbreviation', 'code' => 'PRN', 'display' => 'As needed', ]], 'text' => 'SOS', ], ], 'asNeeded' => true, ]; } $repeat = null; $gtsCode = null; $gtsDisplay = null; if (preg_match('/\b(OD|ONCE\s*DAILY|DAILY)\b/', $text) === 1) { $repeat = ['frequency' => 1, 'period' => 1, 'periodUnit' => 'd']; $gtsCode = 'QD'; $gtsDisplay = 'Once daily'; } elseif (preg_match('/\b(BD|BID|TWICE)\b/', $text) === 1) { $repeat = ['frequency' => 2, 'period' => 1, 'periodUnit' => 'd']; $gtsCode = 'BID'; $gtsDisplay = 'Twice daily'; } elseif (preg_match('/\b(TDS|TID|THRICE|3\s*TIMES)\b/', $text) === 1) { $repeat = ['frequency' => 3, 'period' => 1, 'periodUnit' => 'd']; $gtsCode = 'TID'; $gtsDisplay = 'Thrice daily'; } elseif (preg_match('/\b(QID|4\s*TIMES)\b/', $text) === 1) { $repeat = ['frequency' => 4, 'period' => 1, 'periodUnit' => 'd']; $gtsCode = 'QID'; $gtsDisplay = 'Four times daily'; } elseif (preg_match('/\b(HS|BEDTIME|NIGHT)\b/', $text) === 1) { $repeat = ['frequency' => 1, 'period' => 1, 'periodUnit' => 'd']; $gtsCode = 'HS'; $gtsDisplay = 'At bedtime'; } elseif (preg_match('/Q(\d+)H/', $text, $m) === 1 || preg_match('/EVERY\s*(\d+)\s*HOUR/', $text, $m) === 1) { $hours = (int) ($m[1] ?? 6); $repeat = ['frequency' => 1, 'period' => $hours, 'periodUnit' => 'h']; } elseif (str_contains($text, 'ALTERNATE') || str_contains($text, 'QOD')) { $repeat = ['frequency' => 1, 'period' => 2, 'periodUnit' => 'd']; $gtsCode = 'QOD'; $gtsDisplay = 'Every other day'; } elseif (str_contains($text, 'WEEKLY')) { $repeat = ['frequency' => 1, 'period' => 1, 'periodUnit' => 'wk']; } elseif (str_contains($text, 'MONTHLY')) { $repeat = ['frequency' => 1, 'period' => 1, 'periodUnit' => 'mo']; } elseif (str_contains($text, 'STAT')) { $repeat = ['frequency' => 1, 'period' => 1, 'periodUnit' => 'd']; } $res = []; if ($repeat !== null) { $res['repeat'] = $repeat; } if ($gtsCode !== null) { $res['code'] = [ 'coding' => [[ 'system' => 'http://terminology.hl7.org/CodeSystem/v3-GTSAbbreviation', 'code' => $gtsCode, 'display' => $gtsDisplay, ]], 'text' => trim($freqText !== '' ? $freqText : $text), ]; } elseif ($freqText !== '') { $res['code'] = ['text' => trim($freqText)]; } return [ 'timing' => ! empty($res) ? $res : null, 'asNeeded' => false, ]; } /** * Build ABDM FHIR R4 MedicationDispense Document Bundle * * @param array $patient * @param array $store * @param array $sale * @param array> $items * @return array */ public function buildPharmacyDispenseBundle(array $patient, array $store, array $sale, array $items): array { $issuedAt = $this->isoTimestamp(); $bundleUuid = $this->generateUuid(); $compositionUuid = $this->generateUuid(); $patientUuid = $this->generateUuid(); $practitionerUuid = $this->generateUuid(); $organizationUuid = $this->generateUuid(); $patientRef = 'urn:uuid:' . $patientUuid; $practitionerRef = 'urn:uuid:' . $practitionerUuid; $organizationRef = 'urn:uuid:' . $organizationUuid; $resourceEntries = []; // 1. Practitioner (Pharmacist or Prescribing Doctor) $practName = trim((string)(($sale['doctor_name'] ?? '') ?: (($store['registered_pharmacist_name'] ?? '') ?: 'Pharmacist'))); $practRegNo = trim((string)(($sale['doctor_reg_no'] ?? '') ?: (($store['pharmacist_reg_no'] ?? '') ?: ''))); $practHprId = trim((string)($store['pharmacist_hpr_id'] ?? '')); $practIdentifiers = []; if ($practHprId !== '') { $practIdentifiers[] = [ 'type' => ['coding' => [['system' => 'http://terminology.hl7.org/CodeSystem/v2-0203', 'code' => 'PRN', 'display' => 'Provider number']]], 'system' => 'https://hpr.abdm.gov.in', 'value' => $practHprId ]; } if ($practRegNo !== '') { $practIdentifiers[] = [ 'type' => ['coding' => [['system' => 'http://terminology.hl7.org/CodeSystem/v2-0203', 'code' => 'MD', 'display' => 'Medical License number']]], 'system' => 'https://doctor.ndhm.gov.in', 'value' => $practRegNo ]; } $resourceEntries[] = [ 'fullUrl' => $practitionerRef, 'resource' => [ 'resourceType' => 'Practitioner', 'id' => $practitionerUuid, 'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/Practitioner']], 'identifier' => $practIdentifiers, 'name' => [['text' => $practName]] ] ]; // 2. Organization (Hospital Pharmacy / Store) $hfrId = trim((string)(($store['abdm_hfr_id'] ?? '') ?: (($store['store_code'] ?? '') ?: 'IN0710001234'))); $resourceEntries[] = [ 'fullUrl' => $organizationRef, 'resource' => [ 'resourceType' => 'Organization', 'id' => $organizationUuid, 'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/Organization']], 'identifier' => [[ 'type' => ['coding' => [['system' => 'http://terminology.hl7.org/CodeSystem/v2-0203', 'code' => 'PRN', 'display' => 'Provider number']]], 'system' => 'https://facility.ndhm.gov.in', 'value' => $hfrId ]], 'name' => (string)(($store['store_name'] ?? '') ?: 'Hospital Medical Store'), 'telecom' => [ ['system' => 'phone', 'value' => (string)(($store['contact_phone'] ?? '') ?: '+91 9999999999')], ['system' => 'email', 'value' => (string)(($store['contact_email'] ?? '') ?: 'pharmacy@hospital.in')] ], 'address' => [[ 'line' => [(string)(($store['address'] ?? '') ?: 'Hospital Building')], 'city' => (string)(($store['state_name'] ?? '') ?: 'Delhi'), 'state' => (string)(($store['state_name'] ?? '') ?: 'Delhi'), 'postalCode' => '110001', 'country' => 'IN' ]] ] ]; // 3. Patient $patientResource = $this->buildPatientResource($patient); $patientResource['id'] = $patientUuid; // Inject ABHA identifiers if provided in sale or patient $abhaNumber = trim((string)(($sale['abha_id'] ?? '') ?: ($patient['abha_id'] ?? ''))); $abhaAddress = trim((string)(($sale['abha_address'] ?? '') ?: ($patient['abha_address'] ?? ''))); if (!isset($patientResource['identifier'])) { $patientResource['identifier'] = []; } if ($abhaNumber !== '') { $patientResource['identifier'][] = [ 'type' => ['coding' => [['system' => 'https://nrces.in/ndhm/fhir/r4/CodeSystem/ndhm-identifier-type-code', 'code' => 'ABHA', 'display' => 'Ayushman Bharat Health Account']]], 'system' => 'https://healthid.ndhm.gov.in', 'value' => $abhaNumber ]; } if ($abhaAddress !== '') { $patientResource['identifier'][] = [ 'type' => ['coding' => [['system' => 'https://nrces.in/ndhm/fhir/r4/CodeSystem/ndhm-identifier-type-code', 'code' => 'ABHA-ADDRESS', 'display' => 'ABHA Address']]], 'system' => 'https://abdm.gov.in', 'value' => $abhaAddress ]; } $resourceEntries[] = ['fullUrl' => $patientRef, 'resource' => $patientResource]; // 4. MedicationDispense Resources $medicationRefs = []; foreach ($items as $idx => $it) { $medUuid = $this->generateUuid(); $medRef = 'urn:uuid:' . $medUuid; $medicationRefs[] = ['reference' => $medRef]; $snomedCode = trim((string)($it['snomed_ct_code'] ?? '')); $snomedDisplay = trim((string)($it['snomed_display'] ?? ($it['item_name'] ?? 'Medicine'))); $coding = []; if ($snomedCode !== '') { $coding[] = [ 'system' => 'http://snomed.info/sct', 'code' => $snomedCode, 'display' => $snomedDisplay ]; } $medResource = [ 'resourceType' => 'MedicationDispense', 'id' => $medUuid, 'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/MedicationDispense']], 'status' => 'completed', 'medicationCodeableConcept' => [ 'coding' => $coding, 'text' => (string)($it['item_name'] ?? 'Medicine') ], 'subject' => ['reference' => $patientRef, 'display' => (string)(($sale['patient_name'] ?? '') ?: 'Patient')], 'performer' => [[ 'actor' => ['reference' => $practitionerRef, 'display' => $practName] ]], 'location' => ['reference' => $organizationRef, 'display' => (string)(($store['store_name'] ?? '') ?: 'Pharmacy')], 'quantity' => [ 'value' => (int)($it['qty'] ?? 1), 'unit' => (string)($it['unit_pack'] ?? 'Units'), 'system' => 'http://unitsofmeasure.org', 'code' => '1' ], 'daysSupply' => [ 'value' => 5, 'unit' => 'days', 'system' => 'http://unitsofmeasure.org', 'code' => 'd' ], 'whenPrepared' => $issuedAt, 'whenHandedOver' => $issuedAt, 'dosageInstruction' => [[ 'text' => 'As directed by physician' ]] ]; $resourceEntries[] = ['fullUrl' => $medRef, 'resource' => $medResource]; } // 5. Composition (Record Head) $compositionResource = [ 'resourceType' => 'Composition', 'id' => $compositionUuid, 'meta' => [ 'profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/MedicationDispenseDocument'], 'versionId' => '1', 'lastUpdated' => $issuedAt ], 'language' => 'en-IN', 'identifier' => [ 'system' => 'https://ndhm.in/phr', 'value' => (string)(($sale['abdm_care_context_ref'] ?? '') ?: ('PHARM-' . (($sale['invoice_no'] ?? '') ?: $compositionUuid))) ], 'status' => 'final', 'type' => [ 'coding' => [[ 'system' => 'http://loinc.org', 'code' => '60590-7', 'display' => 'Medication dispensed.extended Document' ]], 'text' => 'Pharmacy Medication Dispense Record' ], 'subject' => ['reference' => $patientRef, 'display' => (string)(($sale['patient_name'] ?? '') ?: 'Patient')], 'date' => $issuedAt, 'author' => [['reference' => $practitionerRef, 'display' => $practName]], 'title' => 'Pharmacy Medication Dispensation Receipt', 'custodian' => ['reference' => $organizationRef, 'display' => (string)(($store['store_name'] ?? '') ?: 'Hospital Pharmacy')], 'section' => [[ 'title' => 'Medications Dispensed', 'code' => [ 'coding' => [[ 'system' => 'http://loinc.org', 'code' => '10160-0', 'display' => 'History of Medication use' ]] ], 'entry' => $medicationRefs ]] ]; array_unshift($resourceEntries, [ 'fullUrl' => 'urn:uuid:' . $compositionUuid, 'resource' => $compositionResource ]); return [ 'resourceType' => 'Bundle', 'id' => $bundleUuid, 'meta' => [ 'versionId' => '1', 'lastUpdated' => $issuedAt, 'profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/DocumentBundle'] ], 'identifier' => [ 'system' => 'https://ndhm.in/phr', 'value' => $bundleUuid ], 'type' => 'document', 'timestamp' => $issuedAt, 'entry' => $resourceEntries ]; } }