ABDM HIU callback hardening, OPD ABHA optional confirm, and AJAX memory cleanup

This commit is contained in:
DevSoftTech Server 2026-07-10 00:09:35 +05:30
parent 658e67ad3b
commit acabc6abdd
10 changed files with 1931 additions and 101 deletions

View File

@ -624,6 +624,10 @@ $routes->post('AbdmGateway/scan_share_link_patient/(:num)', 'AbdmGateway::scanSh
$routes->post('AbdmGateway/scan_share_lookup_result_callback', 'AbdmGateway::scanShareLookupResultCallback');
$routes->post('AbdmGateway/consent_request', 'AbdmGateway::consentRequest', ['filter' => $abdmPermFilter]);
$routes->post('AbdmGateway/consent_callback', 'AbdmGateway::consentCallback');
$routes->post('AbdmGateway/hip_consent_notify_callback', 'AbdmGateway::hipConsentNotifyCallback');
$routes->post('api/v3/hiu/consent/on-notify', 'AbdmGateway::hipConsentNotifyCallback');
$routes->post('api/v3/hiu/consent/on_notify', 'AbdmGateway::hipConsentNotifyCallback');
$routes->post('api/v3/hiu/consent/notify', 'AbdmGateway::hipConsentNotifyCallback');
$routes->post('AbdmGateway/consent_revoked_callback', 'AbdmGateway::consentRevokedCallback');
$routes->post('AbdmGateway/record_linked_callback', 'AbdmGateway::recordLinkedCallback');
// ABDM M3 HIU (HMS -> Gateway)
@ -637,9 +641,17 @@ $routes->post('AbdmHiu/consent_request_fetch', 'AbdmHiu::consentRequestFetch', [
$routes->post('AbdmHiu/health_information_request', 'AbdmHiu::healthInformationRequest', ['filter' => $abdmPermFilter]);
$routes->post('AbdmHiu/consent_reconcile', 'AbdmHiu::consentReconcile', ['filter' => $abdmPermFilter]);
$routes->post('AbdmHiu/data_fetch', 'AbdmHiu::dataFetch', ['filter' => $abdmPermFilter]);
$routes->post('AbdmHiu/consent_update_callback', 'AbdmHiu::consentUpdateWebhook');
$routes->post('AbdmHiu/health_information_on_request_callback', 'AbdmHiu::healthInformationOnRequestWebhook');
$routes->post('AbdmHiu/health_information_data_push_callback', 'AbdmHiu::healthInformationDataPushWebhook');
$routes->post('api/v3/hiu/consent/request', 'AbdmHiu::consentRequest', ['filter' => $abdmPermFilter]);
$routes->post('api/v3/hiu/consent/request/status', 'AbdmHiu::consentRequestStatus', ['filter' => $abdmPermFilter]);
$routes->post('api/v3/hiu/consent/request/fetch', 'AbdmHiu::consentRequestFetch', ['filter' => $abdmPermFilter]);
$routes->post('api/v3/hiu/consent/on-update', 'AbdmHiu::consentUpdateWebhook');
$routes->post('api/v3/hiu/consent/on_update', 'AbdmHiu::consentUpdateWebhook');
$routes->post('api/v3/hiu/health-information/on-request', 'AbdmHiu::healthInformationOnRequestWebhook');
$routes->post('api/v3/hiu/health-information/on_request', 'AbdmHiu::healthInformationOnRequestWebhook');
$routes->post('api/v3/hiu/data/push', 'AbdmHiu::healthInformationDataPushWebhook');
$routes->post('api/v3/hiu/health-information/request', 'AbdmHiu::healthInformationRequest', ['filter' => $abdmPermFilter]);
$routes->post('api/v1/hiu/consent/status', 'AbdmHiu::consentReconcile', ['filter' => $abdmPermFilter]);
$routes->post('api/v1/hiu/data/fetch', 'AbdmHiu::dataFetch', ['filter' => $abdmPermFilter]);

View File

@ -670,6 +670,121 @@ class AbdmGateway extends BaseController
]);
}
// =========================================================================
// HIP Consent Notify Callback — gateway forwards consent notify updates.
// POST /AbdmGateway/hip_consent_notify_callback (no auth filter — public webhook)
// =========================================================================
public function hipConsentNotifyCallback()
{
$signatureFailure = $this->validateWebhookSignature();
if ($signatureFailure !== null) {
return $signatureFailure;
}
$payload = $this->request->getJSON(true);
if (! is_array($payload) || empty($payload)) {
return $this->response->setStatusCode(400)->setJSON([
'ok' => 0,
'error_text' => 'Invalid JSON payload',
]);
}
$consentObj = [];
if (is_array($payload['consent'] ?? null)) {
$consentObj = (array) $payload['consent'];
} elseif (is_array($payload['consentDetail'] ?? null)) {
$consentObj = (array) $payload['consentDetail'];
}
$requestId = trim((string) (
$payload['request_id']
?? $payload['requestId']
?? ($payload['response']['requestId'] ?? '')
?? ''
));
$consentId = trim((string) (
$consentObj['id']
?? $payload['consent_id']
?? $payload['consentId']
?? ''
));
$abhaAddress = trim((string) (
$consentObj['patient']['id']
?? $payload['abha_address']
?? $payload['abha_id']
?? ''
));
$statusRaw = trim((string) (
$consentObj['status']
?? $payload['consent_status']
?? $payload['status']
?? 'status_checked'
));
$status = strtolower($statusRaw);
// Keep existing M3 workflow in sync for dateRange and consent status lookups.
try {
$m3Service = new \App\Libraries\Abdm\M3HiuWorkflowService();
$m3Service->ingestConsentUpdateWebhook($payload);
} catch (\Throwable $e) {
// Fail-open: callback must still ACK to avoid upstream retry storms.
}
if ($this->db->tableExists('consent_logs')) {
$logStatus = match ($status) {
'granted', 'approved', 'active' => 'GRANTED',
'revoked', 'denied' => 'REVOKED',
'expired' => 'EXPIRED',
default => 'GRANTED',
};
$dateRange = $consentObj['permission']['dateRange'] ?? [];
$expiresAt = trim((string) ($dateRange['to'] ?? $payload['expires_at'] ?? ''));
$this->db->table('consent_logs')->insert([
'patient_id' => (int) ($payload['patient_id'] ?? 0),
'abha_id' => $abhaAddress !== '' ? $abhaAddress : null,
'consent_id' => $consentId !== '' ? $consentId : ($requestId !== '' ? $requestId : null),
'purpose' => trim((string) ($consentObj['purpose']['text'] ?? '')) ?: null,
'status' => $logStatus,
'created_at' => Time::now('Asia/Kolkata')->toDateTimeString(),
'expires_at' => $expiresAt !== '' ? $expiresAt : null,
]);
}
if ($this->db->tableExists('abdm_api_logs')) {
$this->db->table('abdm_api_logs')->insert([
'channel' => 'bridge',
'event_type' => 'abdm.hip.consent.notify.callback',
'endpoint' => '/AbdmGateway/hip_consent_notify_callback',
'http_method' => 'POST',
'entity_type' => 'consent',
'entity_id' => $consentId !== '' ? $consentId : $requestId,
'request_json' => (string) json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
'response_code' => 202,
'response_json' => (string) json_encode([
'ok' => 1,
'status' => 'accepted',
'request_id' => $requestId,
'consent_id' => $consentId,
'consent_status' => $statusRaw,
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
'status' => 'success',
'error_message' => null,
'created_at' => Time::now('Asia/Kolkata')->toDateTimeString(),
]);
}
return $this->response->setStatusCode(202)->setJSON([
'ok' => 1,
'status' => 'accepted',
'request_id' => $requestId,
'consent_id' => $consentId !== '' ? $consentId : null,
'consent_status' => $statusRaw,
]);
}
public function scanShareLookup()
{
if (! $this->request->isAJAX()) {

View File

@ -53,6 +53,75 @@ class AbdmHiu extends BaseController
return $this->handleOperation('data_fetch', 'DATA_PENDING');
}
public function consentUpdateWebhook()
{
$signatureFailure = $this->validateGatewayWebhookSignature();
if ($signatureFailure !== null) {
return $signatureFailure;
}
$payload = $this->request->getJSON(true);
if (! is_array($payload) || empty($payload)) {
return $this->response->setStatusCode(400)->setJSON([
'ok' => 0,
'error' => 'Invalid consent callback payload',
]);
}
$result = $this->service->ingestConsentUpdateWebhook($payload);
$httpCode = (int) ($result['ok'] ?? 0) === 1 ? 202 : 400;
return $this->response->setStatusCode($httpCode)->setJSON($result);
}
public function healthInformationOnRequestWebhook()
{
$signatureFailure = $this->validateGatewayWebhookSignature();
if ($signatureFailure !== null) {
return $signatureFailure;
}
$payload = $this->request->getJSON(true);
if (! is_array($payload) || empty($payload)) {
return $this->response->setStatusCode(400)->setJSON([
'ok' => 0,
'error' => 'Invalid health-information callback payload',
]);
}
$result = $this->service->ingestHealthInformationCallback($payload, 'hi_on_request_callback');
return $this->response->setStatusCode(202)->setJSON([
'ok' => 1,
'status' => 'accepted',
'result' => $result,
]);
}
public function healthInformationDataPushWebhook()
{
$signatureFailure = $this->validateGatewayWebhookSignature();
if ($signatureFailure !== null) {
return $signatureFailure;
}
$payload = $this->request->getJSON(true);
if (! is_array($payload) || empty($payload)) {
return $this->response->setStatusCode(400)->setJSON([
'ok' => 0,
'error' => 'Invalid data push payload',
]);
}
$result = $this->service->ingestHealthInformationCallback($payload, 'hi_data_push_callback');
return $this->response->setStatusCode(202)->setJSON([
'ok' => 1,
'status' => 'accepted',
'result' => $result,
]);
}
public function timeline()
{
$filters = [
@ -328,4 +397,59 @@ class AbdmHiu extends BaseController
{
return preg_match('/^[A-Za-z0-9._-]+@[A-Za-z0-9.-]+$/', trim($value)) === 1;
}
private function validateGatewayWebhookSignature(): ?\CodeIgniter\HTTP\ResponseInterface
{
$secret = trim((string) (
$this->readRuntimeSetting('GATEWAY_TO_HMS_HMAC_SECRET')
?: $this->readRuntimeSetting('EKA_WEBHOOK_SECRET')
?: $this->readRuntimeSetting('HMS_WEBHOOK_SECRET')
));
// If no secret is configured, accept payload to keep callback flow functional.
if ($secret === '') {
return null;
}
$rawBody = (string) $this->request->getBody();
$provided = trim((string) (
$this->request->getHeaderLine('X-Signature')
?: $this->request->getHeaderLine('X-Hub-Signature-256')
));
if ($provided === '') {
return $this->response->setStatusCode(401)->setJSON([
'ok' => 0,
'error' => 'Missing webhook signature',
]);
}
$expected = hash_hmac('sha256', $rawBody, $secret);
$normalizedProvided = preg_replace('/^sha256=/i', '', $provided) ?? '';
if (! hash_equals($expected, $normalizedProvided)) {
return $this->response->setStatusCode(401)->setJSON([
'ok' => 0,
'error' => 'Invalid webhook signature',
]);
}
return null;
}
private function readRuntimeSetting(string $name): string
{
if (! $this->db->tableExists('hospital_setting')) {
return '';
}
$row = $this->db->table('hospital_setting')
->select('s_value')
->where('s_name', $name)
->orderBy('id', 'DESC')
->get(1)
->getRowArray();
return trim((string) ($row['s_value'] ?? ''));
}
}

View File

@ -1660,15 +1660,6 @@ class Opd extends BaseController
]);
}
if ($abhaAddress !== '' && ! $this->isValidAbhaAddress($abhaAddress)) {
return $this->response->setJSON([
'insertid' => 0,
'error_text' => 'Invalid ABHA Address format.',
'csrfName' => csrf_token(),
'csrfHash' => csrf_hash(),
]);
}
$sql = "select * from patient_master where id=" . $pid;
$query = $this->db->query($sql);
$personInfo = $query->getResult();

View File

@ -0,0 +1,66 @@
<?php
namespace App\Database\Migrations;
use CodeIgniter\Database\Migration;
class AdjustAbdmHiuWorkflowRequestOperationIndex extends Migration
{
public function up(): void
{
if (! $this->db->tableExists('abdm_hiu_workflows')) {
return;
}
$indexes = $this->db->query('SHOW INDEX FROM abdm_hiu_workflows')->getResultArray();
$hasReqOpUnique = false;
$hasReqOpIdx = false;
foreach ($indexes as $idx) {
$name = (string) ($idx['Key_name'] ?? '');
if ($name === 'uniq_abdm_hiu_req_op') {
$hasReqOpUnique = true;
}
if ($name === 'idx_abdm_hiu_req_op') {
$hasReqOpIdx = true;
}
}
if ($hasReqOpUnique) {
$this->db->query('ALTER TABLE abdm_hiu_workflows DROP INDEX uniq_abdm_hiu_req_op');
}
if (! $hasReqOpIdx) {
$this->db->query('ALTER TABLE abdm_hiu_workflows ADD INDEX idx_abdm_hiu_req_op (request_id, operation)');
}
}
public function down(): void
{
if (! $this->db->tableExists('abdm_hiu_workflows')) {
return;
}
$indexes = $this->db->query('SHOW INDEX FROM abdm_hiu_workflows')->getResultArray();
$hasReqOpUnique = false;
$hasReqOpIdx = false;
foreach ($indexes as $idx) {
$name = (string) ($idx['Key_name'] ?? '');
if ($name === 'uniq_abdm_hiu_req_op') {
$hasReqOpUnique = true;
}
if ($name === 'idx_abdm_hiu_req_op') {
$hasReqOpIdx = true;
}
}
if ($hasReqOpIdx) {
$this->db->query('ALTER TABLE abdm_hiu_workflows DROP INDEX idx_abdm_hiu_req_op');
}
if (! $hasReqOpUnique) {
$this->db->query('ALTER TABLE abdm_hiu_workflows ADD UNIQUE INDEX uniq_abdm_hiu_req_op (request_id, operation)');
}
}
}

View File

@ -14,6 +14,23 @@ class M3HiuGatewayClient
$this->baseUrl = rtrim((string) (getenv('EATRIA_BRIDGE_URL') ?: 'https://abdm-bridge.e-atria.in/api'), '/');
}
private function sanitizeBearerToken(string $token): string
{
$token = trim($token);
if ($token === '') {
return '';
}
if (stripos($token, 'Bearer ') === 0) {
$token = trim(substr($token, 7));
}
$token = trim($token, " \t\n\r\0\x0B\"'");
$token = (string) preg_replace('/\s+/u', '', $token);
return $token;
}
public function consentRequest(array $payload): array
{
return $this->call('/v3/hiu/consent/request', $payload, 'consent.request');
@ -54,6 +71,10 @@ class M3HiuGatewayClient
$token = (string) ($settings['token'] ?? '');
$hfrId = (string) ($settings['hfr_id'] ?? '');
$hospitalId = (string) ($settings['hospital_id'] ?? '');
$runtimeBaseUrl = trim((string) ($settings['base_url'] ?? ''));
if ($runtimeBaseUrl !== '') {
$this->baseUrl = rtrim($runtimeBaseUrl, '/');
}
$payloadHfrId = trim((string) ($payload['hfr_id'] ?? ''));
if ($payloadHfrId !== '' && strcasecmp($payloadHfrId, $hfrId) !== 0) {
@ -105,7 +126,7 @@ class M3HiuGatewayClient
: ('HTTP ' . $httpCode . ' non-JSON response');
}
if ($this->isCloudFrontBlockedResponse($httpCode, $raw, $response ?? [])) {
if ($this->isCloudFrontBlockedResponse($httpCode, $raw, $decoded ?? [])) {
$errorText = 'Gateway edge block (CloudFront 403 Request blocked). Retry later or ask gateway team to unblock the endpoint.';
}
@ -178,6 +199,10 @@ class M3HiuGatewayClient
$token = (string) ($settings['token'] ?? '');
$hfrId = (string) ($settings['hfr_id'] ?? '');
$hospitalId = (string) ($settings['hospital_id'] ?? '');
$runtimeBaseUrl = trim((string) ($settings['base_url'] ?? ''));
if ($runtimeBaseUrl !== '') {
$this->baseUrl = rtrim($runtimeBaseUrl, '/');
}
$payloadHfrId = trim((string) ($query['hfr_id'] ?? ''));
if ($payloadHfrId !== '' && strcasecmp($payloadHfrId, $hfrId) !== 0) {
@ -305,24 +330,46 @@ class M3HiuGatewayClient
return ['ok' => 0, 'http_code' => 500, 'error_text' => 'hospital_setting table not found', 'retryable' => 0];
}
$rows = $this->db->table('hospital_setting')
$settingFields = $this->db->getFieldNames('hospital_setting') ?? [];
$orderCol = null;
foreach (['id', 's_id'] as $candidateOrderCol) {
if (in_array($candidateOrderCol, $settingFields, true)) {
$orderCol = $candidateOrderCol;
break;
}
}
$rowsBuilder = $this->db->table('hospital_setting')
->select('s_name, s_value')
->whereIn('s_name', ['EATRIA_BRIDGE_TOKEN', 'ABDM_HFR_ID', 'ABDM_HOSPITAL_ID'])
->get()
->getResultArray();
->whereIn('s_name', [
'EATRIA_BRIDGE_TOKEN',
'ABDM_HFR_ID',
'H_HFR_ID',
'ABDM_HOSPITAL_HFR_ID',
'ABDM_HOSPITAL_ID',
'ABDM_BRIDGE_HOSPITAL_ID',
'EATRIA_BRIDGE_HOSPITAL_ID',
'EATRIA_BRIDGE_URL',
]);
if ($orderCol !== null) {
$rowsBuilder->orderBy($orderCol, 'DESC');
}
$rows = $rowsBuilder->get()->getResultArray();
$kv = [];
foreach ($rows as $row) {
$key = trim((string) ($row['s_name'] ?? ''));
if ($key === '') {
if ($key === '' || array_key_exists($key, $kv)) {
continue;
}
$kv[$key] = trim((string) ($row['s_value'] ?? ''));
}
$token = (string) ($kv['EATRIA_BRIDGE_TOKEN'] ?? '');
$hfrId = (string) ($kv['ABDM_HFR_ID'] ?? '');
$hospitalId = (string) ($kv['ABDM_HOSPITAL_ID'] ?? '');
$token = $this->sanitizeBearerToken((string) ($kv['EATRIA_BRIDGE_TOKEN'] ?? ''));
$hfrId = (string) ($kv['ABDM_HFR_ID'] ?? $kv['H_HFR_ID'] ?? $kv['ABDM_HOSPITAL_HFR_ID'] ?? '');
$hospitalId = (string) ($kv['ABDM_HOSPITAL_ID'] ?? $kv['ABDM_BRIDGE_HOSPITAL_ID'] ?? $kv['EATRIA_BRIDGE_HOSPITAL_ID'] ?? '');
$baseUrl = trim((string) ($kv['EATRIA_BRIDGE_URL'] ?? ''));
if ($token === '') {
return [
@ -346,6 +393,7 @@ class M3HiuGatewayClient
'token' => $token,
'hfr_id' => $hfrId,
'hospital_id' => $hospitalId,
'base_url' => $baseUrl,
];
}

View File

@ -246,6 +246,7 @@ class M3HiuWorkflowService
}
$builder = $this->db->table('abdm_hiu_workflows')->select('*')->orderBy('id', 'DESC');
$fields = $this->db->getFieldNames('abdm_hiu_workflows') ?? [];
$hfrId = trim((string) ($filters['hfr_id'] ?? ''));
if ($hfrId !== '') {
@ -253,7 +254,16 @@ class M3HiuWorkflowService
}
$consentId = trim((string) ($filters['consent_id'] ?? ''));
if ($consentId !== '') {
$builder->groupStart();
$builder->where('consent_id', $consentId);
if (in_array('abdm_consent_request_id', $fields, true)) {
$builder->orWhere('abdm_consent_request_id', $consentId);
}
if (in_array('abdm_consent_artifact_id', $fields, true)) {
$builder->orWhere('abdm_consent_artifact_id', $consentId);
}
$builder->orWhere('request_id', $consentId);
$builder->groupEnd();
}
$transactionId = trim((string) ($filters['transaction_id'] ?? ''));
if ($transactionId !== '') {
@ -261,7 +271,12 @@ class M3HiuWorkflowService
}
$abhaAddress = trim((string) ($filters['abha_address'] ?? ''));
if ($abhaAddress !== '') {
$builder->groupStart();
$builder->where('abha_address', $abhaAddress);
// Older rows may store ABHA address only inside request_json consent.patient.id.
$builder->orLike('request_json', '"abha_address":"' . str_replace('"', '\\"', $abhaAddress) . '"');
$builder->orLike('request_json', '"id":"' . str_replace('"', '\\"', $abhaAddress) . '"');
$builder->groupEnd();
}
$from = trim((string) ($filters['date_from'] ?? ''));
if ($from !== '') {
@ -356,6 +371,188 @@ class M3HiuWorkflowService
];
}
public function ingestConsentUpdateWebhook(array $payload): array
{
if (! $this->db->tableExists('abdm_hiu_workflows')) {
return [
'ok' => 1,
'persisted' => 0,
'message' => 'abdm_hiu_workflows table not found; callback accepted without persistence',
];
}
$consent = is_array($payload['consent'] ?? null) ? (array) $payload['consent'] : [];
$consentDetail = is_array($payload['consentDetail'] ?? null) ? (array) $payload['consentDetail'] : [];
$consentObj = ! empty($consent) ? $consent : $consentDetail;
$status = strtolower(trim((string) (
$consentObj['status']
?? $payload['consent_status']
?? $payload['status']
?? ''
)));
if ($status === '') {
$status = 'status_checked';
}
$consentId = trim((string) (
$consentObj['id']
?? $payload['consent_id']
?? $payload['consentId']
?? $payload['abdm_consent_artifact_id']
?? ''
));
$requestId = trim((string) (
$payload['request_id']
?? $payload['requestId']
?? ($payload['response']['requestId'] ?? '')
?? ''
));
$transactionId = trim((string) (
$payload['transaction_id']
?? $payload['transactionId']
?? ($consentObj['transactionId'] ?? '')
?? ''
));
$abhaAddress = trim((string) (
($consentObj['patient']['id'] ?? '')
?: ($payload['abha_address'] ?? '')
?: ($payload['abha_id'] ?? '')
));
$abhaAddress = $this->canonicalizeAbhaAddress($abhaAddress, (string) ($payload['abha_id'] ?? ''));
$persistPayload = [
'request_id' => $requestId,
'requestId' => $requestId,
'transaction_id' => $transactionId,
'transactionId' => $transactionId,
'consent_id' => $consentId,
'consentId' => $consentId,
'abdm_consent_artifact_id' => $consentId,
'abha_address' => $abhaAddress,
'hfr_id' => trim((string) ($payload['hfr_id'] ?? '')),
'consent' => $consentObj,
'callback' => $payload,
];
$result = [
'ok' => 1,
'http_code' => 202,
'status' => $status,
'consent_id' => $consentId,
'consentId' => $consentId,
'abdm_consent_artifact_id' => $consentId,
'request_id' => $requestId,
'transaction_id' => $transactionId,
'abha_address' => $abhaAddress,
'consent' => $consentObj,
'data' => $payload,
'retryable' => 0,
];
$state = match ($status) {
'granted', 'approved', 'active' => 'GRANTED',
'revoked', 'denied' => 'REVOKED',
'expired' => 'EXPIRED',
default => 'STATUS_CHECKED',
};
$this->upsertWorkflow('consent_callback', $persistPayload, $result, $state, 'success', 202, false);
return [
'ok' => 1,
'persisted' => 1,
'workflow_state' => $state,
'consent_id' => $consentId,
'request_id' => $requestId,
];
}
public function ingestHealthInformationCallback(array $payload, string $operation = 'hi_on_request_callback'): array
{
if (! $this->db->tableExists('abdm_hiu_workflows')) {
return [
'ok' => 1,
'persisted' => 0,
'message' => 'abdm_hiu_workflows table not found; callback accepted without persistence',
];
}
$requestId = trim((string) (
$payload['request_id']
?? $payload['requestId']
?? ($payload['response']['requestId'] ?? '')
?? ''
));
$transactionId = trim((string) (
$payload['transaction_id']
?? $payload['transactionId']
?? ($payload['response']['transactionId'] ?? '')
?? ''
));
$consentId = trim((string) (
($payload['hiRequest']['consent']['id'] ?? '')
?: ($payload['consent']['id'] ?? '')
?: ($payload['consent_id'] ?? '')
?: ($payload['consentId'] ?? '')
));
$abhaAddress = $this->canonicalizeAbhaAddress(trim((string) (
($payload['patient']['id'] ?? '')
?: ($payload['abha_address'] ?? '')
?: ($payload['abha_id'] ?? '')
)), (string) ($payload['abha_id'] ?? ''));
$errorCode = trim((string) ($payload['error']['code'] ?? ''));
$errorMessage = trim((string) ($payload['error']['message'] ?? ''));
$errorText = trim(($errorCode !== '' ? $errorCode . ' ' : '') . $errorMessage);
$hasData = isset($payload['fhir_bundle'])
|| isset($payload['fhirBundle'])
|| isset($payload['bundles'])
|| isset($payload['entries'])
|| isset($payload['data']);
$ok = $errorText === '' ? 1 : 0;
$state = $hasData ? 'DATA_RECEIVED' : ($ok === 1 ? 'DATA_PENDING' : 'FAILED');
$persistPayload = [
'request_id' => $requestId,
'requestId' => $requestId,
'transaction_id' => $transactionId,
'transactionId' => $transactionId,
'consent_id' => $consentId,
'consentId' => $consentId,
'abdm_consent_artifact_id' => $consentId,
'abha_address' => $abhaAddress,
'callback' => $payload,
];
$result = [
'ok' => $ok,
'http_code' => 202,
'status' => strtolower($state),
'error_text' => $errorText,
'request_id' => $requestId,
'transaction_id' => $transactionId,
'consent_id' => $consentId,
'abha_address' => $abhaAddress,
'retryable' => 0,
'data' => $payload,
];
$this->upsertWorkflow($operation, $persistPayload, $result, $state, $ok === 1 ? 'success' : 'failed', 202, false);
return [
'ok' => 1,
'persisted' => 1,
'workflow_state' => $state,
'request_id' => $requestId,
'transaction_id' => $transactionId,
'error_text' => $errorText,
];
}
private function dispatch(string $operation, array $payload): array
{
return match ($operation) {
@ -554,11 +751,14 @@ class M3HiuWorkflowService
if ($operation === 'consent_fetch') {
$consentId = trim((string) ($clean['abdm_consent_artifact_id'] ?? $clean['consentId'] ?? $clean['consent_id'] ?? ''));
if ($consentId === '') {
$consentId = $this->resolveAbdmConsentArtifactIdFromWorkflow($clean);
}
if ($consentId === '') {
return [
'ok' => 0,
'http_code' => 422,
'error_text' => 'consentId is required for consent fetch.',
'error_text' => 'consentId is required for consent fetch. Consent artifact is not available yet; run Check Consent Status until consent is GRANTED, then fetch using artifact id.',
];
}
if ($this->isGatewayRequestIdPattern($consentId)) {
@ -611,10 +811,11 @@ class M3HiuWorkflowService
if (! isset($hiRequest['dateRange']) || ! is_array($hiRequest['dateRange'])) {
$hiRequest['dateRange'] = [
'from' => gmdate('Y-m-d\T00:00:00.000\Z', strtotime('-30 days')),
'from' => gmdate('Y-m-d\T00:00:00.000\Z', strtotime('-365 days')),
'to' => gmdate('Y-m-d\T00:00:00.000\Z'),
];
}
$hiRequest['dateRange'] = $this->resolveSafeHiDateRange($requestIdRef, $consentId, $hiRequest['dateRange']);
if (trim((string) ($hiRequest['dataPushUrl'] ?? '')) === '') {
$hiRequest['dataPushUrl'] = rtrim((string) (getenv('EATRIA_BRIDGE_URL') ?: 'https://abdm-bridge.e-atria.in/api'), '/') . '/v3/hiu/data/push';
}
@ -743,7 +944,7 @@ class M3HiuWorkflowService
'permission' => [
'accessMode' => 'VIEW',
'dateRange' => [
'from' => gmdate('Y-m-d\T00:00:00.000\Z', strtotime('-30 days')),
'from' => gmdate('Y-m-d\T00:00:00.000\Z', strtotime('-365 days')),
'to' => gmdate('Y-m-d\T00:00:00.000\Z'),
],
'dataEraseAt' => gmdate('Y-m-d\T00:00:00.000\Z', strtotime('+365 days')),
@ -793,15 +994,257 @@ class M3HiuWorkflowService
'consent' => [
'id' => $consentId,
],
'dateRange' => [
'from' => gmdate('Y-m-d\T00:00:00.000\Z', strtotime('-30 days')),
'dateRange' => $this->resolveSafeHiDateRange($requestIdRef, $consentId, [
'from' => gmdate('Y-m-d\T00:00:00.000\Z', strtotime('-365 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,
];
}
/**
* Build a safe HI date range by enforcing consent-granted permission window
* (from consent fetch/reconcile details), then clamping with from <= to <= now.
*
* @param array<string, mixed> $fallbackRange
* @return array<string, string>
*/
private function resolveSafeHiDateRange(string $requestIdRef, string $consentId, array $fallbackRange): array
{
$grantedRange = $this->resolveGrantedConsentDateRange($requestIdRef, $consentId);
$consentRange = ! empty($grantedRange)
? $grantedRange
: $this->resolveConsentRequestDateRangeByRequestId($requestIdRef);
$fromRaw = trim((string) ($consentRange['from'] ?? $fallbackRange['from'] ?? ''));
$toRaw = trim((string) ($consentRange['to'] ?? $fallbackRange['to'] ?? ''));
$nowMs = (int) floor(microtime(true) * 1000);
$fromMs = $this->parseIsoUtcToEpochMillis($fromRaw);
$toMs = $this->parseIsoUtcToEpochMillis($toRaw);
$grantedFromMs = $this->parseIsoUtcToEpochMillis((string) ($grantedRange['from'] ?? ''));
$grantedToMs = $this->parseIsoUtcToEpochMillis((string) ($grantedRange['to'] ?? ''));
if ($fromMs === null) {
$fromMs = $nowMs - (365 * 24 * 60 * 60 * 1000);
}
if ($toMs === null) {
$toMs = $nowMs;
}
// HMS-side requirement: keep HI range equal to or strictly within granted consent permission range.
if ($grantedFromMs !== null && $fromMs < $grantedFromMs) {
$fromMs = $grantedFromMs;
}
if ($grantedToMs !== null && $toMs > $grantedToMs) {
$toMs = $grantedToMs;
}
if ($toMs > $nowMs) {
$toMs = $nowMs;
}
if ($fromMs > $toMs) {
if ($grantedFromMs !== null && $grantedFromMs <= $toMs) {
$fromMs = $grantedFromMs;
} else {
$fromMs = $toMs;
}
}
return [
'from' => $this->formatEpochMillisToIsoUtc($fromMs),
'to' => $this->formatEpochMillisToIsoUtc($toMs),
];
}
private function parseIsoUtcToEpochMillis(string $value): ?int
{
$raw = trim($value);
if ($raw === '') {
return null;
}
try {
$dt = new \DateTimeImmutable($raw);
} catch (\Throwable) {
return null;
}
$sec = (int) $dt->format('U');
$micro = (int) $dt->format('u');
return ($sec * 1000) + intdiv($micro, 1000);
}
private function formatEpochMillisToIsoUtc(int $epochMs): string
{
$sec = intdiv($epochMs, 1000);
$ms = $epochMs - ($sec * 1000);
if ($ms < 0) {
$ms = 0;
}
$dt = (new \DateTimeImmutable('@' . $sec))->setTimezone(new \DateTimeZone('UTC'));
return $dt->format('Y-m-d\TH:i:s') . '.' . str_pad((string) $ms, 3, '0', STR_PAD_LEFT) . 'Z';
}
/**
* @return array{from?: string, to?: string}
*/
private function resolveGrantedConsentDateRange(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('response_json, request_json')
->whereIn('operation', ['consent_fetch', 'consent_reconcile', 'consent_status', 'consent_callback']);
$hasFilter = false;
$builder->groupStart();
if ($requestId !== '') {
$builder->orWhere('request_id', $requestId);
$hasFilter = true;
}
if ($consentId !== '') {
$builder->orWhere('consent_id', $consentId);
$hasFilter = true;
if (in_array('abdm_consent_artifact_id', $fields, true)) {
$builder->orWhere('abdm_consent_artifact_id', $consentId);
}
}
$builder->groupEnd();
if (! $hasFilter) {
return [];
}
$rows = $builder->orderBy('id', 'DESC')->get(30)->getResultArray();
foreach ($rows as $row) {
foreach (['response_json', 'request_json'] as $jsonCol) {
$decoded = json_decode((string) ($row[$jsonCol] ?? ''), true);
if (! is_array($decoded)) {
continue;
}
$range = $this->extractPermissionDateRange($decoded);
if (! empty($range)) {
return $range;
}
}
}
return [];
}
/**
* @param array<string, mixed> $data
* @return array{from?: string, to?: string}
*/
private function extractPermissionDateRange(array $data): array
{
$pathCandidates = [
$data['consent']['permission']['dateRange'] ?? null,
$data['consentDetail']['permission']['dateRange'] ?? null,
$data['data']['consent']['permission']['dateRange'] ?? null,
$data['data']['consentDetail']['permission']['dateRange'] ?? null,
$data['response']['consent']['permission']['dateRange'] ?? null,
$data['response']['consentDetail']['permission']['dateRange'] ?? null,
];
foreach ($pathCandidates as $candidate) {
if (is_array($candidate)) {
$from = trim((string) ($candidate['from'] ?? ''));
$to = trim((string) ($candidate['to'] ?? ''));
if ($from !== '' || $to !== '') {
$out = [];
if ($from !== '') {
$out['from'] = $from;
}
if ($to !== '') {
$out['to'] = $to;
}
return $out;
}
}
}
if (isset($data['permission']) && is_array($data['permission'])) {
$range = $data['permission']['dateRange'] ?? null;
if (is_array($range)) {
$from = trim((string) ($range['from'] ?? ''));
$to = trim((string) ($range['to'] ?? ''));
if ($from !== '' || $to !== '') {
$out = [];
if ($from !== '') {
$out['from'] = $from;
}
if ($to !== '') {
$out['to'] = $to;
}
return $out;
}
}
}
foreach ($data as $value) {
if (! is_array($value)) {
continue;
}
$nested = $this->extractPermissionDateRange($value);
if (! empty($nested)) {
return $nested;
}
}
return [];
}
/**
* @return array{from?: string, to?: string}
*/
private function resolveConsentRequestDateRangeByRequestId(string $requestId): array
{
if ($requestId === '' || ! $this->db->tableExists('abdm_hiu_workflows')) {
return [];
}
$row = $this->db->table('abdm_hiu_workflows')
->select('request_json')
->where('operation', 'consent_request')
->where('request_id', $requestId)
->orderBy('id', 'DESC')
->get(1)
->getRowArray();
if (empty($row['request_json'])) {
return [];
}
$decoded = json_decode((string) $row['request_json'], true);
if (! is_array($decoded)) {
return [];
}
$from = trim((string) ($decoded['consent']['permission']['dateRange']['from'] ?? ''));
$to = trim((string) ($decoded['consent']['permission']['dateRange']['to'] ?? ''));
$out = [];
if ($from !== '') {
$out['from'] = $from;
}
if ($to !== '') {
$out['to'] = $to;
}
return $out;
}
private function readSettingValue(string $key): string
{
if (! $this->db->tableExists('hospital_setting')) {
@ -937,7 +1380,10 @@ class M3HiuWorkflowService
'consent_request' => 'REQUESTED',
'consent_status' => 'STATUS_CHECKED',
'consent_fetch' => 'CONSENT_FETCHED',
'consent_callback' => ($statusStr === 'granted' || $statusStr === 'approved' ? 'GRANTED' : ($statusStr === 'revoked' ? 'REVOKED' : ($statusStr === 'expired' ? 'EXPIRED' : 'STATUS_CHECKED'))),
'hi_request' => ($statusStr === 'completed' ? 'COMPLETED' : 'DATA_REQUESTED'),
'hi_on_request_callback' => (($statusStr === 'data_received' || isset($result['fhir_bundle']) || isset($result['bundles']) || isset($result['entries'])) ? 'DATA_RECEIVED' : ($ok ? 'DATA_PENDING' : 'FAILED')),
'hi_data_push_callback' => (($statusStr === 'data_received' || isset($result['fhir_bundle']) || isset($result['bundles']) || isset($result['entries'])) ? 'DATA_RECEIVED' : ($ok ? 'DATA_PENDING' : 'FAILED')),
'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',
@ -949,6 +1395,47 @@ class M3HiuWorkflowService
$requestId = trim((string) ($payload['request_id'] ?? ''));
$transactionId = trim((string) ($payload['transaction_id'] ?? ''));
// HI operations should dedupe by transaction id so retries/new requests
// keep separate timeline rows instead of overwriting prior transactions.
if (in_array($operation, ['hi_request', 'data_fetch'], true)) {
if ($transactionId === '') {
return null;
}
return $this->db->table('abdm_hiu_workflows')
->select('*')
->where('operation', $operation)
->where('transaction_id', $transactionId)
->orderBy('id', 'DESC')
->get(1)
->getRowArray() ?: null;
}
if (in_array($operation, ['hi_on_request_callback', 'hi_data_push_callback'], true)) {
$builder = $this->db->table('abdm_hiu_workflows')->select('*')->where('operation', $operation);
if ($transactionId !== '') {
$row = $builder->where('transaction_id', $transactionId)->orderBy('id', 'DESC')->get(1)->getRowArray();
if (! empty($row)) {
return $row;
}
}
if ($requestId !== '') {
$row = $this->db->table('abdm_hiu_workflows')
->select('*')
->where('operation', $operation)
->where('request_id', $requestId)
->orderBy('id', 'DESC')
->get(1)
->getRowArray();
if (! empty($row)) {
return $row;
}
}
return null;
}
$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();
@ -1089,21 +1576,67 @@ class M3HiuWorkflowService
int $forceId = 0
): void {
$now = Time::now('Asia/Kolkata')->toDateTimeString();
$hmsRequestId = trim((string) ($payload['request_id'] ?? $payload['requestId'] ?? ''));
$hmsRequestId = $this->firstNonEmptyString([
$payload['request_id'] ?? null,
$payload['requestId'] ?? null,
]);
if ($hmsRequestId === '') {
$hmsRequestId = trim((string) ($result['hms_request_id'] ?? ''));
$hmsRequestId = $this->firstNonEmptyString([
$result['hms_request_id'] ?? null,
]);
}
$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'] ?? ''));
$gatewayRequestId = $this->firstNonEmptyString([
$result['gateway_request_id'] ?? null,
$result['request_id'] ?? null,
$result['requestId'] ?? null,
]);
$abdmConsentRequestId = $this->firstNonEmptyString([
$result['abdm_consent_request_id'] ?? null,
$result['consent_request_id'] ?? null,
$payload['consentRequestId'] ?? null,
$payload['consent_request_id'] ?? null,
]);
$transactionId = $this->firstNonEmptyString([
$payload['transaction_id'] ?? null,
$payload['transactionId'] ?? null,
$result['transaction_id'] ?? null,
$result['transactionId'] ?? null,
]);
$consentId = $this->firstNonEmptyString([
$payload['abdm_consent_artifact_id'] ?? null,
$payload['consent_id'] ?? null,
$payload['consentId'] ?? null,
$payload['hiRequest']['consent']['id'] ?? null,
$result['abdm_consent_artifact_id'] ?? null,
$result['consent_id'] ?? null,
$result['consentId'] ?? null,
$result['hiRequest']['consent']['id'] ?? null,
]);
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'] ?? ''));
$abhaAddress = $this->firstNonEmptyString([
$payload['abha_address'] ?? null,
$payload['abha_id'] ?? null,
$result['abha_address'] ?? null,
]);
if ($abhaAddress === '') {
$abhaAddress = $this->firstNonEmptyString([
$payload['consent']['patient']['id'] ?? null,
]);
}
$hfrId = $this->normalizeStoredHfrId($this->firstNonEmptyString([
$payload['hfr_id'] ?? null,
$result['hfr_id'] ?? null,
]));
$hospitalId = $this->firstNonEmptyString([
$payload['hospital_id'] ?? null,
$result['hospital_id'] ?? null,
]);
$errorText = $this->firstNonEmptyString([
$result['error_text'] ?? null,
$result['message'] ?? null,
]);
$retryCount = 0;
$id = $forceId;
@ -1185,6 +1718,21 @@ class M3HiuWorkflowService
return preg_match('/^REQ-/i', trim($value)) === 1;
}
/**
* @param array<int, mixed> $values
*/
private function firstNonEmptyString(array $values): string
{
foreach ($values as $value) {
$text = trim((string) ($value ?? ''));
if ($text !== '') {
return $text;
}
}
return '';
}
private function resolveAbdmConsentRequestIdFromWorkflow(array $payload): string
{
if (! $this->db->tableExists('abdm_hiu_workflows')) {
@ -1262,4 +1810,82 @@ class M3HiuWorkflowService
return '';
}
private function resolveAbdmConsentArtifactIdFromWorkflow(array $payload): string
{
if (! $this->db->tableExists('abdm_hiu_workflows')) {
return '';
}
$fields = $this->db->getFieldNames('abdm_hiu_workflows') ?? [];
$hasAbdmConsentArtifactId = in_array('abdm_consent_artifact_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 ($hasAbdmConsentArtifactId): string {
if ($whereCol === null || $whereVal === '') {
return '';
}
$select = ['consent_id', 'response_json'];
if ($hasAbdmConsentArtifactId) {
$select[] = 'abdm_consent_artifact_id';
}
$rows = $this->db->table('abdm_hiu_workflows')
->select(implode(', ', $select))
->where($whereCol, $whereVal)
->whereIn('operation', ['consent_fetch', 'consent_reconcile', 'consent_status', 'hi_request'])
->orderBy('id', 'DESC')
->get(25)
->getResultArray();
foreach ($rows as $row) {
$candidate = '';
if ($hasAbdmConsentArtifactId) {
$candidate = trim((string) ($row['abdm_consent_artifact_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_artifact_id']
?? $decoded['consent_id']
?? $decoded['consentId']
?? ''
));
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 '';
}
}

File diff suppressed because it is too large Load Diff

View File

@ -30,12 +30,6 @@
<input class="form-control" id="appointment_time" name="appointment_time"
type="time" value="<?= date('H:i') ?>">
</div>
<div class="col-md-3">
<label class="form-label">ABHA Address</label>
<input class="form-control" id="abha_address" name="abha_address" type="text"
maxlength="18" value="<?= esc($person_info[0]->abha_address ?? $person_info[0]->abha ?? '') ?>"
placeholder="e.g. ravi1234 or ravi.1234">
</div>
</div>
<div class="row g-3 mt-2" id="ShowDoctor">
@ -95,7 +89,7 @@
$('#showfee').hide();
$('#btnnextfee').click(function() {
$('#btnnextfee').off('click.opdAppt').on('click.opdAppt', function() {
var checkValue = $('#rdoc_id:checked').val();
if (!checkValue) {
alert('Please Select Doctor Name');
@ -117,12 +111,12 @@
$('#ShowDoctor').hide();
});
$('#backtodoc').click(function() {
$('#backtodoc').off('click.opdAppt').on('click.opdAppt', function() {
$('#ShowDoctor').show();
$('#showfee').hide();
});
$('#btnnextconfirm').click(function() {
$('#btnnextconfirm').off('click.opdAppt').on('click.opdAppt', function() {
var checkValue = $('#fee_id:checked').val();
if (!checkValue) {
alert('Please Select Fee Name');
@ -138,7 +132,6 @@
"pid": $('#pid').val(),
"datepicker_appointment": $('#datepicker_appointment').val(),
"appointment_time": $('#appointment_time').val(),
"abha_address": $('#abha_address').val(),
[csrf.name]: csrf.value
}, function(data) {
updateCsrf(data);

View File

@ -255,12 +255,8 @@
delete_varible();
}
$("#wait").css("display", "none");
var mainEl = document.getElementById('main');
if (mainEl) {
mainEl.innerHTML = html;
} else {
$("#main").html(html);
}
// Use jQuery html() so old node handlers/data are cleaned up.
$("#main").html(html);
executeInjectedScripts('main');
if (typeof initfunc === 'function') {
initfunc();
@ -306,12 +302,8 @@
timeout: REQUEST_TIMEOUT_MS
})
.done(function(html) {
var targetEl = document.getElementById(xdiv);
if (targetEl) {
targetEl.innerHTML = html;
} else {
$("#" + xdiv).html(html);
}
// Use jQuery html() so old node handlers/data are cleaned up.
$("#" + xdiv).html(html);
executeInjectedScripts(xdiv);
if (typeof initfunc === 'function') {
initfunc();