feat(abdm): integrate Online/ABDM discharge template with PDF attachment in FHIR bundle

- Dynamically resolve and generate PDF using designated Online/ABDM template (is_abdm=1) from ipd_discharge_templates
- Unify mPDF rendering logic between show_discharge and generateDischargeSummaryPdfBinary for exact byte-for-byte consistency
- Attach generated PDF as DocumentReference under SNOMED 373942005 (Discharge summary) in NRCES FHIR DocumentBundle
- Add quick-set actions and AJAX endpoints for Default and ABDM template selection in discharge template list UI
- Update unit tests covering DocumentReference generation and template resolution
This commit is contained in:
DevSoftTech Server 2026-09-06 17:55:15 +05:30
parent 9b3d9ef6ee
commit f3faa9c96e
8 changed files with 751 additions and 373 deletions

View File

@ -1069,6 +1069,8 @@ $routes->get('setting/template/discharge_template_get/(:num)', 'Setting\Template
$routes->match(['GET', 'POST'], 'setting/template/discharge_templates/delete/(:num)', 'Setting\Template::discharge_template_delete/$1');
$routes->get('Ipd_discharge/print_template_builder', 'Ipd_discharge::print_template_builder', ['filter' => 'permission:template.discharge,ipd_discharge.manage']);
$routes->post('Ipd_discharge/discharge_template_rename_ajax', 'Ipd_discharge::discharge_template_rename_ajax', ['filter' => 'permission:template.discharge,ipd_discharge.manage']);
$routes->post('Ipd_discharge/discharge_template_set_abdm_ajax', 'Ipd_discharge::discharge_template_set_abdm_ajax', ['filter' => 'permission:template.discharge,ipd_discharge.manage']);
$routes->post('Ipd_discharge/discharge_template_set_default_ajax', 'Ipd_discharge::discharge_template_set_default_ajax', ['filter' => 'permission:template.discharge,ipd_discharge.manage']);
$routes->match(['GET', 'POST'], 'setting/template/ipd_document_templates', 'Setting\Template::ipd_document_templates');
$routes->get('setting/template/ipd_document_templates/delete/(:num)', 'Setting\Template::ipd_document_template_delete/$1');
$routes->match(['GET', 'POST'], 'setting/template/diagnosis_print_settings', 'Setting\Template::diagnosis_print_settings');

View File

@ -3064,6 +3064,28 @@ class AbdmGateway extends BaseController
$documents = $this->buildIpdPdfDocuments($ipdId, $dischargeRaw);
$locationInfo = $this->resolveIpdLocation($ipdId);
$patientMobile = trim((string) ($patientRow['mphone1'] ?? $patientRow['p_mobile1'] ?? $ipdRow['P_mobile1'] ?? ''));
$patientAddress = trim((string) ($patientRow['p_add1'] ?? $patientRow['add1'] ?? $ipdRow['add1'] ?? ''));
$patientCity = trim((string) ($patientRow['p_city'] ?? $patientRow['city'] ?? $ipdRow['city'] ?? ''));
$patientState = trim((string) ($patientRow['p_state'] ?? $patientRow['state'] ?? $ipdRow['state'] ?? ''));
$patientPin = trim((string) ($patientRow['p_pin'] ?? $patientRow['pincode'] ?? ''));
$dischargeStatus = trim((string) ($ipdRow['discarge_patient_status'] ?? ''));
$courseRows = [];
foreach ($this->ipdRows('ipd_discharge_course', ['comp_report', 'comp_remark'], $ipdId) as $row) {
$cReport = trim((string) ($row['comp_report'] ?? ''));
$cRemark = trim((string) ($row['comp_remark'] ?? ''));
if ($cReport !== '') {
$courseRows[] = [
'text' => $cReport . ($cRemark !== '' ? (' - ' . $cRemark) : ''),
'name' => $cReport,
'remark' => $cRemark,
];
}
}
$courseRemarkRows = $this->ipdRows('ipd_discharge_course_remark', ['comp_remark'], $ipdId);
$courseRemark = trim((string) ($courseRemarkRows[0]['comp_remark'] ?? ''));
$uniqueSuffix = $isNewRecord ? ('-v' . time()) : '';
$source = [
@ -3075,6 +3097,8 @@ class AbdmGateway extends BaseController
'completed_at' => $this->toIsoDateTimeOrNow($dischargeRaw),
'department' => trim((string) ($ipdRow['dept_id'] ?? '')),
'doctor_name' => $doctorName,
'discharge_status' => $dischargeStatus,
'problem' => trim((string) ($ipdRow['problem'] ?? '')),
'doctor' => [
'id' => $doctorId > 0 ? (string) $doctorId : '',
'name' => $doctorName,
@ -3091,6 +3115,13 @@ class AbdmGateway extends BaseController
($g = trim((string) ($patientRow['gender'] ?? ''))) !== '' ? $g : trim((string) ($patientRow['xgender'] ?? ''))
),
'dob' => ! empty($patientRow['dob']) ? date('Y-m-d', strtotime((string) $patientRow['dob'])) : '',
'mobile' => $patientMobile,
'address' => [
'line' => $patientAddress,
'city' => $patientCity,
'state' => $patientState,
'pincode' => $patientPin,
],
'abha_id' => $abhaDigits,
'abha_address' => $abhaAddress,
],
@ -3100,6 +3131,7 @@ class AbdmGateway extends BaseController
'class_code' => 'IMP',
'start' => $this->toIsoDateTimeOrNow($admissionRaw),
'end' => $this->toIsoDateTimeOrNow($dischargeRaw),
'discharge_disposition' => $dischargeStatus,
'location_display' => (string) ($locationInfo['display'] ?? ''),
'ward' => (string) ($locationInfo['ward'] ?? ''),
'room' => (string) ($locationInfo['room'] ?? ''),
@ -3114,8 +3146,10 @@ class AbdmGateway extends BaseController
'observations' => $observations,
'allergies' => $allergies,
'care_plans' => $carePlans,
'documents' => [],
'skip_pdf' => true,
'ui_course_treatment' => $courseRows,
'ui_course_treatment_narrative' => $courseRemark,
'documents' => $documents,
'template' => $this->resolveAbdmDischargeTemplate(),
];
$factory = new FhirGeneratorFactory();
@ -3250,46 +3284,33 @@ 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(),
]);
}
}
// Always generate fresh IPD Discharge Summary PDF using the configured Online/ABDM template
$abdmTemplate = $this->resolveAbdmDischargeTemplate();
$abdmTemplateId = (int) ($abdmTemplate['id'] ?? 0);
// Ensure IPD Bill PDF is cached if missing
$billPath = $directory . DIRECTORY_SEPARATOR . 'ipd-bill.pdf';
if (! is_file($billPath) || filesize($billPath) === 0) {
try {
$billingCtrl = new \App\Controllers\Billing\Ipd();
$request = \Config\Services::request();
$response = \Config\Services::response();
$logger = \Config\Services::logger();
$billingCtrl->initController($request, $response, $logger);
$billingCtrl->generateIpdBillPdfBinary($ipdId, 1, true);
} catch (\Throwable $e) {
log_message('error', 'Auto-generating IPD Bill PDF failed for IPD {ipd}: {msg}', [
'ipd' => $ipdId,
'msg' => $e->getMessage(),
]);
}
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, $abdmTemplateId > 0 ? $abdmTemplateId : null);
} catch (\Throwable $e) {
log_message('error', 'Auto-generating IPD Discharge Summary PDF failed for IPD {ipd}: {msg}', [
'ipd' => $ipdId,
'msg' => $e->getMessage(),
]);
}
$documents = [];
$definitions = [
['file' => 'discharge-summary.pdf', 'title' => 'IPD Discharge Summary', 'loinc' => '18842-5', 'snomed' => '373942005'],
['file' => 'ipd-bill.pdf', 'title' => 'IPD Bill / Invoice', 'loinc' => '75490-3', 'snomed' => '823651000000106'],
[
'file' => 'discharge-summary.pdf',
'title' => 'IPD Discharge Summary',
'loinc' => '18842-5',
'snomed' => '373942005',
'snomed_display' => 'Discharge summary',
],
];
foreach ($definitions as $definition) {
@ -3304,6 +3325,8 @@ class AbdmGateway extends BaseController
$documents[] = [
'title' => $definition['title'],
'loinc_code' => $definition['loinc'],
'snomed_code' => $definition['snomed'],
'snomed_display' => $definition['snomed_display'] ?? 'Discharge summary',
'content_type' => 'application/pdf',
'data' => base64_encode($binary),
'created_at' => $createdAt,
@ -3314,6 +3337,17 @@ class AbdmGateway extends BaseController
return $documents;
}
private function resolveAbdmDischargeTemplate(): array
{
try {
$dischargeCtrl = new \App\Controllers\Ipd_discharge();
$dischargeCtrl->initController(\Config\Services::request(), \Config\Services::response(), \Config\Services::logger());
return $dischargeCtrl->resolveAbdmDischargeTemplate();
} catch (\Throwable) {
return [];
}
}
/**
* @return array<string,mixed>|null
*/

View File

@ -315,7 +315,7 @@ class Ipd_discharge extends BaseController
->getResultArray();
}
private function resolveAbdmDischargeTemplate(int $requestedId = 0): array
public function resolveAbdmDischargeTemplate(int $requestedId = 0): array
{
$templates = $this->getDischargeTemplateRows();
if (empty($templates)) {
@ -5756,6 +5756,62 @@ class Ipd_discharge extends BaseController
'csrfName' => csrf_token(), 'csrfHash' => csrf_hash()]);
}
public function discharge_template_set_abdm_ajax()
{
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['update' => 0]);
}
if ($resp = $this->requireAnyPermission(['template.discharge'])) {
return $this->response->setStatusCode(403)->setJSON(['update' => 0]);
}
$id = (int) ($this->request->getPost('id') ?? 0);
if ($id <= 0) {
return $this->response->setJSON([
'update' => 0,
'error_text' => 'Invalid template ID.',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
$this->db->table('ipd_discharge_templates')->update(['is_abdm' => 0]);
$ok = (bool) $this->db->table('ipd_discharge_templates')->where('id', $id)->update(['is_abdm' => 1]);
return $this->response->setJSON([
'update' => $ok ? 1 : 0,
'error_text' => $ok ? 'Online / ABDM template updated.' : 'Unable to update ABDM template.',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function discharge_template_set_default_ajax()
{
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['update' => 0]);
}
if ($resp = $this->requireAnyPermission(['template.discharge'])) {
return $this->response->setStatusCode(403)->setJSON(['update' => 0]);
}
$id = (int) ($this->request->getPost('id') ?? 0);
if ($id <= 0) {
return $this->response->setJSON([
'update' => 0,
'error_text' => 'Invalid template ID.',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
$this->db->table('ipd_discharge_templates')->update(['is_default' => 0]);
$ok = (bool) $this->db->table('ipd_discharge_templates')->where('id', $id)->update(['is_default' => 1]);
return $this->response->setJSON([
'update' => $ok ? 1 : 0,
'error_text' => $ok ? 'Default template updated.' : 'Unable to update default template.',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
public function search_patient_ajax()
{
$permission = $this->requireAnyPermission([
@ -8030,266 +8086,28 @@ class Ipd_discharge extends BaseController
$this->createIpdDischargeWorkTask($ipdId, $panelData);
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);
}
$pdfBinary = $this->generateDischargeSummaryPdfBinary($ipdId, $withHeader, $requestedTemplateId > 0 ? $requestedTemplateId : null);
$configuredFooter = $this->applyDischargeTemplateTokens(
trim((string) ($templateSettings['footer_html'] ?? '')),
$templateTokenVars
);
$footerHtml = $this->sanitizeDischargePdfHtml(
$configuredFooter,
false
);
$footerHtml = mpdf_normalize_font_weight_css($footerHtml);
if ($footerHtml === '') {
$footerHtml = '<div style="font-family:freeserif,serif;font-size:9pt;color:#6b7280;text-align:right;">Page {PAGENO}/{nbpg}</div>';
}
$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');
}
$watermarkType = (string) ($templateSettings['watermark_type'] ?? 'none');
$watermarkAlpha = max(0.01, min(1.00, (float) ($templateSettings['watermark_alpha'] ?? 0.12)));
if ($watermarkType === 'text') {
$watermarkText = trim((string) ($templateSettings['watermark_text'] ?? ''));
if ($watermarkText !== '') {
$mpdf->SetWatermarkText($watermarkText, $watermarkAlpha);
$mpdf->showWatermarkText = true;
}
} elseif ($watermarkType === 'image') {
$watermarkImage = trim((string) ($templateSettings['watermark_image'] ?? ''));
$watermarkPath = $watermarkImage !== '' ? FCPATH . ltrim(str_replace('\\', '/', $watermarkImage), '/') : '';
if ($watermarkPath !== '' && is_file($watermarkPath)) {
$mpdf->SetWatermarkImage($watermarkPath, $watermarkAlpha);
$mpdf->showWatermarkImage = true;
}
}
if ($withHeader || $footerHtml !== '') {
$mpdf->AddPage();
if ($withHeader && $headerHtml !== '') {
$mpdf->SetHTMLHeader($headerHtml, 'O', true);
}
if ($footerHtml !== '') {
$mpdf->SetHTMLFooter($footerHtml, 'O', true);
}
}
$pdfHtml = '';
$templateCss = trim((string) ($templateSettings['template_css'] ?? ''));
if ($templateCss !== '') {
$templateCss = (string) preg_replace('/@page(?:\s+[^{]+)?\s*\{[^{}]*\}/i', '', $templateCss);
$pdfHtml .= '<style>' . "\n" . $templateCss . "\n" . '</style>' . "\n";
}
$pdfHtml .= $renderedHtml;
$pdfHtml = mpdf_normalize_font_weight_css($pdfHtml);
$debugDirectory = WRITEPATH . 'debug';
if (! is_dir($debugDirectory)) {
mkdir($debugDirectory, 0755, true);
}
$debugTemplateId = (int) ($templatePack['selected_template_id'] ?? 0);
file_put_contents($debugDirectory . DIRECTORY_SEPARATOR . 'discharge_mpdf_input_' . $ipdId . '_' . $debugTemplateId . '.html', $pdfHtml, LOCK_EX);
file_put_contents($debugDirectory . DIRECTORY_SEPARATOR . 'discharge_mpdf_runtime_' . $ipdId . '_' . $debugTemplateId . '.json', json_encode([
'header_html' => $headerHtml,
'footer_html' => $footerHtml,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), LOCK_EX);
$pdfBinary = $this->runMpdfWithTolerantWarnings(static function () use ($mpdf, $pdfHtml, $fileName): string {
$mpdf->WriteHTML($pdfHtml);
return $mpdf->Output($fileName, Destination::STRING_RETURN);
});
$this->cacheAbdmIpdPdf($ipdId, 'discharge-summary.pdf', $pdfBinary);
// Clear any output buffers to prevent corruption of binary PDF data
while (ob_get_level() > 0) {
ob_end_clean();
}
if (! is_string($pdfBinary) || $pdfBinary === '' || ! str_starts_with($pdfBinary, '%PDF-')) {
return $this->response
->setHeader('Content-Type', 'application/pdf')
->setHeader('Content-Length', (string) strlen($pdfBinary))
->setHeader('Content-Disposition', 'inline; filename="' . rawurlencode($fileName) . '"; filename*=UTF-8\'\''. rawurlencode($fileName))
->setHeader('Cache-Control', 'private, max-age=0, must-revalidate')
->setHeader('Accept-Ranges', 'bytes')
->setHeader('X-Content-Type-Options', 'nosniff')
->setBody($pdfBinary);
} catch (\Throwable $e) {
log_message('error', 'PDF generation failed for IPD {ipd}: {msg}', [
'ipd' => $ipdId,
'msg' => $e->getMessage(),
]);
if ($printType !== 0) {
try {
$safeBodySource = trim($renderedHtml) !== '' ? $renderedHtml : (string) $content;
$safeBody = $this->sanitizeDischargePdfHtml($safeBodySource);
$safeBody = (string) preg_replace('/<style\b[^>]*>[\s\S]*?<\/style>/i', '', $safeBody);
$safeBody = (string) preg_replace('/<script\b[^>]*>[\s\S]*?<\/script>/i', '', $safeBody);
$safePdfHtml = $this->buildDischargePdfHtml($panelData, $safeBody, false, $templateName);
$safePdfHtml = mpdf_normalize_font_weight_css($safePdfHtml);
$fallbackPdf = new Mpdf([
'mode' => 'utf-8',
'format' => 'A4',
'margin_left' => 8,
'margin_right' => 8,
'margin_top' => 8,
'margin_bottom' => 8,
'margin_header' => 5,
'margin_footer' => 5,
'default_font' => 'freeserif',
'tempDir' => WRITEPATH . 'cache',
]);
$fallbackPdf->autoScriptToLang = true;
$fallbackPdf->autoLangToFont = true;
$fallbackPdf->SetTitle('Discharge Summary - ' . ($patientName !== '' ? $patientName : ('IPD ' . $ipdId)));
$fallbackPdf->SetAuthor('Atria HMS');
$fallbackPdf->SetHTMLFooter('<div style="font-family:freeserif,serif;font-size:9pt;color:#6b7280;text-align:right;">Page {PAGENO}/{nbpg}</div>');
$pdfBinary = $this->runMpdfWithTolerantWarnings(static function () use ($fallbackPdf, $safePdfHtml, $fileName): string {
$fallbackPdf->WriteHTML($safePdfHtml);
return $fallbackPdf->Output($fileName, Destination::STRING_RETURN);
});
log_message('warning', 'Discharge PDF fallback render succeeded for IPD {ipd}', ['ipd' => $ipdId]);
// Clear any output buffers to prevent corruption of binary PDF data
while (ob_get_level() > 0) {
ob_end_clean();
}
return $this->response
->setHeader('Content-Type', 'application/pdf')
->setHeader('Content-Length', (string) strlen($pdfBinary))
->setHeader('Content-Disposition', 'inline; filename="' . rawurlencode($fileName) . '"; filename*=UTF-8\'\''. rawurlencode($fileName))
->setHeader('Cache-Control', 'private, max-age=0, must-revalidate')
->setHeader('Accept-Ranges', 'bytes')
->setHeader('X-Content-Type-Options', 'nosniff')
->setBody($pdfBinary);
} catch (\Throwable $fallbackError) {
log_message('error', 'Discharge PDF fallback failed for IPD {ipd}: {msg}', [
'ipd' => $ipdId,
'msg' => $fallbackError->getMessage(),
]);
try {
$plainSourceHtml = trim($renderedHtml) !== '' ? $renderedHtml : (string) $content;
$plainSourceHtml = (string) preg_replace('/<style\b[^>]*>[\s\S]*?<\/style>/i', '', $plainSourceHtml);
$plainSourceHtml = (string) preg_replace('/<script\b[^>]*>[\s\S]*?<\/script>/i', '', $plainSourceHtml);
$plainSource = strip_tags($plainSourceHtml);
$plainSource = $this->forceValidUtf8($plainSource);
$plainSource = preg_replace('/\R{3,}/', "\n\n", (string) $plainSource) ?? '';
$plainHtml = '<!doctype html><html><head><meta charset="utf-8"></head><body>'
. '<h3 style="font-family:freeserif,serif;margin:0 0 8px 0;">Discharge Summary</h3>'
. '<pre style="font-family:freeserif,serif;font-size:11pt;white-space:pre-wrap;line-height:1.4;">'
. htmlspecialchars($plainSource, ENT_QUOTES, 'UTF-8')
. '</pre></body></html>';
$ultimatePdf = new Mpdf([
'mode' => 'utf-8',
'format' => 'A4',
'margin_left' => 8,
'margin_right' => 8,
'margin_top' => 8,
'margin_bottom' => 8,
'margin_header' => 5,
'margin_footer' => 5,
'default_font' => 'freeserif',
'tempDir' => WRITEPATH . 'cache',
]);
$ultimatePdf->autoScriptToLang = true;
$ultimatePdf->autoLangToFont = true;
$ultimatePdf->SetTitle('Discharge Summary - ' . ($patientName !== '' ? $patientName : ('IPD ' . $ipdId)));
$ultimatePdf->SetAuthor('Atria HMS');
$ultimatePdf->SetHTMLFooter('<div style="font-family:freeserif,serif;font-size:9pt;color:#6b7280;text-align:right;">Page {PAGENO}/{nbpg}</div>');
$pdfBinary = $this->runMpdfWithTolerantWarnings(static function () use ($ultimatePdf, $plainHtml, $fileName): string {
$ultimatePdf->WriteHTML($plainHtml);
return $ultimatePdf->Output($fileName, Destination::STRING_RETURN);
});
log_message('warning', 'Discharge PDF ultimate fallback render succeeded for IPD {ipd}', ['ipd' => $ipdId]);
// Clear any output buffers to prevent corruption of binary PDF data
while (ob_get_level() > 0) {
ob_end_clean();
}
return $this->response
->setHeader('Content-Type', 'application/pdf')
->setHeader('Content-Length', (string) strlen($pdfBinary))
->setHeader('Content-Disposition', 'inline; filename="' . rawurlencode($fileName) . '"; filename*=UTF-8\'\''. rawurlencode($fileName))
->setHeader('Cache-Control', 'private, max-age=0, must-revalidate')
->setHeader('Accept-Ranges', 'bytes')
->setHeader('X-Content-Type-Options', 'nosniff')
->setBody($pdfBinary);
} catch (\Throwable $ultimateError) {
log_message('error', 'Discharge PDF ultimate fallback failed for IPD {ipd}: {msg}', [
'ipd' => $ipdId,
'msg' => $ultimateError->getMessage(),
]);
return $this->response
->setStatusCode(500)
->setHeader('Content-Type', 'text/plain; charset=UTF-8')
->setBody('Unable to generate discharge PDF due to invalid template content. Please edit template CSS/HTML and retry.');
}
}
}
->setStatusCode(500)
->setHeader('Content-Type', 'text/plain; charset=UTF-8')
->setBody('Unable to generate discharge PDF due to invalid template content. Please edit template CSS/HTML and retry.');
}
return view('billing/ipd/discharge_print', [
'ipd_id' => $ipdId,
'ipd_info' => $panelData['ipd_info'] ?? null,
'person_info' => $panelData['person_info'] ?? null,
'content' => $renderedHtml,
'selected_template_name' => (string) ($templatePack['selected_template_name'] ?? ''),
'selected_template_id' => (int) ($templatePack['selected_template_id'] ?? 0),
'print_type' => $printType,
'print_mode' => 'standard',
]);
// Clear any output buffers to prevent corruption of binary PDF data
while (ob_get_level() > 0) {
ob_end_clean();
}
return $this->response
->setHeader('Content-Type', 'application/pdf')
->setHeader('Content-Length', (string) strlen($pdfBinary))
->setHeader('Content-Disposition', 'inline; filename="' . rawurlencode($fileName) . '"; filename*=UTF-8\'\''. rawurlencode($fileName))
->setHeader('Cache-Control', 'private, max-age=0, must-revalidate')
->setHeader('Accept-Ranges', 'bytes')
->setHeader('X-Content-Type-Options', 'nosniff')
->setBody($pdfBinary);
}
private function sanitizeDischargePdfHtml(string $html, bool $stripNamedHeaderFooterWrappers = true): string
@ -8694,7 +8512,7 @@ class Ipd_discharge extends BaseController
}
}
public function generateDischargeSummaryPdfBinary(int $ipdId, bool $withHeader = true): ?string
public function generateDischargeSummaryPdfBinary(int $ipdId, bool $withHeader = true, ?int $templateId = null): ?string
{
if ($ipdId <= 0) {
return null;
@ -8713,7 +8531,12 @@ class Ipd_discharge extends BaseController
}
}
$templatePack = $this->applyDischargeTemplate($content, $panelData, null);
if ($templateId === null || $templateId <= 0) {
$abdmTemplate = $this->resolveAbdmDischargeTemplate(0);
$templateId = (int) ($abdmTemplate['id'] ?? 0);
}
$templatePack = $this->applyDischargeTemplate($content, $panelData, $templateId > 0 ? $templateId : null);
$templateSettings = is_array($templatePack['selected_template_settings'] ?? null)
? $templatePack['selected_template_settings']
: $this->defaultDischargeTemplateSettings();
@ -8745,15 +8568,15 @@ class Ipd_discharge extends BaseController
trim((string) ($templateSettings['footer_html'] ?? '')),
$templateTokenVars
);
$footerHtml = $this->sanitizeDischargePdfHtml($configuredFooter, false);
$footerHtml = $this->sanitizeDischargePdfHtml(
$configuredFooter,
false
);
$footerHtml = mpdf_normalize_font_weight_css($footerHtml);
if ($footerHtml === '') {
$footerHtml = '<div style="font-family:freeserif,serif;font-size:9pt;color:#6b7280;text-align:right;">Page {PAGENO}/{nbpg}</div>';
}
$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'
@ -8783,12 +8606,54 @@ class Ipd_discharge extends BaseController
$mpdf->SetHTMLFooter($footerHtml, 'E');
}
$watermarkType = (string) ($templateSettings['watermark_type'] ?? 'none');
$watermarkAlpha = max(0.01, min(1.00, (float) ($templateSettings['watermark_alpha'] ?? 0.12)));
if ($watermarkType === 'text') {
$watermarkText = trim((string) ($templateSettings['watermark_text'] ?? ''));
if ($watermarkText !== '') {
$mpdf->SetWatermarkText($watermarkText, $watermarkAlpha);
$mpdf->showWatermarkText = true;
}
} elseif ($watermarkType === 'image') {
$watermarkImage = trim((string) ($templateSettings['watermark_image'] ?? ''));
$watermarkPath = $watermarkImage !== '' ? FCPATH . ltrim(str_replace('\\', '/', $watermarkImage), '/') : '';
if ($watermarkPath !== '' && is_file($watermarkPath)) {
$mpdf->SetWatermarkImage($watermarkPath, $watermarkAlpha);
$mpdf->showWatermarkImage = true;
}
}
if ($withHeader || $footerHtml !== '') {
$mpdf->AddPage();
if ($withHeader && $headerHtml !== '') {
$mpdf->SetHTMLHeader($headerHtml, 'O', true);
}
if ($footerHtml !== '') {
$mpdf->SetHTMLFooter($footerHtml, 'O', true);
}
}
$pdfHtml = '';
$templateCss = trim((string) ($templateSettings['template_css'] ?? ''));
if ($templateCss !== '') {
$templateCss = (string) preg_replace('/@page(?:\s+[^{]+)?\s*\{[^{}]*\}/i', '', $templateCss);
$pdfHtml = '<style>' . "\n" . $templateCss . "\n" . '</style>' . "\n" . $pdfHtml;
$pdfHtml .= '<style>' . "\n" . $templateCss . "\n" . '</style>' . "\n";
}
$pdfHtml .= $renderedHtml;
$pdfHtml = mpdf_normalize_font_weight_css($pdfHtml);
$debugDirectory = WRITEPATH . 'debug';
if (! is_dir($debugDirectory)) {
mkdir($debugDirectory, 0755, true);
}
$debugTemplateId = (int) ($templatePack['selected_template_id'] ?? 0);
file_put_contents($debugDirectory . DIRECTORY_SEPARATOR . 'discharge_mpdf_input_' . $ipdId . '_' . $debugTemplateId . '.html', $pdfHtml, LOCK_EX);
file_put_contents($debugDirectory . DIRECTORY_SEPARATOR . 'discharge_mpdf_runtime_' . $ipdId . '_' . $debugTemplateId . '.json', json_encode([
'header_html' => $headerHtml,
'footer_html' => $footerHtml,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), LOCK_EX);
$pdfBinary = $this->runMpdfWithTolerantWarnings(static function () use ($mpdf, $pdfHtml, $fileName): string {
$mpdf->WriteHTML($pdfHtml);
@ -8814,41 +8679,28 @@ class Ipd_discharge 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 {
$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(),
]);
}
}
// Always generate fresh IPD Discharge Summary PDF using the configured Online/ABDM template
$abdmTemplate = $this->resolveAbdmDischargeTemplate();
$abdmTemplateId = (int) ($abdmTemplate['id'] ?? 0);
// Ensure IPD Bill PDF is cached if missing
$billPath = $directory . DIRECTORY_SEPARATOR . 'ipd-bill.pdf';
if (! is_file($billPath) || filesize($billPath) === 0) {
try {
$billingCtrl = new \App\Controllers\Billing\Ipd();
$request = \Config\Services::request();
$response = \Config\Services::response();
$logger = \Config\Services::logger();
$billingCtrl->initController($request, $response, $logger);
$billingCtrl->generateIpdBillPdfBinary($ipdId, 1, true);
} catch (\Throwable $e) {
log_message('error', 'Auto-generating IPD Bill PDF failed for IPD {ipd}: {msg}', [
'ipd' => $ipdId,
'msg' => $e->getMessage(),
]);
}
try {
$this->generateDischargeSummaryPdfBinary($ipdId, true, $abdmTemplateId > 0 ? $abdmTemplateId : null);
} catch (\Throwable $e) {
log_message('error', 'Auto-generating IPD Discharge Summary PDF failed for IPD {ipd}: {msg}', [
'ipd' => $ipdId,
'msg' => $e->getMessage(),
]);
}
$documents = [];
$definitions = [
['file' => 'discharge-summary.pdf', 'title' => 'IPD Discharge Summary', 'loinc' => '18842-5', 'snomed' => '373942005'],
['file' => 'ipd-bill.pdf', 'title' => 'IPD Bill / Invoice', 'loinc' => '75490-3', 'snomed' => '823651000000106'],
[
'file' => 'discharge-summary.pdf',
'title' => 'IPD Discharge Summary',
'loinc' => '18842-5',
'snomed' => '373942005',
'snomed_display' => 'Discharge summary',
],
];
foreach ($definitions as $definition) {
@ -8864,6 +8716,7 @@ class Ipd_discharge extends BaseController
'title' => $definition['title'],
'loinc_code' => $definition['loinc'],
'snomed_code' => $definition['snomed'],
'snomed_display' => $definition['snomed_display'] ?? 'Discharge summary',
'content_type' => 'application/pdf',
'data' => base64_encode($binary),
'created_at' => $createdAt,
@ -9184,13 +9037,25 @@ class Ipd_discharge extends BaseController
$abhaAddress = $abhaIdRaw;
}
$admissionRaw = trim((string) ($ipdRow['register_date'] ?? ''));
$dischargeRaw = trim((string) ($ipdRow['discharge_date'] ?? ''));
$admissionIso = $this->toIsoDateTimeOrNow($admissionRaw);
$dischargeIso = $this->toIsoDateTimeOrNow($dischargeRaw);
$visitDate = $dischargeRaw !== ''
? date('Y-m-d', strtotime($dischargeRaw))
: ($admissionRaw !== '' ? date('Y-m-d', strtotime($admissionRaw)) : date('Y-m-d'));
$admissionDateRaw = trim((string) ($ipdRow['register_date'] ?? ''));
$admissionTimeRaw = trim((string) ($ipdRow['reg_time'] ?? ''));
$admissionCombined = $admissionDateRaw;
if ($admissionCombined !== '' && $admissionTimeRaw !== '') {
$admissionCombined .= ' ' . $admissionTimeRaw;
}
$dischargeDateRaw = trim((string) ($ipdRow['discharge_date'] ?? ''));
$dischargeTimeRaw = trim((string) ($ipdRow['discharge_time'] ?? ''));
$dischargeCombined = $dischargeDateRaw;
if ($dischargeCombined !== '' && $dischargeTimeRaw !== '') {
$dischargeCombined .= ' ' . $dischargeTimeRaw;
}
$admissionIso = $this->toIsoDateTimeOrNow($admissionCombined !== '' ? $admissionCombined : $admissionDateRaw);
$dischargeIso = $this->toIsoDateTimeOrNow($dischargeCombined !== '' ? $dischargeCombined : $dischargeDateRaw);
$visitDate = $dischargeDateRaw !== ''
? date('Y-m-d', strtotime($dischargeDateRaw))
: ($admissionDateRaw !== '' ? date('Y-m-d', strtotime($admissionDateRaw)) : date('Y-m-d'));
$diagnosisRows = $this->byIpdRows('ipd_discharge_diagnosis', ['comp_report'], 'id ASC', $ipdId);
$conditionRows = [];
@ -9200,6 +9065,12 @@ class Ipd_discharge extends BaseController
$conditionRows[] = ['text' => $text, 'code' => ''];
}
}
if (empty($conditionRows) && ! empty($ipdRow['problem'])) {
$prob = trim((string) $ipdRow['problem']);
if ($this->isMeaningfulDischargeValue($prob)) {
$conditionRows[] = ['text' => $prob, 'code' => ''];
}
}
$chiefComplaintsList = [];
if ($this->tableHasColumns('ipd_discharge_complaint', ['ipd_id', 'comp_report'])) {
@ -9446,6 +9317,27 @@ class Ipd_discharge extends BaseController
}
$uiClinicalHistoryText = implode("\n", $historyList);
$courseRows = [];
if ($this->tableHasColumns('ipd_discharge_course', ['ipd_id', 'comp_report'])) {
$cRows = $this->byIpdRows('ipd_discharge_course', ['comp_report', 'comp_remark'], 'id ASC', $ipdId);
foreach ($cRows as $row) {
$cReport = trim((string) ($row['comp_report'] ?? ''));
$cRemark = trim((string) ($row['comp_remark'] ?? ''));
if ($cReport !== '') {
$courseRows[] = [
'text' => $cReport . ($cRemark !== '' ? (' - ' . $cRemark) : ''),
'name' => $cReport,
'remark' => $cRemark,
];
}
}
}
$courseRemark = '';
if ($this->tableHasColumns('ipd_discharge_course_remark', ['ipd_id', 'comp_remark'])) {
$crRow = $this->firstRowByIpd('ipd_discharge_course_remark', $ipdId);
$courseRemark = trim((string) ($crRow['comp_remark'] ?? ''));
}
$documentsList = $this->buildIpdPdfDocumentsList($ipdId, $dischargeIso);
$locationInfo = $this->resolveIpdLocation($ipdId);
@ -9462,6 +9354,13 @@ class Ipd_discharge extends BaseController
$uhidNo = trim((string) ($patientRow['uhid_no'] ?? $patientRow['uhid'] ?? $patientRow['patient_code'] ?? $patientRow['p_id'] ?? $patientId));
$ipdNo = trim((string) ($ipdRow['ipd_no'] ?? $ipdRow['ipd_code'] ?? ('IPD-' . $ipdId)));
$patientMobile = trim((string) ($patientRow['mphone1'] ?? $patientRow['p_mobile1'] ?? $ipdRow['P_mobile1'] ?? ''));
$patientAddress = trim((string) ($patientRow['p_add1'] ?? $patientRow['add1'] ?? $ipdRow['add1'] ?? ''));
$patientCity = trim((string) ($patientRow['p_city'] ?? $patientRow['city'] ?? $ipdRow['city'] ?? ''));
$patientState = trim((string) ($patientRow['p_state'] ?? $patientRow['state'] ?? $ipdRow['state'] ?? ''));
$patientPin = trim((string) ($patientRow['p_pin'] ?? $patientRow['pincode'] ?? ''));
$dischargeStatus = trim((string) ($ipdRow['discarge_patient_status'] ?? ''));
$source = [
'record_id' => (string) $ipdId,
'bundle_identifier' => 'discharge-' . $ipdNo,
@ -9470,6 +9369,8 @@ class Ipd_discharge extends BaseController
'completed_at' => $dischargeIso,
'department' => trim((string) ($ipdRow['dept_id'] ?? '')),
'doctor_name' => trim((string) ($ipdRow['r_doc_name'] ?? '')),
'discharge_status' => $dischargeStatus,
'problem' => trim((string) ($ipdRow['problem'] ?? '')),
'organization' => [
'id' => $hfrId,
'name' => $hospitalName,
@ -9480,6 +9381,13 @@ class Ipd_discharge extends BaseController
'name' => $patientName,
'gender' => $this->normalizeFhirGender($genderRaw),
'dob' => ! empty($patientRow['dob']) ? date('Y-m-d', strtotime((string) $patientRow['dob'])) : '',
'mobile' => $patientMobile,
'address' => [
'line' => $patientAddress,
'city' => $patientCity,
'state' => $patientState,
'pincode' => $patientPin,
],
'abha_id' => $abhaDigits,
'abha_address' => $abhaAddress,
],
@ -9489,6 +9397,7 @@ class Ipd_discharge extends BaseController
'class_code' => 'IMP',
'start' => $admissionIso,
'end' => $dischargeIso,
'discharge_disposition' => $dischargeStatus,
'location_display' => (string) ($locationInfo['display'] ?? ''),
'ward' => (string) ($locationInfo['ward'] ?? ''),
'room' => (string) ($locationInfo['room'] ?? ''),
@ -9502,11 +9411,11 @@ class Ipd_discharge extends BaseController
'ui_surgeries' => $procedureRows,
'ui_final_diagnosis' => $conditionRows,
'ui_summary_investigation' => $uiSummaryInvestigation,
'ui_course_treatment' => $courseRows,
'ui_course_treatment' => $courseRows,
'ui_course_treatment_narrative' => $courseRemark,
'ui_discharge_medicine' => $medicationRows,
'allergies' => $allergyRows,
'documents' => [],
'skip_pdf' => true,
'documents' => $documentsList,
'template' => $this->resolveAbdmDischargeTemplate(),
];

View File

@ -287,7 +287,7 @@ class FhirDocumentBuilder
{
$out = [];
foreach ($data as $key => $value) {
if ($value === null || $value === '') {
if ($value === null || $value === '' || $this->isPlaceholderValue($value)) {
continue;
}
if (is_array($value)) {

View File

@ -83,6 +83,71 @@ abstract class AbstractModuleFhirGenerator
$patient['birthDate'] = $dob;
}
$mobile = trim((string) ($source['patient']['mobile'] ?? $source['patient']['phone'] ?? $source['patient']['p_mobile1'] ?? $source['mobile'] ?? ''));
if ($mobile !== '' && $this->isMeaningfulValue($mobile)) {
$digits = preg_replace('/\D/', '', $mobile);
if (is_string($digits) && strlen($digits) >= 10) {
$phoneVal = str_starts_with($mobile, '+') ? $mobile : ('+91' . substr($digits, -10));
$patient['telecom'] = [
[
'system' => 'phone',
'value' => $phoneVal,
'use' => 'mobile',
],
];
}
}
$addrRaw = $source['patient']['address'] ?? $source['address'] ?? null;
$line = '';
$city = '';
$district = '';
$state = '';
$postalCode = '';
if (is_array($addrRaw)) {
$line = trim((string) ($addrRaw['line'] ?? $addrRaw['text'] ?? $addrRaw['street'] ?? $addrRaw['add1'] ?? ''));
$city = trim((string) ($addrRaw['city'] ?? ''));
$district = trim((string) ($addrRaw['district'] ?? ''));
$state = trim((string) ($addrRaw['state'] ?? ''));
$postalCode = trim((string) ($addrRaw['postalCode'] ?? $addrRaw['pincode'] ?? $addrRaw['pin'] ?? ''));
} elseif (is_string($addrRaw) && $this->isMeaningfulValue($addrRaw)) {
$line = trim($addrRaw);
}
if ($city === '') {
$city = trim((string) ($source['patient']['city'] ?? $source['city'] ?? ''));
}
if ($state === '') {
$state = trim((string) ($source['patient']['state'] ?? $source['state'] ?? ''));
}
if ($postalCode === '') {
$postalCode = trim((string) ($source['patient']['postalCode'] ?? $source['patient']['pincode'] ?? $source['patient']['pin'] ?? $source['pincode'] ?? ''));
}
$addressItem = [];
if ($line !== '' && $this->isMeaningfulValue($line)) {
$addressItem['line'] = [$line];
$addressItem['text'] = $line;
}
if ($city !== '' && $this->isMeaningfulValue($city)) {
$addressItem['city'] = $city;
}
if ($district !== '' && $this->isMeaningfulValue($district)) {
$addressItem['district'] = $district;
}
if ($state !== '' && $this->isMeaningfulValue($state)) {
$addressItem['state'] = $state;
}
if ($postalCode !== '' && $this->isMeaningfulValue($postalCode)) {
$addressItem['postalCode'] = $postalCode;
}
if (! empty($addressItem)) {
$addressItem['country'] = 'IND';
$patient['address'] = [$addressItem];
}
$abhaId = preg_replace('/\D/', '', (string) ($source['patient']['abha_id'] ?? ''));
if (is_string($abhaId) && $abhaId !== '') {
$patient['identifier'][] = [
@ -96,6 +161,19 @@ abstract class AbstractModuleFhirGenerator
];
}
$abhaAddress = trim((string) ($source['patient']['abha_address'] ?? $source['abha_address'] ?? ''));
if ($abhaAddress !== '' && str_contains($abhaAddress, '@')) {
$patient['identifier'][] = [
'type' => ['coding' => [[
'system' => 'http://terminology.hl7.org/CodeSystem/v2-0203',
'code' => 'MR',
'display' => 'Medical record number',
]], 'text' => 'ABHA Address'],
'system' => 'https://healthid.ndhm.gov.in',
'value' => $abhaAddress,
];
}
$uhid = trim((string) ($source['patient']['uhid'] ?? $source['patient']['hospital_patient_id'] ?? $source['patient']['patient_code'] ?? ''));
if ($this->isMeaningfulValue($uhid)) {
$patient['identifier'][] = [
@ -177,7 +255,10 @@ abstract class AbstractModuleFhirGenerator
];
if ($encounterRef !== null) {
$composition['encounter'] = ['reference' => $encounterRef];
$composition['encounter'] = [
'reference' => $encounterRef,
'display' => 'Encounter',
];
}
return $composition;
@ -231,6 +312,9 @@ abstract class AbstractModuleFhirGenerator
$patientName = trim((string) ($source['patient']['name'] ?? $source['patient_name'] ?? 'Patient'));
$statusInput = $enc['discharge_disposition'] ?? $enc['outcome'] ?? $source['discharge_status'] ?? $source['discarge_patient_status'] ?? 'home';
$disposition = $this->resolveDischargeDisposition($statusInput);
$encounterResource = [
'resourceType' => 'Encounter',
'id' => 'encounter-' . $id,
@ -239,7 +323,7 @@ abstract class AbstractModuleFhirGenerator
]],
'text' => [
'status' => 'generated',
'div' => '<div xmlns="http://www.w3.org/1999/xhtml"><p><b>Encounter:</b> ' . $this->escapeXhtml($classDisplay) . ' ' . $this->escapeXhtml($id) . '</p></div>',
'div' => '<div xmlns="http://www.w3.org/1999/xhtml"><p><b>Encounter:</b> ' . $this->escapeXhtml($classDisplay) . ' ' . $this->escapeXhtml($id) . ' - ' . $this->escapeXhtml($disposition['text']) . '</p></div>',
],
'identifier' => $identifiers,
'status' => 'finished',
@ -260,17 +344,74 @@ abstract class AbstractModuleFhirGenerator
'dischargeDisposition' => [
'coding' => [[
'system' => 'http://terminology.hl7.org/CodeSystem/discharge-disposition',
'code' => 'home',
'display' => 'Home',
'code' => $disposition['code'],
'display' => $disposition['display'],
]],
'text' => 'Discharged to Home Care',
]
'text' => $disposition['text'],
],
],
];
return $encounterResource;
}
/**
* @param mixed $status
* @return array{code:string,display:string,text:string}
*/
protected function resolveDischargeDisposition(mixed $status): array
{
$statusStr = strtolower(trim((string) $status));
if ($statusStr === '1' || str_contains($statusStr, 'improved') || str_contains($statusStr, 'recovered')) {
return [
'code' => 'home',
'display' => 'Home',
'text' => 'Improved / Recovered - Discharged to Home Care',
];
}
if ($statusStr === '2' || str_contains($statusStr, 'lama') || str_contains($statusStr, 'advice')) {
return [
'code' => 'aadvice',
'display' => 'Left against medical advice',
'text' => 'Left Against Medical Advice (LAMA)',
];
}
if ($statusStr === '3' || str_contains($statusStr, 'refer') || str_contains($statusStr, 'higher')) {
return [
'code' => 'other-hcf',
'display' => 'Other healthcare facility',
'text' => 'Referred to Higher Centre / Other Healthcare Facility',
];
}
if ($statusStr === '4' || str_contains($statusStr, 'satisfactory')) {
return [
'code' => 'home',
'display' => 'Home',
'text' => 'Satisfactory - Discharged to Home Care',
];
}
if ($statusStr === '5' || str_contains($statusStr, 'expir') || str_contains($statusStr, 'dead') || str_contains($statusStr, 'deceased')) {
return [
'code' => 'exp',
'display' => 'Expired',
'text' => 'Expired / Deceased',
];
}
if ($statusStr === '7' || str_contains($statusStr, 'request')) {
return [
'code' => 'home',
'display' => 'Home',
'text' => 'Discharge on Request - Discharged to Home Care',
];
}
return [
'code' => 'home',
'display' => 'Home',
'text' => 'Discharged to Home Care',
];
}
/** @param array<string,mixed> $source */
protected function buildPractitioner(array $source): ?array
{

View File

@ -148,6 +148,31 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
}
if (is_array($encounter)) {
$encounterDiagnoses = [];
foreach ($diagnosesList as $idx => $cond) {
$text = trim((string) ($cond['text'] ?? $cond['name'] ?? ''));
if ($text === '') {
continue;
}
$conditionId = 'discharge-diagnosis-' . $recordId . '-' . $idx;
$encounterDiagnoses[] = [
'condition' => [
'reference' => 'urn:uuid:' . $conditionId,
'display' => $text,
],
'use' => [
'coding' => [[
'system' => 'http://terminology.hl7.org/CodeSystem/diagnosis-role',
'code' => 'DD',
'display' => 'Discharge diagnosis',
]],
],
'rank' => count($encounterDiagnoses) + 1,
];
}
if ($encounterDiagnoses !== []) {
$encounter['diagnosis'] = $encounterDiagnoses;
}
$builder->addEncounter($encounter);
}
@ -407,8 +432,30 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
}
// ── Care Plans / Discharge Advice ──────────────────────────────────────
foreach ((array) ($source['ui_discharge_summary'] ?? $source['care_plans'] ?? []) as $idx => $cp) {
// ── Care Plans / Discharge Advice & Hospital Course ────────────────────
$carePlansList = (array) ($source['ui_discharge_summary'] ?? $source['care_plans'] ?? []);
// Include hospital course / course of treatment if provided
$courseItems = (array) ($source['ui_course_treatment'] ?? $source['course_treatment'] ?? []);
$courseNarrative = trim((string) ($source['ui_course_treatment_narrative'] ?? $source['course_narrative'] ?? ''));
$courseParts = [];
foreach ($courseItems as $cItem) {
$cText = is_array($cItem) ? trim((string) ($cItem['text'] ?? $cItem['name'] ?? $cItem['comp_report'] ?? '')) : trim((string) $cItem);
if ($cText !== '' && $this->isMeaningfulValue($cText)) {
$courseParts[] = $cText;
}
}
if ($courseNarrative !== '' && $this->isMeaningfulValue($courseNarrative)) {
$courseParts[] = $courseNarrative;
}
if (! empty($courseParts)) {
$carePlansList[] = [
'title' => 'Course of Treatment in Hospital',
'description' => implode("\n", array_unique($courseParts)),
];
}
foreach ($carePlansList as $idx => $cp) {
$title = $this->cleanPlainText((string) ($cp['title'] ?? 'Discharge Advice'));
$description = $this->cleanPlainText((string) ($cp['description'] ?? ''));
if (! $this->isMeaningfulValue($description)) {
@ -544,10 +591,9 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
$sectionDefinitions[] = ['title' => 'Investigations', 'code' => '721981007', 'display' => 'Diagnostic studies report', 'prefix' => 'discharge-investigation-', 'items' => $investigations, 'narrative' => $invNarrative];
}
// 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];
// Care plans / Discharge advice & hospital course
if (! empty($carePlansList)) {
$sectionDefinitions[] = ['title' => 'Care Plan', 'code' => '734163000', 'display' => 'Care plan', 'prefix' => 'discharge-careplan-', 'items' => $carePlansList];
}
// Document Reference
@ -865,13 +911,13 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
{
$hospitalName = trim((string) ($source['organization']['name'] ?? 'Hospital'));
$patientName = trim((string) ($source['patient']['name'] ?? 'Patient'));
$patientId = trim((string) ($source['patient']['id'] ?? 'N/A'));
$gender = ucfirst(trim((string) ($source['patient']['gender'] ?? 'N/A')));
$dob = trim((string) ($source['patient']['dob'] ?? 'N/A'));
$abhaId = trim((string) ($source['patient']['abha_id'] ?? 'N/A'));
$recordId = trim((string) ($source['record_id'] ?? 'N/A'));
$patientId = trim((string) ($source['patient']['id'] ?? ''));
$gender = ucfirst(trim((string) ($source['patient']['gender'] ?? '')));
$dob = trim((string) ($source['patient']['dob'] ?? ''));
$abhaId = trim((string) ($source['patient']['abha_id'] ?? ''));
$recordId = trim((string) ($source['record_id'] ?? ''));
$admDate = ! empty($source['encounter']['start']) ? date('d-m-Y', strtotime((string) $source['encounter']['start'])) : 'N/A';
$admDate = ! empty($source['encounter']['start']) ? date('d-m-Y', strtotime((string) $source['encounter']['start'])) : '';
$disDate = ! empty($source['encounter']['end']) ? date('d-m-Y', strtotime((string) $source['encounter']['end'])) : date('d-m-Y');
$doctorName = trim((string) ($source['doctor_name'] ?? $source['doctor']['name'] ?? 'Attending Physician'));
@ -898,7 +944,7 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
'{{PATIENT_NAME}}' => htmlspecialchars($patientName),
'{{UHID}}' => htmlspecialchars($patientId),
'{{IPD_CODE}}' => htmlspecialchars($recordId),
'{{AGE_GENDER}}' => htmlspecialchars($gender . ($dob !== 'N/A' ? ' / ' . $dob : '')),
'{{AGE_GENDER}}' => htmlspecialchars(trim($gender . ($dob !== '' ? ' / ' . $dob : ''))),
'{{ADMIT_DATE}}' => htmlspecialchars($admDate),
'{{DISCHARGE_DATE}}' => htmlspecialchars($disDate),
'{{DOCTOR_NAMES}}' => htmlspecialchars($doctorName),
@ -1229,11 +1275,64 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
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'] ?? '');
$ipdNo = trim((string) ($source['encounter']['ipd_no'] ?? $source['record_id'] ?? ''));
$admitRaw = trim((string) ($source['encounter']['start'] ?? $source['admission_date'] ?? ''));
$dischargeRaw = trim((string) ($source['encounter']['end'] ?? $source['discharge_date'] ?? $source['completed_at'] ?? ''));
$formatDate = static function (string $raw): string {
if ($raw === '') {
return '';
}
$ts = strtotime($raw);
if ($ts === false) {
return $raw;
}
return (strlen($raw) > 10 && str_contains($raw, 'T')) ? date('d-M-Y h:i A', $ts) : date('d-M-Y', $ts);
};
$admitDisplay = $formatDate($admitRaw);
if ($admitDisplay === '') {
$admitDisplay = date('d-M-Y');
}
$dischargeDisplay = $formatDate($dischargeRaw);
if ($dischargeDisplay === '') {
$dischargeDisplay = date('d-M-Y');
}
$locDisplay = trim((string) ($source['encounter']['location_display'] ?? $source['encounter']['ward'] ?? ''));
$abhaId = trim((string) ($source['patient']['abha_id'] ?? ''));
$patientUhid = trim((string) ($source['patient']['uhid'] ?? $source['patient']['hospital_patient_id'] ?? ''));
$patientMobile = trim((string) ($source['patient']['mobile'] ?? $source['patient']['phone'] ?? $source['mobile'] ?? ''));
$identifierDisplay = '';
if ($abhaId !== '' && $this->isMeaningfulValue($abhaId)) {
$digits = preg_replace('/\D/', '', $abhaId);
$identifierDisplay = (strlen($digits) === 14)
? (substr($digits, 0, 2) . '-' . substr($digits, 2, 4) . '-' . substr($digits, 6, 4) . '-' . substr($digits, 10, 4))
: $abhaId;
} elseif ($patientUhid !== '') {
$identifierDisplay = 'UHID: ' . $patientUhid;
} elseif ($patientId !== '' && $patientId !== '0') {
$identifierDisplay = 'ID: ' . $patientId;
}
// Outcome / Discharge status
$statusInput = $source['encounter']['discharge_disposition'] ?? $source['discharge_status'] ?? $source['discarge_patient_status'] ?? 'home';
$disposition = $this->resolveDischargeDisposition($statusInput);
$outcomeText = $disposition['text'];
// Diagnosis summary
$diagList = [];
foreach ((array) ($source['ui_final_diagnosis'] ?? $source['diagnoses'] ?? $source['conditions'] ?? []) as $d) {
$t = is_array($d) ? trim((string) ($d['text'] ?? $d['name'] ?? '')) : trim((string) $d);
if ($t !== '' && $this->isMeaningfulValue($t)) {
$diagList[] = $t;
}
}
if (empty($diagList) && ! empty($source['problem']) && $this->isMeaningfulValue((string) $source['problem'])) {
$diagList[] = trim((string) $source['problem']);
}
$diagnosisSummary = ! empty($diagList) ? implode(', ', array_unique($diagList)) : '';
$html = '<div xmlns="http://www.w3.org/1999/xhtml" xml:lang="en-IN" lang="en-IN">';
$html .= '<div style="border-bottom:2px solid #0284c7;padding-bottom:6px;margin-bottom:12px;">';
@ -1242,11 +1341,22 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
$html .= '</div>';
$html .= '<table style="width:100%;border-collapse:collapse;margin-bottom:14px;background:#f8fafc;font-size:10pt;">';
$html .= '<tr><td style="border:1px solid #cbd5e1;padding:6px;"><strong>Patient:</strong> ' . htmlspecialchars($patientName) . '</td><td style="border:1px solid #cbd5e1;padding:6px;"><strong>IPD No:</strong> ' . htmlspecialchars($ipdNo) . '</td></tr>';
$html .= '<tr><td style="border:1px solid #cbd5e1;padding:6px;"><strong>Gender / DOB:</strong> ' . htmlspecialchars(ucfirst($gender)) . ($dob !== '' ? (' / ' . htmlspecialchars($dob)) : '') . '</td><td style="border:1px solid #cbd5e1;padding:6px;"><strong>ABHA Number:</strong> ' . htmlspecialchars($abhaId !== '' ? $abhaId : 'N/A') . '</td></tr>';
$html .= '<tr><td style="border:1px solid #cbd5e1;padding:6px;"><strong>Admission Date:</strong> ' . htmlspecialchars($admitDate) . '</td><td style="border:1px solid #cbd5e1;padding:6px;"><strong>Discharge Date:</strong> ' . htmlspecialchars($dischargeDate) . '</td></tr>';
if ($locDisplay !== '' || $drName !== '') {
$html .= '<tr><td style="border:1px solid #cbd5e1;padding:6px;"><strong>Location / Bed:</strong> ' . htmlspecialchars($locDisplay !== '' ? $locDisplay : 'General Ward') . '</td><td style="border:1px solid #cbd5e1;padding:6px;"><strong>Doctor:</strong> ' . htmlspecialchars($drName) . '</td></tr>';
$html .= '<tr><td style="border:1px solid #cbd5e1;padding:6px;"><strong>Patient:</strong> ' . htmlspecialchars($patientName) . '</td><td style="border:1px solid #cbd5e1;padding:6px;"><strong>IPD No:</strong> ' . htmlspecialchars($ipdNo !== '' ? $ipdNo : ('IPD-' . $patientId)) . '</td></tr>';
$genderDob = htmlspecialchars(ucfirst($gender)) . ($dob !== '' ? (' / ' . htmlspecialchars($dob)) : '');
$idCell = $identifierDisplay !== '' ? ('<strong>ABHA / ID:</strong> ' . htmlspecialchars($identifierDisplay)) : ('<strong>Doctor:</strong> ' . htmlspecialchars($drName));
$html .= '<tr><td style="border:1px solid #cbd5e1;padding:6px;"><strong>Gender / DOB:</strong> ' . $genderDob . '</td><td style="border:1px solid #cbd5e1;padding:6px;">' . $idCell . '</td></tr>';
$html .= '<tr><td style="border:1px solid #cbd5e1;padding:6px;"><strong>Admission Date:</strong> ' . htmlspecialchars($admitDisplay) . '</td><td style="border:1px solid #cbd5e1;padding:6px;"><strong>Discharge Date:</strong> ' . htmlspecialchars($dischargeDisplay) . '</td></tr>';
if ($diagnosisSummary !== '') {
$html .= '<tr><td style="border:1px solid #cbd5e1;padding:6px;" colspan="2"><strong>Final Diagnosis:</strong> ' . htmlspecialchars($diagnosisSummary) . '</td></tr>';
}
$html .= '<tr><td style="border:1px solid #cbd5e1;padding:6px;"><strong>Outcome / Status:</strong> <span style="color:#0369a1;font-weight:bold;">' . htmlspecialchars($outcomeText) . '</span></td><td style="border:1px solid #cbd5e1;padding:6px;"><strong>Doctor:</strong> ' . htmlspecialchars($drName) . ($locDisplay !== '' ? (' | <strong>Location:</strong> ' . htmlspecialchars($locDisplay)) : '') . '</td></tr>';
if ($patientMobile !== '') {
$html .= '<tr><td style="border:1px solid #cbd5e1;padding:6px;" colspan="2"><strong>Contact:</strong> ' . htmlspecialchars($patientMobile) . '</td></tr>';
}
$html .= '</table>';

View File

@ -31,9 +31,21 @@
<tr>
<td><?= (int) $i + 1 ?></td>
<td><?= esc((string) ($row['template_name'] ?? '')) ?></td>
<td><?= (int) ($row['is_default'] ?? 0) ? '<span class="badge bg-success">Yes</span>' : '' ?></td>
<td>
<?php if ((int) ($row['is_default'] ?? 0)): ?>
<span class="badge bg-success"><i class="fa fa-check me-1"></i>Yes</span>
<?php else: ?>
<button type="button" class="btn btn-outline-success btn-sm py-0 px-2 btn_set_default" data-id="<?= (int) ($row['id'] ?? 0) ?>" title="Set as default template">Set Default</button>
<?php endif; ?>
</td>
<td><?= (int) ($row['is_audit_only'] ?? 0) ? '<span class="badge bg-warning text-dark">Audit</span>' : '' ?></td>
<td><?= (int) ($row['is_abdm'] ?? 0) ? '<span class="badge bg-info text-dark">Online / ABDM</span>' : '' ?></td>
<td>
<?php if ((int) ($row['is_abdm'] ?? 0)): ?>
<span class="badge bg-info text-dark"><i class="fa fa-check me-1"></i>Online / ABDM</span>
<?php else: ?>
<button type="button" class="btn btn-outline-info btn-sm py-0 px-2 btn_set_abdm" data-id="<?= (int) ($row['id'] ?? 0) ?>" title="Set as Online / ABDM template">Set ABDM</button>
<?php endif; ?>
</td>
<td>
<a class="btn btn-primary btn-sm"
href="javascript:load_form('<?= base_url('Ipd_discharge/print_template_builder') ?>?mode=edit&edit=<?= (int) ($row['id'] ?? 0) ?>','IPD Discharge Template Edit');">Edit</a>
@ -150,5 +162,51 @@
}).catch(function () { showMsg('Request failed.', false); });
});
});
document.querySelectorAll('.btn_set_abdm').forEach(function (btn) {
btn.addEventListener('click', function () {
var id = btn.getAttribute('data-id');
var csrf = getCsrf();
var fd = new FormData();
fd.append(csrf.name, csrf.value);
fd.append('id', id);
fetch('<?= base_url('Ipd_discharge/discharge_template_set_abdm_ajax') ?>', {
method: 'POST',
headers: {'X-Requested-With': 'XMLHttpRequest'},
body: fd
}).then(function (r) { return r.json(); }).then(function (data) {
updateCsrf(data);
if (data.update == 1) {
load_form('<?= base_url('Ipd_discharge/print_template_builder') ?>?mode=list', 'IPD Discharge Templates');
} else {
showMsg(data.error_text || 'Unable to update Online / ABDM template.', false);
}
}).catch(function () { showMsg('Request failed.', false); });
});
});
document.querySelectorAll('.btn_set_default').forEach(function (btn) {
btn.addEventListener('click', function () {
var id = btn.getAttribute('data-id');
var csrf = getCsrf();
var fd = new FormData();
fd.append(csrf.name, csrf.value);
fd.append('id', id);
fetch('<?= base_url('Ipd_discharge/discharge_template_set_default_ajax') ?>', {
method: 'POST',
headers: {'X-Requested-With': 'XMLHttpRequest'},
body: fd
}).then(function (r) { return r.json(); }).then(function (data) {
updateCsrf(data);
if (data.update == 1) {
load_form('<?= base_url('Ipd_discharge/print_template_builder') ?>?mode=list', 'IPD Discharge Templates');
} else {
showMsg(data.error_text || 'Unable to update default template.', false);
}
}).catch(function () { showMsg('Request failed.', false); });
});
});
})();
</script>

View File

@ -800,4 +800,128 @@ final class FhirGeneratorsTest extends CIUnitTestCase
$this->assertContains('Medications', $sectionTitles);
$this->assertContains('Care Plan', $sectionTitles);
}
public function testDischargeMandatoryFieldsPatientEncounterDiagnosisAndOutcome(): void
{
$src = [
'record_id' => 9901,
'bundle_identifier' => 'discharge-A26090009901',
'session_id' => 9901,
'visit_date' => '2026-09-04',
'completed_at' => '2026-09-04T10:00:00+05:30',
'discharge_status' => 'LAMA',
'patient' => [
'id' => 1234,
'uhid' => 'P26091001234',
'name' => 'ANITA SHARMA',
'gender' => 'female',
'dob' => '1985-07-20',
'mobile' => '9876543210',
'address' => '123 Main Street, Civil Lines',
'city' => 'Haridwar',
'district' => 'Haridwar',
'state' => 'Uttarakhand',
'pin' => '249401',
'abha_id' => '91407564383099',
'abha_address' => 'anitasharma@sbx',
],
'encounter' => [
'id' => 'A26090009901',
'class_code' => 'IMP',
'start' => '2026-09-01T09:00:00+05:30',
'end' => '2026-09-04T10:00:00+05:30',
'discharge_disposition' => 'LAMA',
'ward' => 'ICU',
'bed' => 'ICU-02',
],
'doctor' => ['id' => 10, 'name' => 'Dr. R K Sundriyal'],
'organization' => ['id' => 'IN0510000000', 'name' => 'DevSoft Hospital'],
'chief_complaints' => [['text' => 'High grade fever with chills']],
'diagnoses' => [['text' => 'Enteric Fever / Typhoid']],
'procedures' => [['text' => 'Intravenous cannulation and fluid hydration']],
'medications' => [['name' => 'INJ CEFTRIAXONE 1GM', 'dosage' => 'IV | BD | for 5 Days']],
'ui_course_treatment' => [
'Patient admitted with high grade fever. Started on IV antibiotics and fluids. Vitals stabilized on Day 2. Patient requested LAMA on Day 4.',
],
'care_plans' => [
['title' => 'Follow Up Advice', 'description' => 'Consult local physician after 3 days.'],
],
];
$output = $this->factory->discharge()->generate($src);
$bundle = $output['fhir_bundle'];
$composition = $bundle['entry'][0]['resource'];
// 1. Patient Telecom and Address verification
$patients = array_values(array_filter($bundle['entry'], static fn (array $e): bool => ($e['resource']['resourceType'] ?? '') === 'Patient'));
$this->assertCount(1, $patients);
$patient = $patients[0]['resource'];
$this->assertSame('+919876543210', $patient['telecom'][0]['value'] ?? null);
$this->assertSame('phone', $patient['telecom'][0]['system'] ?? null);
$this->assertSame('mobile', $patient['telecom'][0]['use'] ?? null);
$this->assertSame('123 Main Street, Civil Lines', $patient['address'][0]['line'][0] ?? null);
$this->assertSame('Haridwar', $patient['address'][0]['city'] ?? null);
$this->assertSame('Uttarakhand', $patient['address'][0]['state'] ?? null);
$this->assertSame('249401', $patient['address'][0]['postalCode'] ?? null);
$this->assertSame('IND', $patient['address'][0]['country'] ?? null);
// ABHA address identifier
$patientIdSystems = array_column($patient['identifier'], 'system');
$this->assertContains('https://healthid.ndhm.gov.in', $patientIdSystems);
// 2. Encounter: Start/End period, hospitalization and discharge disposition
$encounters = array_values(array_filter($bundle['entry'], static fn (array $e): bool => ($e['resource']['resourceType'] ?? '') === 'Encounter'));
$this->assertCount(1, $encounters);
$encounter = $encounters[0]['resource'];
$this->assertSame('2026-09-01T09:00:00+05:30', $encounter['period']['start'] ?? null);
$this->assertSame('2026-09-04T10:00:00+05:30', $encounter['period']['end'] ?? null);
$this->assertSame('aadvice', $encounter['hospitalization']['dischargeDisposition']['coding'][0]['code'] ?? null);
$this->assertSame('Left against medical advice', $encounter['hospitalization']['dischargeDisposition']['coding'][0]['display'] ?? null);
// 3. Encounter.diagnosis linking to Final Diagnosis Condition
$this->assertNotEmpty($encounter['diagnosis'] ?? []);
$this->assertSame('DD', $encounter['diagnosis'][0]['use']['coding'][0]['code'] ?? null);
$diagRef = $encounter['diagnosis'][0]['condition']['reference'] ?? '';
$this->assertStringStartsWith('urn:uuid:', $diagRef);
// Verify that diagRef points to the final diagnosis Condition
$conditionUrls = array_column(array_filter($bundle['entry'], static fn (array $e): bool => ($e['resource']['resourceType'] ?? '') === 'Condition'), 'fullUrl');
$this->assertContains($diagRef, $conditionUrls);
// 4. CarePlan for Course of Treatment
$carePlans = array_values(array_filter($bundle['entry'], static fn (array $e): bool => ($e['resource']['resourceType'] ?? '') === 'CarePlan'));
$carePlanTitles = array_column(array_column($carePlans, 'resource'), 'title');
$this->assertContains('Course of Treatment in Hospital', $carePlanTitles);
// 5. Composition narrative XHTML checks
$narrative = $composition['text']['div'] ?? '';
$this->assertStringContainsString('ANITA SHARMA', $narrative);
$this->assertStringContainsString('9876543210', $narrative);
$this->assertStringContainsString('Enteric Fever / Typhoid', $narrative);
$this->assertStringContainsString('Left Against Medical Advice', $narrative);
$this->assertStringContainsString('01-Sep-2026', $narrative);
$this->assertStringContainsString('04-Sep-2026', $narrative);
// Assert NO standalone placeholder "NA" or "N/A"
$this->assertDoesNotMatchRegularExpression('/>\s*(NA|N\/A|null)\s*</i', $narrative);
// Also test other disposition mappings
$srcCured = $src;
$srcCured['discharge_status'] = 'Cured / Improved';
unset($srcCured['encounter']['discharge_disposition']);
$outCured = $this->factory->discharge()->generate($srcCured);
$encCured = array_values(array_filter($outCured['fhir_bundle']['entry'], static fn (array $e): bool => ($e['resource']['resourceType'] ?? '') === 'Encounter'))[0]['resource'];
$this->assertSame('home', $encCured['hospitalization']['dischargeDisposition']['coding'][0]['code']);
$srcExp = $src;
$srcExp['discharge_status'] = 'Expired';
unset($srcExp['encounter']['discharge_disposition']);
$outExp = $this->factory->discharge()->generate($srcExp);
$encExp = array_values(array_filter($outExp['fhir_bundle']['entry'], static fn (array $e): bool => ($e['resource']['resourceType'] ?? '') === 'Encounter'))[0]['resource'];
$this->assertSame('exp', $encExp['hospitalization']['dischargeDisposition']['coding'][0]['code']);
}
}