From 3e70ff8c4aa713e4eb990b0fc0a0ff13b62c62b8 Mon Sep 17 00:00:00 2001 From: DevSoftTech Server Date: Thu, 13 Aug 2026 17:11:48 +0530 Subject: [PATCH] Redesign ABHA Aadhaar creation flow --- app/Config/Routes.php | 1 + app/Controllers/Abha.php | 95 ++- app/Libraries/Abdm/AbdmConnectorInterface.php | 14 + app/Libraries/Abdm/DirectAbdmConnector.php | 14 + app/Libraries/Abdm/DreamsoftConnector.php | 16 + app/Libraries/Abdm/EAtriaBridgeConnector.php | 103 ++++ app/Views/billing/Patient_V.php | 55 ++ app/Views/partials/abha_create_modal.php | 539 ++++++++++++++++++ 8 files changed, 829 insertions(+), 8 deletions(-) create mode 100644 app/Views/partials/abha_create_modal.php diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 67730d9..7b23027 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -773,6 +773,7 @@ $routes->post('abha/create/communication', 'Abha::communication', ['filter' => ' $routes->post('abha/create/verify_comm_otp', 'Abha::verifyCommOtp', ['filter' => 'permission:abdm.abha.create']); $routes->post('abha/create/confirm_patient', 'Abha::confirmPatient', ['filter' => 'permission:abdm.abha.create']); $routes->post('abha/create/address', 'Abha::address', ['filter' => 'permission:abdm.abha.create']); +$routes->post('abha/create/address_suggestions', 'Abha::addressSuggestions', ['filter' => 'permission:abdm.abha.create']); $routes->get('abha/card/(:segment)', 'Abha::card/$1', ['filter' => 'permission:abdm.abha.create']); $routes->post('abha/register/validate', 'Abha::validateAbha', ['filter' => 'permission:abdm.abha.create']); $routes->post('abha/register/login/request-otp', 'Abha::loginRequestOtp', ['filter' => 'permission:abdm.abha.create']); diff --git a/app/Controllers/Abha.php b/app/Controllers/Abha.php index 6222e59..6832001 100644 --- a/app/Controllers/Abha.php +++ b/app/Controllers/Abha.php @@ -102,9 +102,9 @@ class Abha extends BaseController ); $name = $this->extractAbhaProfileName($profile, $payload); $photo = (string) ($profile['profilePhoto'] ?? $profile['profile_photo'] ?? $payload['profilePhoto'] ?? $payload['profile_photo'] ?? ''); - // Bridge/gateway verify-otp responses don't always echo the mobile back; - // fall back to the mobile the user actually typed and OTP-verified. - $mobile = (string) ($profile['mobile'] ?? $payload['mobile'] ?? $payload['mobileNumber'] ?? $requestMobile ?? ''); + // A different communication number is not verified until Step 3 succeeds. + // Never persist the operator-entered number merely because Aadhaar OTP passed. + $mobile = (string) ($profile['mobile'] ?? $payload['mobile'] ?? $payload['mobileNumber'] ?? ''); $profileGender = (string) ($profile['gender'] ?? $payload['gender'] ?? ''); $profileDob = (string) ($profile['dob'] ?? $profile['date_of_birth'] ?? $payload['dob'] ?? $payload['date_of_birth'] ?? ''); $verifiedStatus = (string) (($payload['gateway_abha_profile']['status'] ?? '') ?: ($profile['verifiedStatus'] ?? '') ?: ($profile['status'] ?? '') ?: ($payload['verifiedStatus'] ?? '') ?: ($payload['status'] ?? '')); @@ -142,6 +142,8 @@ class Abha extends BaseController 'ok' => 1, 'txn_id' => $newTxnId, 'skip_mobile' => true, + 'card_base64' => $this->extractAbhaCardData($payload), + 'card_content_type' => $this->resolveAbhaCardContentType($payload), 'abha_number' => $abhaNum, 'name' => $name, 'photo' => $photo, @@ -303,6 +305,8 @@ class Abha extends BaseController $responseBase = [ 'ok' => 1, + 'card_base64' => $this->extractAbhaCardData($payload), + 'card_content_type' => $this->resolveAbhaCardContentType($payload), 'abha_number' => $abhaNum, 'name' => $name, 'photo' => $photo, @@ -347,18 +351,93 @@ class Abha extends BaseController } // ------------------------------------------------------------------------- - // Step 4 — Save/finalise ABHA address - // POST abha/create/address - // Gateway has no address-assignment endpoint; return ok=1 as confirmation. + // Step 4 — ABHA address suggestions / assignment + // POST abha/create/address_suggestions, POST abha/create/address // ------------------------------------------------------------------------- + public function addressSuggestions() + { + if (! $this->request->isAJAX()) { + return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']); + } + + $txnId = trim((string) ($this->request->getPost('txn_id') ?? $this->request->getPost('txnId') ?? '')); + if ($txnId === '') { + return $this->response->setJSON(['ok' => 0, 'error_text' => 'txn_id is required']); + } + + try { + $result = AbdmConnectorFactory::make()->abhaAddressSuggestions(['txn_id' => $txnId]); + } catch (\Throwable $e) { + return $this->response->setJSON(['ok' => 0, 'supported' => false, 'error_text' => $e->getMessage()]); + } + + if (empty($result['ok']) || (int) $result['ok'] !== 1) { + // A missing Bridge endpoint must not block ABHA creation — the modal + // keeps the ABDM-assigned address and skips the selection step. + $unsupported = in_array((int) ($result['http_code'] ?? 0), [404, 405], true); + + return $this->response->setJSON([ + 'ok' => 0, + 'supported' => ! $unsupported, + 'error_text' => $unsupported + ? 'The ABDM Bridge does not expose ABHA address suggestions yet.' + : $this->extractBridgeErrorText($result, 'Unable to load ABHA address suggestions.'), + 'request_id' => (string) ($result['request_id'] ?? ''), + ]); + } + + return $this->response->setJSON([ + 'ok' => 1, + 'supported' => true, + 'txn_id' => $txnId, + 'suggestions' => array_values((array) ($result['suggestions'] ?? [])), + ]); + } + public function address() { if (! $this->request->isAJAX()) { return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'Invalid request']); } - // Address assignment is noted for the staff. No gateway call available. - return $this->response->setJSON(['ok' => 1, 'message' => 'ABHA created successfully.']); + $txnId = trim((string) ($this->request->getPost('txn_id') ?? $this->request->getPost('txnId') ?? '')); + $abhaAddress = trim((string) ($this->request->getPost('abha_address') ?? '')); + + if ($abhaAddress === '') { + return $this->response->setJSON(['ok' => 1, 'message' => 'ABHA created successfully.']); + } + + if (! preg_match('/^[a-zA-Z0-9._]{4,}$/', explode('@', $abhaAddress)[0])) { + 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.', + ]); + } + + try { + $result = AbdmConnectorFactory::make()->abhaSetAddress([ + 'txn_id' => $txnId, + 'abha_address' => $abhaAddress, + ]); + } catch (\Throwable $e) { + return $this->response->setJSON(['ok' => 0, 'error_text' => $e->getMessage()]); + } + + if (empty($result['ok']) || (int) $result['ok'] !== 1) { + return $this->response->setJSON([ + 'ok' => 0, + 'error_text' => $this->extractBridgeErrorText($result, 'Unable to set the ABHA address.'), + 'request_id' => (string) ($result['request_id'] ?? ''), + ]); + } + + $data = is_array($result['data'] ?? null) ? $result['data'] : []; + + return $this->response->setJSON([ + 'ok' => 1, + 'abha_address' => (string) ($result['abha_address'] ?? $data['abhaAddress'] ?? $data['abha_address'] ?? $abhaAddress), + 'message' => 'ABHA address updated successfully.', + ]); } // ------------------------------------------------------------------------- diff --git a/app/Libraries/Abdm/AbdmConnectorInterface.php b/app/Libraries/Abdm/AbdmConnectorInterface.php index 171269e..2b8c101 100644 --- a/app/Libraries/Abdm/AbdmConnectorInterface.php +++ b/app/Libraries/Abdm/AbdmConnectorInterface.php @@ -108,6 +108,20 @@ interface AbdmConnectorInterface */ public function abhaMobileVerifyOtp(array $payload): array; + /** + * ABDM M1: List ABHA address suggestions for an in-progress enrolment. + * + * @param array $payload Must contain 'txn_id' + */ + public function abhaAddressSuggestions(array $payload): array; + + /** + * ABDM M1: Set the chosen (suggested or custom) ABHA address. + * + * @param array $payload Must contain 'txn_id' and 'abha_address' + */ + public function abhaSetAddress(array $payload): array; + /** * Fetch hospital facility details and official ABDM Scan & Share QR image. * Gateway response includes hospital_name, hfr_id, and qr_data data URI/base64. diff --git a/app/Libraries/Abdm/DirectAbdmConnector.php b/app/Libraries/Abdm/DirectAbdmConnector.php index a5ac55e..cf7d91c 100644 --- a/app/Libraries/Abdm/DirectAbdmConnector.php +++ b/app/Libraries/Abdm/DirectAbdmConnector.php @@ -152,6 +152,20 @@ class DirectAbdmConnector implements AbdmConnectorInterface ); } + public function abhaAddressSuggestions(array $payload): array + { + throw new \RuntimeException( + 'DirectAbdmConnector::abhaAddressSuggestions() not implemented.' + ); + } + + public function abhaSetAddress(array $payload): array + { + throw new \RuntimeException( + 'DirectAbdmConnector::abhaSetAddress() not implemented.' + ); + } + public function facilityQrCode(): array { return [ diff --git a/app/Libraries/Abdm/DreamsoftConnector.php b/app/Libraries/Abdm/DreamsoftConnector.php index 9c07dbb..0dae0d6 100644 --- a/app/Libraries/Abdm/DreamsoftConnector.php +++ b/app/Libraries/Abdm/DreamsoftConnector.php @@ -167,6 +167,22 @@ class DreamsoftConnector implements AbdmConnectorInterface ); } + public function abhaAddressSuggestions(array $payload): array + { + throw new \RuntimeException( + 'DreamsoftConnector does not support ABHA address selection. ' + . 'Set abdm.connector = eatria_bridge in .env to use M1 endpoints.' + ); + } + + public function abhaSetAddress(array $payload): array + { + throw new \RuntimeException( + 'DreamsoftConnector does not support ABHA address selection. ' + . 'Set abdm.connector = eatria_bridge in .env to use M1 endpoints.' + ); + } + // ------------------------------------------------------------------------- // NHCX Claims // ------------------------------------------------------------------------- diff --git a/app/Libraries/Abdm/EAtriaBridgeConnector.php b/app/Libraries/Abdm/EAtriaBridgeConnector.php index 4b98fdf..3ecf31b 100644 --- a/app/Libraries/Abdm/EAtriaBridgeConnector.php +++ b/app/Libraries/Abdm/EAtriaBridgeConnector.php @@ -810,6 +810,109 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface return $this->post('/v3/abha/mobile/verify-otp', $body); } + public function abhaAddressSuggestions(array $payload): array + { + $txnId = (string) ($payload['txn_id'] ?? $payload['txnId'] ?? ''); + $body = ['txn_id' => $txnId, 'txnId' => $txnId]; + if ($this->hfrId !== '') { + $body['hfr_id'] = $this->hfrId; + } + + $result = $this->postFirstAvailable([ + '/v3/abha/enrollment/suggestion', + '/v3/abha/address/suggestion', + '/v3/abha/enrol/suggestion', + ], $body); + + if (empty($result['ok']) || (int) $result['ok'] !== 1) { + return $result; + } + + $result['suggestions'] = $this->normalizeAbhaAddressSuggestions($result); + + return $result; + } + + public function abhaSetAddress(array $payload): array + { + $txnId = (string) ($payload['txn_id'] ?? $payload['txnId'] ?? ''); + $address = (string) ($payload['abha_address'] ?? $payload['abhaAddress'] ?? ''); + $body = [ + 'txn_id' => $txnId, + 'txnId' => $txnId, + 'abha_address' => $address, + 'abhaAddress' => $address, + ]; + if ($this->hfrId !== '') { + $body['hfr_id'] = $this->hfrId; + } + + return $this->postFirstAvailable([ + '/v3/abha/enrollment/abha-address', + '/v3/abha/address/set', + '/v3/abha/enrol/abha-address', + ], $body); + } + + /** + * Bridge path naming for newer endpoints is still settling; try each + * candidate and keep the first response that is not a 404/405. + * + * @param string[] $paths + * @param array $body + * @return array + */ + private function postFirstAvailable(array $paths, array $body): array + { + $result = ['ok' => 0, 'http_code' => 404, 'error_text' => 'Endpoint not available on the ABDM Bridge.']; + + foreach ($paths as $path) { + $result = $this->post($path, $body); + if (! in_array((int) ($result['http_code'] ?? 0), [404, 405], true)) { + return $result; + } + } + + return $result; + } + + /** + * @param array $result + * @return string[] + */ + private function normalizeAbhaAddressSuggestions(array $result): array + { + $data = is_array($result['data'] ?? null) ? $result['data'] : []; + + $raw = []; + foreach ([ + $result['suggestions'] ?? null, + $result['abhaAddressList'] ?? null, + $result['abha_address_list'] ?? null, + $data['suggestions'] ?? null, + $data['abhaAddressList'] ?? null, + $data['abha_address_list'] ?? null, + ] as $candidate) { + if (is_array($candidate) && $candidate !== []) { + $raw = $candidate; + break; + } + } + + $addresses = []; + foreach ($raw as $entry) { + $value = is_array($entry) + ? (string) ($entry['abhaAddress'] ?? $entry['abha_address'] ?? $entry['address'] ?? $entry['value'] ?? '') + : (string) $entry; + $value = trim($value); + if ($value !== '' && ! in_array($value, $addresses, true)) { + $addresses[] = $value; + } + } + + return $addresses; + } + // ------------------------------------------------------------------------- // Consent // ------------------------------------------------------------------------- diff --git a/app/Views/billing/Patient_V.php b/app/Views/billing/Patient_V.php index 20bad1b..132dab1 100644 --- a/app/Views/billing/Patient_V.php +++ b/app/Views/billing/Patient_V.php @@ -42,6 +42,7 @@ +
@@ -416,6 +417,45 @@
+
+
+
+
+
+ +
Create ABHA
+
+

Register a new Ayushman Bharat Health Account for a patient using their Aadhaar number. OTP is sent to the Aadhaar-linked mobile.

+
    +
  • Aadhaar-based identity verification
  • +
  • Choose or customise the ABHA address
  • +
  • Official ABHA card stored for printing
  • +
+ +
+
+
+
+
+
+
+ +
Verify ABHA Account
+
+

Verify an existing ABHA account using the ABHA address or ABHA number. An OTP is sent to the linked mobile for authentication.

+
    +
  • Verify via ABHA address or ABHA number
  • +
  • Link ABHA to a patient record
  • +
  • View and download the ABHA card
  • +
+ +
+
+
+
+ + +
@@ -515,6 +555,7 @@
+
@@ -1401,6 +1442,20 @@ }); } + $(document).off('click.abhaLauncher').on('click.abhaLauncher', '#abha_open_create_modal_btn', function() { + if (!window.AbhaCreateModal) { + alert('ABHA create modal is unavailable. Reload the page and try again.'); + return; + } + window.AbhaCreateModal.open(function(profile) { + window.AbhaPatientMatchModal.open(profile, profile.candidates || [], function(finalResponse) { + if (typeof showRegResult === 'function') showRegResult(finalResponse); + }); + }); + }).on('click.abhaLauncher', '#abha_open_verify_modal_btn', function() { + openAbhaAccountVerification('', null); + }); + window.abhaRegReset = function() { regTxnId = null; stopQrScanner(); diff --git a/app/Views/partials/abha_create_modal.php b/app/Views/partials/abha_create_modal.php new file mode 100644 index 0000000..5a8c666 --- /dev/null +++ b/app/Views/partials/abha_create_modal.php @@ -0,0 +1,539 @@ + + * Then call: window.AbhaCreateModal.open(function (profile) { ... }); + */ +?> + + + + +