feat(abdm & schema): drop patient_master.p_lname and add abha address selector to M1 verify flow

This commit is contained in:
DevSoftTech Server 2026-10-24 12:56:13 +05:30
parent e946000fb9
commit 5f9784d7ec
15 changed files with 600 additions and 154 deletions

View File

@ -37,3 +37,13 @@ In `app/Controllers/AbdmGateway.php` and the bridge gateway:
- Use valid LOINC codes for composition type and section codes.
- Use SNOMED CT for clinical diagnoses, findings, and conditions.
- Refer to `docs/abdm/CARE_CONTEXT_AND_FHIR_STANDARDS.md` for full implementation specifications.
---
## Rule 4: Patient Name Handling (`patient_master.p_fname`)
- Patient full name is stored strictly in `patient_master.p_fname`.
- The column `patient_master.p_lname` has been permanently dropped and deleted from `patient_master` in all databases.
- NEVER query, select, join, insert, or reference `patient_master.p_lname` in any controllers, models, queries, or FHIR generators.
- When generating FHIR Patient or Composition resources, map the patient's full name directly from `patient_master.p_fname`.
- Note: Doctors in `doctor_master` continue to use `p_fname`, `p_mname`, and `p_lname`.

View File

@ -10,3 +10,9 @@ For all ABDM (Ayushman Bharat Digital Mission) development, linking, Care Contex
The ABDM PHR app displays this field under its "Patient reference" table column. It MUST always be set to the specific care context reference (`PRESC-{patient_id}-S{session_id}-{YYYYMMDD}`, `OPD-{patient_id}-S{session_id}-{YYYYMMDD}`, etc.).
2. **Never overwrite structured references with `p_code` in `AbdmGateway.php`.**
3. **All FHIR bundles must conform to NRCeS FHIR R4 document bundle specifications.**
4. **Patient Full Name Standard (`patient_master.p_fname`):**
- The patient's full name is stored strictly in `patient_master.p_fname`.
- The column `patient_master.p_lname` has been permanently dropped from the database table.
- **DO NOT** use, select, update, or reference `patient_master.p_lname` anywhere in the application.
- Note: For doctors, `doctor_master` continues to use `p_fname`, `p_mname`, and `p_lname`.

View File

@ -345,9 +345,6 @@ class AbdmFhirInspector extends BaseController
}
$select = ['id', 'p_code', 'p_fname', 'gender', 'dob'];
if (in_array('p_lname', $fields, true)) {
$select[] = 'p_lname';
}
if (in_array('mphone1', $fields, true)) {
$select[] = 'mphone1';
}
@ -406,7 +403,7 @@ class AbdmFhirInspector extends BaseController
continue;
}
$name = trim(($row['p_fname'] ?? '') . ' ' . ($row['p_lname'] ?? ''));
$name = trim((string) ($row['p_fname'] ?? ''));
if ($name === '') {
$name = 'Patient #' . $row['id'];
}

View File

@ -14127,6 +14127,7 @@ class AbdmGateway extends BaseController
$result['care_contexts'] = $contexts;
$result['total_linked'] = count($contexts);
$result['links'] = $contexts;
return $this->response->setJSON($result);
}
@ -14304,6 +14305,7 @@ class AbdmGateway extends BaseController
// Fetch already-linked care contexts from Bridge and Local DB (record_links & health_records)
$linkedRefs = [];
$gatewayVerifiedRefs = [];
if ($effAbhaAddress !== '') {
try {
$bridgeLinks = $this->connector->hipGetPatientLinks(['abha_address' => $effAbhaAddress]);
@ -14312,6 +14314,7 @@ class AbdmGateway extends BaseController
foreach ($linkPat['careContexts'] ?? [] as $cc) {
$ref = trim((string) ($cc['referenceNumber'] ?? $cc['ref'] ?? ''));
if ($ref !== '') {
$gatewayVerifiedRefs[] = $ref;
$linkedRefs[] = $ref;
}
}
@ -14382,22 +14385,28 @@ class AbdmGateway extends BaseController
}
$linkedRefs = array_values(array_unique($linkedRefs));
$gatewayVerifiedRefs = array_values(array_unique($gatewayVerifiedRefs));
$contextsList = [];
foreach ($careContextsFull as $cc) {
$ref = $cc['referenceNumber'] ?? $cc['careContextId'] ?? '';
$isGatewayLinked = in_array($ref, $gatewayVerifiedRefs, true);
$isLinked = in_array($ref, $linkedRefs, true);
if (! $isLinked && ! empty($cc['alias_reference'])) {
$isLinked = in_array($cc['alias_reference'], $linkedRefs, true);
if (in_array($cc['alias_reference'], $gatewayVerifiedRefs, true)) {
$isGatewayLinked = true;
}
}
$contextsList[] = [
'ref' => $ref,
'display' => $cc['display'] ?? $ref,
'hi_type' => $cc['record_type'] ?? 'OPConsultRecord',
'is_linked' => $isLinked,
'is_fhir_ready' => (bool) ($cc['is_fhir_ready'] ?? false),
'is_primary' => (bool) ($cc['is_primary'] ?? false),
'entity_id' => (string) ($cc['entity_id'] ?? $cc['opd_id'] ?? ''),
'ref' => $ref,
'display' => $cc['display'] ?? $ref,
'hi_type' => $cc['record_type'] ?? 'OPConsultRecord',
'is_linked' => $isLinked,
'is_gateway_linked' => $isGatewayLinked,
'is_fhir_ready' => (bool) ($cc['is_fhir_ready'] ?? false),
'is_primary' => (bool) ($cc['is_primary'] ?? false),
'entity_id' => (string) ($cc['entity_id'] ?? $cc['opd_id'] ?? ''),
];
}
@ -14422,7 +14431,7 @@ class AbdmGateway extends BaseController
$multiplePatients[] = [
'id' => (int) $or['id'],
'p_code' => (string) ($or['p_code'] ?? ''),
'name' => trim(($or['p_fname'] ?? '') . ' ' . ($or['p_lname'] ?? '')),
'name' => trim((string) ($or['p_fname'] ?? '')),
'gender' => ((int) ($or['gender'] ?? 0) === 2 || strtoupper((string) ($or['gender'] ?? '')) === 'F') ? 'F' : 'M',
'dob' => (string) ($or['dob'] ?? ''),
'is_current' => (int) $or['id'] === $patientId,
@ -14445,10 +14454,11 @@ class AbdmGateway extends BaseController
'abha_number' => $effAbhaNumber,
'phone' => $cleanPhone,
],
'care_contexts' => $contextsList,
'linked_refs' => $linkedRefs,
'hospital_name' => $hospitalName,
'multiple_patients' => $multiplePatients,
'care_contexts' => $contextsList,
'linked_refs' => $linkedRefs,
'gateway_verified_refs' => $gatewayVerifiedRefs,
'hospital_name' => $hospitalName,
'multiple_patients' => $multiplePatients,
]);
}
}

View File

@ -131,9 +131,6 @@ class AbdmTaskBoard extends BaseController
if (! empty($patientIds) && $db->tableExists('patient_master')) {
$pFields = $db->getFieldNames('patient_master') ?? [];
$pSel = ['id', 'p_code', 'p_fname', 'gender', 'dob'];
if (in_array('p_lname', $pFields, true)) {
$pSel[] = 'p_lname';
}
if (in_array('mphone1', $pFields, true)) {
$pSel[] = 'mphone1';
}
@ -1240,9 +1237,6 @@ class AbdmTaskBoard extends BaseController
}
$select = ['id', 'p_code', 'p_fname', 'gender', 'dob'];
if (in_array('p_lname', $fields, true)) {
$select[] = 'p_lname';
}
if (in_array('mphone1', $fields, true)) {
$select[] = 'mphone1';
}
@ -1301,9 +1295,7 @@ class AbdmTaskBoard extends BaseController
continue;
}
$fname = trim((string) ($row['p_fname'] ?? ''));
$lname = trim((string) ($row['p_lname'] ?? ''));
$fullName = trim($fname . ' ' . $lname);
$fullName = trim((string) ($row['p_fname'] ?? ''));
if ($fullName === '') {
$fullName = 'Patient #' . ($row['id'] ?? '');
}
@ -1782,7 +1774,7 @@ class AbdmTaskBoard extends BaseController
$abhaCol = $this->resolveExistingColumn($patientFields, ['abha_address', 'abha_id', 'abha_no', 'abha']);
if ($abhaCol !== null) {
$wellnessRows = $this->db->table('patient_wellness_records w')
->select('w.id, w.patient_id, w.recorded_at, w.care_context_reference, w.bridge_record_id, w.abdm_status, p.p_fname, p.p_lname, p.' . $abhaCol . ' as abha_id', false)
->select('w.id, w.patient_id, w.recorded_at, w.care_context_reference, w.bridge_record_id, w.abdm_status, p.p_fname, p.' . $abhaCol . ' as abha_id', false)
->join('patient_master p', 'p.id = w.patient_id', 'inner')
->where('p.' . $abhaCol . ' !=', '')
->orderBy('w.id', 'DESC')
@ -1809,7 +1801,7 @@ class AbdmTaskBoard extends BaseController
continue;
}
$patientName = trim(($wRow['p_fname'] ?? '') . ' ' . ($wRow['p_lname'] ?? ''));
$patientName = trim((string) ($wRow['p_fname'] ?? ''));
$isPushed = (! empty($wRow['bridge_record_id']) || in_array($wRow['abdm_status'] ?? '', ['queued', 'pushed', 'linked'], true));
$taskId = $this->taskService->createOrRefreshTask(
'wellness_record_publish',

View File

@ -718,47 +718,179 @@ class Abha extends BaseController
}
try {
$helper = new \App\Libraries\Abdm\AbdmV3DirectHelper();
$result = $helper->searchByMobile($mobile);
if (! empty($result['ok']) && ! empty($result['profiles'])) {
return $this->response->setJSON([
'ok' => 1,
'txn_id' => $result['txn_id'] ?? '',
'profiles' => $result['profiles'],
'mobile' => $mobile,
]);
}
// Fallback: If ABDM Sandbox returns no external profiles, check if HMS patient_master has records with this mobile
$db = \Config\Database::connect();
$rows = $db->table('patient_master')
// 1. Fetch matching local patients from patient_master
$localPatients = $db->table('patient_master')
->groupStart()
->where('mphone1', $mobile)
->orWhere('mphone2', $mobile)
->groupEnd()
->get()->getResultArray();
if (! empty($rows)) {
// 2. Fetch all known ABHA addresses from abdm_link_transactions for these patients
$patientIds = array_filter(array_column($localPatients, 'id'));
$patientCodes = array_filter(array_column($localPatients, 'p_code'));
$linkedAddressesByPatient = [];
if (! empty($patientIds) || ! empty($patientCodes)) {
$builder = $db->table('abdm_link_transactions');
$builder->groupStart();
if (! empty($patientIds)) {
$builder->whereIn('patient_id', $patientIds);
}
if (! empty($patientCodes)) {
$builder->orWhereIn('patient_ref', $patientCodes);
}
$builder->groupEnd();
$txns = $builder->select('patient_id, patient_ref, abha_address')->get()->getResultArray();
foreach ($txns as $t) {
$addr = trim((string)($t['abha_address'] ?? ''));
if ($addr !== '') {
if (! empty($t['patient_id'])) {
$linkedAddressesByPatient['id_' . $t['patient_id']][$addr] = true;
}
if (! empty($t['patient_ref'])) {
$linkedAddressesByPatient['ref_' . $t['patient_ref']][$addr] = true;
}
}
}
}
// 3. Query ABDM V3 Direct Helper
$helper = new \App\Libraries\Abdm\AbdmV3DirectHelper();
$result = $helper->searchByMobile($mobile);
if (! empty($result['ok']) && ! empty($result['profiles'])) {
// Enrich ABDM profiles with known local data (UHID, DOB, and associated ABHA addresses)
$enrichedProfiles = [];
foreach ($result['profiles'] as $prof) {
$pIndex = (string)($prof['index'] ?? '');
$pName = trim((string)($prof['name'] ?? ''));
$pAbhaNum = trim((string)($prof['abha_number'] ?? ''));
$last4 = preg_replace('/\D/', '', substr($pAbhaNum, -4));
$pGender = strtoupper(substr(trim((string)($prof['gender'] ?? 'M')), 0, 1));
// Attempt match with local patient_master:
// Priority 1: Match by last 4 digits of abha_id
// Priority 2: Match by exact or partial name
$matchedPatient = null;
if ($last4 !== '') {
foreach ($localPatients as $lp) {
$lpAbha = preg_replace('/\D/', '', (string)($lp['abha_id'] ?? ''));
if ($lpAbha !== '' && str_ends_with($lpAbha, $last4)) {
$matchedPatient = $lp;
break;
}
}
}
if (! $matchedPatient) {
foreach ($localPatients as $lp) {
$lpFName = trim((string)($lp['p_fname'] ?? ''));
if ($lpFName !== '' && (stripos($lpFName, $pName) !== false || stripos($pName, $lpFName) !== false)) {
$matchedPatient = $lp;
break;
}
}
}
$allAddresses = [];
$primaryAddress = '';
$pCode = '';
$pId = 0;
$pDob = (string)($prof['dob'] ?? '');
if ($matchedPatient) {
$pCode = (string)($matchedPatient['p_code'] ?? '');
$pId = (int)($matchedPatient['id'] ?? 0);
if ($pDob === '' && ! empty($matchedPatient['dob'])) {
$pDob = (string)$matchedPatient['dob'];
}
if (! empty($matchedPatient['abha_address'])) {
$addr = trim((string)$matchedPatient['abha_address']);
$allAddresses[$addr] = true;
$primaryAddress = $addr;
}
if (isset($linkedAddressesByPatient['id_' . $pId])) {
foreach (array_keys($linkedAddressesByPatient['id_' . $pId]) as $a) {
$allAddresses[$a] = true;
}
}
if (isset($linkedAddressesByPatient['ref_' . $pCode])) {
foreach (array_keys($linkedAddressesByPatient['ref_' . $pCode]) as $a) {
$allAddresses[$a] = true;
}
}
}
$prof['abha_address'] = $primaryAddress !== '' ? $primaryAddress : (array_keys($allAddresses)[0] ?? '');
$prof['all_abha_addresses'] = array_keys($allAddresses);
$prof['p_code'] = $pCode;
$prof['patient_id'] = $pId;
$prof['dob'] = $pDob;
$enrichedProfiles[] = $prof;
}
return $this->response->setJSON([
'ok' => 1,
'txn_id' => $result['txn_id'] ?? '',
'profiles' => $enrichedProfiles,
'mobile' => $mobile,
]);
}
// 4. Fallback: If ABDM Sandbox returns no external profiles, check if HMS patient_master has records with this mobile
if (! empty($localPatients)) {
$profiles = [];
$idx = 1;
foreach ($rows as $p) {
$pName = trim((string)(($p['fname'] ?? '') . ' ' . ($p['mname'] ?? '') . ' ' . ($p['lname'] ?? '')));
$pAbha = trim((string)($p['abdm_abha_number'] ?? $p['abha_number'] ?? $p['abdm_abha_id'] ?? ''));
foreach ($localPatients as $p) {
$pName = trim((string)($p['p_fname'] ?? ''));
if ($pName === '') {
$pName = 'Patient ' . ($p['p_code'] ?? '');
}
$pAbha = trim((string)($p['abha_id'] ?? ''));
$maskedAbha = $pAbha !== ''
? preg_replace('/^(\d{2})(\d{4})(\d{4})(\d{4})$/', 'xx-xxxx-xxxx-$4', preg_replace('/\D/', '', $pAbha))
: 'xx-xxxx-xxxx-XXXX';
$pGender = 'M';
$rawGen = (string)($p['gender'] ?? '1');
if ($rawGen === '2' || strtoupper($rawGen) === 'F' || strtoupper($rawGen) === 'FEMALE') {
$pGender = 'F';
}
$allAddresses = [];
if (! empty($p['abha_address'])) {
$allAddresses[trim((string)$p['abha_address'])] = true;
}
$pId = (int)($p['id'] ?? 0);
$pCode = (string)($p['p_code'] ?? '');
if (isset($linkedAddressesByPatient['id_' . $pId])) {
foreach (array_keys($linkedAddressesByPatient['id_' . $pId]) as $a) {
$allAddresses[$a] = true;
}
}
if (isset($linkedAddressesByPatient['ref_' . $pCode])) {
foreach (array_keys($linkedAddressesByPatient['ref_' . $pCode]) as $a) {
$allAddresses[$a] = true;
}
}
$profiles[] = [
'index' => (string) $idx++,
'abha_number' => $maskedAbha,
'name' => $pName !== '' ? $pName : 'Patient ' . ($p['p_code'] ?? ''),
'gender' => strtoupper(substr((string)($p['gender'] ?? 'M'), 0, 1)),
'dob' => (string)($p['dob'] ?? ''),
'kyc_verified' => true,
'auth_methods' => ['MOBILE_OTP', 'AADHAAR_OTP'],
'profile_photo' => '',
'local_patient' => true,
'p_code' => (string)($p['p_code'] ?? ''),
'index' => (string) $idx++,
'abha_number' => $maskedAbha,
'name' => $pName,
'gender' => $pGender,
'dob' => (string)($p['dob'] ?? ''),
'kyc_verified' => true,
'auth_methods' => ['MOBILE_OTP', 'AADHAAR_OTP'],
'profile_photo' => '',
'local_patient' => true,
'p_code' => $pCode,
'patient_id' => $pId,
'abha_address' => (string)($p['abha_address'] ?? (array_keys($allAddresses)[0] ?? '')),
'all_abha_addresses' => array_keys($allAddresses),
];
}
return $this->response->setJSON([
@ -1024,6 +1156,61 @@ class Abha extends BaseController
$patientInfo = $this->tryAutoLinkByDirectMatch($abhaNum, $name, $profileMobile, $gender, $dob, $abhaMeta);
$allAbhaAddresses = [];
if ($abhaAddress !== '' && str_contains($abhaAddress, '@')) {
$allAbhaAddresses[$abhaAddress] = true;
}
if (! empty($result['accounts']) && is_array($result['accounts'])) {
foreach ($result['accounts'] as $acc) {
if (is_array($acc)) {
foreach (['preferredAbhaAddress', 'preferredAddress', 'abhaAddress', 'abha_address'] as $ak) {
if (! empty($acc[$ak]) && is_string($acc[$ak]) && str_contains($acc[$ak], '@')) {
$allAbhaAddresses[trim($acc[$ak])] = true;
}
}
}
}
}
$db = \Config\Database::connect();
$patientIdForAddrs = $patientInfo !== null ? (int)($patientInfo['patient_id'] ?? 0) : 0;
$cleanAbhaDigits = preg_replace('/\D/', '', $abhaNum);
$pBuilder = $db->table('patient_master');
$pBuilder->groupStart();
if ($patientIdForAddrs > 0) {
$pBuilder->where('id', $patientIdForAddrs);
}
if ($cleanAbhaDigits !== '') {
$pBuilder->orWhere('abha_id', $cleanAbhaDigits);
}
if ($profileMobile !== '') {
$pBuilder->orWhere('mphone1', $profileMobile)->orWhere('mphone2', $profileMobile);
}
$pBuilder->groupEnd();
$pRows = $pBuilder->select('id, abha_address')->get()->getResultArray();
$matchedPids = [];
foreach ($pRows as $pr) {
$matchedPids[] = (int)$pr['id'];
$pa = trim((string)($pr['abha_address'] ?? ''));
if ($pa !== '' && str_contains($pa, '@')) {
$allAbhaAddresses[$pa] = true;
}
}
if (! empty($matchedPids)) {
$txns = $db->table('abdm_link_transactions')
->select('abha_address')
->whereIn('patient_id', array_unique($matchedPids))
->get()->getResultArray();
foreach ($txns as $t) {
$ta = trim((string)($t['abha_address'] ?? ''));
if ($ta !== '' && str_contains($ta, '@')) {
$allAbhaAddresses[$ta] = true;
}
}
}
$responseBase = [
'ok' => 1,
'txn_id' => $result['txn_id'] ?? $txnId,
@ -1039,7 +1226,8 @@ class Abha extends BaseController
'mobile' => $profileMobile,
'gender' => $gender,
'dob' => $dob,
'abha_address' => $abhaAddress,
'abha_address' => $abhaAddress !== '' ? $abhaAddress : (array_keys($allAbhaAddresses)[0] ?? ''),
'all_abha_addresses'=> array_keys($allAbhaAddresses),
'verified_status' => 'ACTIVE',
'verification_type' => 'VERIFIED',
'kyc_verified' => true,

View File

@ -1263,7 +1263,7 @@ class NursingApi extends BaseController
'hfr_id' => 'IN0510000828',
'patient' => [
'id' => $patientId,
'name' => trim(($patient['p_fname'] ?? '') . (($patient['p_lname'] ?? '') !== '' && ($patient['p_lname'] ?? '') !== '0' && strcasecmp((string)($patient['p_lname'] ?? ''), 'NA') !== 0 ? ' ' . $patient['p_lname'] : '')),
'name' => trim((string) ($patient['p_fname'] ?? '')),
'gender' => match (strtolower(trim((string) ($patient['gender'] ?? '')))) {
'2', 'f', 'female' => 'female',
'1', 'm', 'male' => 'male',
@ -1335,9 +1335,7 @@ class NursingApi extends BaseController
'3', 'o', 'other' => 'O',
default => 'F',
};
$pFname = trim((string) ($patient['p_fname'] ?? ''));
$pLname = trim((string) ($patient['p_lname'] ?? ''));
$cleanPatientName = trim($pFname . ($pLname !== '' && $pLname !== '0' && strcasecmp($pLname, 'NA') !== 0 ? ' ' . $pLname : ''));
$cleanPatientName = trim((string) ($patient['p_fname'] ?? ''));
$connector = \App\Libraries\Abdm\AbdmConnectorFactory::make();
$pushPayload = [
@ -1407,7 +1405,7 @@ class NursingApi extends BaseController
'wellness',
(string) $wellnessId,
$patientId,
trim(($patient['p_fname'] ?? '') . ' ' . ($patient['p_lname'] ?? '')),
trim((string) ($patient['p_fname'] ?? '')),
$effectiveAbha,
'submit',
[

View File

@ -4892,12 +4892,12 @@ class Opd_prescription extends BaseController
$abhaField = $this->resolvePatientAbhaField();
$abhaAddress = $abhaField !== null ? trim((string) ($patientRow[$abhaField] ?? '')) : '';
$patFirstName = $this->sanitizePersonNamePart((string) ($patientRow['p_fname'] ?? ''));
$patLastName = $this->sanitizePersonNamePart((string) ($patientRow['p_lname'] ?? ''));
$patFullName = trim($patFirstName . ($patLastName !== '' ? ' ' . $patLastName : ''));
$patFullName = $this->sanitizePersonNamePart((string) ($patientRow['p_fname'] ?? ''));
if ($patFullName === '') {
$patFullName = trim((string) ($opdRow['P_name'] ?? ''));
}
$patFirstName = $patFullName;
$patLastName = '';
$patientFields = $this->db->getFieldNames('patient_master') ?? [];
$phoneField = $this->resolveFirstField($patientFields, ['p_contact', 'phone', 'mobile', 'contact_no', 'mobile_no', 'p_phone']);
@ -11837,12 +11837,12 @@ class Opd_prescription extends BaseController
$abhaField = $this->resolvePatientAbhaField();
$abhaAddress = $abhaField !== null ? trim((string) ($patientRow[$abhaField] ?? '')) : '';
$patFirstName = $this->sanitizePersonNamePart((string) ($patientRow['p_fname'] ?? ''));
$patLastName = $this->sanitizePersonNamePart((string) ($patientRow['p_lname'] ?? ''));
$patFullName = trim($patFirstName . ($patLastName !== '' ? ' ' . $patLastName : ''));
$patFullName = $this->sanitizePersonNamePart((string) ($patientRow['p_fname'] ?? ''));
if ($patFullName === '') {
$patFullName = trim((string) ($opdRow['P_name'] ?? ''));
}
$patFirstName = $patFullName;
$patLastName = '';
// Resolve phone field (patient_master may use p_contact, phone, mobile, etc.)
$patientFields = $this->db->tableExists('patient_master') ? ($this->db->getFieldNames('patient_master') ?? []) : [];

View File

@ -2349,11 +2349,7 @@ class Patient extends BaseController
$title = trim((string) ($patientRow['title'] ?? ''));
$fname = trim((string) ($patientRow['p_fname'] ?? ''));
$lname = trim((string) ($patientRow['p_lname'] ?? ''));
if ($lname === '0' || strtolower($lname) === 'null') {
$lname = '';
}
$patientName = trim($title . ' ' . $fname . ($lname !== '' ? ' ' . $lname : ''));
$patientName = trim($title . ' ' . $fname);
if ($patientName === '') {
$patientName = 'Patient #' . $pno;
}

View File

@ -33647,7 +33647,7 @@
}
},
"patient_master": {
"create_sql": "CREATE TABLE IF NOT EXISTS `patient_master` ( `id` int NOT NULL AUTO_INCREMENT, `p_code` varchar(50) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT '0' COMMENT 'Patient Code', `old_uhid` varchar(50) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL COMMENT 'If Any OLD HMS Patient Code', `udai` varchar(50) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT '0' COMMENT 'Aadhar Number', `p_fname` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT '0' COMMENT 'Patient Full Name', `p_relative` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL COMMENT 'Patient Relative Type like Son, Husband , Father', `p_rname` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL COMMENT 'Patient Relative Name', `title` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT 'Mr.' COMMENT 'Title like Mr. , Dr., Master etc', `p_lname` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT '0', `mphone1` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT '0' COMMENT 'Patient Phone Number', `mphone2` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT '0' COMMENT 'Patient Another Number', `gender` int DEFAULT '1' COMMENT 'Gender', `insurance_id` int DEFAULT '0' COMMENT 'Insurance Id like Direct Patient 0 ', `insurance_no` varchar(50) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT '0' COMMENT 'Insurance Card No.', `card_holder_name` varchar(50) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT '' COMMENT 'Name on Card', `relation_patient_cardholder` varchar(50) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT '', `issue_date` date DEFAULT NULL, `expiry_date` date DEFAULT NULL, `insurance_card_id` int DEFAULT '0', `dob` date DEFAULT NULL, `estimate_dob` int DEFAULT '1', `age` int DEFAULT NULL, `age_in` varchar(10) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL, `age_in_month` varchar(10) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL, `zip` varchar(50) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL, `email1` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL, `email2` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL, `register_date` datetime DEFAULT NULL, `add1` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL, `add2` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL, `city` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL, `district` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL, `state` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL, `country` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL, `profile_file_id` int DEFAULT '0', `profile_picture_update_by` varchar(50) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT '0', `referby` varchar(200) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL, `insert_date` timestamp NULL DEFAULT CURRENT_TIMESTAMP, `last_update` datetime DEFAULT NULL, `last_visit` datetime DEFAULT NULL, `blood_group` varchar(50) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL, `no_of_visit` int DEFAULT '0', `log` text CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci, `is_smoking` int DEFAULT '0', `is_alcohol` int DEFAULT '0', `is_drug_abuse` int DEFAULT '0', `is_tobacoo` int DEFAULT '0', `is_hypertesion` int DEFAULT '0', `is_niddm` int DEFAULT '0', `is_hbsag` int DEFAULT '0', `is_hcv` int DEFAULT '0', `is_hiv_I_II` int DEFAULT '0', `Others` varchar(50) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL, `abha_id` varchar(30) COLLATE utf8mb3_unicode_ci DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `p_code` (`p_code`), KEY `p_fname_123` (`p_fname`,`p_lname`,`mphone1`,`mphone2`,`email1`,`email2`), KEY `udai` (`udai`), KEY `insert_date` (`insert_date`), KEY `old_uhid` (`old_uhid`), KEY `p_relative` (`p_relative`) ) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;",
"create_sql": "CREATE TABLE IF NOT EXISTS `patient_master` ( `id` int NOT NULL AUTO_INCREMENT, `p_code` varchar(50) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT '0' COMMENT 'Patient Code', `old_uhid` varchar(50) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL COMMENT 'If Any OLD HMS Patient Code', `udai` varchar(50) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT '0' COMMENT 'Aadhar Number', `p_fname` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT '0' COMMENT 'Patient Full Name', `p_relative` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL COMMENT 'Patient Relative Type like Son, Husband , Father', `p_rname` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL COMMENT 'Patient Relative Name', `title` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT 'Mr.' COMMENT 'Title like Mr. , Dr., Master etc', `mphone1` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT '0' COMMENT 'Patient Phone Number', `mphone2` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT '0' COMMENT 'Patient Another Number', `gender` int DEFAULT '1' COMMENT 'Gender', `insurance_id` int DEFAULT '0' COMMENT 'Insurance Id like Direct Patient 0 ', `insurance_no` varchar(50) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT '0' COMMENT 'Insurance Card No.', `card_holder_name` varchar(50) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT '' COMMENT 'Name on Card', `relation_patient_cardholder` varchar(50) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT '', `issue_date` date DEFAULT NULL, `expiry_date` date DEFAULT NULL, `insurance_card_id` int DEFAULT '0', `dob` date DEFAULT NULL, `estimate_dob` int DEFAULT '1', `age` int DEFAULT NULL, `age_in` varchar(10) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL, `age_in_month` varchar(10) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL, `zip` varchar(50) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL, `email1` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL, `email2` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL, `register_date` datetime DEFAULT NULL, `add1` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL, `add2` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL, `city` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL, `district` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL, `state` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL, `country` varchar(100) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL, `profile_file_id` int DEFAULT '0', `profile_picture_update_by` varchar(50) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT '0', `referby` varchar(200) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL, `insert_date` timestamp NULL DEFAULT CURRENT_TIMESTAMP, `last_update` datetime DEFAULT NULL, `last_visit` datetime DEFAULT NULL, `blood_group` varchar(50) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL, `no_of_visit` int DEFAULT '0', `log` text CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci, `is_smoking` int DEFAULT '0', `is_alcohol` int DEFAULT '0', `is_drug_abuse` int DEFAULT '0', `is_tobacoo` int DEFAULT '0', `is_hypertesion` int DEFAULT '0', `is_niddm` int DEFAULT '0', `is_hbsag` int DEFAULT '0', `is_hcv` int DEFAULT '0', `is_hiv_I_II` int DEFAULT '0', `Others` varchar(50) CHARACTER SET utf8mb3 COLLATE utf8mb3_unicode_ci DEFAULT NULL, `abha_id` varchar(30) COLLATE utf8mb3_unicode_ci DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `p_code` (`p_code`), KEY `p_fname_123` (`p_fname`,`mphone1`,`mphone2`,`email1`,`email2`), KEY `udai` (`udai`), KEY `insert_date` (`insert_date`), KEY `old_uhid` (`old_uhid`), KEY `p_relative` (`p_relative`) ) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8mb3 COLLATE=utf8mb3_unicode_ci;",
"columns": {
"id": {
"Field": "id",
@ -33737,17 +33737,6 @@
"Privileges": "select,insert,update,references",
"Comment": "Title like Mr. , Dr., Master etc"
},
"p_lname": {
"Field": "p_lname",
"Type": "varchar(100)",
"Collation": "utf8mb3_unicode_ci",
"Null": "YES",
"Key": "",
"Default": "0",
"Extra": "",
"Privileges": "select,insert,update,references",
"Comment": ""
},
"mphone1": {
"Field": "mphone1",
"Type": "varchar(100)",
@ -34267,7 +34256,6 @@
"type": "BTREE",
"columns": [
"p_fname",
"p_lname",
"mphone1",
"mphone2",
"email1",

View File

@ -28,28 +28,48 @@ class AbdmV3DirectHelper
$this->loadSettings();
}
protected function log(string $level, string $msg): void
{
if (function_exists('log_message')) {
\log_message($level, $msg);
} else {
error_log("[AbdmV3DirectHelper] [{$level}] {$msg}");
}
}
protected function loadSettings(): void
{
try {
$db = \Config\Database::connect();
if ($db->tableExists('hospital_setting')) {
$rows = $db->table('hospital_setting')
->whereIn('s_name', ['ABDM_CLIENT_ID', 'ABDM_CLIENT_SECRET'])
->get()->getResultArray();
foreach ($rows as $r) {
$name = trim((string) ($r['s_name'] ?? ''));
$val = trim((string) ($r['s_value'] ?? ''));
if ($name === 'ABDM_CLIENT_ID' && $val !== '') {
$this->clientId = $val;
}
if ($name === 'ABDM_CLIENT_SECRET' && $val !== '') {
$this->clientSecret = $val;
if (class_exists('\Config\Database') && function_exists('config')) {
$db = \Config\Database::connect();
if ($db->tableExists('hospital_setting')) {
$rows = $db->table('hospital_setting')
->whereIn('s_name', ['ABDM_CLIENT_ID', 'ABDM_CLIENT_SECRET'])
->get()->getResultArray();
foreach ($rows as $r) {
$name = trim((string) ($r['s_name'] ?? ''));
$val = trim((string) ($r['s_value'] ?? ''));
if ($name === 'ABDM_CLIENT_ID' && $val !== '') {
$this->clientId = $val;
}
if ($name === 'ABDM_CLIENT_SECRET' && $val !== '') {
$this->clientSecret = $val;
}
}
}
}
} catch (\Throwable $e) {
if (function_exists('log_message')) {
log_message('warning', '[AbdmV3DirectHelper] loadSettings failed: ' . $e->getMessage());
$this->log('warning', '[AbdmV3DirectHelper] loadSettings failed: ' . $e->getMessage());
}
if (function_exists('env')) {
$envClientId = env('abdm.directClientId', env('BRIDGE_SOURCE_CODE', ''));
if (! empty($envClientId) && $this->clientId === 'SBXID_033661') {
$this->clientId = (string) $envClientId;
}
$envSecret = env('abdm.directClientSecret', '');
if (! empty($envSecret) && $this->clientSecret === '656f79f1-ef99-495f-9f37-713219ecbbcf') {
$this->clientSecret = (string) $envSecret;
}
}
}
@ -104,10 +124,10 @@ class AbdmV3DirectHelper
/**
* Get or refresh gateway session Bearer token.
*/
public function getSessionToken(): string
public function getSessionToken(bool $forceRefresh = false): string
{
$cacheKey = 'abdm_v3_direct_session_token';
if (function_exists('cache')) {
if (! $forceRefresh && function_exists('cache')) {
try {
$cached = cache()->get($cacheKey);
if (is_string($cached) && trim($cached) !== '') {
@ -141,10 +161,10 @@ class AbdmV3DirectHelper
/**
* Get or refresh ABDM RSA Public Certificate PEM.
*/
public function getPublicCertificatePem(): string
public function getPublicCertificatePem(bool $forceRefresh = false): string
{
$cacheKey = 'abdm_v3_direct_public_cert_pem';
if (function_exists('cache')) {
if (! $forceRefresh && function_exists('cache')) {
try {
$cached = cache()->get($cacheKey);
if (is_string($cached) && trim($cached) !== '') {
@ -153,7 +173,7 @@ class AbdmV3DirectHelper
} catch (\Throwable $e) {}
}
$token = $this->getSessionToken();
$token = $this->getSessionToken($forceRefresh);
if ($token === '') {
throw new \RuntimeException('Failed to obtain ABDM session token for certificate fetch.');
}
@ -162,6 +182,7 @@ class AbdmV3DirectHelper
'Authorization: Bearer ' . $token,
'REQUEST-ID: ' . $this->generateUuid(),
'TIMESTAMP: ' . gmdate('Y-m-d\TH:i:s.000\Z'),
'X-Client-ID: ' . $this->clientId,
]);
$rawKey = (string) ($resp['json']['publicKey'] ?? $resp['json']['Certificate'] ?? $resp['raw'] ?? '');
@ -192,13 +213,22 @@ class AbdmV3DirectHelper
$pem = $this->getPublicCertificatePem();
$pubKey = openssl_pkey_get_public($pem);
if ($pubKey === false) {
throw new \RuntimeException('Invalid ABDM public key resource: ' . openssl_error_string());
$pem = $this->getPublicCertificatePem(true);
$pubKey = openssl_pkey_get_public($pem);
if ($pubKey === false) {
throw new \RuntimeException('Invalid ABDM public key resource: ' . openssl_error_string());
}
}
$encrypted = '';
$ok = openssl_public_encrypt($plainText, $encrypted, $pubKey, OPENSSL_PKCS1_OAEP_PADDING);
if (! $ok) {
throw new \RuntimeException('RSA-OAEP encryption failed: ' . openssl_error_string());
$pem = $this->getPublicCertificatePem(true);
$pubKey = openssl_pkey_get_public($pem);
$ok = openssl_public_encrypt($plainText, $encrypted, $pubKey, OPENSSL_PKCS1_OAEP_PADDING);
if (! $ok) {
throw new \RuntimeException('RSA-OAEP encryption failed: ' . openssl_error_string());
}
}
return base64_encode($encrypted);
@ -234,6 +264,20 @@ class AbdmV3DirectHelper
'X-Client-ID: ' . $this->clientId,
]);
// If 401 Unauthorized, refresh token and retry once
if ($resp['code'] === 401) {
$token = $this->getSessionToken(true);
$resp = $this->curlCall($this->searchUrl, 'POST', [
'scope' => ['search-abha'],
'mobile' => $encMobile,
], [
'Authorization: Bearer ' . $token,
'REQUEST-ID: ' . $this->generateUuid(),
'TIMESTAMP: ' . gmdate('Y-m-d\TH:i:s.000\Z'),
'X-Client-ID: ' . $this->clientId,
]);
}
if ($resp['code'] >= 200 && $resp['code'] < 300 && ! empty($resp['json'])) {
$firstBlock = is_array($resp['json']) && isset($resp['json'][0]) ? $resp['json'][0] : $resp['json'];
$txnId = (string) ($firstBlock['txnId'] ?? '');
@ -274,7 +318,7 @@ class AbdmV3DirectHelper
$errMsg = (string) ($resp['json']['message'] ?? $resp['json']['error']['message'] ?? 'ABDM search returned no accounts for this mobile.');
return ['ok' => 0, 'error_text' => $errMsg, 'raw' => $resp['raw']];
} catch (\Throwable $e) {
log_message('error', '[AbdmV3DirectHelper] searchByMobile exception: ' . $e->getMessage());
$this->log('error', '[AbdmV3DirectHelper] searchByMobile exception: ' . $e->getMessage());
return ['ok' => 0, 'error_text' => $e->getMessage()];
}
}
@ -317,6 +361,16 @@ class AbdmV3DirectHelper
'X-Client-ID: ' . $this->clientId,
]);
if ($resp['code'] === 401) {
$token = $this->getSessionToken(true);
$resp = $this->curlCall($this->otpUrl, 'POST', $payload, [
'Authorization: Bearer ' . $token,
'REQUEST-ID: ' . $this->generateUuid(),
'TIMESTAMP: ' . gmdate('Y-m-d\TH:i:s.000\Z'),
'X-Client-ID: ' . $this->clientId,
]);
}
if ($resp['code'] >= 200 && $resp['code'] < 300 && ! empty($resp['json'])) {
$newTxnId = (string) ($resp['json']['txnId'] ?? $searchTxnId);
$msg = (string) ($resp['json']['message'] ?? 'OTP sent successfully.');
@ -330,7 +384,7 @@ class AbdmV3DirectHelper
$errMsg = (string) ($resp['json']['message'] ?? $resp['json']['error']['message'] ?? 'Failed to request OTP from ABDM.');
return ['ok' => 0, 'error_text' => $errMsg, 'raw' => $resp['raw']];
} catch (\Throwable $e) {
log_message('error', '[AbdmV3DirectHelper] requestProfileOtp exception: ' . $e->getMessage());
$this->log('error', '[AbdmV3DirectHelper] requestProfileOtp exception: ' . $e->getMessage());
return ['ok' => 0, 'error_text' => $e->getMessage()];
}
}
@ -380,6 +434,16 @@ class AbdmV3DirectHelper
'X-Client-ID: ' . $this->clientId,
]);
if ($resp['code'] === 401) {
$token = $this->getSessionToken(true);
$resp = $this->curlCall($this->verifyUrl, 'POST', $payload, [
'Authorization: Bearer ' . $token,
'REQUEST-ID: ' . $this->generateUuid(),
'TIMESTAMP: ' . gmdate('Y-m-d\TH:i:s.000\Z'),
'X-Client-ID: ' . $this->clientId,
]);
}
if ($resp['code'] >= 200 && $resp['code'] < 300 && ! empty($resp['json'])) {
$authResult = strtolower((string) ($resp['json']['authResult'] ?? ''));
if ($authResult === 'failed') {
@ -413,7 +477,7 @@ class AbdmV3DirectHelper
$errMsg = (string) ($resp['json']['message'] ?? $resp['json']['error']['message'] ?? 'OTP verification failed.');
return ['ok' => 0, 'error_text' => $errMsg, 'raw' => $resp['raw']];
} catch (\Throwable $e) {
log_message('error', '[AbdmV3DirectHelper] verifyProfileOtp exception: ' . $e->getMessage());
$this->log('error', '[AbdmV3DirectHelper] verifyProfileOtp exception: ' . $e->getMessage());
return ['ok' => 0, 'error_text' => $e->getMessage()];
}
}
@ -580,7 +644,7 @@ class AbdmV3DirectHelper
return ['ok' => 0, 'error_text' => $errMsg, 'raw' => $resp['raw']];
} catch (\Throwable $e) {
log_message('error', '[AbdmV3DirectHelper] verifyLoginOtpByAadhaar exception: ' . $e->getMessage());
$this->log('error', '[AbdmV3DirectHelper] verifyLoginOtpByAadhaar exception: ' . $e->getMessage());
return ['ok' => 0, 'error_text' => $e->getMessage()];
}
}

View File

@ -50,7 +50,7 @@ class AbhaDemographicValidator
}
$builder = $db->table('patient_master')
->select('id, p_code, p_fname, p_lname, gender, dob, age, mphone1, abha_id, abha_address');
->select('id, p_code, p_fname, gender, dob, age, mphone1, abha_id, abha_address');
$matchedAny = false;
$builder->groupStart();
@ -101,7 +101,7 @@ class AbhaDemographicValidator
$conflictId = (int) ($conflict['id'] ?? 0);
$conflictCode = (string) ($conflict['p_code'] ?? ('P-' . $conflictId));
$conflictName = trim(($conflict['p_fname'] ?? '') . ' ' . ($conflict['p_lname'] ?? ''));
$conflictName = trim((string) ($conflict['p_fname'] ?? ''));
$matchedValue = $hasAddressCheck ? $cleanAbhaAddress : $cleanAbhaId;
return [
@ -152,9 +152,6 @@ class AbhaDemographicValidator
// 3. Name Similarity Validation
$patientName = trim((string) ($patientRow['p_fname'] ?? ($patientRow['input_name'] ?? '')));
if (! empty($patientRow['p_lname']) && $patientRow['p_lname'] !== '0') {
$patientName .= ' ' . trim((string) $patientRow['p_lname']);
}
$abhaName = trim((string) ($abhaProfile['full_name'] ?? ($abhaProfile['name'] ?? '')));
if ($patientName !== '' && $abhaName !== '') {

View File

@ -205,6 +205,9 @@
</div>
</div>
<!-- Notice Banner for Pending Linking / Auto-Sync -->
<div id="inspNoticeBanner" class="mb-3 d-none"></div>
<!-- Care Contexts & FHIR Bundles Details Table -->
<div class="table-responsive border rounded">
<table class="table table-sm table-hover mb-0" id="inspCareContextTable">
@ -1287,24 +1290,54 @@
document.getElementById('inspAbhaNumber').textContent = pat.abha_number || '-';
document.getElementById('inspPhone').textContent = pat.phone || '-';
var linkedCount = 0;
var totalCount = contexts.length;
var gatewayLinkedCount = 0;
var localLinkedCount = 0;
var fhirCount = 0;
var pendingPhrList = [];
contexts.forEach(function (c) {
if (c.is_linked) linkedCount++;
if (c.is_gateway_linked) {
gatewayLinkedCount++;
} else if (c.is_linked) {
localLinkedCount++;
pendingPhrList.push(c.ref || c.display || '');
}
if (c.is_fhir_ready) fhirCount++;
});
document.getElementById('inspTotalContexts').textContent = contexts.length;
document.getElementById('inspLinkedContexts').textContent = linkedCount;
document.getElementById('inspUnlinkedContexts').textContent = contexts.length - linkedCount;
document.getElementById('inspTotalContexts').textContent = totalCount;
document.getElementById('inspLinkedContexts').textContent = gatewayLinkedCount;
document.getElementById('inspUnlinkedContexts').textContent = totalCount - gatewayLinkedCount;
document.getElementById('inspFhirCount').textContent = fhirCount;
if (linkedRefs.length > 0) {
document.getElementById('inspGatewayStatus').innerHTML = '<span class="badge bg-success"><i class="bi bi-shield-check me-1"></i>Gateway Verified (' + linkedRefs.length + ' links)</span>';
} else if (linkedCount > 0) {
document.getElementById('inspGatewayStatus').innerHTML = '<span class="badge bg-primary"><i class="bi bi-check2-circle me-1"></i>Local Linked</span>';
if (gatewayLinkedCount > 0) {
document.getElementById('inspGatewayStatus').innerHTML = '<span class="badge bg-success"><i class="bi bi-shield-check me-1"></i>Gateway Verified (' + gatewayLinkedCount + ' in PHR)</span>';
} else if (localLinkedCount > 0) {
document.getElementById('inspGatewayStatus').innerHTML = '<span class="badge bg-warning text-dark"><i class="bi bi-clock-history me-1"></i>Local Ready (' + localLinkedCount + ' pending PHR)</span>';
} else {
document.getElementById('inspGatewayStatus').innerHTML = '<span class="badge bg-warning text-dark"><i class="bi bi-exclamation-triangle me-1"></i>Unlinked</span>';
document.getElementById('inspGatewayStatus').innerHTML = '<span class="badge bg-secondary"><i class="bi bi-exclamation-triangle me-1"></i>Unlinked</span>';
}
var noticeBanner = document.getElementById('inspNoticeBanner');
if (noticeBanner) {
if (localLinkedCount > 0) {
noticeBanner.className = 'alert alert-warning d-flex align-items-center mb-3 py-2 px-3 shadow-sm';
noticeBanner.innerHTML = '<i class="bi bi-info-circle-fill fs-4 text-warning me-3 flex-shrink-0"></i>' +
'<div>' +
'<strong>Pending PHR App Link:</strong> Patient has <strong>' + localLinkedCount + '</strong> record(s) ready in HMS (' + hesc(pendingPhrList.slice(0, 3).join(', ')) + (pendingPhrList.length > 3 ? ' and more' : '') + ') that are not yet confirmed in the PHR app.' +
'<br><span class="small text-muted">Records sync automatically via HIP Auto-Link. Alternatively, ask the patient to tap <strong>"Discover Records" / "Fetch New Records"</strong> in their ABHA app (e.g. ABHA / Aarogya Setu / Eka Care) to pull them instantly.</span>' +
'</div>';
} else if (gatewayLinkedCount > 0 && totalCount === gatewayLinkedCount) {
noticeBanner.className = 'alert alert-success d-flex align-items-center mb-3 py-2 px-3 shadow-sm';
noticeBanner.innerHTML = '<i class="bi bi-check-circle-fill fs-4 text-success me-3 flex-shrink-0"></i>' +
'<div>' +
'<strong>All Records Synced:</strong> All ' + gatewayLinkedCount + ' care context(s) are confirmed linked on ABDM Gateway and accessible in the patient\'s PHR app.' +
'</div>';
} else {
noticeBanner.className = 'mb-3 d-none';
noticeBanner.innerHTML = '';
}
}
if (contexts.length === 0) {
@ -1315,13 +1348,26 @@
var html = '';
contexts.forEach(function (c) {
var ref = c.ref || '';
var isGatewayLinked = !!c.is_gateway_linked;
var isLinked = !!c.is_linked;
var isFhir = !!c.is_fhir_ready;
var hiType = c.hi_type || 'OPConsultRecord';
var linkBadge = isLinked
? '<span class="badge bg-success"><i class="bi bi-check-circle me-1"></i>LINKED</span>'
: '<span class="badge bg-warning text-dark"><i class="bi bi-clock me-1"></i>UNLINKED</span>';
var linkBadge = '';
var actionBtn = '';
if (isGatewayLinked) {
linkBadge = '<span class="badge bg-success" title="Confirmed on ABDM Gateway and linked in patient PHR app"><i class="bi bi-shield-check me-1"></i>Linked in PHR</span>';
actionBtn = '<span class="badge bg-success-subtle text-success border border-success-subtle py-1 px-2"><i class="bi bi-check-all me-1"></i>In PHR</span>';
} else if (isLinked) {
linkBadge = '<span class="badge bg-warning text-dark" title="Record is published in HMS; awaiting ABDM auto-link or PHR app discovery"><i class="bi bi-clock-history me-1"></i>Ready - Pending PHR Link</span>';
actionBtn = '<button type="button" class="btn btn-sm btn-outline-primary py-0 px-2 btn-insp-link" data-ref="' + hesc(ref) + '" data-type="' + hesc(hiType) + '" data-patient-id="' + pat.id + '" data-patient-name="' + hesc(pat.name) + '" data-abha="' + hesc(pat.abha_address || abhaAddress) + '" title="Initiate HIP auto-linking to PHR app"><i class="bi bi-arrow-repeat me-1"></i>Link to PHR</button>';
} else if (!isFhir) {
linkBadge = '<span class="badge bg-secondary"><i class="bi bi-clock me-1"></i>UNLINKED</span>';
actionBtn = '<button type="button" class="btn btn-sm btn-outline-secondary py-0 px-2" disabled title="Clinical findings must be documented before linking to ABDM"><i class="bi bi-clock me-1"></i>Draft / Incomplete</button>';
} else {
linkBadge = '<span class="badge bg-secondary"><i class="bi bi-clock me-1"></i>UNLINKED</span>';
actionBtn = '<button type="button" class="btn btn-sm btn-outline-success py-0 px-2 btn-insp-link" data-ref="' + hesc(ref) + '" data-type="' + hesc(hiType) + '" data-patient-id="' + pat.id + '" data-patient-name="' + hesc(pat.name) + '" data-abha="' + hesc(pat.abha_address || abhaAddress) + '"><i class="bi bi-link-45deg me-1"></i>Link Context</button>';
}
var fhirBadge = isFhir
? '<span class="badge bg-info"><i class="bi bi-file-earmark-code me-1"></i>Ready</span>'
@ -1342,15 +1388,6 @@
}
}
var actionBtn = '';
if (isLinked) {
actionBtn = '<span class="badge bg-success-subtle text-success border border-success-subtle py-1 px-2"><i class="bi bi-check-all me-1"></i>Active</span>';
} else if (!isFhir) {
actionBtn = '<button type="button" class="btn btn-sm btn-outline-secondary py-0 px-2" disabled title="Clinical findings must be documented before linking to ABDM"><i class="bi bi-clock me-1"></i>Draft / Incomplete</button>';
} else {
actionBtn = '<button type="button" class="btn btn-sm btn-outline-success py-0 px-2 btn-insp-link" data-ref="' + hesc(ref) + '" data-type="' + hesc(hiType) + '" data-patient-id="' + pat.id + '" data-patient-name="' + hesc(pat.name) + '" data-abha="' + hesc(pat.abha_address || abhaAddress) + '"><i class="bi bi-link-45deg me-1"></i>Link Context</button>';
}
html += '<tr>';
html += '<td><code class="fw-semibold text-primary">' + hesc(ref) + '</code></td>';
html += '<td>' + hesc(c.display || ref) + '</td>';
@ -2113,11 +2150,14 @@
headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json' }
}).then(function (r) { return r.json(); })
.then(function (res) {
if (res && res.ok === 1 && res.links && res.links.length > 0) {
var links = (res && (res.care_contexts || res.links || (res.data && res.data.patient && res.data.patient.careContexts) || (res.data && res.data[0] && res.data[0].careContexts))) || [];
if (res && res.ok === 1 && links.length > 0) {
if (statusEl) {
statusEl.innerHTML = '<span class="badge bg-success"><i class="bi bi-shield-check me-1"></i>Gateway Verified (' + res.links.length + ' links)</span>';
statusEl.innerHTML = '<span class="badge bg-success"><i class="bi bi-shield-check me-1"></i>Gateway Verified (' + links.length + ' links)</span>';
}
setStatus('ABDM Gateway verified ' + res.links.length + ' linked care context(s) for patient.');
setStatus('ABDM Gateway verified ' + links.length + ' linked care context(s) for patient.');
// Reload inspector so table highlights verified vs pending
loadPatientAbhaInspector(currentAbhaFilter);
} else if (res && res.ok === 1) {
if (statusEl) {
statusEl.innerHTML = '<span class="badge bg-warning text-dark"><i class="bi bi-info-circle me-1"></i>No Gateway Links Found</span>';

View File

@ -68,6 +68,14 @@
.abha-mobile-modal .abha-card-preview { min-height:230px; display:grid; place-items:center; border:1px solid #dce3ec; background:#f8fafc; border-radius:8px; padding:12px; }
.abha-mobile-modal .abha-card-preview img { max-width:100%; max-height:360px; object-fit:contain; border-radius:6px; box-shadow:0 4px 12px rgba(0,0,0,0.08); }
.abha-mobile-modal .bg-primary-subtle { background-color: #e7f1ff !important; color: #0d6efd !important; }
.abha-mobile-modal .bg-success-subtle { background-color: #e6f8f0 !important; color: #198754 !important; }
.abha-mobile-modal .bg-secondary-subtle { background-color: #f1f3f5 !important; color: #495057 !important; }
.abha-mobile-modal .cursor-pointer { cursor: pointer; }
.abha-mobile-modal .abha-addr-card { transition: all 0.15s ease-in-out; }
.abha-mobile-modal .abha-addr-card:hover { border-color: #356cf4 !important; background-color: #f8faff !important; }
.abha-mobile-modal .abha-addr-card.selected { border-color: #356cf4 !important; background-color: #f0f5ff !important; box-shadow: 0 0 0 1px #356cf4; }
@media (max-width:575.98px) {
.abha-mobile-modal .modal-body { padding:16px; }
.abha-mobile-step:not(:last-child)::after { width:18px; }
@ -194,6 +202,17 @@
<div class="text-muted fw-semibold" id="abhaMobileProfileAddress"></div>
</div>
</div>
<!-- ABHA Address Selector when user has one or multiple addresses -->
<div class="card border border-primary-subtle bg-light-subtle p-3 rounded-3 mb-3 d-none" id="abhaAddressSelectorCard">
<label class="form-label fw-bold text-dark mb-2 d-flex align-items-center justify-content-between">
<span><i class="bi bi-at text-primary me-1"></i>Select ABHA Address to Link / Update:</span>
<span class="badge bg-primary rounded-pill" id="abhaAddressCountBadge">1</span>
</label>
<div id="abhaAddressRadioGroup" class="d-flex flex-column gap-2">
<!-- Populated dynamically -->
</div>
</div>
<dl class="abha-profile-list">
<div><dt>ABHA Number</dt><dd id="abhaMobileProfileNumber">-</dd></div>
<div><dt>ABHA Address</dt><dd id="abhaMobileProfileId">-</dd></div>
@ -416,21 +435,39 @@ window.AbhaMobileModal = (function () {
? '<img src="' + (String(prof.profile_photo).indexOf('data:') === 0 ? prof.profile_photo : 'data:image/jpeg;base64,' + prof.profile_photo) + '" class="rounded-circle" style="width:48px;height:48px;object-fit:cover">'
: '<div class="abha-profile-avatar"><i class="bi ' + iconClass + '"></i></div>';
var addrs = Array.isArray(prof.all_abha_addresses) && prof.all_abha_addresses.length > 0
? prof.all_abha_addresses
: (prof.abha_address ? [prof.abha_address] : []);
var addrBadges = '';
if (addrs.length > 0) {
addrBadges = addrs.map(function(a) {
return '<span class="badge bg-primary-subtle text-primary border border-primary-subtle font-monospace"><i class="bi bi-at"></i>' + escapeHtml(a) + '</span>';
}).join(' ');
} else {
addrBadges = '<span class="badge bg-light text-muted border"><i class="bi bi-person-badge me-1"></i>ABHA Profile</span>';
}
var uhidBadge = prof.p_code
? '<span class="badge bg-secondary-subtle text-secondary border border-secondary-subtle"><i class="bi bi-hospital me-1"></i>UHID: ' + escapeHtml(prof.p_code) + '</span>'
: '';
var card = document.createElement('div');
card.className = 'abha-profile-card d-flex align-items-center justify-content-between gap-3 ' + (isSelected ? 'selected' : '');
card.setAttribute('data-index', index);
card.innerHTML =
'<div class="d-flex align-items-center gap-3">' +
'<div class="d-flex align-items-center gap-3 flex-grow-1">' +
'<div class="form-check m-0">' +
'<input class="form-check-input profile-radio" type="radio" name="abhaProfileOption" value="' + index + '" ' + (isSelected ? 'checked' : '') + '>' +
'</div>' +
photoHtml +
'<div>' +
'<div class="d-flex align-items-center gap-2 flex-wrap">' +
'<h6 class="mb-0 fw-bold text-dark">' + escapeHtml(name) + '</h6>' +
'<div class="flex-grow-1">' +
'<div class="d-flex align-items-center gap-2 flex-wrap mb-1">' +
'<h6 class="mb-0 fw-bold text-dark fs-6">' + escapeHtml(name) + '</h6>' +
(isVerified ? '<span class="badge bg-success-subtle text-success border border-success-subtle"><i class="bi bi-patch-check-fill me-1"></i>Active</span>' : '') +
uhidBadge +
'</div>' +
'<div class="text-muted small mt-1">' +
'<div class="mb-1 d-flex flex-wrap gap-1">' + addrBadges + '</div>' +
'<div class="text-muted small">' +
'<span class="me-3"><i class="bi bi-credit-card-2-front me-1"></i>ABHA: <strong>' + escapeHtml(formatAbha(abhaNum)) + '</strong></span>' +
'<span class="me-3"><i class="bi bi-gender-ambiguous me-1"></i>' + escapeHtml(gender) + '</span>' +
(dob && dob !== '-' ? '<span><i class="bi bi-calendar-event me-1"></i>DOB: ' + escapeHtml(dob) + '</span>' : '') +
@ -610,6 +647,82 @@ window.AbhaMobileModal = (function () {
if (elMobile) elMobile.textContent = maskMobile(profile.mobile || searchMobile);
if (elFullAddr) elFullAddr.textContent = [profile.address, profile.district, profile.state, profile.zip].filter(Boolean).join(', ') || '-';
// Gather all known ABHA addresses for this profile
var allAddrs = [];
if (Array.isArray(profile.all_abha_addresses)) {
allAddrs = allAddrs.concat(profile.all_abha_addresses);
}
if (selectedProfile && Array.isArray(selectedProfile.all_abha_addresses)) {
allAddrs = allAddrs.concat(selectedProfile.all_abha_addresses);
}
if (profile.abha_address) {
allAddrs.push(profile.abha_address);
}
if (selectedProfile && selectedProfile.abha_address) {
allAddrs.push(selectedProfile.abha_address);
}
var uniqueAddrs = [];
allAddrs.forEach(function(a) {
a = String(a || '').trim();
if (a && a.indexOf('@') !== -1 && uniqueAddrs.indexOf(a) === -1) {
uniqueAddrs.push(a);
}
});
var currentSelectedAddr = profile.abha_address || uniqueAddrs[0] || '';
profile.abha_address = currentSelectedAddr;
verifiedProfile.abha_address = currentSelectedAddr;
var addrSelectorCard = document.getElementById('abhaAddressSelectorCard');
var addrGroup = document.getElementById('abhaAddressRadioGroup');
var addrCountBadge = document.getElementById('abhaAddressCountBadge');
if (addrSelectorCard && addrGroup) {
if (uniqueAddrs.length > 0) {
addrSelectorCard.classList.remove('d-none');
if (addrCountBadge) addrCountBadge.textContent = uniqueAddrs.length;
addrGroup.innerHTML = '';
uniqueAddrs.forEach(function(addr, idx) {
var isChecked = (addr === currentSelectedAddr);
var itemDiv = document.createElement('div');
itemDiv.className = 'form-check p-2 px-3 border rounded-2 bg-white d-flex align-items-center gap-2 cursor-pointer abha-addr-card ' + (isChecked ? 'selected border-primary' : 'border-secondary-subtle');
itemDiv.innerHTML =
'<input class="form-check-input m-0 abha-addr-radio cursor-pointer" type="radio" name="selectedAbhaAddressOption" id="addrOpt_' + idx + '" value="' + escapeHtml(addr) + '" ' + (isChecked ? 'checked' : '') + '>' +
'<label class="form-check-label flex-grow-1 cursor-pointer fw-semibold font-monospace text-dark mb-0" for="addrOpt_' + idx + '">' +
'<i class="bi bi-at text-primary me-1"></i>' + escapeHtml(addr) +
'</label>' +
(isChecked ? '<span class="badge bg-success-subtle text-success border border-success-subtle active-badge"><i class="bi bi-check2 me-1"></i>Selected</span>' : '');
itemDiv.addEventListener('click', function(e) {
if (e.target.tagName !== 'INPUT') {
var r = itemDiv.querySelector('input[type="radio"]');
if (r) r.checked = true;
}
profile.abha_address = addr;
verifiedProfile.abha_address = addr;
if (elAddr) elAddr.textContent = addr;
if (elId) elId.textContent = addr;
addrGroup.querySelectorAll('.abha-addr-card').forEach(function(div) {
div.classList.remove('selected', 'border-primary');
div.classList.add('border-secondary-subtle');
var b = div.querySelector('.active-badge');
if (b) b.remove();
});
itemDiv.classList.remove('border-secondary-subtle');
itemDiv.classList.add('selected', 'border-primary');
var span = document.createElement('span');
span.className = 'badge bg-success-subtle text-success border border-success-subtle active-badge';
span.innerHTML = '<i class="bi bi-check2 me-1"></i>Selected';
itemDiv.appendChild(span);
});
addrGroup.appendChild(itemDiv);
});
} else {
addrSelectorCard.classList.add('d-none');
}
}
var photo = profile.photo ? (String(profile.photo).indexOf('data:') === 0 ? profile.photo : 'data:image/jpeg;base64,' + profile.photo) : '';
var photoEl = document.getElementById('abhaMobilePhoto');
if (photoEl) {
@ -704,11 +817,62 @@ window.AbhaMobileModal = (function () {
if (compareBtn) compareBtn.onclick = function () {
if (!verifiedProfile) return;
var profile = verifiedProfile;
// If auto-matched to an existing HMS patient:
if (profile.need_confirmation === false && Number(profile.patient_id || 0) > 0) {
alertBox('success', 'This ABHA is already linked to HMS patient <strong>' + escapeHtml(profile.p_code || '') + '</strong>. Close this window when done.');
if (typeof onVerified === 'function') onVerified(profile);
var c = csrf();
var postData = new URLSearchParams();
postData.append('action', 'existing');
postData.append('update_mode', 'update');
postData.append('patient_id', profile.patient_id);
postData.append('abha_number', profile.abha_number || '');
postData.append('name', profile.name || '');
postData.append('mobile', profile.mobile || searchMobile);
postData.append('gender', profile.gender || '');
postData.append('dob', profile.dob || '');
postData.append('abha_address', profile.abha_address || '');
postData.append('photo', profile.photo || '');
postData.append('card_base64', profile.card_base64 || '');
postData.append('verified_status', 'ACTIVE');
postData.append('verification_type', 'VERIFIED');
postData.append('kyc_verified', profile.kyc_verified ? '1' : '0');
postData.append('mobile_verified', '1');
postData.append('<?= csrf_token() ?>', c);
compareBtn.disabled = true;
compareBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Linking to HMS...';
fetch('<?= base_url('abha/create/confirm_patient') ?>', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'X-Requested-With': 'XMLHttpRequest'
},
body: postData.toString()
})
.then(function(r) { return r.json(); })
.then(function(resp) {
compareBtn.disabled = false;
compareBtn.innerHTML = '<i class="bi bi-people me-1"></i>Compare with HMS Patients';
if (resp && resp.ok == 1) {
alertBox('success', 'ABHA <strong>' + escapeHtml(profile.abha_address) + '</strong> successfully linked with HMS Patient <strong>' + escapeHtml(profile.p_code) + '</strong>.');
modalEl.addEventListener('hidden.bs.modal', function handler() {
modalEl.removeEventListener('hidden.bs.modal', handler);
if (typeof onVerified === 'function') onVerified(Object.assign({}, profile, resp));
});
setTimeout(function() { modal.hide(); }, 800);
} else {
alertBox('danger', (resp && resp.error_text) || 'Failed to update patient record.');
}
})
.catch(function() {
compareBtn.disabled = false;
compareBtn.innerHTML = '<i class="bi bi-people me-1"></i>Compare with HMS Patients';
alertBox('danger', 'Error updating patient record in HMS.');
});
return;
}
modalEl.addEventListener('hidden.bs.modal', function handler() {
modalEl.removeEventListener('hidden.bs.modal', handler);
if (typeof onVerified === 'function') onVerified(profile);

View File

@ -138,7 +138,6 @@ final class AbdmTaskBoardBackfillTest extends CIUnitTestCase
id INTEGER PRIMARY KEY,
p_code TEXT,
p_fname TEXT,
p_lname TEXT,
dob TEXT,
age TEXT,
gender INTEGER,
@ -153,8 +152,7 @@ final class AbdmTaskBoardBackfillTest extends CIUnitTestCase
$db->table('patient_master')->insert([
'id' => $testPatientId,
'p_code' => 'P-TEST-998876',
'p_fname' => 'Janvi',
'p_lname' => 'Bisht',
'p_fname' => 'Janvi Bisht',
'mphone1' => '9876543212',
'gender' => 2,
'abha_address' => 'janvibisht2506@sbx',
@ -199,7 +197,6 @@ final class AbdmTaskBoardBackfillTest extends CIUnitTestCase
id INTEGER PRIMARY KEY,
p_code TEXT,
p_fname TEXT,
p_lname TEXT,
dob TEXT,
age TEXT,
gender INTEGER,
@ -214,8 +211,7 @@ final class AbdmTaskBoardBackfillTest extends CIUnitTestCase
$db->table('patient_master')->insert([
'id' => $testPatientId,
'p_code' => 'P-TEST-998877',
'p_fname' => 'Test',
'p_lname' => 'AbhaPatient',
'p_fname' => 'Test AbhaPatient',
'mphone1' => '9876543210',
'gender' => 1,
'abha_address' => 'testpatient998877@sbx',