feat(abdm): add Find ABHA by Aadhaar Number flow (M1 VRFY_ABHA_401 to VRFY_ABHA_405)

This commit is contained in:
DevSoftTech Server 2026-10-12 11:58:53 +05:30
parent 39017a43cf
commit e59bd8d2b7
5 changed files with 1060 additions and 1 deletions

View File

@ -843,6 +843,8 @@ $routes->post('abha/find/mobile/search', 'Abha::findMobileSearch', ['filter' =>
$routes->post('abha/find/mobile/request-otp', 'Abha::findMobileRequestOtp', ['filter' => 'permission:abdm.abha.create']);
$routes->post('abha/find/mobile/verify-otp', 'Abha::findMobileVerifyOtp', ['filter' => 'permission:abdm.abha.create']);
$routes->post('abha/find/mobile/select-account', 'Abha::selectAccount', ['filter' => 'permission:abdm.abha.create']);
$routes->post('abha/find/aadhaar/request-otp', 'Abha::findAadhaarRequestOtp', ['filter' => 'permission:abdm.abha.create']);
$routes->post('abha/find/aadhaar/verify-otp', 'Abha::findAadhaarVerifyOtp', ['filter' => 'permission:abdm.abha.create']);
$routes->post('abha/create/confirm_patient', 'Abha::confirmPatient', ['filter' => 'permission:abdm.abha.create']);
$routes->post('abha/create/address', 'Abha::address', ['filter' => 'permission:abdm.abha.create']);
$routes->post('abha/create/address_suggestions', 'Abha::addressSuggestions', ['filter' => 'permission:abdm.abha.create']);

View File

@ -1082,6 +1082,321 @@ class Abha extends BaseController
}
}
// -------------------------------------------------------------------------
// ABDM M1 Find ABHA via Aadhaar Flow
// TEST POINT: 12.1 VRFY_ABHA_401 to VRFY_ABHA_405
// FLOW: AADHAAR NUMBER -> OTP -> COMPLETE PROFILE FETCHED / CREATE ABHA
// -------------------------------------------------------------------------
/**
* VRFY_ABHA_401: Request OTP using 12-digit Aadhaar Number
* POST abha/find/aadhaar/request-otp
*/
public function findAadhaarRequestOtp()
{
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
}
$aadhaar = preg_replace('/\D/', '', trim((string) ($this->request->getPost('aadhaar') ?? '')));
if (strlen($aadhaar) !== 12) {
return $this->response->setJSON([
'ok' => 0,
'error_text' => 'Aadhaar Number is not valid. Please enter a valid 12-digit Aadhaar Number.',
]);
}
try {
$helper = new \App\Libraries\Abdm\AbdmV3DirectHelper();
$result = $helper->requestLoginOtpByAadhaar($aadhaar);
if (! empty($result['ok']) && (int) $result['ok'] === 1) {
return $this->response->setJSON([
'ok' => 1,
'txn_id' => $result['txn_id'] ?? '',
'message' => $result['message'] ?? 'OTP sent to Aadhaar linked mobile number.',
'resend_after' => $result['resend_after'] ?? 60,
]);
}
return $this->response->setJSON([
'ok' => 0,
'error_text' => $result['error_text'] ?? 'Failed to send OTP to Aadhaar linked mobile.',
]);
} catch (\Throwable $e) {
log_message('error', '[ABHA] findAadhaarRequestOtp exception: ' . $e->getMessage());
return $this->response->setStatusCode(500)->setJSON([
'ok' => 0,
'error_text' => $e->getMessage(),
]);
}
}
/**
* VRFY_ABHA_402, 403, 404, 405: Verify Aadhaar OTP and fetch complete ABHA profile + card
* POST abha/find/aadhaar/verify-otp
*/
public function findAadhaarVerifyOtp()
{
if (! $this->request->isAJAX()) {
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']);
}
$txnId = trim((string) ($this->request->getPost('txn_id') ?? $this->request->getPost('txnId') ?? ''));
$otp = preg_replace('/\D/', '', trim((string) ($this->request->getPost('otp') ?? '')));
if ($txnId === '' || strlen($otp) !== 6) {
return $this->response->setJSON([
'ok' => 0,
'error_text' => 'Transaction ID and 6-digit OTP are required.',
]);
}
try {
$helper = new \App\Libraries\Abdm\AbdmV3DirectHelper();
$result = $helper->verifyLoginOtpByAadhaar($txnId, $otp);
// VRFY_ABHA_403: No ABHA user registered with this Aadhaar Number
if (! empty($result['no_abha_found'])) {
return $this->response->setJSON([
'ok' => 0,
'no_abha_found' => true,
'error_text' => 'NO ABHA user registered with this Aadhaar Number',
]);
}
if (empty($result['ok']) || (int) $result['ok'] !== 1) {
return $this->response->setJSON([
'ok' => 0,
'error_text' => $result['error_text'] ?? 'Incorrect OTP',
]);
}
// VRFY_ABHA_404: ABHA exists -> extract profile details & card
$fullProfile = is_array($result['full_profile'] ?? null) ? $result['full_profile'] : [];
$account = is_array($result['account'] ?? null) ? $result['account'] : [];
$userToken = (string) ($result['token'] ?? '');
$firstNonEmpty = static function (array $values): string {
foreach ($values as $value) {
if (is_scalar($value) && trim((string) $value) !== '') {
return trim((string) $value);
}
}
return '';
};
$abhaNum = $firstNonEmpty([
$account['ABHANumber'] ?? null,
$account['abhaNumber'] ?? null,
$account['abha_number'] ?? null,
$account['abha_id'] ?? null,
$fullProfile['ABHANumber'] ?? null,
$fullProfile['abhaNumber'] ?? null,
$fullProfile['abha_id'] ?? null,
]);
$abhaAddress = $firstNonEmpty([
$account['preferredAbhaAddress'] ?? null,
$account['preferredAddress'] ?? null,
$account['abhaAddress'] ?? null,
$account['abha_address'] ?? null,
$fullProfile['preferredAbhaAddress'] ?? null,
$fullProfile['preferredAddress'] ?? null,
$fullProfile['abhaAddress'] ?? null,
$fullProfile['abha_address'] ?? null,
]);
$name = $firstNonEmpty([
$account['name'] ?? null,
$account['fullName'] ?? null,
$account['full_name'] ?? null,
$fullProfile['name'] ?? null,
trim(($fullProfile['firstName'] ?? '') . ' ' . ($fullProfile['middleName'] ?? '') . ' ' . ($fullProfile['lastName'] ?? '')),
]);
$genderRaw = $firstNonEmpty([
$account['gender'] ?? null,
$fullProfile['gender'] ?? null,
]);
$genderUpper = strtoupper($genderRaw);
if ($genderUpper === 'M' || $genderUpper === '1' || $genderUpper === 'MALE') {
$gender = 'Male';
} elseif ($genderUpper === 'F' || $genderUpper === '2' || $genderUpper === 'FEMALE') {
$gender = 'Female';
} elseif ($genderUpper === 'O' || $genderUpper === '3' || $genderUpper === 'OTHER') {
$gender = 'Other';
} else {
$gender = $genderRaw;
}
$dob = $firstNonEmpty([
$account['dob'] ?? null,
$account['dateOfBirth'] ?? null,
$account['date_of_birth'] ?? null,
$fullProfile['dob'] ?? null,
$fullProfile['dateOfBirth'] ?? null,
$fullProfile['date_of_birth'] ?? null,
]);
if ($dob === '') {
$day = $firstNonEmpty([$account['dayOfBirth'] ?? null, $account['day_of_birth'] ?? null, $fullProfile['dayOfBirth'] ?? null, $fullProfile['day_of_birth'] ?? null]);
$month = $firstNonEmpty([$account['monthOfBirth'] ?? null, $account['month_of_birth'] ?? null, $fullProfile['monthOfBirth'] ?? null, $fullProfile['month_of_birth'] ?? null]);
$year = $firstNonEmpty([$account['yearOfBirth'] ?? null, $account['year_of_birth'] ?? null, $fullProfile['yearOfBirth'] ?? null, $fullProfile['year_of_birth'] ?? null]);
if ($year !== '' && $month !== '' && $day !== '') {
$dob = sprintf('%04d-%02d-%02d', (int) $year, (int) $month, (int) $day);
} elseif ($year !== '') {
$dob = $year;
}
}
$profileMobile = $firstNonEmpty([
$account['mobile'] ?? null,
$account['mobileNumber'] ?? null,
$account['mobile_number'] ?? null,
$fullProfile['mobile'] ?? null,
$fullProfile['mobileNumber'] ?? null,
]);
$photo = $firstNonEmpty([
$account['profilePhoto'] ?? null,
$account['profile_photo'] ?? null,
$fullProfile['profilePhoto'] ?? null,
$fullProfile['profile_photo'] ?? null,
$fullProfile['photo'] ?? null,
$fullProfile['kycPhoto'] ?? null,
]);
$address = $firstNonEmpty([
$fullProfile['address'] ?? null,
$fullProfile['addressLine'] ?? null,
$fullProfile['address_line'] ?? null,
$account['address'] ?? null,
$account['addressLine'] ?? null,
]);
$districtName = $firstNonEmpty([
$fullProfile['district'] ?? null,
$fullProfile['districtName'] ?? null,
$fullProfile['district_name'] ?? null,
$account['district'] ?? null,
$account['districtName'] ?? null,
]);
$stateName = $firstNonEmpty([
$fullProfile['state'] ?? null,
$fullProfile['stateName'] ?? null,
$fullProfile['state_name'] ?? null,
$account['state'] ?? null,
$account['stateName'] ?? null,
]);
$zip = $firstNonEmpty([
$fullProfile['pinCode'] ?? null,
$fullProfile['pincode'] ?? null,
$fullProfile['pin'] ?? null,
$account['pinCode'] ?? null,
$account['pincode'] ?? null,
]);
$email = $firstNonEmpty([
$fullProfile['email'] ?? null,
$account['email'] ?? null,
]);
$cardBase64 = (string) ($result['card_base64'] ?? '');
// Fallback to Bridge card endpoint if not retrieved directly
if ($cardBase64 === '' && ($abhaNum !== '' || $abhaAddress !== '')) {
try {
$bridge = \App\Libraries\Abdm\AbdmConnectorFactory::make();
$cardResult = $bridge->fetchOfficialAbhaCard([
'abha_number' => $abhaNum,
'abha_address' => $abhaAddress,
'token' => $userToken,
'x_token' => $userToken,
]);
if (! empty($cardResult['ok']) && ! empty($cardResult['card_base64'])) {
$cardBase64 = (string) $cardResult['card_base64'];
}
} catch (\Throwable $e) {
log_message('warning', '[ABHA] Bridge card fetch fallback failed: ' . $e->getMessage());
}
}
$abhaMeta = [
'abha_address' => $abhaAddress,
'profile_photo' => $photo,
'verified_status' => 'ACTIVE',
'verification_type' => 'VERIFIED',
'kyc_verified' => true,
'mobile_verified' => true,
'address' => $address,
'district' => $districtName,
'state' => $stateName,
'zip' => $zip,
'email' => $email,
];
$patientInfo = $this->tryAutoLinkByDirectMatch($abhaNum, $name, $profileMobile, $gender, $dob, $abhaMeta);
$responseBase = [
'ok' => 1,
'txn_id' => $result['txn_id'] ?? $txnId,
'token' => $result['token'] ?? '',
'accounts' => $result['accounts'] ?? [],
'card_base64' => $cardBase64,
'card_content_type' => 'image/png',
'card_source' => $cardBase64 !== '' ? 'abdm' : 'generated',
'card_message' => $cardBase64 !== '' ? 'Official ABHA Card from ABDM' : '',
'abha_number' => $abhaNum,
'name' => $name,
'photo' => $photo,
'mobile' => $profileMobile,
'gender' => $gender,
'dob' => $dob,
'abha_address' => $abhaAddress,
'verified_status' => 'ACTIVE',
'verification_type' => 'VERIFIED',
'kyc_verified' => true,
'mobile_verified' => true,
'address' => $address,
'district' => $districtName,
'state' => $stateName,
'zip' => $zip,
'email' => $email,
];
if ($patientInfo !== null) {
return $this->response->setJSON($responseBase + [
'need_confirmation' => false,
'patient_id' => $patientInfo['patient_id'],
'p_code' => $patientInfo['p_code'],
'is_new_patient' => $patientInfo['is_new'],
]);
}
$db = \Config\Database::connect();
$fields = $db->getFieldNames('patient_master') ?? [];
$abhaField = $this->resolveAbhaFieldName($fields);
$abhaNumClean = preg_replace('/\D/', '', $abhaNum);
$candidates = $this->findMatchingCandidates($db, $fields, $name, $profileMobile, $gender, $dob, '', $abhaField, $abhaNumClean);
return $this->response->setJSON($responseBase + [
'need_confirmation' => true,
'patient_id' => 0,
'p_code' => '',
'is_new_patient' => null,
'candidates' => $candidates,
]);
} catch (\Throwable $e) {
log_message('error', '[ABHA] findAadhaarVerifyOtp exception: ' . $e->getMessage());
return $this->response->setStatusCode(500)->setJSON([
'ok' => 0,
'error_text' => $e->getMessage(),
]);
}
}
// -------------------------------------------------------------------------
// Step 4 — ABHA address suggestions / assignment
// POST abha/create/address_suggestions, POST abha/create/address

View File

@ -418,6 +418,173 @@ class AbdmV3DirectHelper
}
}
/**
* M1 12.1 VRFY_ABHA_401: Request OTP using 12-digit Aadhaar Number.
* Upstream: POST /abha/api/v3/profile/login/request/otp
*
* @return array{ok: int, txn_id?: string, message?: string, error_text?: string, resend_after?: int}
*/
public function requestLoginOtpByAadhaar(string $aadhaarNumber): array
{
$digits = preg_replace('/\D/', '', $aadhaarNumber);
if (strlen($digits) !== 12) {
return ['ok' => 0, 'error_text' => 'Aadhaar Number is not valid.'];
}
try {
$token = $this->getSessionToken();
if ($token === '') {
return ['ok' => 0, 'error_text' => 'Could not obtain ABDM session token.'];
}
$encAadhaar = $this->encrypt($digits);
$payload = [
'scope' => ['abha-login', 'aadhaar-verify'],
'loginHint' => 'aadhaar',
'loginId' => $encAadhaar,
'otpSystem' => 'aadhaar',
];
$resp = $this->curlCall($this->otpUrl, 'POST', $payload, [
'Authorization: Bearer ' . $token,
'REQUEST-ID: ' . $this->generateUuid(),
'TIMESTAMP: ' . gmdate('Y-m-d\TH:i:s.000\Z'),
'X-Client-ID: ' . $this->clientId,
]);
if ($resp['code'] >= 200 && $resp['code'] < 300 && ! empty($resp['json'])) {
$newTxnId = (string) ($resp['json']['txnId'] ?? '');
$msg = (string) ($resp['json']['message'] ?? 'OTP sent to Aadhaar linked mobile number.');
return [
'ok' => 1,
'txn_id' => $newTxnId,
'message' => $msg,
'resend_after' => 60,
];
}
$errMsg = (string) ($resp['json']['message'] ?? $resp['json']['error']['message'] ?? '');
if ($errMsg === '') {
$errMsg = 'Failed to request Aadhaar OTP from ABDM.';
}
if (stripos($errMsg, 'invalid') !== false && stripos($errMsg, 'aadhaar') !== false) {
$errMsg = 'Aadhaar Number is not valid.';
}
return ['ok' => 0, 'error_text' => $errMsg, 'raw' => $resp['raw']];
} catch (\Throwable $e) {
log_message('error', '[AbdmV3DirectHelper] requestLoginOtpByAadhaar exception: ' . $e->getMessage());
return ['ok' => 0, 'error_text' => $e->getMessage()];
}
}
/**
* M1 12.1 VRFY_ABHA_402, 403, 404: Verify Aadhaar OTP and fetch complete ABHA profile + card.
* Upstream: POST /abha/api/v3/profile/login/verify
*
* @return array{ok: int, txn_id?: string, token?: string, accounts?: array, account?: array, full_profile?: array, card_base64?: string, no_abha_found?: bool, error_text?: string}
*/
public function verifyLoginOtpByAadhaar(string $otpTxnId, string $otp): array
{
$digits = preg_replace('/\D/', '', $otp);
if (strlen($digits) !== 6) {
return ['ok' => 0, 'error_text' => 'Enter a valid 6-digit OTP.'];
}
try {
$token = $this->getSessionToken();
if ($token === '') {
return ['ok' => 0, 'error_text' => 'Could not obtain ABDM session token.'];
}
$encOtp = $this->encrypt($digits);
$payload = [
'scope' => ['abha-login', 'aadhaar-verify'],
'authData' => [
'authMethods' => ['otp'],
'otp' => [
'txnId' => $otpTxnId,
'otpValue' => $encOtp,
],
],
];
$resp = $this->curlCall($this->verifyUrl, 'POST', $payload, [
'Authorization: Bearer ' . $token,
'REQUEST-ID: ' . $this->generateUuid(),
'TIMESTAMP: ' . gmdate('Y-m-d\TH:i:s.000\Z'),
'X-Client-ID: ' . $this->clientId,
]);
if ($resp['code'] >= 200 && $resp['code'] < 300 && ! empty($resp['json'])) {
$authResult = strtolower((string) ($resp['json']['authResult'] ?? ''));
if ($authResult === 'failed') {
$failMsg = (string) ($resp['json']['message'] ?? 'Incorrect OTP.');
return ['ok' => 0, 'error_text' => $failMsg];
}
$userToken = (string) ($resp['json']['token'] ?? '');
$accounts = (array) ($resp['json']['accounts'] ?? []);
$firstAcc = isset($accounts[0]) && is_array($accounts[0]) ? $accounts[0] : [];
// Check VRFY_ABHA_403: No ABHA user registered with this Aadhaar Number
if (empty($userToken) && empty($accounts)) {
return [
'ok' => 0,
'no_abha_found' => true,
'error_text' => 'NO ABHA user registered with this Aadhaar Number',
];
}
$fullProfile = [];
$cardBase64 = '';
if ($userToken !== '') {
$fullProfile = $this->fetchProfileDetails($userToken, $token);
$cardBase64 = $this->fetchCardPng($userToken, $token);
}
return [
'ok' => 1,
'txn_id' => (string) ($resp['json']['txnId'] ?? $otpTxnId),
'token' => $userToken,
'accounts' => $accounts,
'account' => $firstAcc,
'full_profile' => $fullProfile,
'card_base64' => $cardBase64,
];
}
$errMsg = (string) ($resp['json']['message'] ?? $resp['json']['error']['message'] ?? '');
// Check if error is VRFY_ABHA_403: ABHA does not exist for this Aadhaar
if (stripos($errMsg, 'no abha') !== false ||
stripos($errMsg, 'not found') !== false ||
stripos($errMsg, 'not exist') !== false ||
stripos($errMsg, 'no user') !== false ||
stripos($errMsg, 'no account') !== false) {
return [
'ok' => 0,
'no_abha_found' => true,
'error_text' => 'NO ABHA user registered with this Aadhaar Number',
'raw' => $resp['raw'],
];
}
if (stripos($errMsg, 'otp') !== false || stripos($errMsg, 'invalid') !== false || stripos($errMsg, 'incorrect') !== false) {
$errMsg = 'Incorrect OTP';
}
if ($errMsg === '') {
$errMsg = 'OTP verification failed.';
}
return ['ok' => 0, 'error_text' => $errMsg, 'raw' => $resp['raw']];
} catch (\Throwable $e) {
log_message('error', '[AbdmV3DirectHelper] verifyLoginOtpByAadhaar exception: ' . $e->getMessage());
return ['ok' => 0, 'error_text' => $e->getMessage()];
}
}
/**
* Fetch full profile details using the profile X-Token.
* Upstream: GET /abha/api/v3/profile/account

View File

@ -45,6 +45,7 @@
<?= view('partials/abha_create_modal') ?>
<?= view('partials/abha_qr_modal') ?>
<?= view('partials/abha_mobile_modal') ?>
<?= view('partials/abha_aadhaar_modal') ?>
<?= view('partials/abdm_hip_link_modal') ?>
<section class="content">
<div class="row">
@ -447,6 +448,7 @@
<button type="button" class="btn btn-success" onclick="abhaRegSelectMethod('number')"><i class="bi bi-person-vcard me-1"></i>By ABHA Number or ID</button>
<button type="button" class="btn btn-outline-primary" onclick="abhaRegSelectMethod('address')"><i class="bi bi-at me-1"></i>By ABHA Address</button>
<button type="button" class="btn btn-outline-secondary" onclick="abhaRegSelectMethod('mobile')"><i class="bi bi-phone me-1"></i>Find ABHA via Mobile</button>
<button type="button" class="btn btn-outline-info" onclick="abhaRegSelectMethod('aadhaar')"><i class="bi bi-fingerprint me-1"></i>Find ABHA via Aadhaar</button>
</div>
</div>
</div>
@ -1404,7 +1406,8 @@
address: { icon: '@', title: 'By ABHA Address', sub: 'Secure modal verification' },
qr: { icon: '📷', title: 'Scan Patient ABHA QR', sub: 'Scan QR → patient details' },
facility: { icon: '🏥', title: 'Scan Facility QR', sub: 'Patient scans via ABHA app' },
mobile: { icon: '📱', title: 'Find ABHA via Mobile', sub: 'Find an ABHA linked to this mobile number' }
mobile: { icon: '📱', title: 'Find ABHA via Mobile', sub: 'Find an ABHA linked to this mobile number' },
aadhaar: { icon: '🪪', title: 'Find ABHA via Aadhaar', sub: 'Retrieve ABHA using 12-digit Aadhaar number' }
};
window.abhaRegSelectMethod = function(method) {
@ -1428,10 +1431,29 @@
} else if (method === 'mobile') {
$('#abhareg_panel_mobile').addClass('d-none');
openAbhaMobileVerification();
} else if (method === 'aadhaar') {
$('#abhareg_panel_mobile').addClass('d-none');
openAbhaAadhaarVerification();
} else if (method === 'qr') { openAbhaQrScan(); }
if (method === 'facility') { loadFacilityQr(false); }
};
function openAbhaAadhaarVerification() {
if (!window.AbhaAadhaarModal) {
alert('ABHA Aadhaar verification modal is unavailable. Reload the page and try again.');
return;
}
window.AbhaAadhaarModal.open('', function(profile) {
if (profile && profile.need_confirmation === false && Number(profile.patient_id || 0) > 0) {
showRegResult(profile);
return;
}
window.AbhaPatientMatchModal.open(profile, profile.candidates || [], function(finalResponse) {
showRegResult(finalResponse);
});
});
}
function openAbhaMobileVerification() {
if (!window.AbhaMobileModal) {
alert('ABHA mobile verification modal is unavailable. Reload the page and try again.');

View File

@ -0,0 +1,553 @@
<?php
/**
* Partial: "Find ABHA by Aadhaar Number" modal.
*
* Compliant with ABDM M1 Guidelines (12.1 VRFY_ABHA_401 to VRFY_ABHA_405):
* VRFY_ABHA_401: Fetch ABHA details using Aadhaar Number
* VRFY_ABHA_402: Incorrect OTP handling
* VRFY_ABHA_403: ABHA Details not exists to Aadhaar Number -> Provision to create ABHA
* VRFY_ABHA_404: ABHA Details exists to Aadhaar Number -> Display Profile & Card
* VRFY_ABHA_405: Resend OTP Functionality (60s timer, max 2 times)
*
* Include once per page: <?= view('partials/abha_aadhaar_modal') ?>
* Then call: window.AbhaAadhaarModal.open(prefillAadhaar, function (profile) { ... });
*/
?>
<style>
.abha-aadhaar-modal .modal-content { border: 0; border-radius: 12px; overflow: hidden; }
.abha-aadhaar-modal .modal-header { border-bottom: 1px solid #e5eaf1; padding: 18px 22px; }
.abha-aadhaar-modal .modal-body { padding: 22px; }
.abha-aadhaar-steps { display: flex; justify-content: center; align-items: center; margin-bottom: 24px; }
.abha-aadhaar-step { display: flex; align-items: center; color: #8792a3; font-weight: 600; font-size: 0.9rem; }
.abha-aadhaar-step:not(:last-child)::after { content: ""; width: 44px; height: 2px; background: #dce3ec; margin: 0 8px; }
.abha-aadhaar-step.done:not(:last-child)::after { background: #0d6efd; }
.abha-aadhaar-step .step-num { width: 32px; height: 32px; display: grid; place-items: center; border-radius: 50%; background: #eef2f6; font-weight: 700; margin-right: 8px; flex-shrink: 0; }
.abha-aadhaar-step.active .step-num { color: #0d6efd; background: #fff; border: 2px solid #0d6efd; box-shadow: 0 0 0 4px #edf2ff; }
.abha-aadhaar-step.active { color: #0d6efd; }
.abha-aadhaar-step.done .step-num { color: #fff; background: #0d6efd; }
.abha-aadhaar-step.done { color: #0d6efd; }
.abha-aadhaar-step .step-title { font-weight: 600; white-space: nowrap; }
.abha-aadhaar-modal .abha-profile-list { margin: 0; }
.abha-aadhaar-modal .abha-profile-list > div { display: grid; grid-template-columns: 150px minmax(0, 1fr); gap: 14px; padding: 10px 0; border-bottom: 1px solid #edf0f4; }
.abha-aadhaar-modal .abha-profile-list dt { color: #6b7688; font-weight: 500; }
.abha-aadhaar-modal .abha-profile-list dd { margin: 0; text-align: right; font-weight: 600; overflow-wrap: anywhere; }
.abha-aadhaar-modal .abha-card-preview { min-height: 230px; display: grid; place-items: center; border: 1px solid #dce3ec; background: #f8fafc; border-radius: 8px; padding: 12px; }
.abha-aadhaar-modal .abha-card-preview img { max-width: 100%; max-height: 360px; object-fit: contain; border-radius: 6px; box-shadow: 0 4px 12px rgba(0,0,0,0.08); }
@media (max-width: 575.98px) {
.abha-aadhaar-modal .modal-body { padding: 16px; }
.abha-aadhaar-step:not(:last-child)::after { width: 20px; }
.abha-aadhaar-step .step-num { width: 26px; height: 26px; font-size: 0.8rem; margin-right: 4px; }
.abha-aadhaar-modal .abha-profile-list > div { grid-template-columns: 110px minmax(0, 1fr); }
}
</style>
<div class="modal fade abha-aadhaar-modal" id="abhaAadhaarModal" tabindex="-1" aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
<div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title fw-bold text-dark">
<i class="bi bi-fingerprint text-primary me-2"></i>Find ABHA by Aadhaar Number
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<!-- 3-Step Stepper -->
<div class="abha-aadhaar-steps" aria-label="Verification progress">
<div class="abha-aadhaar-step active" data-step="1">
<span class="step-num">1</span>
<span class="step-title">Aadhaar Number</span>
</div>
<div class="abha-aadhaar-step" data-step="2">
<span class="step-num">2</span>
<span class="step-title">Verify OTP</span>
</div>
<div class="abha-aadhaar-step" data-step="3">
<span class="step-num">3</span>
<span class="step-title">ABHA Profile</span>
</div>
</div>
<div id="abhaAadhaarAlert"></div>
<!-- Step 1: Aadhaar Number Input -->
<section id="abhaAadhaarStep1">
<div class="card border-0 bg-light p-4 rounded-3 mb-3">
<label class="form-label fw-semibold text-dark fs-6" for="abhaAadhaarInput">
Enter 12-Digit Aadhaar Number <span class="text-danger">*</span>
</label>
<div class="input-group input-group-lg mb-2">
<span class="input-group-text bg-white"><i class="bi bi-person-badge"></i></span>
<input type="text" class="form-control font-monospace" id="abhaAadhaarInput"
maxlength="14" inputmode="numeric" autocomplete="off"
placeholder="0000 0000 0000">
<button type="button" class="btn btn-primary px-4 fw-semibold" id="abhaAadhaarSendOtpBtn">
Send OTP <i class="bi bi-arrow-right ms-1"></i>
</button>
</div>
<div class="form-text text-muted mb-3">
<i class="bi bi-info-circle me-1"></i>System will retrieve the ABHA profile linked to this Aadhaar number via UIDAI OTP authentication.
</div>
<!-- Beneficiary Consent -->
<div class="border rounded p-3 bg-white mt-2">
<div class="form-check">
<input class="form-check-input" type="checkbox" id="abhaAadhaarConsent" checked>
<label class="form-check-label small text-muted" for="abhaAadhaarConsent">
I confirm that the patient/beneficiary has consented to authenticate using their Aadhaar Number to retrieve their ABHA profile as per ABDM guidelines.
</label>
</div>
</div>
</div>
</section>
<!-- Step 2: OTP Verification -->
<section id="abhaAadhaarStep2" class="d-none">
<div class="card border-0 bg-light p-4 rounded-3 mb-3">
<div class="d-flex align-items-center gap-2 mb-2">
<i class="bi bi-shield-lock fs-4 text-primary"></i>
<h6 class="mb-0 fw-bold">Enter Aadhaar OTP</h6>
</div>
<p class="text-muted small mb-3" id="abhaAadhaarOtpHint">
An OTP has been sent to the mobile number registered with your Aadhaar.
</p>
<div class="col-md-6 col-sm-8 mb-3">
<label class="form-label fw-semibold" for="abhaAadhaarOtpInput">6-digit OTP <span class="text-danger">*</span></label>
<div class="input-group input-group-lg">
<span class="input-group-text bg-white"><i class="bi bi-key"></i></span>
<input type="text" class="form-control text-center fw-bold fs-4" id="abhaAadhaarOtpInput"
maxlength="6" inputmode="numeric" autocomplete="one-time-code" placeholder="------">
</div>
</div>
<div class="d-flex justify-content-between align-items-center flex-wrap gap-2 mt-2">
<div>
<button type="button" class="btn btn-outline-secondary" id="abhaAadhaarBackBtn">
<i class="bi bi-arrow-left me-1"></i>Change Aadhaar
</button>
<button type="button" class="btn btn-link text-decoration-none px-2" id="abhaAadhaarResendBtn" disabled>
Resend OTP in 60s
</button>
</div>
<button type="button" class="btn btn-success btn-lg px-4" id="abhaAadhaarVerifyOtpBtn">
<i class="bi bi-patch-check me-1"></i>Verify OTP &amp; Fetch Profile
</button>
</div>
</div>
<!-- VRFY_ABHA_403: No ABHA user registered with this Aadhaar container -->
<div id="abhaAadhaarNotFoundCard" class="card border-warning bg-warning-subtle p-3 rounded-3 mt-3 d-none">
<div class="d-flex align-items-start gap-3">
<i class="bi bi-exclamation-triangle-fill text-warning fs-3 mt-1"></i>
<div class="flex-grow-1">
<h6 class="fw-bold text-dark mb-1">NO ABHA user registered with this Aadhaar Number</h6>
<p class="text-muted small mb-2">
Aadhaar verification was successful, but no ABHA account exists for this Aadhaar number in the ABDM registry.
</p>
<button type="button" class="btn btn-primary btn-sm" id="abhaAadhaarCreateNowBtn">
<i class="bi bi-plus-circle me-1"></i>Create ABHA with this Aadhaar
</button>
</div>
</div>
</div>
</section>
<!-- Step 3: Verified ABHA Profile Details -->
<section id="abhaAadhaarStep3" class="d-none">
<div class="alert alert-success py-2 mb-3">
<i class="bi bi-check-circle-fill me-2"></i>ABHA profile successfully retrieved and verified via Aadhaar!
</div>
<div class="row g-4">
<div class="col-lg-6">
<div class="d-flex align-items-center gap-3 mb-3">
<img id="abhaAadhaarPhoto" class="rounded border shadow-sm d-none" alt="ABHA photo" style="width: 80px; height: 80px; object-fit: cover;">
<div>
<span class="badge bg-success-subtle text-success border border-success-subtle mb-1">
<i class="bi bi-patch-check-fill me-1"></i>ABHA Verified
</span>
<h4 class="mb-0 fw-bold" id="abhaAadhaarProfileName">-</h4>
<div class="text-muted small font-monospace" id="abhaAadhaarProfileAddress">-</div>
</div>
</div>
<dl class="abha-profile-list">
<div><dt>ABHA Number</dt><dd id="abhaAadhaarProfileNumber">-</dd></div>
<div><dt>ABHA Address</dt><dd id="abhaAadhaarProfileId">-</dd></div>
<div><dt>Date of Birth</dt><dd id="abhaAadhaarProfileDob">-</dd></div>
<div><dt>Gender</dt><dd id="abhaAadhaarProfileGender">-</dd></div>
<div><dt>Mobile</dt><dd id="abhaAadhaarProfileMobile">-</dd></div>
<div><dt>Address</dt><dd id="abhaAadhaarProfileFullAddress">-</dd></div>
</dl>
</div>
<div class="col-lg-6">
<div class="abha-card-preview" id="abhaAadhaarCardWrap">
<div class="text-center text-muted">
<i class="bi bi-card-image fs-1 d-block mb-2 text-primary"></i>
<span id="abhaAadhaarCardMessage">Loading ABHA card...</span>
</div>
</div>
<a class="btn btn-outline-primary w-100 mt-2 d-none" id="abhaAadhaarDownloadCard" download="ABHA-Card.png">
<i class="bi bi-download me-1"></i>Download Official ABHA Card
</a>
</div>
</div>
<div class="d-flex justify-content-end gap-2 mt-4 pt-3 border-top">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Close</button>
<button type="button" class="btn btn-success" id="abhaAadhaarCompareBtn">
<i class="bi bi-people me-1"></i>Compare with HMS Patients
</button>
</div>
</section>
</div>
</div>
</div>
</div>
<script>
window.AbhaAadhaarModal = (function () {
'use strict';
var modal;
var onCompletedCallback = null;
var currentTxnId = '';
var currentAadhaar = '';
var verifiedProfile = null;
var resendTimer = null;
var resendRemaining = 0;
var resendAttempts = 0; // VRFY_ABHA_405: max 2 times
function csrf() {
var input = document.querySelector('input[name="<?= csrf_token() ?>"]');
return input ? input.value : '<?= csrf_hash() ?>';
}
function escapeHtml(val) {
return $('<div>').text(val == null ? '' : String(val)).html();
}
function digits(val) {
return String(val || '').replace(/\D/g, '');
}
function formatAadhaar(val) {
var d = digits(val).slice(0, 12);
var parts = [];
for (var i = 0; i < d.length; i += 4) {
parts.push(d.slice(i, i + 4));
}
return parts.join(' ');
}
function validateVerhoeff(num) {
num = digits(num);
if (num.length !== 12 || num.charAt(0) === '0' || num.charAt(0) === '1') {
return false;
}
var d = [
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
[1, 2, 3, 4, 0, 6, 7, 8, 9, 5],
[2, 3, 4, 0, 1, 7, 8, 9, 5, 6],
[3, 4, 0, 1, 2, 8, 9, 5, 6, 7],
[4, 0, 1, 2, 3, 9, 5, 6, 7, 8],
[5, 9, 8, 7, 6, 0, 4, 3, 2, 1],
[6, 5, 9, 8, 7, 1, 0, 4, 3, 2],
[7, 6, 5, 9, 8, 2, 1, 0, 4, 3],
[8, 7, 6, 5, 9, 3, 2, 1, 0, 4],
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
];
var p = [
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
[1, 5, 7, 6, 2, 8, 3, 0, 9, 4],
[5, 8, 0, 3, 7, 9, 6, 1, 4, 2],
[8, 9, 1, 6, 0, 4, 3, 5, 2, 7],
[9, 4, 5, 3, 1, 2, 6, 8, 7, 0],
[4, 2, 8, 6, 5, 7, 3, 9, 0, 1],
[2, 7, 9, 3, 8, 0, 6, 4, 1, 5],
[7, 0, 4, 6, 9, 1, 3, 2, 5, 8]
];
var c = 0;
var inverted = num.split('').reverse();
for (var i = 0; i < inverted.length; i++) {
c = d[c][p[i % 8][parseInt(inverted[i], 10)]];
}
return (c === 0);
}
function alertBox(type, msg) {
if (!msg) {
$('#abhaAadhaarAlert').empty();
return;
}
$('#abhaAadhaarAlert').html('<div class="alert alert-' + type + ' alert-dismissible fade show py-2 mb-3" role="alert">' +
msg + '<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button></div>');
}
function showStep(step) {
$('#abhaAadhaarStep1, #abhaAadhaarStep2, #abhaAadhaarStep3').addClass('d-none');
$('#abhaAadhaarStep' + step).removeClass('d-none');
$('.abha-aadhaar-step').each(function () {
var itemStep = Number($(this).data('step'));
$(this).toggleClass('active', itemStep === step).toggleClass('done', itemStep < step);
});
alertBox('', '');
}
function stopTimer() {
if (resendTimer) window.clearInterval(resendTimer);
resendTimer = null;
}
function startResendTimer(seconds) {
stopTimer();
resendRemaining = Math.max(60, Number(seconds) || 60);
var button = $('#abhaAadhaarResendBtn').prop('disabled', true);
function tick() {
if (resendRemaining <= 0) {
stopTimer();
if (resendAttempts < 2) {
var left = 2 - resendAttempts;
button.prop('disabled', false).text('Resend OTP (' + left + ' left)');
} else {
button.prop('disabled', true).text('Resend limit reached (max 2)');
}
return;
}
button.text('Resend OTP in ' + resendRemaining + 's');
resendRemaining--;
}
tick();
resendTimer = window.setInterval(tick, 1000);
}
function requestOtp(isResend) {
var rawAadhaar = digits($('#abhaAadhaarInput').val());
if (rawAadhaar.length !== 12 || !validateVerhoeff(rawAadhaar)) {
$('#abhaAadhaarInput').addClass('is-invalid').trigger('focus');
alertBox('danger', 'Aadhaar Number is not valid. Please enter a valid 12-digit Aadhaar Number.');
return;
}
$('#abhaAadhaarInput').removeClass('is-invalid');
if (!isResend && !$('#abhaAadhaarConsent').is(':checked')) {
alertBox('warning', 'Please provide beneficiary consent to proceed.');
return;
}
if (isResend) {
if (resendAttempts >= 2) {
alertBox('warning', 'Maximum 2 resend OTP attempts allowed. Please enter the OTP or try again later.');
return;
}
resendAttempts++;
} else {
resendAttempts = 0;
}
currentAadhaar = rawAadhaar;
var sendBtn = isResend ? $('#abhaAadhaarResendBtn') : $('#abhaAadhaarSendOtpBtn');
var originalBtnHtml = sendBtn.html();
sendBtn.prop('disabled', true).html('<span class="spinner-border spinner-border-sm me-1"></span>Sending OTP...');
$.post('<?= base_url('abha/find/aadhaar/request-otp') ?>', {
aadhaar: rawAadhaar,
'<?= csrf_token() ?>': csrf()
}, function (res) {
sendBtn.prop('disabled', false).html(originalBtnHtml);
if (!res || res.ok != 1) {
var err = (res && res.error_text) ? res.error_text : 'Aadhaar Number is not valid.';
alertBox('danger', escapeHtml(err));
return;
}
currentTxnId = res.txn_id || '';
var hintMsg = res.message || 'OTP sent to Aadhaar linked mobile number.';
$('#abhaAadhaarOtpHint').html('<i class="bi bi-info-circle me-1"></i>' + escapeHtml(hintMsg));
$('#abhaAadhaarOtpInput').val('');
$('#abhaAadhaarNotFoundCard').addClass('d-none');
showStep(2);
startResendTimer(res.resend_after || 60);
$('#abhaAadhaarOtpInput').trigger('focus');
}, 'json').fail(function (xhr) {
sendBtn.prop('disabled', false).html(originalBtnHtml);
var err = (xhr.responseJSON && xhr.responseJSON.error_text) ? xhr.responseJSON.error_text : 'Failed to request OTP from ABDM.';
alertBox('danger', escapeHtml(err));
});
}
function verifyOtp() {
var otp = digits($('#abhaAadhaarOtpInput').val());
if (otp.length !== 6) {
$('#abhaAadhaarOtpInput').addClass('is-invalid').trigger('focus');
alertBox('warning', 'Please enter a valid 6-digit OTP.');
return;
}
$('#abhaAadhaarOtpInput').removeClass('is-invalid');
var btn = $('#abhaAadhaarVerifyOtpBtn').prop('disabled', true)
.html('<span class="spinner-border spinner-border-sm me-1"></span>Verifying...');
$.post('<?= base_url('abha/find/aadhaar/verify-otp') ?>', {
txn_id: currentTxnId,
otp: otp,
aadhaar: currentAadhaar,
'<?= csrf_token() ?>': csrf()
}, function (res) {
btn.prop('disabled', false).html('<i class="bi bi-patch-check me-1"></i>Verify OTP &amp; Fetch Profile');
// VRFY_ABHA_403: ABHA details do not exist
if (res && res.no_abha_found) {
alertBox('warning', '<strong>NO ABHA user registered with this Aadhaar Number</strong>');
$('#abhaAadhaarNotFoundCard').removeClass('d-none');
return;
}
if (!res || res.ok != 1) {
var err = (res && res.error_text) ? res.error_text : 'Incorrect OTP';
if (/incorrect|invalid|fail/i.test(err)) {
err = 'Incorrect OTP';
}
alertBox('danger', escapeHtml(err));
return;
}
// VRFY_ABHA_404: ABHA exists -> render profile
renderProfile(res);
}, 'json').fail(function (xhr) {
btn.prop('disabled', false).html('<i class="bi bi-patch-check me-1"></i>Verify OTP &amp; Fetch Profile');
var err = (xhr.responseJSON && xhr.responseJSON.error_text) ? xhr.responseJSON.error_text : 'Incorrect OTP';
if (xhr.responseJSON && xhr.responseJSON.no_abha_found) {
alertBox('warning', '<strong>NO ABHA user registered with this Aadhaar Number</strong>');
$('#abhaAadhaarNotFoundCard').removeClass('d-none');
return;
}
alertBox('danger', escapeHtml(err));
});
}
function renderProfile(profile) {
verifiedProfile = profile;
stopTimer();
var photo = profile.photo || profile.profile_photo || '';
if (photo) {
var photoSrc = String(photo).indexOf('data:') === 0 ? photo : 'data:image/jpeg;base64,' + photo;
$('#abhaAadhaarPhoto').attr('src', photoSrc).removeClass('d-none');
} else {
$('#abhaAadhaarPhoto').addClass('d-none');
}
$('#abhaAadhaarProfileName').text(profile.name || '-');
$('#abhaAadhaarProfileAddress').text(profile.abha_address || '-');
$('#abhaAadhaarProfileNumber').text(profile.abha_number || '-');
$('#abhaAadhaarProfileId').text(profile.abha_address || '-');
$('#abhaAadhaarProfileDob').text(profile.dob || '-');
$('#abhaAadhaarProfileGender').text(profile.gender || '-');
$('#abhaAadhaarProfileMobile').text(profile.mobile || '-');
var fullAddr = [profile.address, profile.district, profile.state, profile.zip].filter(Boolean).join(', ');
$('#abhaAadhaarProfileFullAddress').text(fullAddr || '-');
// ABHA Card render
if (profile.card_base64) {
var cardSrc = String(profile.card_base64).indexOf('data:') === 0
? profile.card_base64
: 'data:image/png;base64,' + profile.card_base64;
$('#abhaAadhaarCardWrap').html('<img src="' + cardSrc + '" alt="ABHA Card">');
$('#abhaAadhaarDownloadCard').attr('href', cardSrc).removeClass('d-none');
} else {
$('#abhaAadhaarCardWrap').html('<div class="text-center text-muted p-4"><i class="bi bi-card-image fs-1 d-block mb-2"></i>Official ABHA card preview not available</div>');
$('#abhaAadhaarDownloadCard').addClass('d-none');
}
showStep(3);
}
function bindEvents() {
$('#abhaAadhaarInput').on('input', function () {
var formatted = formatAadhaar($(this).val());
$(this).val(formatted);
$(this).removeClass('is-invalid');
});
$('#abhaAadhaarOtpInput').on('input', function () {
$(this).val(digits($(this).val()).slice(0, 6));
$(this).removeClass('is-invalid');
});
$('#abhaAadhaarSendOtpBtn').on('click', function () {
requestOtp(false);
});
$('#abhaAadhaarResendBtn').on('click', function () {
requestOtp(true);
});
$('#abhaAadhaarVerifyOtpBtn').on('click', function () {
verifyOtp();
});
$('#abhaAadhaarBackBtn').on('click', function () {
stopTimer();
showStep(1);
});
// Switch to Create ABHA when not found (VRFY_ABHA_403)
$('#abhaAadhaarCreateNowBtn').on('click', function () {
modal.hide();
if (window.AbhaCreateModal) {
window.AbhaCreateModal.open(function (created) {
if (typeof onCompletedCallback === 'function') {
onCompletedCallback(created);
}
});
setTimeout(function () {
$('#abhaCreateAadhaar').val(formatAadhaar(currentAadhaar)).trigger('input');
}, 300);
} else {
alert('ABHA Create Modal is unavailable.');
}
});
$('#abhaAadhaarCompareBtn').on('click', function () {
if (!verifiedProfile) return;
modal.hide();
if (typeof onCompletedCallback === 'function') {
onCompletedCallback(verifiedProfile);
}
});
}
return {
open: function (prefillAadhaar, callback) {
onCompletedCallback = callback;
currentTxnId = '';
currentAadhaar = '';
verifiedProfile = null;
resendAttempts = 0;
stopTimer();
if (!modal) {
var el = document.getElementById('abhaAadhaarModal');
if (el && window.bootstrap && window.bootstrap.Modal) {
modal = new bootstrap.Modal(el);
}
bindEvents();
}
$('#abhaAadhaarInput').val(prefillAadhaar ? formatAadhaar(prefillAadhaar) : '').removeClass('is-invalid');
$('#abhaAadhaarOtpInput').val('').removeClass('is-invalid');
$('#abhaAadhaarConsent').prop('checked', true);
$('#abhaAadhaarNotFoundCard').addClass('d-none');
$('#abhaAadhaarPhoto, #abhaAadhaarDownloadCard').addClass('d-none');
showStep(1);
if (modal) {
modal.show();
}
}
};
})();
</script>