diff --git a/app/Controllers/AbdmTaskBoard.php b/app/Controllers/AbdmTaskBoard.php
index 2bb7aa6..ef0535a 100644
--- a/app/Controllers/AbdmTaskBoard.php
+++ b/app/Controllers/AbdmTaskBoard.php
@@ -204,7 +204,11 @@ class AbdmTaskBoard extends BaseController
$abhaId = trim((string) $this->request->getPost('abha_id'));
if (! $this->isValidAbhaNumber($abhaId)) {
- return $this->response->setJSON(['ok' => 0, 'error_text' => 'ABHA ID must be a 14-digit number']);
+ return $this->response->setJSON(['ok' => 0, 'error_text' => 'ABHA ID must be a 14-digit number or valid ABHA address']);
+ }
+ $digits = preg_replace('/\D/', '', $abhaId);
+ if (strlen($digits) === 14) {
+ $abhaId = $digits;
}
$taskType = (string) ($task['task_type'] ?? '');
@@ -328,7 +332,12 @@ class AbdmTaskBoard extends BaseController
private function isValidAbhaNumber(string $abhaId): bool
{
- return preg_match('/^\d{14}$/', $abhaId) === 1;
+ $clean = preg_replace('/\D/', '', $abhaId);
+ if (strlen($clean) === 14) {
+ return true;
+ }
+
+ return str_contains($abhaId, '@') && preg_match('/^[a-zA-Z0-9.\-_]{3,}@[a-zA-Z]{3,}$/', $abhaId) === 1;
}
private function backfillPatientAbhaTasks(): void
@@ -356,7 +365,7 @@ class AbdmTaskBoard extends BaseController
}
$abha = trim((string) ($row[$abhaField] ?? ''));
- if (preg_match('/^\d{14}$/', $abha) === 1) {
+ if ($this->isValidAbhaNumber($abha)) {
continue;
}
@@ -446,8 +455,25 @@ class AbdmTaskBoard extends BaseController
continue;
}
- $abha = trim((string) ($row['abha_id'] ?? $row['abha_no'] ?? $row['abha_address'] ?? $row['abha'] ?? ''));
- if (preg_match('/^\d{14}$/', $abha) !== 1) {
+ $rawAbha = '';
+ $abhaAddress = '';
+ foreach (['abha_address', 'abha_id', 'abha_no', 'abha'] as $f) {
+ $val = trim((string) ($row[$f] ?? ''));
+ if ($val === '') {
+ continue;
+ }
+ if ($abhaAddress === '' && str_contains($val, '@')) {
+ $abhaAddress = $val;
+ continue;
+ }
+ $digits = preg_replace('/\D/', '', $val);
+ if ($rawAbha === '' && strlen($digits) === 14) {
+ $rawAbha = $digits;
+ }
+ }
+
+ $abha = $rawAbha !== '' ? $rawAbha : $abhaAddress;
+ if ($abha === '') {
continue;
}
@@ -477,6 +503,7 @@ class AbdmTaskBoard extends BaseController
->where('task_type', $taskType)
->where('entity_type', 'lab_request')
->where('entity_id', (string) $labReqId)
+ ->whereIn('status', ['pending', 'in_progress', 'completed'])
->get(1)
->getRowArray();
if (! empty($exists)) {
diff --git a/app/Controllers/Abha.php b/app/Controllers/Abha.php
index b123ee3..dd4b476 100644
--- a/app/Controllers/Abha.php
+++ b/app/Controllers/Abha.php
@@ -1698,9 +1698,22 @@ class Abha extends BaseController
$result['data']['description'] ?? null,
];
+ $genericPhrases = [
+ 'abdm gateway returned an error.',
+ 'abdm_upstream_error',
+ 'gateway error',
+ 'upstream error',
+ 'bad request',
+ 'an error occurred.',
+ ];
+ $firstGeneric = null;
+
foreach ($candidates as $candidate) {
if (is_string($candidate) && trim($candidate) !== '') {
$trimmed = trim($candidate);
+ if (stripos($trimmed, 'loginHint') !== false && stripos($trimmed, 'invalid') !== false) {
+ return 'ABDM gateway rejected login hint: OTP verification by ABHA Address is not supported directly by ABDM. Please verify using the 14-digit ABHA Number or Find ABHA via Mobile.';
+ }
if (stripos($trimmed, 'loginId') !== false && stripos($trimmed, 'invalid') !== false) {
return 'Aadhaar Number is not valid. Valid 12-digit Aadhaar number is required.';
}
@@ -1719,6 +1732,10 @@ class Abha extends BaseController
}
return 'Incorrect OTP';
}
+ if (in_array(strtolower($cleaned), $genericPhrases, true)) {
+ $firstGeneric ??= $cleaned;
+ continue;
+ }
return $cleaned;
}
}
@@ -1739,7 +1756,9 @@ class Abha extends BaseController
continue;
}
$fieldMsg = trim($message);
- if (strcasecmp($field, 'loginId') === 0 && stripos($fieldMsg, 'invalid') !== false) {
+ if (strcasecmp($field, 'loginHint') === 0 && stripos($fieldMsg, 'invalid') !== false) {
+ $fieldErrors[] = 'ABDM gateway rejected login hint: OTP verification by ABHA Address is not supported directly by ABDM. Please verify using the 14-digit ABHA Number or Find ABHA via Mobile.';
+ } elseif (strcasecmp($field, 'loginId') === 0 && stripos($fieldMsg, 'invalid') !== false) {
$fieldErrors[] = 'Aadhaar Number is not valid. Valid 12-digit Aadhaar number is required.';
} elseif (preg_match('/(?:attempts.*exceeded|403)/i', $fieldMsg)) {
$fieldErrors[] = 'Incorrect OTP. Maximum number of attempts exceeded. Please generate a fresh OTP and try again.';
@@ -1754,6 +1773,10 @@ class Abha extends BaseController
return implode('; ', $fieldErrors);
}
+ if ($firstGeneric !== null) {
+ return $firstGeneric;
+ }
+
if (preg_match('/(?:invalid.*otp|incorrect.*otp|otp.*expired)/i', $fallback)) {
return 'Incorrect OTP';
}
@@ -2834,7 +2857,9 @@ class Abha extends BaseController
$errorText .= ' (Ref: ' . $requestId . ')';
}
- if (stripos($errorText, 'Please make a valid request.') !== false) {
+ if (stripos($errorText, 'Unknown column') !== false || stripos($errorText, 'preferredAbhaAddress') !== false || stripos($errorText, 'not found') !== false || stripos($errorText, 'does not exist') !== false) {
+ $errorText = 'ABHA Address does not exist or was not found in the ABDM registry.';
+ } elseif (stripos($errorText, 'Please make a valid request.') !== false) {
$errorText = 'Bridge validation rejected this request. Please contact bridge support with the reference shown. '
. $errorText;
}
@@ -2915,13 +2940,81 @@ class Abha extends BaseController
'blocked_auth_methods' => is_array($blockedMethods) ? array_values($blockedMethods) : [],
'masked_mobile' => $maskedMobile,
'aadhaar_masked_mobile' => $aadhaarMaskedMobile,
- 'account' => [
- 'name' => trim((string) ($account['name'] ?? $account['fullName'] ?? $account['full_name'] ?? $data['name'] ?? $data['fullName'] ?? $data['full_name'] ?? '')),
- 'abha_number' => trim((string) ($account['ABHANumber'] ?? $account['abhaNumber'] ?? $account['abha_id'] ?? $account['healthIdNumber'] ?? $data['ABHANumber'] ?? $data['healthIdNumber'] ?? $data['abha_id'] ?? '')),
- 'abha_address' => trim((string) ($account['abhaAddress'] ?? $account['preferredAddress'] ?? $account['preferredAbhaAddress'] ?? $account['abha_address'] ?? $data['abhaAddress'] ?? $data['preferredAddress'] ?? $data['preferredAbhaAddress'] ?? $data['abha_address'] ?? '')),
- 'masked_mobile' => $maskedMobile,
- 'aadhaar_masked_mobile' => $aadhaarMaskedMobile,
- ],
+ 'account' => (function () use ($account, $data, $result, $maskedMobile, $aadhaarMaskedMobile): array {
+ $resolvedAbhaNumber = '';
+ foreach ([
+ $data['healthIdNumber'] ?? null,
+ $data['ABHANumber'] ?? null,
+ $data['abhaNumber'] ?? null,
+ $data['abha_number'] ?? null,
+ $data['abha_id'] ?? null,
+ $account['healthIdNumber'] ?? null,
+ $account['ABHANumber'] ?? null,
+ $account['abhaNumber'] ?? null,
+ $account['abha_number'] ?? null,
+ $account['abha_id'] ?? null,
+ $result['healthIdNumber'] ?? null,
+ $result['ABHANumber'] ?? null,
+ $result['abhaNumber'] ?? null,
+ $result['abha_number'] ?? null,
+ $result['abha_id'] ?? null,
+ ] as $candidateNum) {
+ if (is_string($candidateNum) && trim($candidateNum) !== '') {
+ $resolvedAbhaNumber = trim($candidateNum);
+ break;
+ }
+ }
+
+ $resolvedName = '';
+ foreach ([
+ $data['fullName'] ?? null,
+ $data['full_name'] ?? null,
+ $data['name'] ?? null,
+ $account['fullName'] ?? null,
+ $account['full_name'] ?? null,
+ $account['name'] ?? null,
+ $result['fullName'] ?? null,
+ $result['full_name'] ?? null,
+ $result['name'] ?? null,
+ ] as $candidateName) {
+ if (is_string($candidateName) && trim($candidateName) !== '' && strcasecmp(trim($candidateName), 'ABHA User') !== 0) {
+ $resolvedName = trim($candidateName);
+ break;
+ }
+ }
+ if ($resolvedName === '') {
+ $resolvedName = trim((string) ($account['name'] ?? $data['name'] ?? $result['name'] ?? 'ABHA User'));
+ }
+
+ $resolvedAddress = '';
+ foreach ([
+ $data['abhaAddress'] ?? null,
+ $data['preferredAddress'] ?? null,
+ $data['preferredAbhaAddress'] ?? null,
+ $data['abha_address'] ?? null,
+ $account['abhaAddress'] ?? null,
+ $account['preferredAddress'] ?? null,
+ $account['preferredAbhaAddress'] ?? null,
+ $account['abha_address'] ?? null,
+ $result['abhaAddress'] ?? null,
+ $result['preferredAddress'] ?? null,
+ $result['preferredAbhaAddress'] ?? null,
+ $result['abha_address'] ?? null,
+ ] as $candidateAddr) {
+ if (is_string($candidateAddr) && trim($candidateAddr) !== '') {
+ $resolvedAddress = trim($candidateAddr);
+ break;
+ }
+ }
+
+ return [
+ 'name' => $resolvedName,
+ 'abha_number' => $resolvedAbhaNumber,
+ 'abha_address' => $resolvedAddress,
+ 'masked_mobile' => $maskedMobile,
+ 'aadhaar_masked_mobile' => $aadhaarMaskedMobile,
+ ];
+ })(),
]);
}
diff --git a/app/Controllers/Diagnosis.php b/app/Controllers/Diagnosis.php
index 4fb73ea..c5b4868 100644
--- a/app/Controllers/Diagnosis.php
+++ b/app/Controllers/Diagnosis.php
@@ -4549,7 +4549,23 @@ class Diagnosis extends BaseController
}
if ($patient) {
- $abhaId = trim((string) ($patient->abha_id ?? $patient->abha_no ?? $patient->abha_address ?? $patient->abha ?? ''));
+ $rawAbha = '';
+ $abhaAddress = '';
+ foreach (['abha_address', 'abha_id', 'abha_no', 'abha'] as $f) {
+ $val = trim((string) ($patient->{$f} ?? ''));
+ if ($val === '') {
+ continue;
+ }
+ if ($abhaAddress === '' && str_contains($val, '@')) {
+ $abhaAddress = $val;
+ continue;
+ }
+ $digits = preg_replace('/\D/', '', $val);
+ if ($rawAbha === '' && strlen($digits) === 14) {
+ $rawAbha = $digits;
+ }
+ }
+ $abhaId = $rawAbha !== '' ? $rawAbha : $abhaAddress;
}
}
@@ -4568,8 +4584,15 @@ class Diagnosis extends BaseController
$bridgeSync = new BridgeSyncService();
$bridgeSync->enqueue($eventType, $payload, 'lab_request', (string) $labReqId);
+ $cleanDigits = preg_replace('/\D/', '', $abhaId);
+ $hasValidAbha = ($cleanDigits !== '' && strlen($cleanDigits) === 14)
+ || (str_contains($abhaId, '@') && preg_match('/^[a-zA-Z0-9.\-_]{3,}@[a-zA-Z]{3,}$/', $abhaId) === 1);
+
// Create ABDM work tasks for verified/finalized reports so staff can submit them.
- if ((int) ($labReq->status ?? 0) >= 2 && preg_match('/^\d{14}$/', $abhaId) === 1) {
+ if ((int) ($labReq->status ?? 0) >= 2 && $hasValidAbha) {
+ if (strlen($cleanDigits) === 14) {
+ $abhaId = $cleanDigits;
+ }
$taskType = $this->isRadiologyType((int) ($labReq->lab_type ?? 0))
? 'radiology_report_publish'
: 'lab_report_publish';
diff --git a/app/Libraries/Abdm/EAtriaBridgeConnector.php b/app/Libraries/Abdm/EAtriaBridgeConnector.php
index c1cf80e..1e4b617 100644
--- a/app/Libraries/Abdm/EAtriaBridgeConnector.php
+++ b/app/Libraries/Abdm/EAtriaBridgeConnector.php
@@ -696,6 +696,36 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface
$result['aadhaar_masked_mobile'] = $aadhaarMaskedMobile;
}
+ $resolvedAbhaNumber = '';
+ foreach ([
+ $data['healthIdNumber'] ?? null,
+ $data['ABHANumber'] ?? null,
+ $data['abhaNumber'] ?? null,
+ $data['abha_number'] ?? null,
+ $firstAccount['healthIdNumber'] ?? null,
+ $firstAccount['ABHANumber'] ?? null,
+ $firstAccount['abhaNumber'] ?? null,
+ $firstAccount['abha_number'] ?? null,
+ $result['abha_number'] ?? null,
+ $result['healthIdNumber'] ?? null,
+ ] as $candidateNum) {
+ if (is_string($candidateNum) && trim($candidateNum) !== '') {
+ $resolvedAbhaNumber = trim($candidateNum);
+ break;
+ }
+ }
+ if ($resolvedAbhaNumber !== '') {
+ $result['abha_number'] = $resolvedAbhaNumber;
+ if (isset($result['account']) && is_array($result['account'])) {
+ $result['account']['abha_number'] = $resolvedAbhaNumber;
+ $result['account']['abhaNumber'] = $resolvedAbhaNumber;
+ }
+ if (isset($result['data']['account']) && is_array($result['data']['account'])) {
+ $result['data']['account']['abha_number'] = $resolvedAbhaNumber;
+ $result['data']['account']['abhaNumber'] = $resolvedAbhaNumber;
+ }
+ }
+
// HMS callers expect a status-like field; treat successful search as VALID when absent.
$result['status'] = $status !== '' ? $status : 'VALID';
return $result;
diff --git a/app/Libraries/Abdm/Sync/AbdmTaskBoardSyncService.php b/app/Libraries/Abdm/Sync/AbdmTaskBoardSyncService.php
index 5304c71..f32ace6 100644
--- a/app/Libraries/Abdm/Sync/AbdmTaskBoardSyncService.php
+++ b/app/Libraries/Abdm/Sync/AbdmTaskBoardSyncService.php
@@ -1202,11 +1202,18 @@ class AbdmTaskBoardSyncService
$dateFields = array_intersect(['reported_time', 'collected_time', 'Request_Date'], $labFields);
$dateSelect = ! empty($dateFields) ? (', r.' . implode(', r.', $dateFields)) : '';
+ $abhaParts = [];
+ foreach (['abha_id', 'abha_no', 'abha_address', 'abha'] as $f) {
+ if (in_array($f, $patientFields, true)) {
+ $abhaParts[] = 'p.' . $f;
+ }
+ }
+ $abhaSelect = ! empty($abhaParts) ? (', ' . implode(', ', $abhaParts)) : '';
+
$labRows = $this->db->table('lab_request r')
- ->select('r.id, r.patient_id, r.patient_name, r.lab_type, r.charge_id, r.status' . $dateSelect . ', p.' . $abhaCol . ' as abha_id', false)
+ ->select('r.id, r.patient_id, r.patient_name, r.lab_type, r.charge_id, r.status' . $dateSelect . $abhaSelect, false)
->join('patient_master p', 'p.id = r.patient_id', 'inner')
->where('r.status >=', 2)
- ->where('p.' . $abhaCol . ' !=', '')
->orderBy('r.id', 'DESC')
->limit($limit)
->get()
@@ -1215,8 +1222,29 @@ class AbdmTaskBoardSyncService
foreach ($labRows as $lRow) {
$labReqId = (int) ($lRow['id'] ?? 0);
$patientId = (int) ($lRow['patient_id'] ?? 0);
- $abhaId = trim((string) ($lRow['abha_id'] ?? ''));
- if ($labReqId <= 0 || $patientId <= 0 || preg_match('/^\d{14}$/', $abhaId) !== 1) {
+ if ($labReqId <= 0 || $patientId <= 0) {
+ continue;
+ }
+
+ $rawAbha = '';
+ $abhaAddress = '';
+ foreach (['abha_address', 'abha_id', 'abha_no', 'abha'] as $f) {
+ $val = trim((string) ($lRow[$f] ?? ''));
+ if ($val === '') {
+ continue;
+ }
+ if ($abhaAddress === '' && str_contains($val, '@')) {
+ $abhaAddress = $val;
+ continue;
+ }
+ $digits = preg_replace('/\D/', '', $val);
+ if ($rawAbha === '' && strlen($digits) === 14) {
+ $rawAbha = $digits;
+ }
+ }
+
+ $abhaId = $rawAbha !== '' ? $rawAbha : $abhaAddress;
+ if ($abhaId === '') {
continue;
}
diff --git a/app/Views/abdm/task_board.php b/app/Views/abdm/task_board.php
index 03679d2..cfa4ccd 100644
--- a/app/Views/abdm/task_board.php
+++ b/app/Views/abdm/task_board.php
@@ -1081,9 +1081,13 @@
var taskType = (row.getAttribute('data-task-type') || '').toLowerCase();
var abhaInput = row.querySelector('.abha-input');
var abhaId = (abhaInput ? abhaInput.value : (row.getAttribute('data-abha-id') || '')).trim();
+ var abhaClean = abhaId.replace(/\D/g, '');
+ if (abhaClean.length === 14) {
+ abhaId = abhaClean;
+ }
- if (!/^\d{14}$/.test(abhaId)) {
- setStatus('ABHA must be a 14-digit number before FHIR preview.', true);
+ if (abhaClean.length !== 14 && (!abhaId.includes('@') || !/^[a-zA-Z0-9.\-_]{3,}@[a-zA-Z]{3,}$/.test(abhaId))) {
+ setStatus('ABHA must be a 14-digit number or valid ABHA address before FHIR preview.', true);
return true;
}
@@ -1220,8 +1224,13 @@
var recordId = row ? (parseInt(row.getAttribute('data-entity-id') || '0', 10) || 0) : 0;
var taskId = row ? (parseInt(row.getAttribute('data-task-id') || '0', 10) || 0) : 0;
- if (!/^\d{14}$/.test(abhaId) || patientId <= 0 || recordId <= 0) {
- setStatus('A 14-digit ABHA and valid Immunization record are required.', true);
+ var abhaClean = abhaId.replace(/\D/g, '');
+ if (abhaClean.length === 14) {
+ abhaId = abhaClean;
+ }
+ var hasValidAbha = abhaClean.length === 14 || (abhaId.includes('@') && /^[a-zA-Z0-9.\-_]{3,}@[a-zA-Z]{3,}$/.test(abhaId));
+ if (!hasValidAbha || patientId <= 0 || recordId <= 0) {
+ setStatus('A 14-digit ABHA or valid ABHA address and valid Immunization record are required.', true);
return;
}
@@ -1308,8 +1317,13 @@
return;
}
var abha = modalAbhaInput.value.trim();
- if (!/^\d{14}$/.test(abha)) {
- setStatus('ABHA must be a 14-digit number.', true);
+ var abhaClean = abha.replace(/\D/g, '');
+ if (abhaClean.length === 14) {
+ abha = abhaClean;
+ }
+ var hasValidAbha = abhaClean.length === 14 || (abha.includes('@') && /^[a-zA-Z0-9.\-_]{3,}@[a-zA-Z]{3,}$/.test(abha));
+ if (!hasValidAbha) {
+ setStatus('ABHA must be a 14-digit number or valid ABHA address.', true);
return;
}
var rowInput = selectedRow.querySelector('.abha-input');
diff --git a/app/Views/partials/abha_verify_modal.php b/app/Views/partials/abha_verify_modal.php
index 4ad2662..1612012 100644
--- a/app/Views/partials/abha_verify_modal.php
+++ b/app/Views/partials/abha_verify_modal.php
@@ -120,7 +120,9 @@ window.AbhaVerifyModal = (function () {
var raw = response && (response.error_text || response.message)
? String(response.error_text || response.message)
: fallback;
- if (/UIDAI Error code/i.test(raw) || /ABDM-1204/i.test(raw) || /attempts.*exceeded/i.test(raw) || /invalid.*otp/i.test(raw) || /incorrect.*otp/i.test(raw) || /otp.*match/i.test(raw) || /otp.*expired/i.test(raw)) {
+ if (/loginHint/i.test(raw) || /Invalid Login Hint/i.test(raw)) {
+ raw = 'ABDM gateway rejected login hint: OTP verification by ABHA Address is not supported directly by ABDM. Please verify using the 14-digit ABHA Number or Find ABHA via Mobile.';
+ } else if (/UIDAI Error code/i.test(raw) || /ABDM-1204/i.test(raw) || /attempts.*exceeded/i.test(raw) || /invalid.*otp/i.test(raw) || /incorrect.*otp/i.test(raw) || /otp.*match/i.test(raw) || /otp.*expired/i.test(raw)) {
if (/attempts.*exceeded/i.test(raw) || /403/.test(raw)) {
raw = 'Incorrect OTP. Maximum number of attempts exceeded. Please generate a fresh OTP and try again.';
} else {
@@ -205,10 +207,14 @@ window.AbhaVerifyModal = (function () {
if (!response.txn_id) { alertBox('warning', 'The Bridge found this ABHA but did not return the login transaction ID. Update /v3/abha/login/search before OTP verification can continue.'); return; }
lookupResponse = response;
var account = response.account || {};
+ var hasAbhaNumber = !!(account.abha_number || account.ABHANumber || account.abhaNumber || response.abha_id || response.abha_number);
$('#abhaVerifyFoundName').text(account.name || 'ABHA account found');
$('#abhaVerifyFoundIdentity').text(account.abha_address || account.abha_number || identifier);
renderAuthMethods(response);
showStep(2);
+ if (lookupType === 'address' && !hasAbhaNumber) {
+ alertBox('info', 'Found ABHA Address ' + escapeHtml(account.abha_address || identifier) + '. If ABDM rejects profile OTP for this address, verify using the patient\'s 14-digit ABHA Number or "Find ABHA via Mobile".');
+ }
}, 'json').fail(function (xhr) {
button.prop('disabled', false).html('Find Account');
alertBox('danger', apiMessage(xhr.responseJSON, 'Unable to look up the ABHA account.'));
@@ -218,8 +224,10 @@ window.AbhaVerifyModal = (function () {
var method = $('input[name="abhaVerifyAuth"]:checked').val();
if (!method || !lookupResponse) return;
var account = lookupResponse.account || {};
+ var abhaId = account.abha_number || account.ABHANumber || account.abhaNumber || lookupResponse.abha_id || lookupResponse.abha_number || '';
+ var abhaAddress = account.abha_address || account.abhaAddress || lookupResponse.abha_address || '';
var button = $('#abhaVerifySendOtpBtn').prop('disabled', true).html('Sending');
- $.post('= base_url('abha/register/login/request-otp') ?>', { txn_id:lookupResponse.txn_id, auth_method:method, abha_id:account.abha_number || '', abha_address:account.abha_address || '', '= csrf_token() ?>':csrf() }, function (response) {
+ $.post('= base_url('abha/register/login/request-otp') ?>', { txn_id:lookupResponse.txn_id, auth_method:method, abha_id:abhaId, abha_address:abhaAddress, '= csrf_token() ?>':csrf() }, function (response) {
button.prop('disabled', false).html('Send OTP');
if (!response || response.ok != 1) { alertBox('danger', apiMessage(response, 'Unable to send OTP.')); return; }
lookupResponse.txn_id = response.txn_id || lookupResponse.txn_id;
diff --git a/tests/unit/Abdm/AbdmTaskBoardBackfillTest.php b/tests/unit/Abdm/AbdmTaskBoardBackfillTest.php
new file mode 100644
index 0000000..081b0e4
--- /dev/null
+++ b/tests/unit/Abdm/AbdmTaskBoardBackfillTest.php
@@ -0,0 +1,28 @@
+newInstanceWithoutConstructor();
+ $method = new ReflectionMethod(AbdmTaskBoard::class, 'isValidAbhaNumber');
+ $method->setAccessible(true);
+
+ // 14 digits clean
+ $this->assertTrue($method->invoke($controller, '91747451787143'));
+ // 14 digits with standard hyphens
+ $this->assertTrue($method->invoke($controller, '91-7474-5178-7143'));
+ // ABHA address
+ $this->assertTrue($method->invoke($controller, 'singhkeshav301976@sbx'));
+ $this->assertTrue($method->invoke($controller, 'patient@abdm'));
+
+ // Invalid cases
+ $this->assertFalse($method->invoke($controller, ''));
+ $this->assertFalse($method->invoke($controller, '0'));
+ $this->assertFalse($method->invoke($controller, '12345'));
+ $this->assertFalse($method->invoke($controller, 'invalid_address'));
+ }
+}
diff --git a/tests/unit/Abdm/AbhaOtpErrorHandlingTest.php b/tests/unit/Abdm/AbhaOtpErrorHandlingTest.php
index 2826a79..6932cc1 100644
--- a/tests/unit/Abdm/AbhaOtpErrorHandlingTest.php
+++ b/tests/unit/Abdm/AbhaOtpErrorHandlingTest.php
@@ -62,4 +62,42 @@ final class AbhaOtpErrorHandlingTest extends CIUnitTestCase
$result = $this->extractMethod->invoke($this->controller, $payload, 'Please enter a valid OTP. Entered OTP is either expired or incorrect.');
$this->assertSame('Incorrect OTP', $result);
}
+
+ public function testExtractBridgeErrorTextSkipsGenericUpstreamErrorAndExtractsLoginHint(): void
+ {
+ $payload = [
+ 'ok' => 0,
+ 'error' => [
+ 'source' => 'abdm_gateway',
+ 'code' => 'ABDM_UPSTREAM_ERROR',
+ 'message' => 'ABDM gateway returned an error.',
+ 'http_status' => 400,
+ ],
+ 'data' => [
+ 'loginHint' => 'Invalid Login Hint',
+ 'timestamp' => '2026-10-01 16:20:23',
+ ],
+ ];
+
+ $result = $this->extractMethod->invoke($this->controller, $payload, 'The ABDM Bridge could not send the OTP.');
+ $this->assertStringContainsString('ABDM gateway rejected login hint', $result);
+ $this->assertStringContainsString('14-digit ABHA Number', $result);
+ $this->assertStringNotContainsString('ABDM gateway returned an error.', $result);
+ }
+
+ public function testExtractBridgeErrorTextPreservesGenericWhenNoFieldErrors(): void
+ {
+ $payload = [
+ 'ok' => 0,
+ 'error' => [
+ 'source' => 'abdm_gateway',
+ 'code' => 'ABDM_UPSTREAM_ERROR',
+ 'message' => 'ABDM gateway returned an error.',
+ 'http_status' => 400,
+ ],
+ ];
+
+ $result = $this->extractMethod->invoke($this->controller, $payload, 'The ABDM Bridge could not send the OTP.');
+ $this->assertSame('ABDM gateway returned an error.', $result);
+ }
}