fix(abdm): sync care context reference format and auto-push FHIR bundle on linking

- Align care context reference format (RAD- prefix for radiology, clean Ymd date) between discovery, shareDiagnosisReportBundle, and AbdmTaskBoardSyncService
- Automatically push FHIR document bundle to Bridge (POST /api/v3/records/push) on care context linking or already-linked confirmation in abdm_hip_link_modal
- Provide clear instructions to user regarding PHR app 'Fetch Records' pull flow
- Add unit test testShareDiagnosisReportBundleFormatsRadiologyCareContext
This commit is contained in:
DevSoftTech Server 2026-09-29 15:06:31 +05:30
parent 41d89a1969
commit 1a21f16f7b
4 changed files with 153 additions and 30 deletions

View File

@ -2007,11 +2007,11 @@ class AbdmGateway extends BaseController
return $this->response->setStatusCode(403)->setJSON(['error' => 'AJAX only']);
}
$labReqId = (int) ($this->request->getPost('lab_req_id') ?? 0);
$patientId = (int) ($this->request->getPost('patient_id') ?? 0);
$abhaId = trim((string) ($this->request->getPost('abha_id') ?? ''));
$abhaAddressPost = trim((string) ($this->request->getPost('abha_address') ?? ''));
$consentHandle = trim((string) ($this->request->getPost('consent_handle') ?? ''));
$labReqId = (int) ($this->request->getPost('lab_req_id') ?? $_POST['lab_req_id'] ?? 0);
$patientId = (int) ($this->request->getPost('patient_id') ?? $_POST['patient_id'] ?? 0);
$abhaId = trim((string) ($this->request->getPost('abha_id') ?? $_POST['abha_id'] ?? ''));
$abhaAddressPost = trim((string) ($this->request->getPost('abha_address') ?? $_POST['abha_address'] ?? ''));
$consentHandle = trim((string) ($this->request->getPost('consent_handle') ?? $_POST['consent_handle'] ?? ''));
if ($labReqId <= 0 || $patientId <= 0) {
return $this->response->setJSON(['ok' => 0, 'error' => 'lab_req_id and patient_id are required']);
@ -2103,14 +2103,16 @@ class AbdmGateway extends BaseController
}
// -- Load LOINC code for the panel from lab_repo -----------------------
$labRepoRow = $this->db->table('lab_request lr')
->select('lr.lab_repo_id, repo.loinc_code AS repo_loinc_code, repo.Title')
->join('lab_repo repo', 'repo.mstRepoKey = lr.lab_repo_id', 'left')
->where('lr.id', $labReqId)
->get(1)
->getRowArray() ?? [];
$repoLoincCode = trim((string) ($labRepoRow['repo_loinc_code'] ?? ''));
$repoLoincCode = '';
if ($this->db->tableExists('lab_repo')) {
$labRepoRow = $this->db->table('lab_request lr')
->select('lr.lab_repo_id, repo.loinc_code AS repo_loinc_code, repo.Title')
->join('lab_repo repo', 'repo.mstRepoKey = lr.lab_repo_id', 'left')
->where('lr.id', $labReqId)
->get(1)
->getRowArray() ?? [];
$repoLoincCode = trim((string) ($labRepoRow['repo_loinc_code'] ?? ''));
}
if ($repoLoincCode !== '') {
$diagnosticReport['loinc_code'] = $repoLoincCode;
}
@ -2144,7 +2146,16 @@ class AbdmGateway extends BaseController
$bundleJson = (string) json_encode($bundle, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
// -- Store health_record -----------------------------------------------
$ccRef = 'LAB-' . $labReqId . '-' . $visitDate;
$requestedCcRef = trim((string) ($this->request->getPost('careContextId') ?? $this->request->getPost('care_context_reference') ?? ''));
if ($requestedCcRef !== '') {
$ccRef = $requestedCcRef;
} else {
$prefix = $isImaging ? 'RAD' : 'LAB';
$dateClean = str_replace('-', '', $visitDate);
$ccRef = $prefix . '-' . $labReqId . '-' . $dateClean;
}
$careContextDisplay = ($isImaging ? 'Radiology Report - ' : 'Diagnostic Report - ') . ($testTitle !== '' ? $testTitle : ($isImaging ? 'Radiology Report' : 'Lab Report')) . ' (' . date('d M Y', strtotime($visitDate)) . ')';
$healthRecordId = $this->storeHealthRecord([
'patient_id' => $patientId,
'abha_id' => $abhaId,
@ -2181,6 +2192,7 @@ class AbdmGateway extends BaseController
'status' => 'local_stored',
'queue_id' => null,
'consent_handle' => null,
'care_context_reference' => $ccRef,
'message' => 'ABHA not available. Record stored locally for later discovery.',
]);
}
@ -2200,9 +2212,9 @@ class AbdmGateway extends BaseController
'record_type' => 'DiagnosticReportRecord',
'visit_date' => $visitDate,
'care_context_reference' => $ccRef,
'care_context_display' => $testTitle !== '' ? $testTitle : 'Lab Report',
'notes' => $testTitle !== '' ? $testTitle : 'Lab Report',
'queue_id' => 'LAB-' . $labReqId . '-' . $visitDate,
'care_context_display' => $careContextDisplay,
'notes' => $careContextDisplay,
'queue_id' => $ccRef,
'record_data' => $bundle,
]);
$this->logGatewayPushResolution('diagnostic_report', $result);
@ -2235,13 +2247,14 @@ class AbdmGateway extends BaseController
]);
return $this->response->setJSON([
'ok' => $connectorError === null ? 1 : 0,
'queue_id' => $queueId,
'consent_handle' => $effectiveConsent,
'status' => $connectorError === null ? 'queued' : 'failed',
'message' => $connectorError === null && $consentWarning !== '' ? 'Record pushed to gateway without active consent. Recheck link status in M2 ABDM Gateway.' : null,
'warning' => $consentWarning !== '' ? $consentWarning : null,
'error' => $connectorError,
'ok' => $connectorError === null ? 1 : 0,
'queue_id' => $queueId,
'consent_handle' => $effectiveConsent,
'care_context_reference' => $ccRef,
'status' => $connectorError === null ? 'queued' : 'failed',
'message' => $connectorError === null && $consentWarning !== '' ? 'Record pushed to gateway without active consent. Recheck link status in M2 ABDM Gateway.' : null,
'warning' => $consentWarning !== '' ? $consentWarning : null,
'error' => $connectorError,
]);
}

View File

@ -726,14 +726,16 @@ class AbdmTaskBoardSyncService
$visitDate = date('Y-m-d');
$prefix = match ($taskType) {
'lab_report_publish', 'radiology_report_publish' => 'LAB-',
'radiology_report_publish' => 'RAD-',
'lab_report_publish' => 'LAB-',
'immunization_record_publish' => 'IMM-',
'wellness_record_publish' => 'WELLNESS-',
'health_document_publish' => 'DOC-',
'ipd_discharge_publish' => 'IPD-',
'wellness_record_publish' => 'WELLNESS-',
'health_document_publish' => 'DOC-',
'ipd_discharge_publish' => 'DISCHARGE-',
default => 'REC-',
};
$careContextRef = $prefix . $entityId . '-' . $visitDate;
$cleanVisitDate = str_replace('-', '', $visitDate);
$careContextRef = $prefix . $entityId . '-' . $cleanVisitDate;
$careContextDisplay = $hiType . ' ' . $visitDate;
$pushData = [

View File

@ -596,6 +596,64 @@
modal.show();
};
function syncAndPushTaskFhirRecord(selectedContexts, alertBox, abhaAddress) {
if (!window.currentHipTaskContext || !window.currentHipTaskContext.taskType || !window.currentHipTaskContext.entityId) {
return;
}
var ctxTaskType = window.currentHipTaskContext.taskType;
var ctxEntityId = window.currentHipTaskContext.entityId;
var ctxPatientId = window.currentHipTaskContext.patientId || 0;
var firstRef = (selectedContexts && selectedContexts.length > 0) ? selectedContexts[0].ref : '';
var csrf = getCsrfData();
if (ctxTaskType === 'radiology_report_publish' || ctxTaskType === 'lab_report_publish') {
var pushData = new URLSearchParams();
pushData.append('lab_req_id', ctxEntityId);
pushData.append('patient_id', ctxPatientId);
pushData.append('abha_id', abhaAddress);
if (firstRef) pushData.append('care_context_reference', firstRef);
pushData.append(csrf.name || 'csrf_hms', csrf.hash);
fetch('<?= base_url('AbdmGateway/share_diagnosis_report_bundle') ?>', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'application/json'
},
body: pushData.toString()
}).then(function(r) { return r.json(); }).then(function(pRes) {
updateCsrf(pRes);
if (pRes && pRes.ok === 1 && alertBox) {
alertBox.innerHTML += '<div class="mt-2 pt-2 border-top border-success-subtle text-dark small"><i class="bi bi-file-earmark-medical text-success me-1"></i><strong>FHIR Document Stored on Bridge:</strong> Report bundle is active on Bridge for Care Context <code>' + (pRes.care_context_reference || firstRef) + '</code>.<br><i class="bi bi-info-circle text-primary me-1"></i><strong>To view in PHR App:</strong> Open your PHR app (e.g. ABHA App) &rarr; tap <em>Linked Facilities</em> &rarr; tap <em>"Fetch Records" / "Pull Records"</em>.</div>';
}
}).catch(function() {});
} else if (ctxTaskType === 'opd_prescription_publish') {
var pushData = new URLSearchParams();
pushData.append('opd_id', ctxEntityId);
pushData.append('patient_id', ctxPatientId);
pushData.append('abha_id', abhaAddress);
pushData.append('push_to_gateway', '1');
if (firstRef) pushData.append('care_context_reference', firstRef);
pushData.append(csrf.name || 'csrf_hms', csrf.hash);
fetch('<?= base_url('AbdmGateway/share_prescription_bundle') ?>', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
'X-Requested-With': 'XMLHttpRequest',
'Accept': 'application/json'
},
body: pushData.toString()
}).then(function(r) { return r.json(); }).then(function(pRes) {
updateCsrf(pRes);
if (pRes && pRes.ok === 1 && alertBox) {
alertBox.innerHTML += '<div class="mt-2 pt-2 border-top border-success-subtle text-dark small"><i class="bi bi-file-earmark-medical text-success me-1"></i><strong>FHIR Document Stored on Bridge:</strong> Consultation bundle is active on Bridge for Care Context <code>' + (pRes.care_context_reference || firstRef) + '</code>.<br><i class="bi bi-info-circle text-primary me-1"></i><strong>To view in PHR App:</strong> Open your PHR app (e.g. ABHA App) &rarr; tap <em>Linked Facilities</em> &rarr; tap <em>"Fetch Records" / "Pull Records"</em>.</div>';
}
}).catch(function() {});
}
}
/**
* Execute 2-step HIP-Initiated Linking via Method 4.
*/
@ -665,6 +723,8 @@
+ 'The selected care context(s) are already linked with this patient\'s ABHA. Patient can view and fetch the records directly in their PHR app.';
alertBox.classList.remove('d-none');
syncAndPushTaskFhirRecord(selectedContexts, alertBox, abhaAddress);
// If initiated for a specific task board task, sync task to completed so status isn't stuck on Ready to link!
if (window.currentHipTaskContext && window.currentHipTaskContext.taskId > 0) {
var tId = window.currentHipTaskContext.taskId;
@ -783,6 +843,8 @@
alertBox.innerHTML = '<strong><i class="bi bi-check-circle-fill me-1"></i>Success!</strong> '
+ 'Care contexts successfully submitted to ABDM for linking. Patient can now discover and access them in their ABHA / PHR app.';
syncAndPushTaskFhirRecord(selectedContexts, alertBox, abhaAddress);
// If initiated for a specific task board task, mark the task as completed
if (window.currentHipTaskContext && window.currentHipTaskContext.taskId > 0) {
var tId = window.currentHipTaskContext.taskId;
@ -832,6 +894,8 @@
alertBox.innerHTML = '<strong><i class="bi bi-check-circle-fill me-1"></i>Already Linked!</strong> '
+ 'ABDM reports this care context is already linked to patient\'s ABHA. Task status updated to Linked.';
syncAndPushTaskFhirRecord(selectedContexts, alertBox, abhaAddress);
if (window.currentHipTaskContext && window.currentHipTaskContext.taskId > 0) {
var tId = window.currentHipTaskContext.taskId;
var taskNote = 'Care context confirmed already linked in ABDM: ' + selectedContexts.map(function(c) { return c.ref; }).join(', ');

View File

@ -20,6 +20,7 @@ final class CareContextDiscoveryTest extends CIUnitTestCase
$db->query("CREATE TABLE IF NOT EXISTS " . $db->prefixTable('lab_request') . " (
id INTEGER PRIMARY KEY,
patient_id INTEGER,
patient_name TEXT,
lab_type INTEGER,
charge_id INTEGER,
report_name TEXT,
@ -33,7 +34,8 @@ final class CareContextDiscoveryTest extends CIUnitTestCase
$db->query("CREATE TABLE IF NOT EXISTS " . $db->prefixTable('charge_master') . " (
id INTEGER PRIMARY KEY,
charge_name TEXT
charge_name TEXT,
charge_type TEXT
)");
$db->query("CREATE TABLE IF NOT EXISTS " . $db->prefixTable('opd_prescription') . " (
@ -49,7 +51,12 @@ final class CareContextDiscoveryTest extends CIUnitTestCase
patient_id INTEGER,
abha_id TEXT,
hi_type TEXT,
entity_type TEXT,
entity_id TEXT,
fhir_bundle TEXT,
care_context_reference TEXT,
consent_handle TEXT,
push_status TEXT,
created_at TEXT,
updated_at TEXT
)");
@ -280,4 +287,41 @@ final class CareContextDiscoveryTest extends CIUnitTestCase
$this->assertTrue($foundIpd, 'IPD DischargeSummaryRecord should be discovered');
$this->assertTrue($foundImm, 'ImmunizationRecord should be discovered');
}
public function testShareDiagnosisReportBundleFormatsRadiologyCareContext(): void
{
$_SERVER['HTTP_X_REQUESTED_WITH'] = 'xmlhttprequest';
$_POST['lab_req_id'] = '31';
$_POST['patient_id'] = '12';
$_POST['abha_id'] = 'singhkeshav301976@sbx';
$_POST['care_context_reference'] = 'RAD-31-20260929';
$req = service('request');
$req->setHeader('X-Requested-With', 'XMLHttpRequest');
$this->gateway->initController($req, service('response'), service('logger'));
$mockConnector = $this->createMock(\App\Libraries\Abdm\AbdmConnectorInterface::class);
$mockConnector->method('pushRecord')
->willReturnCallback(function (array $data) {
return [
'ok' => 1,
'http_code' => 201,
'status' => 'queued',
'queue_id' => $data['care_context_reference'],
'record_id' => 999,
];
});
$refGateway = new ReflectionClass($this->gateway);
$prop = $refGateway->getProperty('connector');
$prop->setAccessible(true);
$prop->setValue($this->gateway, $mockConnector);
$res = $this->gateway->shareDiagnosisReportBundle();
$body = json_decode($res->getBody(), true);
$this->assertSame(1, $body['ok'] ?? 0);
$this->assertSame('RAD-31-20260929', $body['care_context_reference'] ?? '');
}
}