Fetch official ABHA card and surface bridge card provenance

This commit is contained in:
DevSoftTech Server 2026-08-15 03:16:25 +05:30
parent b4714270a9
commit b25ab98fd7
9 changed files with 83 additions and 18 deletions

View File

@ -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']);

View File

@ -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 '';

View File

@ -53,6 +53,13 @@ interface AbdmConnectorInterface
*/
public function abhaLoginVerifyOtp(array $payload): array;
/**
* Fetch the official ABHA card from the Bridge card endpoint.
*
* @param array<string, mixed> $payload Must contain abha_number or abha_address.
*/
public function fetchOfficialAbhaCard(array $payload): array;
// -------------------------------------------------------------------------
// Consent
// -------------------------------------------------------------------------

View File

@ -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
// -------------------------------------------------------------------------

View File

@ -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
// -------------------------------------------------------------------------

View File

@ -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 '';

View File

@ -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
? ''
: '<div class="small text-warning mt-1"><i class="bi bi-exclamation-triangle me-1"></i>Provisional card generated by the Bridge, not the official ABDM card.</div>';
$('#abhaCreateCardWrap').html('<img src="' + cardSrc + '" alt="ABHA card">' + note);
$('#abhaCreateDownloadCard').attr('href', cardSrc).removeClass('d-none');
} else {
$('#abhaCreateCardWrap').html('<div class="text-center text-muted"><i class="bi bi-card-image fs-1 d-block mb-2"></i><span>Official ABHA card was not returned by the Bridge.</span></div>');
var reason = profile.card_message ? escapeHtml(profile.card_message) : 'ABDM did not issue an official ABHA card for this session.';
$('#abhaCreateCardWrap').html('<div class="text-center text-muted"><i class="bi bi-card-image fs-1 d-block mb-2"></i><span>' + reason + '</span></div>');
$('#abhaCreateDownloadCard').addClass('d-none');
}
showStep(4);

View File

@ -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'
? ''
: '<div class="small text-warning mt-1"><i class="bi bi-exclamation-triangle me-1"></i>Provisional card generated by the Bridge, not the official ABDM card.</div>';
$('#abhaMobileCardWrap').html('<img src="' + cardSrc + '" alt="ABHA card">' + note);
} else {
$('#abhaMobileCardWrap').html('<div class="text-center text-muted"><i class="bi bi-card-image fs-1 d-block mb-2"></i>Official ABHA card was not returned by the Bridge.</div>');
var reason = profile.card_message ? escapeHtml(profile.card_message) : 'ABDM did not issue an official ABHA card for this session.';
$('#abhaMobileCardWrap').html('<div class="text-center text-muted"><i class="bi bi-card-image fs-1 d-block mb-2"></i>' + reason + '</div>');
}
showStep(3);
}

View File

@ -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
? ''
: '<div class="small text-warning mt-1"><i class="bi bi-exclamation-triangle me-1"></i>Provisional card generated by the Bridge, not the official ABDM card.</div>';
$('#abhaVerifyCardWrap').html('<img src="' + cardSrc + '" alt="ABHA card">' + note);
$('#abhaVerifyDownloadCard').attr('href', cardSrc).removeClass('d-none');
} else {
$('#abhaVerifyCardWrap').html('<div class="text-center text-muted"><i class="bi bi-card-image fs-1 d-block mb-2"></i>Official ABHA card was not returned by the Bridge.</div>');
var reason = profile.card_message ? escapeHtml(profile.card_message) : 'ABDM did not issue an official ABHA card for this session.';
$('#abhaVerifyCardWrap').html('<div class="text-center text-muted"><i class="bi bi-card-image fs-1 d-block mb-2"></i>' + reason + '</div>');
$('#abhaVerifyDownloadCard').addClass('d-none');
}
showStep(4);