fix(abdm): enforce granted hi_types and dateRange, check live consent status before fetch, safe json parsing
This commit is contained in:
parent
7f6ae881e7
commit
e41e6bd96f
|
|
@ -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 = '<div xmlns="http://www.w3.org/1999/xhtml"><h3>' . esc($docTitle) . '</h3>'
|
||||
. '<p><strong>Patient Name:</strong> ' . esc($this->patientDisplayName($patientRow)) . '</p>'
|
||||
. '<p><strong>Patient Code:</strong> ' . esc($patientRow['p_code'] ?? '') . '</p>'
|
||||
. '<p><strong>Gender:</strong> ' . esc($patientRow['gender'] ?? '') . '</p>'
|
||||
. '<p><strong>Age:</strong> ' . esc($patientRow['age'] ?? '') . '</p>'
|
||||
. '<p><strong>Registration Date:</strong> ' . esc($patientRow['created_at'] ?? $patientRow['insert_date'] ?? date('Y-m-d')) . '</p>'
|
||||
. '<p><strong>Registration Date:</strong> ' . esc($regDate) . '</p>'
|
||||
. '</div>';
|
||||
$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',
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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('<span class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>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('<div class="text-muted small text-center">Fetching latest records...</div>');
|
||||
$('#abdmFetchResultModalBody').html('<div class="text-muted small text-center p-4"><div class="spinner-border spinner-border-sm me-2 text-primary" role="status"></div>Verifying consent status & fetching records...</div>');
|
||||
|
||||
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('<div class="alert alert-warning small text-center m-3">' + escHtml(err.message || err) + '</div>');
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -1428,9 +1444,9 @@ $(function() {
|
|||
isExpired = true;
|
||||
}
|
||||
}
|
||||
if (isExpired) {
|
||||
if (isExpired || status === 'DENIED' || status === 'REVOKED') {
|
||||
$('#abdmPrintFetchedRecordsBtn').hide();
|
||||
$('#abdmFetchResultModalBody').html('<div class="text-muted small text-center p-3">This consent request expired on <strong>' + escHtml(fmtConsentTs(eraseAt)) + '</strong>.<br>As per ABDM guidelines, health records are erased and no longer accessible.</div>');
|
||||
$('#abdmFetchResultModalBody').html('<div class="text-muted small text-center p-3">This consent request is <strong>' + escHtml(status || 'EXPIRED') + '</strong>.<br>As per ABDM guidelines, health records are erased and no longer accessible.</div>');
|
||||
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.');
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user