- Fix groupWorkflowRowsIntoSessions() to correlate workflow rows by stable request_id/abdm_consent_request_id instead of chronological proximity to the newest CONSENT_REQUEST row. Background polling reconciles all historical sessions in one batch, so older sessions' rows previously got silently merged into the newest session, causing 'Show Data' to display an unrelated older session's single facility instead of all consented facilities. - Skip synthetic orphan sessions (stray rows with no owning CONSENT_REQUEST) in getAbdmConsentRequestsList()/getAbdmConsentArtifactDetail() so they don't show as phantom history rows or get picked as 'most recent'. - Fix abdm_documents() consent_request_id filter to match all known sibling artifact IDs for a session (via abdm_hiu_consent_artifacts), not just an exact single artifact id, so all facilities under one consent request appear together. - Fix CreateAbdmHiuConsentArtifacts migration: reduce VARCHAR(180) to VARCHAR(100) on consent_request_id/artifact_id to satisfy MySQL's 1000-byte composite key length limit (was silently failing to migrate). - Add fetchAllArtifactsAfterGrant() cascade in M3HiuWorkflowService to fetch every known artifact under a granted consent, used by both the cron poll and a new abdm_check_live_status() endpoint/button. - getLatestAbdmSyncSnapshot() session-scoping fix to avoid an older granted session masking a newer in-progress one. - Relabel Dr. Joshi Clinic documents (previously mislabeled 'Test Hospital' from the HIP's own FHIR bundle Organization name).
1856 lines
72 KiB
PHP
1856 lines
72 KiB
PHP
<?php
|
|
|
|
namespace App\Libraries\Abdm;
|
|
|
|
use CodeIgniter\I18n\Time;
|
|
use App\Libraries\Abdm\M3HiuGatewayClient;
|
|
use App\Libraries\Abdm\M3HiuDocumentRepository;
|
|
|
|
class M3HiuWorkflowService
|
|
{
|
|
private \CodeIgniter\Database\BaseConnection $db;
|
|
private M3HiuGatewayClient $client;
|
|
private M3HiuDocumentRepository $documentRepository;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->db = \Config\Database::connect();
|
|
$this->client = new M3HiuGatewayClient();
|
|
$this->documentRepository = new M3HiuDocumentRepository();
|
|
}
|
|
|
|
public function runOperation(string $operation, array $payload): array
|
|
{
|
|
$hasWorkflowTable = $this->db->tableExists('abdm_hiu_workflows');
|
|
|
|
$normalized = $this->normalizePayload($payload);
|
|
$retiredError = $this->retiredOperationError($operation, $normalized);
|
|
if ($retiredError !== null) {
|
|
if ($hasWorkflowTable) {
|
|
$this->upsertWorkflow(
|
|
$operation,
|
|
$normalized,
|
|
$retiredError,
|
|
'RETIRED',
|
|
'failed',
|
|
(int) ($retiredError['http_code'] ?? 410),
|
|
false
|
|
);
|
|
$retiredError['workflow_persisted'] = 1;
|
|
}
|
|
|
|
return $retiredError;
|
|
}
|
|
|
|
$prepared = $this->preparePayloadForOperation($operation, $normalized);
|
|
if (($prepared['ok'] ?? 0) !== 1) {
|
|
$failedResult = [
|
|
'ok' => 0,
|
|
'http_code' => (int) ($prepared['http_code'] ?? 422),
|
|
'error_text' => (string) ($prepared['error_text'] ?? 'Validation failed'),
|
|
'retryable' => 0,
|
|
'request_id' => (string) ($normalized['request_id'] ?? ''),
|
|
'transaction_id' => (string) ($normalized['transaction_id'] ?? ''),
|
|
'consent_id' => (string) ($normalized['consent_id'] ?? ''),
|
|
'abha_address' => (string) ($normalized['abha_address'] ?? ''),
|
|
];
|
|
if ($hasWorkflowTable) {
|
|
$this->upsertWorkflow(
|
|
$operation,
|
|
$normalized,
|
|
$failedResult,
|
|
'FAILED',
|
|
'failed',
|
|
(int) $failedResult['http_code'],
|
|
false
|
|
);
|
|
$failedResult['workflow_persisted'] = 1;
|
|
}
|
|
|
|
return $failedResult;
|
|
}
|
|
$dispatchPayload = (array) ($prepared['payload'] ?? []);
|
|
|
|
if ($hasWorkflowTable && ! in_array($operation, ['consent_status', 'consent_reconcile', 'data_fetch'], true)) {
|
|
$duplicate = $this->findDuplicate($operation, $normalized);
|
|
if ($duplicate !== null) {
|
|
$dupResponse = json_decode((string) ($duplicate['response_json'] ?? ''), true);
|
|
if (is_array($dupResponse) && ! empty($dupResponse)) {
|
|
$dupResponse['duplicate'] = 1;
|
|
$dupResponse['workflow_state'] = (string) ($duplicate['workflow_state'] ?? '');
|
|
return $dupResponse;
|
|
}
|
|
}
|
|
}
|
|
|
|
$result = $this->dispatch($operation, $dispatchPayload);
|
|
$httpCode = (int) ($result['http_code'] ?? 0);
|
|
$ok = (int) ($result['ok'] ?? 0) === 1;
|
|
|
|
$state = $this->resolveState($operation, $result);
|
|
$status = $ok ? 'success' : 'failed';
|
|
$retryable = (int) ($result['retryable'] ?? 0) === 1;
|
|
|
|
$persistPayload = $dispatchPayload;
|
|
foreach ([
|
|
'request_id', 'requestId', 'transaction_id', 'transactionId',
|
|
'abha_address', 'consent_id', 'consentId',
|
|
'abdm_consent_request_id', 'abdm_consent_artifact_id', 'hfr_id',
|
|
] as $key) {
|
|
if (! array_key_exists($key, $persistPayload) && array_key_exists($key, $normalized)) {
|
|
$persistPayload[$key] = $normalized[$key];
|
|
}
|
|
}
|
|
|
|
if ($hasWorkflowTable) {
|
|
$workflowId = $this->upsertWorkflow($operation, $persistPayload, $result, $state, $status, $httpCode, $retryable);
|
|
$result['workflow_persisted'] = 1;
|
|
|
|
if ($operation === 'data_fetch' && $ok) {
|
|
$docPersist = $this->documentRepository->persistFromDataFetch($persistPayload, $result, $workflowId);
|
|
$result['documents_persisted'] = (int) ($docPersist['saved'] ?? 0);
|
|
$result['documents_updated'] = (int) ($docPersist['updated'] ?? 0);
|
|
}
|
|
} else {
|
|
$result['workflow_persisted'] = 0;
|
|
$result['workflow_warning'] = 'abdm_hiu_workflows table not found; operation executed without timeline persistence';
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
private function retiredOperationError(string $operation, array $payload): ?array
|
|
{
|
|
if (! in_array($operation, ['consent_fetch', 'hi_request'], true)) {
|
|
return null;
|
|
}
|
|
|
|
return [
|
|
'ok' => 0,
|
|
'http_code' => 410,
|
|
'error_text' => 'Operation ' . $operation . ' is retired. Use consent_reconcile and data_fetch polling only.',
|
|
'retryable' => 0,
|
|
'request_id' => (string) ($payload['request_id'] ?? ''),
|
|
'transaction_id' => (string) ($payload['transaction_id'] ?? ''),
|
|
'consent_id' => (string) ($payload['consent_id'] ?? ''),
|
|
'abha_address' => (string) ($payload['abha_address'] ?? ''),
|
|
'workflow_state' => 'RETIRED',
|
|
'replacement_ops' => ['consent_reconcile', 'data_fetch'],
|
|
];
|
|
}
|
|
|
|
public function retryDue(int $limit = 20): array
|
|
{
|
|
if (! $this->db->tableExists('abdm_hiu_workflows')) {
|
|
return ['processed' => 0, 'success' => 0, 'failed' => 0, 'skipped' => 0];
|
|
}
|
|
|
|
$rows = $this->db->table('abdm_hiu_workflows')
|
|
->select('*')
|
|
->where('status', 'failed')
|
|
->where('is_retryable', 1)
|
|
->where('retry_count <', 3)
|
|
->groupStart()
|
|
->where('next_retry_at IS NULL', null, false)
|
|
->orWhere('next_retry_at <=', Time::now('Asia/Kolkata')->toDateTimeString())
|
|
->groupEnd()
|
|
->orderBy('id', 'ASC')
|
|
->get($limit)
|
|
->getResultArray();
|
|
|
|
$summary = ['processed' => 0, 'success' => 0, 'failed' => 0, 'skipped' => 0];
|
|
|
|
foreach ($rows as $row) {
|
|
$summary['processed']++;
|
|
$operation = trim((string) ($row['operation'] ?? ''));
|
|
$payload = json_decode((string) ($row['request_json'] ?? ''), true);
|
|
if (! is_array($payload) || $operation === '') {
|
|
$summary['skipped']++;
|
|
continue;
|
|
}
|
|
|
|
if (in_array($operation, ['consent_fetch', 'hi_request'], true)) {
|
|
$summary['skipped']++;
|
|
continue;
|
|
}
|
|
|
|
$result = $this->dispatch($operation, $payload);
|
|
$ok = (int) ($result['ok'] ?? 0) === 1;
|
|
$state = $this->resolveState($operation, $result);
|
|
$status = $ok ? 'success' : 'failed';
|
|
$retryable = (int) ($result['retryable'] ?? 0) === 1;
|
|
|
|
$this->upsertWorkflow($operation, $payload, $result, $state, $status, (int) ($result['http_code'] ?? 0), $retryable, (int) ($row['id'] ?? 0));
|
|
|
|
if ($ok) {
|
|
$summary['success']++;
|
|
} else {
|
|
$summary['failed']++;
|
|
}
|
|
}
|
|
|
|
return $summary;
|
|
}
|
|
|
|
public function pollNatGateway(int $limit = 30): array
|
|
{
|
|
if (! $this->db->tableExists('abdm_hiu_workflows')) {
|
|
return ['processed' => 0, 'consent_updates' => 0, 'data_updates' => 0, 'failed' => 0, 'skipped' => 0];
|
|
}
|
|
|
|
$summary = ['processed' => 0, 'consent_updates' => 0, 'data_updates' => 0, 'failed' => 0, 'skipped' => 0];
|
|
$consentRows = $this->db->table('abdm_hiu_workflows')
|
|
->select('*')
|
|
->where('operation', 'consent_request')
|
|
->where('status', 'success')
|
|
->orderBy('id', 'DESC')
|
|
->get($limit)
|
|
->getResultArray();
|
|
|
|
$seenConsentRequests = [];
|
|
foreach ($consentRows as $row) {
|
|
$requestId = trim((string) ($row['request_id'] ?? ''));
|
|
if ($requestId === '' || isset($seenConsentRequests[$requestId])) {
|
|
$summary['skipped']++;
|
|
continue;
|
|
}
|
|
$seenConsentRequests[$requestId] = true;
|
|
$summary['processed']++;
|
|
|
|
$payload = [
|
|
'request_id' => $requestId,
|
|
'transaction_id' => trim((string) ($row['transaction_id'] ?? '')),
|
|
'consent_id' => trim((string) ($row['consent_id'] ?? '')),
|
|
'abdm_consent_request_id' => trim((string) ($row['abdm_consent_request_id'] ?? '')),
|
|
'abdm_consent_artifact_id' => trim((string) ($row['abdm_consent_artifact_id'] ?? '')),
|
|
'abha_address' => trim((string) ($row['abha_address'] ?? '')),
|
|
'hfr_id' => $this->normalizeStoredHfrId((string) ($row['hfr_id'] ?? '')),
|
|
];
|
|
|
|
$consentResult = $this->runOperation('consent_reconcile', $payload);
|
|
if ((int) ($consentResult['ok'] ?? 0) === 1) {
|
|
$summary['consent_updates']++;
|
|
|
|
$cascade = $this->fetchAllArtifactsAfterGrant($payload, $consentResult);
|
|
$summary['data_updates'] += $cascade['data_updates'];
|
|
$summary['failed'] += $cascade['failed'];
|
|
if (! $cascade['granted']) {
|
|
$summary['skipped']++;
|
|
}
|
|
} else {
|
|
$summary['failed']++;
|
|
}
|
|
}
|
|
|
|
return $summary;
|
|
}
|
|
|
|
/**
|
|
* Shared GRANTED-consent cascade: once a consent_reconcile call confirms
|
|
* GRANTED, this fetches decrypted data for EVERY known consent artifact
|
|
* under the umbrella consent_request_id -- not just one facility/HIP --
|
|
* since a single M3 HIU consent can produce one artifact PER linked HIP
|
|
* facility (verified 2026-07-30: a 4-facility consent only surfaced one
|
|
* facility's records when data_fetch was called for a single artifact id).
|
|
* Used by both the `abdm:hiu-poll` cron (pollNatGateway) and the
|
|
* patient-facing "Check Live Status" action so a manual click gets the
|
|
* same full multi-facility fetch the cron would eventually perform.
|
|
*
|
|
* @param array $payload Same shape as the consent_reconcile payload
|
|
* (request_id, transaction_id, consent_id,
|
|
* abdm_consent_request_id,
|
|
* abdm_consent_artifact_id, abha_address, hfr_id).
|
|
* @param array $consentResult The result already returned by
|
|
* runOperation('consent_reconcile', $payload).
|
|
*/
|
|
public function fetchAllArtifactsAfterGrant(array $payload, array $consentResult): array
|
|
{
|
|
$result = ['granted' => false, 'data_updates' => 0, 'failed' => 0, 'artifact_ids' => []];
|
|
|
|
$consentState = strtolower(trim((string) (
|
|
$consentResult['workflow_state']
|
|
?? $consentResult['status']
|
|
?? $consentResult['consent_status']
|
|
?? ''
|
|
)));
|
|
if ($consentState !== 'granted') {
|
|
return $result;
|
|
}
|
|
$result['granted'] = true;
|
|
|
|
// IMPORTANT: use the UMBRELLA consent_request_id here, not a single
|
|
// resolved consent artifact id. Multi-facility ABDM M3 consents
|
|
// produce ONE consent artifact PER linked HIP facility under the
|
|
// same consent_request_id. Per the bridge's own contract, calling
|
|
// GET /v1/hiu/data/fetch with the consent_request_id (instead of
|
|
// one specific artifact id) returns ALL sessions/artifacts merged
|
|
// in a single response (verified 2026-07-28: querying by
|
|
// consent_request_id surfaced a sibling artifact that had NEVER
|
|
// been seen by HMS before, alongside the originally-tracked one).
|
|
// Falls back to a single artifact id only if no request id is
|
|
// available (should not normally happen for a granted consent).
|
|
$resolvedConsentId = trim((string) (
|
|
$consentResult['abdm_consent_request_id']
|
|
?? $consentResult['consent_request_id']
|
|
?? $consentResult['abdm_consent_artifact_id']
|
|
?? $consentResult['consent_id']
|
|
?? ''
|
|
));
|
|
if ($resolvedConsentId === '' || $this->isGatewayRequestIdPattern($resolvedConsentId)) {
|
|
return $result;
|
|
}
|
|
|
|
$dataPayload = $payload;
|
|
$dataPayload['consent_id'] = $resolvedConsentId;
|
|
$dataPayload['consent_request_id'] = $resolvedConsentId;
|
|
$dataPayload['abdm_consent_artifact_id'] = $resolvedConsentId;
|
|
$dataPayload['abdm_consent_request_id'] = $resolvedConsentId;
|
|
|
|
$dataResult = $this->runOperation('data_fetch', $dataPayload);
|
|
if ((int) ($dataResult['ok'] ?? 0) === 1) {
|
|
$result['data_updates']++;
|
|
} else {
|
|
$result['failed']++;
|
|
}
|
|
|
|
// Merged-by-request-id fetch discovers NEW sibling artifacts, but
|
|
// does not reliably keep returning sessions for artifacts already
|
|
// established earlier (verified: an already-"received" artifact
|
|
// stopped appearing in the merged list while still returning real
|
|
// data when queried by its own artifact id). So remember every
|
|
// artifact id ever seen for this consent request, and re-poll each
|
|
// one individually every cycle too, for full coverage.
|
|
$singleArtifactId = trim((string) (
|
|
$consentResult['abdm_consent_artifact_id'] ?? ''
|
|
));
|
|
$fetchSessions = is_array($dataResult['sessions'] ?? null) ? $dataResult['sessions'] : [];
|
|
$this->recordDiscoveredArtifacts(
|
|
$resolvedConsentId,
|
|
$fetchSessions,
|
|
$payload['abha_address'] ?? '',
|
|
$payload['hfr_id'] ?? ''
|
|
);
|
|
if ($singleArtifactId !== '' && ! $this->isGatewayRequestIdPattern($singleArtifactId)) {
|
|
$this->rememberArtifact($resolvedConsentId, $singleArtifactId, $payload['abha_address'] ?? '', $payload['hfr_id'] ?? '');
|
|
}
|
|
|
|
foreach ($this->getKnownArtifactIds($resolvedConsentId) as $artifactId) {
|
|
if ($artifactId === '' || $artifactId === $resolvedConsentId) {
|
|
continue;
|
|
}
|
|
$result['artifact_ids'][] = $artifactId;
|
|
|
|
$artifactPayload = $payload;
|
|
$artifactPayload['consent_id'] = $artifactId;
|
|
$artifactPayload['consent_request_id'] = $artifactId;
|
|
$artifactPayload['abdm_consent_artifact_id'] = $artifactId;
|
|
$artifactPayload['abdm_consent_request_id'] = $artifactId;
|
|
|
|
$artifactResult = $this->runOperation('data_fetch', $artifactPayload);
|
|
if ((int) ($artifactResult['ok'] ?? 0) === 1) {
|
|
$result['data_updates']++;
|
|
$this->markArtifactPolled($resolvedConsentId, $artifactId, (string) ($artifactResult['consent_status'] ?? ''));
|
|
} else {
|
|
$result['failed']++;
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
|
|
/**
|
|
* Extracts and upserts every distinct artifact id found in a data_fetch
|
|
* result's sessions[] array (each session's consent_id is typically
|
|
* "artifactId:internalRequestId" or a bare artifact id) into the
|
|
* abdm_hiu_consent_artifacts registry, so future poll cycles keep
|
|
* checking each artifact individually.
|
|
*/
|
|
private function recordDiscoveredArtifacts(string $consentRequestId, array $sessions, string $abhaAddress, string $hfrId): void
|
|
{
|
|
foreach ($sessions as $session) {
|
|
if (! is_array($session)) {
|
|
continue;
|
|
}
|
|
$rawConsentId = trim((string) ($session['consent_id'] ?? ''));
|
|
if ($rawConsentId === '') {
|
|
continue;
|
|
}
|
|
$artifactId = strpos($rawConsentId, ':') !== false
|
|
? trim(explode(':', $rawConsentId)[0])
|
|
: $rawConsentId;
|
|
if ($artifactId === '' || $this->isGatewayRequestIdPattern($artifactId)) {
|
|
continue;
|
|
}
|
|
$this->rememberArtifact($consentRequestId, $artifactId, $abhaAddress, $hfrId, (string) ($session['status'] ?? ''));
|
|
}
|
|
}
|
|
|
|
private function rememberArtifact(string $consentRequestId, string $artifactId, string $abhaAddress, string $hfrId, string $status = ''): void
|
|
{
|
|
if (! $this->db->tableExists('abdm_hiu_consent_artifacts') || $consentRequestId === '' || $artifactId === '') {
|
|
return;
|
|
}
|
|
$now = Time::now('Asia/Kolkata')->toDateTimeString();
|
|
$existing = $this->db->table('abdm_hiu_consent_artifacts')
|
|
->where('consent_request_id', $consentRequestId)
|
|
->where('artifact_id', $artifactId)
|
|
->get(1)
|
|
->getRowArray();
|
|
|
|
if (! empty($existing)) {
|
|
$update = ['updated_at' => $now];
|
|
if ($status !== '') {
|
|
$update['last_status'] = $status;
|
|
}
|
|
$this->db->table('abdm_hiu_consent_artifacts')->where('id', (int) $existing['id'])->update($update);
|
|
return;
|
|
}
|
|
|
|
$this->db->table('abdm_hiu_consent_artifacts')->insert([
|
|
'consent_request_id' => $consentRequestId,
|
|
'artifact_id' => $artifactId,
|
|
'abha_address' => $abhaAddress !== '' ? $abhaAddress : null,
|
|
'hfr_id' => $hfrId !== '' ? $hfrId : null,
|
|
'last_status' => $status !== '' ? $status : null,
|
|
'created_at' => $now,
|
|
'updated_at' => $now,
|
|
]);
|
|
}
|
|
|
|
private function markArtifactPolled(string $consentRequestId, string $artifactId, string $status): void
|
|
{
|
|
if (! $this->db->tableExists('abdm_hiu_consent_artifacts')) {
|
|
return;
|
|
}
|
|
$now = Time::now('Asia/Kolkata')->toDateTimeString();
|
|
$update = ['last_polled_at' => $now, 'updated_at' => $now];
|
|
if ($status !== '') {
|
|
$update['last_status'] = $status;
|
|
}
|
|
$this->db->table('abdm_hiu_consent_artifacts')
|
|
->where('consent_request_id', $consentRequestId)
|
|
->where('artifact_id', $artifactId)
|
|
->update($update);
|
|
}
|
|
|
|
private function getKnownArtifactIds(string $consentRequestId): array
|
|
{
|
|
if (! $this->db->tableExists('abdm_hiu_consent_artifacts') || $consentRequestId === '') {
|
|
return [];
|
|
}
|
|
$rows = $this->db->table('abdm_hiu_consent_artifacts')
|
|
->select('artifact_id')
|
|
->where('consent_request_id', $consentRequestId)
|
|
->get()
|
|
->getResultArray();
|
|
|
|
return array_values(array_unique(array_map(
|
|
static fn (array $row): string => trim((string) ($row['artifact_id'] ?? '')),
|
|
$rows
|
|
)));
|
|
}
|
|
|
|
public function listTimeline(array $filters, int $limit = 200): array
|
|
{
|
|
if (! $this->db->tableExists('abdm_hiu_workflows')) {
|
|
return [];
|
|
}
|
|
|
|
$builder = $this->db->table('abdm_hiu_workflows')->select('*')->orderBy('id', 'DESC');
|
|
$fields = $this->db->getFieldNames('abdm_hiu_workflows') ?? [];
|
|
|
|
$hfrId = trim((string) ($filters['hfr_id'] ?? ''));
|
|
if ($hfrId !== '') {
|
|
$builder->where('hfr_id', $hfrId);
|
|
}
|
|
$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 !== '') {
|
|
$builder->where('transaction_id', $transactionId);
|
|
}
|
|
$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 !== '') {
|
|
$builder->where('created_at >=', $from . ' 00:00:00');
|
|
}
|
|
$to = trim((string) ($filters['date_to'] ?? ''));
|
|
if ($to !== '') {
|
|
$builder->where('created_at <=', $to . ' 23:59:59');
|
|
}
|
|
|
|
return $builder->get($limit)->getResultArray();
|
|
}
|
|
|
|
public function natPollSummary(int $lookbackMinutes = 180): array
|
|
{
|
|
if (! $this->db->tableExists('abdm_hiu_workflows')) {
|
|
return [
|
|
'ok' => 0,
|
|
'message' => 'abdm_hiu_workflows table not found',
|
|
'last_polled_at' => null,
|
|
'consent_updates' => 0,
|
|
'data_updates' => 0,
|
|
'failed' => 0,
|
|
'pending' => 0,
|
|
'recent_errors' => [],
|
|
];
|
|
}
|
|
|
|
$fromTime = date('Y-m-d H:i:s', strtotime('-' . max(1, $lookbackMinutes) . ' minutes'));
|
|
|
|
$lastRow = $this->db->table('abdm_hiu_workflows')
|
|
->select('updated_at')
|
|
->whereIn('operation', ['consent_reconcile', 'data_fetch'])
|
|
->orderBy('id', 'DESC')
|
|
->get(1)
|
|
->getRowArray();
|
|
|
|
$rows = $this->db->table('abdm_hiu_workflows')
|
|
->select('operation, status, workflow_state')
|
|
->whereIn('operation', ['consent_reconcile', 'data_fetch'])
|
|
->where('updated_at >=', $fromTime)
|
|
->get()
|
|
->getResultArray();
|
|
|
|
$errorRows = $this->db->table('abdm_hiu_workflows')
|
|
->select('updated_at, operation, request_id, transaction_id, last_error')
|
|
->whereIn('operation', ['consent_reconcile', 'data_fetch'])
|
|
->where('status', 'failed')
|
|
->where('updated_at >=', $fromTime)
|
|
->orderBy('updated_at', 'DESC')
|
|
->orderBy('id', 'DESC')
|
|
->get(8)
|
|
->getResultArray();
|
|
|
|
$consentUpdates = 0;
|
|
$dataUpdates = 0;
|
|
$failed = 0;
|
|
$pending = 0;
|
|
|
|
foreach ($rows as $row) {
|
|
$operation = trim((string) ($row['operation'] ?? ''));
|
|
$status = strtolower(trim((string) ($row['status'] ?? '')));
|
|
$state = strtolower(trim((string) ($row['workflow_state'] ?? '')));
|
|
|
|
if ($status === 'failed') {
|
|
$failed++;
|
|
continue;
|
|
}
|
|
|
|
if ($operation === 'consent_reconcile' && in_array($state, ['granted', 'status_checked'], true)) {
|
|
$consentUpdates++;
|
|
continue;
|
|
}
|
|
|
|
if ($operation === 'data_fetch' && in_array($state, ['data_received', 'completed'], true)) {
|
|
$dataUpdates++;
|
|
continue;
|
|
}
|
|
|
|
$pending++;
|
|
}
|
|
|
|
return [
|
|
'ok' => 1,
|
|
'last_polled_at' => $lastRow['updated_at'] ?? null,
|
|
'lookback_minutes' => $lookbackMinutes,
|
|
'consent_updates' => $consentUpdates,
|
|
'data_updates' => $dataUpdates,
|
|
'failed' => $failed,
|
|
'pending' => $pending,
|
|
'recent_errors' => $errorRows,
|
|
];
|
|
}
|
|
|
|
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) {
|
|
'consent_request' => $this->client->consentRequest($payload),
|
|
'consent_status' => $this->client->consentRequestStatus($payload),
|
|
'consent_reconcile' => $this->client->reconcileConsentStatus($payload),
|
|
'data_fetch' => $this->client->fetchDecryptedData($payload),
|
|
default => ['ok' => 0, 'http_code' => 400, 'error_text' => 'Unsupported operation: ' . $operation, 'retryable' => 0],
|
|
};
|
|
}
|
|
|
|
private function normalizePayload(array $payload): array
|
|
{
|
|
$normalized = $payload;
|
|
$generatedRequestId = false;
|
|
|
|
// Accept both legacy snake_case and guide-defined camelCase keys.
|
|
if (! isset($normalized['request_id']) && isset($normalized['requestId'])) {
|
|
$normalized['request_id'] = (string) $normalized['requestId'];
|
|
}
|
|
if (! isset($normalized['transaction_id']) && isset($normalized['transactionId'])) {
|
|
$normalized['transaction_id'] = (string) $normalized['transactionId'];
|
|
}
|
|
if (! isset($normalized['consent_id']) && isset($normalized['consentId'])) {
|
|
$normalized['consent_id'] = (string) $normalized['consentId'];
|
|
}
|
|
if (! isset($normalized['consent_id']) && isset($normalized['consentRequestId'])) {
|
|
$normalized['consent_id'] = (string) $normalized['consentRequestId'];
|
|
}
|
|
if (! isset($normalized['abdm_consent_request_id']) && isset($normalized['consentRequestId'])) {
|
|
$normalized['abdm_consent_request_id'] = (string) $normalized['consentRequestId'];
|
|
}
|
|
if (! isset($normalized['abdm_consent_artifact_id']) && isset($normalized['consentId'])) {
|
|
$normalized['abdm_consent_artifact_id'] = (string) $normalized['consentId'];
|
|
}
|
|
|
|
if (isset($normalized['abha_id']) && ! isset($normalized['abha_address'])) {
|
|
$normalized['abha_address'] = (string) $normalized['abha_id'];
|
|
}
|
|
|
|
foreach (['request_id', 'transaction_id', 'consent_id', 'abha_address', 'hfr_id'] as $k) {
|
|
if (! isset($normalized[$k])) {
|
|
$normalized[$k] = '';
|
|
}
|
|
$normalized[$k] = trim((string) $normalized[$k]);
|
|
}
|
|
|
|
$normalized['abha_address'] = $this->canonicalizeAbhaAddress(
|
|
(string) ($normalized['abha_address'] ?? ''),
|
|
(string) ($normalized['abha_id'] ?? '')
|
|
);
|
|
|
|
if ($normalized['request_id'] === '') {
|
|
$normalized['request_id'] = 'REQ-HIU-' . date('YmdHis') . '-' . strtoupper(bin2hex(random_bytes(3)));
|
|
$generatedRequestId = true;
|
|
}
|
|
|
|
if (! isset($normalized['requestId']) || trim((string) $normalized['requestId']) === '') {
|
|
$normalized['requestId'] = $normalized['request_id'];
|
|
}
|
|
if (! isset($normalized['timestamp']) || trim((string) $normalized['timestamp']) === '') {
|
|
$normalized['timestamp'] = gmdate('Y-m-d\TH:i:s.000\Z');
|
|
}
|
|
if ($normalized['transaction_id'] !== '' && (! isset($normalized['transactionId']) || trim((string) $normalized['transactionId']) === '')) {
|
|
$normalized['transactionId'] = $normalized['transaction_id'];
|
|
}
|
|
if ($normalized['consent_id'] !== '' && (! isset($normalized['consentId']) || trim((string) $normalized['consentId']) === '')) {
|
|
$normalized['consentId'] = $normalized['consent_id'];
|
|
}
|
|
|
|
$normalized['__generated_request_id'] = $generatedRequestId ? 1 : 0;
|
|
|
|
return $normalized;
|
|
}
|
|
|
|
private function preparePayloadForOperation(string $operation, array $normalized): array
|
|
{
|
|
$requestIdGenerated = (int) ($normalized['__generated_request_id'] ?? 0) === 1;
|
|
$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;
|
|
}
|
|
}
|
|
|
|
// Preserve requester.identifier for bridge deployments that validate strict requester schema.
|
|
$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;
|
|
}
|
|
|
|
$consent['hiTypes'] = $this->normalizeConsentHiTypes($consent['hiTypes'] ?? []);
|
|
if (empty($consent['hiTypes'])) {
|
|
return [
|
|
'ok' => 0,
|
|
'http_code' => 422,
|
|
'error_text' => 'consent.hiTypes is required for consent request.',
|
|
];
|
|
}
|
|
|
|
$consent = $this->clampConsentPermissionDateRangeToNow($consent);
|
|
|
|
return [
|
|
'ok' => 1,
|
|
'payload' => $base + ['consent' => $consent],
|
|
];
|
|
}
|
|
|
|
if ($operation === 'consent_status') {
|
|
$consentRequestId = trim((string) ($clean['abdm_consent_request_id'] ?? $clean['consentRequestId'] ?? $clean['consent_id'] ?? ''));
|
|
if ($consentRequestId === '') {
|
|
$consentRequestId = $this->resolveAbdmConsentRequestIdFromWorkflow($clean);
|
|
}
|
|
if ($consentRequestId === '') {
|
|
return [
|
|
'ok' => 0,
|
|
'http_code' => 422,
|
|
'error_text' => 'consentRequestId is required for consent status. Run consent request first or provide the ABDM consentRequestId from init response.',
|
|
];
|
|
}
|
|
if ($this->isGatewayRequestIdPattern($consentRequestId)) {
|
|
return [
|
|
'ok' => 0,
|
|
'http_code' => 422,
|
|
'error_text' => 'mapping_error: consentRequestId appears to be a local temporary request id; use consentRequestId returned by consent request/status.',
|
|
];
|
|
}
|
|
|
|
return [
|
|
'ok' => 1,
|
|
'payload' => $base + ['consentRequestId' => $consentRequestId],
|
|
];
|
|
}
|
|
|
|
if ($operation === 'consent_reconcile') {
|
|
$requestIdRef = trim((string) ($clean['request_id'] ?? $clean['requestId'] ?? ''));
|
|
if ($requestIdGenerated) {
|
|
$requestIdRef = '';
|
|
}
|
|
$consentRequestId = trim((string) ($clean['abdm_consent_request_id'] ?? $clean['consentRequestId'] ?? ''));
|
|
$consentId = trim((string) ($clean['abdm_consent_artifact_id'] ?? $clean['consentId'] ?? $clean['consent_id'] ?? ''));
|
|
|
|
if ($requestIdRef === '' && $consentRequestId === '' && $consentId === '') {
|
|
return [
|
|
'ok' => 0,
|
|
'http_code' => 422,
|
|
'error_text' => 'One of request_id, consent_request_id, or consent_id is required for consent reconciliation.',
|
|
];
|
|
}
|
|
|
|
$query = [];
|
|
if ($requestIdRef !== '') {
|
|
$query['request_id'] = $requestIdRef;
|
|
}
|
|
if ($consentRequestId !== '' && ! $this->isGatewayRequestIdPattern($consentRequestId)) {
|
|
$query['consent_request_id'] = $consentRequestId;
|
|
}
|
|
if ($consentId !== '' && ! $this->isGatewayRequestIdPattern($consentId)) {
|
|
$query['consent_id'] = $consentId;
|
|
}
|
|
|
|
return [
|
|
'ok' => 1,
|
|
'payload' => $query,
|
|
];
|
|
}
|
|
|
|
if ($operation === 'data_fetch') {
|
|
// Method 3 (per bridge gateway team, 2026-07-29): GET /v1/hiu/data/fetch
|
|
// ?abha_address=... returns ALL historical decrypted records across every
|
|
// past consent for the patient, instead of only the single consent/request
|
|
// targeted by Methods 1/2. Callers opt into this broader lookup explicitly
|
|
// (fetch_by_abha_address=1) since it is not scoped to one consent artifact.
|
|
$fetchByAbha = (int) ($clean['fetch_by_abha_address'] ?? 0) === 1;
|
|
if ($fetchByAbha) {
|
|
$abhaAddress = trim((string) ($clean['abha_address'] ?? ''));
|
|
if ($abhaAddress === '') {
|
|
return [
|
|
'ok' => 0,
|
|
'http_code' => 422,
|
|
'error_text' => 'abha_address is required to fetch all historical records by ABHA address.',
|
|
];
|
|
}
|
|
|
|
return [
|
|
'ok' => 1,
|
|
'payload' => ['abha_address' => $abhaAddress],
|
|
];
|
|
}
|
|
|
|
// Method 1 (per bridge gateway team, 2026-07-29): GET /v1/hiu/data/fetch
|
|
// ?request_id=... fetches data for the ONE specific consent request that
|
|
// request_id belongs to -- this is the "self"/current session's own data,
|
|
// distinct from Method 2 (consent_id, which can resolve to a different/
|
|
// stale session sharing the same consent artifact) and Method 3 (abha_address,
|
|
// which returns unrelated historical sessions across every past consent).
|
|
// Callers opt in explicitly via fetch_by_request_id=1.
|
|
$fetchByRequestId = (int) ($clean['fetch_by_request_id'] ?? 0) === 1;
|
|
if ($fetchByRequestId) {
|
|
$requestIdOnly = trim((string) (
|
|
$clean['abdm_consent_request_id']
|
|
?? $clean['consentRequestId']
|
|
?? $clean['consent_request_id']
|
|
?? ''
|
|
));
|
|
if ($requestIdOnly === '') {
|
|
return [
|
|
'ok' => 0,
|
|
'http_code' => 422,
|
|
'error_text' => 'consent_request_id is required to fetch data by request_id (Method 1).',
|
|
];
|
|
}
|
|
|
|
return [
|
|
'ok' => 1,
|
|
'payload' => ['request_id' => $requestIdOnly],
|
|
];
|
|
}
|
|
|
|
$requestFallback = '';
|
|
if (! $requestIdGenerated) {
|
|
$requestFallback = trim((string) (
|
|
$clean['request_id']
|
|
?? $clean['requestId']
|
|
?? ''
|
|
));
|
|
}
|
|
|
|
$consentId = trim((string) (
|
|
$clean['abdm_consent_artifact_id']
|
|
?? $clean['consentId']
|
|
?? $clean['consent_id']
|
|
?? $clean['abdm_consent_request_id']
|
|
?? $clean['consentRequestId']
|
|
?? $clean['consent_request_id']
|
|
?? $requestFallback
|
|
));
|
|
if ($consentId === '') {
|
|
$consentId = $this->resolveAbdmConsentArtifactIdFromWorkflow($clean);
|
|
}
|
|
if ($consentId === '') {
|
|
$consentId = $this->resolveAbdmConsentRequestIdFromWorkflow($clean);
|
|
}
|
|
|
|
if ($consentId === '') {
|
|
return [
|
|
'ok' => 0,
|
|
'http_code' => 422,
|
|
'error_text' => 'consent_id is required for decrypted data fetch polling. Use consent artifact id, consent request id, or original request_id after consent is GRANTED.',
|
|
];
|
|
}
|
|
|
|
$query = [];
|
|
$query['consent_id'] = $consentId;
|
|
|
|
return [
|
|
'ok' => 1,
|
|
'payload' => $query,
|
|
];
|
|
}
|
|
|
|
return [
|
|
'ok' => 0,
|
|
'http_code' => 400,
|
|
'error_text' => 'Unsupported operation: ' . $operation,
|
|
];
|
|
}
|
|
|
|
private function stripInternalNoise(array $payload): array
|
|
{
|
|
unset(
|
|
$payload['csrf_test_name'],
|
|
$payload['patient_id'],
|
|
$payload['abha_id'],
|
|
$payload['abhaId'],
|
|
$payload['__generated_request_id']
|
|
);
|
|
|
|
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'] ?? ''));
|
|
}
|
|
|
|
$istTz = new \DateTimeZone('Asia/Kolkata');
|
|
$utcTz = new \DateTimeZone('UTC');
|
|
$nowIst = new \DateTimeImmutable('now', $istTz);
|
|
$safeToUtc = $nowIst
|
|
->setTimezone($utcTz)
|
|
->modify('-120 seconds');
|
|
$fromUtc = $nowIst
|
|
->modify('-365 days')
|
|
->setTime(0, 0, 0)
|
|
->setTimezone($utcTz)
|
|
->format('Y-m-d\TH:i:s.000\Z');
|
|
$toUtc = $safeToUtc
|
|
->format('Y-m-d\TH:i:s.000\Z');
|
|
$eraseAtUtc = $nowIst
|
|
->modify('+365 days')
|
|
->setTime(23, 59, 59)
|
|
->setTimezone($utcTz)
|
|
->format('Y-m-d\TH:i:s.000\Z');
|
|
|
|
$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' => $fromUtc,
|
|
'to' => $toUtc,
|
|
],
|
|
'dataEraseAt' => $eraseAtUtc,
|
|
'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 clampConsentPermissionDateRangeToNow(array $consent): array
|
|
{
|
|
if (! isset($consent['permission']) || ! is_array($consent['permission'])) {
|
|
return $consent;
|
|
}
|
|
if (! isset($consent['permission']['dateRange']) || ! is_array($consent['permission']['dateRange'])) {
|
|
return $consent;
|
|
}
|
|
|
|
$toRaw = trim((string) ($consent['permission']['dateRange']['to'] ?? ''));
|
|
if ($toRaw === '') {
|
|
return $consent;
|
|
}
|
|
|
|
$safeNowUtc = (new \DateTimeImmutable('now', new \DateTimeZone('UTC')))
|
|
->modify('-120 seconds');
|
|
$safeNowIso = $safeNowUtc->format('Y-m-d\TH:i:s.000\Z');
|
|
|
|
// IMPORTANT: always refresh dateRange.to to the current moment at actual
|
|
// dispatch time, rather than only clamping it when it looks like it's in
|
|
// the future. A stale 'to' (e.g. from a re-dispatched/retried payload
|
|
// that reused an old consent block built hours/days earlier) would
|
|
// otherwise silently pass this check forever, permanently excluding any
|
|
// clinical records created after that stale cutoff (confirmed 2026-07-29:
|
|
// a retried consent_request sent dateRange.to a full day behind the true
|
|
// request time, per bridge team diagnostics). Always sending the freshest
|
|
// possible 'to' ensures HMS requests all records up to the exact minute
|
|
// of the request, per bridge API contract.
|
|
try {
|
|
new \DateTimeImmutable($toRaw); // validate shape only
|
|
} catch (\Throwable $e) {
|
|
// Fall through — invalid shape gets replaced below too.
|
|
}
|
|
$consent['permission']['dateRange']['to'] = $safeNowIso;
|
|
|
|
return $consent;
|
|
}
|
|
|
|
/**
|
|
* Normalize legacy/record hiTypes to HIU consent hiTypes accepted by bridge.
|
|
*
|
|
* @param mixed $hiTypes
|
|
* @return array<int, string>
|
|
*/
|
|
private function normalizeConsentHiTypes($hiTypes): array
|
|
{
|
|
if (is_string($hiTypes)) {
|
|
$hiTypes = [$hiTypes];
|
|
}
|
|
if (! is_array($hiTypes)) {
|
|
return [];
|
|
}
|
|
|
|
$map = [
|
|
'opconsultrecord' => 'OPConsultation',
|
|
'opconsultation' => 'OPConsultation',
|
|
'diagnosticreportrecord' => 'DiagnosticReport',
|
|
'diagnosticreport' => 'DiagnosticReport',
|
|
'prescriptionrecord' => 'Prescription',
|
|
'prescription' => 'Prescription',
|
|
'dischargesummaryrecord' => 'DischargeSummary',
|
|
'dischargesummary' => 'DischargeSummary',
|
|
'healthdocumentrecord' => 'HealthDocumentRecord',
|
|
'healthdocument' => 'HealthDocumentRecord',
|
|
'immunization' => 'ImmunizationRecord',
|
|
'immunizationrecord' => 'ImmunizationRecord',
|
|
'wellness' => 'WellnessRecord',
|
|
'wellnessrecord' => 'WellnessRecord',
|
|
'invoice' => 'Invoice',
|
|
'invoicerecord' => 'Invoice',
|
|
];
|
|
|
|
$normalized = [];
|
|
foreach ($hiTypes as $type) {
|
|
$raw = trim((string) $type);
|
|
if ($raw === '') {
|
|
continue;
|
|
}
|
|
|
|
$key = strtolower($raw);
|
|
$resolved = $map[$key] ?? $raw;
|
|
if (! in_array($resolved, $normalized, true)) {
|
|
$normalized[] = $resolved;
|
|
}
|
|
}
|
|
|
|
return $normalized;
|
|
}
|
|
|
|
private function readSettingValue(string $key): string
|
|
{
|
|
if (! $this->db->tableExists('hospital_setting')) {
|
|
return '';
|
|
}
|
|
|
|
$row = $this->db->table('hospital_setting')
|
|
->select('s_value')
|
|
->where('s_name', $key)
|
|
->orderBy('id', 'DESC')
|
|
->get(1)
|
|
->getRowArray();
|
|
|
|
return trim((string) ($row['s_value'] ?? ''));
|
|
}
|
|
|
|
private function resolveRequesterName(): string
|
|
{
|
|
foreach (['ABDM_HMS_NAME', 'H_Name', 'HOSPITAL_NAME'] as $key) {
|
|
$value = $this->readSettingValue($key);
|
|
if ($value !== '') {
|
|
return $value;
|
|
}
|
|
}
|
|
|
|
$envName = trim((string) (getenv('HOSPITAL_NAME') ?: getenv('app.name') ?: ''));
|
|
return $envName;
|
|
}
|
|
|
|
private function resolveHospitalHfrId(): string
|
|
{
|
|
$value = $this->readSettingValue('ABDM_HFR_ID');
|
|
if ($value !== '') {
|
|
return $value;
|
|
}
|
|
|
|
return trim((string) (getenv('ABDM_HFR_ID') ?: ''));
|
|
}
|
|
|
|
private function normalizeStoredHfrId(string $value): string
|
|
{
|
|
$candidate = trim($value);
|
|
if ($candidate === '' || strcasecmp($candidate, 'UNKNOWN') === 0) {
|
|
return $this->resolveHospitalHfrId();
|
|
}
|
|
|
|
return $candidate;
|
|
}
|
|
|
|
private function resolveHiuServiceId(): string
|
|
{
|
|
if (! $this->db->tableExists('hospital_setting')) {
|
|
$envHiu = trim((string) (getenv('ABDM_HIU_ID') ?: ''));
|
|
if ($envHiu !== '') {
|
|
return $envHiu;
|
|
}
|
|
return trim((string) (getenv('ABDM_HFR_ID') ?: ''));
|
|
}
|
|
|
|
$row = $this->db->table('hospital_setting')
|
|
->select('s_value')
|
|
->whereIn('s_name', ['ABDM_HIU_ID', 'EATRIA_HIU_ID'])
|
|
->orderBy('id', 'DESC')
|
|
->get(1)
|
|
->getRowArray();
|
|
|
|
$value = trim((string) ($row['s_value'] ?? ''));
|
|
if ($value !== '') {
|
|
return $value;
|
|
}
|
|
|
|
$hfr = $this->readSettingValue('ABDM_HFR_ID');
|
|
if ($hfr !== '') {
|
|
return $hfr;
|
|
}
|
|
|
|
$envHiu = trim((string) (getenv('ABDM_HIU_ID') ?: ''));
|
|
if ($envHiu !== '') {
|
|
return $envHiu;
|
|
}
|
|
|
|
return trim((string) (getenv('ABDM_HFR_ID') ?: ''));
|
|
}
|
|
|
|
private function canonicalizeAbhaAddress(string $abhaAddress, string $abhaId): string
|
|
{
|
|
$candidate = trim($abhaAddress);
|
|
if ($candidate !== '' && $this->isValidAbhaAddress($candidate)) {
|
|
return $candidate;
|
|
}
|
|
|
|
if ($candidate !== '') {
|
|
$digits = preg_replace('/\D+/', '', $candidate) ?? '';
|
|
if (preg_match('/^\d{14}$/', $digits) === 1) {
|
|
$domain = trim((string) (getenv('ABDM_ABHA_ADDRESS_DEFAULT_DOMAIN') ?: 'sbx'));
|
|
return $digits . '@' . $domain;
|
|
}
|
|
}
|
|
|
|
$abhaDigits = preg_replace('/\D+/', '', trim($abhaId)) ?? '';
|
|
if (preg_match('/^\d{14}$/', $abhaDigits) === 1) {
|
|
$domain = trim((string) (getenv('ABDM_ABHA_ADDRESS_DEFAULT_DOMAIN') ?: 'sbx'));
|
|
return $abhaDigits . '@' . $domain;
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
private function isValidAbhaAddress(string $value): bool
|
|
{
|
|
return preg_match('/^[A-Za-z0-9._-]+@[A-Za-z0-9.-]+$/', trim($value)) === 1;
|
|
}
|
|
|
|
private function resolveState(string $operation, array $result): string
|
|
{
|
|
$ok = (int) ($result['ok'] ?? 0) === 1;
|
|
if (! $ok) {
|
|
if (str_contains(strtolower((string) ($result['error_text'] ?? '')), 'mapping_error')) {
|
|
return 'MAPPING_ERROR';
|
|
}
|
|
return 'FAILED';
|
|
}
|
|
|
|
$statusStr = strtolower(trim((string) ($result['status'] ?? $result['consent_status'] ?? '')));
|
|
if (in_array($statusStr, ['revoked', 'denied'], true)) {
|
|
return 'REVOKED';
|
|
}
|
|
if (in_array($statusStr, ['expired'], true)) {
|
|
return 'EXPIRED';
|
|
}
|
|
|
|
return match ($operation) {
|
|
'consent_request' => 'REQUESTED',
|
|
'consent_status' => 'STATUS_CHECKED',
|
|
'consent_fetch' => 'CONSENT_FETCHED',
|
|
'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',
|
|
};
|
|
}
|
|
|
|
private function findDuplicate(string $operation, array $payload): ?array
|
|
{
|
|
$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();
|
|
if (! empty($row)) {
|
|
return $row;
|
|
}
|
|
}
|
|
|
|
if ($transactionId !== '') {
|
|
$row = $this->db->table('abdm_hiu_workflows')
|
|
->select('*')
|
|
->where('operation', $operation)
|
|
->where('transaction_id', $transactionId)
|
|
->orderBy('id', 'DESC')
|
|
->get(1)
|
|
->getRowArray();
|
|
if (! empty($row)) {
|
|
return $row;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function generateUuidV4(): string
|
|
{
|
|
$bytes = random_bytes(16);
|
|
$bytes[6] = chr((ord($bytes[6]) & 0x0f) | 0x40);
|
|
$bytes[8] = chr((ord($bytes[8]) & 0x3f) | 0x80);
|
|
|
|
return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex($bytes), 4));
|
|
}
|
|
|
|
private function upsertWorkflow(
|
|
string $operation,
|
|
array $payload,
|
|
array $result,
|
|
string $state,
|
|
string $status,
|
|
int $httpCode,
|
|
bool $retryable,
|
|
int $forceId = 0
|
|
): int {
|
|
$now = Time::now('Asia/Kolkata')->toDateTimeString();
|
|
$hmsRequestId = $this->firstNonEmptyString([
|
|
$payload['request_id'] ?? null,
|
|
$payload['requestId'] ?? null,
|
|
]);
|
|
if ($hmsRequestId === '') {
|
|
$hmsRequestId = $this->firstNonEmptyString([
|
|
$result['hms_request_id'] ?? null,
|
|
]);
|
|
}
|
|
$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 = $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;
|
|
if ($id <= 0) {
|
|
$existing = $this->findDuplicate($operation, ['request_id' => $hmsRequestId, 'transaction_id' => $transactionId]);
|
|
if (! empty($existing)) {
|
|
$id = (int) ($existing['id'] ?? 0);
|
|
$retryCount = (int) ($existing['retry_count'] ?? 0);
|
|
}
|
|
} else {
|
|
$existing = $this->db->table('abdm_hiu_workflows')->where('id', $id)->get(1)->getRowArray();
|
|
$retryCount = (int) ($existing['retry_count'] ?? 0);
|
|
}
|
|
|
|
if ($status === 'failed' && $retryable) {
|
|
$retryCount++;
|
|
}
|
|
|
|
$nextRetryAt = null;
|
|
if ($status === 'failed' && $retryable && $retryCount < 4) {
|
|
$backoffMinutes = min(30, 1 * (2 ** max(0, $retryCount - 1)));
|
|
$nextRetryAt = date('Y-m-d H:i:s', strtotime('+' . $backoffMinutes . ' minutes'));
|
|
}
|
|
|
|
$row = [
|
|
'hospital_id' => $hospitalId !== '' ? $hospitalId : null,
|
|
'hfr_id' => $hfrId !== '' ? $hfrId : null,
|
|
'abha_address' => $abhaAddress !== '' ? $abhaAddress : null,
|
|
'consent_id' => $consentId !== '' ? $consentId : null,
|
|
'request_id' => $hmsRequestId !== '' ? $hmsRequestId : null,
|
|
'transaction_id' => $transactionId !== '' ? $transactionId : null,
|
|
'operation' => $operation,
|
|
'workflow_state' => $state,
|
|
'status' => $status,
|
|
'http_code' => $httpCode > 0 ? $httpCode : null,
|
|
'is_retryable' => $retryable ? 1 : 0,
|
|
'retry_count' => $retryCount,
|
|
'next_retry_at' => $nextRetryAt,
|
|
'last_error' => $errorText !== '' ? mb_substr($errorText, 0, 2000) : null,
|
|
'request_json' => (string) json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
|
'response_json' => (string) json_encode($result, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
|
'updated_at' => $now,
|
|
];
|
|
|
|
if (in_array('hms_request_id', $this->db->getFieldNames('abdm_hiu_workflows') ?? [], true)) {
|
|
$row['hms_request_id'] = $hmsRequestId !== '' ? $hmsRequestId : null;
|
|
}
|
|
if (in_array('gateway_request_id', $this->db->getFieldNames('abdm_hiu_workflows') ?? [], true)) {
|
|
$row['gateway_request_id'] = $gatewayRequestId !== '' ? $gatewayRequestId : null;
|
|
}
|
|
if (in_array('abdm_consent_request_id', $this->db->getFieldNames('abdm_hiu_workflows') ?? [], true)) {
|
|
$row['abdm_consent_request_id'] = $abdmConsentRequestId !== '' ? $abdmConsentRequestId : null;
|
|
}
|
|
if (in_array('abdm_consent_artifact_id', $this->db->getFieldNames('abdm_hiu_workflows') ?? [], true)) {
|
|
$row['abdm_consent_artifact_id'] = $consentId !== '' ? $consentId : null;
|
|
}
|
|
|
|
if (in_array($state, ['COMPLETED', 'CONSENT_FETCHED'], true)) {
|
|
$row['completed_at'] = $now;
|
|
}
|
|
if ($state === 'EXPIRED') {
|
|
$row['expired_at'] = $now;
|
|
}
|
|
if ($state === 'REVOKED') {
|
|
$row['revoked_at'] = $now;
|
|
}
|
|
|
|
if ($id > 0) {
|
|
$this->db->table('abdm_hiu_workflows')->where('id', $id)->update($row);
|
|
return $id;
|
|
}
|
|
|
|
$row['created_at'] = $now;
|
|
$this->db->table('abdm_hiu_workflows')->insert($row);
|
|
return (int) ($this->db->insertID() ?: 0);
|
|
}
|
|
|
|
private function isGatewayRequestIdPattern(string $value): bool
|
|
{
|
|
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')) {
|
|
return '';
|
|
}
|
|
|
|
$fields = $this->db->getFieldNames('abdm_hiu_workflows') ?? [];
|
|
$hasAbdmConsentRequestId = in_array('abdm_consent_request_id', $fields, true);
|
|
|
|
$requestId = trim((string) ($payload['requestId'] ?? $payload['request_id'] ?? ''));
|
|
$transactionId = trim((string) ($payload['transactionId'] ?? $payload['transaction_id'] ?? ''));
|
|
$abhaAddress = trim((string) ($payload['abha_address'] ?? $payload['abha_id'] ?? ''));
|
|
|
|
$tryBuilder = function (?string $whereCol, string $whereVal) use ($hasAbdmConsentRequestId): string {
|
|
if ($whereCol === null || $whereVal === '') {
|
|
return '';
|
|
}
|
|
|
|
$select = ['consent_id', 'response_json'];
|
|
if ($hasAbdmConsentRequestId) {
|
|
$select[] = 'abdm_consent_request_id';
|
|
}
|
|
|
|
$rows = $this->db->table('abdm_hiu_workflows')
|
|
->select(implode(', ', $select))
|
|
->where($whereCol, $whereVal)
|
|
->whereIn('operation', ['consent_request', 'consent_status'])
|
|
->orderBy('id', 'DESC')
|
|
->get(25)
|
|
->getResultArray();
|
|
|
|
foreach ($rows as $row) {
|
|
$candidate = '';
|
|
if ($hasAbdmConsentRequestId) {
|
|
$candidate = trim((string) ($row['abdm_consent_request_id'] ?? ''));
|
|
}
|
|
if ($candidate === '') {
|
|
$candidate = trim((string) ($row['consent_id'] ?? ''));
|
|
}
|
|
if ($candidate !== '' && ! $this->isGatewayRequestIdPattern($candidate)) {
|
|
return $candidate;
|
|
}
|
|
|
|
$decoded = json_decode((string) ($row['response_json'] ?? ''), true);
|
|
if (is_array($decoded)) {
|
|
$candidate = trim((string) (
|
|
$decoded['abdm_consent_request_id']
|
|
?? $decoded['consent_request_id']
|
|
?? $decoded['consentRequestId']
|
|
?? ''
|
|
));
|
|
if ($candidate !== '' && ! $this->isGatewayRequestIdPattern($candidate)) {
|
|
return $candidate;
|
|
}
|
|
}
|
|
}
|
|
|
|
return '';
|
|
};
|
|
|
|
$resolved = $tryBuilder('request_id', $requestId);
|
|
if ($resolved !== '') {
|
|
return $resolved;
|
|
}
|
|
|
|
$resolved = $tryBuilder('transaction_id', $transactionId);
|
|
if ($resolved !== '') {
|
|
return $resolved;
|
|
}
|
|
|
|
$resolved = $tryBuilder('abha_address', $abhaAddress);
|
|
if ($resolved !== '') {
|
|
return $resolved;
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
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 '';
|
|
}
|
|
}
|