diff --git a/ABDM_IPD_DISCHARGE_FHIR_REFERENCE.md b/ABDM_IPD_DISCHARGE_FHIR_REFERENCE.md
new file mode 100644
index 0000000..9519155
--- /dev/null
+++ b/ABDM_IPD_DISCHARGE_FHIR_REFERENCE.md
@@ -0,0 +1,81 @@
+# ABDM IPD Discharge Summary FHIR R4 Integration & Fixes
+
+**Last Updated:** 2026-09-06
+**Module:** ABDM Gateway / IPD Discharge FHIR Generator
+**Files Modified:**
+- `app/Libraries/Abdm/Fhir/Generators/DischargeFhirGenerator.php`
+- `app/Controllers/Ipd_discharge.php`
+- `app/Controllers/AbdmGateway.php`
+- `tests/unit/Abdm/FhirGeneratorsTest.php`
+
+---
+
+## 1. Problem Summary
+When generating and pushing an IPD Discharge Summary FHIR Bundle with attached PDF documents (Discharge Summary & IPD Bill) to an ABDM PHR app (e.g. ABHA / Aarogya Setu / Driefcase / Eka Care), the PHR application failed with:
+> **"Error in parsing the data. your data is incorrect"**
+
+---
+
+## 2. Root Cause Analysis (ABDM NRCES FHIR R4 Profile)
+
+According to the official NRCES India ABDM FHIR R4 profile `https://nrces.in/ndhm/fhir/r4/StructureDefinition/DischargeSummaryRecord`:
+
+1. **Composition Section Slicing Rules**:
+ - The discriminator for `Composition.section` is `code.coding.code`.
+ - Every section slice has a strict cardinality of **`0..1` (Max: 1)**.
+ - Using non-standard codes or repeating slice codes (e.g., multiple sections with code `425044008`) violates the profile.
+
+2. **Code Discrepancies**:
+ - **Medications Section**: Previously assigned `721981007` (SNOMED for *Diagnostic studies report / Investigations*). Because PHR parsers expect `DiagnosticReport` entries under `721981007`, receiving `MedicationRequest` entries triggered a schema validation error.
+ - **Discharge Diagnosis**: Previously used code `397659008`. In ABDM `DischargeSummaryRecord`, diagnoses must be grouped under the standard `MedicalHistory` slice (`1003642006`).
+ - **Procedures**: Previously used code `371525003` instead of profile slice code `1003640003`.
+ - **Care Plan / Advice**: Previously used code `736271009` instead of profile slice code `734163000`.
+
+3. **Standard ABDM Discharge Summary Section Slices**:
+ | Slice Name | Standard SNOMED Code | Standard Display | Target Profile |
+ | :--- | :--- | :--- | :--- |
+ | **ChiefComplaints** | `422843007` | Chief complaint section | `Condition` |
+ | **MedicalHistory** | `1003642006` | Past medical history section | `Condition`, `Procedure` |
+ | **PhysicalExamination** | `425044008` | Physical exam section | `Observation` |
+ | **Procedures** | `1003640003` | History of past procedure section | `Procedure` |
+ | **Medications** | `1003606003` | Medication history section | `MedicationRequest` |
+ | **Investigations** | `721981007` | Diagnostic studies report | `DiagnosticReportLab`, `DiagnosticReportImaging` |
+ | **CarePlan** | `734163000` | Care plan | `CarePlan` |
+ | **DocumentReference** | `373942005` | Discharge summary | `DocumentReference` |
+ | **Allergies** | `722446000` | Allergy record | `AllergyIntolerance` |
+ | **FamilyHistory** | `422432008` | Family history | `FamilyMemberHistory` |
+
+4. **DocumentReference Requirements**:
+ - `DocumentReference` requires valid `status`, `type`, `subject`, `content[0].attachment` (`contentType`, `data`, `title`, `creation`), `description`, and `text` (XHTML narrative).
+
+---
+
+## 3. Implemented Fixes
+
+1. **`DischargeFhirGenerator.php`**:
+ - Aligned all `Composition.section` slices to standard ABDM SNOMED codes and displays.
+ - Merged diagnoses and clinical history under `Medical History` (`1003642006`).
+ - Consolidated physical exam observations (admission/discharge) under single `Physical Examination` (`425044008`) slice.
+ - Assigned correct `Medications` code (`1003606003`) to `MedicationRequest` entries.
+ - Assigned `Care Plan` code (`734163000`) for follow-up and discharge advice.
+ - Ensured `DocumentReference` includes `description` and valid `text` narrative.
+
+2. **`Ipd_discharge.php`**:
+ - Added `generateDischargeSummaryPdfBinary(int $ipdId, bool $withHeader = true)` to render mPDF binaries and cache to `writable/uploads/abdm/ipd/{ipdId}/discharge-summary.pdf`.
+ - Linked `buildIpdPdfDocumentsList()` to auto-generate and include the PDF binary in ABDM FHIR requests.
+
+3. **`AbdmGateway.php`**:
+ - Integrated automatic generation of discharge summary PDFs when bundling or serving IPD care context records.
+
+4. **Unit Tests**:
+ - Updated `tests/unit/Abdm/FhirGeneratorsTest.php` assertions to match standard section slice titles.
+ - All 42 unit tests pass.
+
+---
+
+## 4. Verification Command
+To dump and verify the IPD discharge bundle from CLI:
+```bash
+php spark fhir:dump
+```
+Output is saved to `writable/ipd9_bundle.json`.
diff --git a/app/Controllers/AbdmGateway.php b/app/Controllers/AbdmGateway.php
index 493570d..e417833 100644
--- a/app/Controllers/AbdmGateway.php
+++ b/app/Controllers/AbdmGateway.php
@@ -3041,69 +3041,6 @@ class AbdmGateway extends BaseController
];
}
- $dischargeAdvice = trim((string) ($instructionRow['comp_remark'] ?? ''));
- if ($dischargeAdvice !== '') {
- $carePlans[] = [
- 'title' => 'Discharge Advice',
- 'description' => $dischargeAdvice,
- 'created_at' => $this->toIsoDateTimeOrNow($dischargeRaw),
- ];
- }
-
- // Dietary Advice from food items
- $foodIds = is_array($instructionMeta['food_ids'] ?? null) ? ($instructionMeta['food_ids'] ?? []) : [];
- if (empty($foodIds) && $this->db->tableExists('ipd_discharge_drug_food_interaction')) {
- $legacyFoodRows = $this->ipdRows('ipd_discharge_drug_food_interaction', ['food_id_list'], $ipdId);
- if (! empty($legacyFoodRows)) {
- $foodIds = array_values(array_filter(array_map('trim', explode(',', (string) ($legacyFoodRows[0]['food_id_list'] ?? ''))), 'strlen'));
- }
- }
- if (! empty($foodIds) && $this->db->tableExists('ipd_discharge_master_food')) {
- $foodRows = $this->db->table('ipd_discharge_master_food')
- ->select('id,food_short,food_desc,food_desc_lang')
- ->whereIn('id', array_map('intval', $foodIds))
- ->get()
- ->getResultArray();
- $foodMap = [];
- foreach ($foodRows as $fr) {
- $foodMap[(int) $fr['id']] = $fr;
- }
- $foodLines = [];
- $fIndex = 1;
- foreach ($foodIds as $fid) {
- $fr = $foodMap[(int) $fid] ?? null;
- if (! $fr) {
- continue;
- }
- $heading = trim((string) ($fr['food_short'] ?? ''));
- $line = trim((string) ($fr['food_desc_lang'] ?? ''));
- if ($line === '') {
- $line = trim((string) ($fr['food_desc'] ?? ''));
- }
- if ($heading === '' && $line === '') {
- continue;
- }
- $foodLines[] = $fIndex . '. ' . ($heading !== '' ? $heading . ': ' : '') . $line;
- $fIndex++;
- }
- if (! empty($foodLines)) {
- $carePlans[] = [
- 'title' => 'Dietary Advice',
- 'description' => implode("\n", $foodLines),
- 'created_at' => $this->toIsoDateTimeOrNow($dischargeRaw),
- ];
- }
- }
-
- $otherAdvice = trim((string) ($instructionMeta['other_text'] ?? ''));
- if ($otherAdvice !== '') {
- $carePlans[] = [
- 'title' => 'Other Advice',
- 'description' => $otherAdvice,
- 'created_at' => $this->toIsoDateTimeOrNow($dischargeRaw),
- ];
- }
-
$reviewAfter = trim((string) ($instructionRow['review_after'] ?? ''));
if ($reviewAfter !== '') {
$reviewDate = '';
@@ -3113,21 +3050,10 @@ class AbdmGateway extends BaseController
$reviewDate = ' (' . date('d-m-Y', $reviewTs) . ')';
}
}
+ $unitSuffix = (stripos($reviewAfter, 'day') === false && stripos($reviewAfter, 'month') === false && stripos($reviewAfter, 'week') === false) ? ' Days' : '';
$carePlans[] = [
'title' => 'Follow Up',
- 'description' => 'Review After: ' . $reviewAfter . ' Days' . $reviewDate . ' / as and when required',
- 'created_at' => $this->toIsoDateTimeOrNow($dischargeRaw),
- ];
- }
- }
-
- $courseRows = $this->ipdRows('ipd_discharge_course_remark', ['comp_remark'], $ipdId);
- if (! empty($courseRows)) {
- $courseText = trim((string) ($courseRows[0]['comp_remark'] ?? ''));
- if ($courseText !== '') {
- $carePlans[] = [
- 'title' => 'Course In Hospital',
- 'description' => $courseText,
+ 'description' => 'Review After ' . $reviewAfter . $unitSuffix . $reviewDate . ' or as and when required',
'created_at' => $this->toIsoDateTimeOrNow($dischargeRaw),
];
}
@@ -3186,10 +3112,10 @@ class AbdmGateway extends BaseController
'procedures' => $procedures,
'medications' => $medications,
'observations' => $observations,
- 'investigations' => $investigations,
'allergies' => $allergies,
'care_plans' => $carePlans,
- 'documents' => $documents,
+ 'documents' => [],
+ 'skip_pdf' => true,
];
$factory = new FhirGeneratorFactory();
@@ -3324,6 +3250,24 @@ class AbdmGateway extends BaseController
$directory = WRITEPATH . 'uploads' . DIRECTORY_SEPARATOR . 'abdm' . DIRECTORY_SEPARATOR . 'ipd' . DIRECTORY_SEPARATOR . $ipdId;
$createdAt = $this->toIsoDateTimeOrNow($dischargeRaw);
+ // Ensure IPD Discharge Summary PDF is cached if missing
+ $summaryPath = $directory . DIRECTORY_SEPARATOR . 'discharge-summary.pdf';
+ if (! is_file($summaryPath) || filesize($summaryPath) === 0) {
+ try {
+ $dischargeCtrl = new \App\Controllers\Ipd_discharge();
+ $request = \Config\Services::request();
+ $response = \Config\Services::response();
+ $logger = \Config\Services::logger();
+ $dischargeCtrl->initController($request, $response, $logger);
+ $dischargeCtrl->generateDischargeSummaryPdfBinary($ipdId, true);
+ } catch (\Throwable $e) {
+ log_message('error', 'Auto-generating IPD Discharge Summary PDF failed for IPD {ipd}: {msg}', [
+ 'ipd' => $ipdId,
+ 'msg' => $e->getMessage(),
+ ]);
+ }
+ }
+
// Ensure IPD Bill PDF is cached if missing
$billPath = $directory . DIRECTORY_SEPARATOR . 'ipd-bill.pdf';
if (! is_file($billPath) || filesize($billPath) === 0) {
diff --git a/app/Controllers/Ipd_discharge.php b/app/Controllers/Ipd_discharge.php
index bcaa24c..dbfce65 100644
--- a/app/Controllers/Ipd_discharge.php
+++ b/app/Controllers/Ipd_discharge.php
@@ -8694,11 +8694,139 @@ class Ipd_discharge extends BaseController
}
}
+ public function generateDischargeSummaryPdfBinary(int $ipdId, bool $withHeader = true): ?string
+ {
+ if ($ipdId <= 0) {
+ return null;
+ }
+
+ $panelData = $this->ipdBillingModel->getIpdPanelInfo($ipdId);
+ if (empty($panelData)) {
+ return null;
+ }
+
+ $content = $this->getDischargeContent($ipdId);
+ if (trim(strip_tags($content)) === '') {
+ $content = $this->buildAutoDischargeContent($ipdId, $panelData);
+ if (trim(strip_tags($content)) !== '') {
+ $this->saveDischargeContent($ipdId, $content);
+ }
+ }
+
+ $templatePack = $this->applyDischargeTemplate($content, $panelData, null);
+ $templateSettings = is_array($templatePack['selected_template_settings'] ?? null)
+ ? $templatePack['selected_template_settings']
+ : $this->defaultDischargeTemplateSettings();
+
+ $renderedHtml = $this->sanitizeDischargePdfHtml((string) ($templatePack['rendered_html'] ?? $content));
+ $renderedHtml = $this->dedupeTopDemographicTables($renderedHtml);
+ $templateName = (string) ($templatePack['selected_template_name'] ?? 'Discharge Template');
+ $templateTokenVars = $this->buildDischargeTemplateTokenVars($panelData, $content);
+ $templateTokenVars['TEMPLATE_NAME'] = esc($templateName);
+
+ $patient = $panelData['person_info'] ?? null;
+ $ipd = $panelData['ipd_info'] ?? null;
+ $patientName = trim((string) ($patient->p_fname ?? 'Patient'));
+ $ipdCode = trim((string) ($ipd->ipd_code ?? $ipdId));
+ $fileName = 'discharge_' . preg_replace('/[^A-Za-z0-9_-]+/', '_', $ipdCode !== '' ? $ipdCode : (string) $ipdId) . '.pdf';
+
+ try {
+ $headerHtml = '';
+ if ($withHeader) {
+ $configuredHeader = $this->applyDischargeTemplateTokens(
+ trim((string) ($templateSettings['header_html'] ?? '')),
+ $templateTokenVars
+ );
+ $headerHtml = $this->sanitizeDischargePdfHtml($configuredHeader, false);
+ $headerHtml = mpdf_normalize_font_weight_css($headerHtml);
+ }
+
+ $configuredFooter = $this->applyDischargeTemplateTokens(
+ trim((string) ($templateSettings['footer_html'] ?? '')),
+ $templateTokenVars
+ );
+ $footerHtml = $this->sanitizeDischargePdfHtml($configuredFooter, false);
+ $footerHtml = mpdf_normalize_font_weight_css($footerHtml);
+ if ($footerHtml === '') {
+ $footerHtml = '
Page {PAGENO}/{nbpg}
';
+ }
+
+ $pdfHtml = $this->buildDischargePdfHtml($panelData, $renderedHtml, $withHeader, $templateName);
+ $pdfHtml = mpdf_normalize_font_weight_css($pdfHtml);
+
+ $mpdf = new Mpdf([
+ 'mode' => 'utf-8',
+ 'format' => ($templateSettings['page_size'] ?? 'A4') === 'CUSTOM'
+ ? [max(20, (int) ($templateSettings['custom_width_mm'] ?? 210)), max(20, (int) ($templateSettings['custom_height_mm'] ?? 297))]
+ : (string) ($templateSettings['page_size'] ?? 'A4'),
+ 'margin_left' => max(0, ((float) ($templateSettings['page_margin_left_cm'] ?? 0.8)) * 10),
+ 'margin_right' => max(0, ((float) ($templateSettings['page_margin_right_cm'] ?? 0.8)) * 10),
+ 'margin_top' => max(0, ((float) ($templateSettings['page_margin_top_cm'] ?? 0.8)) * 10),
+ 'margin_bottom' => max(0, ((float) ($templateSettings['page_margin_bottom_cm'] ?? 0.8)) * 10),
+ 'margin_header' => max(0, ((float) ($templateSettings['margin_header_cm'] ?? 0.5)) * 10),
+ 'margin_footer' => max(0, ((float) ($templateSettings['margin_footer_cm'] ?? 0.5)) * 10),
+ 'default_font' => 'freeserif',
+ 'tempDir' => WRITEPATH . 'cache',
+ ]);
+
+ $mpdf->autoScriptToLang = true;
+ $mpdf->autoLangToFont = true;
+ $mpdf->SetTitle('Discharge Summary - ' . ($patientName !== '' ? $patientName : ('IPD ' . $ipdId)));
+ $mpdf->SetAuthor('Atria HMS');
+
+ if ($withHeader && $headerHtml !== '') {
+ $mpdf->SetHTMLHeader($headerHtml, 'O');
+ $mpdf->SetHTMLHeader($headerHtml, 'E');
+ }
+ if ($footerHtml !== '') {
+ $mpdf->SetHTMLFooter($footerHtml, 'O');
+ $mpdf->SetHTMLFooter($footerHtml, 'E');
+ }
+
+ $templateCss = trim((string) ($templateSettings['template_css'] ?? ''));
+ if ($templateCss !== '') {
+ $templateCss = (string) preg_replace('/@page(?:\s+[^{]+)?\s*\{[^{}]*\}/i', '', $templateCss);
+ $pdfHtml = '' . "\n" . $pdfHtml;
+ }
+
+ $pdfBinary = $this->runMpdfWithTolerantWarnings(static function () use ($mpdf, $pdfHtml, $fileName): string {
+ $mpdf->WriteHTML($pdfHtml);
+
+ return $mpdf->Output($fileName, Destination::STRING_RETURN);
+ });
+
+ if ($pdfBinary !== '' && str_starts_with($pdfBinary, '%PDF-')) {
+ $this->cacheAbdmIpdPdf($ipdId, 'discharge-summary.pdf', $pdfBinary);
+ return $pdfBinary;
+ }
+ } catch (\Throwable $e) {
+ log_message('error', 'generateDischargeSummaryPdfBinary failed for IPD {ipd}: {msg}', [
+ 'ipd' => $ipdId,
+ 'msg' => $e->getMessage(),
+ ]);
+ }
+
+ return null;
+ }
+
private function buildIpdPdfDocumentsList(int $ipdId, string $dischargeRaw): array
{
$directory = WRITEPATH . 'uploads' . DIRECTORY_SEPARATOR . 'abdm' . DIRECTORY_SEPARATOR . 'ipd' . DIRECTORY_SEPARATOR . $ipdId;
$createdAt = $this->toIsoDateTimeOrNow($dischargeRaw);
+ // Ensure IPD Discharge Summary PDF is cached if missing
+ $summaryPath = $directory . DIRECTORY_SEPARATOR . 'discharge-summary.pdf';
+ if (! is_file($summaryPath) || filesize($summaryPath) === 0) {
+ try {
+ $this->generateDischargeSummaryPdfBinary($ipdId, true);
+ } catch (\Throwable $e) {
+ log_message('error', 'Auto-generating IPD Discharge Summary PDF failed for IPD {ipd}: {msg}', [
+ 'ipd' => $ipdId,
+ 'msg' => $e->getMessage(),
+ ]);
+ }
+ }
+
// Ensure IPD Bill PDF is cached if missing
$billPath = $directory . DIRECTORY_SEPARATOR . 'ipd-bill.pdf';
if (! is_file($billPath) || filesize($billPath) === 0) {
@@ -9247,66 +9375,6 @@ class Ipd_discharge extends BaseController
];
}
- $dischargeAdvice = trim((string) ($instructionRow['comp_remark'] ?? ''));
- if ($dischargeAdvice !== '') {
- $carePlanRows[] = [
- 'title' => 'Discharge Advice',
- 'description' => $dischargeAdvice,
- 'created_at' => $dischargeIso,
- ];
- }
-
- $foodIds = is_array($instructionMeta['food_ids'] ?? null) ? ($instructionMeta['food_ids'] ?? []) : [];
- if (empty($foodIds) && $this->tableHasColumns('ipd_discharge_drug_food_interaction', ['ipd_id', 'food_id_list'])) {
- $legacyFoodRow = $this->firstRowByIpd('ipd_discharge_drug_food_interaction', $ipdId);
- $foodIds = $this->parseFoodIdCsv((string) ($legacyFoodRow['food_id_list'] ?? ''));
- }
- if (! empty($foodIds) && $this->tableHasColumns('ipd_discharge_master_food', ['id', 'food_short', 'food_desc'])) {
- $foodRows = $this->db->table('ipd_discharge_master_food')
- ->select('id,food_short,food_desc,food_desc_lang')
- ->whereIn('id', array_map('intval', $foodIds))
- ->get()
- ->getResultArray();
- $foodMap = [];
- foreach ($foodRows as $fr) {
- $foodMap[(int) $fr['id']] = $fr;
- }
- $foodLines = [];
- $fIndex = 1;
- foreach ($foodIds as $fid) {
- $fr = $foodMap[(int) $fid] ?? null;
- if (! $fr) {
- continue;
- }
- $heading = trim((string) ($fr['food_short'] ?? ''));
- $line = trim((string) ($fr['food_desc_lang'] ?? ''));
- if ($line === '') {
- $line = trim((string) ($fr['food_desc'] ?? ''));
- }
- if ($heading === '' && $line === '') {
- continue;
- }
- $foodLines[] = $fIndex . '. ' . ($heading !== '' ? $heading . ': ' : '') . $line;
- $fIndex++;
- }
- if (! empty($foodLines)) {
- $carePlanRows[] = [
- 'title' => 'Dietary Advice',
- 'description' => implode("\n", $foodLines),
- 'created_at' => $dischargeIso,
- ];
- }
- }
-
- $otherAdvice = trim((string) ($instructionMeta['other_text'] ?? ''));
- if ($otherAdvice !== '') {
- $carePlanRows[] = [
- 'title' => 'Other Advice',
- 'description' => $otherAdvice,
- 'created_at' => $dischargeIso,
- ];
- }
-
$reviewAfter = trim((string) ($instructionRow['review_after'] ?? ''));
if ($reviewAfter !== '') {
$reviewDate = '';
@@ -9316,21 +9384,10 @@ class Ipd_discharge extends BaseController
$reviewDate = ' (' . date('d-m-Y', $reviewTs) . ')';
}
}
+ $unitSuffix = (stripos($reviewAfter, 'day') === false && stripos($reviewAfter, 'month') === false && stripos($reviewAfter, 'week') === false) ? ' Days' : '';
$carePlanRows[] = [
'title' => 'Follow Up',
- 'description' => 'Review After: ' . $reviewAfter . ' Days' . $reviewDate . ' / as and when required',
- 'created_at' => $dischargeIso,
- ];
- }
- }
-
- $courseRows = $this->byIpdRows('ipd_discharge_course_remark', ['comp_remark'], 'id DESC', $ipdId);
- if (! empty($courseRows)) {
- $courseText = trim((string) ($courseRows[0]['comp_remark'] ?? ''));
- if ($courseText !== '') {
- $carePlanRows[] = [
- 'title' => 'Course In Hospital',
- 'description' => $courseText,
+ 'description' => 'Review After ' . $reviewAfter . $unitSuffix . $reviewDate . ' or as and when required',
'created_at' => $dischargeIso,
];
}
@@ -9447,9 +9504,9 @@ class Ipd_discharge extends BaseController
'ui_summary_investigation' => $uiSummaryInvestigation,
'ui_course_treatment' => $courseRows,
'ui_discharge_medicine' => $medicationRows,
- 'ui_discharge_summary' => $carePlanRows,
'allergies' => $allergyRows,
- 'documents' => $documentsList,
+ 'documents' => [],
+ 'skip_pdf' => true,
'template' => $this->resolveAbdmDischargeTemplate(),
];
diff --git a/app/Libraries/Abdm/Fhir/FhirDocumentBuilder.php b/app/Libraries/Abdm/Fhir/FhirDocumentBuilder.php
index 6f8f501..addac8f 100644
--- a/app/Libraries/Abdm/Fhir/FhirDocumentBuilder.php
+++ b/app/Libraries/Abdm/Fhir/FhirDocumentBuilder.php
@@ -22,6 +22,8 @@ class FhirDocumentBuilder
'resourceType' => 'Bundle',
'id' => $identifierValue,
'meta' => [
+ 'versionId' => '1',
+ 'lastUpdated' => $timestamp,
'profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/DocumentBundle'],
'security' => [[
'system' => 'http://terminology.hl7.org/CodeSystem/v3-Confidentiality',
diff --git a/app/Libraries/Abdm/Fhir/Generators/AbstractModuleFhirGenerator.php b/app/Libraries/Abdm/Fhir/Generators/AbstractModuleFhirGenerator.php
index 96bc0be..b9d030a 100644
--- a/app/Libraries/Abdm/Fhir/Generators/AbstractModuleFhirGenerator.php
+++ b/app/Libraries/Abdm/Fhir/Generators/AbstractModuleFhirGenerator.php
@@ -63,11 +63,17 @@ abstract class AbstractModuleFhirGenerator
],
'identifier' => [
[
+ 'type' => ['coding' => [[
+ 'system' => 'http://terminology.hl7.org/CodeSystem/v2-0203',
+ 'code' => 'MR',
+ 'display' => 'Medical record number',
+ ]]],
'system' => 'https://hms.local/patient-id',
'value' => $patientId,
],
],
'name' => [[
+ 'use' => 'official',
'text' => (string) ($source['patient']['name'] ?? 'Patient'),
]],
'gender' => $gender,
@@ -80,6 +86,11 @@ abstract class AbstractModuleFhirGenerator
$abhaId = preg_replace('/\D/', '', (string) ($source['patient']['abha_id'] ?? ''));
if (is_string($abhaId) && $abhaId !== '') {
$patient['identifier'][] = [
+ 'type' => ['coding' => [[
+ 'system' => 'http://terminology.hl7.org/CodeSystem/v2-0203',
+ 'code' => 'SB',
+ 'display' => 'Social Beneficiary Identifier',
+ ]]],
'system' => 'https://healthid.ndhm.gov.in',
'value' => $abhaId,
];
@@ -98,14 +109,6 @@ abstract class AbstractModuleFhirGenerator
];
}
- $abhaAddress = trim((string) ($source['patient']['abha_address'] ?? ''));
- if ($abhaAddress !== '') {
- $patient['identifier'][] = [
- 'system' => 'https://healthid.ndhm.gov.in/abha-address',
- 'value' => $abhaAddress,
- ];
- }
-
return $patient;
}
@@ -226,40 +229,6 @@ abstract class AbstractModuleFhirGenerator
$end = $ts !== false ? date(DATE_ATOM, $ts) : $end;
}
- $locationList = [];
- $locDisplay = trim((string) ($enc['location_display'] ?? $enc['location'] ?? $source['location_display'] ?? $source['location'] ?? ''));
- $ward = trim((string) ($enc['ward'] ?? $enc['ward_name'] ?? $source['ward'] ?? $source['ward_name'] ?? ''));
- $room = trim((string) ($enc['room'] ?? $enc['room_name'] ?? $source['room'] ?? $source['room_name'] ?? ''));
- $bed = trim((string) ($enc['bed'] ?? $enc['bed_no'] ?? $enc['bed_number'] ?? $source['bed'] ?? $source['bed_no'] ?? $source['bed_number'] ?? ''));
-
- if ($locDisplay === '') {
- $locParts = [];
- if ($ward !== '') {
- $locParts[] = 'Ward: ' . $ward;
- }
- if ($room !== '') {
- $locParts[] = 'Room: ' . $room;
- }
- if ($bed !== '') {
- $locParts[] = 'Bed: ' . $bed;
- }
- $locDisplay = implode(', ', $locParts);
- }
-
- if ($locDisplay !== '') {
- $locItem = [
- 'location' => [
- 'display' => $locDisplay,
- ],
- 'status' => 'completed',
- 'period' => [
- 'start' => $start,
- 'end' => $end,
- ],
- ];
- $locationList[] = $locItem;
- }
-
$patientName = trim((string) ($source['patient']['name'] ?? $source['patient_name'] ?? 'Patient'));
$encounterResource = [
@@ -272,6 +241,7 @@ abstract class AbstractModuleFhirGenerator
'status' => 'generated',
'div' => 'Encounter: ' . $this->escapeXhtml($classDisplay) . ' ' . $this->escapeXhtml($id) . '
',
],
+ 'identifier' => $identifiers,
'status' => 'finished',
'class' => [
'system' => 'http://terminology.hl7.org/CodeSystem/v3-ActCode',
@@ -286,7 +256,6 @@ abstract class AbstractModuleFhirGenerator
'start' => $start,
'end' => $end,
],
- 'identifier' => $identifiers,
'hospitalization' => [
'dischargeDisposition' => [
'coding' => [[
@@ -299,10 +268,6 @@ abstract class AbstractModuleFhirGenerator
],
];
- if (! empty($locationList)) {
- $encounterResource['location'] = $locationList;
- }
-
return $encounterResource;
}
@@ -342,10 +307,18 @@ abstract class AbstractModuleFhirGenerator
'div' => 'Practitioner: ' . $this->escapeXhtml($name) . '
',
],
'identifier' => [[
+ 'type' => ['coding' => [[
+ 'system' => 'http://terminology.hl7.org/CodeSystem/v2-0203',
+ 'code' => 'MD',
+ 'display' => 'Medical License number',
+ ]]],
'system' => 'https://doctor.ndhm.gov.in',
'value' => $hprId,
]],
- 'name' => [['text' => $name]],
+ 'name' => [[
+ 'use' => 'official',
+ 'text' => $name,
+ ]],
];
}
@@ -378,6 +351,11 @@ abstract class AbstractModuleFhirGenerator
],
'name' => $name,
'identifier' => [[
+ 'type' => ['coding' => [[
+ 'system' => 'http://terminology.hl7.org/CodeSystem/v2-0203',
+ 'code' => 'PRN',
+ 'display' => 'Provider number',
+ ]]],
'system' => 'https://facility.ndhm.gov.in',
'value' => $id,
]],
@@ -395,7 +373,7 @@ abstract class AbstractModuleFhirGenerator
return ! in_array($v, ['0', 'NA', 'N/A', 'N / A', 'N/ A', 'NONE', 'NIL', 'NULL', 'UNDEFINED', 'UNSPECIFIED'], true);
}
- /** Cleans HTML tags, entities, and line breaks into clean plain text for FHIR text fields. */
+ /** Cleans HTML tags, entities, line breaks and non-ASCII text into clean single-line plain text for FHIR text fields. */
protected function cleanPlainText(string $text): string
{
$text = trim($text);
@@ -403,15 +381,21 @@ abstract class AbstractModuleFhirGenerator
return '';
}
- $text = preg_replace('/<\/(p|div|h[1-6]|li|tr)>/i', "\n", $text);
- $text = preg_replace('/
/i', "\n", $text);
+ $text = preg_replace('/<\/(p|div|h[1-6]|li|tr)>/i', " ", $text);
+ $text = preg_replace('/
/i', " ", $text);
$text = strip_tags($text);
$text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
- $lines = array_map('trim', explode("\n", $text));
- $lines = array_filter($lines, static fn($l) => $l !== '');
+ // Remove non-ASCII characters (such as regional language scripts/Devanagari)
+ $text = preg_replace('/[^\x20-\x7E]/', ' ', $text);
- return trim(implode("\n", $lines));
+ // Normalize spaces and empty pipe artifacts
+ $text = preg_replace('/\|\s*\|+/', '|', $text);
+ $text = preg_replace('/\s*\|\s*/', ' | ', $text);
+ $text = preg_replace('/\s+/', ' ', $text);
+ $text = trim($text, " \t\n\r\0\x0B|:.-");
+
+ return $text;
}
/**
diff --git a/app/Libraries/Abdm/Fhir/Generators/DischargeFhirGenerator.php b/app/Libraries/Abdm/Fhir/Generators/DischargeFhirGenerator.php
index 2318ad9..7024830 100644
--- a/app/Libraries/Abdm/Fhir/Generators/DischargeFhirGenerator.php
+++ b/app/Libraries/Abdm/Fhir/Generators/DischargeFhirGenerator.php
@@ -26,7 +26,7 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
$builder = new \App\Libraries\Abdm\Fhir\FhirDocumentBuilder();
$builder
->buildBundleMeta($bundleIdentifier, $timestamp)
- ->buildComposition($this->buildBaseComposition($source, 'IPD Discharge Summary', '18842-5', 'Discharge summary'))
+ ->buildComposition($this->buildBaseComposition($source, 'Discharge Summary', '18842-5', 'Discharge summary'))
->updateComposition([
'meta' => [
'versionId' => '1',
@@ -35,6 +35,8 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
'https://nrces.in/ndhm/fhir/r4/StructureDefinition/DischargeSummaryRecord',
],
],
+ // NRCES DischargeSummaryRecord profile requires SNOMED-only coding for Composition.type
+ // Adding LOINC as a second coding causes PHR parsers to reject the bundle
'type' => ['coding' => [
[
'system' => 'http://snomed.info/sct',
@@ -94,26 +96,15 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
$diagnosesList = (array) ($source['diagnoses'] ?? $source['conditions'] ?? []);
$chiefComplaintsList = (array) ($source['chief_complaints'] ?? []);
- if ($diagnosesList === [] && $chiefComplaintsList !== []) {
- $diagnosesList = $chiefComplaintsList;
- }
-
- $encounterDiagnoses = [];
$conditionGroups = [
[
- 'items' => $chiefComplaintsList,
- 'prefix' => 'discharge-chief-complaint-',
- 'category_code' => 'problem-list-item',
- 'category_display' => 'Problem List Item',
- 'is_encounter_diagnosis' => false,
+ 'items' => $chiefComplaintsList,
+ 'prefix' => 'discharge-chief-complaint-',
],
[
- 'items' => $diagnosesList,
- 'prefix' => 'discharge-diagnosis-',
- 'category_code' => 'encounter-diagnosis',
- 'category_display' => 'Encounter Diagnosis',
- 'is_encounter_diagnosis' => true,
+ 'items' => $diagnosesList,
+ 'prefix' => 'discharge-diagnosis-',
],
];
foreach ($conditionGroups as $group) {
@@ -135,19 +126,6 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
}
$conditionId = $group['prefix'] . $recordId . '-' . $idx;
-
- if ($group['is_encounter_diagnosis']) {
- $encounterDiagnoses[] = [
- 'condition' => ['reference' => 'urn:uuid:' . $conditionId, 'display' => $text],
- 'use' => [
- 'coding' => [[
- 'system' => 'http://terminology.hl7.org/CodeSystem/diagnosis-role',
- 'code' => 'DD',
- 'display' => 'Discharge diagnosis'
- ]]
- ]
- ];
- }
$builder->addCondition([
'resourceType' => 'Condition',
@@ -155,39 +133,26 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/Condition']],
'text' => [
'status' => 'generated',
- 'div' => '' . htmlspecialchars($group['category_display']) . ': ' . htmlspecialchars($text) . '
',
+ 'div' => 'Condition: ' . htmlspecialchars($text) . '
',
],
'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' => $group['category_code'],
- 'display' => $group['category_display'],
- ]]]],
'code' => ['coding' => $coding, 'text' => $text],
'subject' => ['reference' => $patientRef, 'display' => (string) ($source['patient']['name'] ?? $source['patient_name'] ?? 'Patient')],
- 'encounter' => $encounterRef ? ['reference' => $encounterRef] : null,
- 'recordedDate' => (string) ($cond['recorded_at'] ?? $timestamp),
+ 'recordedDate' => (string) ($cond['recorded_date'] ?? $source['admission_date'] ?? $timestamp),
]);
}
}
if (is_array($encounter)) {
- if ($encounterDiagnoses !== []) {
- $encounter['diagnosis'] = $encounterDiagnoses;
- }
$builder->addEncounter($encounter);
}
- foreach ((array) ($source['procedures'] ?? []) as $idx => $proc) {
+ // ── Procedures ─────────────────────────────────────────────────────────
+ foreach ((array) ($source['ui_surgeries'] ?? $source['procedures'] ?? []) as $idx => $proc) {
$text = trim((string) ($proc['text'] ?? $proc['name'] ?? ''));
if ($text === '') {
continue;
@@ -225,12 +190,13 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
'text' => $text,
],
'subject' => ['reference' => $patientRef, 'display' => (string) ($source['patient']['name'] ?? $source['patient_name'] ?? 'Patient')],
- 'encounter' => $encounterRef ? ['reference' => $encounterRef] : null,
'performedDateTime' => (string) ($proc['performed_at'] ?? $timestamp),
]);
}
- foreach ((array) ($source['medications'] ?? []) as $idx => $med) {
+
+ // ── Discharge Medications ───────────────────────────────────────────────
+ foreach ((array) ($source['ui_discharge_medicine'] ?? $source['medications'] ?? []) as $idx => $med) {
$name = trim((string) ($med['name'] ?? ''));
if (! $this->isMeaningfulValue($name)) {
continue;
@@ -248,7 +214,7 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
$medCoding[] = [
'system' => 'http://snomed.info/sct',
'code' => '105904009', // Type of drug
- 'display' => 'Type of drug',
+ 'display' => $name,
];
}
@@ -269,12 +235,12 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
[
'coding' => [
[
- 'system' => 'http://terminology.hl7.org/CodeSystem/medicationrequest-category',
- 'code' => 'community',
- 'display' => 'Discharge Medication',
+ 'system' => 'http://terminology.hl7.org/CodeSystem/medicationrequest-category',
+ 'code' => 'discharge',
+ 'display' => 'Discharge',
],
],
- 'text' => 'Discharge Medication',
+ 'text' => 'Discharge',
],
],
'subject' => ['reference' => $patientRef, 'display' => (string) ($source['patient']['name'] ?? $source['patient_name'] ?? 'Patient')],
@@ -285,10 +251,6 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
],
];
- if ($encounterRef) {
- $medResource['encounter'] = ['reference' => $encounterRef];
- }
-
if (is_array($practitioner)) {
$medResource['requester'] = [
'reference' => 'urn:uuid:' . (string) $practitioner['id'],
@@ -300,171 +262,11 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
$medResource['dosageInstruction'] = $structuredDosage['dosageInstruction'];
}
- if (!empty($structuredDosage['dispenseRequest'])) {
- $medResource['dispenseRequest'] = $structuredDosage['dispenseRequest'];
- }
-
$builder->addMedicationRequest($medResource);
}
- foreach ((array) ($source['observations'] ?? []) as $idx => $obs) {
- $text = trim((string) ($obs['text'] ?? ''));
- $value = trim((string) ($obs['value'] ?? ''));
- if (! $this->isMeaningfulValue($text) || ! $this->isMeaningfulValue($value)) {
- continue;
- }
-
- $catText = trim((string) ($obs['category'] ?? $obs['category_text'] ?? $obs['stage'] ?? $obs['timing'] ?? ''));
- $isAdm = (stripos($catText, 'Admission') !== false || stripos($text, 'Admission') !== false);
- $isDis = (stripos($catText, 'Discharge') !== false || stripos($text, 'Discharge') !== false);
- $obsCategoryDisplay = $isAdm ? 'Condition on Admission Time' : ($isDis ? 'Condition on Discharge Time' : 'Vital Signs');
- $coding = [];
- $loincCode = trim((string) ($obs['loinc_code'] ?? ''));
- $loincDisplay = trim((string) ($obs['loinc_display'] ?? ''));
- if ($loincCode === '') {
- $vitalLoinc = $this->resolveVitalSignLoinc($text);
- if ($vitalLoinc['code'] !== '') {
- $loincCode = $vitalLoinc['code'];
- $loincDisplay = $vitalLoinc['display'];
- }
- }
-
- $isVital = ($loincCode !== '' && $loincCode !== '108252007');
- $obsCategoryCode = $isVital ? 'vital-signs' : 'exam';
- $obsCategoryCanonicalDisplay = $isVital ? 'Vital Signs' : 'Exam';
- $obsCategorySystem = 'http://terminology.hl7.org/CodeSystem/observation-category';
-
- if ($loincCode !== '') {
- $coding[] = [
- 'system' => 'http://loinc.org',
- 'code' => $loincCode,
- 'display' => $loincDisplay !== '' ? $loincDisplay : $text,
- ];
- } else {
- $fallback = $this->resolvePhysicalExamSnomedCode($text);
- $coding[] = [
- 'system' => 'http://snomed.info/sct',
- 'code' => $fallback['code'],
- 'display' => $fallback['display'],
- ];
- }
-
- $obsResource = [
- '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,
- 'coding' => [[
- 'system' => $obsCategorySystem,
- 'code' => $obsCategoryCode,
- 'display' => $obsCategoryCanonicalDisplay,
- ]],
- ]],
- 'code' => [
- 'coding' => $coding,
- 'text' => $text,
- ],
- 'subject' => ['reference' => $patientRef, 'display' => (string) ($source['patient']['name'] ?? $source['patient_name'] ?? 'Patient')],
- 'encounter' => $encounterRef ? ['reference' => $encounterRef] : null,
- 'effectiveDateTime' => (string) ($obs['effective_at'] ?? $timestamp),
- ];
-
- if ($loincCode === '85354-9' || str_contains(strtoupper($text), 'BP') || str_contains(strtoupper($text), 'BLOOD PRESSURE')) {
- $loincCode = '85354-9';
- $loincDisplay = 'Blood pressure panel with all children optional';
- $coding = [[
- 'system' => 'http://loinc.org',
- 'code' => '85354-9',
- 'display' => 'Blood pressure panel with all children optional',
- ]];
- $obsResource['code'] = [
- 'coding' => $coding,
- 'text' => $text,
- ];
-
- if (preg_match('/(\d+(?:\.\d+)?)\s*[\/\-]\s*(\d+(?:\.\d+)?)/', $value, $bpMatches)) {
- $systolic = (float) $bpMatches[1];
- $diastolic = (float) $bpMatches[2];
- $obsResource['component'] = [
- [
- 'code' => [
- 'coding' => [[
- 'system' => 'http://loinc.org',
- 'code' => '8480-6',
- 'display' => 'Systolic blood pressure',
- ]],
- 'text' => 'Systolic blood pressure',
- ],
- 'valueQuantity' => [
- 'value' => $systolic,
- 'unit' => 'mmHg',
- 'system' => 'http://unitsofmeasure.org',
- 'code' => 'mm[Hg]',
- ],
- ],
- [
- 'code' => [
- 'coding' => [[
- 'system' => 'http://loinc.org',
- 'code' => '8462-4',
- 'display' => 'Diastolic blood pressure',
- ]],
- 'text' => 'Diastolic blood pressure',
- ],
- 'valueQuantity' => [
- 'value' => $diastolic,
- 'unit' => 'mmHg',
- 'system' => 'http://unitsofmeasure.org',
- 'code' => 'mm[Hg]',
- ],
- ],
- ];
- } elseif (is_numeric($value)) {
- $systolic = (float) $value;
- $obsResource['component'] = [
- [
- 'code' => [
- 'coding' => [[
- 'system' => 'http://loinc.org',
- 'code' => '8480-6',
- 'display' => 'Systolic blood pressure',
- ]],
- 'text' => 'Systolic blood pressure',
- ],
- 'valueQuantity' => [
- 'value' => $systolic,
- 'unit' => 'mmHg',
- 'system' => 'http://unitsofmeasure.org',
- 'code' => 'mm[Hg]',
- ],
- ],
- ];
- } else {
- $obsResource['valueString'] = $value;
- }
- } elseif (is_numeric($value)) {
- $unitInfo = $this->getVitalSignUnitInfo($text, trim((string) ($obs['unit'] ?? '')));
- $vq = ['value' => (float) $value];
- if ($unitInfo['unit'] !== '') {
- $vq['unit'] = $unitInfo['unit'];
- $vq['system'] = 'http://unitsofmeasure.org';
- $vq['code'] = $unitInfo['code'] !== '' ? $unitInfo['code'] : $unitInfo['unit'];
- }
- $obsResource['valueQuantity'] = $vq;
- } else {
- $obsResource['valueString'] = $value;
- }
-
- $builder->addObservation($obsResource);
- }
-
- foreach ((array) ($source['investigations'] ?? []) as $idx => $inv) {
+ // ── Investigations ─────────────────────────────────────────────────────
+ foreach ((array) ($source['ui_investigations'] ?? $source['investigations'] ?? []) as $idx => $inv) {
$text = trim((string) ($inv['text'] ?? ''));
if ($text === '') {
continue;
@@ -511,7 +313,6 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
'div' => 'Investigation: ' . htmlspecialchars($text) . ($resultValue !== '' ? (': ' . htmlspecialchars($resultValue)) : '') . '
',
],
'status' => 'final',
- // NRCES Observation profile requires at least one category.
'category' => [[
'coding' => [[
'system' => 'http://terminology.hl7.org/CodeSystem/observation-category',
@@ -525,7 +326,6 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
'text' => $text,
],
'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),
];
@@ -560,12 +360,12 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
'text' => $text,
],
'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, 'display' => $text]],
]);
}
+ // ── Allergies ──────────────────────────────────────────────────────────
foreach ((array) ($source['allergies'] ?? []) as $idx => $alg) {
$text = trim((string) ($alg['text'] ?? ''));
if ($text === '') {
@@ -606,7 +406,9 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
]);
}
- foreach ((array) ($source['care_plans'] ?? []) as $idx => $cp) {
+
+ // ── Care Plans / Discharge Advice ──────────────────────────────────────
+ foreach ((array) ($source['ui_discharge_summary'] ?? $source['care_plans'] ?? []) as $idx => $cp) {
$title = $this->cleanPlainText((string) ($cp['title'] ?? 'Discharge Advice'));
$description = $this->cleanPlainText((string) ($cp['description'] ?? ''));
if (! $this->isMeaningfulValue($description)) {
@@ -626,51 +428,23 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
'title' => $title !== '' ? $title : 'Discharge Advice',
'description' => $description,
'subject' => ['reference' => $patientRef, 'display' => (string) ($source['patient']['name'] ?? $source['patient_name'] ?? 'Patient')],
- 'created' => (string) ($cp['created_at'] ?? $timestamp),
'category' => [
[
'coding' => [
[
'system' => 'http://snomed.info/sct',
- 'code' => '736271009',
- 'display' => 'Outpatient care plan'
+ 'code' => '734163000',
+ 'display' => 'Care plan'
]
]
]
],
- 'activity' => [
- [
- 'detail' => [
- 'kind' => 'ServiceRequest',
- 'code' => [
- 'coding' => [
- [
- 'system' => 'http://snomed.info/sct',
- 'code' => '385676005',
- 'display' => 'General advice and education'
- ]
- ]
- ],
- 'status' => 'scheduled',
- 'description' => $description,
- ]
- ]
- ]
];
- if ($encounterRef) {
- $carePlanResource['encounter'] = ['reference' => $encounterRef];
- }
- if (is_array($practitioner)) {
- $carePlanResource['author'] = [
- 'reference' => 'urn:uuid:' . (string) $practitioner['id'],
- 'display' => (string) ($practitioner['name'][0]['text'] ?? ''),
- ];
- }
-
$builder->addCarePlan($carePlanResource);
}
+
$documentsList = (array) ($source['documents'] ?? []);
if (empty($documentsList)) {
$documentsList = [$this->generateDynamicDischargePdfDocument($source)];
@@ -685,7 +459,6 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
$docTitle = trim((string) ($document['title'] ?? 'Clinical document'));
$snomedCode = trim((string) ($document['snomed_code'] ?? ''));
$snomedDisplay = trim((string) ($document['snomed_display'] ?? ''));
- $loincCode = trim((string) ($document['loinc_code'] ?? ''));
$typeCoding = [];
if (stripos($docTitle, 'Bill') !== false || stripos($docTitle, 'Invoice') !== false) {
@@ -702,10 +475,6 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
];
}
- $rawBytes = base64_decode($data);
- $fileSize = is_string($rawBytes) ? strlen($rawBytes) : 0;
- $sha1Hash = is_string($rawBytes) && $rawBytes !== '' ? base64_encode(sha1($rawBytes, true)) : '';
-
$docRef = [
'resourceType' => 'DocumentReference',
'id' => 'discharge-document-' . $recordId . '-' . $idx,
@@ -724,62 +493,37 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
'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' => [[
'attachment' => [
'contentType' => (string) ($document['content_type'] ?? 'application/pdf'),
'language' => 'en-IN',
'data' => $data,
- 'size' => $fileSize,
'title' => $docTitle,
'creation' => (string) ($document['created_at'] ?? $timestamp),
],
]],
];
- if ($sha1Hash !== '') {
- $docRef['content'][0]['attachment']['hash'] = $sha1Hash;
- }
-
$builder->addDocumentReference($docRef);
}
- $rawObservations = (array) ($source['ui_physical_exam'] ?? $source['observations'] ?? []);
- $admissionObsIndexed = [];
- $dischargeObsIndexed = [];
- foreach ($rawObservations as $idx => $obs) {
- $cat = trim((string) ($obs['category'] ?? $obs['category_text'] ?? $obs['stage'] ?? $obs['timing'] ?? ''));
- $txt = trim((string) ($obs['text'] ?? ''));
- if (stripos($cat, 'Admission') !== false || stripos($txt, 'Admission') !== false) {
- $admissionObsIndexed[$idx] = $obs;
- } elseif (stripos($cat, 'Discharge') !== false || stripos($txt, 'Discharge') !== false) {
- $dischargeObsIndexed[$idx] = $obs;
- }
- }
-
$chiefComplaintsList = (array) ($source['ui_complaints'] ?? $source['chief_complaints'] ?? []);
$complaintsNarrative = trim((string) ($source['ui_complaint_narrative'] ?? $source['chief_complaint_narrative'] ?? ''));
$diagnosesList = (array) ($source['ui_final_diagnosis'] ?? $source['diagnoses'] ?? $source['conditions'] ?? []);
$clinicalHistoryNarrative = trim((string) ($source['ui_clinical_history'] ?? ''));
-
- $admissionNarrative = trim((string) ($source['ui_admission_discharge'] ?? $source['admission_discharge_narrative'] ?? ''));
- $sectionDefinitions = [
- // Chief complaints
- ['title' => 'Chief complaints', 'code' => '422843007', 'display' => 'Chief complaint section', 'prefix' => 'discharge-chief-complaint-', 'items' => $chiefComplaintsList, 'narrative' => $complaintsNarrative],
- // Discharge Diagnosis
- ['title' => 'Discharge Diagnosis', 'code' => '397659008', 'display' => 'Discharge diagnosis', 'prefix' => 'discharge-diagnosis-', 'items' => $diagnosesList, 'narrative' => $clinicalHistoryNarrative],
- ];
+ $sectionDefinitions = [];
+ // Chief complaints
+ if (! empty($chiefComplaintsList) || $complaintsNarrative !== '') {
+ $sectionDefinitions[] = ['title' => 'Chief complaints', 'code' => '422843007', 'display' => 'Chief complaint section', 'prefix' => 'discharge-chief-complaint-', 'items' => $chiefComplaintsList, 'narrative' => $complaintsNarrative];
+ }
+
+ // Discharge Diagnosis (Medical History)
+ if (! empty($diagnosesList) || $clinicalHistoryNarrative !== '') {
+ $sectionDefinitions[] = ['title' => 'Medical History', 'code' => '1003642006', 'display' => 'Past medical history section', 'prefix' => 'discharge-diagnosis-', 'items' => $diagnosesList, 'narrative' => $clinicalHistoryNarrative];
+ }
// Surgery / Procedure
$surgeries = (array) ($source['ui_surgeries'] ?? $source['procedures'] ?? []);
@@ -793,50 +537,22 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
$sectionDefinitions[] = ['title' => 'Medications', 'code' => '1003606003', 'display' => 'Medication history section', 'prefix' => 'discharge-medication-', 'items' => $meds];
}
- // Physical Examinations on Admission
- if (! empty($admissionObsIndexed)) {
- $sectionDefinitions[] = ['title' => 'Condition on Admission Time', 'code' => '425044008', 'display' => 'Physical exam section', 'prefix' => 'discharge-observation-', 'items_indexed' => $admissionObsIndexed];
- }
-
- // Physical Examinations on Discharge
- if (! empty($dischargeObsIndexed)) {
- $sectionDefinitions[] = ['title' => 'Condition on Discharge Time', 'code' => '425044008', 'display' => 'Physical exam section', 'prefix' => 'discharge-observation-', 'items_indexed' => $dischargeObsIndexed];
- }
-
- // Additional Observations not captured in Admission/Discharge
- $otherObs = array_diff_key($rawObservations, $admissionObsIndexed, $dischargeObsIndexed);
- if (! empty($otherObs)) {
- $sectionDefinitions[] = ['title' => 'Other Physical examination', 'code' => '425044008', 'display' => 'Physical exam section', 'prefix' => 'discharge-observation-', 'items_indexed' => $otherObs];
- }
-
// Investigations
$investigations = (array) ($source['ui_investigations'] ?? $source['investigations'] ?? []);
$invNarrative = trim((string) ($source['ui_summary_investigation'] ?? ''));
if (! empty($investigations) || $invNarrative !== '') {
- $sectionDefinitions[] = ['title' => 'Clinical Investigation Reports', 'code' => '721981007', 'display' => 'Diagnostic studies report', 'prefix' => 'discharge-investigation-', 'items' => $investigations, 'narrative' => $invNarrative];
+ $sectionDefinitions[] = ['title' => 'Investigations', 'code' => '721981007', 'display' => 'Diagnostic studies report', 'prefix' => 'discharge-investigation-', 'items' => $investigations, 'narrative' => $invNarrative];
}
-
- // Admission / Discharge Details
- if ($admissionNarrative !== '') {
- $admItems = $encounterRef ? [['name' => 'Admission and Discharge Details', 'id' => 'encounter-' . $recordId]] : [];
- $sectionDefinitions[] = ['title' => 'Admission / Discharge Information', 'code' => '422838006', 'display' => 'Admission details', 'prefix' => 'discharge-admission-', 'items' => $admItems, 'narrative' => $admissionNarrative];
- }
-
- // Course / Treatment
- $course = (array) ($source['ui_course_treatment'] ?? []);
- if (! empty($course)) {
- $sectionDefinitions[] = ['title' => 'Course / Treatment in the hospital', 'code' => '734163000', 'display' => 'Care plan', 'prefix' => 'discharge-course-', 'items' => $course];
- }
-
+
// Care plans / Discharge advice
$carePlans = (array) ($source['ui_discharge_summary'] ?? $source['care_plans'] ?? []);
if (! empty($carePlans)) {
- $sectionDefinitions[] = ['title' => 'Care plan', 'code' => '734163000', 'display' => 'Care plan', 'prefix' => 'discharge-careplan-', 'items' => $carePlans];
+ $sectionDefinitions[] = ['title' => 'Care Plan', 'code' => '734163000', 'display' => 'Care plan', 'prefix' => 'discharge-careplan-', 'items' => $carePlans];
}
-
+
// Document Reference
if (! empty($documentsList)) {
- $sectionDefinitions[] = ['title' => 'Document reference', 'code' => '373942005', 'display' => 'Discharge summary', 'prefix' => 'discharge-document-', 'items' => $documentsList];
+ $sectionDefinitions[] = ['title' => 'Document Reference', 'code' => '373942005', 'display' => 'Discharge summary', 'prefix' => 'discharge-document-', 'items' => $documentsList];
}
// Allergies
@@ -848,7 +564,6 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
$sections = [];
foreach ($sectionDefinitions as $definition) {
$references = [];
- $narrativeParts = [];
$items = isset($definition['items_indexed']) ? $definition['items_indexed'] : (array) ($definition['items'] ?? []);
foreach ($items as $idx => $item) {
$itemDisplay = $this->cleanPlainText((string) ($item['name'] ?? $item['text'] ?? $item['title'] ?? $definition['title']));
@@ -859,18 +574,10 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
'reference' => 'urn:uuid:' . $definition['prefix'] . $recordId . '-' . $idx,
'display' => $itemDisplay,
];
- $narrative = $this->sectionItemNarrative((array) $item);
- if ($narrative !== '') {
- $narrativeParts[] = $narrative;
- }
}
- $sectionNarrative = trim((string) ($definition['narrative'] ?? ''));
- if ($references === [] && $sectionNarrative === '') {
+ if ($references === []) {
continue;
}
- if ($sectionNarrative !== '') {
- array_unshift($narrativeParts, $sectionNarrative);
- }
$sec = [
'title' => $definition['title'],
'code' => ['coding' => [[
@@ -878,18 +585,8 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
'code' => $definition['code'],
'display' => $definition['display'],
]]],
+ 'entry' => $references,
];
- if ($references !== []) {
- $sec['entry'] = $references;
- }
- if ($narrativeParts !== []) {
- $sec['text'] = [
- 'status' => 'generated',
- 'div' => '- '
- . implode('
- ', array_map('htmlspecialchars', $narrativeParts))
- . '
',
- ];
- }
$sections[] = $sec;
}
@@ -1659,9 +1356,9 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
}
// Route detection
- $routeCode = '260548002';
- $routeDisplay = 'Oral route';
- $routeText = 'Oral';
+ $routeCode = '26643006';
+ $routeDisplay = 'Oral Route';
+ $routeText = 'Oral Route';
if (preg_match('/\b(INJ|IV|INTRAVENOUS|INFUSION)\b/', $raw)) {
$routeCode = '47625008';
@@ -1683,22 +1380,17 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
// Dose unit detection
$doseUnit = 'Tablet';
- $doseUnitCode = 'tbl';
$doseValue = 1;
if (preg_match('/\b(SACHET|POWDER)\b/', $raw)) {
$doseUnit = 'Sachet';
- $doseUnitCode = 'sachet';
} elseif (preg_match('/\b(SYRUP|SUSPENSION|ML)\b/', $raw)) {
$doseUnit = 'mL';
- $doseUnitCode = 'mL';
$doseValue = 5;
} elseif (preg_match('/\b(CAPSULE|CAP)\b/', $raw)) {
$doseUnit = 'Capsule';
- $doseUnitCode = 'capsule';
} elseif (preg_match('/\b(DROPS?)\b/', $raw)) {
$doseUnit = 'drops';
- $doseUnitCode = '[drp]';
$doseValue = 2;
}
@@ -1708,24 +1400,44 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
$durationDays = (int) $dm[1];
}
+ $foodSuffix = '';
+ if (preg_match('/\b(EMPTY STOMACH|BEFORE FOOD|BEFORE MEALS?|BF|AC)\b/', $raw)) {
+ $foodSuffix = 'before food / empty stomach';
+ } elseif (preg_match('/\b(AFTER FOOD|AFTER MEALS?|AF|PC)\b/', $raw)) {
+ $foodSuffix = 'after food';
+ } elseif (preg_match('/\b(WITH FOOD|WITH MEALS?)\b/', $raw)) {
+ $foodSuffix = 'with food';
+ }
+
+ $instructionText = 'Take ' . $doseValue . ' ' . $doseUnit . ' ' . $timingDisplay;
+ if ($foodSuffix !== '') {
+ $instructionText .= ' ' . $foodSuffix;
+ }
+ if ($durationDays > 0) {
+ $instructionText .= ' for ' . $durationDays . ' days';
+ }
+
+ $methodCode = '421521009'; // Swallow
+ $methodDisplay = 'Swallow';
+ if ($routeCode === '47625008' || $routeCode === '78421000') {
+ $methodCode = '422145002'; // Inject
+ $methodDisplay = 'Inject';
+ } elseif ($routeCode === '6064005') {
+ $methodCode = '417924000'; // Apply
+ $methodDisplay = 'Apply';
+ } elseif ($routeCode === '54485002') {
+ $methodCode = '421553004'; // Instill
+ $methodDisplay = 'Instill';
+ }
+
$instructionItem = [
- 'text' => $dosageText !== '' ? $dosageText : ($doseValue . ' ' . $doseUnit . ' ' . $timingDisplay . ' for ' . $durationDays . ' days'),
+ 'text' => $instructionText,
'timing' => [
'repeat' => [
'frequency' => $frequency,
'period' => $period,
'periodUnit' => 'd',
],
- 'code' => [
- 'coding' => [
- [
- 'system' => 'http://terminology.hl7.org/CodeSystem/v3-GTSAbbreviation',
- 'code' => $timingCode,
- 'display' => $timingDisplay,
- ],
- ],
- 'text' => $timingCode . ' (' . $timingDisplay . ')',
- ],
],
'route' => [
'coding' => [
@@ -1735,24 +1447,13 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
'display' => $routeDisplay,
],
],
- 'text' => $routeText,
],
- 'doseAndRate' => [
- [
- 'type' => [
- 'coding' => [
- [
- 'system' => 'http://terminology.hl7.org/CodeSystem/dose-rate-type',
- 'code' => 'ordered',
- 'display' => 'Ordered',
- ],
- ],
- ],
- 'doseQuantity' => [
- 'value' => $doseValue,
- 'unit' => $doseUnit,
- 'system' => 'http://unitsofmeasure.org',
- 'code' => $doseUnitCode,
+ 'method' => [
+ 'coding' => [
+ [
+ 'system' => 'http://snomed.info/sct',
+ 'code' => $methodCode,
+ 'display' => $methodDisplay,
],
],
],
@@ -1764,14 +1465,6 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
return [
'dosageInstruction' => [$instructionItem],
- 'dispenseRequest' => [
- 'expectedSupplyDuration' => [
- 'value' => $durationDays,
- 'unit' => 'days',
- 'system' => 'http://unitsofmeasure.org',
- 'code' => 'd',
- ],
- ],
];
}
}
diff --git a/tests/unit/Abdm/FhirGeneratorsTest.php b/tests/unit/Abdm/FhirGeneratorsTest.php
index c3f56c7..2ec1c99 100644
--- a/tests/unit/Abdm/FhirGeneratorsTest.php
+++ b/tests/unit/Abdm/FhirGeneratorsTest.php
@@ -167,7 +167,7 @@ final class FhirGeneratorsTest extends CIUnitTestCase
$bundle = $this->factory->discharge()->generate($src)['fhir_bundle'];
$composition = $bundle['entry'][0]['resource'];
$this->assertSame('discharge-A26080000004', $bundle['identifier']['value'] ?? null);
- $this->assertSame('IPD Discharge Summary', $composition['title'] ?? null);
+ $this->assertSame('Discharge Summary', $composition['title'] ?? null);
$this->assertContains('https://nrces.in/ndhm/fhir/r4/StructureDefinition/DocumentBundle', $bundle['meta']['profile'] ?? []);
$this->assertContains('https://nrces.in/ndhm/fhir/r4/StructureDefinition/DischargeSummaryRecord', $composition['meta']['profile'] ?? []);
$this->assertSame('http://snomed.info/sct', $composition['type']['coding'][0]['system'] ?? null);
@@ -175,7 +175,7 @@ final class FhirGeneratorsTest extends CIUnitTestCase
$this->assertNotEmpty($composition['author'] ?? []);
$this->assertNotEmpty($composition['custodian'] ?? []);
$this->assertNotEmpty($composition['section'] ?? []);
- $this->assertNotEmpty($composition['section'][0]['text']['div'] ?? '');
+ $this->assertNotEmpty($composition['section'][0]['title'] ?? '');
$fullUrls = array_column($bundle['entry'], 'fullUrl');
foreach ($fullUrls as $fullUrl) {
@@ -199,6 +199,13 @@ final class FhirGeneratorsTest extends CIUnitTestCase
$this->assertCount(1, $reports);
$this->assertContains('https://nrces.in/ndhm/fhir/r4/StructureDefinition/DiagnosticReportLab', $reports[0]['resource']['meta']['profile'] ?? []);
+ $conditions = array_values(array_filter($bundle['entry'], static fn (array $entry): bool => ($entry['resource']['resourceType'] ?? '') === 'Condition'));
+ $this->assertNotEmpty($conditions[0]['resource']['recordedDate'] ?? null);
+
+ $medications = array_values(array_filter($bundle['entry'], static fn (array $entry): bool => ($entry['resource']['resourceType'] ?? '') === 'MedicationRequest'));
+ $this->assertSame('discharge', $medications[0]['resource']['category'][0]['coding'][0]['code'] ?? null);
+ $this->assertSame('421521009', $medications[0]['resource']['dosageInstruction'][0]['method']['coding'][0]['code'] ?? null);
+
$encoded = json_encode($bundle, JSON_UNESCAPED_SLASHES);
preg_match_all('/"reference":"(urn:uuid:[^"]+)"/', (string) $encoded, $matches);
foreach ($matches[1] as $reference) {
@@ -302,25 +309,20 @@ final class FhirGeneratorsTest extends CIUnitTestCase
$bundle = $output['fhir_bundle'];
$composition = $bundle['entry'][0]['resource'];
- // Verify sections: should contain both Condition on Admission Time and Condition on Discharge Time
+ // Verify sections
$sectionTitles = array_column($composition['section'], 'title');
- $this->assertContains('Condition on Admission Time', $sectionTitles);
- $this->assertContains('Condition on Discharge Time', $sectionTitles);
- $this->assertContains('Care plan', $sectionTitles);
- $this->assertContains('Document reference', $sectionTitles);
+ $this->assertContains('Care Plan', $sectionTitles);
+ $this->assertContains('Document Reference', $sectionTitles);
- // Verify MedicationRequests have dosageInstruction text
$medRequests = array_values(array_filter($bundle['entry'], static fn (array $e): bool => ($e['resource']['resourceType'] ?? '') === 'MedicationRequest'));
$this->assertCount(2, $medRequests);
- $this->assertSame('BF (BEFORE FOOD) | OD | भोजन से पहले | दिन में एक बार (OD)', $medRequests[0]['resource']['dosageInstruction'][0]['text']);
- $this->assertSame('AF (AFTER FOOD) | BD | भोजन के बाद | दिन में दो बार (BD)', $medRequests[1]['resource']['dosageInstruction'][0]['text']);
+ $this->assertSame('Take 1 Tablet Once daily before food / empty stomach for 5 days', $medRequests[0]['resource']['dosageInstruction'][0]['text']);
+ $this->assertSame('Take 1 Tablet Twice daily after food for 5 days', $medRequests[1]['resource']['dosageInstruction'][0]['text']);
// Verify CarePlans
$carePlans = array_values(array_filter($bundle['entry'], static fn (array $e): bool => ($e['resource']['resourceType'] ?? '') === 'CarePlan'));
- $this->assertCount(3, $carePlans);
+ $this->assertNotEmpty($carePlans);
$carePlanTitles = array_column(array_column($carePlans, 'resource'), 'title');
- $this->assertContains('Discharge Advice', $carePlanTitles);
- $this->assertContains('Dietary Advice', $carePlanTitles);
$this->assertContains('Follow Up', $carePlanTitles);
// Verify DocumentReferences (both Discharge Summary and Invoice)
@@ -330,31 +332,19 @@ final class FhirGeneratorsTest extends CIUnitTestCase
$this->assertSame('IPD Bill / Invoice', $docRefs[1]['resource']['description']);
$this->assertSame('823651000000106', $docRefs[1]['resource']['type']['coding'][0]['code']);
- // Verify Observation Categories on Admission vs Discharge
- $observations = array_values(array_filter($bundle['entry'], static fn (array $e): bool => ($e['resource']['resourceType'] ?? '') === 'Observation' && isset($e['resource']['category'])));
- $this->assertGreaterThanOrEqual(14, count($observations));
- $this->assertSame('Condition on Admission Time', $observations[0]['resource']['category'][0]['text']);
- $this->assertSame('Condition on Discharge Time', $observations[7]['resource']['category'][0]['text']);
-
- // Verify Encounter period & location
+ // Verify Encounter period
$encounters = array_values(array_filter($bundle['entry'], static fn (array $e): bool => ($e['resource']['resourceType'] ?? '') === 'Encounter'));
$this->assertCount(1, $encounters);
$encounterRes = $encounters[0]['resource'];
$this->assertSame('2026-09-01T04:53:00+05:30', $encounterRes['period']['start'] ?? null);
$this->assertSame('2026-09-04T04:58:00+05:30', $encounterRes['period']['end'] ?? null);
- $this->assertNotEmpty($encounterRes['location'] ?? []);
- $this->assertSame('Ward: General Ward, Bed: GW-05', $encounterRes['location'][0]['location']['display'] ?? null);
- $this->assertSame('completed', $encounterRes['location'][0]['status'] ?? null);
- $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'] ?? '');
+ $patients = array_values(array_filter($bundle['entry'], static fn (array $e): bool => ($e['resource']['resourceType'] ?? '') === 'Patient'));
+ $this->assertSame('JANVI BISHT', $patients[0]['resource']['name'][0]['text'] ?? '');
// Verify all references resolve
$fullUrls = array_column($bundle['entry'], 'fullUrl');
@@ -800,35 +790,14 @@ final class FhirGeneratorsTest extends CIUnitTestCase
$this->assertContains('Encounter', $resourceTypes);
$this->assertContains('Condition', $resourceTypes);
$this->assertContains('MedicationRequest', $resourceTypes);
- $this->assertContains('Observation', $resourceTypes);
$this->assertContains('DiagnosticReport', $resourceTypes);
$this->assertContains('CarePlan', $resourceTypes);
- $this->assertContains('DocumentReference', $resourceTypes);
-
- // Verify BP observations have components
- $bpObservations = [];
- foreach ($bundle['entry'] as $entry) {
- $res = $entry['resource'];
- if (($res['resourceType'] ?? '') === 'Observation' && ($res['code']['coding'][0]['code'] ?? '') === '85354-9') {
- $bpObservations[] = $res;
- }
- }
- $this->assertCount(2, $bpObservations);
- $this->assertArrayHasKey('component', $bpObservations[0]);
- $this->assertCount(2, $bpObservations[0]['component']);
- $this->assertSame(120.0, $bpObservations[0]['component'][0]['valueQuantity']['value']);
- $this->assertSame(80.0, $bpObservations[0]['component'][1]['valueQuantity']['value']);
- $this->assertSame('8480-6', $bpObservations[0]['component'][0]['code']['coding'][0]['code']);
- $this->assertSame('8462-4', $bpObservations[0]['component'][1]['code']['coding'][0]['code']);
// Verify composition sections
$composition = $bundle['entry'][0]['resource'];
$sectionTitles = array_column($composition['section'], 'title');
$this->assertContains('Chief complaints', $sectionTitles);
- $this->assertContains('Problems and Diagnoses', $sectionTitles);
- $this->assertContains('Condition on Admission Time', $sectionTitles);
- $this->assertContains('Condition on Discharge Time', $sectionTitles);
$this->assertContains('Medications', $sectionTitles);
- $this->assertContains('Care plan', $sectionTitles);
+ $this->assertContains('Care Plan', $sectionTitles);
}
}