diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 002edda..7a02366 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -499,6 +499,7 @@ $routes->group('billing', function($routes) { $routes->post('patient/save_profile_image/(:num)', 'Patient::save_profile_image/$1'); $routes->match(['get', 'post'], 'patient/patient_file_upload/(:num)', 'Patient::patient_file_upload/$1'); $routes->get('patient/city', 'Patient::city'); + $routes->get('patient/district_list', 'Patient::district_list'); $routes->get('patient/get_name', 'Patient::get_name'); $routes->post('patient/update', 'Patient::update'); $routes->post('patient/update_aadhar', 'Patient::update_aadhar'); diff --git a/app/Controllers/AbdmGateway.php b/app/Controllers/AbdmGateway.php index 64ea41e..efd357a 100644 --- a/app/Controllers/AbdmGateway.php +++ b/app/Controllers/AbdmGateway.php @@ -2762,13 +2762,13 @@ class AbdmGateway extends BaseController $fields = $this->db->getFieldNames('patient_master') ?? []; $abhaField = $this->resolveFirstExistingColumn($fields, ['abha_id', 'abha_no', 'abha', 'abha_address']); - $select = 'id,p_code,p_fname,mphone1'; + $select = 'id,p_code,p_fname,mphone1,dob,gender'; if ($abhaField) { $select .= ',' . $abhaField . ' AS patient_abha'; } $bucket = []; - $append = static function (array $rows, string $reason) use (&$bucket): void { + $append = static function (array $rows, string $reason, int $score) use (&$bucket): void { foreach ($rows as $row) { $id = (int) ($row['id'] ?? 0); if ($id <= 0) { @@ -2776,26 +2776,88 @@ class AbdmGateway extends BaseController } if (! isset($bucket[$id])) { $row['match_reasons'] = []; + $row['match_score'] = 0; $bucket[$id] = $row; } if (! in_array($reason, $bucket[$id]['match_reasons'], true)) { $bucket[$id]['match_reasons'][] = $reason; } + if ($score > ($bucket[$id]['match_score'] ?? 0)) { + $bucket[$id]['match_score'] = $score; + } } }; + // Definitive identifier matches (score 4) if ($abhaField && ($identity['abha_number'] !== '' || $identity['abha_address'] !== '')) { $abha = $identity['abha_number'] !== '' ? $identity['abha_number'] : $identity['abha_address']; $rows = $this->db->table('patient_master')->select($select)->where($abhaField, $abha)->limit(10)->get()->getResultArray(); - $append($rows, 'ABHA'); + $append($rows, 'ABHA', 4); } - if ($identity['phone'] !== '') { + // Demographic matches — primary criteria + $nameUpper = strtoupper(trim((string) ($identity['patient_name'] ?? ''))); + $rawGender = strtoupper(trim((string) ($identity['gender'] ?? ''))); + $genderDb = ($rawGender === 'F' || $rawGender === '2') ? 2 : ($rawGender !== '' ? 1 : null); + $dob = trim((string) ($identity['dob'] ?? '')); + $dobValid = preg_match('/^\d{4}-\d{2}-\d{2}$/', $dob) ? $dob : ''; + $yearOnly = $dobValid !== '' ? (int) substr($dobValid, 0, 4) : 0; + + if ($nameUpper !== '' && $dobValid !== '') { + // Name + exact DOB [+ gender] — high confidence (score 3) + $q = $this->db->table('patient_master')->select($select) + ->where('UPPER(p_fname)', $nameUpper) + ->where('dob', $dobValid); + if ($genderDb !== null) { + $q->where('gender', $genderDb); + $rows = $q->limit(10)->get()->getResultArray(); + $append($rows, 'Name + DOB + Gender', 3); + } else { + $rows = $q->limit(10)->get()->getResultArray(); + $append($rows, 'Name + DOB', 3); + } + } + + if ($nameUpper !== '' && $yearOnly > 0) { + // Name + year of birth [+ gender] — medium confidence (score 2) + $q = $this->db->table('patient_master')->select($select) + ->where('UPPER(p_fname)', $nameUpper) + ->where('YEAR(dob)', $yearOnly); + if ($genderDb !== null) { + $q->where('gender', $genderDb); + $rows = $q->limit(10)->get()->getResultArray(); + $append($rows, 'Name + Year of Birth + Gender', 2); + } else { + $rows = $q->limit(10)->get()->getResultArray(); + $append($rows, 'Name + Year of Birth', 2); + } + } + + // Phone match — supplementary only (score 1), ambiguous for families + if (($identity['phone'] ?? '') !== '') { $rows = $this->db->table('patient_master')->select($select)->where('mphone1', $identity['phone'])->limit(10)->get()->getResultArray(); - $append($rows, 'Phone'); + $append($rows, 'Phone', 1); } - return array_values($bucket); + // Assign confidence label and sort by score descending + $confidenceLabel = static function (int $score): string { + if ($score >= 4) { return 'definitive'; } + if ($score === 3) { return 'high'; } + if ($score === 2) { return 'medium'; } + return 'low'; + }; + + $result = array_values($bucket); + usort($result, static function ($a, $b) { + return ($b['match_score'] ?? 0) <=> ($a['match_score'] ?? 0); + }); + + foreach ($result as &$row) { + $row['match_confidence'] = $confidenceLabel((int) ($row['match_score'] ?? 0)); + } + unset($row); + + return $result; } private function resolveFirstExistingColumn(array $fields, array $candidates): ?string @@ -3702,6 +3764,7 @@ class AbdmGateway extends BaseController // Accept txnId (API native) or txn_id (HMS form shorthand) $txnId = trim((string) ($body['txnId'] ?? $body['txn_id'] ?? $this->request->getPost('txnId') ?? $this->request->getPost('txn_id') ?? '')); $otp = trim((string) ($body['otp'] ?? $this->request->getPost('otp') ?? '')); + $requestedPid = (int) ($body['p_id'] ?? $this->request->getPost('p_id') ?? 0); if ($txnId === '' || $otp === '') { return $this->response->setJSON(['ok' => 0, 'error_text' => 'txnId and otp are required']); @@ -3713,7 +3776,11 @@ class AbdmGateway extends BaseController return $this->response->setStatusCode(500)->setJSON(['ok' => 0, 'error_text' => $e->getMessage()]); } - return $this->response->setJSON($result); + if (! empty($result['ok']) && (int) $result['ok'] === 1) { + $result = $this->enrichGatewayOtpVerifyResult($result, $requestedPid); + } + + return $this->response->setJSON($this->sanitizeGatewayOtpVerifyResponse($result)); } /** @@ -3759,6 +3826,7 @@ class AbdmGateway extends BaseController // Accept txnId (API native) or txn_id (HMS form shorthand) $txnId = trim((string) ($body['txnId'] ?? $body['txn_id'] ?? $this->request->getPost('txnId') ?? $this->request->getPost('txn_id') ?? '')); $otp = trim((string) ($body['otp'] ?? $this->request->getPost('otp') ?? '')); + $requestedPid = (int) ($body['p_id'] ?? $this->request->getPost('p_id') ?? 0); if ($txnId === '' || $otp === '') { return $this->response->setJSON(['ok' => 0, 'error_text' => 'txnId and otp are required']); @@ -3770,7 +3838,449 @@ class AbdmGateway extends BaseController return $this->response->setStatusCode(500)->setJSON(['ok' => 0, 'error_text' => $e->getMessage()]); } - return $this->response->setJSON($result); + if (! empty($result['ok']) && (int) $result['ok'] === 1) { + $result = $this->enrichGatewayOtpVerifyResult($result, $requestedPid); + } + + return $this->response->setJSON($this->sanitizeGatewayOtpVerifyResponse($result)); + } + + /** + * @param array $result + * @return array + */ + private function enrichGatewayOtpVerifyResult(array $result, int $requestedPid): array + { + $payload = []; + if (isset($result['data']) && is_array($result['data'])) { + $payload = $result['data']; + } elseif (is_array($result)) { + $payload = $result; + } + + $profile = $this->pickGatewayOtpProfile($payload); + $gatewayPatientPayload = is_array($payload['gateway_patient'] ?? null) ? $payload['gateway_patient'] : []; + + $abhaRaw = trim((string) ( + $profile['ABHANumber'] + ?? $profile['abha_number'] + ?? $profile['abha_id'] + ?? $payload['ABHANumber'] + ?? $payload['abha_number'] + ?? $payload['abha_id'] + ?? '' + )); + $abhaDigits = preg_replace('/\D/', '', $abhaRaw); + $abhaAddress = trim((string) ( + $profile['preferredAddress'] + ?? $profile['preferredAbhaAddress'] + ?? $profile['abha_address'] + ?? $payload['preferredAddress'] + ?? $payload['preferredAbhaAddress'] + ?? $payload['abha_address'] + ?? '' + )); + $gatewayMobile = preg_replace('/\D/', '', (string) ( + $profile['mobile'] + ?? $payload['mobile'] + ?? $payload['mobileNumber'] + ?? ($gatewayPatientPayload['mobile'] ?? '') + )); + + $gatewayAddress = trim((string) ( + ($profile['address'] ?? '') + ?: ($profile['address_line'] ?? '') + ?: ($payload['address'] ?? '') + ?: ($payload['address_line'] ?? '') + ?: ($payload['gateway_abha_profile']['address'] ?? '') + ?: ($gatewayPatientPayload['address_line'] ?? '') + ?: '' + )); + $gatewayCity = trim((string) ( + ($profile['city'] ?? '') + ?: ($payload['city'] ?? '') + ?: ($payload['gateway_abha_profile']['city'] ?? '') + ?: ($gatewayPatientPayload['city'] ?? '') + ?: '' + )); + $gatewayDistrict = trim((string) ( + ($profile['districtName'] ?? '') + ?: ($profile['district_name'] ?? '') + ?: ($payload['districtName'] ?? '') + ?: ($payload['district_name'] ?? '') + ?: ($payload['gateway_abha_profile']['district_name'] ?? '') + ?: ($gatewayPatientPayload['district'] ?? '') + ?: '' + )); + $gatewayState = trim((string) ( + ($profile['stateName'] ?? '') + ?: ($profile['state_name'] ?? '') + ?: ($payload['stateName'] ?? '') + ?: ($payload['state_name'] ?? '') + ?: ($payload['gateway_abha_profile']['state_name'] ?? '') + ?: ($gatewayPatientPayload['state_name'] ?? '') + ?: '' + )); + $gatewayZip = trim((string) ( + ($profile['pinCode'] ?? '') + ?: ($profile['pin_code'] ?? '') + ?: ($payload['pinCode'] ?? '') + ?: ($payload['pin_code'] ?? '') + ?: ($payload['pincode'] ?? '') + ?: ($payload['gateway_abha_profile']['pin_code'] ?? '') + ?: ($gatewayPatientPayload['pincode'] ?? '') + ?: '' + )); + $gatewayEmail = trim((string) ( + ($profile['email'] ?? '') + ?: ($payload['email'] ?? '') + ?: ($payload['gateway_abha_profile']['email'] ?? '') + ?: ($gatewayPatientPayload['email'] ?? '') + ?: '' + )); + + $verifiedStatus = trim((string) ( + ($payload['gateway_abha_profile']['status'] ?? '') + ?: ($profile['verifiedStatus'] ?? '') + ?: ($profile['status'] ?? '') + ?: ($payload['verifiedStatus'] ?? '') + ?: ($payload['status'] ?? '') + ?: '' + )); + $verificationType = trim((string) ( + ($payload['gateway_abha_profile']['abha_type'] ?? '') + ?: ($profile['verificationType'] ?? '') + ?: ($profile['abhaType'] ?? '') + ?: ($payload['verificationType'] ?? '') + ?: ($payload['abhaType'] ?? '') + ?: '' + )); + $kycVerified = $profile['kycVerified'] ?? $payload['kycVerified'] ?? null; + if ($kycVerified === null) { + $kycVerified = strtolower(trim((string) ($payload['gateway_abha_profile']['status'] ?? ''))) === 'verified' ? 1 : 0; + } + $mobileVerified = $profile['mobileVerified'] + ?? $payload['mobileVerified'] + ?? ($payload['gateway_abha_profile']['mobile_verified'] ?? null); + + $patientId = $this->resolveGatewayOtpPatientId($requestedPid, $payload, $abhaDigits, $gatewayMobile); + if ($patientId <= 0 || ! $this->db->tableExists('patient_master')) { + return $result; + } + + $fields = $this->db->getFieldNames('patient_master') ?? []; + $updates = []; + + $abhaField = $this->resolveFirstExistingColumn($fields, ['abha_id', 'abha_no', 'abha']); + if ($abhaField !== null && $abhaDigits !== '') { + $updates[$abhaField] = $abhaDigits; + } + if ($abhaAddress !== '' && in_array('abha_address', $fields, true)) { + $updates['abha_address'] = $abhaAddress; + } + + if ($gatewayMobile !== '' && in_array('mphone1', $fields, true)) { + $updates['mphone1'] = $gatewayMobile; + } + if ($gatewayAddress !== '' && in_array('add1', $fields, true)) { + $updates['add1'] = $gatewayAddress; + } + if ($gatewayCity !== '' && in_array('city', $fields, true)) { + $updates['city'] = $gatewayCity; + } + if ($gatewayDistrict !== '' && in_array('district', $fields, true)) { + $updates['district'] = $gatewayDistrict; + } + if ($gatewayState !== '' && in_array('state', $fields, true)) { + $updates['state'] = $gatewayState; + } + if ($gatewayZip !== '' && in_array('zip', $fields, true)) { + $updates['zip'] = $gatewayZip; + } + if ($gatewayEmail !== '' && in_array('email1', $fields, true)) { + $updates['email1'] = $gatewayEmail; + } + + if ($verifiedStatus !== '' && in_array('abha_verified_status', $fields, true)) { + $updates['abha_verified_status'] = strtoupper($verifiedStatus); + } + if ($verificationType !== '' && in_array('abha_verification_type', $fields, true)) { + $updates['abha_verification_type'] = strtoupper($verificationType); + } + if ($kycVerified !== null && in_array('abha_kyc_verified', $fields, true)) { + $updates['abha_kyc_verified'] = $this->toDbBool($kycVerified); + } + if ($mobileVerified !== null && in_array('abha_mobile_verified', $fields, true)) { + $updates['abha_mobile_verified'] = $this->toDbBool($mobileVerified); + } + + if (in_array('abdm_linked_at', $fields, true) && ($abhaDigits !== '' || $abhaAddress !== '')) { + $updates['abdm_linked_at'] = date('Y-m-d H:i:s'); + } + + if ($updates !== []) { + $this->db->table('patient_master')->where('id', $patientId)->update($updates); + } + + $result['patient_id'] = $patientId; + if (isset($result['data']) && is_array($result['data'])) { + $result['data']['patient_id'] = $patientId; + } + + return $result; + } + + /** + * @param array $payload + */ + private function resolveGatewayOtpPatientId(int $requestedPid, array $payload, string $abhaDigits, string $mobile): int + { + if (! $this->db->tableExists('patient_master')) { + return 0; + } + + if ($requestedPid > 0) { + $row = $this->db->table('patient_master')->select('id')->where('id', $requestedPid)->get(1)->getRowArray(); + if ($row !== null) { + return (int) ($row['id'] ?? 0); + } + } + + $fields = $this->db->getFieldNames('patient_master') ?? []; + $abhaField = $this->resolveFirstExistingColumn($fields, ['abha_id', 'abha_no', 'abha']); + if ($abhaField !== null && $abhaDigits !== '') { + $row = $this->db->table('patient_master')->select('id')->where($abhaField, $abhaDigits)->get(1)->getRowArray(); + if ($row !== null) { + return (int) ($row['id'] ?? 0); + } + } + + if ($mobile !== '' && in_array('mphone1', $fields, true)) { + $row = $this->db->table('patient_master')->select('id')->where('mphone1', $mobile)->get(1)->getRowArray(); + if ($row !== null) { + return (int) ($row['id'] ?? 0); + } + } + + $fallbackIds = [ + (int) ($payload['patient_id'] ?? 0), + (int) ($payload['gateway_patient']['id'] ?? 0), + ]; + foreach ($fallbackIds as $pid) { + if ($pid <= 0) { + continue; + } + $row = $this->db->table('patient_master')->select('id')->where('id', $pid)->get(1)->getRowArray(); + if ($row !== null) { + return (int) ($row['id'] ?? 0); + } + } + + return 0; + } + + /** + * @param array $payload + * @return array + */ + private function pickGatewayOtpProfile(array $payload): array + { + $candidates = [ + $payload['ABHAProfile'] ?? null, + $payload['gateway_abha_profile'] ?? null, + $payload['profile'] ?? null, + $payload['accounts'][0] ?? null, + $payload['gateway_patient'] ?? null, + $payload, + ]; + + foreach ($candidates as $candidate) { + if (! is_array($candidate)) { + continue; + } + + foreach (['ABHANumber', 'abha_number', 'abha_id', 'preferredAddress', 'preferredAbhaAddress', 'abha_address', 'fullName', 'full_name', 'firstName', 'dob', 'date_of_birth'] as $key) { + if (array_key_exists($key, $candidate) && trim((string) $candidate[$key]) !== '') { + return $candidate; + } + } + } + + return []; + } + + private function toDbBool($value): int + { + if (is_bool($value)) { + return $value ? 1 : 0; + } + + $v = strtolower(trim((string) $value)); + if ($v === '1' || $v === 'true' || $v === 'yes' || $v === 'y') { + return 1; + } + + return 0; + } + + /** + * Return only safe/required fields for gateway ABHA OTP verification APIs. + * + * @param array $result + * @return array + */ + private function sanitizeGatewayOtpVerifyResponse(array $result): array + { + $ok = ! empty($result['ok']) && (int) $result['ok'] === 1; + if (! $ok) { + return [ + 'ok' => 0, + 'error_text' => (string) ($result['error_text'] ?? $result['message'] ?? $result['data']['message'] ?? 'OTP verification failed'), + 'request_id' => (string) ($result['request_id'] ?? ''), + ]; + } + + $payload = []; + if (isset($result['data']) && is_array($result['data'])) { + $payload = $result['data']; + } elseif (is_array($result)) { + $payload = $result; + } + + $profile = $this->pickGatewayOtpProfile($payload); + $gatewayPatientPayload = is_array($payload['gateway_patient'] ?? null) ? $payload['gateway_patient'] : []; + + $txnId = (string) ($payload['txnId'] ?? $payload['txn_id'] ?? $result['txnId'] ?? $result['txn_id'] ?? ''); + $abhaRaw = (string) ($payload['ABHANumber'] ?? $payload['abha_number'] ?? $payload['abha_id'] ?? $profile['ABHANumber'] ?? $profile['abha_number'] ?? $profile['abha_id'] ?? ''); + $abhaDigits = preg_replace('/\D/', '', $abhaRaw); + $abhaAddress = (string) ($payload['preferredAbhaAddress'] ?? $payload['abha_address'] ?? $profile['preferredAbhaAddress'] ?? $profile['abha_address'] ?? $profile['preferredAddress'] ?? ''); + $name = trim((string) ( + $payload['name'] + ?? $payload['full_name'] + ?? $profile['name'] + ?? $profile['fullName'] + ?? $profile['full_name'] + ?? trim(implode(' ', array_filter([ + (string) ($profile['firstName'] ?? ''), + (string) ($profile['middleName'] ?? ''), + (string) ($profile['lastName'] ?? ''), + ]))) + )); + $mobile = (string) ($payload['mobile'] ?? $payload['mobileNumber'] ?? $profile['mobile'] ?? ($gatewayPatientPayload['mobile'] ?? '')); + $gender = (string) ($payload['gender'] ?? $profile['gender'] ?? ($gatewayPatientPayload['gender'] ?? '')); + $dob = (string) ($payload['dob'] ?? $payload['date_of_birth'] ?? $profile['dob'] ?? $profile['date_of_birth'] ?? ($gatewayPatientPayload['date_of_birth'] ?? '')); + $address = (string) ( + ($payload['address'] ?? '') + ?: ($payload['address_line'] ?? '') + ?: ($profile['address'] ?? '') + ?: ($profile['address_line'] ?? '') + ?: ($payload['gateway_abha_profile']['address'] ?? '') + ?: ($gatewayPatientPayload['address_line'] ?? '') + ?: '' + ); + $pinCode = (string) ( + ($payload['pinCode'] ?? '') + ?: ($payload['pin_code'] ?? '') + ?: ($payload['pincode'] ?? '') + ?: ($profile['pinCode'] ?? '') + ?: ($profile['pin_code'] ?? '') + ?: ($payload['gateway_abha_profile']['pin_code'] ?? '') + ?: ($gatewayPatientPayload['pincode'] ?? '') + ?: '' + ); + $stateCode = (string) ( + ($payload['stateCode'] ?? '') + ?: ($payload['state_code'] ?? '') + ?: ($profile['stateCode'] ?? '') + ?: ($profile['state_code'] ?? '') + ?: ($payload['gateway_abha_profile']['state_code'] ?? '') + ?: ($gatewayPatientPayload['state_code'] ?? '') + ?: '' + ); + $stateName = (string) ( + ($payload['stateName'] ?? '') + ?: ($payload['state_name'] ?? '') + ?: ($profile['stateName'] ?? '') + ?: ($profile['state_name'] ?? '') + ?: ($payload['gateway_abha_profile']['state_name'] ?? '') + ?: ($gatewayPatientPayload['state_name'] ?? '') + ?: '' + ); + $districtCode = (string) ( + ($payload['districtCode'] ?? '') + ?: ($payload['district_code'] ?? '') + ?: ($profile['districtCode'] ?? '') + ?: ($profile['district_code'] ?? '') + ?: ($payload['gateway_abha_profile']['district_code'] ?? '') + ?: '' + ); + $districtName = (string) ( + ($payload['districtName'] ?? '') + ?: ($payload['district_name'] ?? '') + ?: ($profile['districtName'] ?? '') + ?: ($profile['district_name'] ?? '') + ?: ($payload['gateway_abha_profile']['district_name'] ?? '') + ?: ($gatewayPatientPayload['district'] ?? '') + ?: '' + ); + + $matchedPatientId = (int) ($result['patient_id'] ?? $payload['patient_id'] ?? 0); + + return [ + 'ok' => 1, + 'request_id' => (string) ($result['request_id'] ?? ''), + 'message' => (string) ($payload['message'] ?? $result['message'] ?? 'ABHA OTP verified successfully'), + 'txn_id' => $txnId, + 'abha_number' => $abhaDigits, + 'abha_address' => $abhaAddress, + 'name' => $name, + 'mobile' => $mobile, + 'gender' => $gender, + 'dob' => $dob, + 'address' => $address, + 'pin_code' => $pinCode, + 'state_code' => $stateCode, + 'state_name' => $stateName, + 'district_code' => $districtCode, + 'district_name' => $districtName, + 'patient_id' => $matchedPatientId > 0 ? $matchedPatientId : null, + 'data' => [ + 'txnId' => $txnId, + 'abha_number' => $abhaDigits, + 'name' => $name, + 'mobile' => $mobile, + 'gender' => $gender, + 'dob' => $dob, + 'address' => $address, + 'pin_code' => $pinCode, + 'state_code' => $stateCode, + 'state_name' => $stateName, + 'district_code' => $districtCode, + 'district_name' => $districtName, + 'patient_id' => $matchedPatientId > 0 ? $matchedPatientId : null, + 'profile' => [ + 'ABHANumber' => $abhaDigits, + 'abha_number' => $abhaDigits, + 'abha_id' => $abhaDigits, + 'preferredAbhaAddress' => $abhaAddress, + 'preferredAddress' => $abhaAddress, + 'name' => $name, + 'full_name' => $name, + 'gender' => $gender, + 'dob' => $dob, + 'mobile' => $mobile, + 'address' => $address, + 'pinCode' => $pinCode, + 'stateCode' => $stateCode, + 'stateName' => $stateName, + 'districtCode' => $districtCode, + 'districtName' => $districtName, + 'firstName' => (string) ($profile['firstName'] ?? ''), + 'middleName' => (string) ($profile['middleName'] ?? ''), + 'lastName' => (string) ($profile['lastName'] ?? ''), + ], + ], + ]; } // ========================================================================= diff --git a/app/Controllers/AbdmOpdQueue.php b/app/Controllers/AbdmOpdQueue.php index 4404efd..0b6555d 100644 --- a/app/Controllers/AbdmOpdQueue.php +++ b/app/Controllers/AbdmOpdQueue.php @@ -307,7 +307,7 @@ class AbdmOpdQueue extends BaseController $abhaField = $this->resolveFirstExistingColumn($fields, ['abha_id', 'abha_no', 'abha', 'abha_address']); $aadhaarField = $this->resolveFirstExistingColumn($fields, ['udai', 'aadhar_no', 'aadhaar_no', 'aadhaar', 'adhar_no']); - $matches = $this->findPatientMatches($db, $abhaField, $aadhaarField, $abhaRaw, $abhaAddr, $aadhaarRaw, $phone); + $matches = $this->findPatientMatches($db, $abhaField, $aadhaarField, $abhaRaw, $abhaAddr, $aadhaarRaw, $phone, $name, $gender, $dob); if ($action === 'check') { return $this->response->setJSON([ @@ -494,9 +494,12 @@ class AbdmOpdQueue extends BaseController string $abhaRaw, string $abhaAddr, string $aadhaarRaw, - string $phone + string $phone, + string $name = '', + string $gender = '', + string $dob = '' ): array { - $select = 'id,p_code,p_fname,mphone1'; + $select = 'id,p_code,p_fname,mphone1,dob,gender'; if ($abhaField) { $select .= ',' . $abhaField . ' AS patient_abha'; } @@ -505,7 +508,7 @@ class AbdmOpdQueue extends BaseController } $bucket = []; - $append = static function (array $rows, string $reason) use (&$bucket): void { + $append = static function (array $rows, string $reason, int $score) use (&$bucket): void { foreach ($rows as $row) { $id = (int) ($row['id'] ?? 0); if ($id <= 0) { @@ -513,35 +516,95 @@ class AbdmOpdQueue extends BaseController } if (! isset($bucket[$id])) { $row['match_reasons'] = []; + $row['match_score'] = 0; $bucket[$id] = $row; } if (! in_array($reason, $bucket[$id]['match_reasons'], true)) { $bucket[$id]['match_reasons'][] = $reason; } + if ($score > ($bucket[$id]['match_score'] ?? 0)) { + $bucket[$id]['match_score'] = $score; + } } }; + // Definitive identifier matches (score 4) if ($abhaField && $abhaRaw !== '') { $rows = $db->table('patient_master')->select($select)->where($abhaField, $abhaRaw)->limit(10)->get()->getResultArray(); - $append($rows, 'ABHA No'); + $append($rows, 'ABHA No', 4); } if ($abhaField && $abhaAddr !== '' && $abhaAddr !== $abhaRaw) { $rows = $db->table('patient_master')->select($select)->where($abhaField, $abhaAddr)->limit(10)->get()->getResultArray(); - $append($rows, 'ABHA Address'); + $append($rows, 'ABHA Address', 4); } if ($aadhaarField && $aadhaarRaw !== '') { $rows = $db->table('patient_master')->select($select)->where($aadhaarField, $aadhaarRaw)->limit(10)->get()->getResultArray(); - $append($rows, 'Aadhaar'); + $append($rows, 'Aadhaar', 4); } + // Demographic matches — primary criteria + $nameUpper = strtoupper(trim($name)); + $genderDb = ($gender === 'F' || $gender === '2') ? 2 : ($gender !== '' ? 1 : null); + $dobValid = preg_match('/^\d{4}-\d{2}-\d{2}$/', $dob) ? $dob : ''; + $yearOnly = $dobValid !== '' ? (int) substr($dobValid, 0, 4) : 0; + + if ($nameUpper !== '' && $dobValid !== '') { + // Name + exact DOB [+ gender] — high confidence (score 3) + $q = $db->table('patient_master')->select($select) + ->where('UPPER(p_fname)', $nameUpper) + ->where('dob', $dobValid); + if ($genderDb !== null) { + $q->where('gender', $genderDb); + $rows = $q->limit(10)->get()->getResultArray(); + $append($rows, 'Name + DOB + Gender', 3); + } else { + $rows = $q->limit(10)->get()->getResultArray(); + $append($rows, 'Name + DOB', 3); + } + } + + if ($nameUpper !== '' && $yearOnly > 0) { + // Name + year of birth [+ gender] — medium confidence (score 2) + $q = $db->table('patient_master')->select($select) + ->where('UPPER(p_fname)', $nameUpper) + ->where('YEAR(dob)', $yearOnly); + if ($genderDb !== null) { + $q->where('gender', $genderDb); + $rows = $q->limit(10)->get()->getResultArray(); + $append($rows, 'Name + Year of Birth + Gender', 2); + } else { + $rows = $q->limit(10)->get()->getResultArray(); + $append($rows, 'Name + Year of Birth', 2); + } + } + + // Phone match — supplementary only (score 1), ambiguous for families if ($phone !== '') { $rows = $db->table('patient_master')->select($select)->where('mphone1', $phone)->limit(10)->get()->getResultArray(); - $append($rows, 'Phone'); + $append($rows, 'Phone', 1); } - return array_values($bucket); + // Assign human-readable confidence label and sort by score descending + $confidenceLabel = static function (int $score): string { + if ($score >= 4) { return 'definitive'; } + if ($score === 3) { return 'high'; } + if ($score === 2) { return 'medium'; } + return 'low'; + }; + + $result = array_values($bucket); + usort($result, static function ($a, $b) { + return ($b['match_score'] ?? 0) <=> ($a['match_score'] ?? 0); + }); + + foreach ($result as &$row) { + $row['match_confidence'] = $confidenceLabel((int) ($row['match_score'] ?? 0)); + } + unset($row); + + return $result; } private function createPatientFromToken( diff --git a/app/Controllers/Abha.php b/app/Controllers/Abha.php index aa7293d..5c9ad1a 100644 --- a/app/Controllers/Abha.php +++ b/app/Controllers/Abha.php @@ -86,16 +86,29 @@ class Abha extends BaseController $profile = $this->pickGatewayAbhaProfile($payload); $abhaNum = (string) ($profile['ABHANumber'] ?? $profile['abha_id'] ?? $payload['ABHANumber'] ?? $payload['abha_id'] ?? ''); - $abhaAddress = (string) ($profile['preferredAbhaAddress'] ?? $profile['abha_address'] ?? $payload['preferredAbhaAddress'] ?? $payload['abha_address'] ?? ''); + $abhaAddress = (string) ( + ($profile['preferredAddress'] ?? '') + ?: ($profile['preferredAbhaAddress'] ?? '') + ?: ($profile['abha_address'] ?? '') + ?: ($payload['preferredAddress'] ?? '') + ?: ($payload['preferredAbhaAddress'] ?? '') + ?: ($payload['abha_address'] ?? '') + ?: '' + ); $name = (string) ($profile['name'] ?? $profile['fullName'] ?? $profile['full_name'] ?? $payload['name'] ?? $payload['full_name'] ?? ''); $photo = (string) ($profile['profilePhoto'] ?? $profile['profile_photo'] ?? $payload['profilePhoto'] ?? $payload['profile_photo'] ?? ''); $mobile = (string) ($profile['mobile'] ?? $payload['mobile'] ?? $payload['mobileNumber'] ?? ''); $profileGender = (string) ($profile['gender'] ?? $payload['gender'] ?? ''); $profileDob = (string) ($profile['dob'] ?? $profile['date_of_birth'] ?? $payload['dob'] ?? $payload['date_of_birth'] ?? ''); - $verifiedStatus = (string) ($profile['verifiedStatus'] ?? $payload['verifiedStatus'] ?? ''); - $verificationType = (string) ($profile['verificationType'] ?? $payload['verificationType'] ?? ''); + $verifiedStatus = (string) (($payload['gateway_abha_profile']['status'] ?? '') ?: ($profile['verifiedStatus'] ?? '') ?: ($profile['status'] ?? '') ?: ($payload['verifiedStatus'] ?? '') ?: ($payload['status'] ?? '')); + $verificationType = (string) (($payload['gateway_abha_profile']['abha_type'] ?? '') ?: ($profile['verificationType'] ?? '') ?: ($profile['abhaType'] ?? '') ?: ($payload['verificationType'] ?? '') ?: ($payload['abhaType'] ?? '')); $kycVerified = $profile['kycVerified'] ?? $payload['kycVerified'] ?? null; $mobileVerified = $profile['mobileVerified'] ?? $payload['mobileVerified'] ?? null; + $address = (string) (($profile['address'] ?? '') ?: ($profile['address_line'] ?? '') ?: ($payload['address'] ?? '') ?: ($payload['address_line'] ?? '') ?: ($payload['gateway_abha_profile']['address'] ?? '') ?: ($payload['gateway_patient']['address_line'] ?? '')); + $zip = (string) (($profile['pinCode'] ?? '') ?: ($profile['pin_code'] ?? '') ?: ($payload['pinCode'] ?? '') ?: ($payload['pin_code'] ?? '') ?: ($payload['gateway_abha_profile']['pin_code'] ?? '') ?: ($payload['gateway_patient']['pincode'] ?? '')); + $stateName = (string) (($profile['stateName'] ?? '') ?: ($profile['state_name'] ?? '') ?: ($payload['stateName'] ?? '') ?: ($payload['state_name'] ?? '') ?: ($payload['gateway_abha_profile']['state_name'] ?? '') ?: ($payload['gateway_patient']['state_name'] ?? '')); + $districtName = (string) (($profile['districtName'] ?? '') ?: ($profile['district_name'] ?? '') ?: ($payload['districtName'] ?? '') ?: ($payload['district_name'] ?? '') ?: ($payload['gateway_abha_profile']['district_name'] ?? '') ?: ($payload['gateway_patient']['district'] ?? '')); + $email = (string) (($profile['email'] ?? '') ?: ($payload['email'] ?? '') ?: ($payload['gateway_abha_profile']['email'] ?? '') ?: ($payload['gateway_patient']['email'] ?? '')); $patientInfo = $this->autoCreateOrFindPatient( $abhaNum, @@ -110,6 +123,11 @@ class Abha extends BaseController 'verification_type' => $verificationType, 'kyc_verified' => $kycVerified, 'mobile_verified' => $mobileVerified, + 'address' => $address, + 'district' => $districtName, + 'state' => $stateName, + 'zip' => $zip, + 'email' => $email, ] ); @@ -198,16 +216,29 @@ class Abha extends BaseController $profile = $this->pickGatewayAbhaProfile($payload); $abhaNum = (string) ($profile['ABHANumber'] ?? $profile['abha_id'] ?? $payload['ABHANumber'] ?? $payload['abha_id'] ?? ''); - $abhaAddress = (string) ($profile['preferredAbhaAddress'] ?? $profile['abha_address'] ?? $payload['preferredAbhaAddress'] ?? $payload['abha_address'] ?? ''); + $abhaAddress = (string) ( + ($profile['preferredAddress'] ?? '') + ?: ($profile['preferredAbhaAddress'] ?? '') + ?: ($profile['abha_address'] ?? '') + ?: ($payload['preferredAddress'] ?? '') + ?: ($payload['preferredAbhaAddress'] ?? '') + ?: ($payload['abha_address'] ?? '') + ?: '' + ); $name = (string) ($profile['name'] ?? $profile['fullName'] ?? $profile['full_name'] ?? $payload['name'] ?? $payload['full_name'] ?? ''); $photo = (string) ($profile['profilePhoto'] ?? $profile['profile_photo'] ?? $payload['profilePhoto'] ?? $payload['profile_photo'] ?? ''); $gender = (string) ($profile['gender'] ?? $payload['gender'] ?? ''); $dob = (string) ($profile['dob'] ?? $profile['date_of_birth'] ?? $payload['dob'] ?? $payload['date_of_birth'] ?? ''); $mobile = (string) ($profile['mobile'] ?? $payload['mobile'] ?? $payload['mobileNumber'] ?? ''); - $verifiedStatus = (string) ($profile['verifiedStatus'] ?? $payload['verifiedStatus'] ?? ''); - $verificationType = (string) ($profile['verificationType'] ?? $payload['verificationType'] ?? ''); + $verifiedStatus = (string) (($payload['gateway_abha_profile']['status'] ?? '') ?: ($profile['verifiedStatus'] ?? '') ?: ($profile['status'] ?? '') ?: ($payload['verifiedStatus'] ?? '') ?: ($payload['status'] ?? '')); + $verificationType = (string) (($payload['gateway_abha_profile']['abha_type'] ?? '') ?: ($profile['verificationType'] ?? '') ?: ($profile['abhaType'] ?? '') ?: ($payload['verificationType'] ?? '') ?: ($payload['abhaType'] ?? '')); $kycVerified = $profile['kycVerified'] ?? $payload['kycVerified'] ?? null; $mobileVerified = $profile['mobileVerified'] ?? $payload['mobileVerified'] ?? null; + $address = (string) (($profile['address'] ?? '') ?: ($profile['address_line'] ?? '') ?: ($payload['address'] ?? '') ?: ($payload['address_line'] ?? '') ?: ($payload['gateway_abha_profile']['address'] ?? '') ?: ($payload['gateway_patient']['address_line'] ?? '')); + $zip = (string) (($profile['pinCode'] ?? '') ?: ($profile['pin_code'] ?? '') ?: ($payload['pinCode'] ?? '') ?: ($payload['pin_code'] ?? '') ?: ($payload['gateway_abha_profile']['pin_code'] ?? '') ?: ($payload['gateway_patient']['pincode'] ?? '')); + $stateName = (string) (($profile['stateName'] ?? '') ?: ($profile['state_name'] ?? '') ?: ($payload['stateName'] ?? '') ?: ($payload['state_name'] ?? '') ?: ($payload['gateway_abha_profile']['state_name'] ?? '') ?: ($payload['gateway_patient']['state_name'] ?? '')); + $districtName = (string) (($profile['districtName'] ?? '') ?: ($profile['district_name'] ?? '') ?: ($payload['districtName'] ?? '') ?: ($payload['district_name'] ?? '') ?: ($payload['gateway_abha_profile']['district_name'] ?? '') ?: ($payload['gateway_patient']['district'] ?? '')); + $email = (string) (($profile['email'] ?? '') ?: ($payload['email'] ?? '') ?: ($payload['gateway_abha_profile']['email'] ?? '') ?: ($payload['gateway_patient']['email'] ?? '')); $patientInfo = $this->autoCreateOrFindPatient( $abhaNum, @@ -222,6 +253,11 @@ class Abha extends BaseController 'verification_type' => $verificationType, 'kyc_verified' => $kycVerified, 'mobile_verified' => $mobileVerified, + 'address' => $address, + 'district' => $districtName, + 'state' => $stateName, + 'zip' => $zip, + 'email' => $email, ] ); @@ -498,6 +534,31 @@ class Abha extends BaseController $target['abha_mobile_verified'] = $this->toDbBool($abhaMeta['mobile_verified']); } + $address = trim((string) ($abhaMeta['address'] ?? '')); + if ($address !== '' && in_array('add1', $fields, true)) { + $target['add1'] = $address; + } + + $district = trim((string) ($abhaMeta['district'] ?? '')); + if ($district !== '' && in_array('district', $fields, true)) { + $target['district'] = strtoupper($district); + } + + $state = trim((string) ($abhaMeta['state'] ?? '')); + if ($state !== '' && in_array('state', $fields, true)) { + $target['state'] = strtoupper($state); + } + + $zip = trim((string) ($abhaMeta['zip'] ?? '')); + if ($zip !== '' && in_array('zip', $fields, true)) { + $target['zip'] = $zip; + } + + $email = trim((string) ($abhaMeta['email'] ?? '')); + if ($email !== '' && in_array('email1', $fields, true)) { + $target['email1'] = $email; + } + $profilePhoto = trim((string) ($abhaMeta['profile_photo'] ?? '')); if ($profilePhoto !== '' && in_array('abha_profile_photo_base64', $fields, true)) { $target['abha_profile_photo_base64'] = $profilePhoto; @@ -609,10 +670,11 @@ class Abha extends BaseController private function pickGatewayAbhaProfile(array $payload): array { $candidates = [ - $payload['gateway_patient'] ?? null, $payload['ABHAProfile'] ?? null, - $payload['accounts'][0] ?? null, + $payload['gateway_abha_profile'] ?? null, $payload['profile'] ?? null, + $payload['accounts'][0] ?? null, + $payload['gateway_patient'] ?? null, $payload, ]; @@ -630,7 +692,7 @@ class Abha extends BaseController */ private function looksLikeGatewayAbhaProfile(array $profile): bool { - foreach (['ABHANumber', 'abha_id', 'preferredAbhaAddress', 'abha_address', 'name', 'fullName', 'full_name', 'gender', 'dob', 'date_of_birth', 'profilePhoto', 'profile_photo'] as $key) { + foreach (['ABHANumber', 'abha_id', 'preferredAddress', 'preferredAbhaAddress', 'abha_address', 'name', 'fullName', 'full_name', 'gender', 'dob', 'date_of_birth', 'profilePhoto', 'profile_photo'] as $key) { if (array_key_exists($key, $profile) && trim((string) $profile[$key]) !== '') { return true; } diff --git a/app/Controllers/Patient.php b/app/Controllers/Patient.php index f9aaaa6..a29e9d9 100644 --- a/app/Controllers/Patient.php +++ b/app/Controllers/Patient.php @@ -21,6 +21,16 @@ class Patient extends BaseController ->findAll(); $data['user'] = $user; + + $data['india_states'] = []; + if ($this->db->tableExists('india_state')) { + $data['india_states'] = $this->db->table('india_state') + ->select('id, state_name') + ->orderBy('state_name', 'ASC') + ->get() + ->getResultArray(); + } + return view('billing/Patient_V', $data); } @@ -357,7 +367,7 @@ class Patient extends BaseController $result = $this->enrichAbhaVerifyResult($result, $requestedPid); } - return $this->response->setJSON($result); + return $this->response->setJSON($this->sanitizeAbhaVerifyApiResponse($result)); } public function abhaMobileGenerateOtp() @@ -409,7 +419,7 @@ class Patient extends BaseController $result = $this->enrichAbhaVerifyResult($result, $requestedPid); } - return $this->response->setJSON($result); + return $this->response->setJSON($this->sanitizeAbhaVerifyApiResponse($result)); } @@ -1237,6 +1247,28 @@ class Patient extends BaseController return $this->response->setJSON($patientModel->getCitySuggestions($q)); } + public function district_list() + { + $q = trim((string) $this->request->getGet('term')); + if ($q === '' || strlen($q) < 2) { + return $this->response->setJSON([]); + } + + if (! $this->db->tableExists('abdm_district_master')) { + return $this->response->setJSON([]); + } + + $rows = $this->db->table('abdm_district_master') + ->select('district_name AS value, district_name AS label, state_code') + ->like('district_name', $q) + ->orderBy('district_name', 'ASC') + ->limit(15) + ->get() + ->getResultArray(); + + return $this->response->setJSON($rows); + } + public function get_name() { $q = strtolower((string) $this->request->getGet('term')); @@ -1385,14 +1417,104 @@ class Patient extends BaseController } $profile = $this->pickGatewayAbhaProfile($payload); + $gatewayPatientPayload = is_array($payload['gateway_patient'] ?? null) ? $payload['gateway_patient'] : []; $abhaNumberRaw = trim((string) ($profile['ABHANumber'] ?? $profile['abha_id'] ?? $payload['ABHANumber'] ?? $payload['abha_id'] ?? '')); $abhaDigits = preg_replace('/\D/', '', $abhaNumberRaw); - $abhaAddress = trim((string) ($profile['preferredAbhaAddress'] ?? $profile['abha_address'] ?? $payload['abha_address'] ?? $payload['preferredAbhaAddress'] ?? '')); + $abhaAddress = trim((string) ( + $profile['preferredAddress'] + ?? $profile['preferredAbhaAddress'] + ?? $profile['abha_address'] + ?? $payload['abha_address'] + ?? $payload['preferredAddress'] + ?? $payload['preferredAbhaAddress'] + ?? '' + )); $gatewayName = trim((string) ($profile['name'] ?? $profile['fullName'] ?? $profile['full_name'] ?? $payload['name'] ?? $payload['full_name'] ?? '')); $gatewayMobile = trim((string) ($profile['mobile'] ?? $payload['mobile'] ?? $payload['mobileNumber'] ?? '')); $gatewayDob = trim((string) ($profile['dob'] ?? $profile['date_of_birth'] ?? $payload['dob'] ?? $payload['date_of_birth'] ?? '')); $gatewayGender = trim((string) ($profile['gender'] ?? $payload['gender'] ?? '')); $gatewayPhoto = trim((string) ($profile['profilePhoto'] ?? $profile['profile_photo'] ?? $payload['profilePhoto'] ?? $payload['profile_photo'] ?? '')); + $gatewayEmail = trim((string) ($profile['email'] ?? $payload['email'] ?? '')); + $gatewayAddress = trim((string) ( + $profile['address'] + ?? $profile['address_line'] + ?? $payload['address'] + ?? $payload['address_line'] + ?? ($gatewayPatientPayload['address_line'] ?? '') + ?? '' + )); + $gatewayZip = trim((string) ( + $profile['pinCode'] + ?? $profile['pin_code'] + ?? $payload['pinCode'] + ?? $payload['pin_code'] + ?? $payload['pincode'] + ?? ($gatewayPatientPayload['pincode'] ?? '') + ?? '' + )); + $gatewayStateCode = trim((string) ( + $profile['stateCode'] + ?? $profile['state_code'] + ?? $payload['stateCode'] + ?? $payload['state_code'] + ?? ($gatewayPatientPayload['state_code'] ?? '') + ?? '' + )); + $gatewayStateName = trim((string) ( + $profile['stateName'] + ?? $profile['state_name'] + ?? $payload['stateName'] + ?? $payload['state_name'] + ?? ($gatewayPatientPayload['state_name'] ?? '') + ?? '' + )); + $gatewayDistrictCode = trim((string) ( + $profile['districtCode'] + ?? $profile['district_code'] + ?? $payload['districtCode'] + ?? $payload['district_code'] + ?? ($payload['gateway_abha_profile']['district_code'] ?? '') + ?? '' + )); + $gatewayDistrictName = trim((string) ( + $profile['districtName'] + ?? $profile['district_name'] + ?? $payload['districtName'] + ?? $payload['district_name'] + ?? ($payload['gateway_abha_profile']['district_name'] ?? '') + ?? ($gatewayPatientPayload['district'] ?? '') + ?? '' + )); + $verifiedStatus = trim((string) ( + ($payload['gateway_abha_profile']['status'] ?? '') + ?: ($profile['verifiedStatus'] ?? '') + ?: ($profile['status'] ?? '') + ?: ($profile['abhaStatus'] ?? '') + ?: ($payload['verifiedStatus'] ?? '') + ?: ($payload['status'] ?? '') + ?: '' + )); + $verificationType = trim((string) ( + ($payload['gateway_abha_profile']['abha_type'] ?? '') + ?: ($profile['verificationType'] ?? '') + ?: ($profile['abhaType'] ?? '') + ?: ($payload['verificationType'] ?? '') + ?: ($payload['abhaType'] ?? '') + ?: '' + )); + $kycVerified = $profile['kycVerified'] ?? $payload['kycVerified'] ?? null; + if ($kycVerified === null) { + $kycVerified = strtolower(trim((string) ($payload['gateway_abha_profile']['status'] ?? ''))) === 'verified' ? 1 : 0; + } + $mobileVerified = $profile['mobileVerified'] + ?? $payload['mobileVerified'] + ?? ($payload['gateway_abha_profile']['mobile_verified'] ?? null); + $gatewayCity = trim((string) ($profile['city'] ?? $payload['city'] ?? ($gatewayPatientPayload['city'] ?? ''))); + if ($gatewayCity === '') { + $gatewayCity = $this->extractCityFromAddress($gatewayAddress, $gatewayDistrictName, $gatewayStateName); + } + + $this->upsertAbdmLocationMasters($gatewayStateCode, $gatewayStateName, $gatewayDistrictCode, $gatewayDistrictName); $patient = $this->findPatientForAbhaProfile($requestedPid, $abhaDigits, $gatewayMobile); $patientId = (int) ($patient['id'] ?? 0); @@ -1415,6 +1537,39 @@ class Patient extends BaseController } $pmFields = $this->db->getFieldNames('patient_master') ?? []; + if ($gatewayMobile !== '' && in_array('mphone1', $pmFields, true)) { + $updates['mphone1'] = preg_replace('/\D/', '', $gatewayMobile); + } + if ($gatewayAddress !== '' && in_array('add1', $pmFields, true)) { + $updates['add1'] = $gatewayAddress; + } + if ($gatewayCity !== '' && in_array('city', $pmFields, true)) { + $updates['city'] = $gatewayCity; + } + if ($gatewayDistrictName !== '' && in_array('district', $pmFields, true)) { + $updates['district'] = $gatewayDistrictName; + } + if ($gatewayStateName !== '' && in_array('state', $pmFields, true)) { + $updates['state'] = $gatewayStateName; + } + if ($gatewayZip !== '' && in_array('zip', $pmFields, true)) { + $updates['zip'] = $gatewayZip; + } + if ($gatewayEmail !== '' && in_array('email1', $pmFields, true)) { + $updates['email1'] = $gatewayEmail; + } + if ($verifiedStatus !== '' && in_array('abha_verified_status', $pmFields, true)) { + $updates['abha_verified_status'] = $verifiedStatus; + } + if ($verificationType !== '' && in_array('abha_verification_type', $pmFields, true)) { + $updates['abha_verification_type'] = $verificationType; + } + if ($kycVerified !== null && in_array('abha_kyc_verified', $pmFields, true)) { + $updates['abha_kyc_verified'] = (int) ((string) $kycVerified === '1' || (string) $kycVerified === 'true' || (string) $kycVerified === 'yes'); + } + if ($mobileVerified !== null && in_array('abha_mobile_verified', $pmFields, true)) { + $updates['abha_mobile_verified'] = (int) ((string) $mobileVerified === '1' || (string) $mobileVerified === 'true' || (string) $mobileVerified === 'yes'); + } if ($abhaAddress !== '' && in_array('abha_address', $pmFields, true)) { $updates['abha_address'] = $abhaAddress; } @@ -1442,6 +1597,13 @@ class Patient extends BaseController 'mobile' => $gatewayMobile, 'dob' => $gatewayDob, 'gender' => $gatewayGender, + 'address' => $gatewayAddress, + 'city' => $gatewayCity, + 'zip' => $gatewayZip, + 'state_code' => $gatewayStateCode, + 'state_name' => $gatewayStateName, + 'district_code' => $gatewayDistrictCode, + 'district_name' => $gatewayDistrictName, 'abha_number' => $abhaDigits, 'abha_address' => $abhaAddress, 'photo_base64' => $gatewayPhoto, @@ -1490,6 +1652,188 @@ class Patient extends BaseController return $result; } + /** + * Return only safe/required fields for ABHA OTP verification APIs. + * Prevents leaking connector tokens, full bridge profile dumps, and base64 photo blobs. + * + * @param array $result + * @return array + */ + private function sanitizeAbhaVerifyApiResponse(array $result): array + { + $ok = ! empty($result['ok']) && (int) $result['ok'] === 1; + if (! $ok) { + return [ + 'ok' => 0, + 'error_text' => (string) ($result['error_text'] ?? $result['message'] ?? $result['data']['message'] ?? 'OTP verification failed'), + 'request_id' => (string) ($result['request_id'] ?? ''), + ]; + } + + $payload = []; + if (isset($result['data']) && is_array($result['data'])) { + $payload = $result['data']; + } elseif (is_array($result)) { + $payload = $result; + } + + $profile = $this->pickGatewayAbhaProfile($payload); + + $txnId = (string) ($payload['txnId'] ?? $payload['txn_id'] ?? $result['txnId'] ?? $result['txn_id'] ?? ''); + $abhaRaw = (string) ($payload['ABHANumber'] ?? $payload['abha_number'] ?? $payload['abha_id'] ?? $profile['ABHANumber'] ?? $profile['abha_number'] ?? $profile['abha_id'] ?? ''); + $abhaDigits = preg_replace('/\D/', '', $abhaRaw); + $abhaAddress = (string) ($payload['preferredAbhaAddress'] ?? $payload['abha_address'] ?? $profile['preferredAbhaAddress'] ?? $profile['abha_address'] ?? $profile['preferredAddress'] ?? ''); + $name = trim((string) ( + $payload['name'] + ?? $payload['full_name'] + ?? $profile['name'] + ?? $profile['fullName'] + ?? $profile['full_name'] + ?? trim(implode(' ', array_filter([ + (string) ($profile['firstName'] ?? ''), + (string) ($profile['middleName'] ?? ''), + (string) ($profile['lastName'] ?? ''), + ]))) + )); + $mobile = (string) ($payload['mobile'] ?? $payload['mobileNumber'] ?? $profile['mobile'] ?? ''); + $gender = (string) ($payload['gender'] ?? $profile['gender'] ?? ''); + $dob = (string) ($payload['dob'] ?? $payload['date_of_birth'] ?? $profile['dob'] ?? $profile['date_of_birth'] ?? ''); + $address = (string) ($payload['address'] ?? $payload['address_line'] ?? $profile['address'] ?? $profile['address_line'] ?? ''); + $pinCode = (string) ($payload['pinCode'] ?? $payload['pin_code'] ?? $payload['pincode'] ?? $profile['pinCode'] ?? $profile['pin_code'] ?? ''); + $stateCode = (string) ($payload['stateCode'] ?? $payload['state_code'] ?? $profile['stateCode'] ?? $profile['state_code'] ?? ''); + $stateName = (string) ($payload['stateName'] ?? $payload['state_name'] ?? $profile['stateName'] ?? $profile['state_name'] ?? ''); + $districtCode = (string) ($payload['districtCode'] ?? $payload['district_code'] ?? $profile['districtCode'] ?? $profile['district_code'] ?? ''); + $districtName = (string) ($payload['districtName'] ?? $payload['district_name'] ?? $profile['districtName'] ?? $profile['district_name'] ?? ''); + + $matchedPatientId = (int) ($result['hms_patient']['matched_patient_id'] ?? 0); + $patientId = (int) ($result['patient_id'] ?? $payload['patient_id'] ?? $matchedPatientId); + $pCode = (string) ($result['p_code'] ?? $payload['p_code'] ?? ''); + $isNew = $result['is_new_patient'] ?? $payload['is_new_patient'] ?? null; + + $safeProfile = [ + 'ABHANumber' => $abhaDigits, + 'abha_number' => $abhaDigits, + 'abha_id' => $abhaDigits, + 'preferredAbhaAddress' => $abhaAddress, + 'preferredAddress' => $abhaAddress, + 'name' => $name, + 'full_name' => $name, + 'gender' => $gender, + 'dob' => $dob, + 'mobile' => $mobile, + 'address' => $address, + 'pinCode' => $pinCode, + 'stateCode' => $stateCode, + 'stateName' => $stateName, + 'districtCode' => $districtCode, + 'districtName' => $districtName, + 'firstName' => (string) ($profile['firstName'] ?? ''), + 'middleName' => (string) ($profile['middleName'] ?? ''), + 'lastName' => (string) ($profile['lastName'] ?? ''), + ]; + + return [ + 'ok' => 1, + 'request_id' => (string) ($result['request_id'] ?? ''), + 'message' => (string) ($payload['message'] ?? $result['message'] ?? 'ABHA OTP verified successfully'), + 'txn_id' => $txnId, + 'abha_number' => $abhaDigits, + 'abha_address' => $abhaAddress, + 'name' => $name, + 'mobile' => $mobile, + 'gender' => $gender, + 'dob' => $dob, + 'address' => $address, + 'pin_code' => $pinCode, + 'state_code' => $stateCode, + 'state_name' => $stateName, + 'district_code' => $districtCode, + 'district_name' => $districtName, + 'patient_id' => $patientId > 0 ? $patientId : null, + 'p_code' => $pCode !== '' ? $pCode : null, + 'is_new_patient' => $isNew, + 'data' => [ + 'txnId' => $txnId, + 'abha_number' => $abhaDigits, + 'name' => $name, + 'mobile' => $mobile, + 'gender' => $gender, + 'dob' => $dob, + 'address' => $address, + 'pin_code' => $pinCode, + 'state_code' => $stateCode, + 'state_name' => $stateName, + 'district_code' => $districtCode, + 'district_name' => $districtName, + 'profile' => $safeProfile, + ], + ]; + } + + private function extractCityFromAddress(string $address, string $districtName = '', string $stateName = ''): string + { + $address = trim($address); + if ($address === '') { + return ''; + } + + $districtNorm = mb_strtoupper(trim($districtName)); + $stateNorm = mb_strtoupper(trim($stateName)); + $parts = array_values(array_filter(array_map('trim', explode(',', $address)), static fn (string $v): bool => $v !== '')); + if ($parts === []) { + return ''; + } + + for ($i = count($parts) - 1; $i >= 0; $i--) { + $candidate = trim((string) ($parts[$i] ?? '')); + if ($candidate === '' || preg_match('/\d/', $candidate) === 1) { + continue; + } + + $norm = mb_strtoupper($candidate); + if (($districtNorm !== '' && $norm === $districtNorm) || ($stateNorm !== '' && $norm === $stateNorm)) { + continue; + } + + return $candidate; + } + + return ''; + } + + private function upsertAbdmLocationMasters(string $stateCode, string $stateName, string $districtCode, string $districtName): void + { + // State data lives in india_state (read-only reference — no writes needed from ABHA flow). + + if ($districtCode !== '' && $districtName !== '' && $this->db->tableExists('abdm_district_master')) { + $districtBuilder = $this->db->table('abdm_district_master') + ->select('id') + ->where('district_code', $districtCode); + if ($stateCode !== '') { + $districtBuilder->where('state_code', $stateCode); + } + $districtRow = $districtBuilder->limit(1)->get()->getRowArray(); + + if ($districtRow === null) { + $this->db->table('abdm_district_master')->insert([ + 'district_code' => $districtCode, + 'district_name' => $districtName, + 'state_code' => $stateCode, + 'created_at' => date('Y-m-d H:i:s'), + 'updated_at' => date('Y-m-d H:i:s'), + ]); + } else { + $this->db->table('abdm_district_master') + ->where('id', (int) ($districtRow['id'] ?? 0)) + ->update([ + 'district_name' => $districtName, + 'state_code' => $stateCode, + 'updated_at' => date('Y-m-d H:i:s'), + ]); + } + } + } + /** * @param array $payload * @return array @@ -1497,10 +1841,11 @@ class Patient extends BaseController private function pickGatewayAbhaProfile(array $payload): array { $candidates = [ - $payload['gateway_patient'] ?? null, $payload['ABHAProfile'] ?? null, - $payload['accounts'][0] ?? null, + $payload['gateway_abha_profile'] ?? null, $payload['profile'] ?? null, + $payload['accounts'][0] ?? null, + $payload['gateway_patient'] ?? null, $payload, ]; diff --git a/app/Database/Migrations/2026-06-24-000610_CreateAbdmLocationMasters.php b/app/Database/Migrations/2026-06-24-000610_CreateAbdmLocationMasters.php new file mode 100644 index 0000000..db30aad --- /dev/null +++ b/app/Database/Migrations/2026-06-24-000610_CreateAbdmLocationMasters.php @@ -0,0 +1,119 @@ +createStateMaster(); + $this->createDistrictMaster(); + } + + public function down(): void + { + if ($this->hasTable('abdm_district_master')) { + $this->forge->dropTable('abdm_district_master', true); + } + + if ($this->hasTable('abdm_state_master')) { + $this->forge->dropTable('abdm_state_master', true); + } + } + + private function createStateMaster(): void + { + if ($this->hasTable('abdm_state_master')) { + return; + } + + $this->forge->addField([ + 'id' => [ + 'type' => 'INT', + 'constraint' => 11, + 'unsigned' => true, + 'auto_increment' => true, + ], + 'state_code' => [ + 'type' => 'VARCHAR', + 'constraint' => 10, + 'null' => false, + ], + 'state_name' => [ + 'type' => 'VARCHAR', + 'constraint' => 120, + 'null' => false, + ], + 'created_at' => [ + 'type' => 'DATETIME', + 'null' => true, + ], + 'updated_at' => [ + 'type' => 'DATETIME', + 'null' => true, + ], + ]); + + $this->forge->addKey('id', true); + $this->forge->addUniqueKey('state_code'); + $this->forge->addKey('state_name'); + $this->forge->createTable('abdm_state_master', true); + } + + private function createDistrictMaster(): void + { + if ($this->hasTable('abdm_district_master')) { + return; + } + + $this->forge->addField([ + 'id' => [ + 'type' => 'INT', + 'constraint' => 11, + 'unsigned' => true, + 'auto_increment' => true, + ], + 'district_code' => [ + 'type' => 'VARCHAR', + 'constraint' => 10, + 'null' => false, + ], + 'district_name' => [ + 'type' => 'VARCHAR', + 'constraint' => 120, + 'null' => false, + ], + 'state_code' => [ + 'type' => 'VARCHAR', + 'constraint' => 10, + 'null' => false, + ], + 'created_at' => [ + 'type' => 'DATETIME', + 'null' => true, + ], + 'updated_at' => [ + 'type' => 'DATETIME', + 'null' => true, + ], + ]); + + $this->forge->addKey('id', true); + $this->forge->addUniqueKey(['district_code', 'state_code']); + $this->forge->addKey('district_name'); + $this->forge->addKey('state_code'); + $this->forge->createTable('abdm_district_master', true); + } + + private function hasTable(string $table): bool + { + if (! preg_match('/^[a-zA-Z0-9_]+$/', $table)) { + return false; + } + + $row = $this->db->query("SHOW TABLES LIKE '" . $table . "'")->getRowArray(); + return ! empty($row); + } +} diff --git a/app/Database/Migrations/2026-06-24-000611_DropAbdmStateMaster.php b/app/Database/Migrations/2026-06-24-000611_DropAbdmStateMaster.php new file mode 100644 index 0000000..c77ce81 --- /dev/null +++ b/app/Database/Migrations/2026-06-24-000611_DropAbdmStateMaster.php @@ -0,0 +1,40 @@ +db->query("SHOW TABLES LIKE 'abdm_state_master'")->getRowArray(); + if (! empty($row)) { + $this->forge->dropTable('abdm_state_master', true); + } + } + + public function down(): void + { + // Recreate the table if rolled back (data is not restored) + if (! empty($this->db->query("SHOW TABLES LIKE 'abdm_state_master'")->getRowArray())) { + return; + } + + $this->forge->addField([ + 'id' => ['type' => 'INT', 'constraint' => 11, 'unsigned' => true, 'auto_increment' => true], + 'state_code' => ['type' => 'VARCHAR', 'constraint' => 10, 'null' => false], + 'state_name' => ['type' => 'VARCHAR', 'constraint' => 120, 'null' => false], + 'created_at' => ['type' => 'DATETIME', 'null' => true], + 'updated_at' => ['type' => 'DATETIME', 'null' => true], + ]); + $this->forge->addKey('id', true); + $this->forge->addUniqueKey('state_code'); + $this->forge->createTable('abdm_state_master', true); + } +} diff --git a/app/Database/Seeds/AbdmLocationMasterSeeder.php b/app/Database/Seeds/AbdmLocationMasterSeeder.php new file mode 100644 index 0000000..031daf1 --- /dev/null +++ b/app/Database/Seeds/AbdmLocationMasterSeeder.php @@ -0,0 +1,205 @@ + + */ + private function stateMap(): array + { + return [ + '1' => 'JAMMU AND KASHMIR', + '2' => 'HIMACHAL PRADESH', + '3' => 'PUNJAB', + '4' => 'CHANDIGARH', + '5' => 'UTTARAKHAND', + '6' => 'HARYANA', + '7' => 'DELHI', + '8' => 'RAJASTHAN', + '9' => 'UTTAR PRADESH', + '10' => 'BIHAR', + '11' => 'SIKKIM', + '12' => 'ARUNACHAL PRADESH', + '13' => 'NAGALAND', + '14' => 'MANIPUR', + '15' => 'MIZORAM', + '16' => 'TRIPURA', + '17' => 'MEGHALAYA', + '18' => 'ASSAM', + '19' => 'WEST BENGAL', + '20' => 'JHARKHAND', + '21' => 'ODISHA', + '22' => 'CHHATTISGARH', + '23' => 'MADHYA PRADESH', + '24' => 'GUJARAT', + '25' => 'DAMAN AND DIU', + '26' => 'DADRA AND NAGAR HAVELI', + '27' => 'MAHARASHTRA', + '28' => 'ANDHRA PRADESH', + '29' => 'KARNATAKA', + '30' => 'GOA', + '31' => 'LAKSHADWEEP', + '32' => 'KERALA', + '33' => 'TAMIL NADU', + '34' => 'PUDUCHERRY', + '35' => 'ANDAMAN AND NICOBAR ISLANDS', + '36' => 'TELANGANA', + '37' => 'ANDHRA PRADESH', + '38' => 'LADAKH', + ]; + } + + /** + * District seed data. + * state_code '5' = Uttarakhand (ABDM numeric code, matches india_state.id). + * District codes follow NIC format (UK01–UK13). + * The legacy ABDM-numeric entry (code 56) is kept for backward compatibility. + * + * @return array + */ + private function districtSeeds(): array + { + return [ + // Legacy ABDM-numeric code (from live ABHA verify responses) + ['district_code' => '56', 'district_name' => 'UDHAM SINGH NAGAR', 'state_code' => '5'], + + // Uttarakhand — NIC district codes + ['district_code' => 'UK01', 'district_name' => 'ALMORA', 'state_code' => '5'], + + // Uttar Pradesh — NIC district codes (state_code '9' = india_state.id) + ['district_code' => 'UP01', 'district_name' => 'AGRA', 'state_code' => '9'], + ['district_code' => 'UP02', 'district_name' => 'ALIGARH', 'state_code' => '9'], + ['district_code' => 'UP03', 'district_name' => 'AMBEDKAR NAGAR', 'state_code' => '9'], + ['district_code' => 'UP04', 'district_name' => 'AMETHI', 'state_code' => '9'], + ['district_code' => 'UP05', 'district_name' => 'AMROHA', 'state_code' => '9'], + ['district_code' => 'UP06', 'district_name' => 'AURAIYA', 'state_code' => '9'], + ['district_code' => 'UP07', 'district_name' => 'AYODHYA', 'state_code' => '9'], + ['district_code' => 'UP08', 'district_name' => 'AZAMGARH', 'state_code' => '9'], + ['district_code' => 'UP09', 'district_name' => 'BAGHPAT', 'state_code' => '9'], + ['district_code' => 'UP10', 'district_name' => 'BAHRAICH', 'state_code' => '9'], + ['district_code' => 'UP11', 'district_name' => 'BALLIA', 'state_code' => '9'], + ['district_code' => 'UP12', 'district_name' => 'BALRAMPUR', 'state_code' => '9'], + ['district_code' => 'UP13', 'district_name' => 'BANDA', 'state_code' => '9'], + ['district_code' => 'UP14', 'district_name' => 'BARABANKI', 'state_code' => '9'], + ['district_code' => 'UP15', 'district_name' => 'BAREILLY', 'state_code' => '9'], + ['district_code' => 'UP16', 'district_name' => 'BASTI', 'state_code' => '9'], + ['district_code' => 'UP17', 'district_name' => 'BHADOHI', 'state_code' => '9'], + ['district_code' => 'UP18', 'district_name' => 'BIJNOR', 'state_code' => '9'], + ['district_code' => 'UP19', 'district_name' => 'BUDAUN', 'state_code' => '9'], + ['district_code' => 'UP20', 'district_name' => 'BULANDSHAHR', 'state_code' => '9'], + ['district_code' => 'UP21', 'district_name' => 'CHANDAULI', 'state_code' => '9'], + ['district_code' => 'UP22', 'district_name' => 'CHITRAKOOT', 'state_code' => '9'], + ['district_code' => 'UP23', 'district_name' => 'DEORIA', 'state_code' => '9'], + ['district_code' => 'UP24', 'district_name' => 'ETAH', 'state_code' => '9'], + ['district_code' => 'UP25', 'district_name' => 'ETAWAH', 'state_code' => '9'], + ['district_code' => 'UP26', 'district_name' => 'FARRUKHABAD', 'state_code' => '9'], + ['district_code' => 'UP27', 'district_name' => 'FATEHPUR', 'state_code' => '9'], + ['district_code' => 'UP28', 'district_name' => 'FIROZABAD', 'state_code' => '9'], + ['district_code' => 'UP29', 'district_name' => 'GAUTAM BUDDHA NAGAR', 'state_code' => '9'], + ['district_code' => 'UP30', 'district_name' => 'GHAZIABAD', 'state_code' => '9'], + ['district_code' => 'UP31', 'district_name' => 'GHAZIPUR', 'state_code' => '9'], + ['district_code' => 'UP32', 'district_name' => 'GONDA', 'state_code' => '9'], + ['district_code' => 'UP33', 'district_name' => 'GORAKHPUR', 'state_code' => '9'], + ['district_code' => 'UP34', 'district_name' => 'HAMIRPUR', 'state_code' => '9'], + ['district_code' => 'UP35', 'district_name' => 'HAPUR', 'state_code' => '9'], + ['district_code' => 'UP36', 'district_name' => 'HARDOI', 'state_code' => '9'], + ['district_code' => 'UP37', 'district_name' => 'HATHRAS', 'state_code' => '9'], + ['district_code' => 'UP38', 'district_name' => 'JALAUN', 'state_code' => '9'], + ['district_code' => 'UP39', 'district_name' => 'JAUNPUR', 'state_code' => '9'], + ['district_code' => 'UP40', 'district_name' => 'JHANSI', 'state_code' => '9'], + ['district_code' => 'UP41', 'district_name' => 'KANNAUJ', 'state_code' => '9'], + ['district_code' => 'UP42', 'district_name' => 'KANPUR DEHAT', 'state_code' => '9'], + ['district_code' => 'UP43', 'district_name' => 'KANPUR NAGAR', 'state_code' => '9'], + ['district_code' => 'UP44', 'district_name' => 'KANSHIRAM NAGAR (KASGANJ)', 'state_code' => '9'], + ['district_code' => 'UP45', 'district_name' => 'KAUSHAMBI', 'state_code' => '9'], + ['district_code' => 'UP46', 'district_name' => 'KUSHINAGAR', 'state_code' => '9'], + ['district_code' => 'UP47', 'district_name' => 'LAKHIMPUR KHERI', 'state_code' => '9'], + ['district_code' => 'UP48', 'district_name' => 'LALITPUR', 'state_code' => '9'], + ['district_code' => 'UP49', 'district_name' => 'LUCKNOW', 'state_code' => '9'], + ['district_code' => 'UP50', 'district_name' => 'MAHARAJGANJ', 'state_code' => '9'], + ['district_code' => 'UP51', 'district_name' => 'MAHOBA', 'state_code' => '9'], + ['district_code' => 'UP52', 'district_name' => 'MAINPURI', 'state_code' => '9'], + ['district_code' => 'UP53', 'district_name' => 'MATHURA', 'state_code' => '9'], + ['district_code' => 'UP54', 'district_name' => 'MAU', 'state_code' => '9'], + ['district_code' => 'UP55', 'district_name' => 'MEERUT', 'state_code' => '9'], + ['district_code' => 'UP56', 'district_name' => 'MIRZAPUR', 'state_code' => '9'], + ['district_code' => 'UP57', 'district_name' => 'MORADABAD', 'state_code' => '9'], + ['district_code' => 'UP58', 'district_name' => 'MUZAFFARNAGAR', 'state_code' => '9'], + ['district_code' => 'UP59', 'district_name' => 'PILIBHIT', 'state_code' => '9'], + ['district_code' => 'UP60', 'district_name' => 'PRATAPGARH', 'state_code' => '9'], + ['district_code' => 'UP61', 'district_name' => 'PRAYAGRAJ', 'state_code' => '9'], + ['district_code' => 'UP62', 'district_name' => 'RAE BARELI', 'state_code' => '9'], + ['district_code' => 'UP63', 'district_name' => 'RAMPUR', 'state_code' => '9'], + ['district_code' => 'UP64', 'district_name' => 'SAHARANPUR', 'state_code' => '9'], + ['district_code' => 'UP65', 'district_name' => 'SAMBHAL', 'state_code' => '9'], + ['district_code' => 'UP66', 'district_name' => 'SANT KABIR NAGAR', 'state_code' => '9'], + ['district_code' => 'UP67', 'district_name' => 'SHAHJAHANPUR', 'state_code' => '9'], + ['district_code' => 'UP68', 'district_name' => 'SHAMLI', 'state_code' => '9'], + ['district_code' => 'UP69', 'district_name' => 'SHRAVASTI', 'state_code' => '9'], + ['district_code' => 'UP70', 'district_name' => 'SIDDHARTHNAGAR', 'state_code' => '9'], + ['district_code' => 'UP71', 'district_name' => 'SITAPUR', 'state_code' => '9'], + ['district_code' => 'UP72', 'district_name' => 'SONBHADRA', 'state_code' => '9'], + ['district_code' => 'UP73', 'district_name' => 'SULTANPUR', 'state_code' => '9'], + ['district_code' => 'UP74', 'district_name' => 'UNNAO', 'state_code' => '9'], + ['district_code' => 'UP75', 'district_name' => 'VARANASI', 'state_code' => '9'], + ['district_code' => 'UK02', 'district_name' => 'BAGESHWAR', 'state_code' => '5'], + ['district_code' => 'UK03', 'district_name' => 'CHAMOLI', 'state_code' => '5'], + ['district_code' => 'UK04', 'district_name' => 'CHAMPAWAT', 'state_code' => '5'], + ['district_code' => 'UK05', 'district_name' => 'DEHRADUN', 'state_code' => '5'], + ['district_code' => 'UK06', 'district_name' => 'HARIDWAR', 'state_code' => '5'], + ['district_code' => 'UK07', 'district_name' => 'NAINITAL', 'state_code' => '5'], + ['district_code' => 'UK08', 'district_name' => 'PAURI GARHWAL', 'state_code' => '5'], + ['district_code' => 'UK09', 'district_name' => 'PITHORAGARH', 'state_code' => '5'], + ['district_code' => 'UK10', 'district_name' => 'RUDRAPRAYAG', 'state_code' => '5'], + ['district_code' => 'UK11', 'district_name' => 'TEHRI GARHWAL', 'state_code' => '5'], + ['district_code' => 'UK12', 'district_name' => 'UDHAM SINGH NAGAR', 'state_code' => '5'], + ['district_code' => 'UK13', 'district_name' => 'UTTARKASHI', 'state_code' => '5'], + ]; + } + + public function run(): void + { + if (! $this->db->tableExists('abdm_district_master')) { + // Table is created by migration 2026-06-24-000610. + return; + } + + $now = date('Y-m-d H:i:s'); + + // State reference data lives in india_state (pre-seeded). No writes needed here. + + // Upsert districts by unique (district_code, state_code). + $districtTable = $this->db->table('abdm_district_master'); + foreach ($this->districtSeeds() as $row) { + $existing = $districtTable + ->select('id') + ->where('district_code', $row['district_code']) + ->where('state_code', $row['state_code']) + ->limit(1) + ->get() + ->getRowArray(); + + if ($existing) { + $districtTable->where('id', (int) ($existing['id'] ?? 0))->update([ + 'district_name' => $row['district_name'], + 'updated_at' => $now, + ]); + continue; + } + + $districtTable->insert([ + 'district_code' => $row['district_code'], + 'district_name' => $row['district_name'], + 'state_code' => $row['state_code'], + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + } +} diff --git a/app/Libraries/Abdm/EAtriaBridgeConnector.php b/app/Libraries/Abdm/EAtriaBridgeConnector.php index 2912ded..38c7ee0 100644 --- a/app/Libraries/Abdm/EAtriaBridgeConnector.php +++ b/app/Libraries/Abdm/EAtriaBridgeConnector.php @@ -107,6 +107,51 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface return ''; } + /** + * @param array $payload + * @return array + */ + private function redactSensitiveForLog(array $payload): array + { + $walk = function ($value, string $key = '') use (&$walk) { + if (is_array($value)) { + $out = []; + foreach ($value as $k => $v) { + $kk = (string) $k; + $out[$k] = $walk($v, $kk); + } + return $out; + } + + $lowerKey = strtolower($key); + if (in_array($lowerKey, ['token', 'refresh_token', 'refreshtoken', 'authorization', 'auth', 'otp'], true)) { + return '[REDACTED]'; + } + + if (in_array($lowerKey, ['photo', 'profilephoto', 'profile_photo'], true)) { + return '[REDACTED_IMAGE_BASE64]'; + } + + if (is_string($value)) { + $trimmed = trim($value); + + // Redact JWT-like values by shape. + if (preg_match('/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/', $trimmed)) { + return '[REDACTED_JWT]'; + } + + // Redact probable base64 blobs (e.g., profile photo) to keep logs small. + if (strlen($trimmed) > 400 && preg_match('/^[A-Za-z0-9+\/=\r\n]+$/', $trimmed)) { + return '[REDACTED_LARGE_BASE64]'; + } + } + + return $value; + }; + + return $walk($payload); + } + // ------------------------------------------------------------------------- // Internal HTTP helper // ------------------------------------------------------------------------- @@ -176,16 +221,22 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface // Log outgoing request (mask token) $maskedToken = $this->token !== '' ? (substr($this->token, 0, 6) . '***' . substr($this->token, -4)) : '(none)'; + $safeRequestBody = $this->redactSensitiveForLog($body); log_message('debug', '[EAtriaBridge] --> ' . $method . ' ' . $url . ' | token=' . $maskedToken - . ' | body=' . json_encode($body)); + . ' | body=' . json_encode($safeRequestBody)); $raw = curl_exec($ch); $httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); $curlErr = curl_error($ch); curl_close($ch); - log_message('debug', '[EAtriaBridge] <-- HTTP ' . $httpCode . ' | raw=' . (string) $raw); + $safeRawForLog = (string) $raw; + $decodedRawForLog = json_decode((string) $raw, true); + if (is_array($decodedRawForLog)) { + $safeRawForLog = (string) json_encode($this->redactSensitiveForLog($decodedRawForLog), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + } + log_message('debug', '[EAtriaBridge] <-- HTTP ' . $httpCode . ' | raw=' . $safeRawForLog); if ($curlErr !== '') { log_message('error', '[EAtriaBridge] cURL error on ' . $url . ': ' . $curlErr); @@ -236,9 +287,11 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface $decodedResp = json_decode($rawResponse, true); $responseJson = is_array($decodedResp) - ? (string) json_encode($decodedResp, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) + ? (string) json_encode($this->redactSensitiveForLog($decodedResp), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : (trim($rawResponse) !== '' ? $rawResponse : null); + $safeRequestBody = $this->redactSensitiveForLog($requestBody); + $db->table('abdm_api_logs')->insert([ 'channel' => 'eatria_bridge', 'event_type' => $eventType !== '' ? $eventType : $path, @@ -246,7 +299,7 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface 'http_method' => strtoupper($method), 'entity_type' => null, 'entity_id' => null, - 'request_json' => (string) json_encode($requestBody, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + 'request_json' => (string) json_encode($safeRequestBody, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), 'response_code' => $httpCode > 0 ? $httpCode : null, 'response_json' => $responseJson, 'status' => $status, diff --git a/app/Views/abdm/opd_queue.php b/app/Views/abdm/opd_queue.php index d658593..1123e5f 100644 --- a/app/Views/abdm/opd_queue.php +++ b/app/Views/abdm/opd_queue.php @@ -497,26 +497,42 @@ if (r.requires_confirmation) { const matches = Array.isArray(r.matches) ? r.matches : []; + const confidenceBadge = conf => { + const map = { definitive: ['success','Definitive'], high: ['primary','High'], medium: ['warning','Medium'], low: ['secondary','Low'] }; + const [cls, lbl] = map[conf] || ['secondary', 'Low']; + return `${lbl}`; + }; + const genderLabel = g => g == 2 ? 'F' : g == 1 ? 'M' : '—'; const cards = matches.map(m => { const reasons = Array.isArray(m.match_reasons) ? m.match_reasons.join(', ') : ''; - return `
+ const conf = m.match_confidence || 'low'; + const borderCls = conf === 'definitive' ? 'border-success' : conf === 'high' ? 'border-primary' : conf === 'medium' ? 'border-warning' : 'border-secondary'; + return `
-
${esc(m.p_code || 'UHID N/A')} - ${esc(m.p_fname || 'Unnamed')}
-
Phone: ${esc(m.mphone1 || '—')} | ABHA: ${esc(m.patient_abha || '—')} | Aadhaar: ${esc(m.patient_aadhaar || '—')}
-
Matched by: ${esc(reasons || 'Data match')}
- +
${esc(m.p_code || 'UHID N/A')} - ${esc(m.p_fname || 'Unnamed')} ${confidenceBadge(conf)}
+
DOB: ${esc(m.dob || '—')} | Gender: ${genderLabel(m.gender)} | Phone: ${esc(m.mphone1 || '—')}
+
ABHA: ${esc(m.patient_abha || '—')} | Aadhaar: ${esc(m.patient_aadhaar || '—')}
+
Matched by: ${esc(reasons || 'Data match')}
+
`; }).join(''); + const createNewCard = `
+
+
None of the above — Register as New Patient
+
This ABHA does not belong to any listed record. A new HMS patient will be created.
+ +
+
`; + body.innerHTML = `
- Similar patient records found. Confirm existing patient or create a new record. + Possible matching records found in HMS. Select the correct patient to link, or register as new.
- ${cards} - - Open Manual Patient Registration (/billing/patient) - `; + ${cards}${createNewCard} + Open Manual Patient Registration + `; } else { abdmQCreateNewPatient(t.id, payloadEncoded, true); } diff --git a/app/Views/abdm/task_board.php b/app/Views/abdm/task_board.php index 5882814..f0684ed 100644 --- a/app/Views/abdm/task_board.php +++ b/app/Views/abdm/task_board.php @@ -611,16 +611,35 @@ cardHtml += '
Phone: ' + ((identity.phone || '-') + '') + '
'; if (matches.length > 0) { - cardHtml += '
Matching HMS Patients
'; + cardHtml += '
Possible Matching HMS Patients
'; matches.forEach(function (m) { + var conf = m.match_confidence || 'low'; + var confLabels = { definitive: 'Definitive', high: 'High', medium: 'Medium', low: 'Low' }; + var confBadgeClass = { definitive: 'bg-success', high: 'bg-primary', medium: 'bg-warning text-dark', low: 'bg-secondary' }; + var confBadge = '' + (confLabels[conf] || 'Low') + ''; + var reasons = Array.isArray(m.match_reasons) ? m.match_reasons.join(', ') : ''; + var genderLabel = m.gender == 2 ? 'F' : m.gender == 1 ? 'M' : '—'; + var borderClass = conf === 'definitive' ? 'border-success' : conf === 'high' ? 'border-primary' : conf === 'medium' ? 'border-warning' : 'border-secondary'; var mLabel = ((m.p_code || 'UHID') + ' - ' + (m.p_fname || 'Unnamed')); - cardHtml += '
'; - cardHtml += '
' + mLabel + '
'; - cardHtml += ''; + cardHtml += '
'; + cardHtml += '
'; + cardHtml += '
' + mLabel + '' + confBadge + '
'; + cardHtml += ''; + cardHtml += '
'; + cardHtml += '
DOB: ' + (m.dob || '—') + ' | Gender: ' + genderLabel + ' | Phone: ' + (m.mphone1 || '—') + '
'; + cardHtml += '
Matched by: ' + reasons + '
'; cardHtml += '
'; }); + // Add "Create New Patient" as a selectable card option at the end of the list + cardHtml += '
'; + cardHtml += '
'; + cardHtml += '
None of the above — Register as New Patient
'; + cardHtml += ''; + cardHtml += '
'; + cardHtml += '
This ABHA does not belong to any listed record.
'; + cardHtml += '
'; } else { - cardHtml += '
No existing patient matched from ABHA/phone.
'; + cardHtml += '
No existing patient matched from HMS demographics or ABHA.
'; } cardHtml += '
'; @@ -659,6 +678,28 @@ }); }); + scanResolvePane.querySelectorAll('[data-scan-create-new="1"]').forEach(function (btn) { + btn.addEventListener('click', function () { + setSandboxStatus('Creating new patient from scan result...', 'muted'); + post('/' + queueId, { + action: 'create_new', + abha_number: (identity.abha_number || ''), + abha_address: (identity.abha_address || ''), + patient_name: (identity.patient_name || ''), + phone: (identity.phone || ''), + gender: (identity.gender || ''), + dob: (identity.dob || '') + }, function (resp) { + if (!resp || parseInt(resp.ok || 0, 10) !== 1) { + setSandboxStatus((resp && resp.error_text) ? resp.error_text : 'Unable to create patient.', 'danger'); + return; + } + clearScanResolvePane(); + setSandboxStatus('New patient created: ' + (resp.p_code || resp.patient_id || '-') + '.', 'success'); + }); + }); + }); + var createBtn = document.getElementById('btnScanCreateNewPatient'); if (createBtn) { createBtn.addEventListener('click', function () { diff --git a/app/Views/billing/Patient_V.php b/app/Views/billing/Patient_V.php index 4f744d7..5d72a37 100644 --- a/app/Views/billing/Patient_V.php +++ b/app/Views/billing/Patient_V.php @@ -261,15 +261,20 @@
+ id="input_district" placeholder="Type to search or add new…" type="text" + autocomplete="off"> +
Type to search; unlisted districts can be entered freely.
- +
@@ -1325,7 +1330,16 @@ $('#input_city').val(p.city); } if (p.state && !$('#input_state').val()) { - $('#input_state').val(p.state); + // Try to match existing option (case-insensitive); fall back to first partial match + var stateTarget = (p.state || '').trim().toUpperCase(); + var $sel = $('#input_state'); + var bestVal = ''; + $sel.find('option').each(function() { + if ($(this).val().toUpperCase() === stateTarget) { bestVal = $(this).val(); return false; } + }); + if (bestVal) { + $sel.val(bestVal); + } } if (p.dob && !$('#datepicker_dob').val()) { $('#datepicker_dob').val(p.dob); @@ -1351,7 +1365,28 @@ autofocus: true, select: function(event, ui) { $("#input_district").val(ui.item.l_district); - $("#input_state").val(ui.item.l_state); + var stateVal = (ui.item.l_state || '').trim().toUpperCase(); + var $stateSelect = $("#input_state"); + var matched = false; + $stateSelect.find('option').each(function() { + if ($(this).val().toUpperCase() === stateVal) { + $stateSelect.val($(this).val()); + matched = true; + return false; + } + }); + if (!matched && stateVal !== '') { + $stateSelect.append($('