diff --git a/app/Controllers/AbdmGateway.php b/app/Controllers/AbdmGateway.php
index 2080f94..da43343 100644
--- a/app/Controllers/AbdmGateway.php
+++ b/app/Controllers/AbdmGateway.php
@@ -5296,6 +5296,91 @@ class AbdmGateway extends BaseController
$db = \Config\Database::connect();
$records = [];
+ // Parse granted HI types and date range filters (from ABDM consent)
+ $rawHiTypes = $body['hiTypes'] ?? $body['hi_types'] ?? null;
+ if (is_string($rawHiTypes)) {
+ $rawHiTypes = [$rawHiTypes];
+ }
+ $grantedHiTypes = null;
+ $normMap = [
+ 'opconsultrecord' => 'OPConsultation',
+ 'opconsultation' => 'OPConsultation',
+ 'diagnosticreportrecord' => 'DiagnosticReport',
+ 'diagnosticreport' => 'DiagnosticReport',
+ 'prescriptionrecord' => 'Prescription',
+ 'prescription' => 'Prescription',
+ 'dischargesummaryrecord' => 'DischargeSummary',
+ 'dischargesummary' => 'DischargeSummary',
+ 'healthdocumentrecord' => 'HealthDocumentRecord',
+ 'healthdocument' => 'HealthDocumentRecord',
+ 'immunizationrecord' => 'ImmunizationRecord',
+ 'immunization' => 'ImmunizationRecord',
+ 'wellnessrecord' => 'WellnessRecord',
+ 'wellness' => 'WellnessRecord',
+ 'invoicerecord' => 'Invoice',
+ 'invoice' => 'Invoice',
+ ];
+ if (is_array($rawHiTypes) && ! empty($rawHiTypes)) {
+ $grantedHiTypes = [];
+ foreach ($rawHiTypes as $ht) {
+ $k = strtolower(trim((string) $ht));
+ $grantedHiTypes[] = $normMap[$k] ?? trim((string) $ht);
+ }
+ }
+
+ $dateRange = $body['dateRange'] ?? $body['date_range'] ?? null;
+ $dateFrom = null;
+ $dateTo = null;
+ if (is_array($dateRange)) {
+ if (! empty($dateRange['from'])) {
+ $dateFrom = substr(trim((string) $dateRange['from']), 0, 10);
+ }
+ if (! empty($dateRange['to'])) {
+ $dateTo = substr(trim((string) $dateRange['to']), 0, 10);
+ }
+ }
+
+ $isTypeAllowed = static function (string $type) use ($grantedHiTypes, $normMap): bool {
+ if ($grantedHiTypes === null || empty($grantedHiTypes)) {
+ return true;
+ }
+ $std = $normMap[strtolower(trim($type))] ?? trim($type);
+ return in_array($std, $grantedHiTypes, true);
+ };
+
+ $isDateInRange = static function (?string $dateStr) use ($dateFrom, $dateTo): bool {
+ if ($dateStr === null || trim($dateStr) === '') {
+ return true;
+ }
+ $d = substr(trim($dateStr), 0, 10);
+ if ($dateFrom !== null && $d < $dateFrom) {
+ return false;
+ }
+ if ($dateTo !== null && $d > $dateTo) {
+ return false;
+ }
+ return true;
+ };
+
+ $extractBundleDate = static function (array $bundle): ?string {
+ if (! empty($bundle['timestamp'])) {
+ return substr((string) $bundle['timestamp'], 0, 10);
+ }
+ foreach ($bundle['entry'] ?? [] as $e) {
+ $res = $e['resource'] ?? [];
+ if (! empty($res['date'])) {
+ return substr((string) $res['date'], 0, 10);
+ }
+ if (! empty($res['effectiveDateTime'])) {
+ return substr((string) $res['effectiveDateTime'], 0, 10);
+ }
+ if (! empty($res['authoredOn'])) {
+ return substr((string) $res['authoredOn'], 0, 10);
+ }
+ }
+ return null;
+ };
+
foreach ($careContextRefs as $ref) {
$ref = trim((string) $ref);
if ($ref === '') {
@@ -5304,9 +5389,16 @@ class AbdmGateway extends BaseController
// Strategy 0a: Live On-Demand Fresh Generation for LAB / Diagnostic Report (LAB-xxxx or RAD-xxxx)
if (preg_match('/^(?:LAB|RAD)-/i', $ref)) {
+ if (! $isTypeAllowed('DiagnosticReport')) {
+ continue;
+ }
try {
$diag = $this->buildDiagnosticReportBundle($ref);
- if ($diag !== null && !empty($diag['bundle'])) {
+ if ($diag !== null && ! empty($diag['bundle'])) {
+ $bDate = $extractBundleDate($diag['bundle']);
+ if (! $isDateInRange($bDate)) {
+ continue;
+ }
$records[] = [
'careContextReference' => $ref,
'hiType' => $diag['hiType'] ?? 'DiagnosticReportRecord',
@@ -5323,9 +5415,16 @@ class AbdmGateway extends BaseController
// Strategy 0b: Live On-Demand Fresh Generation for Discharge Summary (DISCHARGE-xxxx or IPD-xxxx)
if (preg_match('/^(?:DISCHARGE|IPD)-/i', $ref)) {
+ if (! $isTypeAllowed('DischargeSummary')) {
+ continue;
+ }
try {
$discharge = $this->buildDischargeSummaryBundle($ref);
- if ($discharge !== null && !empty($discharge['bundle'])) {
+ if ($discharge !== null && ! empty($discharge['bundle'])) {
+ $bDate = $extractBundleDate($discharge['bundle']);
+ if (! $isDateInRange($bDate)) {
+ continue;
+ }
$records[] = [
'careContextReference' => $ref,
'hiType' => $discharge['hiType'] ?? 'DischargeSummaryRecord',
@@ -5342,9 +5441,19 @@ class AbdmGateway extends BaseController
// Strategy 0c: Live On-Demand Fresh Generation for OPD Consult & Prescription Records
if (preg_match('/^(OPD|PRESCRIPTION|PRESC|RX)-(\d+)(?:-S(\d+))?/i', $ref, $m)) {
+ $refPrefix = strtoupper($m[1]);
+ $isPresc = in_array($refPrefix, ['PRESC', 'PRESCRIPTION', 'RX'], true);
+ $reqType = $isPresc ? 'Prescription' : 'OPConsultation';
+ if (! $isTypeAllowed($reqType)) {
+ continue;
+ }
try {
$opd = $this->buildOpConsultationBundle($ref);
- if ($opd !== null && !empty($opd['bundle'])) {
+ if ($opd !== null && ! empty($opd['bundle'])) {
+ $bDate = $extractBundleDate($opd['bundle']);
+ if (! $isDateInRange($bDate)) {
+ continue;
+ }
$records[] = [
'careContextReference' => $ref,
'hiType' => $opd['hiType'] ?? 'OPConsultRecord',
@@ -5361,6 +5470,9 @@ class AbdmGateway extends BaseController
// Strategy 0b-reg: Live On-Demand Fresh Generation for Patient Registration / File (REG-xxxx)
if (preg_match('/^REG-(.+)$/i', $ref, $regM)) {
+ if (! $isTypeAllowed('HealthDocumentRecord')) {
+ continue;
+ }
$pRef = trim($regM[1]);
$patientRow = null;
if ($db->tableExists('patient_master')) {
@@ -5378,12 +5490,16 @@ class AbdmGateway extends BaseController
$abhaId = $this->resolvePatientAbhaIdentifier($patId);
$patientResource = $this->buildAbdmPatientResource($patientRow, $patId, $abhaId);
$docTitle = 'Patient File - ' . $this->patientDisplayName($patientRow);
+ $regDate = $patientRow['created_at'] ?? $patientRow['insert_date'] ?? date('Y-m-d');
+ if (! $isDateInRange($regDate)) {
+ continue;
+ }
$html = '
' . esc($docTitle) . '
'
. '
Patient Name: ' . esc($this->patientDisplayName($patientRow)) . '
'
. '
Patient Code: ' . esc($patientRow['p_code'] ?? '') . '
'
. '
Gender: ' . esc($patientRow['gender'] ?? '') . '
'
. '
Age: ' . esc($patientRow['age'] ?? '') . '
'
- . '
Registration Date: ' . esc($patientRow['created_at'] ?? $patientRow['insert_date'] ?? date('Y-m-d')) . '
'
+ . '
Registration Date: ' . esc($regDate) . '
'
. '
';
$bundle = $this->buildSimpleHealthDocumentBundle($patientResource, $docTitle, 'text/html', base64_encode($html));
$records[] = [
@@ -5399,6 +5515,9 @@ class AbdmGateway extends BaseController
// Strategy 0b: Live On-Demand Fresh Generation for Invoice Record
if (preg_match('/^INVOICE-(CHG|OPD|IPD)-(\d+)/i', $ref, $invM)) {
+ if (! $isTypeAllowed('Invoice')) {
+ continue;
+ }
$invSource = match(strtoupper($invM[1])) {
'CHG' => 'charges_invoice',
'OPD' => 'opd_invoice',
@@ -5407,7 +5526,11 @@ class AbdmGateway extends BaseController
$invId = (int) $invM[2];
try {
$invPayload = $this->buildInvoiceSourceRecordPayload($invSource, $invId, 0, '');
- if ($invPayload !== null && !empty($invPayload['bundle'])) {
+ if ($invPayload !== null && ! empty($invPayload['bundle'])) {
+ $bDate = $extractBundleDate($invPayload['bundle']);
+ if (! $isDateInRange($bDate)) {
+ continue;
+ }
$records[] = [
'careContextReference' => $ref,
'hiType' => 'InvoiceRecord',
@@ -5423,6 +5546,9 @@ class AbdmGateway extends BaseController
// Strategy 0c: Live Retrieval & On-Demand Fresh Generation for Wellness Record
if (str_starts_with(strtoupper($ref), 'WELLNESS-')) {
+ if (! $isTypeAllowed('WellnessRecord')) {
+ continue;
+ }
$foundBundle = null;
$foundDisplay = null;
@@ -5518,6 +5644,10 @@ class AbdmGateway extends BaseController
}
if ($foundBundle !== null && ! empty($foundBundle)) {
+ $bDate = $extractBundleDate($foundBundle);
+ if (! $isDateInRange($bDate)) {
+ continue;
+ }
$records[] = [
'careContextReference' => $ref,
'hiType' => 'WellnessRecord',
@@ -5530,10 +5660,17 @@ class AbdmGateway extends BaseController
// Strategy 0d: Live On-Demand Fresh Generation for Health Document Record (DOC-file-{id}, DOC-{id})
if (preg_match('/^DOC-(?:file-)?(\d+)/i', $ref, $docM)) {
+ if (! $isTypeAllowed('HealthDocumentRecord')) {
+ continue;
+ }
$docTargetId = (int) $docM[1];
try {
$docPayload = $this->buildHealthDocumentRecordPayload(0, '', $docTargetId);
- if ($docPayload !== null && !empty($docPayload['bundle'])) {
+ if ($docPayload !== null && ! empty($docPayload['bundle'])) {
+ $bDate = $extractBundleDate($docPayload['bundle']);
+ if (! $isDateInRange($bDate)) {
+ continue;
+ }
$records[] = [
'careContextReference' => $ref,
'hiType' => 'HealthDocumentRecord',
@@ -5563,7 +5700,7 @@ class AbdmGateway extends BaseController
'push_status' => 'linked',
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
- ]);
+ ]);
}
}
continue;
@@ -5575,11 +5712,18 @@ class AbdmGateway extends BaseController
// Strategy 0e: Live On-Demand Fresh Generation for Immunization Record (IMM-{id})
if (preg_match('/^IMM-(?:PAT-(\d+)|(?:(\d+)-V)?(\d+))/i', $ref, $immM)) {
+ if (! $isTypeAllowed('ImmunizationRecord')) {
+ continue;
+ }
$immTargetId = (int) (! empty($immM[3]) ? $immM[3] : (! empty($immM[2]) ? $immM[2] : ($immM[1] ?? 0)));
$immPatientId = (int) (! empty($immM[1]) ? $immM[1] : (! empty($immM[2]) ? $immM[2] : 0));
try {
$immPayload = $this->buildImmunizationGatewayPayload($immPatientId, $immTargetId, '', false);
if ($immPayload !== null && ! empty($immPayload['bundle'])) {
+ $bDate = $extractBundleDate($immPayload['bundle']);
+ if (! $isDateInRange($bDate)) {
+ continue;
+ }
$vaccineName = 'Vaccine';
if (! empty($immPayload['records'][0]['vaccine_name'])) {
$vaccineName = trim((string) $immPayload['records'][0]['vaccine_name']);
@@ -5645,7 +5789,7 @@ class AbdmGateway extends BaseController
$hrRow = $hrBuilder->get(1)->getRowArray();
}
- if ($hrRow && !empty($hrRow['record_data'])) {
+ if ($hrRow && ! empty($hrRow['record_data'])) {
$bundleData = json_decode((string) $hrRow['record_data'], true);
if (is_array($bundleData)) {
$recHiType = $hrRow['hi_type'] ?? $hrRow['record_type'] ?? 'OPConsultRecord';
@@ -5654,6 +5798,13 @@ class AbdmGateway extends BaseController
} elseif (str_starts_with($ref, 'OPD-')) {
$recHiType = 'OPConsultRecord';
}
+ if (! $isTypeAllowed($recHiType)) {
+ continue;
+ }
+ $bDate = $extractBundleDate($bundleData);
+ if (! $isDateInRange($bDate)) {
+ continue;
+ }
$defaultDisp = match ($recHiType) {
'ImmunizationRecord' => 'Immunization Record',
'DiagnosticReportRecord' => 'Diagnostic Report',
@@ -5683,15 +5834,23 @@ class AbdmGateway extends BaseController
}
}
- if ($hrRow && !empty($hrRow['fhir_bundle_enc'])) {
+ if ($hrRow && ! empty($hrRow['fhir_bundle_enc'])) {
try {
$enc = new FhirEncryptionService();
$decrypted = $enc->decrypt((string) $hrRow['fhir_bundle_enc']);
$bundleData = json_decode($decrypted, true);
if (is_array($bundleData)) {
+ $recHiType = $hrRow['hi_type'] ?? $hrRow['record_type'] ?? 'OPConsultRecord';
+ if (! $isTypeAllowed($recHiType)) {
+ continue;
+ }
+ $bDate = $extractBundleDate($bundleData);
+ if (! $isDateInRange($bDate)) {
+ continue;
+ }
$records[] = [
'careContextReference' => $ref,
- 'hiType' => $hrRow['hi_type'] ?? $hrRow['record_type'] ?? 'OPConsultRecord',
+ 'hiType' => $recHiType,
'display' => $hrRow['care_context_display'] ?? 'Consultation Record',
'bundle' => $bundleData,
];
@@ -5701,14 +5860,21 @@ class AbdmGateway extends BaseController
}
}
-
// Strategy B: Build compliant FHIR Document Bundle from patient_master & OPD
// STRICTLY restricted to OPD/consultation care contexts.
// NEVER synthesize dummy consultation records for invoices, discharge, labs, radiology, etc.
if (preg_match('/^(?:OPD|PRESCRIPTION|PRESC|CONSULT)/i', $ref)) {
+ $isPresc = (bool) preg_match('/^(?:PRESC|PRESCRIPTION)/i', $ref);
+ $reqType = $isPresc ? 'Prescription' : 'OPConsultation';
+ if (! $isTypeAllowed($reqType)) {
+ continue;
+ }
$bundle = $this->assembleFhirBundleForCareContext($ref);
if ($bundle !== null) {
- $isPresc = (bool) preg_match('/^(?:PRESC|PRESCRIPTION)/i', $ref);
+ $bDate = $extractBundleDate($bundle);
+ if (! $isDateInRange($bDate)) {
+ continue;
+ }
$records[] = [
'careContextReference' => $ref,
'hiType' => $isPresc ? 'PrescriptionRecord' : 'OPConsultRecord',
diff --git a/app/Controllers/Patient.php b/app/Controllers/Patient.php
index c43c277..a247022 100644
--- a/app/Controllers/Patient.php
+++ b/app/Controllers/Patient.php
@@ -2017,36 +2017,37 @@ class Patient extends BaseController
// documents from BOTH external and same-entity facilities are returned.
$sessionIds = $this->resolveAllRelatedConsentIds($filterConsentRequestId);
- // ABDM M3 HIU compliance: check if this consent request is expired
- $wfCheck = $this->db->table('abdm_hiu_workflows')
- ->select(['id', 'workflow_state', 'request_json', 'response_json', 'expired_at'])
+ // ABDM M3 HIU compliance: check if this consent request is expired, revoked, or denied
+ $wfCheckQuery = $this->db->table('abdm_hiu_workflows')
+ ->select(['id', 'workflow_state', 'request_json', 'expired_at'])
->groupStart()
->where('request_id', $filterConsentRequestId)
->orWhere('consent_id', $filterConsentRequestId)
->orWhereIn('abdm_consent_request_id', $sessionIds !== [] ? $sessionIds : [$filterConsentRequestId])
- ->orWhereIn('abdm_consent_artifact_id', $sessionIds !== [] ? $sessionIds : [$filterConsentRequestId])
- ->groupEnd()
+ ->orWhereIn('abdm_consent_artifact_id', $sessionIds !== [] ? $sessionIds : [$filterConsentRequestId]);
+ if (ctype_digit($filterConsentRequestId)) {
+ $wfCheckQuery->orWhere('id', (int) $filterConsentRequestId);
+ }
+ $wfCheck = $wfCheckQuery->groupEnd()
->orderBy('id', 'DESC')
->get(1)
->getRowArray();
- $isFilterExpired = false;
- if (is_array($wfCheck)) {
- if (strtoupper(trim((string) ($wfCheck['workflow_state'] ?? ''))) === 'EXPIRED') {
- $isFilterExpired = true;
- } else {
- $filterEraseAt = $this->extractDataEraseAtFromWorkflowRow($wfCheck);
- if ($filterEraseAt !== '') {
- $filterTs = strtotime($filterEraseAt);
- if ($filterTs !== false && $filterTs <= time()) {
- $isFilterExpired = true;
- }
+ $wfState = is_array($wfCheck) ? strtoupper(trim((string) ($wfCheck['workflow_state'] ?? ''))) : '';
+ $isFilterTerminal = in_array($wfState, ['EXPIRED', 'DENIED', 'REVOKED'], true);
+ if (! $isFilterTerminal && is_array($wfCheck)) {
+ $filterEraseAt = $this->extractDataEraseAtFromWorkflowRow($wfCheck);
+ if ($filterEraseAt !== '') {
+ $filterTs = strtotime($filterEraseAt);
+ if ($filterTs !== false && $filterTs <= time()) {
+ $isFilterTerminal = true;
+ $wfState = 'EXPIRED';
}
}
}
- if ($isFilterExpired) {
- // Purge cached records for this expired consent as per ABDM dataEraseAt guidelines
+ if ($isFilterTerminal) {
+ // Purge cached records for this expired/revoked/denied consent as per ABDM dataEraseAt guidelines
$purgeRefs = array_unique(array_merge([$filterConsentRequestId], $sessionIds));
$this->purgeAbdmDocumentsByRefs($purgeRefs);
return $this->response->setJSON([
@@ -2054,7 +2055,8 @@ class Patient extends BaseController
'count' => 0,
'items' => [],
'expired' => 1,
- 'error' => 'This consent request has expired. Health records are erased and no longer accessible as per ABDM guidelines.',
+ 'status' => $wfState !== '' ? $wfState : 'EXPIRED',
+ 'error' => 'This consent request is ' . ($wfState !== '' ? $wfState : 'EXPIRED') . '. Health records are erased and no longer accessible as per ABDM guidelines.',
'abha' => $abhaContext,
'last_sync' => $lastSync,
]);
@@ -2216,24 +2218,28 @@ class Patient extends BaseController
$docFields = $this->db->getFieldNames('abdm_hiu_documents') ?? [];
$sessionIds = $this->resolveAllRelatedConsentIds($consentRequestId);
- // ABDM M3 HIU compliance: check if consent has expired
- $wfCheck = $this->db->table('abdm_hiu_workflows')
- ->select(['id', 'workflow_state', 'request_json', 'response_json', 'expired_at'])
+ // ABDM M3 HIU compliance: check if consent has expired, been denied, or revoked
+ $wfCheckQuery = $this->db->table('abdm_hiu_workflows')
+ ->select(['id', 'workflow_state', 'request_json', 'expired_at'])
->groupStart()
->where('request_id', $consentRequestId)
->orWhere('consent_id', $consentRequestId)
->orWhereIn('abdm_consent_request_id', $sessionIds !== [] ? $sessionIds : [$consentRequestId])
- ->orWhereIn('abdm_consent_artifact_id', $sessionIds !== [] ? $sessionIds : [$consentRequestId])
- ->groupEnd()
+ ->orWhereIn('abdm_consent_artifact_id', $sessionIds !== [] ? $sessionIds : [$consentRequestId]);
+ if (ctype_digit($consentRequestId)) {
+ $wfCheckQuery->orWhere('id', (int) $consentRequestId);
+ }
+ $wfCheck = $wfCheckQuery->groupEnd()
->orderBy('id', 'DESC')
->get(1)
->getRowArray();
if (is_array($wfCheck)) {
$eraseAt = $this->extractDataEraseAtFromWorkflowRow($wfCheck);
- if (strtoupper(trim((string) ($wfCheck['workflow_state'] ?? ''))) === 'EXPIRED' || ($eraseAt !== '' && strtotime($eraseAt) <= time())) {
+ $wfState = strtoupper(trim((string) ($wfCheck['workflow_state'] ?? '')));
+ if (in_array($wfState, ['EXPIRED', 'DENIED', 'REVOKED'], true) || ($eraseAt !== '' && strtotime($eraseAt) <= time())) {
$this->purgeAbdmDocumentsByRefs(array_merge([$consentRequestId], $sessionIds));
- return $this->response->setStatusCode(403)->setBody('This consent request has expired. Health records are erased and cannot be viewed or printed as per ABDM guidelines.');
+ return $this->response->setStatusCode(403)->setBody('This consent request is ' . ($wfState !== '' ? $wfState : 'EXPIRED') . '. Health records are erased and cannot be viewed or printed as per ABDM guidelines.');
}
}
@@ -3098,46 +3104,95 @@ class Patient extends BaseController
?? ''
));
+ $service = new \App\Libraries\Abdm\M3HiuWorkflowService();
+
if ($overrideConsentId !== '' || $overrideConsentRequestId !== '') {
$consentArtifactRef = $overrideConsentId;
$consentRequestRef = $overrideConsentRequestId;
$lookupTarget = $consentRequestRef !== '' ? $consentRequestRef : $consentArtifactRef;
- // Verify whether this specific consent request is expired under ABDM dataEraseAt rules
- $checkWf = $this->db->table('abdm_hiu_workflows')
- ->select(['id', 'workflow_state', 'request_json', 'response_json', 'expired_at'])
+ // Actively reconcile live status with the bridge before fetching records
+ $reconcilePayload = [
+ 'abha_address' => $abhaContext['abha_address'],
+ 'request_id' => $lookupTarget,
+ 'abdm_consent_request_id' => $consentRequestRef,
+ 'abdm_consent_artifact_id'=> $consentArtifactRef,
+ 'consent_id' => $consentArtifactRef !== '' ? $consentArtifactRef : $lookupTarget,
+ ];
+
+ $liveStatus = '';
+ try {
+ $reconcile = $service->runOperation('consent_reconcile', $reconcilePayload);
+ if (is_array($reconcile) && (int) ($reconcile['ok'] ?? 0) === 1) {
+ $liveStatus = strtoupper(trim((string) (
+ $reconcile['workflow_state']
+ ?? $reconcile['status']
+ ?? $reconcile['consent_status']
+ ?? $reconcile['consent']['status']
+ ?? $reconcile['consentDetail']['status']
+ ?? ''
+ )));
+ }
+ } catch (\Throwable $e) {
+ log_message('warning', '[abdm_content_fetch_only] consent_reconcile pre-fetch check warning: ' . $e->getMessage());
+ $reconcile = [];
+ }
+
+ // Check local workflow anchor row
+ $checkWfQuery = $this->db->table('abdm_hiu_workflows')
+ ->select(['id', 'workflow_state', 'request_json', 'expired_at'])
->groupStart()
->where('request_id', $lookupTarget)
->orWhere('consent_id', $lookupTarget)
->orWhere('abdm_consent_request_id', $lookupTarget)
- ->orWhere('abdm_consent_artifact_id', $lookupTarget)
- ->groupEnd()
+ ->orWhere('abdm_consent_artifact_id', $lookupTarget);
+ if (ctype_digit($lookupTarget)) {
+ $checkWfQuery->orWhere('id', (int) $lookupTarget);
+ }
+ $checkWf = $checkWfQuery->groupEnd()
->orderBy('id', 'DESC')
->get(1)
->getRowArray();
- $isTargetExpired = false;
+ $anchorRowId = is_array($checkWf) ? (int) ($checkWf['id'] ?? 0) : 0;
+ if ($anchorRowId > 0 && ! empty($reconcile) && (int) ($reconcile['ok'] ?? 0) === 1) {
+ $this->persistConsentPhaseOnRequestRow($anchorRowId, $reconcile);
+ }
+
+ $currentStatus = $liveStatus !== '' ? $liveStatus : strtoupper(trim((string) ($checkWf['workflow_state'] ?? '')));
+
+ // Verify whether this specific consent request is expired under ABDM dataEraseAt rules
+ $isTargetExpired = ($currentStatus === 'EXPIRED');
$targetEraseAt = '';
- if (is_array($checkWf)) {
- if (strtoupper(trim((string) ($checkWf['workflow_state'] ?? ''))) === 'EXPIRED') {
- $isTargetExpired = true;
- }
+ if (! $isTargetExpired && is_array($checkWf)) {
$targetEraseAt = $this->extractDataEraseAtFromWorkflowRow($checkWf);
if ($targetEraseAt !== '') {
$targetTs = strtotime($targetEraseAt);
if ($targetTs !== false && $targetTs <= time()) {
$isTargetExpired = true;
+ $currentStatus = 'EXPIRED';
}
}
}
- if ($isTargetExpired) {
+ if ($isTargetExpired || in_array($currentStatus, ['EXPIRED', 'DENIED', 'REVOKED'], true)) {
+ $statusWord = $currentStatus !== '' ? $currentStatus : 'EXPIRED';
$relatedPurgeIds = $this->resolveAllRelatedConsentIds($lookupTarget);
$this->purgeAbdmDocumentsByRefs(array_merge([$lookupTarget], $relatedPurgeIds));
return $this->response->setStatusCode(422)->setJSON([
'ok' => 0,
- 'phase' => 'EXPIRED',
- 'error' => 'This consent request expired on ' . ($targetEraseAt ? date('d/m/Y h:i:s A', strtotime($targetEraseAt)) : 'an earlier date') . '. Under ABDM guidelines, records cannot be fetched or accessed under an expired consent. Please create a new consent request.',
+ 'phase' => $statusWord,
+ 'status' => $statusWord,
+ 'error' => "This consent request is {$statusWord}. Under ABDM guidelines, records cannot be fetched or accessed under an {$statusWord} consent. Please create a new consent request.",
+ ]);
+ }
+
+ if (! in_array($currentStatus, ['GRANTED', 'COMPLETED', 'ACTIVE', 'APPROVED'], true)) {
+ return $this->response->setStatusCode(422)->setJSON([
+ 'ok' => 0,
+ 'phase' => $currentStatus !== '' ? $currentStatus : 'REQUESTED',
+ 'status' => $currentStatus !== '' ? $currentStatus : 'REQUESTED',
+ 'error' => "Consent is not granted yet (current status: {$currentStatus}). Once approved by the patient in their ABHA app, please try again.",
]);
}
@@ -3469,7 +3524,35 @@ class Patient extends BaseController
]);
}
- return $this->response->setJSON($this->getAbdmConsentRequestsList($abhaAddress));
+ $rawList = $this->getAbdmConsentRequestsList($abhaAddress);
+ $latestRequest = $rawList['requests'][0] ?? null;
+ if ($latestRequest && ! in_array(strtoupper(trim((string) ($latestRequest['status'] ?? ''))), ['EXPIRED', 'DENIED', 'REVOKED'], true)) {
+ $sessionRequestId = trim((string) ($latestRequest['request_id'] ?? ''));
+ $sessionConsentRequestId = trim((string) ($latestRequest['consent_request_id'] ?? ''));
+ $rawConsentId = trim((string) ($latestRequest['consent_id'] ?? ''));
+ $sessionConsentId = $rawConsentId !== '' ? trim((string) explode(':', $rawConsentId)[0]) : '';
+ $anchorRowId = (int) ($latestRequest['id'] ?? 0);
+ if ($sessionRequestId !== '' || $sessionConsentRequestId !== '' || $sessionConsentId !== '') {
+ try {
+ $service = new \App\Libraries\Abdm\M3HiuWorkflowService();
+ $reconcile = $service->runOperation('consent_reconcile', [
+ 'abha_address' => $abhaAddress,
+ 'request_id' => $sessionRequestId,
+ 'abdm_consent_request_id' => $sessionConsentRequestId,
+ 'abdm_consent_artifact_id'=> $sessionConsentId,
+ 'consent_id' => $sessionConsentId,
+ ]);
+ if ((int) ($reconcile['ok'] ?? 0) === 1 && $anchorRowId > 0) {
+ $this->persistConsentPhaseOnRequestRow($anchorRowId, $reconcile);
+ $rawList = $this->getAbdmConsentRequestsList($abhaAddress);
+ }
+ } catch (\Throwable $e) {
+ // Silent fallback to cached list
+ }
+ }
+ }
+
+ return $this->response->setJSON($rawList);
}
/**
@@ -7003,6 +7086,27 @@ class Patient extends BaseController
$ids = [$ref];
+ if (ctype_digit($ref) && $this->db->tableExists('abdm_hiu_workflows')) {
+ $fields = $this->db->getFieldNames('abdm_hiu_workflows') ?? [];
+ $selCols = ['id'];
+ foreach (['abdm_consent_request_id', 'abdm_consent_artifact_id', 'consent_id', 'request_id', 'gateway_request_id'] as $sc) {
+ if (in_array($sc, $fields, true)) {
+ $selCols[] = $sc;
+ }
+ }
+ $anchorRow = $this->db->table('abdm_hiu_workflows')->select(implode(', ', $selCols))->where('id', (int) $ref)->get(1)->getRowArray();
+ if (is_array($anchorRow)) {
+ foreach ($selCols as $col) {
+ if ($col !== 'id') {
+ $val = trim((string) ($anchorRow[$col] ?? ''));
+ if ($val !== '' && ! preg_match('/^REQ-/i', $val)) {
+ $ids[] = $val;
+ }
+ }
+ }
+ }
+ }
+
if (strpos($ref, ':') !== false) {
$parts = explode(':', $ref);
foreach ($parts as $p) {
@@ -7097,8 +7201,12 @@ class Patient extends BaseController
*/
private function extractDataEraseAtFromWorkflowRow(array $row): string
{
+ if (! empty($row['expired_at'])) {
+ return (string) $row['expired_at'];
+ }
$req = json_decode((string) ($row['request_json'] ?? ''), true);
- $resp = json_decode((string) ($row['response_json'] ?? ''), true);
+ $respStr = (string) ($row['response_json'] ?? '');
+ $resp = (strlen($respStr) > 0 && strlen($respStr) <= 65536) ? json_decode($respStr, true) : null;
$containers = [
is_array($req) ? ($req['consent'] ?? null) : null,
is_array($req) ? ($req['consentDetail'] ?? null) : null,
diff --git a/app/Views/billing/Patient_Profile_Opd_V.php b/app/Views/billing/Patient_Profile_Opd_V.php
index 54b7ed9..bb26e79 100644
--- a/app/Views/billing/Patient_Profile_Opd_V.php
+++ b/app/Views/billing/Patient_Profile_Opd_V.php
@@ -1153,6 +1153,22 @@ $(function() {
var fetchOnlyRunningIdx = -1;
var currentAbdmFetchModalIdx = -1;
+ function safeParseJson(resp) {
+ return resp.text().then(function(text) {
+ var data;
+ try {
+ data = JSON.parse(text);
+ } catch (e) {
+ var cleanText = (text || '').replace(/<[^>]*>/g, ' ').replace(/\s+/g, ' ').trim();
+ throw new Error('Server returned an unexpected response (HTTP ' + resp.status + '): ' + (cleanText ? cleanText.substring(0, 150) : 'Empty response'));
+ }
+ if (!resp.ok && (!data || data.ok !== 1)) {
+ throw new Error((data && data.error) || ('Server returned HTTP ' + resp.status));
+ }
+ return data;
+ });
+ }
+
function consentStatusBadgeClass(status) {
switch ((status || '').toString().toUpperCase()) {
case 'GRANTED': return 'bg-info-subtle text-info border border-info-subtle';
@@ -1276,8 +1292,6 @@ $(function() {
$('#btnNewAbdmRequest').prop('disabled', autoFlowRunning || fetchOnlyRunningIdx !== -1);
}
- // Renders one request's detail (from already-loaded list data, no extra
- // network call) into the shared Consent Details modal.
function showAbdmConsentDetailForIndex(idx) {
var consent = currentAbdmRequests[idx];
$('#abdmConsentDetailBody').html(renderAbdmConsentDetail(consent));
@@ -1337,7 +1351,7 @@ $(function() {
$btn.prop('disabled', true).html('Refreshing...');
fetch(abdmConsentRequestsUrl, { credentials: 'same-origin' })
- .then(function(resp) { return resp.json(); })
+ .then(safeParseJson)
.then(function(data) {
if (!data || data.ok !== 1) {
throw new Error((data && data.error) || 'Unable to load consent request history.');
@@ -1355,7 +1369,7 @@ $(function() {
}
// Re-pulls data for a specific (already GRANTED/COMPLETED) request row using
- // its saved consent reference, without creating a brand-new consent request.
+ // its saved consent reference, verifying consent status first.
function runFetchRecordsForRow(idx) {
if (autoFlowRunning || fetchOnlyRunningIdx !== -1) {
return;
@@ -1374,21 +1388,22 @@ $(function() {
isExpired = true;
}
}
- if (isExpired) {
- setAbdmStatus('This consent request has expired. Under ABDM guidelines, records cannot be fetched or accessed under an expired consent.', true);
+ if (isExpired || status === 'DENIED' || status === 'REVOKED') {
+ setAbdmStatus('This consent request is ' + (status || 'EXPIRED') + '. Under ABDM guidelines, records cannot be fetched or accessed.', true);
+ loadAbdmConsentRequests();
return;
}
fetchOnlyRunningIdx = idx;
currentAbdmFetchModalIdx = idx;
applyNewRequestButtonState();
- setAbdmStatus('Fetching latest records using existing granted consent...', false);
+ setAbdmStatus('Verifying consent status and fetching latest records...', false);
openAbdmFetchResultModal();
- $('#abdmFetchResultModalBody').html('Fetching latest records...
');
+ $('#abdmFetchResultModalBody').html('Verifying consent status & fetching records...
');
var url = abdmFetchOnlyUrl + '?consent_id=' + encodeURIComponent(consent.consent_id || '') + '&consent_request_id=' + encodeURIComponent(consent.consent_request_id || '');
fetch(url, { credentials: 'same-origin' })
- .then(function(resp) { return resp.json(); })
+ .then(safeParseJson)
.then(function(data) {
if (!data || data.ok !== 1) {
throw new Error((data && data.error) || 'Fetch failed.');
@@ -1404,9 +1419,10 @@ $(function() {
fetchOnlyRunningIdx = -1;
applyNewRequestButtonState();
setAbdmStatus('Fetch failed: ' + (err.message || err), true);
- // Even if the fetch call itself failed, still show whatever records
- // are already saved locally for this consent request.
- loadAbdmFetchResultModalData(consent);
+ // Immediately refresh table so that if consent is expired/denied/revoked,
+ // the row badge turns EXPIRED/DENIED and the "Show Data" button is removed!
+ loadAbdmConsentRequests();
+ $('#abdmFetchResultModalBody').html('' + escHtml(err.message || err) + '
');
});
}
@@ -1428,9 +1444,9 @@ $(function() {
isExpired = true;
}
}
- if (isExpired) {
+ if (isExpired || status === 'DENIED' || status === 'REVOKED') {
$('#abdmPrintFetchedRecordsBtn').hide();
- $('#abdmFetchResultModalBody').html('This consent request expired on ' + escHtml(fmtConsentTs(eraseAt)) + '.
As per ABDM guidelines, health records are erased and no longer accessible.
');
+ $('#abdmFetchResultModalBody').html('This consent request is ' + escHtml(status || 'EXPIRED') + '.
As per ABDM guidelines, health records are erased and no longer accessible.
');
return;
}
@@ -1440,7 +1456,7 @@ $(function() {
}
fetch(url, { credentials: 'same-origin' })
- .then(function(resp) { return resp.json(); })
+ .then(safeParseJson)
.then(function(data) {
if (!data || data.ok !== 1) {
throw new Error((data && data.error) || 'Unable to load fetched records.');
@@ -1789,7 +1805,7 @@ $(function() {
}
fetch(url, { credentials: 'same-origin' })
- .then(function(resp) { return resp.json(); })
+ .then(safeParseJson)
.then(function(data) {
if (!data || data.ok !== 1) {
throw new Error((data && data.error) || 'Unable to load ABDM records.');
@@ -1816,7 +1832,7 @@ $(function() {
}
fetch(url, { credentials: 'same-origin' })
- .then(function(resp) { return resp.json(); })
+ .then(safeParseJson)
.then(function(data) {
if (!data || data.ok !== 1) {
throw new Error((data && data.error) || 'Auto flow failed.');
@@ -1868,7 +1884,7 @@ $(function() {
return;
}
fetch(abdmDocDetailBaseUrl + '/' + encodeURIComponent(docId), { credentials: 'same-origin' })
- .then(function(resp) { return resp.json(); })
+ .then(safeParseJson)
.then(function(data) {
if (!data || data.ok !== 1) {
throw new Error((data && data.error) || 'Unable to load document detail.');
@@ -1899,7 +1915,7 @@ $(function() {
setAbdmStatus('Checking live status with ABDM bridge…');
fetch(abdmCheckLiveStatusUrl, { credentials: 'same-origin' })
- .then(function(resp) { return resp.json(); })
+ .then(safeParseJson)
.then(function(data) {
if (!data || data.ok !== 1) {
throw new Error((data && data.error) || 'Live status check failed.');