devsofttech_hms_ci4/tests/unit/Abdm/CareContextDiscoveryTest.php

700 lines
25 KiB
PHP

<?php
namespace Tests\Unit\Abdm;
use App\Controllers\AbdmGateway;
use CodeIgniter\Test\CIUnitTestCase;
use ReflectionClass;
final class CareContextDiscoveryTest extends CIUnitTestCase
{
private AbdmGateway $gateway;
protected function setUp(): void
{
parent::setUp();
$db = \Config\Database::connect();
$forge = \Config\Database::forge();
$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,
Report_Data TEXT,
report_data_Impression TEXT,
status INTEGER,
Request_Date TEXT,
reported_time TEXT,
collected_time TEXT
)");
$db->query("CREATE TABLE IF NOT EXISTS " . $db->prefixTable('charge_master') . " (
id INTEGER PRIMARY KEY,
charge_name TEXT,
charge_type TEXT
)");
$db->query("CREATE TABLE IF NOT EXISTS " . $db->prefixTable('opd_prescription') . " (
id INTEGER PRIMARY KEY,
p_id INTEGER,
opd_id INTEGER,
date_opd_visit TEXT,
session_id INTEGER,
p_datetime TEXT,
bp TEXT,
pulse TEXT,
temp TEXT,
spo2 TEXT,
weight TEXT,
height TEXT,
complaints TEXT,
diagnosis TEXT,
Provisional_diagnosis TEXT,
advice TEXT,
investigation TEXT,
Finding_Examinations TEXT,
diagnosis_json TEXT,
complaint_snomed_json TEXT
)");
$db->query("CREATE TABLE IF NOT EXISTS " . $db->prefixTable('opd_prescrption_prescribed') . " (
id INTEGER PRIMARY KEY,
prescription_id INTEGER,
medicine_name TEXT,
dosage_summary TEXT
)");
$db->query("CREATE TABLE IF NOT EXISTS " . $db->prefixTable('opd_master') . " (
opd_id INTEGER PRIMARY KEY,
p_id INTEGER,
apointment_date TEXT,
opd_book_date TEXT,
doc_name TEXT,
opd_status INTEGER
)");
$db->query("CREATE TABLE IF NOT EXISTS " . $db->prefixTable('health_records') . " (
id INTEGER PRIMARY KEY,
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
)");
$db->query("CREATE TABLE IF NOT EXISTS " . $db->prefixTable('patient_master') . " (
id INTEGER PRIMARY KEY,
p_code TEXT,
p_fname TEXT,
dob TEXT,
age TEXT,
gender INTEGER,
abha_id TEXT,
abha_address TEXT,
mphone1 TEXT
)");
$db->query("CREATE TABLE IF NOT EXISTS " . $db->prefixTable('ipd_master') . " (
id INTEGER PRIMARY KEY,
p_id INTEGER,
discharge_date TEXT,
register_date TEXT,
ipd_status INTEGER
)");
$db->query("CREATE TABLE IF NOT EXISTS " . $db->prefixTable('immunization_records') . " (
id INTEGER PRIMARY KEY,
patient_id INTEGER,
vaccine_name TEXT,
given_date TEXT,
abdm_care_context_reference TEXT
)");
$db->query("CREATE TABLE IF NOT EXISTS " . $db->prefixTable('invoice_master') . " (
id INTEGER PRIMARY KEY,
invoice_code TEXT,
attach_id INTEGER,
attach_type INTEGER,
inv_date TEXT,
net_amount REAL
)");
$db->query("CREATE TABLE IF NOT EXISTS " . $db->prefixTable('record_links') . " (
id INTEGER PRIMARY KEY AUTOINCREMENT,
health_record_id INTEGER,
abdm_txn_id TEXT,
care_context_reference TEXT,
abha_id TEXT,
link_status TEXT,
response_json TEXT,
linked_at TEXT,
created_at TEXT,
updated_at TEXT
)");
$db->resetDataCache();
$db->table('charge_master')->emptyTable();
$db->table('lab_request')->emptyTable();
$db->table('opd_prescription')->emptyTable();
$db->table('opd_master')->emptyTable();
$db->table('health_records')->emptyTable();
$db->table('patient_master')->emptyTable();
$db->table('ipd_master')->emptyTable();
$db->table('record_links')->emptyTable();
$db->table('immunization_records')->emptyTable();
$db->table('invoice_master')->emptyTable();
$db->table('patient_master')->insert([
'id' => 12,
'p_code' => 'P26071000012',
'p_fname' => 'KESHAV SINGH',
'dob' => '1976-06-30',
'age' => null,
'gender' => 1,
'abha_id' => '91747451787143',
'abha_address' => 'singhkeshav301976@sbx',
'mphone1' => '7817828379',
]);
$db->table('charge_master')->insert([
'id' => 19,
'charge_name' => 'USG WHOLE ABDOMEN',
]);
$db->table('lab_request')->insert([
'id' => 31,
'patient_id' => 12,
'lab_type' => 6,
'charge_id' => 19,
'report_name' => 'USG WHOLE ABDOMEN',
'Report_Data' => 'Liver is normal. Gall bladder normal.',
'report_data_Impression' => 'Normal study.',
'status' => 1,
'Request_Date' => '2026-09-29',
'reported_time' => '2026-09-29 11:00:00',
]);
$db->table('opd_prescription')->insert([
'id' => 101,
'p_id' => 12,
'date_opd_visit' => '2026-09-28',
'session_id' => 50,
'p_datetime' => '2026-09-28 10:00:00',
]);
$this->gateway = new AbdmGateway();
$this->gateway->initController(
\Config\Services::request(),
\Config\Services::response(),
\Config\Services::logger()
);
}
public function testCareContextsDiscoveryWithTargetTask(): void
{
$reflector = new ReflectionClass($this->gateway);
$method = $reflector->getMethod('findCareContextsForPatient');
$method->setAccessible(true);
// Test with patient 12 and task_type='radiology_report_publish', entity_id='31'
[$v3List, $fullList] = $method->invoke(
$this->gateway,
12,
'P-12',
'KESHAV SINGH',
'radiology_report_publish',
'31',
591
);
$this->assertNotEmpty($fullList, 'Care contexts list should not be empty for patient 12');
// Verify the primary context is the radiology report #31
$primaryCtx = $fullList[0];
$this->assertTrue($primaryCtx['is_primary'] ?? false, 'First care context should be marked as primary');
$this->assertSame('DiagnosticReportRecord', $primaryCtx['record_type']);
$this->assertStringStartsWith('RAD-31-', $primaryCtx['careContextId']);
$this->assertTrue($primaryCtx['is_fhir_ready']);
// Verify that v3List also contains referenceNumber
$this->assertSame($primaryCtx['careContextId'], $v3List[0]['referenceNumber']);
// Verify secondary contexts (e.g. OPD prescription) are also discovered
$hasOpd = false;
foreach ($fullList as $ctx) {
if ($ctx['record_type'] === 'OPConsultRecord') {
$hasOpd = true;
$this->assertFalse($ctx['is_primary']);
}
}
$this->assertTrue($hasOpd, 'Patient OPD consultations should also be included in care contexts');
}
public function testCareContextsDiscoveryDraftStatus(): void
{
$db = \Config\Database::connect();
// Insert incomplete lab request (status = 0 and empty findings)
$db->table('lab_request')->insert([
'id' => 99,
'patient_id' => 12,
'lab_type' => 1,
'charge_id' => 0,
'report_name' => 'Draft Blood Test',
'Report_Data' => '',
'report_data_Impression' => '',
'status' => 0,
'Request_Date' => '2026-09-29',
]);
$reflector = new ReflectionClass($this->gateway);
$method = $reflector->getMethod('findCareContextsForPatient');
$method->setAccessible(true);
[$v3List, $fullList] = $method->invoke(
$this->gateway,
12,
'P-12',
'KESHAV SINGH'
);
$draftCtx = null;
foreach ($fullList as $ctx) {
if (str_starts_with($ctx['careContextId'], 'LAB-99-')) {
$draftCtx = $ctx;
break;
}
}
$this->assertNotNull($draftCtx);
$this->assertFalse($draftCtx['is_fhir_ready'], 'Incomplete lab report should not be marked FHIR ready');
}
public function testHipPatientCareContextsEndpoint(): void
{
$_SERVER['HTTP_X_REQUESTED_WITH'] = 'xmlhttprequest';
$_REQUEST['patient_id'] = '12';
$_REQUEST['abha_address'] = '91747451787143';
$_REQUEST['task_type'] = 'radiology_report_publish';
$_REQUEST['entity_id'] = '31';
$_REQUEST['task_id'] = '591';
$req = service('request');
$req->setHeader('X-Requested-With', 'XMLHttpRequest');
$this->gateway->initController($req, service('response'), service('logger'));
$res = $this->gateway->hipPatientCareContexts();
$body = json_decode($res->getBody(), true);
$this->assertSame(1, $body['ok'] ?? 0);
$this->assertNotEmpty($body['care_contexts'] ?? []);
}
public function testCareContextsDiscoveryWithIpdAndImmunization(): void
{
$db = \Config\Database::connect();
$db->table('ipd_master')->insert([
'id' => 55,
'p_id' => 12,
'discharge_date' => '2026-09-20',
'register_date' => '2026-09-15',
'ipd_status' => 1,
]);
$db->table('immunization_records')->insert([
'id' => 77,
'patient_id' => 12,
'vaccine_name' => 'Covaxin',
'given_date' => '2026-09-18',
'abdm_care_context_reference' => 'IMM-77-20260918',
]);
$reflector = new ReflectionClass($this->gateway);
$method = $reflector->getMethod('findCareContextsForPatient');
$method->setAccessible(true);
[$v3List, $fullList] = $method->invoke(
$this->gateway,
12,
'P-12',
'KESHAV SINGH'
);
$foundIpd = false;
$foundImm = false;
foreach ($fullList as $ctx) {
if ($ctx['record_type'] === 'DischargeSummaryRecord') {
$foundIpd = true;
$this->assertStringContainsString('DISCHARGE-55-', $ctx['careContextId']);
}
if ($ctx['record_type'] === 'ImmunizationRecord') {
$foundImm = true;
$this->assertSame('IMM-77-20260918', $ctx['careContextId']);
}
}
$this->assertTrue($foundIpd, 'IPD DischargeSummaryRecord should be discovered');
$this->assertTrue($foundImm, 'ImmunizationRecord should be discovered');
}
public function testImmunizationTaskReturnsPrimaryCareContextInDiscovery(): void
{
$db = \Config\Database::connect();
$db->table('immunization_records')->insert([
'id' => 11,
'patient_id' => 11,
'vaccine_name' => 'PCV',
'given_date' => '2026-10-06 01:54:00',
'abdm_care_context_reference' => 'IMM-11',
]);
$reflector = new ReflectionClass($this->gateway);
$method = $reflector->getMethod('findCareContextsForPatient');
$method->setAccessible(true);
[$v3List, $fullList] = $method->invoke(
$this->gateway,
11,
'P-11',
'DEVENDER SINGH',
'immunization_record_publish',
'11'
);
$this->assertNotEmpty($fullList);
$primary = $fullList[0];
$this->assertSame('IMM-11', $primary['careContextId']);
$this->assertSame('ImmunizationRecord', $primary['record_type']);
$this->assertTrue($primary['is_primary']);
$this->assertTrue($primary['is_fhir_ready']);
$this->assertStringContainsString('PCV', $primary['display']);
}
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'] ?? '');
}
public function testDiscoversOpdMasterVisitWhenPrescriptionNotCreated(): void
{
$db = \Config\Database::connect();
$db->table('opd_master')->insert([
'opd_id' => 77,
'p_id' => 12,
'apointment_date' => '2026-09-29 14:30:00',
'opd_book_date' => '2026-09-29 14:00:00',
'doc_name' => 'NIDHI PANDEY',
]);
$refGateway = new ReflectionClass($this->gateway);
$method = $refGateway->getMethod('findCareContextsForPatient');
$method->setAccessible(true);
[$v3, $full] = $method->invoke($this->gateway, 12, 'P26071000012', 'KESHAV SINGH');
$refs = array_column($v3, 'referenceNumber');
$this->assertContains('OPD-12-S77-20260929', $refs);
}
public function testDiscoversInvoiceCareContextWhenInvoiceTaskRequested(): void
{
$db = \Config\Database::connect();
$db->table('invoice_master')->emptyTable();
$db->table('invoice_master')->insert([
'id' => 88,
'invoice_code' => 'INV-88',
'attach_id' => 12,
'attach_type' => 0,
'inv_date' => '2026-09-30',
'net_amount' => 450.00,
]);
$refGateway = new ReflectionClass($this->gateway);
$method = $refGateway->getMethod('findCareContextsForPatient');
$method->setAccessible(true);
[$v3, $full] = $method->invoke(
$this->gateway,
12,
'P26071000012',
'KESHAV SINGH',
'invoice_publish',
'88',
0,
'charges_invoice'
);
$refs = array_column($v3, 'referenceNumber');
$this->assertContains('INVOICE-CHG-88-2026-09-30', $refs);
$invoiceCtx = null;
foreach ($full as $ctx) {
if ($ctx['careContextId'] === 'INVOICE-CHG-88-2026-09-30') {
$invoiceCtx = $ctx;
break;
}
}
$this->assertNotNull($invoiceCtx);
$this->assertSame('InvoiceRecord', $invoiceCtx['record_type']);
$this->assertTrue($invoiceCtx['is_primary']);
$this->assertTrue($invoiceCtx['is_fhir_ready']);
}
public function testDiscoversPrescriptionAndWellnessCareContextsWhenPresent(): void
{
$db = \Config\Database::connect();
$db->table('opd_prescription')->insert([
'id' => 901,
'p_id' => 15350,
'opd_id' => 15350,
'session_id' => 901,
'date_opd_visit' => '2026-10-06',
'bp' => '120/80',
'pulse' => '72',
'temp' => '98.4',
'spo2' => '98',
'weight' => '70',
'height' => '175',
]);
$db->table('opd_prescrption_prescribed')->insert([
'id' => 101,
'prescription_id' => 901,
'medicine_name' => 'CAP PANTOP DSR',
'dosage_summary' => '1 Tab | OD | 5 days',
]);
$refGateway = new ReflectionClass($this->gateway);
$method = $refGateway->getMethod('findCareContextsForPatient');
$method->setAccessible(true);
[$v3, $full] = $method->invoke($this->gateway, 15350, 'P-15350', 'DEVENDER SINGH');
$refs = array_column($v3, 'referenceNumber');
$this->assertContains('OPD-15350-S901-20261006', $refs);
$this->assertContains('PRESC-15350-S901-20261006', $refs);
$this->assertContains('WELLNESS-901-20261006', $refs);
$types = array_column($full, 'record_type');
$this->assertContains('OPConsultRecord', $types);
$this->assertContains('PrescriptionRecord', $types);
$this->assertContains('WellnessRecord', $types);
}
public function testDiscoversWellnessTaskBoardCareContext(): void
{
$db = \Config\Database::connect();
$db->table('opd_prescription')->insert([
'id' => 33723,
'p_id' => 15350,
'opd_id' => 15350,
'session_id' => 33723,
'date_opd_visit' => '2026-10-06',
'bp' => '130/85',
'pulse' => '75',
]);
$refGateway = new ReflectionClass($this->gateway);
$method = $refGateway->getMethod('findCareContextsForPatient');
$method->setAccessible(true);
[$v3, $full] = $method->invoke(
$this->gateway,
15350,
'P-15350',
'DEVENDER SINGH',
'wellness_record_publish',
'33723'
);
$refs = array_column($v3, 'referenceNumber');
$this->assertContains('WELLNESS-33723-20261006', $refs);
$primary = $full[0];
$this->assertSame('WELLNESS-33723-20261006', $primary['careContextId']);
$this->assertSame('WellnessRecord', $primary['record_type']);
$this->assertTrue($primary['is_primary']);
}
public function testUnfilledOpdConsultShowsSingleDraftRecordWithDoctorName(): void
{
$db = \Config\Database::connect();
$db->table('opd_master')->insert([
'opd_id' => 33793,
'p_id' => 15353,
'apointment_date' => '2026-10-06 14:00:00',
'opd_book_date' => '2026-10-06 14:00:00',
'doc_name' => 'Jayashri Joshi',
'opd_status' => 1, // Booked / waiting (not done)
]);
$db->table('opd_prescription')->insert([
'id' => 33727,
'opd_id' => 33793,
'p_id' => 15353,
'session_id' => 0,
'date_opd_visit' => '2026-10-06',
'complaints' => '',
'diagnosis' => '',
]);
$refGateway = new ReflectionClass($this->gateway);
$method = $refGateway->getMethod('findCareContextsForPatient');
$method->setAccessible(true);
[$v3, $full] = $method->invoke($this->gateway, 15353, 'P26081015353', 'DHAIRYA SINGH BISHT');
// Verify only ONE OPD consult record is returned (no duplicate!)
$opdConsults = array_values(array_filter($full, fn ($c) => ($c['record_type'] ?? '') === 'OPConsultRecord'));
$this->assertCount(1, $opdConsults, 'Should return exactly 1 consolidated OPD consultation record, not duplicates');
$consult = $opdConsults[0];
$this->assertStringContainsString('Dr. Jayashri Joshi', $consult['display'], 'Should include the doctor name');
$this->assertFalse($consult['is_fhir_ready'], 'Unfilled booking should NOT be marked FHIR ready');
}
public function testCompletedOpdConsultShowsFhirReady(): void
{
$db = \Config\Database::connect();
$db->table('opd_master')->insert([
'opd_id' => 33794,
'p_id' => 15353,
'apointment_date' => '2026-10-06 15:00:00',
'opd_book_date' => '2026-10-06 15:00:00',
'doc_name' => 'Jayashri Joshi',
'opd_status' => 2, // Visit Done
]);
$db->table('opd_prescription')->insert([
'id' => 33728,
'opd_id' => 33794,
'p_id' => 15353,
'session_id' => 0,
'date_opd_visit' => '2026-10-06',
'complaints' => 'Fever and Cough',
'diagnosis' => 'Viral Pharyngitis',
]);
$refGateway = new ReflectionClass($this->gateway);
$method = $refGateway->getMethod('findCareContextsForPatient');
$method->setAccessible(true);
[$v3, $full] = $method->invoke($this->gateway, 15353, 'P26081015353', 'DHAIRYA SINGH BISHT');
$opdConsults = array_values(array_filter($full, fn ($c) => ($c['record_type'] ?? '') === 'OPConsultRecord' && str_contains($c['careContextId'], '33728')));
$this->assertCount(1, $opdConsults);
$this->assertTrue($opdConsults[0]['is_fhir_ready'], 'Completed consultation with findings must be marked FHIR ready');
}
public function testCareContextsPartitioningByAbhaAddress(): void
{
$db = \Config\Database::connect();
// Setup patient 999
$db->table('patient_master')->insert([
'id' => 999,
'p_code' => 'P-999',
'p_fname' => 'PARTITION TEST',
'dob' => '1985-01-01',
'gender' => 1,
'abha_id' => '91999999999999',
'abha_address' => 'newuser@sbx',
'mphone1' => '9999999999',
]);
// Insert 2 prescriptions for patient 999
$db->table('opd_prescription')->insert([
'id' => 701,
'opd_id' => 701,
'p_id' => 999,
'session_id' => 701,
'date_opd_visit' => '2026-08-01',
'complaints' => 'Old visit complaints',
]);
$db->table('opd_prescription')->insert([
'id' => 702,
'opd_id' => 702,
'p_id' => 999,
'session_id' => 702,
'date_opd_visit' => '2026-10-22',
'complaints' => 'New visit complaints',
]);
// Link old prescription #701 to 'olduser@sbx'
$oldRef = 'OPD-999-S701-20260801';
$db->table('record_links')->insert([
'care_context_reference' => $oldRef,
'abha_id' => 'olduser@sbx',
'link_status' => 'linked',
'created_at' => '2026-08-01 10:00:00',
]);
// Link new prescription #702 to 'newuser@sbx'
$newRef = 'OPD-999-S702-20261022';
$db->table('record_links')->insert([
'care_context_reference' => $newRef,
'abha_id' => 'newuser@sbx',
'link_status' => 'linked',
'created_at' => '2026-10-22 10:00:00',
]);
$refGateway = new ReflectionClass($this->gateway);
$method = $refGateway->getMethod('findCareContextsForPatient');
$method->setAccessible(true);
// Discovery for 'newuser@sbx': MUST contain $newRef and MUST NOT contain $oldRef
[$v3New, $fullNew] = $method->invoke($this->gateway, 999, 'P-999', 'PARTITION TEST', '', '', 0, '', false, 'newuser@sbx');
$newRefs = array_column($v3New, 'referenceNumber');
$this->assertContains($newRef, $newRefs, 'New ABHA address should discover its own linked record');
$this->assertNotContains($oldRef, $newRefs, 'New ABHA address MUST NOT discover records linked to olduser@sbx');
// Discovery for 'olduser@sbx': MUST contain $oldRef and MUST NOT contain $newRef
[$v3Old, $fullOld] = $method->invoke($this->gateway, 999, 'P-999', 'PARTITION TEST', '', '', 0, '', false, 'olduser@sbx');
$oldRefs = array_column($v3Old, 'referenceNumber');
$this->assertContains($oldRef, $oldRefs, 'Old ABHA address should discover its own linked record');
$this->assertNotContains($newRef, $oldRefs, 'Old ABHA address MUST NOT discover records linked to newuser@sbx');
}
}