ABDM: finalize ABHA verify persistence, M2 routes/workflows, and location masters seeding

This commit is contained in:
DevSoftTech Server 2026-06-24 03:15:22 +05:30
parent 75e71c2343
commit 985d476bc1
12 changed files with 1546 additions and 56 deletions

View File

@ -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');

View File

@ -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<string, mixed> $result
* @return array<string, mixed>
*/
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<string, mixed> $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<string, mixed> $payload
* @return array<string, mixed>
*/
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<string, mixed> $result
* @return array<string, mixed>
*/
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'] ?? ''),
],
],
];
}
// =========================================================================

View File

@ -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(

View File

@ -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;
}

View File

@ -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<string, mixed> $result
* @return array<string, mixed>
*/
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<string, mixed> $payload
* @return array<string, mixed>
@ -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,
];

View File

@ -0,0 +1,119 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class CreateAbdmLocationMasters extends Migration
{
public function up(): void
{
$this->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);
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
/**
* Drop abdm_state_master — superseded by the existing india_state table
* which already has all 38 states/UTs and is used as the authoritative
* reference for the patient registration State dropdown.
*/
class DropAbdmStateMaster extends Migration
{
public function up(): void
{
$row = $this->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);
}
}

View File

@ -0,0 +1,205 @@
<?php
namespace App\Database\Seeds;
use CodeIgniter\Database\Seeder;
class AbdmLocationMasterSeeder extends Seeder
{
/**
* Canonical India state/UT code map used by ABDM payloads.
*
* @return array<string, string>
*/
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<int, array{district_code:string,district_name:string,state_code:string}>
*/
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,
]);
}
}
}

View File

@ -107,6 +107,51 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface
return '';
}
/**
* @param array<string, mixed> $payload
* @return array<string, mixed>
*/
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,

View File

@ -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 `<span class="badge bg-${cls} ms-1">${lbl}</span>`;
};
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 `<div class="card mb-2 border-warning">
const conf = m.match_confidence || 'low';
const borderCls = conf === 'definitive' ? 'border-success' : conf === 'high' ? 'border-primary' : conf === 'medium' ? 'border-warning' : 'border-secondary';
return `<div class="card mb-2 ${borderCls}">
<div class="card-body py-2 px-3 small">
<div class="fw-bold">${esc(m.p_code || 'UHID N/A')} - ${esc(m.p_fname || 'Unnamed')}</div>
<div class="text-muted">Phone: ${esc(m.mphone1 || '—')} | ABHA: ${esc(m.patient_abha || '—')} | Aadhaar: ${esc(m.patient_aadhaar || '—')}</div>
<div class="text-warning">Matched by: ${esc(reasons || 'Data match')}</div>
<button class="btn btn-sm btn-outline-primary mt-2" onclick="abdmQResolveExisting(${Number(t.id) || 0}, ${Number(m.id) || 0}, '${payloadEncoded}')">Use This Patient</button>
<div class="fw-bold">${esc(m.p_code || 'UHID N/A')} - ${esc(m.p_fname || 'Unnamed')} ${confidenceBadge(conf)}</div>
<div class="text-muted">DOB: ${esc(m.dob || '—')} | Gender: ${genderLabel(m.gender)} | Phone: ${esc(m.mphone1 || '—')}</div>
<div class="text-muted">ABHA: ${esc(m.patient_abha || '—')} | Aadhaar: ${esc(m.patient_aadhaar || '—')}</div>
<div class="text-muted fst-italic">Matched by: ${esc(reasons || 'Data match')}</div>
<button class="btn btn-sm btn-outline-primary mt-2" onclick="abdmQResolveExisting(${Number(t.id) || 0}, ${Number(m.id) || 0}, '${payloadEncoded}')">Link to This Patient</button>
</div>
</div>`;
}).join('');
const createNewCard = `<div class="card mb-2 border-dashed border-2 border-secondary">
<div class="card-body py-2 px-3 small">
<div class="fw-bold text-secondary"><i class="bi bi-person-plus me-1"></i>None of the above — Register as New Patient</div>
<div class="text-muted">This ABHA does not belong to any listed record. A new HMS patient will be created.</div>
<button class="btn btn-sm btn-success mt-2" onclick="abdmQCreateNewPatient(${Number(t.id) || 0}, '${payloadEncoded}')">Create New Patient</button>
</div>
</div>`;
body.innerHTML = `
<div class="alert alert-warning py-2 small mb-2">
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.
</div>
${cards}
<button class="btn btn-sm btn-success w-100 mt-2" onclick="abdmQCreateNewPatient(${Number(t.id) || 0}, '${payloadEncoded}')">Create New Patient</button>
<a href="${BASE}billing/patient" class="btn btn-sm btn-outline-secondary w-100 mt-2" target="_blank">Open Manual Patient Registration (/billing/patient)</a>
<button class="btn btn-sm btn-outline-secondary w-100 mt-2" data-bs-dismiss="modal">Close</button>`;
${cards}${createNewCard}
<a href="${BASE}billing/patient" class="btn btn-sm btn-outline-secondary w-100 mt-1" target="_blank">Open Manual Patient Registration</a>
<button class="btn btn-sm btn-outline-secondary w-100 mt-1" data-bs-dismiss="modal">Close</button>`;
} else {
abdmQCreateNewPatient(t.id, payloadEncoded, true);
}

View File

@ -611,16 +611,35 @@
cardHtml += '<div class="small mb-2"><strong>Phone:</strong> ' + ((identity.phone || '-') + '') + '</div>';
if (matches.length > 0) {
cardHtml += '<div class="small fw-semibold mb-1">Matching HMS Patients</div>';
cardHtml += '<div class="small fw-semibold mb-1">Possible Matching HMS Patients</div>';
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 = '<span class="badge ' + (confBadgeClass[conf] || 'bg-secondary') + ' ms-1 small">' + (confLabels[conf] || 'Low') + '</span>';
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 += '<div class="d-flex justify-content-between align-items-center border rounded p-1 mb-1 bg-white">';
cardHtml += '<div class="small">' + mLabel + '</div>';
cardHtml += '<button type="button" class="btn btn-sm btn-outline-primary" data-scan-use-existing="1" data-patient-id="' + (m.id || 0) + '">Use</button>';
cardHtml += '<div class="border rounded p-1 mb-1 bg-white ' + borderClass + '">';
cardHtml += '<div class="d-flex justify-content-between align-items-start">';
cardHtml += '<div class="small"><span class="fw-semibold">' + mLabel + '</span>' + confBadge + '</div>';
cardHtml += '<button type="button" class="btn btn-sm btn-outline-primary ms-2" data-scan-use-existing="1" data-patient-id="' + (m.id || 0) + '">Link</button>';
cardHtml += '</div>';
cardHtml += '<div class="small text-muted">DOB: ' + (m.dob || '—') + ' | Gender: ' + genderLabel + ' | Phone: ' + (m.mphone1 || '—') + '</div>';
cardHtml += '<div class="small text-muted fst-italic">Matched by: ' + reasons + '</div>';
cardHtml += '</div>';
});
// Add "Create New Patient" as a selectable card option at the end of the list
cardHtml += '<div class="border rounded border-2 border-secondary p-1 mb-1 bg-white">';
cardHtml += '<div class="d-flex justify-content-between align-items-start">';
cardHtml += '<div class="small"><span class="fw-semibold text-secondary">None of the above — Register as New Patient</span></div>';
cardHtml += '<button type="button" class="btn btn-sm btn-success ms-2" data-scan-create-new="1">Create New</button>';
cardHtml += '</div>';
cardHtml += '<div class="small text-muted">This ABHA does not belong to any listed record.</div>';
cardHtml += '</div>';
} else {
cardHtml += '<div class="small text-muted mb-2">No existing patient matched from ABHA/phone.</div>';
cardHtml += '<div class="small text-muted mb-2">No existing patient matched from HMS demographics or ABHA.</div>';
}
cardHtml += '<div class="d-flex gap-2 mt-2">';
@ -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('<?= base_url('AbdmGateway/scan_share_link_patient') ?>/' + 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 () {

View File

@ -261,15 +261,20 @@
<div class="form-group">
<label class="form-label">District</label>
<input class="form-control" name="input_district"
id="input_district" placeholder="District" type="text"
autocomplete="on">
id="input_district" placeholder="Type to search or add new…" type="text"
autocomplete="off">
<div class="form-text text-muted small">Type to search; unlisted districts can be entered freely.</div>
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label class="form-label">State</label>
<input class="form-control" name="input_state"
id="input_state" placeholder="State" type="text" autocomplete="on">
<select class="form-select" name="input_state" id="input_state">
<option value="">— Select State —</option>
<?php foreach (($india_states ?? []) as $st): ?>
<option value="<?= esc($st['state_name']) ?>"><?= esc($st['state_name']) ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
</div>
@ -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($('<option>').val(ui.item.l_state).text(ui.item.l_state).prop('selected', true));
}
}
});
$("#input_district").autocomplete({
source: "<?= base_url('billing/patient/district_list') ?>",
minLength: 2,
select: function(event, ui) {
$("#input_district").val(ui.item.value);
return false;
}
});