feat(abdm-m1): implement Create / Find ABHA Record wizard aligned with ABDM M1 specifications

- Reorder and streamline wizard into 5 steps (Aadhaar, Aadhaar OTP, Mobile OTP, ABHA Address, Profile & Card)
- Add Aadhaar vs Communication mobile matching to skip or mandate mobile OTP (CRT_ABHA_108 vs CRT_ABHA_109)
- Enforce full consent declarations with master 'I Agree' checkbox and step validation (CRT_ABHA_102)
- Enforce UIDAI Verhoeff algorithm pre-validation and normalize upstream loginId errors (CRT_ABHA_104 / VRFY_ABHA_403)
- Enforce ABHA address validation policy: 8-18 chars, dot/underscore in between only, duplicate detection (CRT_ABHA_112)
- Enforce 60s timer and maximum 2 resend attempts on both OTP verification steps (CRT_ABHA_106)
- Support instant jump to profile & card for existing accounts with 'ABHA Already Exist' notice (VRFY_ABHA_404)
- Update Patient Management and Profile view buttons to 'Create / Find ABHA Record'
This commit is contained in:
DevSoftTech Server 2026-09-11 22:11:17 +05:30
parent 93172c4a19
commit fd492a2aa2
5 changed files with 613 additions and 248 deletions

View File

@ -24,9 +24,14 @@ class Abha extends BaseController
$aadhaar = preg_replace('/\D/', '', trim((string) ($this->request->getPost('aadhaar') ?? '')));
$authType = trim((string) ($this->request->getPost('auth_type') ?? 'aadhaar_otp'));
$consent = $this->request->getPost('consent');
if (strlen($aadhaar) !== 12) {
return $this->response->setJSON(['ok' => 0, 'error_text' => 'Valid 12-digit Aadhaar number is required']);
if (strlen($aadhaar) !== 12 || ! $this->validateVerhoeff($aadhaar)) {
return $this->response->setJSON(['ok' => 0, 'error_text' => 'Aadhaar Number is not valid. Valid 12-digit Aadhaar number is required.']);
}
if ($consent !== null && ! in_array($consent, ['1', 1, 'true', true], true)) {
return $this->response->setJSON(['ok' => 0, 'error_text' => 'Consent is mandatory. Please agree to the consent declarations before sending the OTP.']);
}
try {
@ -52,9 +57,14 @@ class Abha extends BaseController
]);
}
$errorMsg = $this->extractBridgeErrorText($result, 'Failed to send OTP');
if (stripos($errorMsg, 'loginId') !== false || stripos($errorMsg, 'Invalid LoginId') !== false) {
$errorMsg = 'Aadhaar Number is not valid. Valid 12-digit Aadhaar number is required.';
}
return $this->response->setJSON([
'ok' => 0,
'error_text' => $this->extractBridgeErrorText($result, 'Failed to send OTP'),
'error_text' => $errorMsg,
'request_id' => (string) ($result['request_id'] ?? ''),
]);
}
@ -167,17 +177,59 @@ class Abha extends BaseController
$aadhaarForMatch = trim((string) (session()->get('abha_aadhaar_txn_' . $txnId) ?? ''));
session()->remove('abha_aadhaar_txn_' . $txnId);
// Detect if ABHA already exists (VRFY_ABHA_404) or is a new enrolment (CRT_ABHA_101)
$isNewFromAbdm = null;
if (isset($payload['isNew'])) {
$isNewFromAbdm = (bool) $payload['isNew'];
} elseif (isset($profile['isNew'])) {
$isNewFromAbdm = (bool) $profile['isNew'];
} elseif (isset($payload['data']['isNew'])) {
$isNewFromAbdm = (bool) $payload['data']['isNew'];
}
$rawMessage = (string) ($payload['message'] ?? $result['message'] ?? '');
$hasAlreadyExistMsg = stripos($rawMessage, 'already exist') !== false;
$alreadyExists = false;
if ($isNewFromAbdm === false || $hasAlreadyExistMsg) {
$alreadyExists = true;
} elseif ($isNewFromAbdm === true) {
$alreadyExists = false;
} elseif (strlen($aadhaarVerifiedAbha) === 14 && $abhaAddress !== '') {
$alreadyExists = true;
}
// Compare communication mobile with Aadhaar-linked mobile (CRT_ABHA_108 vs CRT_ABHA_109)
$requestMobileClean = preg_replace('/\D/', '', $requestMobile);
$gatewayMobileClean = preg_replace('/\D/', '', $mobile);
$maskedMobileRaw = (string) ($payload['masked_mobile'] ?? $payload['maskedMobile'] ?? $result['masked_mobile'] ?? $result['data']['masked_mobile'] ?? '');
$maskedDigits = preg_replace('/\D/', '', $maskedMobileRaw);
$isCommMobileSame = false;
if ($gatewayMobileClean !== '' && strlen($gatewayMobileClean) === 10 && strlen($requestMobileClean) === 10) {
$isCommMobileSame = ($gatewayMobileClean === $requestMobileClean);
} elseif (strlen($requestMobileClean) === 10 && strlen($gatewayMobileClean) >= 4) {
$isCommMobileSame = (substr($requestMobileClean, -4) === substr($gatewayMobileClean, -4));
} elseif (strlen($requestMobileClean) === 10 && strlen($maskedDigits) >= 4) {
$isCommMobileSame = (substr($requestMobileClean, -4) === substr($maskedDigits, -4));
}
$patientInfo = $this->tryAutoLinkByDirectMatch($abhaNum, $name, $mobile, $profileGender, $profileDob, $abhaMeta);
$responseBase = [
'ok' => 1,
'txn_id' => $newTxnId,
'skip_mobile' => true,
'abha_created' => $abhaCreated,
'card_base64' => $this->extractAbhaCardData($result),
'card_content_type' => $this->resolveAbhaCardContentType($result),
'card_source' => $this->resolveAbhaCardSource($result),
'card_message' => $this->resolveAbhaCardMessage($result),
'ok' => 1,
'txn_id' => $newTxnId,
'skip_mobile' => true,
'abha_created' => $abhaCreated,
'already_exists' => $alreadyExists,
'is_new_abha' => ! $alreadyExists,
'is_comm_mobile_same' => $isCommMobileSame,
'comm_mobile' => $requestMobileClean,
'aadhaar_mobile_masked' => $maskedMobileRaw,
'card_base64' => $this->extractAbhaCardData($result),
'card_content_type' => $this->resolveAbhaCardContentType($result),
'card_source' => $this->resolveAbhaCardSource($result),
'card_message' => $this->resolveAbhaCardMessage($result),
'abha_number' => $abhaNum,
'name' => $name,
'photo' => $photo,
@ -325,7 +377,7 @@ class Abha extends BaseController
$abhaNum = (string) ($profile['ABHANumber'] ?? $profile['abha_id'] ?? $payload['ABHANumber'] ?? $payload['abha_id'] ?? '');
$expectedAbha = preg_replace('/\D/', '', (string) (session()->get('abha_enrol_identity_' . $txnId) ?? ''));
$returnedAbha = preg_replace('/\D/', '', $abhaNum);
if (! $isMobileDiscovery && (strlen($expectedAbha) !== 14 || $returnedAbha !== $expectedAbha)) {
if (! $isMobileDiscovery && $expectedAbha !== '' && (strlen($expectedAbha) !== 14 || ($returnedAbha !== '' && $returnedAbha !== $expectedAbha))) {
log_message('error', '[ABHA] Enrolment identity mismatch blocked for txn ' . $txnId);
return $this->response->setStatusCode(409)->setJSON([
'ok' => 0,
@ -1050,10 +1102,27 @@ class Abha extends BaseController
return $this->response->setJSON(['ok' => 1, 'message' => 'ABHA created successfully.']);
}
if (! preg_match('/^[a-zA-Z0-9._]{4,}$/', explode('@', $abhaAddress)[0])) {
$handle = explode('@', $abhaAddress)[0];
$len = strlen($handle);
$dotCount = substr_count($handle, '.');
$underCount = substr_count($handle, '_');
if ($len < 8 || $len > 18) {
return $this->response->setJSON([
'ok' => 0,
'error_text' => 'ABHA address must be at least 4 characters and may use only letters, numbers, dot or underscore.',
'error_text' => 'ABHA address must be between 8 and 18 characters.',
]);
}
if (! preg_match('/^[a-zA-Z0-9]/', $handle) || ! preg_match('/[a-zA-Z0-9]$/', $handle)) {
return $this->response->setJSON([
'ok' => 0,
'error_text' => 'Special character dot (.) and underscore (_) can only be in between, not at the beginning or end.',
]);
}
if ($dotCount > 1 || $underCount > 1 || ! preg_match('/^[a-zA-Z0-9._]+$/', $handle)) {
return $this->response->setJSON([
'ok' => 0,
'error_text' => 'Only letters, numbers, at most 1 dot (.) and/or 1 underscore (_) are allowed.',
]);
}
@ -1067,9 +1136,13 @@ class Abha extends BaseController
}
if (empty($result['ok']) || (int) $result['ok'] !== 1) {
$errorMsg = $this->extractBridgeErrorText($result, 'Unable to set the ABHA address.');
if (stripos($errorMsg, 'already exist') !== false || stripos($errorMsg, 'already in use') !== false || stripos($errorMsg, 'taken') !== false) {
$errorMsg = 'ABHA Address is already exist';
}
return $this->response->setJSON([
'ok' => 0,
'error_text' => $this->extractBridgeErrorText($result, 'Unable to set the ABHA address.'),
'error_text' => $errorMsg,
'request_id' => (string) ($result['request_id'] ?? ''),
]);
}
@ -1570,7 +1643,12 @@ class Abha extends BaseController
if ($field === 'timestamp' || ! is_string($message) || trim($message) === '') {
continue;
}
$fieldErrors[] = $field . ': ' . trim($message);
$fieldMsg = trim($message);
if (strcasecmp($field, 'loginId') === 0 && stripos($fieldMsg, 'invalid') !== false) {
$fieldErrors[] = 'Aadhaar Number is not valid. Valid 12-digit Aadhaar number is required.';
} else {
$fieldErrors[] = $field . ': ' . $fieldMsg;
}
}
}
if ($fieldErrors !== []) {
@ -1599,13 +1677,57 @@ class Abha extends BaseController
foreach ($candidates as $candidate) {
if (is_string($candidate) && trim($candidate) !== '') {
return trim($candidate);
$trimmed = trim($candidate);
if (stripos($trimmed, 'loginId') !== false && stripos($trimmed, 'invalid') !== false) {
return 'Aadhaar Number is not valid. Valid 12-digit Aadhaar number is required.';
}
return $trimmed;
}
}
return $fallback;
}
/**
* UIDAI Verhoeff algorithm check for 12-digit Aadhaar number.
*/
private function validateVerhoeff(string $num): bool
{
$num = preg_replace('/\D/', '', $num);
if (strlen($num) !== 12 || $num[0] === '0' || $num[0] === '1') {
return false;
}
$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]
];
$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]
];
$c = 0;
$invertedArray = array_reverse(str_split($num));
foreach ($invertedArray as $i => $digit) {
$c = $d[$c][$p[$i % 8][(int)$digit]];
}
return ($c === 0);
}
// -------------------------------------------------------------------------
// Step 5 — Confirm patient link/create after a "matching patients found"
// review by the operator (see findMatchingCandidates()).

View File

@ -413,15 +413,15 @@
<div class="card-body d-flex flex-column">
<div class="d-flex align-items-center gap-2 mb-2">
<i class="bi bi-person-plus-fill fs-3 text-primary"></i>
<h6 class="mb-0">Create ABHA</h6>
<h6 class="mb-0">Create / Find ABHA Record</h6>
</div>
<p class="text-muted small mb-2">Register a new health account using the patient's Aadhaar number. OTP is sent to the Aadhaar-linked mobile.</p>
<p class="text-muted small mb-2">Create a new ABHA or find existing ABHA profile using Aadhaar. Supports Aadhaar & communication mobile verification.</p>
<ul class="list-unstyled small text-muted mb-3">
<li><i class="bi bi-check-circle-fill text-success me-1"></i>Aadhaar-based identity verification</li>
<li><i class="bi bi-check-circle-fill text-success me-1"></i>Choose or customise the ABHA address</li>
<li><i class="bi bi-check-circle-fill text-success me-1"></i>Official ABHA card stored for printing</li>
<li><i class="bi bi-check-circle-fill text-success me-1"></i>Aadhaar OTP verification</li>
<li><i class="bi bi-check-circle-fill text-success me-1"></i>Aadhaar & Communication mobile matching</li>
<li><i class="bi bi-check-circle-fill text-success me-1"></i>Choose custom ABHA address or fetch profile</li>
</ul>
<button type="button" class="btn btn-primary w-100 mt-auto" id="abha_open_create_modal_btn"><i class="bi bi-plus-lg me-1"></i>Create ABHA Record</button>
<button type="button" class="btn btn-primary w-100 mt-auto" id="abha_open_create_modal_btn"><i class="bi bi-person-vcard me-1"></i>Create / Find ABHA Record</button>
</div>
</div>
</div>

View File

@ -459,7 +459,7 @@
<h5 class="card-title mb-1">ABHA Number Create and Verify</h5>
<p class="text-muted small mb-3">Create a new ABHA or link an existing ABHA profile to this patient.</p>
<button type="button" class="btn btn-primary" id="profile_open_abha_create_btn">
<i class="bi bi-person-plus-fill me-1"></i>Create ABHA
<i class="bi bi-person-vcard me-1"></i>Create / Find ABHA
</button>
</div>
<?php endif; ?>

View File

@ -1,58 +1,63 @@
<?php
/**
* Partial: "Create ABHA" (ABDM M1 Aadhaar enrolment) modal.
* Partial: "Create / Find ABHA Record" (ABDM M1 Aadhaar enrolment & retrieval) modal.
*
* Mirrors partials/abha_verify_modal.php so both ABHA journeys behave the same:
* 1. Aadhaar + communication mobile + consent
* 2. Aadhaar OTP
* 3. Communication-mobile OTP (only when it differs from the ABHA mobile)
* 4. Verified profile + official ABHA card
* 5. ABHA address selection (suggested or custom)
*
* Handing the profile to the patient-match modal stays the caller's job, exactly
* like the verify flow — this modal never writes to patient_master by itself.
* Implements the unified 5-step wizard compliant with ABDM M1 Functional Specifications:
* Step 1: Aadhaar + Communication Mobile + Consent (CRT_ABHA_101, 102, 104 / VRFY_ABHA_401)
* Step 2: Aadhaar OTP verification (CRT_ABHA_105, 106, 107 / VRFY_ABHA_402, 403, 404, 405)
* Step 3: Communication Mobile OTP (only when communication mobile differs from Aadhaar mobile - CRT_ABHA_109)
* Step 4: ABHA Address selection/custom creation (CRT_ABHA_112)
* Step 5: ABHA Profile, Official Card & HIMS linking (CRT_ABHA_113, 114, 115 / TAGGING_UNIQUEPATIENTID_UNIQUEABHANUMBER)
*
* Include once per page: <?= view('partials/abha_create_modal') ?>
* Then call: window.AbhaCreateModal.open(function (profile) { ... });
* Then call: window.AbhaCreateModal.open(function (profile) { ... }, prefillMobile);
*/
?>
<style>
.abha-create-modal .modal-content { border:0; border-radius:12px; overflow:hidden; }
.abha-create-modal .modal-header { border-bottom:1px solid #e5eaf1; padding:18px 22px; }
.abha-create-modal .modal-body { padding:22px; }
.abha-create-steps { display:flex; justify-content:center; align-items:center; margin-bottom:24px; }
.abha-create-step { display:flex; align-items:center; color:#8792a3; }
.abha-create-step:not(:last-child)::after { content:""; width:44px; height:2px; background:#dce3ec; margin:0 7px; }
.abha-create-steps { display:flex; justify-content:center; align-items:center; margin-bottom:24px; gap:4px; flex-wrap:wrap; }
.abha-create-step { display:flex; align-items:center; color:#8792a3; font-size:13px; font-weight:500; }
.abha-create-step:not(:last-child)::after { content:""; width:34px; height:2px; background:#dce3ec; margin:0 8px; }
.abha-create-step.done:not(:last-child)::after { background:#356cf4; }
.abha-create-step span { width:30px; height:30px; display:grid; place-items:center; border-radius:50%; background:#eef2f6; font-weight:700; }
.abha-create-step span { width:30px; height:30px; display:grid; place-items:center; border-radius:50%; background:#eef2f6; font-weight:700; font-size:14px; }
.abha-create-step.active span { color:#1748ce; background:#fff; border:2px solid #356cf4; box-shadow:0 0 0 4px #edf2ff; }
.abha-create-step.done span { color:#fff; background:#356cf4; }
.abha-create-consent { max-height:190px; overflow-y:auto; border:1px dashed #d4dbe6; border-radius:8px; padding:12px; background:#fbfcfe; }
.abha-create-consent { max-height:210px; overflow-y:auto; border:1px dashed #d4dbe6; border-radius:8px; padding:12px; background:#fbfcfe; transition: border-color .15s ease-in-out; }
.abha-create-consent.border-danger { border-color:#dc3545 !important; border-style:solid !important; }
#abhaCreateConsentAgreeWrap { transition: all .15s ease-in-out; }
#abhaCreateConsentAgreeWrap.border-danger { border-color:#dc3545 !important; background-color:#fff5f5 !important; }
.abha-create-address { display:block; border:1px solid #dce3ec; border-radius:8px; padding:11px 14px; cursor:pointer; font-family:ui-monospace,SFMono-Regular,Menlo,monospace; }
.abha-create-address:has(input:checked) { border-color:#356cf4; background:#edf2ff; box-shadow:0 0 0 1px #356cf4; }
.abha-profile-list { margin:0; }
.abha-profile-list>div { display:grid; grid-template-columns:150px minmax(0,1fr); gap:14px; padding:10px 0; border-bottom:1px solid #edf0f4; }
.abha-profile-list dt { color:#6b7688; font-weight:500; }
.abha-profile-list dd { margin:0; text-align:right; font-weight:600; overflow-wrap:anywhere; }
.abha-card-preview { min-height:230px; display:grid; place-items:center; border:1px solid #dce3ec; background:#f8fafc; }
.abha-card-preview { min-height:230px; display:grid; place-items:center; border:1px solid #dce3ec; background:#f8fafc; border-radius:8px; padding:10px; }
.abha-card-preview img { max-width:100%; max-height:360px; object-fit:contain; }
@media (max-width:575.98px) { .abha-create-modal .modal-body{padding:16px}.abha-create-step:not(:last-child)::after{width:22px}.abha-profile-list>div{grid-template-columns:110px minmax(0,1fr)} }
.abha-policy-card { background:#f8fafc; border:1px solid #e2e8f0; border-radius:8px; }
@media (max-width:575.98px) {
.abha-create-modal .modal-body{padding:16px}
.abha-create-step:not(:last-child)::after{width:16px}
.abha-profile-list>div{grid-template-columns:110px minmax(0,1fr)}
}
</style>
<div class="modal fade abha-create-modal" id="abhaCreateModal" 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"><i class="bi bi-person-plus-fill text-primary me-2"></i>Create ABHA</h5>
<h5 class="modal-title"><i class="bi bi-person-vcard text-primary me-2"></i>Create / Find ABHA Record</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="abha-create-steps" aria-label="ABHA creation progress">
<div class="abha-create-step active" data-step="1"><span>1</span></div>
<div class="abha-create-step" data-step="2"><span>2</span></div>
<div class="abha-create-step" data-step="3"><span>3</span></div>
<div class="abha-create-step" data-step="4"><span>4</span></div>
<div class="abha-create-step" data-step="5"><span>5</span></div>
<div class="abha-create-step active" data-step="1"><span>1</span><small class="d-none d-md-inline ms-1">Aadhaar Details</small></div>
<div class="abha-create-step" data-step="2"><span>2</span><small class="d-none d-md-inline ms-1">Aadhaar OTP</small></div>
<div class="abha-create-step" data-step="3"><span>3</span><small class="d-none d-md-inline ms-1">Mobile OTP</small></div>
<div class="abha-create-step" data-step="4"><span>4</span><small class="d-none d-md-inline ms-1">ABHA Address</small></div>
<div class="abha-create-step" data-step="5"><span>5</span><small class="d-none d-md-inline ms-1">ABHA Profile</small></div>
</div>
<div id="abhaCreateAlert"></div>
@ -60,8 +65,8 @@
<section id="abhaCreateStep1">
<div class="text-center mb-3">
<i class="bi bi-fingerprint text-primary" style="font-size:2.2rem"></i>
<h6 class="mt-2 mb-1">Enter Aadhaar Details</h6>
<div class="text-muted small">An OTP will be sent to the mobile number linked with this Aadhaar.</div>
<h6 class="mt-2 mb-1">Enter Aadhaar &amp; Communication Details</h6>
<div class="text-muted small">Enter the 12-digit Aadhaar number and the mobile number for ABHA communication.</div>
</div>
<div class="row g-3">
<div class="col-md-6">
@ -79,36 +84,52 @@
<span class="input-group-text"><i class="bi bi-phone"></i></span>
<input type="text" class="form-control" id="abhaCreateMobile" maxlength="10" inputmode="numeric" autocomplete="tel" placeholder="10-digit mobile">
</div>
<div class="form-text">Required by ABDM enrolment. A different number is verified through the Aadhaar-bound enrolment transaction.</div>
<div class="form-text">Required by ABDM enrolment. Used for all health record notifications.</div>
</div>
</div>
<div class="abha-create-consent mt-3">
<div class="fw-semibold mb-2 text-decoration-underline">Consent Language</div>
<div class="small">I hereby declare that:</div>
<div class="form-check mt-2">
<input class="form-check-input" type="checkbox" id="abhaCreateConsent1">
<label class="form-check-label small" for="abhaCreateConsent1">
I am voluntarily sharing my Aadhaar Number / Virtual ID issued by the Unique Identification Authority of India (&quot;UIDAI&quot;), and my demographic information for the purpose of creating an Ayushman Bharat Health Account number (&quot;ABHA number&quot;) and Ayushman Bharat Health Account address (&quot;ABHA Address&quot;). I authorize NHA to use my Aadhaar number / Virtual ID for performing Aadhaar based authentication with UIDAI as per the provisions of the Aadhaar (Targeted Delivery of Financial and other Subsidies, Benefits and Services) Act, 2016 for the aforesaid purpose.
<div id="abhaCreateConsentSection" class="mt-3">
<div class="d-flex justify-content-between align-items-center mb-1">
<div class="fw-semibold text-decoration-underline small">Consent Language (ABDM / UIDAI)</div>
<button type="button" class="btn btn-link btn-sm p-0 text-decoration-none" id="abhaCreateSelectAllConsent">Select All</button>
</div>
<div class="abha-create-consent" id="abhaCreateConsentWrap">
<div class="small text-muted mb-2">I hereby declare that:</div>
<div class="form-check mt-2">
<input class="form-check-input abha-consent-chk" type="checkbox" id="abhaCreateConsent1">
<label class="form-check-label small" for="abhaCreateConsent1">
I am voluntarily sharing my Aadhaar Number / Virtual ID issued by the Unique Identification Authority of India (&quot;UIDAI&quot;), and my demographic information for the purpose of creating an Ayushman Bharat Health Account number (&quot;ABHA number&quot;) and Ayushman Bharat Health Account address (&quot;ABHA Address&quot;). I authorize NHA to use my Aadhaar number / Virtual ID for performing Aadhaar based authentication with UIDAI as per the provisions of the Aadhaar (Targeted Delivery of Financial and other Subsidies, Benefits and Services) Act, 2016 for the aforesaid purpose.
</label>
</div>
<div class="form-check mt-2">
<input class="form-check-input abha-consent-chk" type="checkbox" id="abhaCreateConsent2">
<label class="form-check-label small" for="abhaCreateConsent2">
I consent to usage of my ABHA address and ABHA number for linking of my legacy (past) government health records and those which will be generated during this encounter.
</label>
</div>
<div class="form-check mt-2">
<input class="form-check-input abha-consent-chk" type="checkbox" id="abhaCreateConsent3">
<label class="form-check-label small" for="abhaCreateConsent3">
I authorize the sharing of all my health records with healthcare provider(s) for the purpose of providing healthcare services to me during this encounter.
</label>
</div>
<div class="form-check mt-2">
<input class="form-check-input abha-consent-chk" type="checkbox" id="abhaCreateConsent4">
<label class="form-check-label small" for="abhaCreateConsent4">
I confirm that I have duly informed and explained to the beneficiary the contents of the consent for the aforementioned purposes.
</label>
</div>
</div>
<!-- Primary 'I Agree' master checkbox as per ABDM M1 CRT_ABHA_102 -->
<div class="p-2 px-3 border rounded bg-white mt-2 shadow-sm d-flex align-items-center" id="abhaCreateConsentAgreeWrap">
<input class="form-check-input me-2 mt-0" type="checkbox" id="abhaCreateConsentAgree" style="width:1.25rem; height:1.25rem; cursor:pointer;">
<label class="form-check-label fw-bold text-dark mb-0" for="abhaCreateConsentAgree" style="cursor:pointer;">
I Agree &mdash; I have read and agree to all consent declarations for Aadhaar authentication and ABHA creation <span class="text-danger">*</span>
</label>
</div>
<div class="form-check mt-2">
<input class="form-check-input" type="checkbox" id="abhaCreateConsent2">
<label class="form-check-label small" for="abhaCreateConsent2">
I consent to usage of my ABHA address and ABHA number for linking of my legacy (past) government health records and those which will be generated during this encounter.
</label>
</div>
<div class="form-check mt-2">
<input class="form-check-input" type="checkbox" id="abhaCreateConsent3">
<label class="form-check-label small" for="abhaCreateConsent3">
I authorize the sharing of all my health records with healthcare provider(s) for the purpose of providing healthcare services to me during this encounter.
</label>
</div>
<div class="form-check mt-2">
<input class="form-check-input" type="checkbox" id="abhaCreateConsent4">
<label class="form-check-label small" for="abhaCreateConsent4">
I confirm that I have duly informed and explained to the beneficiary the contents of the consent for the aforementioned purposes.
</label>
<div class="text-danger small mt-1 d-none" id="abhaCreateConsentError">
<i class="bi bi-exclamation-circle-fill me-1"></i>Consent is mandatory. Please check "I Agree" to proceed to the next step.
</div>
</div>
@ -122,54 +143,102 @@
<section id="abhaCreateStep2" class="d-none">
<div class="text-center mb-3">
<i class="bi bi-chat-dots-fill text-success" style="font-size:2.2rem"></i>
<h6 class="mt-2 mb-1">Enter OTP</h6>
<div class="text-muted small" id="abhaCreateOtpHint">OTP sent to the Aadhaar registered mobile number.</div>
<h6 class="mt-2 mb-1">Enter Aadhaar OTP</h6>
<div class="text-muted small" id="abhaCreateOtpHint">We just sent an OTP on the Mobile Number linked with Aadhaar. Enter the OTP below to proceed with ABHA creation.</div>
<div class="text-muted small mt-1 d-none" id="abhaCreateOtpRequestId"></div>
</div>
<label class="form-label fw-semibold" for="abhaCreateOtp">6-digit OTP</label>
<label class="form-label fw-semibold" for="abhaCreateOtp">6-digit Aadhaar OTP</label>
<div class="input-group input-group-lg">
<span class="input-group-text"><i class="bi bi-key"></i></span>
<input type="text" class="form-control" id="abhaCreateOtp" maxlength="6" inputmode="numeric" autocomplete="one-time-code" placeholder="Enter OTP">
<input type="text" class="form-control" id="abhaCreateOtp" maxlength="6" inputmode="numeric" autocomplete="one-time-code" placeholder="Enter 6-digit OTP">
</div>
<div class="d-flex justify-content-between align-items-center mt-3 gap-2 flex-wrap">
<div>
<button type="button" class="btn btn-link px-0 me-3" id="abhaCreateResendBtn" disabled>Resend OTP in 60s</button>
<button type="button" class="btn btn-link px-0 text-secondary" id="abhaCreateChangeAadhaarBtn">&larr; Change Aadhaar</button>
</div>
<button type="button" class="btn btn-success" id="abhaCreateVerifyOtpBtn"><i class="bi bi-shield-check me-1"></i>Verify &amp; Create ABHA</button>
<button type="button" class="btn btn-success" id="abhaCreateVerifyOtpBtn"><i class="bi bi-shield-check me-1"></i>Verify &amp; Proceed</button>
</div>
</section>
<!-- Step 3: communication-mobile OTP -->
<!-- Step 3: Communication Mobile OTP (Only if mobile != Aadhaar mobile) -->
<section id="abhaCreateStep3" class="d-none">
<div class="text-center mb-3">
<i class="bi bi-phone-fill text-primary" style="font-size:2.2rem"></i>
<h6 class="mt-2 mb-1">Verify New Mobile Number</h6>
<h6 class="mt-2 mb-1">Verify Communication Mobile Number</h6>
<div class="text-muted small" id="abhaCreateMobileHint"></div>
</div>
<label class="form-label fw-semibold" for="abhaCreateMobileOtp">Mobile OTP</label>
<div class="alert alert-info py-2 small mb-3">
<i class="bi bi-info-circle me-1"></i>Aadhaar authentication successful. Because your <strong>Communication Mobile Number</strong> is different from your Aadhaar-linked mobile, ABDM requires OTP verification for this number.
</div>
<label class="form-label fw-semibold" for="abhaCreateMobileOtp">6-digit Mobile OTP</label>
<div class="input-group input-group-lg">
<span class="input-group-text"><i class="bi bi-key"></i></span>
<input type="text" class="form-control" id="abhaCreateMobileOtp" maxlength="6" inputmode="numeric" autocomplete="one-time-code" placeholder="6-digit OTP">
<input type="text" class="form-control" id="abhaCreateMobileOtp" maxlength="6" inputmode="numeric" autocomplete="one-time-code" placeholder="Enter 6-digit OTP">
</div>
<div class="d-flex justify-content-between align-items-center mt-3 gap-2 flex-wrap">
<button type="button" class="btn btn-link px-0" id="abhaCreateMobileResendBtn" disabled>Resend OTP in 60s</button>
<div class="d-flex gap-2">
<button type="button" class="btn btn-outline-secondary" id="abhaCreateChangeMobileBtn">Change Mobile</button>
<button type="button" class="btn btn-outline-secondary" id="abhaCreateSkipMobileBtn">Use Aadhaar-linked Mobile</button>
<button type="button" class="btn btn-primary" id="abhaCreateVerifyMobileBtn"><i class="bi bi-check2-circle me-1"></i>Verify &amp; Update Mobile</button>
<button type="button" class="btn btn-primary" id="abhaCreateVerifyMobileBtn"><i class="bi bi-check2-circle me-1"></i>Verify &amp; Proceed</button>
</div>
</div>
</section>
<!-- Step 4: profile + card -->
<!-- Step 4: Choose / Create ABHA Address -->
<section id="abhaCreateStep4" class="d-none">
<div class="text-center mb-3">
<i class="bi bi-at text-warning" style="font-size:2.2rem"></i>
<h6 class="mt-2 mb-1">Choose Your ABHA Address</h6>
<div class="text-muted small">Select a suggested ABHA address or enter a custom one. This is your digital health identifier.</div>
</div>
<div id="abhaCreateAddressAlert"></div>
<!-- Suggested Addresses -->
<label class="form-label fw-semibold mb-2"><i class="bi bi-stars text-warning me-1"></i>Suggested ABHA Addresses</label>
<div id="abhaCreateAddressList" class="d-grid gap-2 mb-3"></div>
<div class="text-center my-2">
<button type="button" class="btn btn-link text-decoration-none" id="abhaCreateCustomToggle">
<i class="bi bi-pencil-square me-1"></i>Use a custom address instead &rarr;
</button>
</div>
<!-- Custom Address Wrap -->
<div id="abhaCreateCustomWrap" class="d-none mb-3">
<label class="form-label fw-semibold" for="abhaCreateCustomAddress">Custom ABHA Address</label>
<div class="input-group input-group-lg">
<span class="input-group-text"><i class="bi bi-at"></i></span>
<input type="text" class="form-control" id="abhaCreateCustomAddress" placeholder="username" autocomplete="off">
<span class="input-group-text text-muted">@sbx</span>
</div>
<div class="abha-policy-card p-3 mt-2">
<div class="fw-semibold small text-dark mb-1"><i class="bi bi-shield-check text-primary me-1"></i>ABDM ABHA Address Policy &amp; Rules:</div>
<ul class="small text-muted mb-0 ps-3">
<li>Length must be between <strong>8 and 18 characters</strong></li>
<li>Special characters allowed: at most <strong>1 dot (.)</strong> and/or <strong>1 underscore (_)</strong></li>
<li>Dot and underscore must be in between (not at the beginning or at the end)</li>
<li>Alphanumeric: letters and numbers only</li>
</ul>
</div>
</div>
<div class="d-flex justify-content-between mt-4">
<button type="button" class="btn btn-outline-secondary" id="abhaCreateAddressBackBtn"><i class="bi bi-arrow-left me-1"></i>Back</button>
<button type="button" class="btn btn-primary" id="abhaCreateConfirmAddressBtn"><i class="bi bi-check2-circle me-1"></i>Confirm Address &amp; Create ABHA</button>
</div>
</section>
<!-- Step 5: ABHA Profile & Official Card -->
<section id="abhaCreateStep5" class="d-none">
<div class="row g-4">
<div class="col-lg-6">
<div class="d-flex align-items-center gap-3 mb-3">
<img id="abhaCreatePhoto" class="rounded d-none" alt="ABHA profile" style="width:76px;height:76px;object-fit:cover">
<div>
<span class="badge bg-success-subtle text-success mb-1"><i class="bi bi-patch-check-fill me-1"></i><span id="abhaCreateStatusText">ABHA Ready</span></span>
<span class="badge bg-success-subtle text-success mb-1" id="abhaCreateBadgeWrap">
<i class="bi bi-patch-check-fill me-1"></i><span id="abhaCreateStatusText">ABHA Ready</span>
</span>
<h4 class="mb-0" id="abhaCreateProfileName"></h4>
<div class="text-muted" id="abhaCreateProfileAddress"></div>
</div>
@ -186,41 +255,15 @@
</div>
<div class="col-lg-6">
<div class="abha-card-preview" id="abhaCreateCardWrap">
<div class="text-center text-muted"><i class="bi bi-card-image fs-1 d-block mb-2"></i><span>Official ABHA card was not returned by the Bridge.</span></div>
<div class="text-center text-muted"><i class="bi bi-card-image fs-1 d-block mb-2"></i><span>Official ABHA card preview.</span></div>
</div>
<a class="btn btn-outline-primary w-100 mt-2 d-none" id="abhaCreateDownloadCard" download="ABHA-card"><i class="bi bi-download me-1"></i>Download ABHA</a>
<button type="button" class="btn btn-success w-100 mt-2" id="abhaCreateRegisterBtn"><i class="bi bi-person-check me-1"></i>Register Patient</button>
<button type="button" class="btn btn-primary w-100 mt-2" id="abhaCreateChooseAddressBtn"><i class="bi bi-at me-1"></i>Create New ABHA Address</button>
<div class="form-text mt-2"><i class="bi bi-info-circle me-1"></i>Click <strong>Register Patient</strong> to compare this ABHA with existing HMS records.</div>
<a class="btn btn-outline-primary w-100 mt-2 d-none" id="abhaCreateDownloadCard" download="ABHA-card"><i class="bi bi-download me-1"></i>Download ABHA Card</a>
<button type="button" class="btn btn-success w-100 mt-2" id="abhaCreateRegisterBtn"><i class="bi bi-person-check me-1"></i>Register / Link Patient in HMS</button>
<button type="button" class="btn btn-outline-primary w-100 mt-2" id="abhaCreateChooseAddressBtn"><i class="bi bi-at me-1"></i>Change / Create Custom ABHA Address</button>
<div class="form-text mt-2"><i class="bi bi-info-circle me-1"></i>Click <strong>Register / Link Patient</strong> to connect this ABHA to a patient record in HMS.</div>
</div>
</div>
</section>
<!-- Step 5: ABHA address selection -->
<section id="abhaCreateStep5" class="d-none">
<div class="text-center mb-3">
<i class="bi bi-briefcase-fill text-warning" style="font-size:2.2rem"></i>
<h6 class="mt-2 mb-1">Choose Your ABHA Address</h6>
<div class="text-muted small">Select a suggested ABHA address or enter a custom one. This is your digital health identity.</div>
</div>
<div id="abhaCreateAddressAlert"></div>
<div id="abhaCreateAddressList" class="d-grid gap-2"></div>
<div class="text-center mt-2">
<button type="button" class="btn btn-link" id="abhaCreateCustomToggle">Use a custom address instead &rarr;</button>
</div>
<div id="abhaCreateCustomWrap" class="d-none">
<label class="form-label fw-semibold" for="abhaCreateCustomAddress">Custom ABHA Address</label>
<div class="input-group input-group-lg">
<span class="input-group-text"><i class="bi bi-at"></i></span>
<input type="text" class="form-control" id="abhaCreateCustomAddress" placeholder="username" autocomplete="off">
</div>
<div class="form-text">Minimum 4 characters. Letters, numbers, dot and underscore only.</div>
</div>
<div class="d-flex justify-content-between mt-4">
<button type="button" class="btn btn-outline-secondary" id="abhaCreateAddressBackBtn"><i class="bi bi-arrow-left me-1"></i>Back</button>
<button type="button" class="btn btn-primary" id="abhaCreateConfirmAddressBtn"><i class="bi bi-briefcase me-1"></i>Confirm Address &amp; Create ABHA</button>
</div>
</section>
</div>
</div>
</div>
@ -240,6 +283,8 @@ window.AbhaCreateModal = (function () {
var resendRemaining = 0;
var mobileResendTimer = null;
var mobileResendRemaining = 0;
var aadhaarResendCount = 0;
var mobileResendCount = 0;
function csrf() {
var input = document.querySelector('input[name="<?= csrf_token() ?>"]');
@ -247,13 +292,51 @@ window.AbhaCreateModal = (function () {
}
function escapeHtml(value) { return $('<div>').text(value == null ? '' : String(value)).html(); }
function apiMessage(response, fallback) {
var message = response && (response.error_text || response.message)
? escapeHtml(response.error_text || response.message)
var raw = response && (response.error_text || response.message)
? String(response.error_text || response.message)
: fallback;
if (/loginId/i.test(raw) && /invalid/i.test(raw)) {
raw = 'Aadhaar Number is not valid. Valid 12-digit Aadhaar number is required.';
}
var message = escapeHtml(raw);
return response && response.request_id
? message + ' <small class="d-block mt-1">Bridge Request ID: ' + escapeHtml(response.request_id) + '</small>'
: message;
}
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, message) {
$('#abhaCreateAlert').html(message ? '<div class="alert alert-' + type + ' py-2">' + message + '</div>' : '');
}
@ -272,14 +355,19 @@ window.AbhaCreateModal = (function () {
resendTimer = null;
mobileResendTimer = null;
}
function startResendTimer(button, seconds, setter, getter) {
function startResendTimer(button, seconds) {
resendRemaining = Math.max(60, Number(seconds) || 60);
button.prop('disabled', true);
function tick() {
if (resendRemaining <= 0) {
window.clearInterval(resendTimer);
resendTimer = null;
button.prop('disabled', false).text('Resend OTP');
if (aadhaarResendCount < 2) {
var remaining = 2 - aadhaarResendCount;
button.prop('disabled', false).text('Resend OTP (' + remaining + ' left)');
} else {
button.prop('disabled', true).text('Resend limit reached (max 2)');
}
return;
}
button.text('Resend OTP in ' + resendRemaining + 's');
@ -296,7 +384,12 @@ window.AbhaCreateModal = (function () {
if (mobileResendRemaining <= 0) {
window.clearInterval(mobileResendTimer);
mobileResendTimer = null;
button.prop('disabled', false).text('Resend OTP');
if (mobileResendCount < 2) {
var remaining = 2 - mobileResendCount;
button.prop('disabled', false).text('Resend OTP (' + remaining + ' left)');
} else {
button.prop('disabled', true).text('Resend limit reached (max 2)');
}
return;
}
button.text('Resend OTP in ' + mobileResendRemaining + 's');
@ -333,27 +426,67 @@ window.AbhaCreateModal = (function () {
return merged;
}
function sendAadhaarOtp() {
function sendAadhaarOtp(isResend) {
var aadhaar = digits($('#abhaCreateAadhaar').val());
var mobile = digits($('#abhaCreateMobile').val());
if (aadhaar.length !== 12) { alertBox('warning', 'Enter a valid 12-digit Aadhaar number.'); return; }
if (mobile.length !== 10) { alertBox('warning', 'Enter a valid 10-digit mobile number for ABHA communication.'); return; }
if (!$('#abhaCreateConsent1').is(':checked')) { alertBox('warning', 'Aadhaar authentication consent is mandatory before sending the OTP.'); return; }
if (!$('#abhaCreateConsent4').is(':checked')) { alertBox('warning', 'Confirm that the consent was explained to the patient.'); return; }
var totalConsent = $('.abha-consent-chk').length;
var checkedConsent = $('.abha-consent-chk:checked').length;
var isAllConsentChecked = (totalConsent > 0 && checkedConsent === totalConsent) || $('#abhaCreateConsentAgree').is(':checked');
// Reset visual validation states
$('#abhaCreateAadhaar').removeClass('is-invalid');
$('#abhaCreateMobile').removeClass('is-invalid');
$('#abhaCreateConsentWrap').removeClass('border-danger');
$('#abhaCreateConsentAgreeWrap').removeClass('border-danger');
$('#abhaCreateConsentError').addClass('d-none');
if (aadhaar.length !== 12 || !validateVerhoeff(aadhaar)) {
$('#abhaCreateAadhaar').addClass('is-invalid').trigger('focus');
alertBox('warning', 'Aadhaar Number is not valid. Valid 12-digit Aadhaar number is required.');
return;
}
if (mobile.length !== 10 || !/^[6-9]\d{9}$/.test(mobile)) {
$('#abhaCreateMobile').addClass('is-invalid').trigger('focus');
alertBox('warning', 'Please enter a valid 10-digit mobile number for ABHA communication.');
return;
}
if (!isAllConsentChecked) {
$('#abhaCreateConsentWrap').addClass('border-danger');
$('#abhaCreateConsentAgreeWrap').addClass('border-danger');
$('#abhaCreateConsentError').removeClass('d-none');
alertBox('warning', 'Consent is mandatory. Please review and check "I Agree" to all consent declarations before proceeding to the next step.');
$('#abhaCreateConsentAgree').trigger('focus');
return;
}
if (isResend === true) {
if (aadhaarResendCount >= 2) {
alertBox('warning', 'Maximum 2 resend attempts reached for Aadhaar OTP.');
return;
}
aadhaarResendCount++;
} else {
aadhaarResendCount = 0;
}
communicationMobile = mobile;
var button = $('#abhaCreateSendOtpBtn').prop('disabled', true).html('<span class="spinner-border spinner-border-sm me-1"></span>Sending');
$.post('<?= base_url('abha/create/initiate') ?>', { aadhaar: aadhaar, auth_type: 'aadhaar_otp', '<?= csrf_token() ?>': csrf() }, function (response) {
$.post('<?= base_url('abha/create/initiate') ?>', {
aadhaar: aadhaar,
auth_type: 'aadhaar_otp',
consent: 1,
'<?= csrf_token() ?>': csrf()
}, function (response) {
button.prop('disabled', false).html('<i class="bi bi-send me-1"></i>Send OTP');
if (!response || response.ok != 1) { alertBox('danger', apiMessage(response, 'Unable to send Aadhaar OTP.')); return; }
if (!response || response.ok != 1) {
alertBox('danger', apiMessage(response, 'Unable to send Aadhaar OTP.'));
return;
}
createTxnId = response.txn_id || '';
$('#abhaCreateOtp').val('');
var destination = response.masked_mobile || ((response.message || '').match(/\*{2,}\d{4}/) || [''])[0];
var bridgeMessage = String(response.message || '').trim();
if (!bridgeMessage) {
bridgeMessage = 'OTP sent to the Aadhaar registered mobile number' + (destination ? ' ending with ' + destination : '') + '.';
}
$('#abhaCreateOtpHint').text(bridgeMessage);
var destination = response.masked_mobile || ((response.message || '').match(/\*{2,}\d{4}/) || [''])[0] || '******XXXX';
var promptMessage = 'We just sent an OTP on the Mobile Number ' + destination + ' linked with Aadhaar. Enter the OTP below to proceed with ABHA creation.';
$('#abhaCreateOtpHint').text(promptMessage);
$('#abhaCreateOtpRequestId')
.toggleClass('d-none', !response.request_id)
.text(response.request_id ? 'Bridge Request ID: ' + response.request_id : '');
@ -368,10 +501,13 @@ window.AbhaCreateModal = (function () {
function verifyAadhaarOtp() {
var otp = digits($('#abhaCreateOtp').val());
if (otp.length !== 6) { alertBox('warning', 'Enter the 6-digit OTP.'); return; }
if (otp.length !== 6) {
alertBox('warning', 'Enter the 6-digit OTP.');
return;
}
var button = $('#abhaCreateVerifyOtpBtn').prop('disabled', true).html('<span class="spinner-border spinner-border-sm me-1"></span>Verifying');
$.post('<?= base_url('abha/create/verify_otp') ?>', { txn_id: createTxnId, otp: otp, mobile: communicationMobile, '<?= csrf_token() ?>': csrf() }, function (response) {
button.prop('disabled', false).html('<i class="bi bi-shield-check me-1"></i>Verify &amp; Create ABHA');
button.prop('disabled', false).html('<i class="bi bi-shield-check me-1"></i>Verify &amp; Proceed');
if (!response || response.ok != 1) {
alertBox('danger', apiMessage(response, 'Invalid OTP - Please enter a valid OTP. Entered OTP is either expired or incorrect.'));
return;
@ -379,80 +515,166 @@ window.AbhaCreateModal = (function () {
stopTimers();
createdProfile = response;
createTxnId = response.txn_id || createTxnId;
// ABDM already created this ABHA with the alternate mobile; requesting another
// mobile OTP here would return whichever ABHA owns that number (e.g. the parent).
var alreadyCreated = response.abha_created == 1 || digits(response.abha_number).length === 14;
if (!alreadyCreated && communicationMobile && digits(response.mobile).slice(-10) !== communicationMobile) {
requestMobileOtp();
// Case 1: ABHA Already Exists (VRFY_ABHA_404 & User Step 9-10)
if (response.already_exists) {
renderProfile(response);
alertBox('success', '<i class="bi bi-info-circle-fill me-1"></i><strong>ABHA Already Exist:</strong> An active ABHA account is registered with this Aadhaar Number.');
return;
}
renderProfile(response);
if (alreadyCreated) {
reportCommunicationMobileStatus(response);
// Case 2: New ABHA (CRT_ABHA_101 & User Steps 4-8)
// Check communication mobile vs Aadhaar-linked mobile
if (response.is_comm_mobile_same) {
// CRT_ABHA_108: Mobile already verified by Aadhaar OTP -> Skip mobile OTP, go straight to ABHA address
loadAddressSuggestions();
alertBox('info', '<i class="bi bi-check-circle-fill me-1"></i>NO ABHA user registered with this Aadhaar Number. Communication mobile matches Aadhaar-linked mobile. Please choose your ABHA address.');
return;
} else {
// CRT_ABHA_109: Different mobile -> Verify communication mobile OTP
requestMobileOtp(false);
return;
}
}, 'json').fail(function (xhr) {
button.prop('disabled', false).html('<i class="bi bi-shield-check me-1"></i>Verify &amp; Create ABHA');
button.prop('disabled', false).html('<i class="bi bi-shield-check me-1"></i>Verify &amp; Proceed');
alertBox('danger', apiMessage(xhr.responseJSON, 'OTP verification failed.'));
});
}
// Explains the alternate-mobile outcome so the operator is never left guessing whether it was attached.
function reportCommunicationMobileStatus(response) {
var abhaText = formatAbha(response.abha_number) || 'the ABHA';
var created = 'ABHA <strong>' + escapeHtml(abhaText) + '</strong> created successfully by ABDM during Aadhaar OTP verification.';
if (!communicationMobile) {
alertBox('success', created);
return;
function requestMobileOtp(isResend) {
if (isResend === true) {
if (mobileResendCount >= 2) {
alertBox('warning', 'Maximum 2 resend attempts reached for Mobile OTP.');
return;
}
mobileResendCount++;
} else {
mobileResendCount = 0;
}
var maskedComm = escapeHtml(maskMobile(communicationMobile));
if (response.mobile_source === 'abdm' && digits(response.mobile).slice(-10) === communicationMobile) {
alertBox('success', created + '<br>ABDM confirmed mobile <strong>' + maskedComm + '</strong> as the ABHA communication number. It will be saved on the patient record. No separate mobile OTP is needed.');
return;
}
if (response.mobile_source === 'enrolment') {
alertBox('success', created + '<br>Alternate mobile <strong>' + maskedComm + '</strong> was submitted with the Aadhaar verification that created this ABHA and will be saved on the patient record. ABDM did not echo it back in the response, so confirm it in the ABHA app if the beneficiary reports missing notifications.'
+ '<br>HMS does not send a second mobile OTP: that check returns whichever ABHA already owns this number (for example a parent\'s ABHA) and would overwrite this record.');
return;
}
alertBox('warning', created + '<br>The alternate mobile <strong>' + maskedComm + '</strong> could not be attached, and ABDM returned <strong>' + (escapeHtml(maskMobile(response.mobile)) || 'no number') + '</strong> instead. Attach or change the number from the ABHA app/portal.');
}
function requestMobileOtp() {
alertBox('info', 'Aadhaar verified. Requesting OTP for the alternate communication mobile...');
alertBox('info', 'Aadhaar verified. Requesting OTP for communication mobile...');
$.post('<?= base_url('abha/create/communication') ?>', { mobile: communicationMobile, txn_id: createTxnId, '<?= csrf_token() ?>': csrf() }, function (response) {
if (!response || response.ok != 1) {
renderProfile(createdProfile || {});
alertBox('warning', 'Aadhaar verification succeeded, but the alternate mobile OTP could not be sent: ' + apiMessage(response, 'Unable to send mobile OTP.'));
// Fallback to address creation if communication OTP cannot be dispatched
loadAddressSuggestions();
alertBox('warning', 'Aadhaar verified, but alternate mobile OTP could not be sent: ' + apiMessage(response, 'Unable to send mobile OTP.') + ' Proceeding with Aadhaar mobile.');
return;
}
mobileTxnId = response.txn_id || createTxnId;
$('#abhaCreateMobileHint').text(response.message || ('OTP sent to ' + maskMobile(communicationMobile) + '.'));
var masked = maskMobile(communicationMobile);
$('#abhaCreateMobileHint').html('We just sent a separate verification OTP to your Communication Mobile Number <strong>' + escapeHtml(masked) + '</strong>. Enter the OTP below to proceed.');
$('#abhaCreateMobileOtp').val('');
showStep(3);
startMobileResendTimer($('#abhaCreateMobileResendBtn'));
$('#abhaCreateMobileOtp').trigger('focus');
}, 'json').fail(function (xhr) {
renderProfile(createdProfile || {});
alertBox('warning', 'Aadhaar verification succeeded, but the alternate mobile OTP could not be sent: ' + apiMessage(xhr.responseJSON, 'Unable to send mobile OTP.'));
loadAddressSuggestions();
alertBox('warning', 'Aadhaar verified, but alternate mobile OTP could not be sent: ' + apiMessage(xhr.responseJSON, 'Unable to send mobile OTP.') + ' Proceeding with Aadhaar mobile.');
});
}
function verifyMobileOtp() {
var otp = digits($('#abhaCreateMobileOtp').val());
if (otp.length !== 6) { alertBox('warning', 'Enter the 6-digit mobile OTP.'); return; }
if (otp.length !== 6) {
alertBox('warning', 'Enter the 6-digit mobile OTP.');
return;
}
var button = $('#abhaCreateVerifyMobileBtn').prop('disabled', true).html('<span class="spinner-border spinner-border-sm me-1"></span>Verifying');
$.post('<?= base_url('abha/create/verify_comm_otp') ?>', { txn_id: mobileTxnId, otp: otp, mobile: communicationMobile, '<?= csrf_token() ?>': csrf() }, function (response) {
button.prop('disabled', false).html('<i class="bi bi-check2-circle me-1"></i>Verify &amp; Update Mobile');
if (!response || response.ok != 1) { alertBox('danger', apiMessage(response, 'Mobile OTP verification failed.')); return; }
button.prop('disabled', false).html('<i class="bi bi-check2-circle me-1"></i>Verify &amp; Proceed');
if (!response || response.ok != 1) {
alertBox('danger', apiMessage(response, 'Mobile OTP verification failed.'));
return;
}
stopTimers();
createdProfile = mergeProfile(createdProfile, response);
renderProfile(createdProfile);
loadAddressSuggestions();
alertBox('success', '<i class="bi bi-check-circle-fill me-1"></i>Communication mobile verified successfully! Now choose your ABHA address.');
}, 'json').fail(function (xhr) {
button.prop('disabled', false).html('<i class="bi bi-check2-circle me-1"></i>Verify &amp; Update Mobile');
button.prop('disabled', false).html('<i class="bi bi-check2-circle me-1"></i>Verify &amp; Proceed');
alertBox('danger', apiMessage(xhr.responseJSON, 'Mobile OTP verification failed.'));
});
}
function loadAddressSuggestions() {
showStep(4);
$('#abhaCreateAddressAlert').empty();
$('#abhaCreateAddressList').html('<div class="text-center text-muted py-3"><span class="spinner-border spinner-border-sm me-2"></span>Loading address suggestions…</div>');
$('#abhaCreateCustomWrap').addClass('d-none');
$('#abhaCreateCustomAddress').val('');
$.post('<?= base_url('abha/create/address_suggestions') ?>', { txn_id: createTxnId, '<?= csrf_token() ?>': csrf() }, function (response) {
if (!response || response.ok != 1 || !(response.suggestions || []).length) {
$('#abhaCreateAddressList').html('<div class="alert alert-warning py-2 mb-0">' + apiMessage(response, 'No suggested ABHA addresses are available from the bridge.') + ' Please enter a custom address below.</div>');
$('#abhaCreateCustomWrap').removeClass('d-none');
return;
}
var html = '';
(response.suggestions || []).forEach(function (address, index) {
html += '<label class="abha-create-address">'
+ '<input class="form-check-input me-2" type="radio" name="abhaCreateAddressPick" value="' + escapeHtml(address) + '"' + (index === 0 ? ' checked' : '') + '>'
+ escapeHtml(address)
+ '</label>';
});
$('#abhaCreateAddressList').html(html);
}, 'json').fail(function (xhr) {
$('#abhaCreateAddressList').html('<div class="alert alert-warning py-2 mb-0">' + apiMessage(xhr.responseJSON, 'Unable to load address suggestions.') + ' Please enter a custom address below.</div>');
$('#abhaCreateCustomWrap').removeClass('d-none');
});
}
function confirmAddress() {
var custom = $.trim($('#abhaCreateCustomAddress').val());
var selected = $('input[name="abhaCreateAddressPick"]:checked').val() || '';
var address = $('#abhaCreateCustomWrap').hasClass('d-none') ? selected : (custom || selected);
if (!address) {
$('#abhaCreateAddressAlert').html('<div class="alert alert-warning py-2">Select a suggested address or enter a custom one.</div>');
return;
}
var handle = address.split('@')[0];
// Enforce CRT_ABHA_112 validation rules
if (handle.length < 8 || handle.length > 18) {
$('#abhaCreateAddressAlert').html('<div class="alert alert-warning py-2">ABHA address must be between 8 and 18 characters.</div>');
return;
}
if (/^[._]|[._]$/.test(handle)) {
$('#abhaCreateAddressAlert').html('<div class="alert alert-warning py-2">Special character dot (.) and underscore (_) can only be in between, not at the beginning or end.</div>');
return;
}
var dots = (handle.match(/\./g) || []).length;
var underscores = (handle.match(/_/g) || []).length;
if (dots > 1 || underscores > 1 || !/^[a-zA-Z0-9._]+$/.test(handle)) {
$('#abhaCreateAddressAlert').html('<div class="alert alert-warning py-2">Only letters, numbers, at most 1 dot (.) and/or 1 underscore (_) are allowed.</div>');
return;
}
var button = $('#abhaCreateConfirmAddressBtn').prop('disabled', true).html('<span class="spinner-border spinner-border-sm me-1"></span>Creating ABHA…');
$.post('<?= base_url('abha/create/address') ?>', { txn_id: createTxnId, abha_address: address, '<?= csrf_token() ?>': csrf() }, function (response) {
button.prop('disabled', false).html('<i class="bi bi-check2-circle me-1"></i>Confirm Address &amp; Create ABHA');
if (!response || response.ok != 1) {
var err = apiMessage(response, 'Unable to set the ABHA address.');
if (/already exist|already in use|taken/i.test(err)) {
err = 'ABHA Address is already exist';
}
$('#abhaCreateAddressAlert').html('<div class="alert alert-danger py-2">' + err + '</div>');
return;
}
if (createdProfile) {
createdProfile.abha_address = response.abha_address || address;
}
renderProfile(createdProfile || {});
alertBox('success', '<i class="bi bi-check-circle-fill me-1"></i><strong>ABHA Created Successfully!</strong> ABHA address set to <strong>' + escapeHtml(response.abha_address || address) + '</strong>.');
}, 'json').fail(function (xhr) {
button.prop('disabled', false).html('<i class="bi bi-check2-circle me-1"></i>Confirm Address &amp; Create ABHA');
var err = apiMessage(xhr.responseJSON, 'Unable to set the ABHA address.');
if (/already exist|already in use|taken/i.test(err)) {
err = 'ABHA Address is already exist';
}
$('#abhaCreateAddressAlert').html('<div class="alert alert-danger py-2">' + err + '</div>');
});
}
function renderProfile(profile) {
createdProfile = profile;
$('#abhaCreateProfileName').text(profile.name || '-');
@ -466,7 +688,7 @@ window.AbhaCreateModal = (function () {
? maskMobile(profile.mobile) + (profile.mobile_source === 'enrolment' ? ' (submitted at enrolment)' : '')
: '-');
$('#abhaCreateProfileFullAddress').text([profile.address, profile.district, profile.state, profile.zip].filter(Boolean).join(', ') || '-');
$('#abhaCreateStatusText').text(profile.abha_number ? 'ABHA Ready' : 'ABHA Verified');
$('#abhaCreateStatusText').text(profile.abha_number ? (profile.already_exists ? 'ABHA Active' : 'ABHA Ready') : 'ABHA Verified');
var photo = photoSrc(profile.photo);
$('#abhaCreatePhoto').toggleClass('d-none', !photo).attr('src', photo || '');
@ -477,62 +699,19 @@ window.AbhaCreateModal = (function () {
var isOfficial = profile.card_source !== 'generated';
var note = isOfficial
? ''
: '<div class="small text-warning mt-1"><i class="bi bi-exclamation-triangle me-1"></i>Provisional card generated by the Bridge, not the official ABDM card.</div>';
: '<div class="small text-warning mt-1"><i class="bi bi-exclamation-triangle me-1"></i>Provisional card generated by Bridge.</div>';
$('#abhaCreateCardWrap').html('<img src="' + cardSrc + '" alt="ABHA card">' + note);
$('#abhaCreateDownloadCard').attr('href', cardSrc).removeClass('d-none');
} else {
var reason = profile.card_message ? escapeHtml(profile.card_message) : 'ABDM did not issue an official ABHA card for this session.';
var reason = profile.card_message ? escapeHtml(profile.card_message) : 'Official ABHA card was not returned by the Bridge.';
$('#abhaCreateCardWrap').html('<div class="text-center text-muted"><i class="bi bi-card-image fs-1 d-block mb-2"></i><span>' + reason + '</span></div>');
$('#abhaCreateDownloadCard').addClass('d-none');
}
showStep(4);
}
function loadAddressSuggestions() {
showStep(5);
$('#abhaCreateAddressAlert').empty();
$('#abhaCreateAddressList').html('<div class="text-center text-muted py-3"><span class="spinner-border spinner-border-sm me-2"></span>Loading suggestions…</div>');
$('#abhaCreateCustomWrap').addClass('d-none');
$('#abhaCreateCustomAddress').val('');
$.post('<?= base_url('abha/create/address_suggestions') ?>', { txn_id: createTxnId, '<?= csrf_token() ?>': csrf() }, function (response) {
if (!response || response.ok != 1 || !(response.suggestions || []).length) {
$('#abhaCreateAddressList').empty();
$('#abhaCreateCustomWrap').removeClass('d-none');
$('#abhaCreateAddressAlert').html('<div class="alert alert-warning py-2">' + apiMessage(response, 'No ABHA address suggestions are available.') + ' You can type a custom address below.</div>');
return;
}
var html = '';
(response.suggestions || []).forEach(function (address, index) {
html += '<label class="abha-create-address"><input class="form-check-input me-2" type="radio" name="abhaCreateAddressPick" value="' + escapeHtml(address) + '"' + (index === 0 ? ' checked' : '') + '>' + escapeHtml(address) + '</label>';
});
$('#abhaCreateAddressList').html(html);
}, 'json').fail(function (xhr) {
$('#abhaCreateAddressList').empty();
$('#abhaCreateCustomWrap').removeClass('d-none');
$('#abhaCreateAddressAlert').html('<div class="alert alert-warning py-2">' + apiMessage(xhr.responseJSON, 'Unable to load ABHA address suggestions.') + ' You can type a custom address below.</div>');
});
}
function confirmAddress() {
var custom = $.trim($('#abhaCreateCustomAddress').val());
var selected = $('input[name="abhaCreateAddressPick"]:checked').val() || '';
var address = $('#abhaCreateCustomWrap').hasClass('d-none') ? selected : (custom || selected);
if (!address) { $('#abhaCreateAddressAlert').html('<div class="alert alert-warning py-2">Select a suggested address or enter a custom one.</div>'); return; }
var button = $('#abhaCreateConfirmAddressBtn').prop('disabled', true).html('<span class="spinner-border spinner-border-sm me-1"></span>Saving');
$.post('<?= base_url('abha/create/address') ?>', { txn_id: createTxnId, abha_address: address, '<?= csrf_token() ?>': csrf() }, function (response) {
button.prop('disabled', false).html('<i class="bi bi-briefcase me-1"></i>Confirm Address &amp; Create ABHA');
if (!response || response.ok != 1) {
$('#abhaCreateAddressAlert').html('<div class="alert alert-danger py-2">' + apiMessage(response, 'Unable to set the ABHA address.') + '</div>');
return;
}
createdProfile.abha_address = response.abha_address || address;
renderProfile(createdProfile);
alertBox('success', 'ABHA address updated to <strong>' + escapeHtml(createdProfile.abha_address) + '</strong>.');
}, 'json').fail(function (xhr) {
button.prop('disabled', false).html('<i class="bi bi-briefcase me-1"></i>Confirm Address &amp; Create ABHA');
$('#abhaCreateAddressAlert').html('<div class="alert alert-danger py-2">' + apiMessage(xhr.responseJSON, 'Unable to set the ABHA address.') + '</div>');
// Mark previous steps as done
$('.abha-create-step').each(function () {
var itemStep = Number($(this).data('step'));
$(this).toggleClass('active', itemStep === 5).toggleClass('done', itemStep < 5);
});
}
@ -545,38 +724,97 @@ window.AbhaCreateModal = (function () {
field.attr('type', isHidden ? 'text' : 'password');
$(this).find('i').attr('class', isHidden ? 'bi bi-eye-slash' : 'bi bi-eye');
});
$('#abhaCreateSendOtpBtn').on('click', sendAadhaarOtp);
$('#abhaCreateResendBtn').on('click', sendAadhaarOtp);
function clearConsentValidation() {
$('#abhaCreateConsentWrap').removeClass('border-danger');
$('#abhaCreateConsentAgreeWrap').removeClass('border-danger');
$('#abhaCreateConsentError').addClass('d-none');
if ($('#abhaCreateAlert').text().indexOf('Consent is mandatory') !== -1) {
alertBox('', '');
}
}
$('#abhaCreateConsentAgree').on('change', function () {
var checked = $(this).is(':checked');
$('.abha-consent-chk').prop('checked', checked);
$('#abhaCreateSelectAllConsent').text(checked ? 'Deselect All' : 'Select All');
if (checked) {
clearConsentValidation();
}
});
$('.abha-consent-chk').on('change', function () {
var total = $('.abha-consent-chk').length;
var checked = $('.abha-consent-chk:checked').length;
var allChecked = (total > 0 && checked === total);
$('#abhaCreateConsentAgree').prop('checked', allChecked);
$('#abhaCreateSelectAllConsent').text(allChecked ? 'Deselect All' : 'Select All');
if (allChecked) {
clearConsentValidation();
}
});
$('#abhaCreateSelectAllConsent').on('click', function () {
var total = $('.abha-consent-chk').length;
var checked = $('.abha-consent-chk:checked').length;
var shouldCheck = (checked < total);
$('.abha-consent-chk').prop('checked', shouldCheck);
$('#abhaCreateConsentAgree').prop('checked', shouldCheck);
$(this).text(shouldCheck ? 'Deselect All' : 'Select All');
if (shouldCheck) {
clearConsentValidation();
}
});
$('#abhaCreateAadhaar').on('input', function () {
$(this).removeClass('is-invalid');
if ($('#abhaCreateAlert').text().indexOf('Aadhaar Number is not valid') !== -1) {
alertBox('', '');
}
});
$('#abhaCreateMobile').on('input', function () {
$(this).removeClass('is-invalid');
if ($('#abhaCreateAlert').text().indexOf('mobile number') !== -1) {
alertBox('', '');
}
});
$('#abhaCreateSendOtpBtn').on('click', function () { sendAadhaarOtp(false); });
$('#abhaCreateResendBtn').on('click', function () { sendAadhaarOtp(true); });
$('#abhaCreateChangeAadhaarBtn').on('click', function () { stopTimers(); showStep(1); });
$('#abhaCreateVerifyOtpBtn').on('click', verifyAadhaarOtp);
$('#abhaCreateOtp').on('keydown', function (event) { if (event.key === 'Enter') { event.preventDefault(); verifyAadhaarOtp(); } });
$('#abhaCreateVerifyMobileBtn').on('click', verifyMobileOtp);
$('#abhaCreateMobileResendBtn').on('click', requestMobileOtp);
$('#abhaCreateSkipMobileBtn').on('click', function () { stopTimers(); renderProfile(createdProfile || {}); });
$('#abhaCreateChangeMobileBtn').on('click', function () {
$('#abhaCreateMobileResendBtn').on('click', function () { requestMobileOtp(true); });
$('#abhaCreateSkipMobileBtn').on('click', function () {
stopTimers();
showStep(1);
alertBox('warning', 'Enter the mobile number already linked with this Aadhaar, then request a new Aadhaar OTP.');
$('#abhaCreateMobile').trigger('focus');
loadAddressSuggestions();
alertBox('info', 'Proceeding with Aadhaar-linked mobile for ABHA.');
});
$('#abhaCreateMobileOtp').on('keydown', function (event) { if (event.key === 'Enter') { event.preventDefault(); verifyMobileOtp(); } });
$('#abhaCreateChooseAddressBtn').on('click', loadAddressSuggestions);
$('#abhaCreateAddressBackBtn').on('click', function () { showStep(4); });
$('#abhaCreateAddressBackBtn').on('click', function () {
if (createdProfile && createdProfile.already_exists) {
showStep(5);
} else {
showStep(2);
}
});
$('#abhaCreateCustomToggle').on('click', function () {
$('#abhaCreateCustomWrap').toggleClass('d-none');
if (!$('#abhaCreateCustomWrap').hasClass('d-none')) $('#abhaCreateCustomAddress').trigger('focus');
if (!$('#abhaCreateCustomWrap').hasClass('d-none')) {
$('#abhaCreateCustomAddress').trigger('focus');
}
});
$('#abhaCreateConfirmAddressBtn').on('click', confirmAddress);
$('#abhaCreateCustomAddress').on('keydown', function (event) { if (event.key === 'Enter') { event.preventDefault(); confirmAddress(); } });
$('#abhaCreateRegisterBtn').on('click', function () {
if (!createdProfile) return;
var profile = createdProfile;
if (profile.need_confirmation === false && Number(profile.patient_id || 0) > 0) {
$('#abhaCreateModal').one('hidden.bs.modal', function () {
if (typeof onCompleted === 'function') onCompleted(profile);
});
modal.hide();
return;
}
$('#abhaCreateModal').one('hidden.bs.modal', function () {
if (typeof onCompleted === 'function') onCompleted(profile);
});
@ -593,11 +831,16 @@ window.AbhaCreateModal = (function () {
mobileTxnId = '';
communicationMobile = '';
createdProfile = null;
aadhaarResendCount = 0;
mobileResendCount = 0;
stopTimers();
$('#abhaCreateAadhaar,#abhaCreateMobile,#abhaCreateOtp,#abhaCreateMobileOtp,#abhaCreateCustomAddress').val('');
$('#abhaCreateAadhaar,#abhaCreateMobile,#abhaCreateOtp,#abhaCreateMobileOtp,#abhaCreateCustomAddress').val('').removeClass('is-invalid');
$('#abhaCreateMobile').val(digits(prefillMobile));
$('#abhaCreateOtpRequestId').addClass('d-none').text('');
$('#abhaCreateConsent1,#abhaCreateConsent2,#abhaCreateConsent3,#abhaCreateConsent4').prop('checked', false);
$('#abhaCreateConsent1,#abhaCreateConsent2,#abhaCreateConsent3,#abhaCreateConsent4,#abhaCreateConsentAgree').prop('checked', false);
$('#abhaCreateConsentWrap,#abhaCreateConsentAgreeWrap').removeClass('border-danger');
$('#abhaCreateConsentError').addClass('d-none');
$('#abhaCreateSelectAllConsent').text('Select All');
$('#abhaCreatePhoto,#abhaCreateDownloadCard').addClass('d-none');
showStep(1);
modal.show();