From fd10df43c31dca05d447f76803742f5fcc6e25af Mon Sep 17 00:00:00 2001 From: DevSoftTech Server Date: Thu, 1 Oct 2026 17:12:17 +0530 Subject: [PATCH] fix(abdm): resolve ABHA address verification, field error extraction, and task board sync - Fix empty string fallback in validateAbha and EAtriaBridgeConnector to properly extract healthIdNumber and real fullName from data payload - Skip generic bridge envelope messages in extractBridgeErrorText to surface specific field rejections like loginHint - Translate non-existent ABHA address errors to 'ABHA Address does not exist or was not found in the ABDM registry' - Support both 14-digit ABHA numbers and ABHA addresses in Diagnosis publish and ABDM Task Board - Prevent completed task duplication in lab/radiology backfill - Add unit tests in AbhaOtpErrorHandlingTest and AbdmTaskBoardBackfillTest --- app/Controllers/AbdmTaskBoard.php | 37 +++++- app/Controllers/Abha.php | 111 ++++++++++++++++-- app/Controllers/Diagnosis.php | 27 ++++- app/Libraries/Abdm/EAtriaBridgeConnector.php | 30 +++++ .../Abdm/Sync/AbdmTaskBoardSyncService.php | 36 +++++- app/Views/abdm/task_board.php | 26 +++- app/Views/partials/abha_verify_modal.php | 12 +- tests/unit/Abdm/AbdmTaskBoardBackfillTest.php | 28 +++++ tests/unit/Abdm/AbhaOtpErrorHandlingTest.php | 38 ++++++ 9 files changed, 317 insertions(+), 28 deletions(-) create mode 100644 tests/unit/Abdm/AbdmTaskBoardBackfillTest.php 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('', { txn_id:lookupResponse.txn_id, auth_method:method, abha_id:account.abha_number || '', abha_address:account.abha_address || '', '':csrf() }, function (response) { + $.post('', { txn_id:lookupResponse.txn_id, auth_method:method, abha_id:abhaId, abha_address:abhaAddress, '':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); + } }