diff --git a/.agents/rules/abdm_standards.md b/.agents/rules/abdm_standards.md index f965a00..684cc94 100644 --- a/.agents/rules/abdm_standards.md +++ b/.agents/rules/abdm_standards.md @@ -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`. + diff --git a/AGENTS.md b/AGENTS.md index 5b8052c..c46916d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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`. + diff --git a/app/Controllers/AbdmFhirInspector.php b/app/Controllers/AbdmFhirInspector.php index 279f53b..be1f181 100644 --- a/app/Controllers/AbdmFhirInspector.php +++ b/app/Controllers/AbdmFhirInspector.php @@ -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']; } diff --git a/app/Controllers/AbdmGateway.php b/app/Controllers/AbdmGateway.php index a5bb031..9520580 100644 --- a/app/Controllers/AbdmGateway.php +++ b/app/Controllers/AbdmGateway.php @@ -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, ]); } } diff --git a/app/Controllers/AbdmTaskBoard.php b/app/Controllers/AbdmTaskBoard.php index 0799d31..2558d3b 100644 --- a/app/Controllers/AbdmTaskBoard.php +++ b/app/Controllers/AbdmTaskBoard.php @@ -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', diff --git a/app/Controllers/Abha.php b/app/Controllers/Abha.php index b460faa..cdfb877 100644 --- a/app/Controllers/Abha.php +++ b/app/Controllers/Abha.php @@ -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, diff --git a/app/Controllers/Api/v1/NursingApi.php b/app/Controllers/Api/v1/NursingApi.php index 681bc9b..748d268 100644 --- a/app/Controllers/Api/v1/NursingApi.php +++ b/app/Controllers/Api/v1/NursingApi.php @@ -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', [ diff --git a/app/Controllers/Opd_prescription.php b/app/Controllers/Opd_prescription.php index 481c747..984f58f 100644 --- a/app/Controllers/Opd_prescription.php +++ b/app/Controllers/Opd_prescription.php @@ -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') ?? []) : []; diff --git a/app/Controllers/Patient.php b/app/Controllers/Patient.php index 3a851f5..8d92ae3 100644 --- a/app/Controllers/Patient.php +++ b/app/Controllers/Patient.php @@ -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; } diff --git a/app/Database/master_schema.json b/app/Database/master_schema.json index 170de2c..9d1653b 100644 --- a/app/Database/master_schema.json +++ b/app/Database/master_schema.json @@ -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", diff --git a/app/Libraries/Abdm/AbdmV3DirectHelper.php b/app/Libraries/Abdm/AbdmV3DirectHelper.php index 9c4f040..5e56cd6 100644 --- a/app/Libraries/Abdm/AbdmV3DirectHelper.php +++ b/app/Libraries/Abdm/AbdmV3DirectHelper.php @@ -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()]; } } diff --git a/app/Libraries/Abdm/AbhaDemographicValidator.php b/app/Libraries/Abdm/AbhaDemographicValidator.php index 0c27818..e46c6a4 100644 --- a/app/Libraries/Abdm/AbhaDemographicValidator.php +++ b/app/Libraries/Abdm/AbhaDemographicValidator.php @@ -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 !== '') { diff --git a/app/Views/abdm/task_board.php b/app/Views/abdm/task_board.php index b964b8e..c1412b7 100644 --- a/app/Views/abdm/task_board.php +++ b/app/Views/abdm/task_board.php @@ -205,6 +205,9 @@ + +
+
@@ -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 = 'Gateway Verified (' + linkedRefs.length + ' links)'; - } else if (linkedCount > 0) { - document.getElementById('inspGatewayStatus').innerHTML = 'Local Linked'; + if (gatewayLinkedCount > 0) { + document.getElementById('inspGatewayStatus').innerHTML = 'Gateway Verified (' + gatewayLinkedCount + ' in PHR)'; + } else if (localLinkedCount > 0) { + document.getElementById('inspGatewayStatus').innerHTML = 'Local Ready (' + localLinkedCount + ' pending PHR)'; } else { - document.getElementById('inspGatewayStatus').innerHTML = 'Unlinked'; + document.getElementById('inspGatewayStatus').innerHTML = 'Unlinked'; + } + + 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 = '' + + '
' + + 'Pending PHR App Link: Patient has ' + localLinkedCount + ' 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.' + + '
Records sync automatically via HIP Auto-Link. Alternatively, ask the patient to tap "Discover Records" / "Fetch New Records" in their ABHA app (e.g. ABHA / Aarogya Setu / Eka Care) to pull them instantly.' + + '
'; + } 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 = '' + + '
' + + 'All Records Synced: All ' + gatewayLinkedCount + ' care context(s) are confirmed linked on ABDM Gateway and accessible in the patient\'s PHR app.' + + '
'; + } 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 - ? 'LINKED' - : 'UNLINKED'; + var linkBadge = ''; + var actionBtn = ''; + if (isGatewayLinked) { + linkBadge = 'Linked in PHR'; + actionBtn = 'In PHR'; + } else if (isLinked) { + linkBadge = 'Ready - Pending PHR Link'; + actionBtn = ''; + } else if (!isFhir) { + linkBadge = 'UNLINKED'; + actionBtn = ''; + } else { + linkBadge = 'UNLINKED'; + actionBtn = ''; + } var fhirBadge = isFhir ? 'Ready' @@ -1342,15 +1388,6 @@ } } - var actionBtn = ''; - if (isLinked) { - actionBtn = 'Active'; - } else if (!isFhir) { - actionBtn = ''; - } else { - actionBtn = ''; - } - html += ''; html += ''; html += ''; @@ -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 = 'Gateway Verified (' + res.links.length + ' links)'; + statusEl.innerHTML = 'Gateway Verified (' + links.length + ' links)'; } - 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 = 'No Gateway Links Found'; diff --git a/app/Views/partials/abha_mobile_modal.php b/app/Views/partials/abha_mobile_modal.php index f1be8c1..746e1da 100644 --- a/app/Views/partials/abha_mobile_modal.php +++ b/app/Views/partials/abha_mobile_modal.php @@ -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 @@
+ +
+ +
+ +
+
+
ABHA Number
-
ABHA Address
-
@@ -416,21 +435,39 @@ window.AbhaMobileModal = (function () { ? '' : '
'; + 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 '' + escapeHtml(a) + ''; + }).join(' '); + } else { + addrBadges = 'ABHA Profile'; + } + + var uhidBadge = prof.p_code + ? 'UHID: ' + escapeHtml(prof.p_code) + '' + : ''; + 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 = - '
' + + '
' + '
' + '' + '
' + photoHtml + - '
' + - '
' + - '
' + escapeHtml(name) + '
' + + '
' + + '
' + + '
' + escapeHtml(name) + '
' + (isVerified ? 'Active' : '') + + uhidBadge + '
' + - '
' + + '
' + addrBadges + '
' + + '
' + 'ABHA: ' + escapeHtml(formatAbha(abhaNum)) + '' + '' + escapeHtml(gender) + '' + (dob && dob !== '-' ? 'DOB: ' + escapeHtml(dob) + '' : '') + @@ -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 = + '' + + '' + + (isChecked ? 'Selected' : ''); + + 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 = '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 ' + escapeHtml(profile.p_code || '') + '. 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('', c); + + compareBtn.disabled = true; + compareBtn.innerHTML = 'Linking to HMS...'; + + fetch('', { + 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 = 'Compare with HMS Patients'; + if (resp && resp.ok == 1) { + alertBox('success', 'ABHA ' + escapeHtml(profile.abha_address) + ' successfully linked with HMS Patient ' + escapeHtml(profile.p_code) + '.'); + 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 = '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); diff --git a/tests/unit/Abdm/AbdmTaskBoardBackfillTest.php b/tests/unit/Abdm/AbdmTaskBoardBackfillTest.php index 2d4d469..79bb556 100644 --- a/tests/unit/Abdm/AbdmTaskBoardBackfillTest.php +++ b/tests/unit/Abdm/AbdmTaskBoardBackfillTest.php @@ -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',
' + hesc(ref) + '' + hesc(c.display || ref) + '