db = \Config\Database::connect(); $this->client = new M3HiuGatewayClient(); } public function runOperation(string $operation, array $payload): array { $hasWorkflowTable = $this->db->tableExists('abdm_hiu_workflows'); $normalized = $this->normalizePayload($payload); $prepared = $this->preparePayloadForOperation($operation, $normalized); if (($prepared['ok'] ?? 0) !== 1) { $failedResult = [ 'ok' => 0, 'http_code' => (int) ($prepared['http_code'] ?? 422), 'error_text' => (string) ($prepared['error_text'] ?? 'Validation failed'), 'retryable' => 0, 'request_id' => (string) ($normalized['request_id'] ?? ''), 'transaction_id' => (string) ($normalized['transaction_id'] ?? ''), 'consent_id' => (string) ($normalized['consent_id'] ?? ''), 'abha_address' => (string) ($normalized['abha_address'] ?? ''), ]; if ($hasWorkflowTable) { $this->upsertWorkflow( $operation, $normalized, $failedResult, 'FAILED', 'failed', (int) $failedResult['http_code'], false ); $failedResult['workflow_persisted'] = 1; } return $failedResult; } $dispatchPayload = (array) ($prepared['payload'] ?? []); 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); if (is_array($dupResponse) && ! empty($dupResponse)) { $dupResponse['duplicate'] = 1; $dupResponse['workflow_state'] = (string) ($duplicate['workflow_state'] ?? ''); return $dupResponse; } } } $result = $this->dispatch($operation, $dispatchPayload); $httpCode = (int) ($result['http_code'] ?? 0); $ok = (int) ($result['ok'] ?? 0) === 1; $state = $this->resolveState($operation, $result); $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, $persistPayload, $result, $state, $status, $httpCode, $retryable); $result['workflow_persisted'] = 1; } else { $result['workflow_persisted'] = 0; $result['workflow_warning'] = 'abdm_hiu_workflows table not found; operation executed without timeline persistence'; } return $result; } public function retryDue(int $limit = 20): array { if (! $this->db->tableExists('abdm_hiu_workflows')) { return ['processed' => 0, 'success' => 0, 'failed' => 0, 'skipped' => 0]; } $rows = $this->db->table('abdm_hiu_workflows') ->select('*') ->where('status', 'failed') ->where('is_retryable', 1) ->where('retry_count <', 3) ->groupStart() ->where('next_retry_at IS NULL', null, false) ->orWhere('next_retry_at <=', Time::now('Asia/Kolkata')->toDateTimeString()) ->groupEnd() ->orderBy('id', 'ASC') ->get($limit) ->getResultArray(); $summary = ['processed' => 0, 'success' => 0, 'failed' => 0, 'skipped' => 0]; foreach ($rows as $row) { $summary['processed']++; $operation = trim((string) ($row['operation'] ?? '')); $payload = json_decode((string) ($row['request_json'] ?? ''), true); if (! is_array($payload) || $operation === '') { $summary['skipped']++; continue; } $result = $this->dispatch($operation, $payload); $ok = (int) ($result['ok'] ?? 0) === 1; $state = $this->resolveState($operation, $result); $status = $ok ? 'success' : 'failed'; $retryable = (int) ($result['retryable'] ?? 0) === 1; $this->upsertWorkflow($operation, $payload, $result, $state, $status, (int) ($result['http_code'] ?? 0), $retryable, (int) ($row['id'] ?? 0)); if ($ok) { $summary['success']++; } else { $summary['failed']++; } } 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')) { return []; } $builder = $this->db->table('abdm_hiu_workflows')->select('*')->orderBy('id', 'DESC'); $hfrId = trim((string) ($filters['hfr_id'] ?? '')); if ($hfrId !== '') { $builder->where('hfr_id', $hfrId); } $consentId = trim((string) ($filters['consent_id'] ?? '')); if ($consentId !== '') { $builder->where('consent_id', $consentId); } $transactionId = trim((string) ($filters['transaction_id'] ?? '')); if ($transactionId !== '') { $builder->where('transaction_id', $transactionId); } $abhaAddress = trim((string) ($filters['abha_address'] ?? '')); if ($abhaAddress !== '') { $builder->where('abha_address', $abhaAddress); } $from = trim((string) ($filters['date_from'] ?? '')); if ($from !== '') { $builder->where('created_at >=', $from . ' 00:00:00'); } $to = trim((string) ($filters['date_to'] ?? '')); if ($to !== '') { $builder->where('created_at <=', $to . ' 23:59:59'); } 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) { 'consent_request' => $this->client->consentRequest($payload), '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], }; } private function normalizePayload(array $payload): array { $normalized = $payload; // Accept both legacy snake_case and guide-defined camelCase keys. if (! isset($normalized['request_id']) && isset($normalized['requestId'])) { $normalized['request_id'] = (string) $normalized['requestId']; } if (! isset($normalized['transaction_id']) && isset($normalized['transactionId'])) { $normalized['transaction_id'] = (string) $normalized['transactionId']; } if (! isset($normalized['consent_id']) && isset($normalized['consentId'])) { $normalized['consent_id'] = (string) $normalized['consentId']; } if (! isset($normalized['consent_id']) && isset($normalized['consentRequestId'])) { $normalized['consent_id'] = (string) $normalized['consentRequestId']; } if (! isset($normalized['abdm_consent_request_id']) && isset($normalized['consentRequestId'])) { $normalized['abdm_consent_request_id'] = (string) $normalized['consentRequestId']; } if (! isset($normalized['abdm_consent_artifact_id']) && isset($normalized['consentId'])) { $normalized['abdm_consent_artifact_id'] = (string) $normalized['consentId']; } if (isset($normalized['abha_id']) && ! isset($normalized['abha_address'])) { $normalized['abha_address'] = (string) $normalized['abha_id']; } foreach (['request_id', 'transaction_id', 'consent_id', 'abha_address', 'hfr_id'] as $k) { if (! isset($normalized[$k])) { $normalized[$k] = ''; } $normalized[$k] = trim((string) $normalized[$k]); } $normalized['abha_address'] = $this->canonicalizeAbhaAddress( (string) ($normalized['abha_address'] ?? ''), (string) ($normalized['abha_id'] ?? '') ); if ($normalized['request_id'] === '') { $normalized['request_id'] = 'REQ-HIU-' . date('YmdHis') . '-' . strtoupper(bin2hex(random_bytes(3))); } if (! isset($normalized['requestId']) || trim((string) $normalized['requestId']) === '') { $normalized['requestId'] = $normalized['request_id']; } if (! isset($normalized['timestamp']) || trim((string) $normalized['timestamp']) === '') { $normalized['timestamp'] = gmdate('Y-m-d\TH:i:s.000\Z'); } if ($normalized['transaction_id'] !== '' && (! isset($normalized['transactionId']) || trim((string) $normalized['transactionId']) === '')) { $normalized['transactionId'] = $normalized['transaction_id']; } if ($normalized['consent_id'] !== '' && (! isset($normalized['consentId']) || trim((string) $normalized['consentId']) === '')) { $normalized['consentId'] = $normalized['consent_id']; } return $normalized; } private function preparePayloadForOperation(string $operation, array $normalized): array { $clean = $this->stripInternalNoise($normalized); $requestId = trim((string) ($clean['requestId'] ?? $clean['request_id'] ?? '')); if ($requestId === '') { $requestId = 'REQ-HIU-' . date('YmdHis') . '-' . strtoupper(bin2hex(random_bytes(3))); } $timestamp = trim((string) ($clean['timestamp'] ?? '')); if ($timestamp === '') { $timestamp = gmdate('Y-m-d\TH:i:s.000\Z'); } $base = [ 'requestId' => $requestId, 'timestamp' => $timestamp, ]; $hfr = trim((string) ($clean['hfr_id'] ?? '')); if ($hfr !== '') { $base['hfr_id'] = $hfr; } if ($operation === 'consent_request') { $consent = $clean['consent'] ?? null; if (! is_array($consent) || empty($consent)) { $consent = $this->buildDefaultConsent($clean); } if (! is_array($consent) || empty($consent)) { return [ 'ok' => 0, 'http_code' => 422, 'error_text' => 'Consent object is required for HIU consent request.', ]; } $patientId = trim((string) ($consent['patient']['id'] ?? '')); if ($patientId === '') { $patientId = trim((string) ($clean['abha_address'] ?? '')); } if (! $this->isValidAbhaAddress($patientId)) { return [ 'ok' => 0, 'http_code' => 422, 'error_text' => 'Patient ABHA address must be in address format like 91510165305101@sbx.', ]; } if (! isset($consent['patient']) || ! is_array($consent['patient'])) { $consent['patient'] = []; } $consent['patient']['id'] = $patientId; $requesterName = trim((string) ($consent['requester']['name'] ?? '')); if ($requesterName === '' || strcasecmp($requesterName, 'Hospital HMS') === 0) { $resolvedRequesterName = $this->resolveRequesterName(); if ($resolvedRequesterName !== '') { if (! isset($consent['requester']) || ! is_array($consent['requester'])) { $consent['requester'] = []; } $consent['requester']['name'] = $resolvedRequesterName; } } $hiuId = trim((string) ($consent['hiu']['id'] ?? '')); if ($hiuId === '') { $hiuId = $this->resolveHiuServiceId(); if ($hiuId === '') { $hiuId = trim((string) ($clean['hfr_id'] ?? '')); } if ($hiuId !== '') { if (! isset($consent['hiu']) || ! is_array($consent['hiu'])) { $consent['hiu'] = []; } $consent['hiu']['id'] = $hiuId; } } $requesterIdentifierValue = trim((string) ($consent['requester']['identifier']['value'] ?? '')); if ($requesterIdentifierValue === '') { $requesterIdentifierValue = $hiuId !== '' ? $hiuId : trim((string) ($clean['hfr_id'] ?? '')); } if ($requesterIdentifierValue !== '') { if (! isset($consent['requester']) || ! is_array($consent['requester'])) { $consent['requester'] = []; } if (! isset($consent['requester']['identifier']) || ! is_array($consent['requester']['identifier'])) { $consent['requester']['identifier'] = []; } $consent['requester']['identifier']['system'] = 'https://abdm.gov.in/facility'; $consent['requester']['identifier']['type'] = 'HIU'; $consent['requester']['identifier']['value'] = $requesterIdentifierValue; } return [ 'ok' => 1, 'payload' => $base + ['consent' => $consent], ]; } 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. 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 a local temporary request id; use consentRequestId returned by consent request/status.', ]; } return [ 'ok' => 1, 'payload' => $base + ['consentRequestId' => $consentRequestId], ]; } if ($operation === 'consent_fetch') { $consentId = trim((string) ($clean['abdm_consent_artifact_id'] ?? $clean['consentId'] ?? $clean['consent_id'] ?? '')); if ($consentId === '') { return [ 'ok' => 0, 'http_code' => 422, 'error_text' => 'consentId is required for consent fetch.', ]; } if ($this->isGatewayRequestIdPattern($consentId)) { return [ 'ok' => 0, 'http_code' => 422, 'error_text' => 'mapping_error: consentId appears to be a local temporary request id; use ABDM consent artifact id only.', ]; } return [ 'ok' => 1, 'payload' => $base + ['consentId' => $consentId], ]; } if ($operation === 'hi_request') { $hiRequest = $clean['hiRequest'] ?? null; if (! is_array($hiRequest) || empty($hiRequest)) { $hiRequest = $this->buildDefaultHiRequest($clean); } if (! is_array($hiRequest) || empty($hiRequest)) { return [ 'ok' => 0, 'http_code' => 422, 'error_text' => 'hiRequest object is required for health information request.', ]; } $consentId = trim((string) ($hiRequest['consent']['id'] ?? $clean['consentId'] ?? $clean['consent_id'] ?? '')); if ($consentId === '') { return [ 'ok' => 0, 'http_code' => 422, 'error_text' => 'hiRequest.consent.id is required for health information request.', ]; } if (! isset($hiRequest['consent']) || ! is_array($hiRequest['consent'])) { $hiRequest['consent'] = []; } $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 + ['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, ]; } return [ 'ok' => 0, 'http_code' => 400, 'error_text' => 'Unsupported operation: ' . $operation, ]; } private function stripInternalNoise(array $payload): array { unset( $payload['csrf_test_name'], $payload['patient_id'], $payload['abha_id'], $payload['abhaId'] ); if (isset($payload['requestId'])) { unset($payload['request_id']); } if (isset($payload['transactionId'])) { unset($payload['transaction_id']); } return $payload; } private function buildDefaultConsent(array $payload): array { $patientId = trim((string) ($payload['abha_address'] ?? '')); if (! $this->isValidAbhaAddress($patientId)) { return []; } $requesterName = $this->resolveRequesterName(); if ($requesterName === '') { $requesterName = 'Hospital HMS'; } $hiuId = $this->resolveHiuServiceId(); if ($hiuId === '') { $hiuId = trim((string) ($payload['hfr_id'] ?? '')); } $consent = [ 'purpose' => [ 'code' => 'CAREMGT', 'text' => 'Care Management', 'refUri' => 'https://abdm.gov.in', ], 'patient' => [ 'id' => $patientId, ], 'requester' => [ 'name' => $requesterName, ], 'hiTypes' => ['OPConsultation', 'DiagnosticReport'], 'permission' => [ 'accessMode' => 'VIEW', 'dateRange' => [ 'from' => gmdate('Y-m-d\T00:00:00.000\Z', strtotime('-30 days')), 'to' => gmdate('Y-m-d\T00:00:00.000\Z'), ], 'dataEraseAt' => gmdate('Y-m-d\T00:00:00.000\Z', strtotime('+365 days')), 'frequency' => [ 'unit' => 'HOUR', 'value' => 1, 'repeats' => 0, ], ], ]; if ($hiuId !== '') { $consent['hiu'] = ['id' => $hiuId]; $consent['requester']['identifier'] = [ 'system' => 'https://abdm.gov.in/facility', 'type' => 'HIU', 'value' => $hiuId, ]; } else { $fallbackId = trim((string) ($payload['hfr_id'] ?? '')); if ($fallbackId !== '') { $consent['requester']['identifier'] = [ 'system' => 'https://abdm.gov.in/facility', 'type' => 'HIU', 'value' => $fallbackId, ]; } } 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')) { return ''; } $row = $this->db->table('hospital_setting') ->select('s_value') ->where('s_name', $key) ->orderBy('id', 'DESC') ->get(1) ->getRowArray(); return trim((string) ($row['s_value'] ?? '')); } private function resolveRequesterName(): string { foreach (['ABDM_HMS_NAME', 'H_Name', 'HOSPITAL_NAME'] as $key) { $value = $this->readSettingValue($key); if ($value !== '') { return $value; } } $envName = trim((string) (getenv('HOSPITAL_NAME') ?: getenv('app.name') ?: '')); 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')) { $envHiu = trim((string) (getenv('ABDM_HIU_ID') ?: '')); if ($envHiu !== '') { return $envHiu; } return trim((string) (getenv('ABDM_HFR_ID') ?: '')); } $row = $this->db->table('hospital_setting') ->select('s_value') ->whereIn('s_name', ['ABDM_HIU_ID', 'EATRIA_HIU_ID']) ->orderBy('id', 'DESC') ->get(1) ->getRowArray(); $value = trim((string) ($row['s_value'] ?? '')); if ($value !== '') { return $value; } $hfr = $this->readSettingValue('ABDM_HFR_ID'); if ($hfr !== '') { return $hfr; } $envHiu = trim((string) (getenv('ABDM_HIU_ID') ?: '')); if ($envHiu !== '') { return $envHiu; } return trim((string) (getenv('ABDM_HFR_ID') ?: '')); } private function canonicalizeAbhaAddress(string $abhaAddress, string $abhaId): string { $candidate = trim($abhaAddress); if ($candidate !== '' && $this->isValidAbhaAddress($candidate)) { return $candidate; } if ($candidate !== '') { $digits = preg_replace('/\D+/', '', $candidate) ?? ''; if (preg_match('/^\d{14}$/', $digits) === 1) { $domain = trim((string) (getenv('ABDM_ABHA_ADDRESS_DEFAULT_DOMAIN') ?: 'sbx')); return $digits . '@' . $domain; } } $abhaDigits = preg_replace('/\D+/', '', trim($abhaId)) ?? ''; if (preg_match('/^\d{14}$/', $abhaDigits) === 1) { $domain = trim((string) (getenv('ABDM_ABHA_ADDRESS_DEFAULT_DOMAIN') ?: 'sbx')); return $abhaDigits . '@' . $domain; } return ''; } private function isValidAbhaAddress(string $value): bool { return preg_match('/^[A-Za-z0-9._-]+@[A-Za-z0-9.-]+$/', trim($value)) === 1; } private function resolveState(string $operation, array $result): string { $ok = (int) ($result['ok'] ?? 0) === 1; if (! $ok) { if (str_contains(strtolower((string) ($result['error_text'] ?? '')), 'mapping_error')) { return 'MAPPING_ERROR'; } return 'FAILED'; } $statusStr = strtolower(trim((string) ($result['status'] ?? $result['consent_status'] ?? ''))); if (in_array($statusStr, ['revoked', 'denied'], true)) { return 'REVOKED'; } if (in_array($statusStr, ['expired'], true)) { return 'EXPIRED'; } return match ($operation) { 'consent_request' => 'REQUESTED', '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', }; } private function findDuplicate(string $operation, array $payload): ?array { $requestId = trim((string) ($payload['request_id'] ?? '')); $transactionId = trim((string) ($payload['transaction_id'] ?? '')); $builder = $this->db->table('abdm_hiu_workflows')->select('*')->where('operation', $operation); if ($requestId !== '') { $row = $builder->where('request_id', $requestId)->orderBy('id', 'DESC')->get(1)->getRowArray(); if (! empty($row)) { return $row; } } if ($transactionId !== '') { $row = $this->db->table('abdm_hiu_workflows') ->select('*') ->where('operation', $operation) ->where('transaction_id', $transactionId) ->orderBy('id', 'DESC') ->get(1) ->getRowArray(); if (! empty($row)) { return $row; } } 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, array $result, string $state, string $status, int $httpCode, bool $retryable, int $forceId = 0 ): void { $now = Time::now('Asia/Kolkata')->toDateTimeString(); $hmsRequestId = trim((string) ($payload['request_id'] ?? $payload['requestId'] ?? '')); if ($hmsRequestId === '') { $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'] ?? $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'] ?? $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 = $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'] ?? '')); $retryCount = 0; $id = $forceId; if ($id <= 0) { $existing = $this->findDuplicate($operation, ['request_id' => $hmsRequestId, 'transaction_id' => $transactionId]); if (! empty($existing)) { $id = (int) ($existing['id'] ?? 0); $retryCount = (int) ($existing['retry_count'] ?? 0); } } else { $existing = $this->db->table('abdm_hiu_workflows')->where('id', $id)->get(1)->getRowArray(); $retryCount = (int) ($existing['retry_count'] ?? 0); } if ($status === 'failed' && $retryable) { $retryCount++; } $nextRetryAt = null; if ($status === 'failed' && $retryable && $retryCount < 4) { $backoffMinutes = min(30, 1 * (2 ** max(0, $retryCount - 1))); $nextRetryAt = date('Y-m-d H:i:s', strtotime('+' . $backoffMinutes . ' minutes')); } $row = [ 'hospital_id' => $hospitalId !== '' ? $hospitalId : null, 'hfr_id' => $hfrId !== '' ? $hfrId : null, 'abha_address' => $abhaAddress !== '' ? $abhaAddress : null, 'consent_id' => $consentId !== '' ? $consentId : null, 'request_id' => $hmsRequestId !== '' ? $hmsRequestId : null, 'transaction_id' => $transactionId !== '' ? $transactionId : null, 'operation' => $operation, 'workflow_state' => $state, 'status' => $status, 'http_code' => $httpCode > 0 ? $httpCode : null, 'is_retryable' => $retryable ? 1 : 0, 'retry_count' => $retryCount, 'next_retry_at' => $nextRetryAt, 'last_error' => $errorText !== '' ? mb_substr($errorText, 0, 2000) : null, 'request_json' => (string) json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), 'response_json' => (string) json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), 'updated_at' => $now, ]; if (in_array('hms_request_id', $this->db->getFieldNames('abdm_hiu_workflows') ?? [], true)) { $row['hms_request_id'] = $hmsRequestId !== '' ? $hmsRequestId : null; } if (in_array('gateway_request_id', $this->db->getFieldNames('abdm_hiu_workflows') ?? [], true)) { $row['gateway_request_id'] = $gatewayRequestId !== '' ? $gatewayRequestId : null; } if (in_array('abdm_consent_request_id', $this->db->getFieldNames('abdm_hiu_workflows') ?? [], true)) { $row['abdm_consent_request_id'] = $abdmConsentRequestId !== '' ? $abdmConsentRequestId : null; } if (in_array('abdm_consent_artifact_id', $this->db->getFieldNames('abdm_hiu_workflows') ?? [], true)) { $row['abdm_consent_artifact_id'] = $consentId !== '' ? $consentId : null; } if (in_array($state, ['COMPLETED', 'CONSENT_FETCHED'], true)) { $row['completed_at'] = $now; } if ($state === 'EXPIRED') { $row['expired_at'] = $now; } if ($state === 'REVOKED') { $row['revoked_at'] = $now; } if ($id > 0) { $this->db->table('abdm_hiu_workflows')->where('id', $id)->update($row); return; } $row['created_at'] = $now; $this->db->table('abdm_hiu_workflows')->insert($row); } private function isGatewayRequestIdPattern(string $value): bool { 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 ''; } }