Update HIU workflow, patient view, and related assets

This commit is contained in:
DevSoftTech Server 2026-07-07 01:13:28 +05:30
parent f243aeeb04
commit 6407db9dbc
9 changed files with 907 additions and 46 deletions

View File

@ -0,0 +1,37 @@
<?php
namespace App\Commands;
use App\Libraries\Abdm\M3HiuWorkflowService;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;
class AbdmHiuPoll extends BaseCommand
{
protected $group = 'ABDM';
protected $name = 'abdm:hiu-poll';
protected $description = 'Poll NAT-safe gateway endpoints for consent reconciliation and decrypted HI data.';
protected $usage = 'abdm:hiu-poll [--limit 30]';
protected $arguments = [];
protected $options = [
'--limit' => 'Maximum recent workflow roots to poll in one run.',
];
public function run(array $params)
{
$limit = (int) (CLI::getOption('limit') ?? 30);
if ($limit <= 0) {
$limit = 30;
}
$service = new M3HiuWorkflowService();
$summary = $service->pollNatGateway($limit);
CLI::write('ABDM HIU NAT Poll Summary', 'yellow');
CLI::write('Processed: ' . (int) ($summary['processed'] ?? 0));
CLI::write('Consent updates: ' . (int) ($summary['consent_updates'] ?? 0), 'green');
CLI::write('Data updates: ' . (int) ($summary['data_updates'] ?? 0), 'green');
CLI::write('Failed: ' . (int) ($summary['failed'] ?? 0), ((int) ($summary['failed'] ?? 0) > 0 ? 'red' : 'green'));
CLI::write('Skipped: ' . (int) ($summary['skipped'] ?? 0));
}
}

View File

@ -630,14 +630,19 @@ $routes->post('AbdmGateway/record_linked_callback', 'AbdmGateway::recordLinkedCa
$routes->get('AbdmHiu', 'AbdmHiu::index', ['filter' => $abdmPermFilter]); $routes->get('AbdmHiu', 'AbdmHiu::index', ['filter' => $abdmPermFilter]);
$routes->get('AbdmHiu/patient_lookup', 'AbdmHiu::patientLookup', ['filter' => $abdmPermFilter]); $routes->get('AbdmHiu/patient_lookup', 'AbdmHiu::patientLookup', ['filter' => $abdmPermFilter]);
$routes->get('AbdmHiu/timeline', 'AbdmHiu::timeline', ['filter' => $abdmPermFilter]); $routes->get('AbdmHiu/timeline', 'AbdmHiu::timeline', ['filter' => $abdmPermFilter]);
$routes->get('AbdmHiu/poll_summary', 'AbdmHiu::pollSummary', ['filter' => $abdmPermFilter]);
$routes->post('AbdmHiu/consent_request', 'AbdmHiu::consentRequest', ['filter' => $abdmPermFilter]); $routes->post('AbdmHiu/consent_request', 'AbdmHiu::consentRequest', ['filter' => $abdmPermFilter]);
$routes->post('AbdmHiu/consent_request_status', 'AbdmHiu::consentRequestStatus', ['filter' => $abdmPermFilter]); $routes->post('AbdmHiu/consent_request_status', 'AbdmHiu::consentRequestStatus', ['filter' => $abdmPermFilter]);
$routes->post('AbdmHiu/consent_request_fetch', 'AbdmHiu::consentRequestFetch', ['filter' => $abdmPermFilter]); $routes->post('AbdmHiu/consent_request_fetch', 'AbdmHiu::consentRequestFetch', ['filter' => $abdmPermFilter]);
$routes->post('AbdmHiu/health_information_request', 'AbdmHiu::healthInformationRequest', ['filter' => $abdmPermFilter]); $routes->post('AbdmHiu/health_information_request', 'AbdmHiu::healthInformationRequest', ['filter' => $abdmPermFilter]);
$routes->post('AbdmHiu/consent_reconcile', 'AbdmHiu::consentReconcile', ['filter' => $abdmPermFilter]);
$routes->post('AbdmHiu/data_fetch', 'AbdmHiu::dataFetch', ['filter' => $abdmPermFilter]);
$routes->post('api/v3/hiu/consent/request', 'AbdmHiu::consentRequest', ['filter' => $abdmPermFilter]); $routes->post('api/v3/hiu/consent/request', 'AbdmHiu::consentRequest', ['filter' => $abdmPermFilter]);
$routes->post('api/v3/hiu/consent/request/status', 'AbdmHiu::consentRequestStatus', ['filter' => $abdmPermFilter]); $routes->post('api/v3/hiu/consent/request/status', 'AbdmHiu::consentRequestStatus', ['filter' => $abdmPermFilter]);
$routes->post('api/v3/hiu/consent/request/fetch', 'AbdmHiu::consentRequestFetch', ['filter' => $abdmPermFilter]); $routes->post('api/v3/hiu/consent/request/fetch', 'AbdmHiu::consentRequestFetch', ['filter' => $abdmPermFilter]);
$routes->post('api/v3/hiu/health-information/request', 'AbdmHiu::healthInformationRequest', ['filter' => $abdmPermFilter]); $routes->post('api/v3/hiu/health-information/request', 'AbdmHiu::healthInformationRequest', ['filter' => $abdmPermFilter]);
$routes->post('api/v1/hiu/consent/status', 'AbdmHiu::consentReconcile', ['filter' => $abdmPermFilter]);
$routes->post('api/v1/hiu/data/fetch', 'AbdmHiu::dataFetch', ['filter' => $abdmPermFilter]);
// HMS M2 adapter endpoints (Gateway -> HMS) - keep lowercase v1 path // HMS M2 adapter endpoints (Gateway -> HMS) - keep lowercase v1 path
$routes->get('api/v1/abdm/gateway/health', 'AbdmGateway::m2GatewayHealth'); $routes->get('api/v1/abdm/gateway/health', 'AbdmGateway::m2GatewayHealth');
$routes->post('api/v1/abdm/gateway/discovery/care-contexts', 'AbdmGateway::m2DiscoveryCareContexts'); $routes->post('api/v1/abdm/gateway/discovery/care-contexts', 'AbdmGateway::m2DiscoveryCareContexts');

View File

@ -43,6 +43,16 @@ class AbdmHiu extends BaseController
return $this->handleOperation('hi_request', 'DATA_REQUESTED'); return $this->handleOperation('hi_request', 'DATA_REQUESTED');
} }
public function consentReconcile()
{
return $this->handleOperation('consent_reconcile', 'STATUS_CHECKED');
}
public function dataFetch()
{
return $this->handleOperation('data_fetch', 'DATA_PENDING');
}
public function timeline() public function timeline()
{ {
$filters = [ $filters = [
@ -62,6 +72,17 @@ class AbdmHiu extends BaseController
]); ]);
} }
public function pollSummary()
{
$lookback = (int) ($this->request->getGet('lookback') ?? 180);
if ($lookback <= 0) {
$lookback = 180;
}
$summary = $this->service->natPollSummary($lookback);
return $this->response->setJSON($summary);
}
public function patientLookup() public function patientLookup()
{ {
if (! $this->db->tableExists('patient_master')) { if (! $this->db->tableExists('patient_master')) {

View File

@ -31,7 +31,17 @@ class M3HiuGatewayClient
public function healthInformationRequest(array $payload): array public function healthInformationRequest(array $payload): array
{ {
return $this->call('/v3/hiu/health-information/request', $payload, 'health-information.request'); return $this->call('/v1/hiu/data/request', $payload, 'health-information.request');
}
public function reconcileConsentStatus(array $payload): array
{
return $this->callGet('/v1/hiu/consent/status', $payload, 'consent.status.reconcile');
}
public function fetchDecryptedData(array $payload): array
{
return $this->callGet('/v1/hiu/data/fetch', $payload, 'data.fetch');
} }
private function call(string $path, array $payload, string $eventKey): array private function call(string $path, array $payload, string $eventKey): array
@ -95,6 +105,10 @@ class M3HiuGatewayClient
: ('HTTP ' . $httpCode . ' non-JSON response'); : ('HTTP ' . $httpCode . ' non-JSON response');
} }
if ($this->isCloudFrontBlockedResponse($httpCode, $raw, $response ?? [])) {
$errorText = 'Gateway edge block (CloudFront 403 Request blocked). Retry later or ask gateway team to unblock the endpoint.';
}
$retryable = 0; $retryable = 0;
if ($curlErr !== '' || $httpCode >= 500 || $httpCode === 0 || str_contains(strtolower($errorText), 'timed out')) { if ($curlErr !== '' || $httpCode >= 500 || $httpCode === 0 || str_contains(strtolower($errorText), 'timed out')) {
$retryable = 1; $retryable = 1;
@ -106,6 +120,11 @@ class M3HiuGatewayClient
$response['error_text'] = $errorText; $response['error_text'] = $errorText;
$response['retryable'] = $retryable; $response['retryable'] = $retryable;
if ($this->isCloudFrontBlockedResponse($httpCode, $raw, $response)) {
$response['retryable'] = 1;
$response['error_text'] = 'Gateway edge block (CloudFront 403 Request blocked). Retry later or ask gateway team to unblock the endpoint.';
}
$gatewayRequestId = trim((string) ($response['request_id'] ?? $response['requestId'] ?? '')); $gatewayRequestId = trim((string) ($response['request_id'] ?? $response['requestId'] ?? ''));
if ($gatewayRequestId === '') { if ($gatewayRequestId === '') {
$gatewayRequestId = trim((string) ($payload['requestId'] ?? $payload['request_id'] ?? '')); $gatewayRequestId = trim((string) ($payload['requestId'] ?? $payload['request_id'] ?? ''));
@ -117,8 +136,8 @@ class M3HiuGatewayClient
'consent_request_id', 'consent_request_id',
'abdm_consent_request_id', 'abdm_consent_request_id',
]); ]);
if ($abdmConsentRequestId === '') { if ($abdmConsentRequestId !== '' && $this->isGatewayRefId($abdmConsentRequestId)) {
$abdmConsentRequestId = trim((string) ($payload['consentRequestId'] ?? '')); $abdmConsentRequestId = '';
} }
if ($abdmConsentRequestId !== '') { if ($abdmConsentRequestId !== '') {
$response['abdm_consent_request_id'] = $abdmConsentRequestId; $response['abdm_consent_request_id'] = $abdmConsentRequestId;
@ -129,8 +148,8 @@ class M3HiuGatewayClient
'consentId', 'consentId',
'consent_id', 'consent_id',
]); ]);
if ($abdmConsentId === '') { if ($abdmConsentId !== '' && $this->isGatewayRefId($abdmConsentId)) {
$abdmConsentId = trim((string) ($payload['consentId'] ?? $payload['consent_id'] ?? '')); $abdmConsentId = '';
} }
if ($abdmConsentId !== '') { if ($abdmConsentId !== '') {
$response['consent_id'] = $abdmConsentId; $response['consent_id'] = $abdmConsentId;
@ -149,6 +168,137 @@ class M3HiuGatewayClient
return $response; return $response;
} }
private function callGet(string $path, array $query, string $eventKey): array
{
$settings = $this->resolveRuntimeSettings();
if (($settings['ok'] ?? 0) !== 1) {
return $settings;
}
$token = (string) ($settings['token'] ?? '');
$hfrId = (string) ($settings['hfr_id'] ?? '');
$hospitalId = (string) ($settings['hospital_id'] ?? '');
$payloadHfrId = trim((string) ($query['hfr_id'] ?? ''));
if ($payloadHfrId !== '' && strcasecmp($payloadHfrId, $hfrId) !== 0) {
return [
'ok' => 0,
'http_code' => 400,
'error_text' => 'hfr_id mismatch: payload hfr_id does not match hospital_setting.ABDM_HFR_ID',
'retryable' => 0,
];
}
$headers = [
'Accept: application/json',
'Authorization: Bearer ' . $token,
];
if ($hospitalId !== '') {
$headers[] = 'X-Hospital-Id: ' . $hospitalId;
}
$cleanQuery = [];
foreach ($query as $k => $v) {
if ($v === null) {
continue;
}
$val = trim((string) $v);
if ($val === '') {
continue;
}
$cleanQuery[$k] = $val;
}
// Bridge polling APIs require hfr_id in query for auth scope validation.
if ($hfrId !== '') {
$cleanQuery['hfr_id'] = $hfrId;
}
$url = $this->baseUrl . $path;
if (! empty($cleanQuery)) {
$url .= '?' . http_build_query($cleanQuery);
}
$ch = curl_init();
curl_setopt_array($ch, [
CURLOPT_URL => $url,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => $this->timeoutSec,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_CUSTOMREQUEST => 'GET',
]);
$raw = curl_exec($ch);
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlErr = curl_error($ch);
curl_close($ch);
$decoded = json_decode((string) $raw, true);
$isJson = is_array($decoded);
$ok = $isJson ? (int) ($decoded['ok'] ?? ($httpCode >= 200 && $httpCode < 300 ? 1 : 0)) : (int) ($httpCode >= 200 && $httpCode < 300);
$errorText = '';
if ($curlErr !== '') {
$errorText = $curlErr;
} elseif ($ok !== 1) {
$errorText = $isJson
? (string) ($decoded['message'] ?? $decoded['error_text'] ?? $decoded['error'] ?? ('HTTP ' . $httpCode))
: ('HTTP ' . $httpCode . ' non-JSON response');
}
if ($this->isCloudFrontBlockedResponse($httpCode, $raw, $decoded ?? [])) {
$errorText = 'Gateway edge block (CloudFront 403 Request blocked). Retry later or ask gateway team to unblock the endpoint.';
}
$retryable = 0;
if ($curlErr !== '' || $httpCode >= 500 || $httpCode === 0 || str_contains(strtolower($errorText), 'timed out')) {
$retryable = 1;
}
$response = $isJson ? $decoded : ['raw' => (string) $raw];
$response['ok'] = $ok;
$response['http_code'] = $httpCode;
$response['error_text'] = $errorText;
$response['retryable'] = $retryable;
if ($this->isCloudFrontBlockedResponse($httpCode, $raw, $response)) {
$response['retryable'] = 1;
$response['error_text'] = 'Gateway edge block (CloudFront 403 Request blocked). Retry later or ask gateway team to unblock the endpoint.';
}
$gatewayRequestId = trim((string) ($response['request_id'] ?? $response['requestId'] ?? $cleanQuery['request_id'] ?? ''));
$response['gateway_request_id'] = $gatewayRequestId;
$abdmConsentRequestId = $this->findFirstValueByKeys($response, [
'consentRequestId',
'consent_request_id',
'abdm_consent_request_id',
]);
if ($abdmConsentRequestId !== '' && ! $this->isGatewayRefId($abdmConsentRequestId)) {
$response['abdm_consent_request_id'] = $abdmConsentRequestId;
$response['consent_request_id'] = $abdmConsentRequestId;
}
$abdmConsentId = $this->findFirstValueByKeys($response, [
'consentId',
'consent_id',
'abdm_consent_artifact_id',
]);
if ($abdmConsentId !== '' && ! $this->isGatewayRefId($abdmConsentId)) {
$response['consent_id'] = $abdmConsentId;
$response['abdm_consent_artifact_id'] = $abdmConsentId;
}
if (! isset($response['transaction_id'])) {
$response['transaction_id'] = (string) ($response['transactionId'] ?? $cleanQuery['transaction_id'] ?? '');
}
$this->writeApiLog($eventKey, $path, $cleanQuery, $response, $httpCode, $ok, $errorText, $hospitalId, 'GET');
return $response;
}
private function resolveRuntimeSettings(): array private function resolveRuntimeSettings(): array
{ {
if (! $this->db->tableExists('hospital_setting')) { if (! $this->db->tableExists('hospital_setting')) {
@ -207,7 +357,8 @@ class M3HiuGatewayClient
int $httpCode, int $httpCode,
int $ok, int $ok,
string $errorText, string $errorText,
string $hospitalId string $hospitalId,
string $httpMethod = 'POST'
): void { ): void {
if (! $this->db->tableExists('abdm_api_logs')) { if (! $this->db->tableExists('abdm_api_logs')) {
return; return;
@ -229,7 +380,7 @@ class M3HiuGatewayClient
'channel' => 'eatria_bridge', 'channel' => 'eatria_bridge',
'event_type' => 'abdm.m3.hiu.' . $eventKey, 'event_type' => 'abdm.m3.hiu.' . $eventKey,
'endpoint' => $this->baseUrl . $path, 'endpoint' => $this->baseUrl . $path,
'http_method' => 'POST', 'http_method' => strtoupper(trim($httpMethod)) !== '' ? strtoupper(trim($httpMethod)) : 'POST',
'entity_type' => 'abdm_hiu', 'entity_type' => 'abdm_hiu',
'entity_id' => $entityId, 'entity_id' => $entityId,
'request_json' => (string) json_encode($request, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), 'request_json' => (string) json_encode($request, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
@ -268,4 +419,25 @@ class M3HiuGatewayClient
return ''; return '';
} }
private function isGatewayRefId(string $value): bool
{
return preg_match('/^REQ-/i', trim($value)) === 1;
}
private function isCloudFrontBlockedResponse(int $httpCode, $raw, array $response): bool
{
if ($httpCode !== 403) {
return false;
}
$text = strtolower((string) $raw);
if ($text === '' && isset($response['data']['raw'])) {
$text = strtolower((string) $response['data']['raw']);
}
return str_contains($text, 'generated by cloudfront')
|| str_contains($text, 'request blocked')
|| str_contains($text, 'the request could not be satisfied');
}
} }

View File

@ -50,7 +50,7 @@ class M3HiuWorkflowService
} }
$dispatchPayload = (array) ($prepared['payload'] ?? []); $dispatchPayload = (array) ($prepared['payload'] ?? []);
if ($hasWorkflowTable && $operation !== 'consent_status') { if ($hasWorkflowTable && ! in_array($operation, ['consent_status', 'consent_reconcile', 'data_fetch', 'hi_request'], true)) {
$duplicate = $this->findDuplicate($operation, $normalized); $duplicate = $this->findDuplicate($operation, $normalized);
if ($duplicate !== null) { if ($duplicate !== null) {
$dupResponse = json_decode((string) ($duplicate['response_json'] ?? ''), true); $dupResponse = json_decode((string) ($duplicate['response_json'] ?? ''), true);
@ -70,8 +70,19 @@ class M3HiuWorkflowService
$status = $ok ? 'success' : 'failed'; $status = $ok ? 'success' : 'failed';
$retryable = (int) ($result['retryable'] ?? 0) === 1; $retryable = (int) ($result['retryable'] ?? 0) === 1;
$persistPayload = $dispatchPayload;
foreach ([
'request_id', 'requestId', 'transaction_id', 'transactionId',
'abha_address', 'consent_id', 'consentId',
'abdm_consent_request_id', 'abdm_consent_artifact_id', 'hfr_id',
] as $key) {
if (! array_key_exists($key, $persistPayload) && array_key_exists($key, $normalized)) {
$persistPayload[$key] = $normalized[$key];
}
}
if ($hasWorkflowTable) { if ($hasWorkflowTable) {
$this->upsertWorkflow($operation, $dispatchPayload, $result, $state, $status, $httpCode, $retryable); $this->upsertWorkflow($operation, $persistPayload, $result, $state, $status, $httpCode, $retryable);
$result['workflow_persisted'] = 1; $result['workflow_persisted'] = 1;
} else { } else {
$result['workflow_persisted'] = 0; $result['workflow_persisted'] = 0;
@ -129,6 +140,105 @@ class M3HiuWorkflowService
return $summary; return $summary;
} }
public function pollNatGateway(int $limit = 30): array
{
if (! $this->db->tableExists('abdm_hiu_workflows')) {
return ['processed' => 0, 'consent_updates' => 0, 'data_updates' => 0, 'failed' => 0, 'skipped' => 0];
}
$summary = ['processed' => 0, 'consent_updates' => 0, 'data_updates' => 0, 'failed' => 0, 'skipped' => 0];
$consentRows = $this->db->table('abdm_hiu_workflows')
->select('*')
->where('operation', 'consent_request')
->where('status', 'success')
->orderBy('id', 'DESC')
->get($limit)
->getResultArray();
$seenConsentRequests = [];
foreach ($consentRows as $row) {
$requestId = trim((string) ($row['request_id'] ?? ''));
if ($requestId === '' || isset($seenConsentRequests[$requestId])) {
$summary['skipped']++;
continue;
}
$seenConsentRequests[$requestId] = true;
$summary['processed']++;
$payload = [
'request_id' => $requestId,
'transaction_id' => trim((string) ($row['transaction_id'] ?? '')),
'consent_id' => trim((string) ($row['consent_id'] ?? '')),
'abdm_consent_request_id' => trim((string) ($row['abdm_consent_request_id'] ?? '')),
'abdm_consent_artifact_id' => trim((string) ($row['abdm_consent_artifact_id'] ?? '')),
'abha_address' => trim((string) ($row['abha_address'] ?? '')),
'hfr_id' => $this->normalizeStoredHfrId((string) ($row['hfr_id'] ?? '')),
];
$consentResult = $this->runOperation('consent_reconcile', $payload);
if ((int) ($consentResult['ok'] ?? 0) === 1) {
$summary['consent_updates']++;
$resolvedConsentId = trim((string) ($consentResult['abdm_consent_artifact_id'] ?? $consentResult['consent_id'] ?? ''));
if ($resolvedConsentId !== '' && ! $this->hasSuccessfulHiRequest($requestId, $resolvedConsentId)) {
if (! $this->shouldAttemptHiRequest($requestId, $resolvedConsentId)) {
$summary['skipped']++;
continue;
}
$hiPayload = $payload;
$hiPayload['consent_id'] = $resolvedConsentId;
$hiPayload['abdm_consent_artifact_id'] = $resolvedConsentId;
$hiPayload['transaction_id'] = $this->generateFreshHiTransactionId($requestId, trim((string) ($payload['transaction_id'] ?? '')));
$hiResult = $this->runOperation('hi_request', $hiPayload);
if ((int) ($hiResult['ok'] ?? 0) !== 1) {
$summary['failed']++;
}
}
} else {
$summary['failed']++;
}
}
$hiRows = $this->db->table('abdm_hiu_workflows')
->select('*')
->where('operation', 'hi_request')
->where('status', 'success')
->orderBy('id', 'DESC')
->get($limit)
->getResultArray();
$seenTransactions = [];
foreach ($hiRows as $row) {
$transactionId = trim((string) ($row['transaction_id'] ?? ''));
$requestId = trim((string) ($row['request_id'] ?? ''));
$key = $transactionId !== '' ? $transactionId : $requestId;
if ($key === '' || isset($seenTransactions[$key])) {
$summary['skipped']++;
continue;
}
$seenTransactions[$key] = true;
$payload = [
'request_id' => $requestId,
'transaction_id' => $transactionId,
'consent_id' => trim((string) ($row['consent_id'] ?? '')),
'abdm_consent_artifact_id' => trim((string) ($row['abdm_consent_artifact_id'] ?? '')),
'abha_address' => trim((string) ($row['abha_address'] ?? '')),
'hfr_id' => $this->normalizeStoredHfrId((string) ($row['hfr_id'] ?? '')),
];
$dataResult = $this->runOperation('data_fetch', $payload);
if ((int) ($dataResult['ok'] ?? 0) === 1) {
$summary['data_updates']++;
} else {
$summary['failed']++;
}
}
return $summary;
}
public function listTimeline(array $filters, int $limit = 200): array public function listTimeline(array $filters, int $limit = 200): array
{ {
if (! $this->db->tableExists('abdm_hiu_workflows')) { if (! $this->db->tableExists('abdm_hiu_workflows')) {
@ -165,6 +275,87 @@ class M3HiuWorkflowService
return $builder->get($limit)->getResultArray(); return $builder->get($limit)->getResultArray();
} }
public function natPollSummary(int $lookbackMinutes = 180): array
{
if (! $this->db->tableExists('abdm_hiu_workflows')) {
return [
'ok' => 0,
'message' => 'abdm_hiu_workflows table not found',
'last_polled_at' => null,
'consent_updates' => 0,
'data_updates' => 0,
'failed' => 0,
'pending' => 0,
'recent_errors' => [],
];
}
$fromTime = date('Y-m-d H:i:s', strtotime('-' . max(1, $lookbackMinutes) . ' minutes'));
$lastRow = $this->db->table('abdm_hiu_workflows')
->select('updated_at')
->whereIn('operation', ['consent_reconcile', 'data_fetch'])
->orderBy('id', 'DESC')
->get(1)
->getRowArray();
$rows = $this->db->table('abdm_hiu_workflows')
->select('operation, status, workflow_state')
->whereIn('operation', ['consent_reconcile', 'data_fetch'])
->where('updated_at >=', $fromTime)
->get()
->getResultArray();
$errorRows = $this->db->table('abdm_hiu_workflows')
->select('updated_at, operation, request_id, transaction_id, last_error')
->whereIn('operation', ['consent_reconcile', 'data_fetch'])
->where('status', 'failed')
->where('updated_at >=', $fromTime)
->orderBy('updated_at', 'DESC')
->orderBy('id', 'DESC')
->get(8)
->getResultArray();
$consentUpdates = 0;
$dataUpdates = 0;
$failed = 0;
$pending = 0;
foreach ($rows as $row) {
$operation = trim((string) ($row['operation'] ?? ''));
$status = strtolower(trim((string) ($row['status'] ?? '')));
$state = strtolower(trim((string) ($row['workflow_state'] ?? '')));
if ($status === 'failed') {
$failed++;
continue;
}
if ($operation === 'consent_reconcile' && in_array($state, ['granted', 'status_checked'], true)) {
$consentUpdates++;
continue;
}
if ($operation === 'data_fetch' && in_array($state, ['data_received', 'completed'], true)) {
$dataUpdates++;
continue;
}
$pending++;
}
return [
'ok' => 1,
'last_polled_at' => $lastRow['updated_at'] ?? null,
'lookback_minutes' => $lookbackMinutes,
'consent_updates' => $consentUpdates,
'data_updates' => $dataUpdates,
'failed' => $failed,
'pending' => $pending,
'recent_errors' => $errorRows,
];
}
private function dispatch(string $operation, array $payload): array private function dispatch(string $operation, array $payload): array
{ {
return match ($operation) { return match ($operation) {
@ -172,6 +363,8 @@ class M3HiuWorkflowService
'consent_status' => $this->client->consentRequestStatus($payload), 'consent_status' => $this->client->consentRequestStatus($payload),
'consent_fetch' => $this->client->consentRequestFetch($payload), 'consent_fetch' => $this->client->consentRequestFetch($payload),
'hi_request' => $this->client->healthInformationRequest($payload), 'hi_request' => $this->client->healthInformationRequest($payload),
'consent_reconcile' => $this->client->reconcileConsentStatus($payload),
'data_fetch' => $this->client->fetchDecryptedData($payload),
default => ['ok' => 0, 'http_code' => 400, 'error_text' => 'Unsupported operation: ' . $operation, 'retryable' => 0], default => ['ok' => 0, 'http_code' => 400, 'error_text' => 'Unsupported operation: ' . $operation, 'retryable' => 0],
}; };
} }
@ -335,18 +528,21 @@ class M3HiuWorkflowService
if ($operation === 'consent_status') { if ($operation === 'consent_status') {
$consentRequestId = trim((string) ($clean['abdm_consent_request_id'] ?? $clean['consentRequestId'] ?? $clean['consent_id'] ?? '')); $consentRequestId = trim((string) ($clean['abdm_consent_request_id'] ?? $clean['consentRequestId'] ?? $clean['consent_id'] ?? ''));
if ($consentRequestId === '') {
$consentRequestId = $this->resolveAbdmConsentRequestIdFromWorkflow($clean);
}
if ($consentRequestId === '') { if ($consentRequestId === '') {
return [ return [
'ok' => 0, 'ok' => 0,
'http_code' => 422, 'http_code' => 422,
'error_text' => 'consentRequestId is required for consent status.', 'error_text' => 'consentRequestId is required for consent status. Run consent request first or provide the ABDM consentRequestId from init response.',
]; ];
} }
if ($this->isGatewayRequestIdPattern($consentRequestId)) { if ($this->isGatewayRequestIdPattern($consentRequestId)) {
return [ return [
'ok' => 0, 'ok' => 0,
'http_code' => 422, 'http_code' => 422,
'error_text' => 'mapping_error: consentRequestId appears to be gateway request_id; use stored ABDM consentRequestId.', 'error_text' => 'mapping_error: consentRequestId appears to be a local temporary request id; use consentRequestId returned by consent request/status.',
]; ];
} }
@ -369,7 +565,7 @@ class M3HiuWorkflowService
return [ return [
'ok' => 0, 'ok' => 0,
'http_code' => 422, 'http_code' => 422,
'error_text' => 'mapping_error: consentId appears to be gateway request_id; use ABDM consent artifact id only.', 'error_text' => 'mapping_error: consentId appears to be a local temporary request id; use ABDM consent artifact id only.',
]; ];
} }
@ -381,7 +577,10 @@ class M3HiuWorkflowService
if ($operation === 'hi_request') { if ($operation === 'hi_request') {
$hiRequest = $clean['hiRequest'] ?? null; $hiRequest = $clean['hiRequest'] ?? null;
if (! is_array($hiRequest)) { if (! is_array($hiRequest) || empty($hiRequest)) {
$hiRequest = $this->buildDefaultHiRequest($clean);
}
if (! is_array($hiRequest) || empty($hiRequest)) {
return [ return [
'ok' => 0, 'ok' => 0,
'http_code' => 422, 'http_code' => 422,
@ -403,9 +602,85 @@ class M3HiuWorkflowService
} }
$hiRequest['consent']['id'] = $consentId; $hiRequest['consent']['id'] = $consentId;
$requestIdRef = trim((string) ($clean['request_id'] ?? $clean['requestId'] ?? ''));
$transactionId = $this->generateFreshHiTransactionId(
$requestIdRef,
trim((string) ($hiRequest['transactionId'] ?? $clean['transactionId'] ?? $clean['transaction_id'] ?? ''))
);
$hiRequest['transactionId'] = $transactionId;
if (! isset($hiRequest['dateRange']) || ! is_array($hiRequest['dateRange'])) {
$hiRequest['dateRange'] = [
'from' => gmdate('Y-m-d\T00:00:00.000\Z', strtotime('-30 days')),
'to' => gmdate('Y-m-d\T00:00:00.000\Z'),
];
}
if (trim((string) ($hiRequest['dataPushUrl'] ?? '')) === '') {
$hiRequest['dataPushUrl'] = rtrim((string) (getenv('EATRIA_BRIDGE_URL') ?: 'https://abdm-bridge.e-atria.in/api'), '/') . '/v3/hiu/data/push';
}
return [ return [
'ok' => 1, 'ok' => 1,
'payload' => $base + ['hiRequest' => $hiRequest], 'payload' => $base + ['transactionId' => $transactionId, 'transaction_id' => $transactionId, 'hiRequest' => $hiRequest],
];
}
if ($operation === 'consent_reconcile') {
$requestIdRef = trim((string) ($clean['request_id'] ?? $clean['requestId'] ?? ''));
$consentRequestId = trim((string) ($clean['abdm_consent_request_id'] ?? $clean['consentRequestId'] ?? ''));
$consentId = trim((string) ($clean['abdm_consent_artifact_id'] ?? $clean['consentId'] ?? $clean['consent_id'] ?? ''));
if ($requestIdRef === '' && $consentRequestId === '' && $consentId === '') {
return [
'ok' => 0,
'http_code' => 422,
'error_text' => 'One of request_id, consent_request_id, or consent_id is required for consent reconciliation.',
];
}
$query = [];
if ($requestIdRef !== '') {
$query['request_id'] = $requestIdRef;
}
if ($consentRequestId !== '' && ! $this->isGatewayRequestIdPattern($consentRequestId)) {
$query['consent_request_id'] = $consentRequestId;
}
if ($consentId !== '' && ! $this->isGatewayRequestIdPattern($consentId)) {
$query['consent_id'] = $consentId;
}
return [
'ok' => 1,
'payload' => $query,
];
}
if ($operation === 'data_fetch') {
$transactionId = trim((string) ($clean['transaction_id'] ?? $clean['transactionId'] ?? ''));
$consentId = trim((string) ($clean['abdm_consent_artifact_id'] ?? $clean['consentId'] ?? $clean['consent_id'] ?? ''));
if ($consentId !== '' && $this->isGatewayRequestIdPattern($consentId)) {
$consentId = '';
}
if ($transactionId === '' && $consentId === '') {
return [
'ok' => 0,
'http_code' => 422,
'error_text' => 'One of transaction_id or consent_id is required for data fetch polling.',
];
}
$query = [];
if ($transactionId !== '') {
$query['transaction_id'] = $transactionId;
}
if ($consentId !== '') {
$query['consent_id'] = $consentId;
}
return [
'ok' => 1,
'payload' => $query,
]; ];
} }
@ -501,6 +776,32 @@ class M3HiuWorkflowService
return $consent; return $consent;
} }
private function buildDefaultHiRequest(array $payload): array
{
$consentId = trim((string) ($payload['abdm_consent_artifact_id'] ?? $payload['consentId'] ?? $payload['consent_id'] ?? ''));
if ($consentId === '' || $this->isGatewayRequestIdPattern($consentId)) {
return [];
}
$requestIdRef = trim((string) ($payload['request_id'] ?? $payload['requestId'] ?? ''));
$transactionId = $this->generateFreshHiTransactionId(
$requestIdRef,
trim((string) ($payload['transactionId'] ?? $payload['transaction_id'] ?? ''))
);
return [
'consent' => [
'id' => $consentId,
],
'dateRange' => [
'from' => gmdate('Y-m-d\T00:00:00.000\Z', strtotime('-30 days')),
'to' => gmdate('Y-m-d\T00:00:00.000\Z'),
],
'dataPushUrl' => rtrim((string) (getenv('EATRIA_BRIDGE_URL') ?: 'https://abdm-bridge.e-atria.in/api'), '/') . '/v3/hiu/data/push',
'transactionId' => $transactionId,
];
}
private function readSettingValue(string $key): string private function readSettingValue(string $key): string
{ {
if (! $this->db->tableExists('hospital_setting')) { if (! $this->db->tableExists('hospital_setting')) {
@ -530,6 +831,26 @@ class M3HiuWorkflowService
return $envName; return $envName;
} }
private function resolveHospitalHfrId(): string
{
$value = $this->readSettingValue('ABDM_HFR_ID');
if ($value !== '') {
return $value;
}
return trim((string) (getenv('ABDM_HFR_ID') ?: ''));
}
private function normalizeStoredHfrId(string $value): string
{
$candidate = trim($value);
if ($candidate === '' || strcasecmp($candidate, 'UNKNOWN') === 0) {
return $this->resolveHospitalHfrId();
}
return $candidate;
}
private function resolveHiuServiceId(): string private function resolveHiuServiceId(): string
{ {
if (! $this->db->tableExists('hospital_setting')) { if (! $this->db->tableExists('hospital_setting')) {
@ -617,6 +938,8 @@ class M3HiuWorkflowService
'consent_status' => 'STATUS_CHECKED', 'consent_status' => 'STATUS_CHECKED',
'consent_fetch' => 'CONSENT_FETCHED', 'consent_fetch' => 'CONSENT_FETCHED',
'hi_request' => ($statusStr === 'completed' ? 'COMPLETED' : 'DATA_REQUESTED'), 'hi_request' => ($statusStr === 'completed' ? 'COMPLETED' : 'DATA_REQUESTED'),
'consent_reconcile' => ($statusStr === 'granted' ? 'GRANTED' : ($statusStr === 'revoked' ? 'REVOKED' : ($statusStr === 'expired' ? 'EXPIRED' : 'STATUS_CHECKED'))),
'data_fetch' => (($statusStr === 'completed' || isset($result['fhir_bundle']) || isset($result['bundles'])) ? 'DATA_RECEIVED' : 'DATA_PENDING'),
default => 'COMPLETED', default => 'COMPLETED',
}; };
} }
@ -650,6 +973,111 @@ class M3HiuWorkflowService
return null; return null;
} }
private function hasSuccessfulHiRequest(string $requestId, string $consentId): bool
{
if (! $this->db->tableExists('abdm_hiu_workflows')) {
return false;
}
$builder = $this->db->table('abdm_hiu_workflows')
->select('id')
->where('operation', 'hi_request')
->where('status', 'success');
$builder->groupStart();
if ($requestId !== '') {
$builder->orWhere('request_id', $requestId);
}
if ($consentId !== '') {
$builder->orWhere('consent_id', $consentId);
if (in_array('abdm_consent_artifact_id', $this->db->getFieldNames('abdm_hiu_workflows') ?? [], true)) {
$builder->orWhere('abdm_consent_artifact_id', $consentId);
}
}
$builder->groupEnd();
$row = $builder->orderBy('id', 'DESC')->get(1)->getRowArray();
return ! empty($row);
}
private function shouldAttemptHiRequest(string $requestId, string $consentId): bool
{
if (! $this->db->tableExists('abdm_hiu_workflows')) {
return true;
}
$row = $this->latestHiRequestRow($requestId, $consentId);
if (empty($row)) {
return true;
}
$status = strtolower(trim((string) ($row['status'] ?? '')));
$retryable = (int) ($row['is_retryable'] ?? 0) === 1;
$retryCount = (int) ($row['retry_count'] ?? 0);
$nextRetryAt = trim((string) ($row['next_retry_at'] ?? ''));
if ($status !== 'failed' || ! $retryable) {
return true;
}
if ($nextRetryAt === '') {
return $retryCount < 4;
}
return strtotime($nextRetryAt) <= time();
}
private function latestHiRequestRow(string $requestId, string $consentId): array
{
if (! $this->db->tableExists('abdm_hiu_workflows')) {
return [];
}
$fields = $this->db->getFieldNames('abdm_hiu_workflows') ?? [];
$builder = $this->db->table('abdm_hiu_workflows')
->select('id, status, is_retryable, retry_count, next_retry_at')
->where('operation', 'hi_request');
$builder->groupStart();
$hasCondition = false;
if ($requestId !== '') {
$builder->where('request_id', $requestId);
$hasCondition = true;
}
if ($consentId !== '') {
if ($hasCondition) {
$builder->orWhere('consent_id', $consentId);
} else {
$builder->where('consent_id', $consentId);
$hasCondition = true;
}
if (in_array('abdm_consent_artifact_id', $fields, true)) {
$builder->orWhere('abdm_consent_artifact_id', $consentId);
}
}
$builder->groupEnd();
if (! $hasCondition) {
return [];
}
return $builder->orderBy('id', 'DESC')->get(1)->getRowArray() ?? [];
}
private function generateFreshHiTransactionId(string $requestId, string $currentTransactionId): string
{
return $this->generateUuidV4();
}
private function generateUuidV4(): string
{
$bytes = random_bytes(16);
$bytes[6] = chr((ord($bytes[6]) & 0x0f) | 0x40);
$bytes[8] = chr((ord($bytes[8]) & 0x3f) | 0x80);
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($bytes), 4));
}
private function upsertWorkflow( private function upsertWorkflow(
string $operation, string $operation,
array $payload, array $payload,
@ -666,14 +1094,14 @@ class M3HiuWorkflowService
$hmsRequestId = trim((string) ($result['hms_request_id'] ?? '')); $hmsRequestId = trim((string) ($result['hms_request_id'] ?? ''));
} }
$gatewayRequestId = trim((string) ($result['gateway_request_id'] ?? $result['request_id'] ?? $result['requestId'] ?? '')); $gatewayRequestId = trim((string) ($result['gateway_request_id'] ?? $result['request_id'] ?? $result['requestId'] ?? ''));
$abdmConsentRequestId = trim((string) ($result['abdm_consent_request_id'] ?? $result['consent_request_id'] ?? $payload['consentRequestId'] ?? '')); $abdmConsentRequestId = trim((string) ($result['abdm_consent_request_id'] ?? $result['consent_request_id'] ?? $payload['consentRequestId'] ?? $payload['consent_request_id'] ?? ''));
$transactionId = trim((string) ($payload['transaction_id'] ?? $payload['transactionId'] ?? $result['transaction_id'] ?? '')); $transactionId = trim((string) ($payload['transaction_id'] ?? $payload['transactionId'] ?? $result['transaction_id'] ?? ''));
$consentId = trim((string) ($payload['abdm_consent_artifact_id'] ?? $payload['consent_id'] ?? $payload['consentId'] ?? $result['abdm_consent_artifact_id'] ?? $result['consent_id'] ?? '')); $consentId = trim((string) ($payload['abdm_consent_artifact_id'] ?? $payload['consent_id'] ?? $payload['consentId'] ?? $result['abdm_consent_artifact_id'] ?? $result['consent_id'] ?? $result['consentId'] ?? ''));
if ($consentId === '' && $operation !== 'consent_fetch' && $operation !== 'hi_request') { if ($consentId === '' && $operation !== 'consent_fetch' && $operation !== 'hi_request') {
$consentId = $abdmConsentRequestId; $consentId = $abdmConsentRequestId;
} }
$abhaAddress = trim((string) ($payload['abha_address'] ?? $payload['abha_id'] ?? $result['abha_address'] ?? '')); $abhaAddress = trim((string) ($payload['abha_address'] ?? $payload['abha_id'] ?? $result['abha_address'] ?? ''));
$hfrId = trim((string) ($payload['hfr_id'] ?? $result['hfr_id'] ?? '')); $hfrId = $this->normalizeStoredHfrId((string) ($payload['hfr_id'] ?? $result['hfr_id'] ?? ''));
$hospitalId = trim((string) ($payload['hospital_id'] ?? $result['hospital_id'] ?? '')); $hospitalId = trim((string) ($payload['hospital_id'] ?? $result['hospital_id'] ?? ''));
$errorText = trim((string) ($result['error_text'] ?? $result['message'] ?? '')); $errorText = trim((string) ($result['error_text'] ?? $result['message'] ?? ''));
@ -702,7 +1130,7 @@ class M3HiuWorkflowService
$row = [ $row = [
'hospital_id' => $hospitalId !== '' ? $hospitalId : null, 'hospital_id' => $hospitalId !== '' ? $hospitalId : null,
'hfr_id' => $hfrId !== '' ? $hfrId : 'UNKNOWN', 'hfr_id' => $hfrId !== '' ? $hfrId : null,
'abha_address' => $abhaAddress !== '' ? $abhaAddress : null, 'abha_address' => $abhaAddress !== '' ? $abhaAddress : null,
'consent_id' => $consentId !== '' ? $consentId : null, 'consent_id' => $consentId !== '' ? $consentId : null,
'request_id' => $hmsRequestId !== '' ? $hmsRequestId : null, 'request_id' => $hmsRequestId !== '' ? $hmsRequestId : null,
@ -754,6 +1182,84 @@ class M3HiuWorkflowService
private function isGatewayRequestIdPattern(string $value): bool private function isGatewayRequestIdPattern(string $value): bool
{ {
return preg_match('/^REQ-\d{14}-[A-Za-z0-9]{6,}$/', trim($value)) === 1; return preg_match('/^REQ-/i', trim($value)) === 1;
}
private function resolveAbdmConsentRequestIdFromWorkflow(array $payload): string
{
if (! $this->db->tableExists('abdm_hiu_workflows')) {
return '';
}
$fields = $this->db->getFieldNames('abdm_hiu_workflows') ?? [];
$hasAbdmConsentRequestId = in_array('abdm_consent_request_id', $fields, true);
$requestId = trim((string) ($payload['requestId'] ?? $payload['request_id'] ?? ''));
$transactionId = trim((string) ($payload['transactionId'] ?? $payload['transaction_id'] ?? ''));
$abhaAddress = trim((string) ($payload['abha_address'] ?? $payload['abha_id'] ?? ''));
$tryBuilder = function (?string $whereCol, string $whereVal) use ($hasAbdmConsentRequestId): string {
if ($whereCol === null || $whereVal === '') {
return '';
}
$select = ['consent_id', 'response_json'];
if ($hasAbdmConsentRequestId) {
$select[] = 'abdm_consent_request_id';
}
$rows = $this->db->table('abdm_hiu_workflows')
->select(implode(', ', $select))
->where($whereCol, $whereVal)
->whereIn('operation', ['consent_request', 'consent_status'])
->orderBy('id', 'DESC')
->get(25)
->getResultArray();
foreach ($rows as $row) {
$candidate = '';
if ($hasAbdmConsentRequestId) {
$candidate = trim((string) ($row['abdm_consent_request_id'] ?? ''));
}
if ($candidate === '') {
$candidate = trim((string) ($row['consent_id'] ?? ''));
}
if ($candidate !== '' && ! $this->isGatewayRequestIdPattern($candidate)) {
return $candidate;
}
$decoded = json_decode((string) ($row['response_json'] ?? ''), true);
if (is_array($decoded)) {
$candidate = trim((string) (
$decoded['abdm_consent_request_id']
?? $decoded['consent_request_id']
?? $decoded['consentRequestId']
?? ''
));
if ($candidate !== '' && ! $this->isGatewayRequestIdPattern($candidate)) {
return $candidate;
}
}
}
return '';
};
$resolved = $tryBuilder('request_id', $requestId);
if ($resolved !== '') {
return $resolved;
}
$resolved = $tryBuilder('transaction_id', $transactionId);
if ($resolved !== '') {
return $resolved;
}
$resolved = $tryBuilder('abha_address', $abhaAddress);
if ($resolved !== '') {
return $resolved;
}
return '';
} }
} }

View File

@ -3,10 +3,19 @@
<div class="col-12"> <div class="col-12">
<div class="card shadow-sm"> <div class="card shadow-sm">
<div class="card-header d-flex justify-content-between align-items-center"> <div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">ABDM M3 HIU Console</h5> <div class="d-flex align-items-center gap-2">
<h5 class="mb-0">ABDM M3 HIU Console</h5>
<span id="poll_summary_badge" class="badge bg-secondary">Poll: waiting</span>
<button class="btn btn-sm btn-outline-warning" id="btnPollErrors" type="button">Show Poll Errors</button>
</div>
<button class="btn btn-sm btn-outline-primary" id="btnRefreshTimeline">Refresh Timeline</button> <button class="btn btn-sm btn-outline-primary" id="btnRefreshTimeline">Refresh Timeline</button>
</div> </div>
<div class="card-body"> <div class="card-body">
<div id="poll_error_panel" class="alert alert-warning d-none mb-3">
<div class="fw-bold mb-1">Recent Poll Errors</div>
<ul id="poll_error_list" class="mb-0 ps-3"></ul>
</div>
<div class="alert alert-info mb-3"> <div class="alert alert-info mb-3">
Search patient first. Consent Request is enabled only when ABHA Number and ABHA Address are both present. Search patient first. Consent Request is enabled only when ABHA Number and ABHA Address are both present.
</div> </div>
@ -118,13 +127,16 @@
var consentPollTimer = null; var consentPollTimer = null;
var consentPollAttempts = 0; var consentPollAttempts = 0;
var consentPollMaxAttempts = 12; var consentPollMaxAttempts = 12;
var recentPollErrors = [];
var opRoute = { var opRoute = {
consent_request: '<?= base_url('AbdmHiu/consent_request') ?>', consent_request: '<?= base_url('AbdmHiu/consent_request') ?>',
consent_status: '<?= base_url('AbdmHiu/consent_request_status') ?>', consent_status: '<?= base_url('AbdmHiu/consent_reconcile') ?>',
consent_fetch: '<?= base_url('AbdmHiu/consent_request_fetch') ?>', consent_fetch: '<?= base_url('AbdmHiu/consent_reconcile') ?>',
hi_request: '<?= base_url('AbdmHiu/health_information_request') ?>' hi_request: '<?= base_url('AbdmHiu/health_information_request') ?>',
data_fetch: '<?= base_url('AbdmHiu/data_fetch') ?>'
}; };
var pollSummaryUrl = '<?= base_url('AbdmHiu/poll_summary') ?>';
var patientLookupUrl = '<?= base_url('AbdmHiu/patient_lookup') ?>'; var patientLookupUrl = '<?= base_url('AbdmHiu/patient_lookup') ?>';
@ -183,7 +195,7 @@
function isConsentApproved(statusText) { function isConsentApproved(statusText) {
var s = (statusText || '').toString().trim().toLowerCase(); var s = (statusText || '').toString().trim().toLowerCase();
return ['approved', 'granted', 'active', 'linked', 'completed'].indexOf(s) !== -1; return ['approved', 'granted', 'active', 'linked', 'completed', 'status_checked'].indexOf(s) !== -1;
} }
function isTerminalConsentState(statusText) { function isTerminalConsentState(statusText) {
@ -192,7 +204,7 @@
} }
function isGatewayRequestId(v) { function isGatewayRequestId(v) {
return /^REQ-\d{14}-[A-Za-z0-9]{6,}$/.test((v || '').toString().trim()); return /^REQ-/i.test((v || '').toString().trim());
} }
function isValidConsentArtifactId(v) { function isValidConsentArtifactId(v) {
@ -204,12 +216,15 @@
var hasPatient = !!selectedPatient; var hasPatient = !!selectedPatient;
var hasNum = hasPatient && selectedPatient.has_abha_number === 1; var hasNum = hasPatient && selectedPatient.has_abha_number === 1;
var hasAddr = hasPatient && selectedPatient.has_abha_address === 1; var hasAddr = hasPatient && selectedPatient.has_abha_address === 1;
var hasConsentRequestRef = (getFieldValue(['consent_request_id', 'consent_id']) !== ''); var consentRequestRef = getFieldValue(['consent_request_id', 'consent_id']);
var hasConsentArtifactRef = (getFieldValue(['consent_artifact_id', 'consent_id']) !== ''); var consentArtifactRef = getFieldValue(['consent_artifact_id', 'consent_id']);
var hasConsentRequestRef = (consentRequestRef !== '' && !isGatewayRequestId(consentRequestRef));
var hasConsentArtifactRef = (consentArtifactRef !== '' && !isGatewayRequestId(consentArtifactRef));
var canFetchByState = isConsentApproved(consentStateText) || isTerminalConsentState(consentStateText);
document.getElementById('btnConsentRequest').disabled = !(hasNum && hasAddr); document.getElementById('btnConsentRequest').disabled = !(hasNum && hasAddr);
document.getElementById('btnConsentStatus').disabled = !hasConsentRequestRef; document.getElementById('btnConsentStatus').disabled = !hasConsentRequestRef;
document.getElementById('btnConsentFetch').disabled = !hasConsentArtifactRef; document.getElementById('btnConsentFetch').disabled = !(hasConsentArtifactRef || (hasConsentRequestRef && canFetchByState));
document.getElementById('btnHiRequest').disabled = !(hasConsentArtifactRef && isConsentApproved(consentStateText)); document.getElementById('btnHiRequest').disabled = !(hasConsentArtifactRef && isConsentApproved(consentStateText));
updateReadinessBox(consentStateText); updateReadinessBox(consentStateText);
} }
@ -218,7 +233,6 @@
selectedPatient = p; selectedPatient = p;
setFieldValue(['selected_patient'], (p.patient_name || '') + (p.patient_code ? ' (' + p.patient_code + ')' : '')); setFieldValue(['selected_patient'], (p.patient_name || '') + (p.patient_code ? ' (' + p.patient_code + ')' : ''));
setFieldValue(['abha_address'], p.abha_address || ''); setFieldValue(['abha_address'], p.abha_address || '');
setFieldValue(['f_abha_address'], p.abha_address || '');
setFieldValue(['request_id'], genReq('REQ-CONSENT')); setFieldValue(['request_id'], genReq('REQ-CONSENT'));
setFieldValue(['transaction_id'], genReq('TXN-HIU')); setFieldValue(['transaction_id'], genReq('TXN-HIU'));
@ -382,15 +396,24 @@
return base; return base;
} }
if (op === 'consent_status') { if (op === 'consent_status' || op === 'consent_fetch') {
var requestIdRef = (raw.request_id || raw.requestId || '').toString().trim();
var consentRequestId = (raw.abdm_consent_request_id || raw.consentRequestId || '').toString().trim(); var consentRequestId = (raw.abdm_consent_request_id || raw.consentRequestId || '').toString().trim();
if (!consentRequestId) { var consentIdRef = (raw.abdm_consent_artifact_id || raw.consentId || raw.consent_id || '').toString().trim();
throw new Error('consentRequestId is required for consent status.');
if (!requestIdRef && !consentRequestId && !consentIdRef) {
throw new Error('Provide one of request_id, consentRequestId, or consentId for consent reconciliation.');
} }
if (isGatewayRequestId(consentRequestId)) {
throw new Error('mapping_error: consentRequestId looks like gateway request_id (REQ-...). Use actual ABDM consentRequestId from init response.'); if (requestIdRef) {
base.request_id = requestIdRef;
}
if (consentRequestId && !isGatewayRequestId(consentRequestId)) {
base.consent_request_id = consentRequestId;
}
if (consentIdRef && !isGatewayRequestId(consentIdRef)) {
base.consent_id = consentIdRef;
} }
base.consentRequestId = consentRequestId;
return base; return base;
} }
@ -400,7 +423,7 @@
throw new Error('consentId is required for consent fetch.'); throw new Error('consentId is required for consent fetch.');
} }
if (isGatewayRequestId(consentId)) { if (isGatewayRequestId(consentId)) {
throw new Error('mapping_error: consentId looks like gateway request_id (REQ-...). Use ABDM consent artifact id only.'); throw new Error('mapping_error: consentId looks like gateway/local request_id (REQ-...). Use ABDM consent artifact id only.');
} }
base.consentId = consentId; base.consentId = consentId;
return base; return base;
@ -409,7 +432,17 @@
if (op === 'hi_request') { if (op === 'hi_request') {
var hiRequest = raw.hiRequest; var hiRequest = raw.hiRequest;
if (!hiRequest || typeof hiRequest !== 'object' || Array.isArray(hiRequest)) { if (!hiRequest || typeof hiRequest !== 'object' || Array.isArray(hiRequest)) {
throw new Error('hiRequest object is required for health information request.'); hiRequest = {
consent: {
id: (raw.consentId || raw.consent_id || '').toString().trim()
},
dateRange: {
from: new Date(Date.now() - (30 * 24 * 60 * 60 * 1000)).toISOString(),
to: new Date().toISOString()
},
dataPushUrl: 'https://abdm-bridge.e-atria.in/api/v3/hiu/data/push',
transactionId: (raw.transactionId || raw.transaction_id || '').toString().trim() || genReq('TXN-HIU')
};
} }
if (!hiRequest.consent || typeof hiRequest.consent !== 'object') { if (!hiRequest.consent || typeof hiRequest.consent !== 'object') {
@ -422,7 +455,13 @@
throw new Error('hiRequest.consent.id is required for health information request.'); throw new Error('hiRequest.consent.id is required for health information request.');
} }
if (!hiRequest.transactionId) {
hiRequest.transactionId = (raw.transactionId || raw.transaction_id || '').toString().trim() || genReq('TXN-HIU');
}
setFieldValue(['transaction_id'], hiRequest.transactionId);
base.hiRequest = hiRequest; base.hiRequest = hiRequest;
base.transactionId = hiRequest.transactionId;
return base; return base;
} }
@ -484,20 +523,18 @@
if (op === 'consent_request') { if (op === 'consent_request') {
startConsentPolling(); startConsentPolling();
} }
if (op === 'consent_status' && isTerminalConsentState(state)) { if ((op === 'consent_status' || op === 'consent_fetch') && isConsentApproved(state) && isValidConsentArtifactId(consentArtifactRef)) {
run('hi_request', { silent: true });
}
if ((op === 'consent_status' || op === 'consent_fetch') && isTerminalConsentState(state)) {
stopConsentPolling(); stopConsentPolling();
} }
} else if (op === 'consent_status') { } else if (op === 'consent_status' || op === 'consent_fetch') {
var errState = ((res.data || {}).workflow_state || res.workflow_state || '').toString(); var errState = ((res.data || {}).workflow_state || res.workflow_state || '').toString();
var errText = (res.error || ((res.data || {}).error_text) || '').toString().toLowerCase(); var errText = (res.error || ((res.data || {}).error_text) || '').toString().toLowerCase();
if (isTerminalConsentState(errState) || errText.indexOf('mapping_error') !== -1) { if (isTerminalConsentState(errState) || errText.indexOf('mapping_error') !== -1) {
stopConsentPolling(); stopConsentPolling();
} }
} else if (op === 'consent_fetch') {
var fetchErr = (res.error || ((res.data || {}).error_text) || '').toString().toLowerCase();
if (fetchErr.indexOf('mapping_error') !== -1) {
setFieldValue(['consent_artifact_id', 'consent_id'], '');
}
} }
fetchTimeline(); fetchTimeline();
@ -528,6 +565,70 @@
consentPollAttempts = 0; consentPollAttempts = 0;
} }
function renderPollErrors() {
var list = document.getElementById('poll_error_list');
if (!list) {
return;
}
list.innerHTML = '';
if (!recentPollErrors.length) {
var li = document.createElement('li');
li.textContent = 'No recent polling errors in selected lookback window.';
list.appendChild(li);
return;
}
recentPollErrors.forEach(function (row) {
var when = (row.updated_at || '-').toString();
var op = (row.operation || '-').toString();
var req = (row.request_id || '-').toString();
var err = (row.last_error || 'Unknown error').toString();
var li = document.createElement('li');
li.textContent = when + ' | ' + op + ' | ' + req + ' | ' + err;
list.appendChild(li);
});
}
function refreshPollSummary() {
fetch(pollSummaryUrl + '?lookback=180', {
headers: { 'X-Requested-With': 'XMLHttpRequest' }
})
.then(function (r) { return r.json(); })
.then(function (res) {
var badge = document.getElementById('poll_summary_badge');
if (!badge) {
return;
}
if ((res.ok || 0) !== 1) {
badge.className = 'badge bg-danger';
badge.textContent = 'Poll: unavailable';
return;
}
var consentUpdates = parseInt(res.consent_updates || 0, 10);
var dataUpdates = parseInt(res.data_updates || 0, 10);
var failed = parseInt(res.failed || 0, 10);
var pending = parseInt(res.pending || 0, 10);
var lastAt = (res.last_polled_at || '-').toString();
recentPollErrors = Array.isArray(res.recent_errors) ? res.recent_errors : [];
renderPollErrors();
badge.className = failed > 0 ? 'badge bg-warning text-dark' : 'badge bg-success';
badge.textContent = 'Poll c:' + consentUpdates + ' d:' + dataUpdates + ' f:' + failed + ' p:' + pending + ' | ' + lastAt;
})
.catch(function () {
var badge = document.getElementById('poll_summary_badge');
if (!badge) {
return;
}
badge.className = 'badge bg-danger';
badge.textContent = 'Poll: unavailable';
recentPollErrors = [];
renderPollErrors();
});
}
function fetchTimeline() { function fetchTimeline() {
var qs = new URLSearchParams({ var qs = new URLSearchParams({
hfr_id: getFieldValue(['f_hfr_id']), hfr_id: getFieldValue(['f_hfr_id']),
@ -561,6 +662,8 @@
'<td>' + (row.last_error || '') + '</td>'; '<td>' + (row.last_error || '') + '</td>';
tbody.appendChild(tr); tbody.appendChild(tr);
}); });
refreshPollSummary();
}); });
} }
@ -606,8 +709,25 @@
if (btnRefreshTimeline) { if (btnRefreshTimeline) {
btnRefreshTimeline.addEventListener('click', fetchTimeline); btnRefreshTimeline.addEventListener('click', fetchTimeline);
} }
var btnPollErrors = document.getElementById('btnPollErrors');
if (btnPollErrors) {
btnPollErrors.addEventListener('click', function () {
var panel = document.getElementById('poll_error_panel');
if (!panel) {
return;
}
panel.classList.toggle('d-none');
if (panel.classList.contains('d-none')) {
btnPollErrors.textContent = 'Show Poll Errors';
} else {
renderPollErrors();
btnPollErrors.textContent = 'Hide Poll Errors';
}
});
}
syncButtons(); syncButtons();
refreshPollSummary();
fetchTimeline(); fetchTimeline();
})(); })();
</script> </script>

View File

@ -500,10 +500,10 @@
<div class="tab-pane fade" id="abha-register" role="tabpanel" aria-labelledby="abhareg-tab"> <div class="tab-pane fade" id="abha-register" role="tabpanel" aria-labelledby="abhareg-tab">
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<h6 class="fw-bold mb-3"><i class="bi bi-person-check-fill me-2 text-primary"></i>ABHA-Based Patient Registration</h6> <h6 class="fw-bold mb-3"><i class="bi bi-person-check-fill me-2 text-primary" style="margin-left: 10px;margin-right: 10px;"></i>ABHA-Based Patient Registration</h6>
<!-- Method selector cards --> <!-- Method selector cards -->
<div id="abhareg_methods" class="row g-2 mb-3"> <div id="abhareg_methods" class="row g-2 mb-3" style="margin-left: 10px;margin-right: 10px;">
<div class="col-6 col-md-3"> <div class="col-6 col-md-3">
<div class="card border abhareg-method-card text-center py-3" data-method="number" role="button" tabindex="0" onclick="abhaRegSelectMethod('number')" style="cursor:pointer"> <div class="card border abhareg-method-card text-center py-3" data-method="number" role="button" tabindex="0" onclick="abhaRegSelectMethod('number')" style="cursor:pointer">
<div class="fs-2 mb-1">🆔</div> <div class="fs-2 mb-1">🆔</div>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB