From b25ab98fd700b6135764ed015f99cbb6280546ac Mon Sep 17 00:00:00 2001 From: DevSoftTech Server Date: Sat, 15 Aug 2026 03:16:25 +0530 Subject: [PATCH] Fetch official ABHA card and surface bridge card provenance --- app/Config/Routes.php | 1 + app/Controllers/Abha.php | 34 +++++++++++++++++-- app/Libraries/Abdm/AbdmConnectorInterface.php | 7 ++++ app/Libraries/Abdm/DirectAbdmConnector.php | 5 +++ app/Libraries/Abdm/DreamsoftConnector.php | 5 +++ app/Libraries/Abdm/EAtriaBridgeConnector.php | 34 ++++++++++++++----- app/Views/partials/abha_create_modal.php | 5 +-- app/Views/partials/abha_mobile_modal.php | 5 +-- app/Views/partials/abha_verify_modal.php | 5 +-- 9 files changed, 83 insertions(+), 18 deletions(-) diff --git a/app/Config/Routes.php b/app/Config/Routes.php index e92a6ca..67bc42a 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -775,6 +775,7 @@ $routes->post('abha/create/confirm_patient', 'Abha::confirmPatient', ['filter' = $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/card', 'Abha::fetchOfficialCard', ['filter' => 'permission:abdm.abha.create']); $routes->post('abha/register/validate', 'Abha::validateAbha', ['filter' => 'permission:abdm.abha.create']); $routes->post('abha/register/qr_scan', 'Abha::qrScan', ['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 5c3e091..94a503b 100644 --- a/app/Controllers/Abha.php +++ b/app/Controllers/Abha.php @@ -153,6 +153,7 @@ class Abha extends BaseController 'card_base64' => $this->extractAbhaCardData($result), 'card_content_type' => $this->resolveAbhaCardContentType($result), 'card_source' => $this->resolveAbhaCardSource($result), + 'card_message' => $this->resolveAbhaCardMessage($result), 'abha_number' => $abhaNum, 'name' => $name, 'photo' => $photo, @@ -325,6 +326,7 @@ class Abha extends BaseController 'card_base64' => $this->extractAbhaCardData($result), 'card_content_type' => $this->resolveAbhaCardContentType($result), 'card_source' => $this->resolveAbhaCardSource($result), + 'card_message' => $this->resolveAbhaCardMessage($result), 'abha_number' => $abhaNum, 'name' => $name, 'photo' => $photo, @@ -2105,6 +2107,7 @@ class Abha extends BaseController 'card_base64' => $cardData, 'card_content_type' => $this->resolveAbhaCardContentType($payload), 'card_source' => $this->resolveAbhaCardSource($payload), + 'card_message' => $this->resolveAbhaCardMessage($payload), 'candidates' => $candidates, ]); } @@ -2224,7 +2227,9 @@ class Abha extends BaseController ]; foreach ($sources as $source) { if (! is_array($source)) { continue; } - foreach (['official_card', 'card_data_uri', 'card_base64', 'card_data', 'abhaCard', 'abha_card', 'cardData', 'card'] as $key) { + // card_base64 first: the connector puts the resolved official card there, + // while official_card can still hold a Bridge-generated image. + foreach (['card_base64', 'official_card', 'card_data_uri', 'card_data', 'abhaCard', 'abha_card', 'cardData', 'card'] as $key) { $value = $source[$key] ?? ''; if (is_string($value) && trim($value) !== '') { return trim($value); } if (is_array($value)) { @@ -2239,6 +2244,23 @@ class Abha extends BaseController return ''; } + private function resolveAbhaCardMessage(array $payload): string + { + $data = is_array($payload['data'] ?? null) ? $payload['data'] : []; + foreach ([ + $payload['card_message'] ?? null, + $payload['message'] ?? null, + $data['message'] ?? null, + ] as $candidate) { + $value = trim((string) $candidate); + if ($value !== '') { + return $value; + } + } + + return ''; + } + private function resolveAbhaCardSource(array $payload): string { $data = is_array($payload['data'] ?? null) ? $payload['data'] : []; @@ -2249,9 +2271,15 @@ class Abha extends BaseController $data['source'] ?? null, ] as $candidate) { $value = strtolower(trim((string) $candidate)); - if ($value !== '') { - return str_contains($value, 'abdm') ? 'abdm' : 'generated'; + if ($value === '') { + continue; } + if (str_contains($value, 'abdm')) { + return 'abdm'; + } + + // "none" means ABDM issued no card for this session, not a local render. + return $value === 'none' ? 'none' : 'generated'; } return ''; diff --git a/app/Libraries/Abdm/AbdmConnectorInterface.php b/app/Libraries/Abdm/AbdmConnectorInterface.php index 2b8c101..1f639c0 100644 --- a/app/Libraries/Abdm/AbdmConnectorInterface.php +++ b/app/Libraries/Abdm/AbdmConnectorInterface.php @@ -53,6 +53,13 @@ interface AbdmConnectorInterface */ public function abhaLoginVerifyOtp(array $payload): array; + /** + * Fetch the official ABHA card from the Bridge card endpoint. + * + * @param array $payload Must contain abha_number or abha_address. + */ + public function fetchOfficialAbhaCard(array $payload): array; + // ------------------------------------------------------------------------- // Consent // ------------------------------------------------------------------------- diff --git a/app/Libraries/Abdm/DirectAbdmConnector.php b/app/Libraries/Abdm/DirectAbdmConnector.php index cf7d91c..a3755de 100644 --- a/app/Libraries/Abdm/DirectAbdmConnector.php +++ b/app/Libraries/Abdm/DirectAbdmConnector.php @@ -65,6 +65,11 @@ class DirectAbdmConnector implements AbdmConnectorInterface throw new \RuntimeException('DirectAbdmConnector::abhaLoginVerifyOtp() not implemented.'); } + public function fetchOfficialAbhaCard(array $payload): array + { + return ['ok' => 0, 'error_text' => 'Official ABHA card download requires the e-Atria Bridge connector.']; + } + // ------------------------------------------------------------------------- // Consent // ------------------------------------------------------------------------- diff --git a/app/Libraries/Abdm/DreamsoftConnector.php b/app/Libraries/Abdm/DreamsoftConnector.php index 0dae0d6..fb8b3ef 100644 --- a/app/Libraries/Abdm/DreamsoftConnector.php +++ b/app/Libraries/Abdm/DreamsoftConnector.php @@ -51,6 +51,11 @@ class DreamsoftConnector implements AbdmConnectorInterface throw new \RuntimeException('ABHA login OTP verification requires the synchronous e-Atria Bridge connector.'); } + public function fetchOfficialAbhaCard(array $payload): array + { + return ['ok' => 0, 'error_text' => 'Official ABHA card download requires the e-Atria Bridge connector.']; + } + // ------------------------------------------------------------------------- // Consent // ------------------------------------------------------------------------- diff --git a/app/Libraries/Abdm/EAtriaBridgeConnector.php b/app/Libraries/Abdm/EAtriaBridgeConnector.php index e133745..b17a1d2 100644 --- a/app/Libraries/Abdm/EAtriaBridgeConnector.php +++ b/app/Libraries/Abdm/EAtriaBridgeConnector.php @@ -733,7 +733,7 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface if ($card === '' || $source !== 'abdm') { return [ 'ok' => 0, - 'error_text' => 'The Bridge did not return an official ABHA card.', + 'error_text' => trim((string) ($result['message'] ?? $data['message'] ?? '')) ?: 'The Bridge did not return an official ABHA card.', 'card_source' => $source, 'request_id' => (string) ($result['request_id'] ?? ''), ]; @@ -773,11 +773,16 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface ?: $this->extractOfficialCard($profile) ?: $this->extractOfficialCard(is_array($result['ABHAProfile'] ?? null) ? $result['ABHAProfile'] : []); + $inlineSource = $this->extractCardSource($result) ?: $this->extractCardSource($data); if ($card !== '') { - $result['card_source'] = $this->extractCardSource($result) ?: $this->extractCardSource($data); + $result['card_base64'] = $card; + $result['card_content_type'] = $this->resolveCardContentType($result) ?: $this->resolveCardContentType($data); + $result['card_source'] = $inlineSource; } - if ($card === '' && ($abhaNumber !== '' || $abhaAddress !== '')) { + // Verify responses often embed a Bridge-generated card under official_card, + // so only the dedicated card endpoint can supply the real ABDM card. + if (($card === '' || $inlineSource === 'generated') && ($abhaNumber !== '' || $abhaAddress !== '')) { // Without the patient X-Token the Bridge can only return a generated // card; with it, ABDM's genuine card is fetched. $patientToken = $this->extractPatientXToken($result); @@ -791,11 +796,16 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface ); if (! empty($cardResult['ok']) && (int) $cardResult['ok'] === 1) { $cardData = is_array($cardResult['data'] ?? null) ? $cardResult['data'] : $cardResult; - $card = $this->extractOfficialCard($cardResult) ?: $this->extractOfficialCard($cardData); - if ($card !== '') { - $result['card_base64'] = $card; + $fetchedCard = $this->extractOfficialCard($cardResult) ?: $this->extractOfficialCard($cardData); + $fetchedSource = $this->extractCardSource($cardResult) ?: $this->extractCardSource($cardData); + // Never trade a card we already hold for another generated one. + if ($fetchedCard !== '' && ($fetchedSource === 'abdm' || $card === '')) { + $result['card_base64'] = $fetchedCard; $result['card_content_type'] = $this->resolveCardContentType($cardData); - $result['card_source'] = $this->extractCardSource($cardResult) ?: $this->extractCardSource($cardData); + $result['card_source'] = $fetchedSource; + } elseif ($fetchedCard === '') { + // Bridge reports the upstream ABDM reason here (e.g. token expired). + $result['card_message'] = trim((string) ($cardResult['message'] ?? $cardData['message'] ?? '')); } } } @@ -810,9 +820,15 @@ class EAtriaBridgeConnector implements AbdmConnectorInterface { foreach ([$source['card_source'] ?? null, $source['source'] ?? null] as $candidate) { $value = strtolower(trim((string) $candidate)); - if ($value !== '') { - return str_contains($value, 'abdm') ? 'abdm' : 'generated'; + if ($value === '') { + continue; } + if (str_contains($value, 'abdm')) { + return 'abdm'; + } + + // "none" means ABDM issued no card for this session, not a local render. + return $value === 'none' ? 'none' : 'generated'; } return ''; diff --git a/app/Views/partials/abha_create_modal.php b/app/Views/partials/abha_create_modal.php index b5fceed..192d986 100644 --- a/app/Views/partials/abha_create_modal.php +++ b/app/Views/partials/abha_create_modal.php @@ -420,14 +420,15 @@ window.AbhaCreateModal = (function () { var card = profile.card_base64 || ''; if (card) { var cardSrc = String(card).indexOf('data:') === 0 ? card : 'data:' + (profile.card_content_type || 'image/png') + ';base64,' + card; - var isOfficial = profile.card_source === 'abdm'; + var isOfficial = profile.card_source !== 'generated'; var note = isOfficial ? '' : '
Provisional card generated by the Bridge, not the official ABDM card.
'; $('#abhaCreateCardWrap').html('ABHA card' + note); $('#abhaCreateDownloadCard').attr('href', cardSrc).removeClass('d-none'); } else { - $('#abhaCreateCardWrap').html('
Official ABHA card was not returned by the Bridge.
'); + var reason = profile.card_message ? escapeHtml(profile.card_message) : 'ABDM did not issue an official ABHA card for this session.'; + $('#abhaCreateCardWrap').html('
' + reason + '
'); $('#abhaCreateDownloadCard').addClass('d-none'); } showStep(4); diff --git a/app/Views/partials/abha_mobile_modal.php b/app/Views/partials/abha_mobile_modal.php index c96f462..cd503bb 100644 --- a/app/Views/partials/abha_mobile_modal.php +++ b/app/Views/partials/abha_mobile_modal.php @@ -229,12 +229,13 @@ window.AbhaMobileModal = (function () { var card = profile.card_base64 || ''; if (card) { var cardSrc = String(card).indexOf('data:') === 0 ? card : 'data:' + (profile.card_content_type || 'image/png') + ';base64,' + card; - var note = profile.card_source === 'abdm' + var note = profile.card_source !== 'generated' ? '' : '
Provisional card generated by the Bridge, not the official ABDM card.
'; $('#abhaMobileCardWrap').html('ABHA card' + note); } else { - $('#abhaMobileCardWrap').html('
Official ABHA card was not returned by the Bridge.
'); + var reason = profile.card_message ? escapeHtml(profile.card_message) : 'ABDM did not issue an official ABHA card for this session.'; + $('#abhaMobileCardWrap').html('
' + reason + '
'); } showStep(3); } diff --git a/app/Views/partials/abha_verify_modal.php b/app/Views/partials/abha_verify_modal.php index f878ba4..e9a9a27 100644 --- a/app/Views/partials/abha_verify_modal.php +++ b/app/Views/partials/abha_verify_modal.php @@ -227,14 +227,15 @@ window.AbhaVerifyModal = (function () { var card = profile.card_base64 || ''; if (card) { var cardSrc = String(card).indexOf('data:') === 0 ? card : 'data:' + (profile.card_content_type || 'image/png') + ';base64,' + card; - var isOfficial = profile.card_source === 'abdm'; + var isOfficial = profile.card_source !== 'generated'; var note = isOfficial ? '' : '
Provisional card generated by the Bridge, not the official ABDM card.
'; $('#abhaVerifyCardWrap').html('ABHA card' + note); $('#abhaVerifyDownloadCard').attr('href', cardSrc).removeClass('d-none'); } else { - $('#abhaVerifyCardWrap').html('
Official ABHA card was not returned by the Bridge.
'); + var reason = profile.card_message ? escapeHtml(profile.card_message) : 'ABDM did not issue an official ABHA card for this session.'; + $('#abhaVerifyCardWrap').html('
' + reason + '
'); $('#abhaVerifyDownloadCard').addClass('d-none'); } showStep(4);