diff --git a/app/Libraries/Abdm/Fhir/Generators/AbstractModuleFhirGenerator.php b/app/Libraries/Abdm/Fhir/Generators/AbstractModuleFhirGenerator.php
index 9f84c54..4c6124e 100644
--- a/app/Libraries/Abdm/Fhir/Generators/AbstractModuleFhirGenerator.php
+++ b/app/Libraries/Abdm/Fhir/Generators/AbstractModuleFhirGenerator.php
@@ -57,6 +57,10 @@ abstract class AbstractModuleFhirGenerator
'meta' => ['profile' => [
'https://nrces.in/ndhm/fhir/r4/StructureDefinition/Patient',
]],
+ 'text' => [
+ 'status' => 'generated',
+ 'div' => '
Patient: ' . htmlspecialchars((string) ($source['patient']['name'] ?? 'Patient')) . ' (' . htmlspecialchars(ucfirst($gender)) . ')
',
+ ],
'identifier' => [
[
'system' => 'https://hms.local/patient-id',
@@ -256,12 +260,18 @@ abstract class AbstractModuleFhirGenerator
$locationList[] = $locItem;
}
+ $patientName = trim((string) ($source['patient']['name'] ?? $source['patient_name'] ?? 'Patient'));
+
$encounterResource = [
'resourceType' => 'Encounter',
'id' => 'encounter-' . $id,
'meta' => ['profile' => [
'https://nrces.in/ndhm/fhir/r4/StructureDefinition/Encounter',
]],
+ 'text' => [
+ 'status' => 'generated',
+ 'div' => 'Encounter: ' . htmlspecialchars($classDisplay) . ' ' . htmlspecialchars($id) . '
',
+ ],
'status' => 'finished',
'class' => [
'system' => 'http://terminology.hl7.org/CodeSystem/v3-ActCode',
@@ -270,6 +280,7 @@ abstract class AbstractModuleFhirGenerator
],
'subject' => [
'reference' => 'urn:uuid:patient-' . (string) ($source['patient']['id'] ?? ''),
+ 'display' => $patientName !== '' ? $patientName : 'Patient',
],
'period' => [
'start' => $start,
@@ -316,6 +327,10 @@ abstract class AbstractModuleFhirGenerator
'meta' => ['profile' => [
'https://nrces.in/ndhm/fhir/r4/StructureDefinition/Practitioner',
]],
+ 'text' => [
+ 'status' => 'generated',
+ 'div' => 'Practitioner: ' . htmlspecialchars($name) . '
',
+ ],
'identifier' => [[
'system' => 'https://doctor.ndhm.gov.in',
'value' => $hprId,
@@ -347,6 +362,10 @@ abstract class AbstractModuleFhirGenerator
'meta' => ['profile' => [
'https://nrces.in/ndhm/fhir/r4/StructureDefinition/Organization',
]],
+ 'text' => [
+ 'status' => 'generated',
+ 'div' => 'Organization: ' . htmlspecialchars($name) . '
',
+ ],
'name' => $name,
'identifier' => [[
'system' => 'https://facility.ndhm.gov.in',
diff --git a/app/Libraries/Abdm/Fhir/Generators/DischargeFhirGenerator.php b/app/Libraries/Abdm/Fhir/Generators/DischargeFhirGenerator.php
index 5108e1b..724b680 100644
--- a/app/Libraries/Abdm/Fhir/Generators/DischargeFhirGenerator.php
+++ b/app/Libraries/Abdm/Fhir/Generators/DischargeFhirGenerator.php
@@ -132,6 +132,10 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
'resourceType' => 'Condition',
'id' => $group['prefix'] . $recordId . '-' . $idx,
'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/Condition']],
+ 'text' => [
+ 'status' => 'generated',
+ 'div' => '' . htmlspecialchars($group['category_display']) . ': ' . htmlspecialchars($text) . '
',
+ ],
'clinicalStatus' => ['coding' => [[
'system' => 'http://terminology.hl7.org/CodeSystem/condition-clinical',
'code' => 'active',
@@ -148,7 +152,7 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
'display' => $group['category_display'],
]]]],
'code' => ['coding' => $coding, 'text' => $text],
- 'subject' => ['reference' => $patientRef],
+ 'subject' => ['reference' => $patientRef, 'display' => (string) ($source['patient']['name'] ?? $source['patient_name'] ?? 'Patient')],
'encounter' => $encounterRef ? ['reference' => $encounterRef] : null,
'recordedDate' => (string) ($cond['recorded_at'] ?? $timestamp),
]);
@@ -180,12 +184,16 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
'resourceType' => 'Procedure',
'id' => 'discharge-procedure-' . $recordId . '-' . $idx,
'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/Procedure']],
+ 'text' => [
+ 'status' => 'generated',
+ 'div' => 'Procedure: ' . htmlspecialchars($text) . '
',
+ ],
'status' => 'completed',
'code' => [
'coding' => $coding,
'text' => $text,
],
- 'subject' => ['reference' => $patientRef],
+ 'subject' => ['reference' => $patientRef, 'display' => (string) ($source['patient']['name'] ?? $source['patient_name'] ?? 'Patient')],
'encounter' => $encounterRef ? ['reference' => $encounterRef] : null,
'performedDateTime' => (string) ($proc['performed_at'] ?? $timestamp),
]);
@@ -225,9 +233,13 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
'resourceType' => 'MedicationRequest',
'id' => 'discharge-medication-' . $recordId . '-' . $idx,
'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/MedicationRequest']],
+ 'text' => [
+ 'status' => 'generated',
+ 'div' => 'Medication: ' . htmlspecialchars($name) . ($dosageText !== '' ? (' - ' . htmlspecialchars($dosageText)) : '') . '
',
+ ],
'status' => 'active',
'intent' => 'order',
- 'subject' => ['reference' => $patientRef],
+ 'subject' => ['reference' => $patientRef, 'display' => (string) ($source['patient']['name'] ?? $source['patient_name'] ?? 'Patient')],
'encounter' => $encounterRef ? ['reference' => $encounterRef] : null,
'authoredOn' => (string) ($med['authored_on'] ?? $timestamp),
'requester' => is_array($practitioner) ? [
@@ -288,6 +300,10 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
'resourceType' => 'Observation',
'id' => 'discharge-observation-' . $recordId . '-' . $idx,
'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/Observation']],
+ 'text' => [
+ 'status' => 'generated',
+ 'div' => '' . htmlspecialchars($text) . ': ' . htmlspecialchars($value) . '
',
+ ],
'status' => 'final',
'category' => [[
'text' => $obsCategoryDisplay,
@@ -301,7 +317,7 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
'coding' => $coding,
'text' => $text,
],
- 'subject' => ['reference' => $patientRef],
+ 'subject' => ['reference' => $patientRef, 'display' => (string) ($source['patient']['name'] ?? $source['patient_name'] ?? 'Patient')],
'encounter' => $encounterRef ? ['reference' => $encounterRef] : null,
'effectiveDateTime' => (string) ($obs['effective_at'] ?? $timestamp),
];
@@ -436,12 +452,16 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
'resourceType' => 'Observation',
'id' => $observationId,
'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/Observation']],
+ 'text' => [
+ 'status' => 'generated',
+ 'div' => 'Investigation: ' . htmlspecialchars($text) . ($resultValue !== '' ? (': ' . htmlspecialchars($resultValue)) : '') . '
',
+ ],
'status' => 'final',
'code' => [
'coding' => $coding,
'text' => $text,
],
- 'subject' => ['reference' => $patientRef],
+ 'subject' => ['reference' => $patientRef, 'display' => (string) ($source['patient']['name'] ?? $source['patient_name'] ?? 'Patient')],
'encounter' => $encounterRef ? ['reference' => $encounterRef] : null,
'effectiveDateTime' => (string) ($inv['reported_at'] ?? $inv['authored_on'] ?? $timestamp),
];
@@ -462,6 +482,10 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
'resourceType' => 'DiagnosticReport',
'id' => 'discharge-investigation-' . $recordId . '-' . $idx,
'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/DiagnosticReportLab']],
+ 'text' => [
+ 'status' => 'generated',
+ 'div' => 'Diagnostic Report: ' . htmlspecialchars($text) . '
',
+ ],
'status' => 'final',
'category' => [['coding' => [[
'system' => 'http://snomed.info/sct',
@@ -472,10 +496,10 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
'coding' => $coding,
'text' => $text,
],
- 'subject' => ['reference' => $patientRef],
+ 'subject' => ['reference' => $patientRef, 'display' => (string) ($source['patient']['name'] ?? $source['patient_name'] ?? 'Patient')],
'encounter' => $encounterRef ? ['reference' => $encounterRef] : null,
'issued' => (string) ($inv['reported_at'] ?? $inv['authored_on'] ?? $timestamp),
- 'result' => [['reference' => 'urn:uuid:' . $observationId]],
+ 'result' => [['reference' => 'urn:uuid:' . $observationId, 'display' => $text]],
]);
}
@@ -489,6 +513,10 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
'resourceType' => 'AllergyIntolerance',
'id' => 'discharge-allergy-' . $recordId . '-' . $idx,
'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/AllergyIntolerance']],
+ 'text' => [
+ 'status' => 'generated',
+ 'div' => 'Allergy: ' . htmlspecialchars($text) . '
',
+ ],
'clinicalStatus' => [
'coding' => [[
'system' => 'http://terminology.hl7.org/CodeSystem/allergyintolerance-clinical',
@@ -507,7 +535,7 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
'code' => [
'text' => $text,
],
- 'patient' => ['reference' => $patientRef],
+ 'patient' => ['reference' => $patientRef, 'display' => (string) ($source['patient']['name'] ?? $source['patient_name'] ?? 'Patient')],
'recordedDate' => (string) ($alg['recorded_at'] ?? $timestamp),
'note' => isset($alg['note']) && trim((string) $alg['note']) !== ''
? [['text' => trim((string) $alg['note'])]]
@@ -526,11 +554,15 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
'resourceType' => 'CarePlan',
'id' => 'discharge-careplan-' . $recordId . '-' . $idx,
'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/CarePlan']],
+ 'text' => [
+ 'status' => 'generated',
+ 'div' => '' . htmlspecialchars($title !== '' ? $title : 'Discharge Advice') . ': ' . htmlspecialchars($description) . '
',
+ ],
'status' => 'active',
'intent' => 'plan',
'title' => $title !== '' ? $title : 'Discharge Advice',
'description' => $description,
- 'subject' => ['reference' => $patientRef],
+ 'subject' => ['reference' => $patientRef, 'display' => (string) ($source['patient']['name'] ?? $source['patient_name'] ?? 'Patient')],
'encounter' => $encounterRef ? ['reference' => $encounterRef] : null,
'created' => (string) ($cp['created_at'] ?? $timestamp),
]);
@@ -565,6 +597,10 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
'resourceType' => 'DocumentReference',
'id' => 'discharge-document-' . $recordId . '-' . $idx,
'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/DocumentReference']],
+ 'text' => [
+ 'status' => 'generated',
+ 'div' => 'Attached Document: ' . htmlspecialchars($docTitle) . '
',
+ ],
'status' => 'current',
'docStatus' => 'final',
'type' => ['coding' => [[
@@ -572,13 +608,18 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
'code' => $snomedCode,
'display' => $docTitle,
]], 'text' => $docTitle],
- 'subject' => ['reference' => $patientRef],
+ 'subject' => [
+ 'reference' => $patientRef,
+ 'display' => (string) ($source['patient']['name'] ?? $source['patient_name'] ?? 'Patient'),
+ ],
'date' => (string) ($document['created_at'] ?? $timestamp),
'author' => is_array($practitioner) ? [[
'reference' => 'urn:uuid:' . (string) $practitioner['id'],
+ 'display' => (string) ($practitioner['name'][0]['text'] ?? ''),
]] : null,
'custodian' => is_array($organization) ? [
'reference' => 'urn:uuid:' . (string) $organization['id'],
+ 'display' => (string) ($organization['name'] ?? ''),
] : null,
'description' => $docTitle,
'content' => [[
@@ -646,7 +687,7 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
$sectionDefinitions[] = ['title' => 'Investigations', 'code' => '721981007', 'display' => 'Diagnostic studies report', 'prefix' => 'discharge-investigation-', 'items' => (array) ($source['investigations'] ?? [])];
$sectionDefinitions[] = ['title' => 'Allergies', 'code' => '722446000', 'display' => 'Allergy record', 'prefix' => 'discharge-allergy-', 'items' => (array) ($source['allergies'] ?? [])];
$sectionDefinitions[] = ['title' => 'Care plan', 'code' => '734163000', 'display' => 'Care plan', 'prefix' => 'discharge-careplan-', 'items' => (array) ($source['care_plans'] ?? [])];
- $sectionDefinitions[] = ['title' => 'Document reference', 'code' => '373942005', 'display' => 'Discharge summary', 'prefix' => 'discharge-document-', 'items' => (array) ($source['documents'] ?? [])];
+ $sectionDefinitions[] = ['title' => 'Document reference', 'code' => '373942005', 'display' => 'Discharge summary', 'prefix' => 'discharge-document-', 'items' => $documentsList];
$sections = [];
foreach ($sectionDefinitions as $definition) {
@@ -654,7 +695,14 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
$narrativeParts = [];
$items = isset($definition['items_indexed']) ? $definition['items_indexed'] : (array) ($definition['items'] ?? []);
foreach ($items as $idx => $item) {
- $references[] = ['reference' => 'urn:uuid:' . $definition['prefix'] . $recordId . '-' . $idx];
+ $itemDisplay = $this->cleanPlainText((string) ($item['name'] ?? $item['text'] ?? $item['title'] ?? $definition['title']));
+ if ($itemDisplay === '') {
+ $itemDisplay = $definition['title'];
+ }
+ $references[] = [
+ 'reference' => 'urn:uuid:' . $definition['prefix'] . $recordId . '-' . $idx,
+ 'display' => $itemDisplay,
+ ];
$narrative = $this->sectionItemNarrative((array) $item);
if ($narrative !== '') {
$narrativeParts[] = $narrative;
@@ -683,9 +731,40 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
] : null,
];
}
- if ($sections !== []) {
- $builder->updateComposition(['section' => $sections]);
+
+ $patientNameClean = trim((string) ($source['patient']['name'] ?? $source['patient_name'] ?? 'Patient'));
+ if ($patientNameClean === '' || strcasecmp($patientNameClean, 'NA') === 0) {
+ $patientNameClean = 'Patient';
}
+ $patientGenderClean = trim((string) ($source['patient']['gender'] ?? ''));
+ $patientDobClean = trim((string) ($source['patient']['dob'] ?? ''));
+ $drNameClean = is_array($practitioner) ? (string) ($practitioner['name'][0]['text'] ?? 'Dr. Physician') : 'Dr. Physician';
+ $hospitalNameClean = is_array($organization) ? (string) ($organization['name'] ?? 'Hospital') : 'Hospital';
+
+ $fullNarrativeHtml = $this->buildFullCompositionNarrativeHtml(
+ $source,
+ 'IPD Discharge Summary',
+ $patientNameClean,
+ $patientId,
+ $patientGenderClean,
+ $patientDobClean,
+ $drNameClean,
+ $hospitalNameClean,
+ $sections
+ );
+
+ $compositionFinalUpdate = [
+ 'language' => 'en-IN',
+ 'confidentiality' => 'N',
+ 'text' => [
+ 'status' => 'generated',
+ 'div' => $fullNarrativeHtml,
+ ],
+ ];
+ if ($sections !== []) {
+ $compositionFinalUpdate['section'] = $sections;
+ }
+ $builder->updateComposition($compositionFinalUpdate);
$bundle = $builder->toBundle();
$validation = $this->validator->validate($bundle, 'discharge', ['resolved' => 1, 'unresolved' => 0, 'fallback_used' => 0]);
@@ -1267,4 +1346,64 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
'created_at' => (string) ($source['completed_at'] ?? date(DATE_ATOM)),
];
}
+
+ /**
+ * Build top-level human-readable XHTML narrative for Composition.text.div
+ *
+ * @param array $source
+ * @param array> $sections
+ */
+ private function buildFullCompositionNarrativeHtml(
+ array $source,
+ string $title,
+ string $patientName,
+ string $patientId,
+ string $gender,
+ string $dob,
+ string $drName,
+ string $hospitalName,
+ array $sections
+ ): string {
+ $ipdNo = (string) ($source['encounter']['ipd_no'] ?? $source['record_id'] ?? '');
+ $admitDate = (string) ($source['encounter']['start'] ?? $source['admission_date'] ?? '');
+ $dischargeDate = (string) ($source['encounter']['end'] ?? $source['discharge_date'] ?? $source['completed_at'] ?? '');
+ $locDisplay = (string) ($source['encounter']['location_display'] ?? '');
+ $abhaId = (string) ($source['patient']['abha_id'] ?? '');
+
+ $html = '';
+ $html .= '
';
+ $html .= '
' . htmlspecialchars($hospitalName) . '
';
+ $html .= '
' . htmlspecialchars($title) . '
';
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '| Patient: ' . htmlspecialchars($patientName) . ' | IPD No: ' . htmlspecialchars($ipdNo) . ' |
';
+ $html .= '| Gender / DOB: ' . htmlspecialchars(ucfirst($gender)) . ($dob !== '' ? (' / ' . htmlspecialchars($dob)) : '') . ' | ABHA Number: ' . htmlspecialchars($abhaId !== '' ? $abhaId : 'N/A') . ' |
';
+ $html .= '| Admission Date: ' . htmlspecialchars($admitDate) . ' | Discharge Date: ' . htmlspecialchars($dischargeDate) . ' |
';
+ if ($locDisplay !== '' || $drName !== '') {
+ $html .= '| Location / Bed: ' . htmlspecialchars($locDisplay !== '' ? $locDisplay : 'General Ward') . ' | Doctor: ' . htmlspecialchars($drName) . ' |
';
+ }
+ $html .= '
';
+
+ foreach ($sections as $sec) {
+ $secTitle = (string) ($sec['title'] ?? '');
+ $secDiv = (string) ($sec['text']['div'] ?? '');
+ if ($secTitle === '' || $secDiv === '') {
+ continue;
+ }
+ if (preg_match('/
]*>(.*)<\/div>/is', $secDiv, $m)) {
+ $inner = trim($m[1]);
+ } else {
+ $inner = $secDiv;
+ }
+ $html .= '
';
+ $html .= '
' . htmlspecialchars($secTitle) . '
';
+ $html .= $inner;
+ $html .= '
';
+ }
+
+ $html .= '
';
+
+ return $html;
+ }
}
diff --git a/tests/unit/Abdm/FhirGeneratorsTest.php b/tests/unit/Abdm/FhirGeneratorsTest.php
index 448d330..0ced269 100644
--- a/tests/unit/Abdm/FhirGeneratorsTest.php
+++ b/tests/unit/Abdm/FhirGeneratorsTest.php
@@ -348,6 +348,14 @@ final class FhirGeneratorsTest extends CIUnitTestCase
$this->assertSame('2026-09-01T04:53:00+05:30', $encounterRes['location'][0]['period']['start'] ?? null);
$this->assertSame('2026-09-04T04:58:00+05:30', $encounterRes['location'][0]['period']['end'] ?? null);
+ // Verify Composition narrative text, language, and confidentiality
+ $this->assertSame('en-IN', $composition['language'] ?? null);
+ $this->assertSame('N', $composition['confidentiality'] ?? null);
+ $this->assertSame('generated', $composition['text']['status'] ?? null);
+ $this->assertStringContainsString('xmlns="http://www.w3.org/1999/xhtml"', $composition['text']['div'] ?? '');
+ $this->assertStringContainsString('JANVI BISHT', $composition['text']['div'] ?? '');
+ $this->assertStringContainsString('DevSoft Tech', $composition['text']['div'] ?? '');
+
// Verify all references resolve
$fullUrls = array_column($bundle['entry'], 'fullUrl');
$encoded = json_encode($bundle, JSON_UNESCAPED_SLASHES);