492 lines
17 KiB
PHP
492 lines
17 KiB
PHP
<?php
|
|
|
|
namespace App\Libraries\Abdm;
|
|
|
|
class M3HiuGatewayClient
|
|
{
|
|
private \CodeIgniter\Database\BaseConnection $db;
|
|
private string $baseUrl;
|
|
private int $timeoutSec = 45;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->db = \Config\Database::connect();
|
|
$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');
|
|
}
|
|
|
|
public function consentRequestStatus(array $payload): array
|
|
{
|
|
return $this->call('/v3/hiu/consent/request/status', $payload, 'consent.request.status');
|
|
}
|
|
|
|
public function consentRequestFetch(array $payload): array
|
|
{
|
|
return $this->call('/v3/hiu/consent/request/fetch', $payload, 'consent.request.fetch');
|
|
}
|
|
|
|
public function healthInformationRequest(array $payload): array
|
|
{
|
|
return $this->call('/v1/hiu/data/request', $payload, 'health-information.request');
|
|
}
|
|
|
|
public function reconcileConsentStatus(array $payload): array
|
|
{
|
|
return $this->callGet('/v1/hiu/consent/status', $payload, 'consent.status.reconcile');
|
|
}
|
|
|
|
public function fetchDecryptedData(array $payload): array
|
|
{
|
|
return $this->callGet('/v1/hiu/data/fetch', $payload, 'data.fetch');
|
|
}
|
|
|
|
private function call(string $path, array $payload, string $eventKey): array
|
|
{
|
|
$settings = $this->resolveRuntimeSettings();
|
|
if (($settings['ok'] ?? 0) !== 1) {
|
|
return $settings;
|
|
}
|
|
|
|
$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) {
|
|
return [
|
|
'ok' => 0,
|
|
'http_code' => 400,
|
|
'error_text' => 'hfr_id mismatch: payload hfr_id does not match hospital_setting.ABDM_HFR_ID',
|
|
'retryable' => 0,
|
|
];
|
|
}
|
|
$payload['hfr_id'] = $hfrId;
|
|
|
|
$url = $this->baseUrl . $path;
|
|
$headers = [
|
|
'Content-Type: application/json',
|
|
'Accept: application/json',
|
|
'Authorization: Bearer ' . $token,
|
|
];
|
|
if ($hospitalId !== '') {
|
|
$headers[] = 'X-Hospital-Id: ' . $hospitalId;
|
|
}
|
|
|
|
$ch = curl_init();
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_URL => $url,
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_TIMEOUT => $this->timeoutSec,
|
|
CURLOPT_SSL_VERIFYPEER => true,
|
|
CURLOPT_HTTPHEADER => $headers,
|
|
CURLOPT_CUSTOMREQUEST => 'POST',
|
|
CURLOPT_POSTFIELDS => json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
|
]);
|
|
|
|
$raw = curl_exec($ch);
|
|
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
$curlErr = curl_error($ch);
|
|
curl_close($ch);
|
|
|
|
$decoded = json_decode((string) $raw, true);
|
|
$isJson = is_array($decoded);
|
|
$ok = $isJson ? (int) ($decoded['ok'] ?? ($httpCode >= 200 && $httpCode < 300 ? 1 : 0)) : (int) ($httpCode >= 200 && $httpCode < 300);
|
|
|
|
$errorText = '';
|
|
if ($curlErr !== '') {
|
|
$errorText = $curlErr;
|
|
} elseif ($ok !== 1) {
|
|
$errorText = $isJson
|
|
? (string) ($decoded['message'] ?? $decoded['error_text'] ?? $decoded['error'] ?? ('HTTP ' . $httpCode))
|
|
: ('HTTP ' . $httpCode . ' non-JSON response');
|
|
}
|
|
|
|
if ($this->isCloudFrontBlockedResponse($httpCode, $raw, $decoded ?? [])) {
|
|
$errorText = 'Gateway edge block (CloudFront 403 Request blocked). Retry later or ask gateway team to unblock the endpoint.';
|
|
}
|
|
|
|
$retryable = 0;
|
|
if ($curlErr !== '' || $httpCode >= 500 || $httpCode === 0 || str_contains(strtolower($errorText), 'timed out')) {
|
|
$retryable = 1;
|
|
}
|
|
|
|
$response = $isJson ? $decoded : ['raw' => (string) $raw];
|
|
$response['ok'] = $ok;
|
|
$response['http_code'] = $httpCode;
|
|
$response['error_text'] = $errorText;
|
|
$response['retryable'] = $retryable;
|
|
|
|
if ($this->isCloudFrontBlockedResponse($httpCode, $raw, $response)) {
|
|
$response['retryable'] = 1;
|
|
$response['error_text'] = 'Gateway edge block (CloudFront 403 Request blocked). Retry later or ask gateway team to unblock the endpoint.';
|
|
}
|
|
|
|
$gatewayRequestId = trim((string) ($response['request_id'] ?? $response['requestId'] ?? ''));
|
|
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 !== '' && $this->isGatewayRefId($abdmConsentRequestId)) {
|
|
$abdmConsentRequestId = '';
|
|
}
|
|
if ($abdmConsentRequestId !== '') {
|
|
$response['abdm_consent_request_id'] = $abdmConsentRequestId;
|
|
$response['consent_request_id'] = $abdmConsentRequestId;
|
|
}
|
|
|
|
$abdmConsentId = $this->findFirstValueByKeys($response, [
|
|
'consentId',
|
|
'consent_id',
|
|
]);
|
|
if ($abdmConsentId !== '' && $this->isGatewayRefId($abdmConsentId)) {
|
|
$abdmConsentId = '';
|
|
}
|
|
if ($abdmConsentId !== '') {
|
|
$response['consent_id'] = $abdmConsentId;
|
|
$response['abdm_consent_artifact_id'] = $abdmConsentId;
|
|
}
|
|
|
|
if (! isset($response['request_id'])) {
|
|
$response['request_id'] = $gatewayRequestId;
|
|
}
|
|
if (! isset($response['transaction_id'])) {
|
|
$response['transaction_id'] = (string) ($response['transactionId'] ?? $payload['transactionId'] ?? $payload['transaction_id'] ?? '');
|
|
}
|
|
|
|
$this->writeApiLog($eventKey, $path, $payload, $response, $httpCode, $ok, $errorText, $hospitalId);
|
|
|
|
return $response;
|
|
}
|
|
|
|
private function callGet(string $path, array $query, string $eventKey): array
|
|
{
|
|
$settings = $this->resolveRuntimeSettings();
|
|
if (($settings['ok'] ?? 0) !== 1) {
|
|
return $settings;
|
|
}
|
|
|
|
$token = (string) ($settings['token'] ?? '');
|
|
$hfrId = (string) ($settings['hfr_id'] ?? '');
|
|
$hospitalId = (string) ($settings['hospital_id'] ?? '');
|
|
$runtimeBaseUrl = trim((string) ($settings['base_url'] ?? ''));
|
|
if ($runtimeBaseUrl !== '') {
|
|
$this->baseUrl = rtrim($runtimeBaseUrl, '/');
|
|
}
|
|
|
|
$payloadHfrId = trim((string) ($query['hfr_id'] ?? ''));
|
|
if ($payloadHfrId !== '' && strcasecmp($payloadHfrId, $hfrId) !== 0) {
|
|
return [
|
|
'ok' => 0,
|
|
'http_code' => 400,
|
|
'error_text' => 'hfr_id mismatch: payload hfr_id does not match hospital_setting.ABDM_HFR_ID',
|
|
'retryable' => 0,
|
|
];
|
|
}
|
|
|
|
$headers = [
|
|
'Accept: application/json',
|
|
'Authorization: Bearer ' . $token,
|
|
];
|
|
if ($hospitalId !== '') {
|
|
$headers[] = 'X-Hospital-Id: ' . $hospitalId;
|
|
}
|
|
|
|
$cleanQuery = [];
|
|
foreach ($query as $k => $v) {
|
|
if ($v === null) {
|
|
continue;
|
|
}
|
|
$val = trim((string) $v);
|
|
if ($val === '') {
|
|
continue;
|
|
}
|
|
$cleanQuery[$k] = $val;
|
|
}
|
|
|
|
// Bridge polling APIs require hfr_id in query for auth scope validation.
|
|
if ($hfrId !== '') {
|
|
$cleanQuery['hfr_id'] = $hfrId;
|
|
}
|
|
|
|
$url = $this->baseUrl . $path;
|
|
if (! empty($cleanQuery)) {
|
|
$url .= '?' . http_build_query($cleanQuery);
|
|
}
|
|
|
|
$ch = curl_init();
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_URL => $url,
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_TIMEOUT => $this->timeoutSec,
|
|
CURLOPT_SSL_VERIFYPEER => true,
|
|
CURLOPT_HTTPHEADER => $headers,
|
|
CURLOPT_CUSTOMREQUEST => 'GET',
|
|
]);
|
|
|
|
$raw = curl_exec($ch);
|
|
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
$curlErr = curl_error($ch);
|
|
curl_close($ch);
|
|
|
|
$decoded = json_decode((string) $raw, true);
|
|
$isJson = is_array($decoded);
|
|
$ok = $isJson ? (int) ($decoded['ok'] ?? ($httpCode >= 200 && $httpCode < 300 ? 1 : 0)) : (int) ($httpCode >= 200 && $httpCode < 300);
|
|
|
|
$errorText = '';
|
|
if ($curlErr !== '') {
|
|
$errorText = $curlErr;
|
|
} elseif ($ok !== 1) {
|
|
$errorText = $isJson
|
|
? (string) ($decoded['message'] ?? $decoded['error_text'] ?? $decoded['error'] ?? ('HTTP ' . $httpCode))
|
|
: ('HTTP ' . $httpCode . ' non-JSON response');
|
|
}
|
|
|
|
if ($this->isCloudFrontBlockedResponse($httpCode, $raw, $decoded ?? [])) {
|
|
$errorText = 'Gateway edge block (CloudFront 403 Request blocked). Retry later or ask gateway team to unblock the endpoint.';
|
|
}
|
|
|
|
$retryable = 0;
|
|
if ($curlErr !== '' || $httpCode >= 500 || $httpCode === 0 || str_contains(strtolower($errorText), 'timed out')) {
|
|
$retryable = 1;
|
|
}
|
|
|
|
$response = $isJson ? $decoded : ['raw' => (string) $raw];
|
|
$response['ok'] = $ok;
|
|
$response['http_code'] = $httpCode;
|
|
$response['error_text'] = $errorText;
|
|
$response['retryable'] = $retryable;
|
|
|
|
if ($this->isCloudFrontBlockedResponse($httpCode, $raw, $response)) {
|
|
$response['retryable'] = 1;
|
|
$response['error_text'] = 'Gateway edge block (CloudFront 403 Request blocked). Retry later or ask gateway team to unblock the endpoint.';
|
|
}
|
|
|
|
$gatewayRequestId = trim((string) ($response['request_id'] ?? $response['requestId'] ?? $cleanQuery['request_id'] ?? ''));
|
|
$response['gateway_request_id'] = $gatewayRequestId;
|
|
|
|
$abdmConsentRequestId = $this->findFirstValueByKeys($response, [
|
|
'consentRequestId',
|
|
'consent_request_id',
|
|
'abdm_consent_request_id',
|
|
]);
|
|
if ($abdmConsentRequestId !== '' && ! $this->isGatewayRefId($abdmConsentRequestId)) {
|
|
$response['abdm_consent_request_id'] = $abdmConsentRequestId;
|
|
$response['consent_request_id'] = $abdmConsentRequestId;
|
|
}
|
|
|
|
$abdmConsentId = $this->findFirstValueByKeys($response, [
|
|
'consentId',
|
|
'consent_id',
|
|
'abdm_consent_artifact_id',
|
|
]);
|
|
if ($abdmConsentId !== '' && ! $this->isGatewayRefId($abdmConsentId)) {
|
|
$response['consent_id'] = $abdmConsentId;
|
|
$response['abdm_consent_artifact_id'] = $abdmConsentId;
|
|
}
|
|
|
|
if (! isset($response['transaction_id'])) {
|
|
$response['transaction_id'] = (string) ($response['transactionId'] ?? $cleanQuery['transaction_id'] ?? '');
|
|
}
|
|
|
|
$this->writeApiLog($eventKey, $path, $cleanQuery, $response, $httpCode, $ok, $errorText, $hospitalId, 'GET');
|
|
|
|
return $response;
|
|
}
|
|
|
|
private function resolveRuntimeSettings(): array
|
|
{
|
|
if (! $this->db->tableExists('hospital_setting')) {
|
|
return ['ok' => 0, 'http_code' => 500, 'error_text' => 'hospital_setting table not found', 'retryable' => 0];
|
|
}
|
|
|
|
$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',
|
|
'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 === '' || array_key_exists($key, $kv)) {
|
|
continue;
|
|
}
|
|
$kv[$key] = trim((string) ($row['s_value'] ?? ''));
|
|
}
|
|
|
|
$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 [
|
|
'ok' => 0,
|
|
'http_code' => 401,
|
|
'error_text' => 'Missing token: set hospital_setting.EATRIA_BRIDGE_TOKEN',
|
|
'retryable' => 0,
|
|
];
|
|
}
|
|
if ($hfrId === '') {
|
|
return [
|
|
'ok' => 0,
|
|
'http_code' => 400,
|
|
'error_text' => 'Missing hfr_id: set hospital_setting.ABDM_HFR_ID',
|
|
'retryable' => 0,
|
|
];
|
|
}
|
|
|
|
return [
|
|
'ok' => 1,
|
|
'token' => $token,
|
|
'hfr_id' => $hfrId,
|
|
'hospital_id' => $hospitalId,
|
|
'base_url' => $baseUrl,
|
|
];
|
|
}
|
|
|
|
private function writeApiLog(
|
|
string $eventKey,
|
|
string $path,
|
|
array $request,
|
|
array $response,
|
|
int $httpCode,
|
|
int $ok,
|
|
string $errorText,
|
|
string $hospitalId,
|
|
string $httpMethod = 'POST'
|
|
): void {
|
|
if (! $this->db->tableExists('abdm_api_logs')) {
|
|
return;
|
|
}
|
|
|
|
$status = $ok === 1 ? 'success' : 'error';
|
|
$entityId = trim((string) (
|
|
$request['consent_id']
|
|
?? $request['consentId']
|
|
?? $request['consentRequestId']
|
|
?? $request['request_id']
|
|
?? $request['requestId']
|
|
?? $request['transaction_id']
|
|
?? $request['transactionId']
|
|
?? ''
|
|
));
|
|
|
|
$this->db->table('abdm_api_logs')->insert([
|
|
'channel' => 'eatria_bridge',
|
|
'event_type' => 'abdm.m3.hiu.' . $eventKey,
|
|
'endpoint' => $this->baseUrl . $path,
|
|
'http_method' => strtoupper(trim($httpMethod)) !== '' ? strtoupper(trim($httpMethod)) : 'POST',
|
|
'entity_type' => 'abdm_hiu',
|
|
'entity_id' => $entityId,
|
|
'request_json' => (string) json_encode($request, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
|
'response_code' => $httpCode,
|
|
'response_json' => (string) json_encode($response, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
|
'status' => $status,
|
|
'error_message' => $errorText !== '' ? mb_substr($errorText, 0, 1000) : null,
|
|
'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 '';
|
|
}
|
|
|
|
private function isGatewayRefId(string $value): bool
|
|
{
|
|
return preg_match('/^REQ-/i', trim($value)) === 1;
|
|
}
|
|
|
|
private function isCloudFrontBlockedResponse(int $httpCode, $raw, array $response): bool
|
|
{
|
|
if ($httpCode !== 403) {
|
|
return false;
|
|
}
|
|
|
|
$text = strtolower((string) $raw);
|
|
if ($text === '' && isset($response['data']['raw'])) {
|
|
$text = strtolower((string) $response['data']['raw']);
|
|
}
|
|
|
|
return str_contains($text, 'generated by cloudfront')
|
|
|| str_contains($text, 'request blocked')
|
|
|| str_contains($text, 'the request could not be satisfied');
|
|
}
|
|
}
|