diff --git a/WORKLOG_COPILOT.md b/WORKLOG_COPILOT.md index 94bdb7c..48c85e6 100644 --- a/WORKLOG_COPILOT.md +++ b/WORKLOG_COPILOT.md @@ -4,6 +4,23 @@ Date: 2026-02-08 --- +## ABDM Immunization Duplicate Control — 2026-08-15 + +- Made ImmunizationRecord care-context references stable per source record (`IMM-{record_id}`), while preserving previously stored references and deterministic aggregate fallbacks. +- Removed the timestamp-based Immunization fallback from `FhirR4Builder` so retries cannot create a new gateway identity. +- Normalized direct and outbox push responses: HTTP 201 succeeds; HTTP 409 succeeds only for `DUPLICATE_RECORD`; HTTP 422 remains a non-retryable validation failure with field details. +- Preserved returned gateway `record_id`, `queue_id`, and `first_pushed_at`, persisting each when the deployed schema provides a matching column. +- Added focused regression tests for stable Immunization references, accepted duplicate responses, generic conflicts, HTTP 201, and detailed HTTP 422 errors. + +## ABDM M2 Link Status + Immunization Task Board — 2026-08-15 + +- Fixed M2 discovery/fetch to use canonical `health_records` care-context references and HI types first, with `patient_records` retained as a backward-compatible fallback. +- Fixed link callbacks to accept snake_case and camelCase request/care-context identifiers, persist request IDs as transaction metadata, update both `record_links` and `health_records`, and complete source-keyed work tasks. +- Made link synchronization idempotent: linked records cannot be downgraded, stale pending callbacks cannot erase failures, and duplicate callbacks preserve the confirmed link timestamp. +- Added the `Immunization Record` task-board tab for completed vaccinations and local M2 registration using the exact `ImmunizationRecord` HI type. +- Task-board Immunization registration forces `m2_only=1`; it stores the FHIR record for M2 discovery/linking and does not call a Push API. +- Added focused tests for callback identifier parsing and status transition behavior. + ## Storestock Module — 2026-04-19 Ported Hospital Stock Management (Storestock) module from CI3 to CI4. diff --git a/app/Controllers/AbdmGateway.php b/app/Controllers/AbdmGateway.php index b869c72..33bf73d 100644 --- a/app/Controllers/AbdmGateway.php +++ b/app/Controllers/AbdmGateway.php @@ -1955,20 +1955,16 @@ class AbdmGateway extends BaseController 'period_start' => $reportedAt, ]; - $pdfAttachment = $this->loadLatestLabPdfAttachment( - (int) ($labReq->charge_id ?? 0), - (int) ($labReq->lab_type ?? 0), - $labReqId, - $testTitle !== '' ? ($testTitle . ' PDF Report') : 'Lab Report PDF' + $pdfAttachment = $this->buildDiagnosticDigitalSharePdf( + $patientRow, + $abhaIdentity, + $diagnosticReport, + $observations, + $practitioner, + $hospitalProfile, + $labReqId ); - if ($pdfAttachment === null) { - return $this->response->setStatusCode(422)->setJSON([ - 'ok' => 0, - 'error' => 'PDF attachment missing for this lab request. Compile and store the report first, then retry ABDM submit.', - ]); - } - $bundle = $fhir->buildLabReportBundle($patient, $diagnosticReport, $observations, $practitioner, $organization, $encounter, $pdfAttachment); $bundleJson = (string) json_encode($bundle, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); @@ -2180,20 +2176,17 @@ class AbdmGateway extends BaseController ]; $fhir = new FhirR4Builder(); - $pdfAttachment = $this->loadLatestLabPdfAttachment( - (int) ($labReq->charge_id ?? 0), - (int) ($labReq->lab_type ?? 0), - $labReqId, - $testTitle !== '' ? ($testTitle . ' PDF Report') : 'Lab Report PDF' + $abhaIdentity = $this->resolvePatientAbhaIdentity($patientId, $abhaId); + $pdfAttachment = $this->buildDiagnosticDigitalSharePdf( + $patientRow, + $abhaIdentity, + $diagnosticReport, + $observations, + $practitioner, + $hospitalProfile, + $labReqId ); - if ($pdfAttachment === null) { - return $this->response->setStatusCode(422)->setJSON([ - 'status' => 'error', - 'message' => 'PDF attachment missing for this lab request. Compile and store the report first, then retry preview.', - ]); - } - $bundle = $fhir->buildLabReportBundle($patient, $diagnosticReport, $observations, $practitioner, $organization, $encounter, $pdfAttachment); return $this->response->setJSON([ @@ -4260,6 +4253,186 @@ class AbdmGateway extends BaseController } } + /** + * @param array $patientRow + * @param array{abha_id:string,abha_address:string} $abhaIdentity + * @param array $diagnosticReport + * @param array> $observations + * @param array|null $practitioner + * @param array $hospitalProfile + */ + private function buildDiagnosticDigitalSharePdf( + array $patientRow, + array $abhaIdentity, + array $diagnosticReport, + array $observations, + ?array $practitioner, + array $hospitalProfile, + int $labReqId + ): ?array { + $escape = static fn (string $value): string => htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); + $contentHtml = trim((string) ($diagnosticReport['report_html'] ?? '')); + if ($contentHtml === '' && $observations !== []) { + $rows = ''; + foreach ($observations as $observation) { + $value = trim((string) ($observation['value'] ?? '')); + $unit = trim((string) ($observation['unit'] ?? '')); + $range = trim(implode(' - ', array_filter([ + trim((string) ($observation['ref_low'] ?? '')), + trim((string) ($observation['ref_high'] ?? '')), + ], static fn (string $part): bool => $part !== ''))); + $rows .= '' . $escape((string) ($observation['test_name'] ?? 'Test')) . '' + . '' . $escape(trim($value . ' ' . $unit)) . '' + . '' . $escape($range) . '' + . '' . $escape((string) ($observation['interpretation'] ?? '')) . ''; + } + $contentHtml = '' + . '' . $rows . ''; + } + if ($contentHtml === '') { + return null; + } + + $conclusion = trim((string) ($diagnosticReport['conclusion'] ?? '')); + if ($conclusion !== '') { + $contentHtml .= ''; + } + $title = trim((string) ($diagnosticReport['title'] ?? 'Diagnostic Report')) ?: 'Diagnostic Report'; + $abha = trim(implode(' / ', array_filter([ + $abhaIdentity['abha_id'], + $abhaIdentity['abha_address'], + ], static fn (string $value): bool => $value !== ''))); + + return $this->renderDigitalSharePdf($contentHtml, 'Digital-Share-LAB-' . $labReqId, [ + 'document_title' => $title, + 'patient_name' => $this->patientDisplayName($patientRow), + 'uhid' => trim((string) ($patientRow['p_code'] ?? '')), + 'abha' => $abha, + 'record_label' => 'Diagnostic Report', + 'record_code' => 'LAB-' . $labReqId, + 'reported_at' => (string) ($diagnosticReport['reported_at'] ?? ''), + 'practitioner' => trim((string) ($practitioner['name'] ?? '')), + 'facility_name' => trim((string) ($hospitalProfile['name'] ?? '')), + ]); + } + + /** + * Render the stable PDF layout used for digital/PHR sharing. + * + * @param array $metadata + * @return array{content_type:string,data_base64:string,title:string,size:int,hash:string}|null + */ + private function renderDigitalSharePdf(string $contentHtml, string $fileStem, array $metadata): ?array + { + $contentHtml = trim($contentHtml); + if ($contentHtml === '') { + return null; + } + + try { + $escape = static fn (string $value): string => htmlspecialchars($value, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); + $hospital = $this->getHospitalProfileForFhir(); + $hospitalName = trim((string) ($metadata['facility_name'] ?? $hospital['name'] ?? '')) ?: 'Healthcare Facility'; + $hospitalAddress = trim((string) ($hospital['address'] ?? '')); + $hospitalContact = trim(implode(' | ', array_filter([ + trim((string) ($hospital['phone'] ?? '')), + trim((string) ($hospital['email'] ?? '')), + ], static fn (string $value): bool => $value !== ''))); + $logoHtml = ''; + $logoName = trim((string) ($hospital['logo'] ?? '')); + foreach (array_filter([ + $logoName !== '' ? FCPATH . 'assets/images/' . ltrim($logoName, '/\\') : '', + $logoName !== '' ? FCPATH . 'assets/img/' . ltrim($logoName, '/\\') : '', + FCPATH . 'assets/img/logo.png', + ]) as $logoPath) { + if (is_file($logoPath)) { + $logoHtml = ''; + break; + } + } + + $patientName = trim((string) ($metadata['patient_name'] ?? '')) ?: 'Patient'; + $uhid = trim((string) ($metadata['uhid'] ?? '')); + $abha = trim((string) ($metadata['abha'] ?? '')); + $recordCode = trim((string) ($metadata['record_code'] ?? '')); + $footerIdentity = trim(implode(' | ', array_filter([$patientName, $uhid !== '' ? 'UHID: ' . $uhid : '']))); + $identityFields = [ + ['Patient', $patientName], + ['UHID', $uhid], + ['ABHA', $abha], + [trim((string) ($metadata['record_label'] ?? 'Record')) ?: 'Record', $recordCode], + ['Reported / Issued', trim((string) ($metadata['reported_at'] ?? date('Y-m-d H:i:s')))], + ['Practitioner', trim((string) ($metadata['practitioner'] ?? ''))], + ]; + $identityFields = array_values(array_filter( + $identityFields, + static fn (array $field): bool => trim((string) ($field[1] ?? '')) !== '' + )); + $identityRows = ''; + for ($index = 0, $count = count($identityFields); $index < $count; $index += 2) { + $identityRows .= ''; + for ($column = 0; $column < 2; $column++) { + $field = $identityFields[$index + $column] ?? null; + if ($field === null) { + $identityRows .= ''; + continue; + } + $identityRows .= '' . $escape((string) $field[0]) . ': ' + . '' . $escape((string) $field[1]) . ''; + } + $identityRows .= ''; + } + + $css = 'body{font-family:DejaVu Sans,sans-serif;color:#17212b;font-size:10.5px}' + . '.brand{width:100%;border-collapse:collapse;border-bottom:2px solid #176b87;margin-bottom:10px}.brand td{vertical-align:middle;padding:0 0 9px 0}' + . '.brand-logo{width:125px}.facility{font-size:19px;font-weight:bold;color:#123b4a}.facility-meta{font-size:9px;color:#52606d;margin-top:2px}' + . '.document-title{font-size:14px;font-weight:bold;text-align:right;color:#176b87}.share-identity{width:100%;border-collapse:collapse;margin-bottom:14px}' + . '.share-identity td{width:50%;border:1px solid #cad7de;background:#f5f9fb;padding:6px;vertical-align:top}.field-label{color:#52606d}' + . '.share-content-table{width:100%;border-collapse:collapse}' + . '.share-content-table th,.share-content-table td{border:1px solid #cbd5df;padding:6px}.share-content-table th{background:#eaf2f5;text-align:left}' + . '.items{width:100%;border-collapse:collapse}.items th,.items td{border:1px solid #cbd5df;padding:7px}.items th{background:#eef2f6;text-align:left}' + . '.num{text-align:right}.totals{margin-top:12px;text-align:right;font-size:12px;line-height:1.6}' + . '.share-conclusion{margin-top:14px;padding:9px;border-left:3px solid #176b87;background:#f5f9fb}table{border-collapse:collapse;max-width:100%}img{max-width:100%}' + . 'h1,h2,h3{margin:8px 0}p{margin:5px 0}'; + $bodyHtml = '' + . '
' . $escape($hospitalName) . '
' + . '
' . $escape($hospitalAddress) . '
' . $escape($hospitalContact) . '
' . $escape((string) ($metadata['document_title'] ?? 'Digital Health Record')) . '
' + . '' . $identityRows . '' . $contentHtml; + + $mpdfTempDir = WRITEPATH . 'cache' . DIRECTORY_SEPARATOR . 'mpdf'; + if (! is_dir($mpdfTempDir)) { + @mkdir($mpdfTempDir, 0755, true); + } + $mpdf = new Mpdf([ + 'format' => 'A4', + 'margin_top' => 10, + 'margin_bottom' => 16, + 'margin_left' => 10, + 'margin_right' => 10, + 'tempDir' => $mpdfTempDir, + 'default_font' => 'freeserif', + ]); + $mpdf->SetHTMLFooter('
' + . $escape($footerIdentity) . 'Page {PAGENO} of {nbpg}
'); + $mpdf->WriteHTML($css, HTMLParserMode::HEADER_CSS); + $mpdf->WriteHTML($bodyHtml, HTMLParserMode::HTML_BODY); + $bytes = $mpdf->Output($fileStem . '.pdf', 'S'); + if (! is_string($bytes) || ! str_starts_with($bytes, '%PDF-')) { + return null; + } + + return [ + 'content_type' => 'application/pdf', + 'data_base64' => base64_encode($bytes), + 'title' => $fileStem . '.pdf', + 'size' => strlen($bytes), + 'hash' => base64_encode(sha1($bytes, true)), + ]; + } catch (\Throwable) { + return null; + } + } + private function resolveStoredFileAbsolutePath(string $fullPath): string { $normalized = str_replace('\\', '/', trim($fullPath)); @@ -5444,23 +5617,44 @@ class AbdmGateway extends BaseController /** * Query hospital_setting for FHIR-required hospital profile fields. * - * @return array{name: string, hfr_id: string} + * @return array{name:string,hfr_id:string,address:string,phone:string,email:string,logo:string} */ private function getHospitalProfileForFhir(): array { try { $rows = $this->db->table('hospital_setting') ->select('s_name, s_value') - ->whereIn('s_name', ['ABDM_HMS_NAME', 'ABDM_HFR_ID', 'H_Name']) + ->whereIn('s_name', [ + 'ABDM_HMS_NAME', 'ABDM_HFR_ID', 'H_Name', 'H_address_1', 'H_address_2', + 'H_phone_No', 'H_Email', 'H_logo', + ]) ->get()->getResultArray(); $map = array_column($rows, 's_value', 's_name'); $name = trim((string) ($map['ABDM_HMS_NAME'] ?? $map['H_Name'] ?? '')); $hfrId = trim((string) ($map['ABDM_HFR_ID'] ?? '')); + $address = trim(implode(', ', array_filter([ + trim((string) ($map['H_address_1'] ?? '')), + trim((string) ($map['H_address_2'] ?? '')), + ], static fn (string $value): bool => $value !== ''))); + $phone = trim((string) ($map['H_phone_No'] ?? '')); + $email = trim((string) ($map['H_Email'] ?? '')); + $logo = trim((string) ($map['H_logo'] ?? '')); } catch (\Throwable $e) { $name = ''; $hfrId = ''; + $address = ''; + $phone = ''; + $email = ''; + $logo = ''; } - return ['name' => $name, 'hfr_id' => $hfrId]; + return [ + 'name' => $name, + 'hfr_id' => $hfrId, + 'address' => $address, + 'phone' => $phone, + 'email' => $email, + 'logo' => $logo, + ]; } /** @@ -6476,11 +6670,29 @@ class AbdmGateway extends BaseController if (! empty($patientRow['dob'])) { $patient['birthDate'] = date('Y-m-d', strtotime((string) $patientRow['dob'])); } - if ($abhaId !== '') { - $patient['identifier'] = [[ - 'system' => str_contains($abhaId, '@') ? 'https://healthid.ndhm.gov.in/abha-address' : 'https://healthid.ndhm.gov.in', - 'value' => $abhaId, - ]]; + $patient['identifier'] = []; + $uhid = trim((string) ($patientRow['p_code'] ?? '')); + if ($uhid !== '') { + $patient['identifier'][] = [ + 'system' => 'https://hms.local/fhir/uhid', + 'value' => $uhid, + ]; + } + $abhaIdentity = $this->resolvePatientAbhaIdentity($patientId, $abhaId); + if ($abhaIdentity['abha_id'] !== '') { + $patient['identifier'][] = [ + 'system' => 'https://healthid.ndhm.gov.in', + 'value' => $abhaIdentity['abha_id'], + ]; + } + if ($abhaIdentity['abha_address'] !== '') { + $patient['identifier'][] = [ + 'system' => 'https://healthid.ndhm.gov.in/abha-address', + 'value' => $abhaIdentity['abha_address'], + ]; + } + if ($patient['identifier'] === []) { + unset($patient['identifier']); } return $patient; } @@ -6791,33 +7003,43 @@ class AbdmGateway extends BaseController if ($invoiceRows === '') { $invoiceRows = 'No line items'; } - $invoicePdfHtml = '

' . $escape($hospitalName) . '

Invoice ' . $escape($invoiceNumber) . '
' - . '
Patient: ' . $escape($patientName) - . '
Gender: ' . $escape((string) ($patient['gender'] ?? '')) - . '
Date of Birth: ' . $escape((string) ($patient['birthDate'] ?? '')) - . '
Visit: ' . $escape($invoiceTypeDisplay) - . '
Visit Date: ' . $escape($encounterStart) - . '
Practitioner: ' . $escape($practitionerName) - . '
Invoice Date: ' . $escape($invoiceDate) . '
' - . '' + $invoicePdfHtml = '
#ItemQtyRate (INR)
' . '' . $invoiceRows . '
#ItemQtyRate (INR)Amount (INR)
' . '
Gross: INR ' . number_format($grossAmount, 2) . '
Net: INR ' . number_format($netAmount, 2) . '
'; - $invoicePdf = $this->renderReportHtmlToPdfAttachment($invoicePdfHtml, $pdfFileStem); - if ($invoicePdf === null) { - throw new \RuntimeException('Unable to render invoice PDF attachment.'); + $patientUhid = ''; + $patientAbhaValues = []; + foreach ((array) ($patient['identifier'] ?? []) as $identifier) { + $system = (string) ($identifier['system'] ?? ''); + if (str_contains($system, '/uhid')) { + $patientUhid = trim((string) ($identifier['value'] ?? '')); + } elseif (str_contains($system, 'healthid.ndhm.gov.in')) { + $value = trim((string) ($identifier['value'] ?? '')); + if ($value !== '') { + $patientAbhaValues[] = $value; + } + } } - $invoicePdfBytes = base64_decode((string) $invoicePdf['data_base64'], true); - if (! is_string($invoicePdfBytes) || ! str_starts_with($invoicePdfBytes, '%PDF-')) { - throw new \RuntimeException('Generated invoice PDF attachment is invalid.'); - } - $documentReference = [ + $patientAbha = implode(' / ', array_values(array_unique($patientAbhaValues))); + $invoicePdf = $this->renderDigitalSharePdf( + $invoicePdfHtml, + $pdfFileStem, + [ + 'document_title' => 'Invoice ' . $invoiceNumber, + 'patient_name' => $patientName, + 'uhid' => $patientUhid, + 'abha' => $patientAbha, + 'record_label' => $invoiceTypeDisplay . ' / Invoice', + 'record_code' => $invoiceNumber, + 'reported_at' => $invoiceDate, + 'practitioner' => $practitionerName, + ] + ); + $documentReference = null; + if ($invoicePdf !== null) { + $invoicePdfBytes = base64_decode((string) $invoicePdf['data_base64'], true); + if (is_string($invoicePdfBytes) && str_starts_with($invoicePdfBytes, '%PDF-')) { + $documentReference = [ 'resourceType' => 'DocumentReference', 'id' => $documentReferenceId, 'meta' => [ @@ -6854,7 +7076,9 @@ class AbdmGateway extends BaseController 'encounter' => [['reference' => $encounterRef]], 'related' => [['reference' => $invoiceRef, 'display' => 'Invoice ' . $invoiceNumber]], ], - ]; + ]; + } + } $encounter = [ 'resourceType' => 'Encounter', @@ -6956,7 +7180,10 @@ class AbdmGateway extends BaseController ]], ]; - $resources = [$composition, $patient, $organization, $encounter, $invoiceResource, $documentReference]; + $resources = [$composition, $patient, $organization, $encounter, $invoiceResource]; + if ($documentReference !== null) { + $resources[] = $documentReference; + } if ($practitioner !== null) { $resources[] = $practitioner; } diff --git a/app/Controllers/Billing/Ipd.php b/app/Controllers/Billing/Ipd.php index 2082480..684ced0 100644 --- a/app/Controllers/Billing/Ipd.php +++ b/app/Controllers/Billing/Ipd.php @@ -988,13 +988,30 @@ class Ipd extends BaseController $ipdCode = (string) ($data['ipd_info']->ipd_code ?? ('IPD-' . $ipdId)); $fileName = 'IPD_Bill_' . preg_replace('/[^A-Za-z0-9_\-]/', '_', $ipdCode) . '_M' . $mode . '.pdf'; + $pdfBinary = $mpdf->Output($fileName, 'S'); + $this->cacheAbdmIpdBillPdf($ipdId, $pdfBinary); + return $this->response ->setHeader('Content-Type', 'application/pdf') ->setHeader('Content-Disposition', 'inline; filename="' . $fileName . '"') ->setHeader('Cache-Control', 'no-store, no-cache, must-revalidate, max-age=0') ->setHeader('Pragma', 'no-cache') ->setHeader('Expires', '0') - ->setBody($mpdf->Output($fileName, 'S')); + ->setBody($pdfBinary); + } + + private function cacheAbdmIpdBillPdf(int $ipdId, string $pdfBinary): void + { + if ($ipdId <= 0 || $pdfBinary === '' || ! str_starts_with($pdfBinary, '%PDF-')) { + return; + } + + $directory = WRITEPATH . 'uploads' . DIRECTORY_SEPARATOR . 'abdm' . DIRECTORY_SEPARATOR . 'ipd' . DIRECTORY_SEPARATOR . $ipdId; + if (! is_dir($directory) && ! mkdir($directory, 0755, true) && ! is_dir($directory)) { + return; + } + + file_put_contents($directory . DIRECTORY_SEPARATOR . 'ipd-bill.pdf', $pdfBinary, LOCK_EX); } public function showIpdForm(int $ipdId, int $formNo = 1) diff --git a/app/Controllers/Ipd_discharge.php b/app/Controllers/Ipd_discharge.php index c4c4c09..7c512de 100644 --- a/app/Controllers/Ipd_discharge.php +++ b/app/Controllers/Ipd_discharge.php @@ -7140,10 +7140,14 @@ class Ipd_discharge extends BaseController try { $headerHtml = ''; if ($withHeader) { - $headerHtml = $this->sanitizeDischargePdfHtml( - $this->applyDischargeTemplateTokens(trim((string) ($templateSettings['header_html'] ?? '')), $templateTokenVars), - false + $configuredHeader = $this->applyDischargeTemplateTokens( + trim((string) ($templateSettings['header_html'] ?? '')), + $templateTokenVars ); + if ($this->isDischargeHeaderBlank($configuredHeader)) { + $configuredHeader = $this->buildDefaultDischargeHeader($templateTokenVars); + } + $headerHtml = $this->sanitizeDischargePdfHtml($configuredHeader, false); $headerHtml = mpdf_normalize_font_weight_css($headerHtml); } @@ -7192,6 +7196,7 @@ class Ipd_discharge extends BaseController 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) { @@ -7719,6 +7724,46 @@ class Ipd_discharge extends BaseController . ''; } + private function cacheAbdmIpdPdf(int $ipdId, string $fileName, string $pdfBinary): void + { + if ($ipdId <= 0 || $pdfBinary === '' || ! str_starts_with($pdfBinary, '%PDF-')) { + return; + } + + $directory = WRITEPATH . 'uploads' . DIRECTORY_SEPARATOR . 'abdm' . DIRECTORY_SEPARATOR . 'ipd' . DIRECTORY_SEPARATOR . $ipdId; + if (! is_dir($directory) && ! mkdir($directory, 0755, true) && ! is_dir($directory)) { + return; + } + + file_put_contents($directory . DIRECTORY_SEPARATOR . $fileName, $pdfBinary, LOCK_EX); + } + + private function isDischargeHeaderBlank(string $headerHtml): bool + { + $withoutMedia = (string) preg_replace('/<(img|svg)\b[^>]*>[\s\S]*?<\/\1>|]*\/?\s*>/i', 'media', $headerHtml); + $visibleText = html_entity_decode(strip_tags($withoutMedia), ENT_QUOTES | ENT_HTML5, 'UTF-8'); + + return trim((string) preg_replace('/\s+/u', ' ', $visibleText)) === ''; + } + + /** @param array $tokens */ + private function buildDefaultDischargeHeader(array $tokens): string + { + $logoPath = trim((string) ($tokens['H_logo_abs'] ?? '')); + $logoHtml = $logoPath !== '' && is_file(html_entity_decode($logoPath, ENT_QUOTES | ENT_HTML5, 'UTF-8')) + ? '' + : ''; + + return '' + . '' . $logoHtml + . '
' + . '
' . ($tokens['H_Name'] ?? '') . '
' + . '
' . ($tokens['hospital_address'] ?? '') . '
' + . '
Phone: ' . ($tokens['H_phone_No'] ?? '') + . (($tokens['H_Email'] ?? '') !== '' ? ' | Email: ' . $tokens['H_Email'] : '') . '
' + . '
'; + } + public function show_file3(int $ipdId) { return $this->show_discharge($ipdId, 3); diff --git a/app/Libraries/Abdm/Fhir/FhirDocumentBuilder.php b/app/Libraries/Abdm/Fhir/FhirDocumentBuilder.php index 872d864..0bedc65 100644 --- a/app/Libraries/Abdm/Fhir/FhirDocumentBuilder.php +++ b/app/Libraries/Abdm/Fhir/FhirDocumentBuilder.php @@ -31,6 +31,14 @@ class FhirDocumentBuilder return $this; } + /** @param array $values */ + public function updateBundleMeta(array $values): self + { + $this->bundleMeta = array_replace($this->bundleMeta, $values); + + return $this; + } + /** * @param array $composition */ @@ -40,6 +48,16 @@ class FhirDocumentBuilder return $this; } + /** @param array $values */ + public function updateComposition(array $values): self + { + if (is_array($this->composition)) { + $this->composition = array_replace($this->composition, $values); + } + + return $this; + } + /** * @param array $resource */ @@ -120,6 +138,12 @@ class FhirDocumentBuilder return $this->addResource($resource); } + /** @param array $resource */ + public function addDocumentReference(array $resource): self + { + return $this->addResource($resource); + } + /** * @return array */ @@ -136,10 +160,71 @@ class FhirDocumentBuilder $entries[] = $entry; } - $bundle['entry'] = $entries; + $bundle['entry'] = $this->normalizeEntryReferences($entries); return $this->stripNulls($bundle); } + /** + * A urn:uuid fullUrl must contain an RFC 4122 UUID. Resource ids remain + * human-readable while all document references use deterministic UUID URNs. + * + * @param array> $entries + * @return array> + */ + private function normalizeEntryReferences(array $entries): array + { + $referenceMap = []; + foreach ($entries as $entry) { + $resource = (array) ($entry['resource'] ?? []); + $id = (string) ($resource['id'] ?? ''); + if ($id === '') { + continue; + } + $referenceMap['urn:uuid:' . $id] = 'urn:uuid:' . $this->uuidForResource($resource); + } + + foreach ($entries as &$entry) { + $resource = (array) ($entry['resource'] ?? []); + $id = (string) ($resource['id'] ?? ''); + if ($id !== '') { + $entry['fullUrl'] = $referenceMap['urn:uuid:' . $id]; + } + $entry['resource'] = $this->rewriteReferences($resource, $referenceMap); + } + unset($entry); + + return $entries; + } + + /** @param array $resource */ + private function uuidForResource(array $resource): string + { + $hex = md5((string) ($resource['resourceType'] ?? 'Resource') . '/' . (string) ($resource['id'] ?? '')); + return substr($hex, 0, 8) . '-' . substr($hex, 8, 4) . '-4' . substr($hex, 13, 3) + . '-a' . substr($hex, 17, 3) . '-' . substr($hex, 20, 12); + } + + /** + * @param mixed $value + * @param array $referenceMap + * @return mixed + */ + private function rewriteReferences($value, array $referenceMap) + { + if (is_string($value)) { + return $referenceMap[$value] ?? $value; + } + if (! is_array($value)) { + return $value; + } + + foreach ($value as $key => $nested) { + $value[$key] = $this->rewriteReferences($nested, $referenceMap); + } + + return $value; + } + /** * @param array $resource */ diff --git a/app/Libraries/Abdm/Fhir/Generators/AbstractModuleFhirGenerator.php b/app/Libraries/Abdm/Fhir/Generators/AbstractModuleFhirGenerator.php index 8d5644d..5c20e03 100644 --- a/app/Libraries/Abdm/Fhir/Generators/AbstractModuleFhirGenerator.php +++ b/app/Libraries/Abdm/Fhir/Generators/AbstractModuleFhirGenerator.php @@ -38,6 +38,9 @@ abstract class AbstractModuleFhirGenerator $patient = [ 'resourceType' => 'Patient', 'id' => 'patient-' . $patientId, + 'meta' => ['profile' => [ + 'https://nrces.in/ndhm/fhir/r4/StructureDefinition/Patient', + ]], 'identifier' => [ [ 'system' => 'https://hms.local/patient-id', @@ -116,11 +119,21 @@ abstract class AbstractModuleFhirGenerator if ($id === '') { return null; } + $classCode = strtoupper(trim((string) ($enc['class_code'] ?? 'AMB'))); + $classDisplay = $classCode === 'IMP' ? 'inpatient encounter' : 'ambulatory'; return [ 'resourceType' => 'Encounter', 'id' => 'encounter-' . $id, + 'meta' => ['profile' => [ + 'https://nrces.in/ndhm/fhir/r4/StructureDefinition/Encounter', + ]], 'status' => 'finished', + 'class' => [ + 'system' => 'http://terminology.hl7.org/CodeSystem/v3-ActCode', + 'code' => $classCode, + 'display' => $classDisplay, + ], 'subject' => [ 'reference' => 'urn:uuid:patient-' . (string) ($source['patient']['id'] ?? ''), ], @@ -152,6 +165,9 @@ abstract class AbstractModuleFhirGenerator return [ 'resourceType' => 'Practitioner', 'id' => 'practitioner-' . ($id !== '' ? $id : md5($name)), + 'meta' => ['profile' => [ + 'https://nrces.in/ndhm/fhir/r4/StructureDefinition/Practitioner', + ]], 'name' => [[ 'text' => $name, ]], @@ -179,6 +195,9 @@ abstract class AbstractModuleFhirGenerator return [ 'resourceType' => 'Organization', 'id' => 'organization-' . ($id !== '' ? $id : md5($name)), + 'meta' => ['profile' => [ + 'https://nrces.in/ndhm/fhir/r4/StructureDefinition/Organization', + ]], 'name' => $name, 'identifier' => $id !== '' ? [[ 'system' => 'https://hms.local/organization-id', diff --git a/app/Libraries/Abdm/Fhir/Generators/DischargeFhirGenerator.php b/app/Libraries/Abdm/Fhir/Generators/DischargeFhirGenerator.php index 7c0a09c..ac7e934 100644 --- a/app/Libraries/Abdm/Fhir/Generators/DischargeFhirGenerator.php +++ b/app/Libraries/Abdm/Fhir/Generators/DischargeFhirGenerator.php @@ -15,14 +15,24 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac $patientId = (string) ($source['patient']['id'] ?? '0'); $sessionId = (string) ($source['session_id'] ?? '0'); $visitDate = (string) ($source['visit_date'] ?? date('Y-m-d')); + $bundleIdentifier = trim((string) ($source['bundle_identifier'] ?? '')); + if ($bundleIdentifier === '') { + $bundleIdentifier = 'discharge-' . $recordId . '-' . strtotime($timestamp); + } $careContextReference = 'DISCHARGE-' . $recordId . '-S' . $sessionId . '-' . $visitDate; $careContextDisplay = 'Discharge Summary ' . $visitDate; $builder = new \App\Libraries\Abdm\Fhir\FhirDocumentBuilder(); $builder - ->buildBundleMeta('discharge-' . $recordId . '-' . strtotime($timestamp), $timestamp) - ->buildComposition($this->buildBaseComposition($source, 'Discharge Summary', '18842-5', 'Discharge summary')) + ->buildBundleMeta($bundleIdentifier, $timestamp) + ->updateBundleMeta(['meta' => ['profile' => [ + 'https://nrces.in/ndhm/fhir/r4/StructureDefinition/DocumentBundle', + ]]]) + ->buildComposition($this->buildBaseComposition($source, 'IPD Discharge Summary', '18842-5', 'Discharge summary')) + ->updateComposition(['meta' => ['profile' => [ + 'https://nrces.in/ndhm/fhir/r4/StructureDefinition/DischargeSummaryRecord', + ]]]) ->addPatient($this->buildBasePatient($source)); $encounter = $this->buildEncounter($source); @@ -52,6 +62,26 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac $builder->addOrganization($organization); } + $compositionUpdate = []; + if (is_array($practitioner)) { + $compositionUpdate['author'] = [[ + 'reference' => 'urn:uuid:' . (string) $practitioner['id'], + 'display' => (string) ($practitioner['name'][0]['text'] ?? ''), + ]]; + } elseif (is_array($organization)) { + $compositionUpdate['author'] = [[ + 'reference' => 'urn:uuid:' . (string) $organization['id'], + 'display' => (string) ($organization['name'] ?? ''), + ]]; + } + if (is_array($organization)) { + $compositionUpdate['custodian'] = [ + 'reference' => 'urn:uuid:' . (string) $organization['id'], + 'display' => (string) ($organization['name'] ?? ''), + ]; + } + $builder->updateComposition($compositionUpdate); + $patientRef = 'urn:uuid:patient-' . $patientId; $encounterRef = is_array($encounter) ? 'urn:uuid:' . (string) ($encounter['id'] ?? '') : null; @@ -62,11 +92,12 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac } $resolved = $this->codingResolver->resolveSnomedForDiagnosisOrFinding((string) ($cond['code'] ?? ''), $text); + $coding = $this->withoutPlaceholderCoding((array) ($resolved['coding'] ?? [])); $builder->addCondition([ 'resourceType' => 'Condition', 'id' => 'discharge-condition-' . $recordId . '-' . $idx, 'code' => [ - 'coding' => $resolved['coding'] ?? [], + 'coding' => $coding, 'text' => $text, ], 'subject' => ['reference' => $patientRef], @@ -81,12 +112,13 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac } $resolved = $this->codingResolver->resolveSnomedForDiagnosisOrFinding((string) ($proc['code'] ?? ''), $text); + $coding = $this->withoutPlaceholderCoding((array) ($resolved['coding'] ?? [])); $builder->addProcedure([ 'resourceType' => 'Procedure', 'id' => 'discharge-procedure-' . $recordId . '-' . $idx, 'status' => 'completed', 'code' => [ - 'coding' => $resolved['coding'] ?? [], + 'coding' => $coding, 'text' => $text, ], 'subject' => ['reference' => $patientRef], @@ -240,6 +272,85 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac ]); } + foreach ((array) ($source['documents'] ?? []) as $idx => $document) { + $data = trim((string) ($document['data'] ?? '')); + if ($data === '') { + continue; + } + + $builder->addDocumentReference([ + 'resourceType' => 'DocumentReference', + 'id' => 'discharge-document-' . $recordId . '-' . $idx, + 'status' => 'current', + 'type' => ['coding' => [[ + 'system' => 'http://loinc.org', + 'code' => (string) ($document['loinc_code'] ?? '55107-7'), + 'display' => (string) ($document['title'] ?? 'Clinical document'), + ]]], + 'subject' => ['reference' => $patientRef], + 'date' => (string) ($document['created_at'] ?? $timestamp), + 'author' => is_array($practitioner) ? [[ + 'reference' => 'urn:uuid:' . (string) $practitioner['id'], + ]] : null, + 'custodian' => is_array($organization) ? [ + 'reference' => 'urn:uuid:' . (string) $organization['id'], + ] : null, + 'description' => (string) ($document['title'] ?? 'Clinical document'), + 'content' => [[ + 'attachment' => [ + 'contentType' => (string) ($document['content_type'] ?? 'application/pdf'), + 'language' => 'en-IN', + 'data' => $data, + 'title' => (string) ($document['title'] ?? 'Clinical document'), + 'creation' => (string) ($document['created_at'] ?? $timestamp), + ], + ]], + ]); + } + + $sectionDefinitions = [ + ['title' => 'Diagnosis', 'code' => '11450-4', 'prefix' => 'discharge-condition-', 'items' => (array) ($source['conditions'] ?? [])], + ['title' => 'Procedures', 'code' => '47519-4', 'prefix' => 'discharge-procedure-', 'items' => (array) ($source['procedures'] ?? [])], + ['title' => 'Discharge medications', 'code' => '10183-2', 'prefix' => 'discharge-medication-', 'items' => (array) ($source['medications'] ?? [])], + ['title' => 'Observations', 'code' => '8716-3', 'prefix' => 'discharge-observation-', 'items' => (array) ($source['observations'] ?? [])], + ['title' => 'Investigations', 'code' => '18776-5', 'prefix' => 'discharge-investigation-', 'items' => (array) ($source['investigations'] ?? [])], + ['title' => 'Allergies', 'code' => '48765-2', 'prefix' => 'discharge-allergy-', 'items' => (array) ($source['allergies'] ?? [])], + ['title' => 'Discharge advice', 'code' => '8653-8', 'prefix' => 'discharge-careplan-', 'items' => (array) ($source['care_plans'] ?? [])], + ['title' => 'Attached documents', 'code' => '55107-7', 'prefix' => 'discharge-document-', 'items' => (array) ($source['documents'] ?? [])], + ]; + $sections = []; + foreach ($sectionDefinitions as $definition) { + $references = []; + $narrativeParts = []; + foreach ($definition['items'] as $idx => $item) { + $references[] = ['reference' => 'urn:uuid:' . $definition['prefix'] . $recordId . '-' . $idx]; + $narrative = $this->sectionItemNarrative((array) $item); + if ($narrative !== '') { + $narrativeParts[] = $narrative; + } + } + if ($references === []) { + continue; + } + $sections[] = [ + 'title' => $definition['title'], + 'code' => ['coding' => [[ + 'system' => 'http://loinc.org', + 'code' => $definition['code'], + ]]], + 'entry' => $references, + 'text' => $narrativeParts !== [] ? [ + 'status' => 'generated', + 'div' => '
  • ' + . implode('
  • ', array_map('htmlspecialchars', $narrativeParts)) + . '
', + ] : null, + ]; + } + if ($sections !== []) { + $builder->updateComposition(['section' => $sections]); + } + $bundle = $builder->toBundle(); $validation = $this->validator->validate($bundle, 'discharge', ['resolved' => 1, 'unresolved' => 0, 'fallback_used' => 0]); @@ -251,4 +362,33 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac 'validation' => $validation->toArray(), ]; } + + /** + * @param array> $coding + * @return array> + */ + private function withoutPlaceholderCoding(array $coding): array + { + return array_values(array_filter($coding, static function (array $item): bool { + $code = strtoupper(trim((string) ($item['code'] ?? ''))); + return $code !== '' && $code !== 'UNMAPPED'; + })); + } + + /** @param array $item */ + private function sectionItemNarrative(array $item): string + { + foreach (['text', 'description', 'title', 'name', 'value'] as $field) { + $value = trim((string) ($item[$field] ?? '')); + if ($value !== '') { + if ($field === 'name') { + $dosage = trim((string) ($item['dosage'] ?? '')); + return trim($value . ($dosage !== '' ? ' - ' . $dosage : '')); + } + return trim(strip_tags($value)); + } + } + + return ''; + } } diff --git a/app/Libraries/Abdm/Fhir/Support/FhirBundleValidator.php b/app/Libraries/Abdm/Fhir/Support/FhirBundleValidator.php index bf6c6fe..8829a24 100644 --- a/app/Libraries/Abdm/Fhir/Support/FhirBundleValidator.php +++ b/app/Libraries/Abdm/Fhir/Support/FhirBundleValidator.php @@ -51,6 +51,10 @@ class FhirBundleValidator $resourceRefs[$rtype . '/' . $rid] = true; $resourceRefs['urn:uuid:' . $rid] = true; } + $fullUrl = trim((string) ($entry['fullUrl'] ?? '')); + if ($fullUrl !== '') { + $resourceRefs[$fullUrl] = true; + } if ($rtype === 'DiagnosticReport') { $results = $resource['result'] ?? []; diff --git a/app/Libraries/Abdm/Sync/AbdmSyncWorkerService.php b/app/Libraries/Abdm/Sync/AbdmSyncWorkerService.php index b456975..1d287a3 100644 --- a/app/Libraries/Abdm/Sync/AbdmSyncWorkerService.php +++ b/app/Libraries/Abdm/Sync/AbdmSyncWorkerService.php @@ -146,14 +146,20 @@ class AbdmSyncWorkerService $result = $this->gatewayClient->pushRecord($recordPayload, $idempotencyKey); - $this->recordModel->update($syncRecordId, [ + $recordUpdate = [ 'sync_status' => (bool) ($result['ok'] ?? false) ? 'done' : ((bool) ($result['retryable'] ?? false) ? 'failed' : 'dead'), 'gateway_record_id' => (int) ($result['gateway_record_id'] ?? 0) ?: null, 'gateway_queue_id' => (string) ($result['gateway_queue_id'] ?? '') ?: null, 'last_synced_at' => date('Y-m-d H:i:s'), 'last_error' => (bool) ($result['ok'] ?? false) ? null : (string) ($result['message'] ?? 'Sync failed'), 'retry_count' => (int) ($record['retry_count'] ?? 0) + ((bool) ($result['ok'] ?? false) ? 0 : 1), - ]); + ]; + if ($this->db->fieldExists('first_pushed_at', 'abdm_sync_record')) { + $recordUpdate['first_pushed_at'] = (string) ($result['first_pushed_at'] ?? '') ?: null; + $this->db->table('abdm_sync_record')->where('id', $syncRecordId)->update($recordUpdate); + } else { + $this->recordModel->update($syncRecordId, $recordUpdate); + } return $result; } diff --git a/app/Libraries/FhirR4Builder.php b/app/Libraries/FhirR4Builder.php index 049310a..fa76583 100644 --- a/app/Libraries/FhirR4Builder.php +++ b/app/Libraries/FhirR4Builder.php @@ -938,6 +938,9 @@ class FhirR4Builder $pdfDocRefRef = 'urn:uuid:' . $pdfDocRefUuid; $pdfContentType = trim((string) ($attachment['content_type'] ?? 'application/pdf')) ?: 'application/pdf'; $pdfTitle = trim((string) ($attachment['title'] ?? 'Lab Report PDF')) ?: 'Lab Report PDF'; + $pdfBytes = base64_decode($pdfData, true); + $pdfSize = (int) ($attachment['size'] ?? (is_string($pdfBytes) ? strlen($pdfBytes) : 0)); + $pdfHash = trim((string) ($attachment['hash'] ?? (is_string($pdfBytes) ? base64_encode(sha1($pdfBytes, true)) : ''))); $resourceEntries[] = ['fullUrl' => 'urn:uuid:' . $pdfBinaryUuid, 'resource' => [ 'resourceType' => 'Binary', @@ -955,12 +958,14 @@ class FhirR4Builder 'type' => ['coding' => [['system' => 'http://snomed.info/sct', 'code' => '721981007', 'display' => 'Diagnostic studies report']]], 'subject' => ['reference' => $patientRef], 'content' => [[ - 'attachment' => [ + 'attachment' => array_filter([ 'contentType' => $pdfContentType, 'url' => 'urn:uuid:' . $pdfBinaryUuid, 'data' => $pdfData, 'title' => $pdfTitle, - ], + 'size' => $pdfSize > 0 ? $pdfSize : null, + 'hash' => $pdfHash !== '' ? $pdfHash : null, + ], static fn ($value): bool => $value !== null), ]], ]]; diff --git a/app/Views/abdm/task_board.php b/app/Views/abdm/task_board.php index 69fadb3..383fbdb 100644 --- a/app/Views/abdm/task_board.php +++ b/app/Views/abdm/task_board.php @@ -23,14 +23,14 @@
- - - - - - - - + + + + + + + +
@@ -165,25 +165,25 @@
- + - + - + - + - + - + - + - +
@@ -757,6 +757,19 @@ statusBox.style.color = isError ? '#dc3545' : '#6c757d'; } + function setSubmissionStatus(msg, queueId) { + setStatus(msg, false); + if (!queueId) { + return; + } + statusBox.appendChild(document.createTextNode(' ')); + var logLink = document.createElement('a'); + logLink.href = '' + '?search=' + encodeURIComponent(queueId); + logLink.textContent = 'View Bridge Log'; + logLink.className = 'ms-1'; + statusBox.appendChild(logLink); + } + function post(url, payload, cb) { payload[csrfName] = csrfHash; var body = Object.keys(payload).map(function (key) { @@ -1665,6 +1678,7 @@ var _fhirInvoiceSource = ''; var _fhirInvoiceBillId = 0; var _fhirInvoicePatientId = 0; + var _fhirPreviewUrl = ''; // ── Inline complaint SNOMED edit (event delegation on fhirFormView) ─ var _editTimer = null; @@ -2535,6 +2549,7 @@ function openFhirModal(url, title, options) { options = options || {}; + _fhirPreviewUrl = url; _fhirSubmitMode = options.submitMode || 'opd'; _fhirSubmitTaskRow = options.taskRow || null; _fhirSubmitAbha = (options.abhaId || '').trim(); @@ -2588,14 +2603,19 @@ } if (btnRegenerateFhirModal) { + var taskType = _fhirSubmitTaskRow ? (_fhirSubmitTaskRow.getAttribute('data-task-type') || '').toLowerCase() : ''; + var canRebuildDiagnostic = _fhirSubmitMode === 'task' + && (taskType === 'lab_report_publish' || taskType === 'radiology_report_publish'); btnRegenerateFhirModal.disabled = false; - btnRegenerateFhirModal.style.display = _fhirSubmitMode === 'task' ? 'none' : ''; - btnRegenerateFhirModal.innerHTML = _fhirSubmitMode === 'invoice' - ? ' Rebuild Bundle' + btnRegenerateFhirModal.style.display = _fhirSubmitMode === 'task' && !canRebuildDiagnostic ? 'none' : ''; + btnRegenerateFhirModal.innerHTML = _fhirSubmitMode === 'invoice' || canRebuildDiagnostic + ? ' Rebuild Digital Record' : ' Regenerate'; - btnRegenerateFhirModal.title = _fhirSubmitMode === 'invoice' - ? 'Rebuild FHIR JSON from current billing data' - : 'Rebuild FHIR bundle from current prescription data'; + btnRegenerateFhirModal.title = canRebuildDiagnostic + ? 'Rebuild FHIR and Digital Share PDF from the latest report data' + : (_fhirSubmitMode === 'invoice' + ? 'Rebuild FHIR and Digital Share PDF from current billing data' + : 'Rebuild FHIR bundle from current prescription data'); } fhirPreviewModal.show(); @@ -2632,6 +2652,20 @@ // Regenerate FHIR bundle in modal btnRegenerateFhirModal.addEventListener('click', function () { var btn = this; + var taskType = _fhirSubmitTaskRow ? (_fhirSubmitTaskRow.getAttribute('data-task-type') || '').toLowerCase() : ''; + if (_fhirSubmitMode === 'task' && (taskType === 'lab_report_publish' || taskType === 'radiology_report_publish')) { + if (_fhirPreviewUrl === '') { + alert('Diagnostic preview URL is unavailable.'); + return; + } + if (!confirm('Rebuild this FHIR record and Digital Share PDF from the latest report data?')) return; + openFhirModal(_fhirPreviewUrl, fhirModalTitle ? fhirModalTitle.textContent : 'Diagnostic FHIR Preview', { + submitMode: 'task', + taskRow: _fhirSubmitTaskRow, + abhaId: _fhirSubmitAbha + }); + return; + } if (_fhirSubmitMode === 'invoice') { if (!_fhirInvoiceSource || _fhirInvoiceBillId <= 0 || _fhirInvoicePatientId <= 0) { alert('Invoice source data is incomplete.'); @@ -2812,17 +2846,13 @@ var queueText = res.queue_id ? (' Queue ID: ' + res.queue_id) : ''; var warningText = res.warning ? (' Warning: ' + res.warning) : ''; - setStatus('Submitted to ABDM gateway.' + queueText + warningText); + setSubmissionStatus('Submitted to ABDM gateway.' + queueText + warningText, res.queue_id || ''); var badge = _fhirSubmitTaskRow.querySelector('.status-pill'); if (badge) { badge.textContent = 'IN_PROGRESS'; badge.className = 'badge bg-info status-pill'; } alert('Submitted to ABDM gateway.' + queueText + warningText); - if (res.queue_id) { - var logUrl = '' + '?search=' + encodeURIComponent(res.queue_id); - window.open(logUrl, '_blank'); - } fhirPreviewModal.hide(); }); return; @@ -2858,17 +2888,13 @@ var queueText = res.queue_id ? (' Queue ID: ' + res.queue_id) : ''; var warningText = res.warning ? (' Warning: ' + res.warning) : ''; - setStatus('Submitted to ABDM gateway.' + queueText + warningText); + setSubmissionStatus('Submitted to ABDM gateway.' + queueText + warningText, res.queue_id || ''); var badge = _fhirSubmitTaskRow.querySelector('.status-pill'); if (badge) { badge.textContent = 'IN_PROGRESS'; badge.className = 'badge bg-info status-pill'; } alert('Submitted to ABDM gateway.' + queueText + warningText); - if (res.queue_id) { - var logUrl = '' + '?search=' + encodeURIComponent(res.queue_id); - window.open(logUrl, '_blank'); - } fhirPreviewModal.hide(); }); return; diff --git a/tests/unit/Abdm/FhirGeneratorsTest.php b/tests/unit/Abdm/FhirGeneratorsTest.php index feda632..357fa1e 100644 --- a/tests/unit/Abdm/FhirGeneratorsTest.php +++ b/tests/unit/Abdm/FhirGeneratorsTest.php @@ -261,6 +261,7 @@ final class FhirGeneratorsTest extends CIUnitTestCase $this->assertIsString($pdfBytes); $this->assertSame('application/pdf', $attachment['contentType'] ?? null); $this->assertStringStartsWith('%PDF-', $pdfBytes); + $this->assertStringNotContainsString('table.items{', $pdfBytes); $this->assertSame(strlen($pdfBytes), $attachment['size'] ?? null); $this->assertSame(base64_encode(sha1($pdfBytes, true)), $attachment['hash'] ?? null); $this->assertSame(