fix(billing,abdm): resolve patient profile edit error and enhance opd prescription fhir mapping

- Fix Undefined variable \ 500 error in Person_Edit_V.php
- Safeguard auth()->user() and variable access in Person_Edit_V.php and Person_profile_V.php
- Enhance getPrescriptionMedicines with fallback dosing, remarks, and SNOMED lookup for OP Consultation FHIR
- Add deep-link SMS notify and link verification in ABDM gateway and task board
- Add PatientEditViewTest covering profile and edit view rendering
This commit is contained in:
DevSoftTech Server 2026-10-06 08:25:48 +05:30
parent 6270b0517a
commit da61637c56
12 changed files with 546 additions and 31 deletions

View File

@ -6241,6 +6241,70 @@ class AbdmGateway extends BaseController
'is_primary' => true,
]);
}
} elseif ($taskType === 'immunization_record_publish' && $this->db->tableExists('immunization_records')) {
$imm = $this->db->table('immunization_records')
->select('id, patient_id, vaccine_name, given_date, abdm_care_context_reference')
->where('id', $taskEntityId)
->get(1)
->getRowArray();
if (! empty($imm)) {
$admDate = ! empty($imm['given_date']) && strtotime((string) $imm['given_date']) > 0
? date('Y-m-d', strtotime((string) $imm['given_date']))
: date('Y-m-d');
$dateStr = date('d M Y', strtotime($admDate));
$ccRef = ! empty($imm['abdm_care_context_reference'])
? trim((string) $imm['abdm_care_context_reference'])
: ('IMM-' . $taskEntityId);
$display = 'ImmunizationRecord - ' . ($imm['vaccine_name'] ?? 'Vaccine') . ' (' . $dateStr . ')';
$addContext([
'careContextId' => $ccRef,
'referenceNumber' => $ccRef,
'display' => $display,
'record_type' => 'ImmunizationRecord',
'patient_id' => $patientId,
'is_fhir_ready' => true,
'is_primary' => true,
]);
}
} elseif ($taskType === 'health_document_publish') {
if ($this->db->tableExists('patient_doc')) {
$pdoc = $this->db->table('patient_doc')->where('id', $taskEntityId)->get(1)->getRowArray();
if (! empty($pdoc)) {
$docDate = ! empty($pdoc['date_issue']) ? $pdoc['date_issue'] : (! empty($pdoc['created_at']) ? $pdoc['created_at'] : date('Y-m-d'));
$dateStr = date('d M Y', strtotime((string) $docDate));
$ccRef = 'DOC-' . $taskEntityId . '-' . date('Ymd', strtotime((string) $docDate));
$display = 'HealthDocumentRecord - ' . (! empty($pdoc['doc_name']) ? $pdoc['doc_name'] : 'Medical Document') . ' (' . $dateStr . ')';
$addContext([
'careContextId' => $ccRef,
'referenceNumber' => $ccRef,
'display' => $display,
'record_type' => 'HealthDocumentRecord',
'patient_id' => $patientId,
'is_fhir_ready' => true,
'is_primary' => true,
]);
}
}
} elseif ($taskType === 'wellness_record_publish') {
if ($this->db->tableExists('opd_prescription')) {
$wopd = $this->db->table('opd_prescription')->where('id', $taskEntityId)->get(1)->getRowArray();
if (! empty($wopd)) {
$vDate = ! empty($wopd['date_opd_visit']) ? $wopd['date_opd_visit'] : date('Y-m-d');
$dateStr = date('d M Y', strtotime((string) $vDate));
$ccRef = 'WELLNESS-' . $patientId . '-S' . $taskEntityId . '-' . date('Ymd', strtotime((string) $vDate));
$display = 'WellnessRecord - ' . $dateStr;
$addContext([
'careContextId' => $ccRef,
'referenceNumber' => $ccRef,
'display' => $display,
'record_type' => 'WellnessRecord',
'patient_id' => $patientId,
'is_fhir_ready' => true,
'is_primary' => true,
]);
}
}
}
}
@ -6419,20 +6483,33 @@ class AbdmGateway extends BaseController
// 6. Query immunization_records / patient_immunization if exists
if ($this->db->tableExists('immunization_records')) {
$immRows = $this->db->table('immunization_records')
->select('id, patient_id, vaccine_name, given_date, abdm_care_context_reference')
->where('patient_id', $patientId)
->orderBy('id', 'DESC')
->limit(10)
$immFields = $this->db->getFieldNames('immunization_records') ?? [];
$hasStatus = in_array('status', $immFields, true);
$builder = $this->db->table('immunization_records')
->select('id, patient_id, vaccine_name, given_date, abdm_care_context_reference' . ($hasStatus ? ', status' : ''))
->where('patient_id', $patientId);
if ($hasStatus) {
// Prioritize completed records, and don't omit administered ones
$builder->orderBy("CASE WHEN status = 'completed' THEN 1 WHEN given_date IS NOT NULL AND given_date > '1970-01-01' THEN 2 ELSE 3 END", 'ASC', false);
}
$immRows = $builder->orderBy('id', 'DESC')
->limit(30)
->get()
->getResultArray();
foreach ($immRows as $imm) {
$admDate = ! empty($imm['given_date']) ? date('Y-m-d', strtotime((string) $imm['given_date'])) : date('Y-m-d');
// If status is present and 'due' (and not given), skip unadministered vaccines
if ($hasStatus && strtolower((string) ($imm['status'] ?? '')) === 'due' && (empty($imm['given_date']) || $imm['given_date'] === '0000-00-00 00:00:00')) {
continue;
}
$admDate = ! empty($imm['given_date']) && strtotime((string) $imm['given_date']) > 0
? date('Y-m-d', strtotime((string) $imm['given_date']))
: date('Y-m-d');
$dateStr = date('d M Y', strtotime($admDate));
$ccRef = ! empty($imm['abdm_care_context_reference'])
? trim((string) $imm['abdm_care_context_reference'])
: ('IMM-' . $imm['id'] . '-' . str_replace('-', '', $admDate));
: ('IMM-' . $imm['id']);
$display = 'ImmunizationRecord - ' . ($imm['vaccine_name'] ?? 'Vaccine') . ' (' . $dateStr . ')';
$addContext([

View File

@ -147,7 +147,7 @@ class AbdmTaskBoard extends BaseController
unset($task);
}
// 2. Lookup health_records by entity_id
// 2. Lookup health_records by entity_id with strict entity_type / patient_id scoping
$latestHr = [];
if ($db->tableExists('health_records')) {
$entityIds = [];
@ -161,23 +161,93 @@ class AbdmTaskBoard extends BaseController
if ($entityIds !== []) {
$rows = $db->table('health_records')
->select('id, entity_type, entity_id, push_status, care_context_reference, linked_at, abdm_txn_id')
->select('id, patient_id, entity_type, hi_type, entity_id, push_status, care_context_reference, linked_at, abdm_txn_id')
->whereIn('entity_id', $entityIds)
->orderBy('id', 'DESC')
->get()
->getResultArray();
foreach ($rows as $row) {
$pId = (int) ($row['patient_id'] ?? 0);
$eType = strtolower(trim((string) ($row['entity_type'] ?? '')));
$hiType = strtolower(trim((string) ($row['hi_type'] ?? '')));
$eid = (string) ($row['entity_id'] ?? '');
if ($eid !== '' && ! isset($latestHr[$eid])) {
$latestHr[$eid] = $row;
if ($eid !== '') {
if ($pId > 0 && $eType !== '') {
$k1 = $pId . '_' . $eType . '_' . $eid;
if (! isset($latestHr[$k1])) {
$latestHr[$k1] = $row;
}
}
if ($pId > 0 && $hiType !== '') {
$k2 = $pId . '_' . $hiType . '_' . $eid;
if (! isset($latestHr[$k2])) {
$latestHr[$k2] = $row;
}
}
if ($eType !== '') {
$k3 = $eType . '_' . $eid;
if (! isset($latestHr[$k3])) {
$latestHr[$k3] = $row;
}
}
if (! isset($latestHr[$eid])) {
$latestHr[$eid] = $row;
}
}
}
}
}
foreach ($tasks as &$task) {
$pId = (int) ($task['patient_id'] ?? 0);
$taskType = strtolower(trim((string) ($task['task_type'] ?? '')));
$taskEntityType = strtolower(trim((string) ($task['entity_type'] ?? '')));
$eid = (string) ($task['entity_id'] ?? '');
$hr = $latestHr[$eid] ?? null;
// Map task type or entity type to standard health_record entity types / hi_types
$candidateTypes = array_values(array_filter([$taskEntityType]));
if ($taskType === 'immunization_record_publish') {
$candidateTypes = array_values(array_unique(array_merge($candidateTypes, ['immunization', 'immunizationrecord'])));
} elseif ($taskType === 'opd_prescription_publish') {
$candidateTypes = array_values(array_unique(array_merge($candidateTypes, ['opd', 'opd_prescription', 'opconsultrecord'])));
} elseif ($taskType === 'ipd_discharge_publish') {
$candidateTypes = array_values(array_unique(array_merge($candidateTypes, ['ipd', 'ipd_discharge', 'dischargesummaryrecord'])));
} elseif ($taskType === 'lab_report_publish' || $taskType === 'radiology_report_publish') {
$candidateTypes = array_values(array_unique(array_merge($candidateTypes, ['lab', 'radiology', 'diagnosticreportrecord'])));
} elseif ($taskType === 'wellness_record_publish') {
$candidateTypes = array_values(array_unique(array_merge($candidateTypes, ['opd_vitals', 'opd', 'wellness', 'wellnessrecord'])));
} elseif ($taskType === 'health_document_publish') {
$candidateTypes = array_values(array_unique(array_merge($candidateTypes, ['patient_document', 'doctor_document', 'file_upload_data', 'patient_doc', 'healthdocumentrecord'])));
} elseif (in_array($taskType, ['invoice_publish', 'invoice_record_publish'], true)) {
$candidateTypes = array_values(array_unique(array_merge($candidateTypes, ['invoice', 'opd_invoice', 'charges_invoice', 'ipd_invoice', 'invoicerecord'])));
}
$hr = null;
// 1) Match with patient_id and entity/hi type
if ($pId > 0) {
foreach ($candidateTypes as $cType) {
$key = $pId . '_' . strtolower($cType) . '_' . $eid;
if (isset($latestHr[$key])) {
$hr = $latestHr[$key];
break;
}
}
}
// 2) Match with entity/hi type without patient_id
if ($hr === null) {
foreach ($candidateTypes as $cType) {
$key = strtolower($cType) . '_' . $eid;
if (isset($latestHr[$key])) {
$hr = $latestHr[$key];
break;
}
}
}
// 3) Only fallback to raw $eid if task has no specific taskType or candidateTypes
if ($hr === null && empty($candidateTypes) && isset($latestHr[$eid])) {
$hr = $latestHr[$eid];
}
$pushStatus = strtolower(trim((string) ($hr['push_status'] ?? '')));
$task['bridge_health_record_id'] = (int) ($hr['id'] ?? 0);
$task['bridge_push_status'] = $pushStatus;
@ -185,7 +255,7 @@ class AbdmTaskBoard extends BaseController
$careContext = trim((string) ($hr['care_context_reference'] ?? ''));
if ($careContext === '') {
$resultText = (string) ($task['last_action_result'] ?? '');
if (preg_match('/(RAD-\d+-\d+|LAB-\d+-\d+|OPD-\d+-\d+|INVOICE-\w+-\d+-\d+|IMM-\d+-\d+|DIS-\d+-\d+)/i', $resultText, $ccm)) {
if (preg_match('/((?:RAD|LAB|OPD|INVOICE|IMM|DISCHARGE|DIS|WELLNESS|DOC)-[A-Za-z0-9_-]+)/i', $resultText, $ccm)) {
$careContext = $ccm[1];
}
}

View File

@ -11887,6 +11887,7 @@ class Opd_prescription extends BaseController
$masterTable = $this->findExistingTable(['opd_med_master']);
$masterById = [];
$masterByName = [];
if ($masterTable !== null) {
$masterFields = $this->db->getFieldNames($masterTable) ?? [];
$masterNameField = $this->resolveFirstField($masterFields, ['item_name', 'med_name']);
@ -11896,14 +11897,19 @@ class Opd_prescription extends BaseController
$masterAtcField = $this->resolveFirstField($masterFields, ['atc_code', 'who_atc_code']);
$medIds = [];
$medNames = [];
foreach ($rows as $row) {
$mid = (int) ($row['med_id'] ?? 0);
if ($mid > 0) {
$medIds[$mid] = $mid;
}
$rawName = strtoupper(trim((string) ($row['med_name'] ?? '')));
if ($rawName !== '') {
$medNames[$rawName] = $rawName;
}
}
if (! empty($medIds)) {
if (! empty($medIds) || ! empty($medNames)) {
$select = ['id'];
if ($masterNameField !== null) {
$select[] = $masterNameField . ' as item_name';
@ -11921,17 +11927,54 @@ class Opd_prescription extends BaseController
$select[] = $masterAtcField . ' as atc_code';
}
$masterRows = $this->db->table($masterTable)
->select(implode(',', $select))
->whereIn('id', array_values($medIds))
->get()
->getResultArray();
$query = $this->db->table($masterTable)->select(implode(',', $select));
if (! empty($medIds) && ! empty($medNames) && $masterNameField !== null) {
$query->groupStart()
->whereIn('id', array_values($medIds))
->orWhereIn($masterNameField, array_values($medNames))
->groupEnd();
} elseif (! empty($medIds)) {
$query->whereIn('id', array_values($medIds));
} elseif (! empty($medNames) && $masterNameField !== null) {
$query->whereIn($masterNameField, array_values($medNames));
}
$masterRows = $query->get()->getResultArray();
foreach ($masterRows as $mrow) {
$mid = (int) ($mrow['id'] ?? 0);
if ($mid > 0) {
$masterById[$mid] = $mrow;
}
$iname = strtoupper(trim((string) ($mrow['item_name'] ?? '')));
if ($iname !== '') {
$masterByName[$iname] = $mrow;
}
}
}
}
// Secondary lookup in mst_items for SNOMED CT and generic name
$mstItemsByName = [];
if ($this->db->tableExists('mst_items')) {
$mstNames = [];
foreach ($rows as $row) {
$rawName = strtoupper(trim((string) ($row['med_name'] ?? '')));
if ($rawName !== '') {
$mstNames[$rawName] = $rawName;
}
}
if (! empty($mstNames)) {
$mstRows = $this->db->table('mst_items')
->select('item_name, generic_name, snomed_ct_code, snomed_display')
->whereIn('item_name', array_values($mstNames))
->get()
->getResultArray();
foreach ($mstRows as $item) {
$k = strtoupper(trim((string) ($item['item_name'] ?? '')));
if ($k !== '') {
$mstItemsByName[$k] = $item;
}
}
}
}
@ -11944,11 +11987,29 @@ class Opd_prescription extends BaseController
$medications = [];
foreach ($rows as $row) {
$dose = $this->resolveDoseMasterLabel($row['dosage'] ?? '', $doseMap);
if ($dose === '' && ! empty($row['dosage_str'])) {
$dose = trim((string) $row['dosage_str']);
}
$when = $this->resolveDoseMasterLabel($row['dosage_when'] ?? '', $whenMap);
if ($when === '' && ! empty($row['dosage_when_str'])) {
$when = trim((string) $row['dosage_when_str']);
}
$frequency = $this->resolveDoseMasterLabel($row['dosage_freq'] ?? '', $frequencyMap);
if ($frequency === '' && ! empty($row['dosage_freq_str'])) {
$frequency = trim((string) $row['dosage_freq_str']);
}
$route = $this->resolveDoseMasterLabel($row['dosage_where'] ?? '', $routeMap);
if ($route === '' && ! empty($row['dosage_where_str'])) {
$route = trim((string) $row['dosage_where_str']);
}
$quantity = trim((string) ($row['qty'] ?? ''));
$duration = trim((string) ($row['no_of_days'] ?? ''));
$remark = trim((string) ($row['remark'] ?? ''));
if (stripos($when, 'week') !== false && stripos($frequency, 'week') !== false) {
$frequency = '';
}
@ -11967,21 +12028,35 @@ class Opd_prescription extends BaseController
$frequency,
$duration,
$quantity !== '' && $quantity !== '0' ? ('Qty: ' . $quantity) : '',
$remark !== '' ? ('Instructions: ' . $remark) : '',
]);
$mid = (int) ($row['med_id'] ?? 0);
$rawDrugName = trim((string) ($row['med_name'] ?? ''));
$master = $mid > 0 ? ($masterById[$mid] ?? []) : [];
$drugName = trim((string) ($row['med_name'] ?? ''));
if ($drugName === '') {
$drugName = trim((string) ($master['item_name'] ?? ''));
if (empty($master) && $rawDrugName !== '') {
$master = $masterByName[strtoupper($rawDrugName)] ?? [];
}
$drugName = $rawDrugName !== '' ? $rawDrugName : trim((string) ($master['item_name'] ?? ''));
$genericName = trim((string) ($master['genericname'] ?? ''));
$medType = trim((string) ($row['med_type'] ?? ''));
if ($medType === '') {
$medType = trim((string) ($master['formulation'] ?? ''));
}
$snomedCode = trim((string) ($master['snomed_code'] ?? ''));
$atcCode = strtoupper(trim((string) ($master['atc_code'] ?? '')));
// Fallback from mst_items if snomed_code is not in opd_med_master
if ($snomedCode === '' && ! empty($mstItemsByName[strtoupper($drugName)])) {
$mst = $mstItemsByName[strtoupper($drugName)];
$snomedCode = trim((string) ($mst['snomed_ct_code'] ?? ''));
if ($genericName === '' && ! empty($mst['generic_name'])) {
$genericName = trim((string) $mst['generic_name']);
}
}
$medications[] = [
'drug_name' => $drugName,
'status' => 'active',
@ -11991,8 +12066,8 @@ class Opd_prescription extends BaseController
'med_id' => $mid,
'generic_name' => $genericName,
'med_type' => $medType,
'snomed_code' => trim((string) ($master['snomed_code'] ?? '')),
'atc_code' => strtoupper(trim((string) ($master['atc_code'] ?? ''))),
'snomed_code' => $snomedCode,
'atc_code' => $atcCode,
];
}

View File

@ -1113,6 +1113,7 @@ class Patient extends BaseController
}
$data['profile_file_path'] = $profile_file_path;
$data['abhaPhotoAvailable'] = $abha_profile_photo_base64 !== '';
if ($edit == 0) {
return view('billing/Person_profile_V', $data);

View File

@ -770,6 +770,12 @@ class AbdmTaskBoardSyncService
};
$cleanVisitDate = str_replace('-', '', $visitDate);
$careContextRef = $prefix . $entityId . '-' . $cleanVisitDate;
if ($taskType === 'immunization_record_publish' && $this->db->tableExists('immunization_records')) {
$immRec = $this->db->table('immunization_records')->select('abdm_care_context_reference')->where('id', (int) $entityId)->get(1)->getRowArray();
if (! empty($immRec['abdm_care_context_reference'])) {
$careContextRef = trim((string) $immRec['abdm_care_context_reference']);
}
}
$careContextDisplay = $hiType . ' ' . $visitDate;
$pushData = [

View File

@ -3685,6 +3685,8 @@
task_type: taskType,
entity_id: entityId,
onLinked: function (res) {
row.setAttribute('data-task-status', 'completed');
row.setAttribute('data-is-linked', '1');
var statusCell = row.children[5];
if (statusCell) {
var refHtml = '';
@ -3693,6 +3695,10 @@
}
statusCell.innerHTML = '<span class="badge bg-success status-pill">LINKED</span>' + refHtml;
}
var actionCell = row.children[6];
if (actionCell) {
actionCell.innerHTML = '<span class="badge bg-success-subtle text-success border border-success-subtle py-1 px-2"><i class="bi bi-check2-all me-1"></i>Done</span>';
}
}
});
}

View File

@ -9,7 +9,11 @@
</nav>
</div>
<?php
$user = auth()->user();
try {
$user = function_exists('auth') ? auth()->user() : null;
} catch (\Throwable $e) {
$user = null;
}
$patientAbhaId = (string) ($data[0]->abha_id ?? $data[0]->abha_no ?? $data[0]->abha ?? $data[0]->abha_address ?? '');
$abhaAddress = trim((string) ($data[0]->abha_address ?? ''));
$abhaVerifiedStatus = trim((string) ($data[0]->abha_verified_status ?? ''));
@ -17,6 +21,7 @@
$abhaKycVerified = (int) ($data[0]->abha_kyc_verified ?? 0) === 1;
$abhaMobileVerified = (int) ($data[0]->abha_mobile_verified ?? 0) === 1;
$abhaLinkedAt = trim((string) ($data[0]->abdm_linked_at ?? ''));
$abhaPhotoAvailable = isset($abhaPhotoAvailable) ? (bool) $abhaPhotoAvailable : (trim((string) ($data[0]->abha_profile_photo_base64 ?? '')) !== '');
$isAbhaVerified = in_array(strtoupper($abhaVerifiedStatus), ['VERIFIED', 'ACTIVE', 'LINKED', '1', 'YES', 'Y', 'TRUE'], true)
|| strtoupper($abhaVerificationType) === 'VERIFIED'
|| ($abhaKycVerified && $abhaMobileVerified);
@ -131,7 +136,7 @@
<div class="row" style="margin-top: 6px;">
<div class="col-md-3"><strong>KYC Verified:</strong> <?= $abhaKycVerified ? 'YES' : 'NO' ?></div>
<div class="col-md-3"><strong>Mobile Verified:</strong> <?= $abhaMobileVerified ? 'YES' : 'NO' ?></div>
<div class="col-md-6"><strong>ABHA Photo:</strong> <?= $abhaPhotoAvailable ? 'AVAILABLE (BASE64 STORED)' : 'NOT AVAILABLE' ?></div>
<div class="col-md-6"><strong>ABHA Photo:</strong> <?= !empty($abhaPhotoAvailable) ? 'AVAILABLE (BASE64 STORED)' : 'NOT AVAILABLE' ?></div>
</div>
<?php if ($abhaLockNotice !== '') : ?>
<div class="row" style="margin-top: 6px;">

View File

@ -56,7 +56,7 @@
$abhaKycVerified = (int) ($data[0]->abha_kyc_verified ?? 0) === 1;
$abhaMobileVerified = (int) ($data[0]->abha_mobile_verified ?? 0) === 1;
$abhaLinkedAt = trim((string) ($data[0]->abdm_linked_at ?? ''));
$abhaPhotoAvailable = trim((string) ($data[0]->abha_profile_photo_base64 ?? '')) !== '';
$abhaPhotoAvailable = isset($abhaPhotoAvailable) ? (bool) $abhaPhotoAvailable : (trim((string) ($data[0]->abha_profile_photo_base64 ?? '')) !== '');
$hasExistingAbha = ($patientAbhaId !== '' || $abhaAddress !== '' || $abhaLinkedAt !== '');
$isAbhaStatusVerified = in_array(strtoupper($abhaVerifiedStatus), ['VERIFIED', 'LINKED', 'ACTIVE', '1', 'YES', 'Y', 'TRUE'], true)
|| strtoupper($abhaVerificationType) === 'VERIFIED'
@ -64,7 +64,11 @@
$isAbhaLinkedAndVerified = $hasExistingAbha && ($isAbhaStatusVerified || $abhaLinkedAt !== '');
?>
<?php
$user = auth()->user();
try {
$user = function_exists('auth') ? auth()->user() : null;
} catch (\Throwable $e) {
$user = null;
}
$canEditOpd = is_object($user) && method_exists($user, 'can') ? $user->can('billing.opd.edit') : false;
$canChargeEdit = is_object($user) && method_exists($user, 'can') ? $user->can('billing.charges.edit') : false;
?>
@ -119,11 +123,11 @@
<div class="d-flex justify-content-between align-items-center border rounded px-3 py-2">
<div>
<div class="small text-muted">OPD Visits</div>
<div class="fw-bold"><?= count($opd_List) ?></div>
<div class="fw-bold"><?= count($opd_List ?? []) ?></div>
</div>
<div>
<div class="small text-muted">Invoices</div>
<div class="fw-bold"><?= count($invoice_list) ?></div>
<div class="fw-bold"><?= count($invoice_list ?? []) ?></div>
</div>
<div>
<div class="small text-muted">Insurance</div>
@ -178,7 +182,7 @@
</div>
<div class="row">
<div class="col-lg-3 col-md-4 label">Gender / Age</div>
<div class="col-lg-9 col-md-8"><?=$data[0]->xgender?> / <?= esc(get_age_1($data[0]->dob ?? null, $data[0]->age ?? '', $data[0]->age_in_month ?? '', $data[0]->estimate_dob ?? '')) ?></div>
<div class="col-lg-9 col-md-8"><?= esc($data[0]->xgender ?? ((int) ($data[0]->gender ?? 0) === 1 ? 'Male' : 'Female')) ?> / <?= esc(get_age_1($data[0]->dob ?? null, $data[0]->age ?? '', $data[0]->age_in_month ?? '', $data[0]->estimate_dob ?? '')) ?></div>
</div>
<div class="row">
<div class="col-lg-3 col-md-4 label">Relation</div>

View File

@ -678,6 +678,75 @@
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> Invoice 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 === 'immunization_record_publish') {
var pushData = new URLSearchParams();
pushData.append('record_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_immunization_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> Immunization 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 === 'ipd_discharge_publish') {
var pushData = new URLSearchParams();
pushData.append('ipd_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_ipd_discharge_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> Discharge summary 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 === 'wellness_record_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_wellness_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> Wellness 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() {});
}
}

View File

@ -73,6 +73,52 @@ final class AbdmTaskBoardBackfillTest extends CIUnitTestCase
$this->assertSame('RAD-51450-20261001', $enriched[1]['bridge_care_context_reference']);
}
public function testEnrichTasksWithHealthRecordStatePreventsCrossModuleCollision(): void
{
$db = \Config\Database::connect();
if ($db->tableExists('health_records')) {
// Insert an IPD discharge health record with entity_id = 999 for patient 18
$db->table('health_records')->insert([
'id' => 9999,
'patient_id' => 18,
'entity_type' => 'ipd',
'hi_type' => 'DischargeSummaryRecord',
'entity_id' => '999',
'push_status' => 'queued',
'care_context_reference' => 'DISCHARGE-999-S999-2026',
]);
$controller = (new ReflectionClass(AbdmTaskBoard::class))->newInstanceWithoutConstructor();
$method = new ReflectionMethod(AbdmTaskBoard::class, 'enrichTasksWithHealthRecordState');
$method->setAccessible(true);
// Task is for patient 11 with entity_type = immunization and entity_id = 999
$mockTasks = [
[
'id' => 101,
'task_code' => 'ABDM-TEST-001',
'task_type' => 'immunization_record_publish',
'patient_id' => 11,
'entity_type' => 'immunization',
'entity_id' => '999',
'status' => 'pending',
'last_action_result' => '',
],
];
$enriched = $method->invoke($controller, $mockTasks);
// Should NOT match the IPD record for patient 18
$this->assertSame(0, $enriched[0]['bridge_submitted']);
$this->assertSame('', $enriched[0]['bridge_care_context_reference']);
// Clean up
$db->table('health_records')->where('id', 9999)->delete();
} else {
$this->assertTrue(true);
}
}
public function testWorkTaskServiceHasGetTasksMethod(): void
{
$serviceClass = new ReflectionClass(\App\Libraries\AbdmWorkTaskService::class);

View File

@ -309,6 +309,39 @@ final class CareContextDiscoveryTest extends CIUnitTestCase
$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';

View File

@ -0,0 +1,123 @@
<?php
namespace Tests\Unit;
use CodeIgniter\Test\CIUnitTestCase;
final class PatientEditViewTest extends CIUnitTestCase
{
protected function setUp(): void
{
parent::setUp();
helper(['common', 'form', 'url', 'age']);
}
private function makePatientData(array $overrides = []): array
{
$patient = (object) array_merge([
'id' => 16,
'p_fname' => 'John Doe',
'p_code' => 'P16',
'p_edit' => 1,
'title' => 'Mr.',
'gender' => 1,
'udai_last4' => '1234',
'abha_id' => '',
'abha_no' => '',
'abha' => '',
'abha_address' => '',
'abha_verified_status' => '',
'abha_verification_type' => '',
'abha_kyc_verified' => 0,
'abha_mobile_verified' => 0,
'abdm_linked_at' => '',
'abha_profile_photo_base64' => '',
'mphone1' => '9876543210',
'estimate_dob' => 0,
'age' => 30,
'age_in_month' => 0,
'dob' => '1995-01-01',
'p_relative' => 'S/o',
'p_rname' => 'Father Doe',
'email1' => 'john@example.com',
'blood_group' => 'O+',
'referby' => '',
'add1' => '123 Test St',
'city' => 'City',
'zip' => '123456',
'district' => 'District',
'state' => 'State',
], $overrides);
return [
'data' => [$patient],
'blood_group' => [(object) ['blood_group' => 'O+'], (object) ['blood_group' => 'A+']],
'refer_master' => [],
'tag_master' => [],
'patient_tag_list' => [],
];
}
public function testViewRendersWithoutAbhaPhotoAvailableVariableInViewData(): void
{
$viewData = $this->makePatientData();
// Do not set $viewData['abhaPhotoAvailable'] explicitly to test fallback in Person_Edit_V
$html = view('billing/Person_Edit_V', $viewData);
$this->assertStringContainsString('ABHA Photo:', $html);
$this->assertStringContainsString('NOT AVAILABLE', $html);
$this->assertStringContainsString('Person Profile - John Doe P16', $html);
}
public function testViewRendersWithAbhaPhotoAvailableWhenBase64Present(): void
{
$viewData = $this->makePatientData([
'abha_profile_photo_base64' => 'data:image/jpeg;base64,/9j/4AAQSkZJRg==',
]);
$viewData['abhaPhotoAvailable'] = true;
$html = view('billing/Person_Edit_V', $viewData);
$this->assertStringContainsString('ABHA Photo:', $html);
$this->assertStringContainsString('AVAILABLE (BASE64 STORED)', $html);
}
public function testViewRendersWithAbhaVerifiedLockedFields(): void
{
$viewData = $this->makePatientData([
'abha_id' => '12345678901234',
'abha_verified_status' => 'VERIFIED',
'abha_verification_type' => 'VERIFIED',
'abha_kyc_verified' => 1,
'abha_mobile_verified' => 1,
]);
$viewData['abhaPhotoAvailable'] = false;
$html = view('billing/Person_Edit_V', $viewData);
$this->assertStringContainsString('ABHA verified: name, gender, ABHA ID, and date of birth are locked.', $html);
$this->assertStringContainsString('KYC Verified:</strong> YES', $html);
$this->assertStringContainsString('Mobile Verified:</strong> YES', $html);
}
public function testProfileViewRendersWithAbhaPhoto(): void
{
$viewData = $this->makePatientData([
'insurance_id' => 0,
'insurance_card_id' => 0,
'log' => '',
]);
$viewData['profile_file_path'] = '/assets/images/no_image.svg';
$viewData['case_master_opd'] = [];
$viewData['case_master_ipd'] = [];
$viewData['data_insurance_card'] = [];
$viewData['opd_List'] = [];
$viewData['invoice_list'] = [];
$viewData['abhaPhotoAvailable'] = true;
$html = view('billing/Person_profile_V', $viewData);
$this->assertStringContainsString('Profile', $html);
$this->assertStringContainsString('AVAILABLE (BASE64 STORED)', $html);
}
}