diff --git a/app/Controllers/AbdmHiu.php b/app/Controllers/AbdmHiu.php index c04210e..edee91c 100644 --- a/app/Controllers/AbdmHiu.php +++ b/app/Controllers/AbdmHiu.php @@ -163,6 +163,7 @@ class AbdmHiu extends BaseController if ($abhaAddress === '') { $abhaAddress = $this->extractAbhaAddressFromLog((string) ($row['patient_log'] ?? '')); } + $abhaAddress = $this->canonicalizeAbhaAddress($abhaAddress, $abhaNumber); $latest = $this->findLatestConsentByAbhaAddress($abhaAddress); $items[] = [ @@ -173,7 +174,7 @@ class AbdmHiu extends BaseController 'abha_number' => $abhaNumber, 'abha_address' => $abhaAddress, 'has_abha_number' => preg_match('/^\d{14}$/', $abhaNumber) === 1 ? 1 : 0, - 'has_abha_address' => $abhaAddress !== '' ? 1 : 0, + 'has_abha_address' => $this->isValidAbhaAddress($abhaAddress) ? 1 : 0, 'latest_consent' => $latest, ]; } @@ -228,8 +229,27 @@ class AbdmHiu extends BaseController return null; } + $fields = $this->db->getFieldNames('abdm_hiu_workflows') ?? []; + $select = [ + 'id', + 'operation', + 'workflow_state', + 'status', + 'consent_id', + 'request_id', + 'transaction_id', + 'created_at', + 'updated_at', + ]; + if (in_array('abdm_consent_request_id', $fields, true)) { + $select[] = 'abdm_consent_request_id'; + } + if (in_array('abdm_consent_artifact_id', $fields, true)) { + $select[] = 'abdm_consent_artifact_id'; + } + $row = $this->db->table('abdm_hiu_workflows') - ->select('id, operation, workflow_state, status, consent_id, request_id, transaction_id, created_at, updated_at') + ->select(implode(', ', $select)) ->where('abha_address', $abhaAddress) ->whereIn('operation', ['consent_request', 'consent_status', 'consent_fetch']) ->orderBy('id', 'DESC') @@ -262,4 +282,29 @@ class AbdmHiu extends BaseController return ''; } + + private function canonicalizeAbhaAddress(string $abhaAddress, string $abhaNumber): string + { + $candidate = trim($abhaAddress); + if ($candidate !== '' && $this->isValidAbhaAddress($candidate)) { + return $candidate; + } + + $addressDigits = preg_replace('/\D+/', '', $candidate) ?? ''; + if (preg_match('/^\d{14}$/', $addressDigits) !== 1) { + $addressDigits = preg_replace('/\D+/', '', trim($abhaNumber)) ?? ''; + } + + if (preg_match('/^\d{14}$/', $addressDigits) === 1) { + $domain = trim((string) (getenv('ABDM_ABHA_ADDRESS_DEFAULT_DOMAIN') ?: 'sbx')); + return $addressDigits . '@' . $domain; + } + + return ''; + } + + private function isValidAbhaAddress(string $value): bool + { + return preg_match('/^[A-Za-z0-9._-]+@[A-Za-z0-9.-]+$/', trim($value)) === 1; + } } diff --git a/app/Database/Migrations/2026-06-27-000613_CreateAbdmPushSyncTables.php b/app/Database/Migrations/2026-06-27-000613_CreateAbdmPushSyncTables.php index 4630b23..db615ed 100644 --- a/app/Database/Migrations/2026-06-27-000613_CreateAbdmPushSyncTables.php +++ b/app/Database/Migrations/2026-06-27-000613_CreateAbdmPushSyncTables.php @@ -145,7 +145,7 @@ class CreateAbdmPushSyncTables extends Migration ], 'care_context_reference' => [ 'type' => 'VARCHAR', - 'constraint' => 255, + 'constraint' => 191, 'null' => false, ], 'care_context_display' => [ diff --git a/app/Database/Migrations/2026-07-01-000702_AddConsentMappingColumnsToAbdmHiuWorkflows.php b/app/Database/Migrations/2026-07-01-000702_AddConsentMappingColumnsToAbdmHiuWorkflows.php new file mode 100644 index 0000000..ba9ab29 --- /dev/null +++ b/app/Database/Migrations/2026-07-01-000702_AddConsentMappingColumnsToAbdmHiuWorkflows.php @@ -0,0 +1,111 @@ +db->tableExists('abdm_hiu_workflows')) { + return; + } + + $fields = $this->db->getFieldNames('abdm_hiu_workflows') ?? []; + + $add = []; + if (! in_array('hms_request_id', $fields, true)) { + $add['hms_request_id'] = [ + 'type' => 'VARCHAR', + 'constraint' => 160, + 'null' => true, + 'after' => 'request_id', + ]; + } + if (! in_array('gateway_request_id', $fields, true)) { + $add['gateway_request_id'] = [ + 'type' => 'VARCHAR', + 'constraint' => 160, + 'null' => true, + 'after' => 'hms_request_id', + ]; + } + if (! in_array('abdm_consent_request_id', $fields, true)) { + $add['abdm_consent_request_id'] = [ + 'type' => 'VARCHAR', + 'constraint' => 160, + 'null' => true, + 'after' => 'consent_id', + ]; + } + if (! in_array('abdm_consent_artifact_id', $fields, true)) { + $add['abdm_consent_artifact_id'] = [ + 'type' => 'VARCHAR', + 'constraint' => 160, + 'null' => true, + 'after' => 'abdm_consent_request_id', + ]; + } + + if (! empty($add)) { + $this->forge->addColumn('abdm_hiu_workflows', $add); + } + + $fields = $this->db->getFieldNames('abdm_hiu_workflows') ?? []; + if (in_array('abdm_consent_request_id', $fields, true)) { + if (! $this->indexExists('abdm_hiu_workflows', 'idx_abdm_hiu_abdm_consent_req')) { + $this->db->query('CREATE INDEX idx_abdm_hiu_abdm_consent_req ON abdm_hiu_workflows (abdm_consent_request_id)'); + } + } + if (in_array('hms_request_id', $fields, true)) { + if (! $this->indexExists('abdm_hiu_workflows', 'idx_abdm_hiu_hms_req')) { + $this->db->query('CREATE INDEX idx_abdm_hiu_hms_req ON abdm_hiu_workflows (hms_request_id)'); + } + } + if (in_array('gateway_request_id', $fields, true)) { + if (! $this->indexExists('abdm_hiu_workflows', 'idx_abdm_hiu_gateway_req')) { + $this->db->query('CREATE INDEX idx_abdm_hiu_gateway_req ON abdm_hiu_workflows (gateway_request_id)'); + } + } + if (in_array('abdm_consent_artifact_id', $fields, true)) { + if (! $this->indexExists('abdm_hiu_workflows', 'idx_abdm_hiu_abdm_consent_artifact')) { + $this->db->query('CREATE INDEX idx_abdm_hiu_abdm_consent_artifact ON abdm_hiu_workflows (abdm_consent_artifact_id)'); + } + } + } + + public function down(): void + { + if (! $this->db->tableExists('abdm_hiu_workflows')) { + return; + } + + $fields = $this->db->getFieldNames('abdm_hiu_workflows') ?? []; + + if (in_array('hms_request_id', $fields, true)) { + $this->forge->dropColumn('abdm_hiu_workflows', 'hms_request_id'); + } + if (in_array('gateway_request_id', $fields, true)) { + $this->forge->dropColumn('abdm_hiu_workflows', 'gateway_request_id'); + } + if (in_array('abdm_consent_request_id', $fields, true)) { + $this->forge->dropColumn('abdm_hiu_workflows', 'abdm_consent_request_id'); + } + if (in_array('abdm_consent_artifact_id', $fields, true)) { + $this->forge->dropColumn('abdm_hiu_workflows', 'abdm_consent_artifact_id'); + } + } + + private function indexExists(string $table, string $indexName): bool + { + $rows = $this->db->query('SHOW INDEX FROM ' . $this->db->escapeIdentifiers($table))->getResultArray(); + foreach ($rows as $row) { + if (strcasecmp((string) ($row['Key_name'] ?? ''), $indexName) === 0) { + return true; + } + } + + return false; + } +} diff --git a/app/Libraries/Abdm/M3HiuGatewayClient.php b/app/Libraries/Abdm/M3HiuGatewayClient.php index b821567..911895b 100644 --- a/app/Libraries/Abdm/M3HiuGatewayClient.php +++ b/app/Libraries/Abdm/M3HiuGatewayClient.php @@ -105,15 +105,44 @@ class M3HiuGatewayClient $response['http_code'] = $httpCode; $response['error_text'] = $errorText; $response['retryable'] = $retryable; + + $gatewayRequestId = trim((string) ($response['request_id'] ?? $response['requestId'] ?? '')); + if ($gatewayRequestId === '') { + $gatewayRequestId = trim((string) ($payload['requestId'] ?? $payload['request_id'] ?? '')); + } + $response['gateway_request_id'] = $gatewayRequestId; + + $abdmConsentRequestId = $this->findFirstValueByKeys($response, [ + 'consentRequestId', + 'consent_request_id', + 'abdm_consent_request_id', + ]); + if ($abdmConsentRequestId === '') { + $abdmConsentRequestId = trim((string) ($payload['consentRequestId'] ?? '')); + } + if ($abdmConsentRequestId !== '') { + $response['abdm_consent_request_id'] = $abdmConsentRequestId; + $response['consent_request_id'] = $abdmConsentRequestId; + } + + $abdmConsentId = $this->findFirstValueByKeys($response, [ + 'consentId', + 'consent_id', + ]); + if ($abdmConsentId === '') { + $abdmConsentId = trim((string) ($payload['consentId'] ?? $payload['consent_id'] ?? '')); + } + if ($abdmConsentId !== '') { + $response['consent_id'] = $abdmConsentId; + $response['abdm_consent_artifact_id'] = $abdmConsentId; + } + if (! isset($response['request_id'])) { - $response['request_id'] = (string) ($response['requestId'] ?? $payload['requestId'] ?? $payload['request_id'] ?? ''); + $response['request_id'] = $gatewayRequestId; } if (! isset($response['transaction_id'])) { $response['transaction_id'] = (string) ($response['transactionId'] ?? $payload['transactionId'] ?? $payload['transaction_id'] ?? ''); } - if (! isset($response['consent_id'])) { - $response['consent_id'] = (string) ($response['consentId'] ?? $response['consentRequestId'] ?? $payload['consentId'] ?? $payload['consentRequestId'] ?? $payload['consent_id'] ?? ''); - } $this->writeApiLog($eventKey, $path, $payload, $response, $httpCode, $ok, $errorText, $hospitalId); @@ -211,4 +240,32 @@ class M3HiuGatewayClient 'created_at' => date('Y-m-d H:i:s'), ]); } + + private function findFirstValueByKeys(array $root, array $keys): string + { + $queue = [$root]; + while (! empty($queue)) { + $node = array_shift($queue); + if (! is_array($node)) { + continue; + } + + foreach ($keys as $key) { + if (isset($node[$key])) { + $value = trim((string) $node[$key]); + if ($value !== '') { + return $value; + } + } + } + + foreach ($node as $child) { + if (is_array($child)) { + $queue[] = $child; + } + } + } + + return ''; + } } diff --git a/app/Libraries/Abdm/M3HiuWorkflowService.php b/app/Libraries/Abdm/M3HiuWorkflowService.php index f935034..b52b55a 100644 --- a/app/Libraries/Abdm/M3HiuWorkflowService.php +++ b/app/Libraries/Abdm/M3HiuWorkflowService.php @@ -21,6 +21,35 @@ class M3HiuWorkflowService $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 && $operation !== 'consent_status') { $duplicate = $this->findDuplicate($operation, $normalized); if ($duplicate !== null) { @@ -33,7 +62,7 @@ class M3HiuWorkflowService } } - $result = $this->dispatch($operation, $normalized); + $result = $this->dispatch($operation, $dispatchPayload); $httpCode = (int) ($result['http_code'] ?? 0); $ok = (int) ($result['ok'] ?? 0) === 1; @@ -42,7 +71,7 @@ class M3HiuWorkflowService $retryable = (int) ($result['retryable'] ?? 0) === 1; if ($hasWorkflowTable) { - $this->upsertWorkflow($operation, $normalized, $result, $state, $status, $httpCode, $retryable); + $this->upsertWorkflow($operation, $dispatchPayload, $result, $state, $status, $httpCode, $retryable); $result['workflow_persisted'] = 1; } else { $result['workflow_persisted'] = 0; @@ -164,6 +193,12 @@ class M3HiuWorkflowService 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']; @@ -176,6 +211,11 @@ class M3HiuWorkflowService $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))); } @@ -196,10 +236,371 @@ class M3HiuWorkflowService 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 === '') { + return [ + 'ok' => 0, + 'http_code' => 422, + 'error_text' => 'consentRequestId is required for consent status.', + ]; + } + 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.', + ]; + } + + 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 gateway 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)) { + 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; + + return [ + 'ok' => 1, + 'payload' => $base + ['hiRequest' => $hiRequest], + ]; + } + + 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 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 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'; } @@ -260,9 +661,17 @@ class M3HiuWorkflowService int $forceId = 0 ): void { $now = Time::now('Asia/Kolkata')->toDateTimeString(); - $requestId = trim((string) ($payload['request_id'] ?? $result['request_id'] ?? '')); - $transactionId = trim((string) ($payload['transaction_id'] ?? $result['transaction_id'] ?? '')); - $consentId = trim((string) ($payload['consent_id'] ?? $result['consent_id'] ?? '')); + $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'] ?? '')); + $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'] ?? '')); + 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'] ?? '')); $hospitalId = trim((string) ($payload['hospital_id'] ?? $result['hospital_id'] ?? '')); @@ -271,7 +680,7 @@ class M3HiuWorkflowService $retryCount = 0; $id = $forceId; if ($id <= 0) { - $existing = $this->findDuplicate($operation, ['request_id' => $requestId, 'transaction_id' => $transactionId]); + $existing = $this->findDuplicate($operation, ['request_id' => $hmsRequestId, 'transaction_id' => $transactionId]); if (! empty($existing)) { $id = (int) ($existing['id'] ?? 0); $retryCount = (int) ($existing['retry_count'] ?? 0); @@ -296,7 +705,7 @@ class M3HiuWorkflowService 'hfr_id' => $hfrId !== '' ? $hfrId : 'UNKNOWN', 'abha_address' => $abhaAddress !== '' ? $abhaAddress : null, 'consent_id' => $consentId !== '' ? $consentId : null, - 'request_id' => $requestId !== '' ? $requestId : null, + 'request_id' => $hmsRequestId !== '' ? $hmsRequestId : null, 'transaction_id' => $transactionId !== '' ? $transactionId : null, 'operation' => $operation, 'workflow_state' => $state, @@ -311,6 +720,19 @@ class M3HiuWorkflowService '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; } @@ -329,4 +751,9 @@ class M3HiuWorkflowService $row['created_at'] = $now; $this->db->table('abdm_hiu_workflows')->insert($row); } + + private function isGatewayRequestIdPattern(string $value): bool + { + return preg_match('/^REQ-\d{14}-[A-Za-z0-9]{6,}$/', trim($value)) === 1; + } } diff --git a/app/Views/abdm/hiu_m3.php b/app/Views/abdm/hiu_m3.php index eec48c6..b4c644f 100644 --- a/app/Views/abdm/hiu_m3.php +++ b/app/Views/abdm/hiu_m3.php @@ -44,11 +44,12 @@