Fix ABDM M3 HIU ID mapping, polling guards, and migration compatibility

This commit is contained in:
DevSoftTech Server 2026-07-02 00:55:05 +05:30
parent 3d40642116
commit 0f8bd43e72
6 changed files with 914 additions and 61 deletions

View File

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

View File

@ -145,7 +145,7 @@ class CreateAbdmPushSyncTables extends Migration
],
'care_context_reference' => [
'type' => 'VARCHAR',
'constraint' => 255,
'constraint' => 191,
'null' => false,
],
'care_context_display' => [

View File

@ -0,0 +1,111 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AddConsentMappingColumnsToAbdmHiuWorkflows extends Migration
{
public function up(): void
{
if (! $this->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;
}
}

View File

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

View File

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

View File

@ -44,11 +44,12 @@
</div>
<div class="row g-2 mb-3">
<div class="col-md-3"><input id="selected_patient" class="form-control" placeholder="Selected Patient" readonly></div>
<div class="col-md-2"><input id="selected_patient" class="form-control" placeholder="Selected Patient" readonly></div>
<div class="col-md-2"><input id="request_id" class="form-control" placeholder="requestId"></div>
<div class="col-md-2"><input id="transaction_id" class="form-control" placeholder="transactionId"></div>
<div class="col-md-2"><input id="consent_id" class="form-control" placeholder="consentId / consentRequestId"></div>
<div class="col-md-3"><input id="abha_address" class="form-control" placeholder="abha_address" readonly></div>
<div class="col-md-2"><input id="consent_request_id" class="form-control" placeholder="ABDM consentRequestId"></div>
<div class="col-md-2"><input id="consent_artifact_id" class="form-control" placeholder="ABDM consentId (artifact)"></div>
<div class="col-md-2"><input id="abha_address" class="form-control" placeholder="abha_address" readonly></div>
</div>
<div class="small mb-3" id="readiness_box"></div>
@ -115,6 +116,8 @@
var csrfHash = '<?= csrf_hash() ?>';
var selectedPatient = null;
var consentPollTimer = null;
var consentPollAttempts = 0;
var consentPollMaxAttempts = 12;
var opRoute = {
consent_request: '<?= base_url('AbdmHiu/consent_request') ?>',
@ -134,6 +137,28 @@
.replace(/'/g, '&#39;');
}
function fieldEl(ids) {
for (var i = 0; i < ids.length; i++) {
var el = document.getElementById(ids[i]);
if (el) {
return el;
}
}
return null;
}
function getFieldValue(ids) {
var el = fieldEl(ids);
return el ? (el.value || '').toString().trim() : '';
}
function setFieldValue(ids, value) {
var el = fieldEl(ids);
if (el) {
el.value = value;
}
}
function genReq(prefix) {
var p = prefix || 'REQ-HIU';
return p + '-' + Date.now();
@ -161,32 +186,53 @@
return ['approved', 'granted', 'active', 'linked', 'completed'].indexOf(s) !== -1;
}
function isTerminalConsentState(statusText) {
var s = (statusText || '').toString().trim().toLowerCase();
return ['approved', 'granted', 'active', 'completed', 'revoked', 'denied', 'expired', 'rejected', 'failed', 'error'].indexOf(s) !== -1;
}
function isGatewayRequestId(v) {
return /^REQ-\d{14}-[A-Za-z0-9]{6,}$/.test((v || '').toString().trim());
}
function isValidConsentArtifactId(v) {
var val = (v || '').toString().trim();
return val !== '' && !isGatewayRequestId(val);
}
function syncButtons(consentStateText) {
var hasPatient = !!selectedPatient;
var hasNum = hasPatient && selectedPatient.has_abha_number === 1;
var hasAddr = hasPatient && selectedPatient.has_abha_address === 1;
var hasConsentRef = (document.getElementById('consent_id').value.trim() !== '' || document.getElementById('request_id').value.trim() !== '');
var hasConsentRequestRef = (getFieldValue(['consent_request_id', 'consent_id']) !== '');
var hasConsentArtifactRef = (getFieldValue(['consent_artifact_id', 'consent_id']) !== '');
document.getElementById('btnConsentRequest').disabled = !(hasNum && hasAddr);
document.getElementById('btnConsentStatus').disabled = !hasConsentRef;
document.getElementById('btnConsentFetch').disabled = !hasConsentRef;
document.getElementById('btnHiRequest').disabled = !(hasConsentRef && isConsentApproved(consentStateText));
document.getElementById('btnConsentStatus').disabled = !hasConsentRequestRef;
document.getElementById('btnConsentFetch').disabled = !hasConsentArtifactRef;
document.getElementById('btnHiRequest').disabled = !(hasConsentArtifactRef && isConsentApproved(consentStateText));
updateReadinessBox(consentStateText);
}
function setSelectedPatient(p) {
selectedPatient = p;
document.getElementById('selected_patient').value = (p.patient_name || '') + (p.patient_code ? ' (' + p.patient_code + ')' : '');
document.getElementById('abha_address').value = p.abha_address || '';
document.getElementById('f_abha_address').value = p.abha_address || '';
document.getElementById('request_id').value = genReq('REQ-CONSENT');
document.getElementById('transaction_id').value = genReq('TXN-HIU');
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'));
var latest = p.latest_consent || {};
var consentRef = (latest.consent_id || latest.request_id || '').toString();
if (consentRef) {
document.getElementById('consent_id').value = consentRef;
document.getElementById('f_consent_id').value = consentRef;
var consentRequestRef = (latest.abdm_consent_request_id || '').toString();
var consentArtifactRef = (latest.abdm_consent_artifact_id || latest.consent_id || '').toString();
if (consentRequestRef && !isGatewayRequestId(consentRequestRef)) {
setFieldValue(['consent_request_id', 'consent_id'], consentRequestRef);
setFieldValue(['f_consent_id'], consentRequestRef);
}
if (isValidConsentArtifactId(consentArtifactRef)) {
setFieldValue(['consent_artifact_id', 'consent_id'], consentArtifactRef);
} else {
setFieldValue(['consent_artifact_id', 'consent_id'], '');
}
var consentState = (latest.workflow_state || latest.status || 'Unknown').toString();
@ -248,26 +294,141 @@
}
var payload = Object.assign({}, custom, {
requestId: document.getElementById('request_id').value.trim(),
transactionId: document.getElementById('transaction_id').value.trim(),
abha_address: document.getElementById('abha_address').value.trim(),
requestId: getFieldValue(['request_id']),
transactionId: getFieldValue(['transaction_id']),
abha_address: getFieldValue(['abha_address']),
patient_id: selectedPatient ? selectedPatient.patient_id : null
});
var consentVal = document.getElementById('consent_id').value.trim();
if (consentVal !== '') {
payload.consentId = consentVal;
payload.consentRequestId = consentVal;
var consentRequestVal = getFieldValue(['consent_request_id', 'consent_id']);
if (consentRequestVal !== '') {
payload.consentRequestId = consentRequestVal;
payload.abdm_consent_request_id = consentRequestVal;
}
var consentArtifactVal = getFieldValue(['consent_artifact_id', 'consent_id']);
if (consentArtifactVal !== '') {
payload.consentId = consentArtifactVal;
payload.abdm_consent_artifact_id = consentArtifactVal;
}
if (!payload.timestamp) {
payload.timestamp = new Date().toISOString();
}
delete payload.abha_id;
delete payload.abhaId;
delete payload.consent_id;
payload[csrfName] = csrfHash;
return payload;
}
function isValidAbhaAddress(v) {
return /^[A-Za-z0-9._-]+@[A-Za-z0-9.-]+$/.test((v || '').toString().trim());
}
function buildGatewayPayload(op, raw) {
var requestId = (raw.requestId || raw.request_id || '').toString().trim();
var timestamp = (raw.timestamp || '').toString().trim();
var abhaAddress = (raw.abha_address || '').toString().trim();
var base = {
requestId: requestId,
timestamp: timestamp || new Date().toISOString()
};
if (op === 'consent_request') {
var consent = raw.consent;
if (!consent || typeof consent !== 'object' || Array.isArray(consent) || Object.keys(consent).length === 0) {
if (!isValidAbhaAddress(abhaAddress)) {
throw new Error('Patient ABHA address must be in format like 91510165305101@sbx.');
}
consent = {
purpose: {
code: 'CAREMGT',
text: 'Care Management',
refUri: 'https://abdm.gov.in'
},
patient: { id: abhaAddress },
requester: { name: 'Hospital HMS' },
hiTypes: ['OPConsultation', 'DiagnosticReport'],
permission: {
accessMode: 'VIEW',
dateRange: {
from: new Date(Date.now() - (30 * 24 * 60 * 60 * 1000)).toISOString(),
to: new Date().toISOString()
},
dataEraseAt: new Date(Date.now() + (365 * 24 * 60 * 60 * 1000)).toISOString(),
frequency: {
unit: 'HOUR',
value: 1,
repeats: 0
}
}
};
}
if (!consent.patient || typeof consent.patient !== 'object') {
consent.patient = {};
}
if (!consent.patient.id && isValidAbhaAddress(abhaAddress)) {
consent.patient.id = abhaAddress;
}
if (!isValidAbhaAddress(consent.patient.id || '')) {
throw new Error('Patient ABHA address must be in format like 91510165305101@sbx.');
}
base.consent = consent;
return base;
}
if (op === 'consent_status') {
var consentRequestId = (raw.abdm_consent_request_id || raw.consentRequestId || '').toString().trim();
if (!consentRequestId) {
throw new Error('consentRequestId is required for consent status.');
}
if (isGatewayRequestId(consentRequestId)) {
throw new Error('mapping_error: consentRequestId looks like gateway request_id (REQ-...). Use actual ABDM consentRequestId from init response.');
}
base.consentRequestId = consentRequestId;
return base;
}
if (op === 'consent_fetch') {
var consentId = (raw.abdm_consent_artifact_id || raw.consentId || '').toString().trim();
if (!consentId) {
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.');
}
base.consentId = consentId;
return base;
}
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.');
}
if (!hiRequest.consent || typeof hiRequest.consent !== 'object') {
hiRequest.consent = {};
}
if (!hiRequest.consent.id) {
hiRequest.consent.id = (raw.consentId || raw.consent_id || '').toString().trim();
}
if (!(hiRequest.consent.id || '').toString().trim()) {
throw new Error('hiRequest.consent.id is required for health information request.');
}
base.hiRequest = hiRequest;
return base;
}
throw new Error('Unsupported operation');
}
function setResult(obj) {
document.getElementById('result_box').textContent = JSON.stringify(obj, null, 2);
}
@ -292,7 +453,13 @@
setResult({ ok: 0, message: 'Unsupported operation' });
return;
}
var payload = readPayload(op);
var payload;
try {
payload = buildGatewayPayload(op, readPayload(op));
} catch (e) {
setResult({ ok: 0, message: e.message || 'Validation failed' });
return;
}
postJson(url, payload).then(function (res) {
if (!options.silent) {
setResult(res);
@ -300,10 +467,15 @@
if ((res.ok || 0) === 1) {
var data = res.data || {};
var consentRef = (data.consentId || data.consentRequestId || data.consent_id || data.requestId || data.request_id || '').toString();
if (consentRef) {
document.getElementById('consent_id').value = consentRef;
document.getElementById('f_consent_id').value = consentRef;
var consentReqRef = (data.abdm_consent_request_id || data.consentRequestId || data.consent_request_id || '').toString();
if (consentReqRef && !isGatewayRequestId(consentReqRef)) {
setFieldValue(['consent_request_id', 'consent_id'], consentReqRef);
setFieldValue(['f_consent_id'], consentReqRef);
}
var consentArtifactRef = (data.abdm_consent_artifact_id || data.consentId || data.consent_id || '').toString();
if (isValidConsentArtifactId(consentArtifactRef)) {
setFieldValue(['consent_artifact_id', 'consent_id'], consentArtifactRef);
}
var state = (data.workflow_state || data.consent_status || data.status || res.workflow_state || '').toString();
@ -312,9 +484,20 @@
if (op === 'consent_request') {
startConsentPolling();
}
if (op === 'consent_status' && isConsentApproved(state)) {
if (op === 'consent_status' && isTerminalConsentState(state)) {
stopConsentPolling();
}
} else if (op === 'consent_status') {
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();
@ -325,7 +508,14 @@
function startConsentPolling() {
stopConsentPolling();
consentPollAttempts = 0;
consentPollTimer = setInterval(function () {
consentPollAttempts++;
if (consentPollAttempts > consentPollMaxAttempts) {
stopConsentPolling();
setResult({ ok: 0, message: 'Consent status polling stopped after max retries. Use Refresh/Status manually or wait for callback.' });
return;
}
run('consent_status', { silent: true });
}, 15000);
}
@ -335,16 +525,17 @@
clearInterval(consentPollTimer);
consentPollTimer = null;
}
consentPollAttempts = 0;
}
function fetchTimeline() {
var qs = new URLSearchParams({
hfr_id: document.getElementById('f_hfr_id').value.trim(),
consent_id: document.getElementById('f_consent_id').value.trim(),
transaction_id: document.getElementById('f_transaction_id').value.trim(),
abha_address: document.getElementById('f_abha_address').value.trim(),
date_from: document.getElementById('f_date_from').value,
date_to: document.getElementById('f_date_to').value
hfr_id: getFieldValue(['f_hfr_id']),
consent_id: getFieldValue(['f_consent_id']),
transaction_id: getFieldValue(['f_transaction_id']),
abha_address: getFieldValue(['f_abha_address']),
date_from: getFieldValue(['f_date_from']),
date_to: getFieldValue(['f_date_to'])
});
fetch('<?= base_url('AbdmHiu/timeline') ?>?' + qs.toString(), {
@ -373,13 +564,19 @@
});
}
document.getElementById('btnPatientSearch').addEventListener('click', searchPatients);
document.getElementById('patient_search').addEventListener('keydown', function (e) {
if (e.key === 'Enter') {
e.preventDefault();
searchPatients();
}
});
var btnPatientSearch = document.getElementById('btnPatientSearch');
if (btnPatientSearch) {
btnPatientSearch.addEventListener('click', searchPatients);
}
var patientSearch = document.getElementById('patient_search');
if (patientSearch) {
patientSearch.addEventListener('keydown', function (e) {
if (e.key === 'Enter') {
e.preventDefault();
searchPatients();
}
});
}
document.querySelectorAll('button[data-op]').forEach(function (btn) {
btn.addEventListener('click', function () {
@ -387,12 +584,28 @@
});
});
document.getElementById('consent_id').addEventListener('input', function () {
syncButtons();
});
var consentReqInput = fieldEl(['consent_request_id', 'consent_id']);
if (consentReqInput) {
consentReqInput.addEventListener('input', function () {
syncButtons();
});
}
document.getElementById('btnApplyFilter').addEventListener('click', fetchTimeline);
document.getElementById('btnRefreshTimeline').addEventListener('click', fetchTimeline);
var consentArtifactInput = fieldEl(['consent_artifact_id']);
if (consentArtifactInput) {
consentArtifactInput.addEventListener('input', function () {
syncButtons();
});
}
var btnApplyFilter = document.getElementById('btnApplyFilter');
if (btnApplyFilter) {
btnApplyFilter.addEventListener('click', fetchTimeline);
}
var btnRefreshTimeline = document.getElementById('btnRefreshTimeline');
if (btnRefreshTimeline) {
btnRefreshTimeline.addEventListener('click', fetchTimeline);
}
syncButtons();
fetchTimeline();