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
This commit is contained in:
parent
8dd34a9085
commit
fd10df43c3
|
|
@ -204,7 +204,11 @@ class AbdmTaskBoard extends BaseController
|
||||||
|
|
||||||
$abhaId = trim((string) $this->request->getPost('abha_id'));
|
$abhaId = trim((string) $this->request->getPost('abha_id'));
|
||||||
if (! $this->isValidAbhaNumber($abhaId)) {
|
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'] ?? '');
|
$taskType = (string) ($task['task_type'] ?? '');
|
||||||
|
|
@ -328,7 +332,12 @@ class AbdmTaskBoard extends BaseController
|
||||||
|
|
||||||
private function isValidAbhaNumber(string $abhaId): bool
|
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
|
private function backfillPatientAbhaTasks(): void
|
||||||
|
|
@ -356,7 +365,7 @@ class AbdmTaskBoard extends BaseController
|
||||||
}
|
}
|
||||||
|
|
||||||
$abha = trim((string) ($row[$abhaField] ?? ''));
|
$abha = trim((string) ($row[$abhaField] ?? ''));
|
||||||
if (preg_match('/^\d{14}$/', $abha) === 1) {
|
if ($this->isValidAbhaNumber($abha)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -446,8 +455,25 @@ class AbdmTaskBoard extends BaseController
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
$abha = trim((string) ($row['abha_id'] ?? $row['abha_no'] ?? $row['abha_address'] ?? $row['abha'] ?? ''));
|
$rawAbha = '';
|
||||||
if (preg_match('/^\d{14}$/', $abha) !== 1) {
|
$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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -477,6 +503,7 @@ class AbdmTaskBoard extends BaseController
|
||||||
->where('task_type', $taskType)
|
->where('task_type', $taskType)
|
||||||
->where('entity_type', 'lab_request')
|
->where('entity_type', 'lab_request')
|
||||||
->where('entity_id', (string) $labReqId)
|
->where('entity_id', (string) $labReqId)
|
||||||
|
->whereIn('status', ['pending', 'in_progress', 'completed'])
|
||||||
->get(1)
|
->get(1)
|
||||||
->getRowArray();
|
->getRowArray();
|
||||||
if (! empty($exists)) {
|
if (! empty($exists)) {
|
||||||
|
|
|
||||||
|
|
@ -1698,9 +1698,22 @@ class Abha extends BaseController
|
||||||
$result['data']['description'] ?? null,
|
$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) {
|
foreach ($candidates as $candidate) {
|
||||||
if (is_string($candidate) && trim($candidate) !== '') {
|
if (is_string($candidate) && trim($candidate) !== '') {
|
||||||
$trimmed = 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) {
|
if (stripos($trimmed, 'loginId') !== false && stripos($trimmed, 'invalid') !== false) {
|
||||||
return 'Aadhaar Number is not valid. Valid 12-digit Aadhaar number is required.';
|
return 'Aadhaar Number is not valid. Valid 12-digit Aadhaar number is required.';
|
||||||
}
|
}
|
||||||
|
|
@ -1719,6 +1732,10 @@ class Abha extends BaseController
|
||||||
}
|
}
|
||||||
return 'Incorrect OTP';
|
return 'Incorrect OTP';
|
||||||
}
|
}
|
||||||
|
if (in_array(strtolower($cleaned), $genericPhrases, true)) {
|
||||||
|
$firstGeneric ??= $cleaned;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
return $cleaned;
|
return $cleaned;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1739,7 +1756,9 @@ class Abha extends BaseController
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
$fieldMsg = trim($message);
|
$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.';
|
$fieldErrors[] = 'Aadhaar Number is not valid. Valid 12-digit Aadhaar number is required.';
|
||||||
} elseif (preg_match('/(?:attempts.*exceeded|403)/i', $fieldMsg)) {
|
} elseif (preg_match('/(?:attempts.*exceeded|403)/i', $fieldMsg)) {
|
||||||
$fieldErrors[] = 'Incorrect OTP. Maximum number of attempts exceeded. Please generate a fresh OTP and try again.';
|
$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);
|
return implode('; ', $fieldErrors);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($firstGeneric !== null) {
|
||||||
|
return $firstGeneric;
|
||||||
|
}
|
||||||
|
|
||||||
if (preg_match('/(?:invalid.*otp|incorrect.*otp|otp.*expired)/i', $fallback)) {
|
if (preg_match('/(?:invalid.*otp|incorrect.*otp|otp.*expired)/i', $fallback)) {
|
||||||
return 'Incorrect OTP';
|
return 'Incorrect OTP';
|
||||||
}
|
}
|
||||||
|
|
@ -2834,7 +2857,9 @@ class Abha extends BaseController
|
||||||
$errorText .= ' (Ref: ' . $requestId . ')';
|
$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 = 'Bridge validation rejected this request. Please contact bridge support with the reference shown. '
|
||||||
. $errorText;
|
. $errorText;
|
||||||
}
|
}
|
||||||
|
|
@ -2915,13 +2940,81 @@ class Abha extends BaseController
|
||||||
'blocked_auth_methods' => is_array($blockedMethods) ? array_values($blockedMethods) : [],
|
'blocked_auth_methods' => is_array($blockedMethods) ? array_values($blockedMethods) : [],
|
||||||
'masked_mobile' => $maskedMobile,
|
'masked_mobile' => $maskedMobile,
|
||||||
'aadhaar_masked_mobile' => $aadhaarMaskedMobile,
|
'aadhaar_masked_mobile' => $aadhaarMaskedMobile,
|
||||||
'account' => [
|
'account' => (function () use ($account, $data, $result, $maskedMobile, $aadhaarMaskedMobile): array {
|
||||||
'name' => trim((string) ($account['name'] ?? $account['fullName'] ?? $account['full_name'] ?? $data['name'] ?? $data['fullName'] ?? $data['full_name'] ?? '')),
|
$resolvedAbhaNumber = '';
|
||||||
'abha_number' => trim((string) ($account['ABHANumber'] ?? $account['abhaNumber'] ?? $account['abha_id'] ?? $account['healthIdNumber'] ?? $data['ABHANumber'] ?? $data['healthIdNumber'] ?? $data['abha_id'] ?? '')),
|
foreach ([
|
||||||
'abha_address' => trim((string) ($account['abhaAddress'] ?? $account['preferredAddress'] ?? $account['preferredAbhaAddress'] ?? $account['abha_address'] ?? $data['abhaAddress'] ?? $data['preferredAddress'] ?? $data['preferredAbhaAddress'] ?? $data['abha_address'] ?? '')),
|
$data['healthIdNumber'] ?? null,
|
||||||
'masked_mobile' => $maskedMobile,
|
$data['ABHANumber'] ?? null,
|
||||||
'aadhaar_masked_mobile' => $aadhaarMaskedMobile,
|
$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,
|
||||||
|
];
|
||||||
|
})(),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4549,7 +4549,23 @@ class Diagnosis extends BaseController
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($patient) {
|
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 = new BridgeSyncService();
|
||||||
$bridgeSync->enqueue($eventType, $payload, 'lab_request', (string) $labReqId);
|
$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.
|
// 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))
|
$taskType = $this->isRadiologyType((int) ($labReq->lab_type ?? 0))
|
||||||
? 'radiology_report_publish'
|
? 'radiology_report_publish'
|
||||||
: 'lab_report_publish';
|
: 'lab_report_publish';
|
||||||
|
|
|
||||||
|
|
@ -696,6 +696,36 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface
|
||||||
$result['aadhaar_masked_mobile'] = $aadhaarMaskedMobile;
|
$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.
|
// HMS callers expect a status-like field; treat successful search as VALID when absent.
|
||||||
$result['status'] = $status !== '' ? $status : 'VALID';
|
$result['status'] = $status !== '' ? $status : 'VALID';
|
||||||
return $result;
|
return $result;
|
||||||
|
|
|
||||||
|
|
@ -1202,11 +1202,18 @@ class AbdmTaskBoardSyncService
|
||||||
$dateFields = array_intersect(['reported_time', 'collected_time', 'Request_Date'], $labFields);
|
$dateFields = array_intersect(['reported_time', 'collected_time', 'Request_Date'], $labFields);
|
||||||
$dateSelect = ! empty($dateFields) ? (', r.' . implode(', r.', $dateFields)) : '';
|
$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')
|
$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')
|
->join('patient_master p', 'p.id = r.patient_id', 'inner')
|
||||||
->where('r.status >=', 2)
|
->where('r.status >=', 2)
|
||||||
->where('p.' . $abhaCol . ' !=', '')
|
|
||||||
->orderBy('r.id', 'DESC')
|
->orderBy('r.id', 'DESC')
|
||||||
->limit($limit)
|
->limit($limit)
|
||||||
->get()
|
->get()
|
||||||
|
|
@ -1215,8 +1222,29 @@ class AbdmTaskBoardSyncService
|
||||||
foreach ($labRows as $lRow) {
|
foreach ($labRows as $lRow) {
|
||||||
$labReqId = (int) ($lRow['id'] ?? 0);
|
$labReqId = (int) ($lRow['id'] ?? 0);
|
||||||
$patientId = (int) ($lRow['patient_id'] ?? 0);
|
$patientId = (int) ($lRow['patient_id'] ?? 0);
|
||||||
$abhaId = trim((string) ($lRow['abha_id'] ?? ''));
|
if ($labReqId <= 0 || $patientId <= 0) {
|
||||||
if ($labReqId <= 0 || $patientId <= 0 || preg_match('/^\d{14}$/', $abhaId) !== 1) {
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1081,9 +1081,13 @@
|
||||||
var taskType = (row.getAttribute('data-task-type') || '').toLowerCase();
|
var taskType = (row.getAttribute('data-task-type') || '').toLowerCase();
|
||||||
var abhaInput = row.querySelector('.abha-input');
|
var abhaInput = row.querySelector('.abha-input');
|
||||||
var abhaId = (abhaInput ? abhaInput.value : (row.getAttribute('data-abha-id') || '')).trim();
|
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)) {
|
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 before FHIR preview.', true);
|
setStatus('ABHA must be a 14-digit number or valid ABHA address before FHIR preview.', true);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1220,8 +1224,13 @@
|
||||||
var recordId = row ? (parseInt(row.getAttribute('data-entity-id') || '0', 10) || 0) : 0;
|
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;
|
var taskId = row ? (parseInt(row.getAttribute('data-task-id') || '0', 10) || 0) : 0;
|
||||||
|
|
||||||
if (!/^\d{14}$/.test(abhaId) || patientId <= 0 || recordId <= 0) {
|
var abhaClean = abhaId.replace(/\D/g, '');
|
||||||
setStatus('A 14-digit ABHA and valid Immunization record are required.', true);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1308,8 +1317,13 @@
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
var abha = modalAbhaInput.value.trim();
|
var abha = modalAbhaInput.value.trim();
|
||||||
if (!/^\d{14}$/.test(abha)) {
|
var abhaClean = abha.replace(/\D/g, '');
|
||||||
setStatus('ABHA must be a 14-digit number.', true);
|
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;
|
return;
|
||||||
}
|
}
|
||||||
var rowInput = selectedRow.querySelector('.abha-input');
|
var rowInput = selectedRow.querySelector('.abha-input');
|
||||||
|
|
|
||||||
|
|
@ -120,7 +120,9 @@ window.AbhaVerifyModal = (function () {
|
||||||
var raw = response && (response.error_text || response.message)
|
var raw = response && (response.error_text || response.message)
|
||||||
? String(response.error_text || response.message)
|
? String(response.error_text || response.message)
|
||||||
: fallback;
|
: 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)) {
|
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.';
|
raw = 'Incorrect OTP. Maximum number of attempts exceeded. Please generate a fresh OTP and try again.';
|
||||||
} else {
|
} 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; }
|
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;
|
lookupResponse = response;
|
||||||
var account = response.account || {};
|
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');
|
$('#abhaVerifyFoundName').text(account.name || 'ABHA account found');
|
||||||
$('#abhaVerifyFoundIdentity').text(account.abha_address || account.abha_number || identifier);
|
$('#abhaVerifyFoundIdentity').text(account.abha_address || account.abha_number || identifier);
|
||||||
renderAuthMethods(response);
|
renderAuthMethods(response);
|
||||||
showStep(2);
|
showStep(2);
|
||||||
|
if (lookupType === 'address' && !hasAbhaNumber) {
|
||||||
|
alertBox('info', '<i class="bi bi-info-circle me-1"></i>Found ABHA Address <strong>' + escapeHtml(account.abha_address || identifier) + '</strong>. 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) {
|
}, 'json').fail(function (xhr) {
|
||||||
button.prop('disabled', false).html('<i class="bi bi-search me-1"></i>Find Account');
|
button.prop('disabled', false).html('<i class="bi bi-search me-1"></i>Find Account');
|
||||||
alertBox('danger', apiMessage(xhr.responseJSON, 'Unable to look up the ABHA 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();
|
var method = $('input[name="abhaVerifyAuth"]:checked').val();
|
||||||
if (!method || !lookupResponse) return;
|
if (!method || !lookupResponse) return;
|
||||||
var account = lookupResponse.account || {};
|
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('<span class="spinner-border spinner-border-sm me-1"></span>Sending');
|
var button = $('#abhaVerifySendOtpBtn').prop('disabled', true).html('<span class="spinner-border spinner-border-sm me-1"></span>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('<i class="bi bi-send me-1"></i>Send OTP');
|
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 OTP.')); return; }
|
if (!response || response.ok != 1) { alertBox('danger', apiMessage(response, 'Unable to send OTP.')); return; }
|
||||||
lookupResponse.txn_id = response.txn_id || lookupResponse.txn_id;
|
lookupResponse.txn_id = response.txn_id || lookupResponse.txn_id;
|
||||||
|
|
|
||||||
28
tests/unit/Abdm/AbdmTaskBoardBackfillTest.php
Normal file
28
tests/unit/Abdm/AbdmTaskBoardBackfillTest.php
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Controllers\AbdmTaskBoard;
|
||||||
|
use CodeIgniter\Test\CIUnitTestCase;
|
||||||
|
|
||||||
|
final class AbdmTaskBoardBackfillTest extends CIUnitTestCase
|
||||||
|
{
|
||||||
|
public function testIsValidAbhaNumberAcceptsCleanAndFormattedAndAddress(): void
|
||||||
|
{
|
||||||
|
$controller = (new ReflectionClass(AbdmTaskBoard::class))->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'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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.');
|
$result = $this->extractMethod->invoke($this->controller, $payload, 'Please enter a valid OTP. Entered OTP is either expired or incorrect.');
|
||||||
$this->assertSame('Incorrect OTP', $result);
|
$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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user