Update HIU workflow, patient view, and related assets
This commit is contained in:
parent
f243aeeb04
commit
6407db9dbc
37
app/Commands/AbdmHiuPoll.php
Normal file
37
app/Commands/AbdmHiuPoll.php
Normal 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));
|
||||
}
|
||||
}
|
||||
|
|
@ -630,14 +630,19 @@ $routes->post('AbdmGateway/record_linked_callback', 'AbdmGateway::recordLinkedCa
|
|||
$routes->get('AbdmHiu', 'AbdmHiu::index', ['filter' => $abdmPermFilter]);
|
||||
$routes->get('AbdmHiu/patient_lookup', 'AbdmHiu::patientLookup', ['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_status', 'AbdmHiu::consentRequestStatus', ['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/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/status', 'AbdmHiu::consentRequestStatus', ['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/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
|
||||
$routes->get('api/v1/abdm/gateway/health', 'AbdmGateway::m2GatewayHealth');
|
||||
$routes->post('api/v1/abdm/gateway/discovery/care-contexts', 'AbdmGateway::m2DiscoveryCareContexts');
|
||||
|
|
|
|||
|
|
@ -43,6 +43,16 @@ class AbdmHiu extends BaseController
|
|||
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()
|
||||
{
|
||||
$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()
|
||||
{
|
||||
if (! $this->db->tableExists('patient_master')) {
|
||||
|
|
|
|||
|
|
@ -31,7 +31,17 @@ class M3HiuGatewayClient
|
|||
|
||||
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
|
||||
|
|
@ -95,6 +105,10 @@ class M3HiuGatewayClient
|
|||
: ('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;
|
||||
if ($curlErr !== '' || $httpCode >= 500 || $httpCode === 0 || str_contains(strtolower($errorText), 'timed out')) {
|
||||
$retryable = 1;
|
||||
|
|
@ -106,6 +120,11 @@ class M3HiuGatewayClient
|
|||
$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'] ?? ''));
|
||||
if ($gatewayRequestId === '') {
|
||||
$gatewayRequestId = trim((string) ($payload['requestId'] ?? $payload['request_id'] ?? ''));
|
||||
|
|
@ -117,8 +136,8 @@ class M3HiuGatewayClient
|
|||
'consent_request_id',
|
||||
'abdm_consent_request_id',
|
||||
]);
|
||||
if ($abdmConsentRequestId === '') {
|
||||
$abdmConsentRequestId = trim((string) ($payload['consentRequestId'] ?? ''));
|
||||
if ($abdmConsentRequestId !== '' && $this->isGatewayRefId($abdmConsentRequestId)) {
|
||||
$abdmConsentRequestId = '';
|
||||
}
|
||||
if ($abdmConsentRequestId !== '') {
|
||||
$response['abdm_consent_request_id'] = $abdmConsentRequestId;
|
||||
|
|
@ -129,8 +148,8 @@ class M3HiuGatewayClient
|
|||
'consentId',
|
||||
'consent_id',
|
||||
]);
|
||||
if ($abdmConsentId === '') {
|
||||
$abdmConsentId = trim((string) ($payload['consentId'] ?? $payload['consent_id'] ?? ''));
|
||||
if ($abdmConsentId !== '' && $this->isGatewayRefId($abdmConsentId)) {
|
||||
$abdmConsentId = '';
|
||||
}
|
||||
if ($abdmConsentId !== '') {
|
||||
$response['consent_id'] = $abdmConsentId;
|
||||
|
|
@ -149,6 +168,137 @@ class M3HiuGatewayClient
|
|||
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
|
||||
{
|
||||
if (! $this->db->tableExists('hospital_setting')) {
|
||||
|
|
@ -207,7 +357,8 @@ class M3HiuGatewayClient
|
|||
int $httpCode,
|
||||
int $ok,
|
||||
string $errorText,
|
||||
string $hospitalId
|
||||
string $hospitalId,
|
||||
string $httpMethod = 'POST'
|
||||
): void {
|
||||
if (! $this->db->tableExists('abdm_api_logs')) {
|
||||
return;
|
||||
|
|
@ -229,7 +380,7 @@ class M3HiuGatewayClient
|
|||
'channel' => 'eatria_bridge',
|
||||
'event_type' => 'abdm.m3.hiu.' . $eventKey,
|
||||
'endpoint' => $this->baseUrl . $path,
|
||||
'http_method' => 'POST',
|
||||
'http_method' => strtoupper(trim($httpMethod)) !== '' ? strtoupper(trim($httpMethod)) : 'POST',
|
||||
'entity_type' => 'abdm_hiu',
|
||||
'entity_id' => $entityId,
|
||||
'request_json' => (string) json_encode($request, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||||
|
|
@ -268,4 +419,25 @@ class M3HiuGatewayClient
|
|||
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ class M3HiuWorkflowService
|
|||
}
|
||||
$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);
|
||||
if ($duplicate !== null) {
|
||||
$dupResponse = json_decode((string) ($duplicate['response_json'] ?? ''), true);
|
||||
|
|
@ -70,8 +70,19 @@ class M3HiuWorkflowService
|
|||
$status = $ok ? 'success' : 'failed';
|
||||
$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) {
|
||||
$this->upsertWorkflow($operation, $dispatchPayload, $result, $state, $status, $httpCode, $retryable);
|
||||
$this->upsertWorkflow($operation, $persistPayload, $result, $state, $status, $httpCode, $retryable);
|
||||
$result['workflow_persisted'] = 1;
|
||||
} else {
|
||||
$result['workflow_persisted'] = 0;
|
||||
|
|
@ -129,6 +140,105 @@ class M3HiuWorkflowService
|
|||
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
|
||||
{
|
||||
if (! $this->db->tableExists('abdm_hiu_workflows')) {
|
||||
|
|
@ -165,6 +275,87 @@ class M3HiuWorkflowService
|
|||
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
|
||||
{
|
||||
return match ($operation) {
|
||||
|
|
@ -172,6 +363,8 @@ class M3HiuWorkflowService
|
|||
'consent_status' => $this->client->consentRequestStatus($payload),
|
||||
'consent_fetch' => $this->client->consentRequestFetch($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],
|
||||
};
|
||||
}
|
||||
|
|
@ -335,18 +528,21 @@ class M3HiuWorkflowService
|
|||
|
||||
if ($operation === 'consent_status') {
|
||||
$consentRequestId = trim((string) ($clean['abdm_consent_request_id'] ?? $clean['consentRequestId'] ?? $clean['consent_id'] ?? ''));
|
||||
if ($consentRequestId === '') {
|
||||
$consentRequestId = $this->resolveAbdmConsentRequestIdFromWorkflow($clean);
|
||||
}
|
||||
if ($consentRequestId === '') {
|
||||
return [
|
||||
'ok' => 0,
|
||||
'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)) {
|
||||
return [
|
||||
'ok' => 0,
|
||||
'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 [
|
||||
'ok' => 0,
|
||||
'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') {
|
||||
$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 [
|
||||
'ok' => 0,
|
||||
'http_code' => 422,
|
||||
|
|
@ -403,9 +602,85 @@ class M3HiuWorkflowService
|
|||
}
|
||||
$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 [
|
||||
'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;
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
if (! $this->db->tableExists('hospital_setting')) {
|
||||
|
|
@ -530,6 +831,26 @@ class M3HiuWorkflowService
|
|||
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
|
||||
{
|
||||
if (! $this->db->tableExists('hospital_setting')) {
|
||||
|
|
@ -617,6 +938,8 @@ class M3HiuWorkflowService
|
|||
'consent_status' => 'STATUS_CHECKED',
|
||||
'consent_fetch' => 'CONSENT_FETCHED',
|
||||
'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',
|
||||
};
|
||||
}
|
||||
|
|
@ -650,6 +973,111 @@ class M3HiuWorkflowService
|
|||
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(
|
||||
string $operation,
|
||||
array $payload,
|
||||
|
|
@ -666,14 +1094,14 @@ class M3HiuWorkflowService
|
|||
$hmsRequestId = trim((string) ($result['hms_request_id'] ?? ''));
|
||||
}
|
||||
$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'] ?? ''));
|
||||
$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') {
|
||||
$consentId = $abdmConsentRequestId;
|
||||
}
|
||||
$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'] ?? ''));
|
||||
$errorText = trim((string) ($result['error_text'] ?? $result['message'] ?? ''));
|
||||
|
||||
|
|
@ -702,7 +1130,7 @@ class M3HiuWorkflowService
|
|||
|
||||
$row = [
|
||||
'hospital_id' => $hospitalId !== '' ? $hospitalId : null,
|
||||
'hfr_id' => $hfrId !== '' ? $hfrId : 'UNKNOWN',
|
||||
'hfr_id' => $hfrId !== '' ? $hfrId : null,
|
||||
'abha_address' => $abhaAddress !== '' ? $abhaAddress : null,
|
||||
'consent_id' => $consentId !== '' ? $consentId : null,
|
||||
'request_id' => $hmsRequestId !== '' ? $hmsRequestId : null,
|
||||
|
|
@ -754,6 +1182,84 @@ class M3HiuWorkflowService
|
|||
|
||||
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 '';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,10 +3,19 @@
|
|||
<div class="col-12">
|
||||
<div class="card shadow-sm">
|
||||
<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>
|
||||
</div>
|
||||
<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">
|
||||
Search patient first. Consent Request is enabled only when ABHA Number and ABHA Address are both present.
|
||||
</div>
|
||||
|
|
@ -118,13 +127,16 @@
|
|||
var consentPollTimer = null;
|
||||
var consentPollAttempts = 0;
|
||||
var consentPollMaxAttempts = 12;
|
||||
var recentPollErrors = [];
|
||||
|
||||
var opRoute = {
|
||||
consent_request: '<?= base_url('AbdmHiu/consent_request') ?>',
|
||||
consent_status: '<?= base_url('AbdmHiu/consent_request_status') ?>',
|
||||
consent_fetch: '<?= base_url('AbdmHiu/consent_request_fetch') ?>',
|
||||
hi_request: '<?= base_url('AbdmHiu/health_information_request') ?>'
|
||||
consent_status: '<?= base_url('AbdmHiu/consent_reconcile') ?>',
|
||||
consent_fetch: '<?= base_url('AbdmHiu/consent_reconcile') ?>',
|
||||
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') ?>';
|
||||
|
||||
|
|
@ -183,7 +195,7 @@
|
|||
|
||||
function isConsentApproved(statusText) {
|
||||
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) {
|
||||
|
|
@ -192,7 +204,7 @@
|
|||
}
|
||||
|
||||
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) {
|
||||
|
|
@ -204,12 +216,15 @@
|
|||
var hasPatient = !!selectedPatient;
|
||||
var hasNum = hasPatient && selectedPatient.has_abha_number === 1;
|
||||
var hasAddr = hasPatient && selectedPatient.has_abha_address === 1;
|
||||
var hasConsentRequestRef = (getFieldValue(['consent_request_id', 'consent_id']) !== '');
|
||||
var hasConsentArtifactRef = (getFieldValue(['consent_artifact_id', 'consent_id']) !== '');
|
||||
var consentRequestRef = getFieldValue(['consent_request_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('btnConsentStatus').disabled = !hasConsentRequestRef;
|
||||
document.getElementById('btnConsentFetch').disabled = !hasConsentArtifactRef;
|
||||
document.getElementById('btnConsentFetch').disabled = !(hasConsentArtifactRef || (hasConsentRequestRef && canFetchByState));
|
||||
document.getElementById('btnHiRequest').disabled = !(hasConsentArtifactRef && isConsentApproved(consentStateText));
|
||||
updateReadinessBox(consentStateText);
|
||||
}
|
||||
|
|
@ -218,7 +233,6 @@
|
|||
selectedPatient = p;
|
||||
setFieldValue(['selected_patient'], (p.patient_name || '') + (p.patient_code ? ' (' + p.patient_code + ')' : ''));
|
||||
setFieldValue(['abha_address'], p.abha_address || '');
|
||||
setFieldValue(['f_abha_address'], p.abha_address || '');
|
||||
setFieldValue(['request_id'], genReq('REQ-CONSENT'));
|
||||
setFieldValue(['transaction_id'], genReq('TXN-HIU'));
|
||||
|
||||
|
|
@ -382,15 +396,24 @@
|
|||
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();
|
||||
if (!consentRequestId) {
|
||||
throw new Error('consentRequestId is required for consent status.');
|
||||
var consentIdRef = (raw.abdm_consent_artifact_id || raw.consentId || raw.consent_id || '').toString().trim();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
@ -400,7 +423,7 @@
|
|||
throw new Error('consentId is required for consent fetch.');
|
||||
}
|
||||
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;
|
||||
return base;
|
||||
|
|
@ -409,7 +432,17 @@
|
|||
if (op === 'hi_request') {
|
||||
var hiRequest = raw.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') {
|
||||
|
|
@ -422,7 +455,13 @@
|
|||
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.transactionId = hiRequest.transactionId;
|
||||
return base;
|
||||
}
|
||||
|
||||
|
|
@ -484,20 +523,18 @@
|
|||
if (op === 'consent_request') {
|
||||
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();
|
||||
}
|
||||
} else if (op === 'consent_status') {
|
||||
} else if (op === 'consent_status' || op === 'consent_fetch') {
|
||||
var errState = ((res.data || {}).workflow_state || res.workflow_state || '').toString();
|
||||
var errText = (res.error || ((res.data || {}).error_text) || '').toString().toLowerCase();
|
||||
if (isTerminalConsentState(errState) || errText.indexOf('mapping_error') !== -1) {
|
||||
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();
|
||||
|
|
@ -528,6 +565,70 @@
|
|||
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() {
|
||||
var qs = new URLSearchParams({
|
||||
hfr_id: getFieldValue(['f_hfr_id']),
|
||||
|
|
@ -561,6 +662,8 @@
|
|||
'<td>' + (row.last_error || '') + '</td>';
|
||||
tbody.appendChild(tr);
|
||||
});
|
||||
|
||||
refreshPollSummary();
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -606,8 +709,25 @@
|
|||
if (btnRefreshTimeline) {
|
||||
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();
|
||||
refreshPollSummary();
|
||||
fetchTimeline();
|
||||
})();
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -500,10 +500,10 @@
|
|||
<div class="tab-pane fade" id="abha-register" role="tabpanel" aria-labelledby="abhareg-tab">
|
||||
<div class="row">
|
||||
<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 -->
|
||||
<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="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>
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 112 KiB |
BIN
public/assets/images/hospital_logo_20260707_010358_a5f13c98.png
Normal file
BIN
public/assets/images/hospital_logo_20260707_010358_a5f13c98.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 73 KiB |
Loading…
Reference in New Issue
Block a user