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.');