From 24089f36a697ce7b471a7d642a713851875e1906 Mon Sep 17 00:00:00 2001 From: DevSoftTech Server Date: Thu, 24 Sep 2026 09:33:19 +0530 Subject: [PATCH] fix(abdm): auto-link ABHA address on discovery/confirm, resolve FHIR by patient_id on-demand, and display in profile --- app/Controllers/AbdmGateway.php | 245 ++++++++++++++++++- app/Controllers/Opd_prescription.php | 42 +++- app/Controllers/Patient.php | 37 +++ app/Views/billing/Person_profile_V.php | 30 ++- app/Views/billing/opd_prescription_basic.php | 16 +- 5 files changed, 360 insertions(+), 10 deletions(-) diff --git a/app/Controllers/AbdmGateway.php b/app/Controllers/AbdmGateway.php index 4b0f415..c9dd607 100644 --- a/app/Controllers/AbdmGateway.php +++ b/app/Controllers/AbdmGateway.php @@ -148,12 +148,35 @@ class AbdmGateway extends BaseController if ($match !== null) { $patient = $match['patient']; $patientId = (int) $patient['id']; + $patientRef = (string) ($patient['p_code'] ?: ('P-' . $patientId)); $patientDisplay = trim((string) ($patient['p_fname'] ?? '')); if ($patientDisplay === '') { $patientDisplay = 'Patient ' . $patientRef; } $matchedBy = $match['matchedBy']; + // Save discovered ABHA address/ID to patient_master if not already present + $discoveredAbha = trim((string) ( + $payload['patient']['id'] + ?? $payload['patient']['abhaAddress'] + ?? $payload['patient']['abha_address'] + ?? $payload['abha_address'] + ?? $payload['abhaAddress'] + ?? $payload['abha_id'] + ?? '' + )); + if ($discoveredAbha !== '' && $this->db->tableExists('patient_master')) { + $pmUpdate = []; + if (str_contains($discoveredAbha, '@') && empty($patient['abha_address'])) { + $pmUpdate['abha_address'] = $discoveredAbha; + } elseif (strlen(preg_replace('/\D/', '', $discoveredAbha)) === 14 && empty($patient['abha_id'])) { + $pmUpdate['abha_id'] = $discoveredAbha; + } + if (! empty($pmUpdate)) { + $this->db->table('patient_master')->where('id', $patientId)->update($pmUpdate); + } + } + [$careContextsV3, $careContextsFull] = $this->findCareContextsForPatient($patientId, $patientRef, $patientDisplay); $this->getAuditService()->log([ @@ -412,6 +435,63 @@ class AbdmGateway extends BaseController $resolvedRefs[$ccRef] = true; } + // On-Demand Resolution: Connect FHIR records by patient_id & session_id + if (! empty($requestedRefs)) { + foreach ($requestedRefs as $ref) { + if (isset($resolvedRefs[$ref])) { + continue; + } + + $targetPatientId = 0; + $targetSessionId = 0; + if (preg_match('/(?:OPD|PRESCRIPTION)-(\d+)(?:-S(\d+))?/i', $ref, $m)) { + $targetPatientId = (int) $m[1]; + $targetSessionId = (int) ($m[2] ?? 0); + } + + $bundleData = null; + if ($this->db->tableExists('opd_fhir_documents')) { + $docBuilder = $this->db->table('opd_fhir_documents'); + if ($targetSessionId > 0) { + $docBuilder->where('opd_session_id', $targetSessionId); + } elseif ($targetPatientId > 0) { + $docBuilder->where('opd_id', $targetPatientId); + } + $docRow = $docBuilder->orderBy('id', 'DESC')->get(1)->getRowArray(); + if (! empty($docRow['bundle_json'])) { + $bundleData = json_decode((string) $docRow['bundle_json'], true); + } + } + + if (! is_array($bundleData)) { + $bundleData = $this->assembleFhirBundleForCareContext($ref); + } + + if (is_array($bundleData)) { + $resolvedRefs[$ref] = true; + $entries[] = [ + 'careContextReference' => $ref, + 'media' => 'application/fhir+json', + 'fhir' => $bundleData, + ]; + + if ($this->db->tableExists('health_records')) { + $this->storeHealthRecord([ + 'patient_id' => $targetPatientId > 0 ? $targetPatientId : null, + 'abha_id' => $abhaLookup, + 'hi_type' => 'OPConsultRecord', + 'entity_type' => 'opd', + 'entity_id' => (string) ($targetSessionId > 0 ? $targetSessionId : $targetPatientId), + 'fhir_bundle' => json_encode($bundleData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES), + 'care_context_reference' => $ref, + 'push_status' => 'linked', + 'reuse_existing' => true, + ]); + } + } + } + } + $this->confirmFetchedHealthRecordsLinked($deliveredHealthRows, $abhaLookup, $requestId, $payload); $this->getAuditService()->log([ @@ -4634,6 +4714,28 @@ class AbdmGateway extends BaseController } $matchedBy = $match['matchedBy']; + // Save discovered ABHA address/ID to patient_master if not already present + $discoveredAbha = trim((string) ( + $payload['patient']['id'] + ?? $payload['patient']['abhaAddress'] + ?? $payload['patient']['abha_address'] + ?? $payload['abha_address'] + ?? $payload['abhaAddress'] + ?? $payload['abha_id'] + ?? '' + )); + if ($discoveredAbha !== '' && $this->db->tableExists('patient_master')) { + $pmUpdate = []; + if (str_contains($discoveredAbha, '@') && empty($patient['abha_address'])) { + $pmUpdate['abha_address'] = $discoveredAbha; + } elseif (strlen(preg_replace('/\D/', '', $discoveredAbha)) === 14 && empty($patient['abha_id'])) { + $pmUpdate['abha_id'] = $discoveredAbha; + } + if (! empty($pmUpdate)) { + $this->db->table('patient_master')->where('id', $patientId)->update($pmUpdate); + } + } + [$careContextsV3, $careContextsFull] = $this->findCareContextsForPatient($patientId, $patientRef, $patientDisplay); $this->getAuditService()->log([ @@ -4809,6 +4911,35 @@ class AbdmGateway extends BaseController } } + // Strategy A.2: Check if opd_fhir_documents has stored FHIR bundle + if ($db->tableExists('opd_fhir_documents')) { + $targetPatientId = 0; + $targetSessionId = 0; + if (preg_match('/(?:OPD|PRESCRIPTION)-(\d+)(?:-S(\d+))?/i', $ref, $m)) { + $targetPatientId = (int) $m[1]; + $targetSessionId = (int) ($m[2] ?? 0); + } + $docBuilder = $db->table('opd_fhir_documents'); + if ($targetSessionId > 0) { + $docBuilder->where('opd_session_id', $targetSessionId); + } elseif ($targetPatientId > 0) { + $docBuilder->where('opd_id', $targetPatientId); + } + $docRow = $docBuilder->orderBy('id', 'DESC')->get(1)->getRowArray(); + if (! empty($docRow['bundle_json'])) { + $bundleData = json_decode((string) $docRow['bundle_json'], true); + if (is_array($bundleData)) { + $records[] = [ + 'careContextReference' => $ref, + 'hiType' => 'PrescriptionRecord', + 'display' => 'Prescription - ' . date('d M Y', strtotime($docRow['generated_at'] ?? 'now')), + 'bundle' => $bundleData, + ]; + continue; + } + } + } + // Strategy B: Build compliant FHIR Document Bundle from patient_master & OPD $bundle = $this->assembleFhirBundleForCareContext($ref); if ($bundle !== null) { @@ -5054,6 +5185,34 @@ class AbdmGateway extends BaseController } $patientId = (int) $patient['id']; + if ($abhaAddress === '') { + $abhaAddress = trim((string) ( + $patient['abha_address'] + ?? $patient['abha_id'] + ?? $patient['abha'] + ?? '' + )); + } + if ($abhaAddress === '' && ! empty($payload['patient']['unverifiedIdentifiers']) && is_array($payload['patient']['unverifiedIdentifiers'])) { + foreach ($payload['patient']['unverifiedIdentifiers'] as $ui) { + $type = strtoupper(trim((string) ($ui['type'] ?? ''))); + $val = trim((string) ($ui['value'] ?? '')); + if (in_array($type, ['HEALTH_ID', 'ABHA_ADDRESS', 'ABHA_NUMBER', 'ABHA'], true) && $val !== '') { + $abhaAddress = $val; + break; + } + } + } + if ($abhaAddress === '' && ! empty($payload['patient']['verifiedIdentifiers']) && is_array($payload['patient']['verifiedIdentifiers'])) { + foreach ($payload['patient']['verifiedIdentifiers'] as $vi) { + $type = strtoupper(trim((string) ($vi['type'] ?? ''))); + $val = trim((string) ($vi['value'] ?? '')); + if (in_array($type, ['HEALTH_ID', 'ABHA_ADDRESS', 'ABHA_NUMBER', 'ABHA'], true) && $val !== '') { + $abhaAddress = $val; + break; + } + } + } $rawPhone = trim((string) ($patient['mphone1'] ?? $patient['mphone2'] ?? '')); $cleanPhone = substr(preg_replace('/\D/', '', $rawPhone), -10); if (strlen($cleanPhone) !== 10) { @@ -5228,6 +5387,10 @@ class AbdmGateway extends BaseController $patientId = (int) $txn['patient_id']; $abhaAddress = trim((string) ($txn['abha_address'] ?? '')); + if ($abhaAddress === '') { + $patientRow = $this->loadPatientRow($patientId); + $abhaAddress = trim((string) ($patientRow['abha_address'] ?? $patientRow['abha_id'] ?? '')); + } $now = date('Y-m-d H:i:s'); $this->db->transStart(); @@ -5238,7 +5401,16 @@ class AbdmGateway extends BaseController 'abdm_linked_at' => $now, ]; if ($abhaAddress !== '') { - $pmUpdate['abha_address'] = $abhaAddress; + if (str_contains($abhaAddress, '@')) { + $pmUpdate['abha_address'] = $abhaAddress; + } else { + $digits = preg_replace('/\D/', '', $abhaAddress); + if (strlen($digits) === 14) { + $pmUpdate['abha_id'] = $abhaAddress; + } else { + $pmUpdate['abha_address'] = $abhaAddress; + } + } } $this->db->table('patient_master')->where('id', $patientId)->update($pmUpdate); } @@ -5280,6 +5452,9 @@ class AbdmGateway extends BaseController } } + // Auto-sync any existing FHIR documents for this patient to health_records + $this->syncPatientFhirToHealthRecords($patientId, $abhaAddress, $selectedRefs); + $this->db->table('abdm_link_transactions') ->where('id', (int) $txn['id']) ->update([ @@ -5356,6 +5531,74 @@ class AbdmGateway extends BaseController return $this->m2LinkConfirm(); } + private function syncPatientFhirToHealthRecords(int $patientId, string $abhaAddress, array $careContextRefs): void + { + if ($patientId <= 0 || ! $this->db->tableExists('health_records')) { + return; + } + + $now = Time::now('Asia/Kolkata')->toDateTimeString(); + + foreach ($careContextRefs as $ref) { + $ref = trim((string) $ref); + if ($ref === '') { + continue; + } + + $sessionId = 0; + if (preg_match('/(?:OPD|PRESCRIPTION)-\d+-S(\d+)/i', $ref, $m)) { + $sessionId = (int) $m[1]; + } + + $docRow = null; + if ($this->db->tableExists('opd_fhir_documents')) { + $b = $this->db->table('opd_fhir_documents'); + if ($sessionId > 0) { + $b->where('opd_session_id', $sessionId); + } else { + $b->where('opd_id', $patientId); + } + $docRow = $b->orderBy('id', 'DESC')->get(1)->getRowArray(); + } + + $bundleJson = $docRow['bundle_json'] ?? null; + if ($bundleJson === null) { + $bundle = $this->assembleFhirBundleForCareContext($ref); + if ($bundle !== null) { + $bundleJson = json_encode($bundle, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + } + } + + if ($bundleJson !== null) { + $existing = $this->db->table('health_records') + ->select('id') + ->where('care_context_reference', $ref) + ->get(1) + ->getRowArray(); + + $hrData = [ + 'patient_id' => $patientId, + 'abha_id' => $abhaAddress !== '' ? $abhaAddress : null, + 'hi_type' => 'OPConsultRecord', + 'entity_type' => 'opd', + 'entity_id' => (string) ($sessionId > 0 ? $sessionId : $patientId), + 'record_data' => $bundleJson, + 'care_context_reference' => $ref, + 'push_status' => 'linked', + 'linked_at' => $now, + 'updated_at' => $now, + ]; + + if (! empty($existing)) { + $this->db->table('health_records')->where('id', (int) $existing['id'])->update($hrData); + } else { + $hrData['created_at'] = $now; + $this->db->table('health_records')->insert($hrData); + } + } + } + } + // ========================================================================= // M2 Discovery & Linking Helpers // ========================================================================= diff --git a/app/Controllers/Opd_prescription.php b/app/Controllers/Opd_prescription.php index e52b3d6..e205cb4 100644 --- a/app/Controllers/Opd_prescription.php +++ b/app/Controllers/Opd_prescription.php @@ -11763,13 +11763,41 @@ class Opd_prescription extends BaseController return false; } - // NOTE: Do NOT also enqueue an 'opd.fhir.generated' bridge_sync_queue event here. - // That event type has no 'abdm.'/'snomed.'/'nhcx.' prefix, so BridgeSyncService - // routes it to the generic CSNOtk terminology bridge (BRIDGE_SYNC_URL), which has - // no handler for it and always returns HTTP 404 -- it has no consumer anywhere in - // this codebase. The bundle is already persisted to opd_fhir_documents above; actual - // ABDM sharing is triggered separately via DreamsoftConnector::sharePrescriptionBundle() - // (event 'abdm.fhir.share.requested'), which correctly routes to ABDM_BRIDGE_URL. + if ($this->db->tableExists('health_records')) { + try { + $patientId = (int) ($patientRow['id'] ?? ($opdRow['p_id'] ?? 0)); + $visitDateRaw = trim((string) ($opdRow['date_opd_visit'] ?? $opdRow['apointment_date'] ?? '')); + $visitDate = $visitDateRaw !== '' ? date('Y-m-d', strtotime($visitDateRaw)) : date('Y-m-d'); + $careContextRef = 'OPD-' . $patientId . '-S' . $sessionId . '-' . str_replace('-', '', $visitDate); + + $existingHr = $this->db->table('health_records') + ->select('id') + ->where('care_context_reference', $careContextRef) + ->get(1) + ->getRowArray(); + + $hrPayload = [ + 'patient_id' => $patientId > 0 ? $patientId : null, + 'abha_id' => $abhaAddress !== '' ? $abhaAddress : null, + 'hi_type' => 'OPConsultRecord', + 'entity_type' => 'opd', + 'entity_id' => (string) $sessionId, + 'record_data' => $bundleJson, + 'care_context_reference' => $careContextRef, + 'push_status' => 'local_discovery_ready', + 'updated_at' => Time::now('Asia/Kolkata')->toDateTimeString(), + ]; + + if (! empty($existingHr)) { + $this->db->table('health_records')->where('id', (int) $existingHr['id'])->update($hrPayload); + } else { + $hrPayload['created_at'] = Time::now('Asia/Kolkata')->toDateTimeString(); + $this->db->table('health_records')->insert($hrPayload); + } + } catch (\Throwable $e) { + log_message('warning', '[storePrescriptionFhirBundle] auto health_records insert error: ' . $e->getMessage()); + } + } return true; } diff --git a/app/Controllers/Patient.php b/app/Controllers/Patient.php index f59fc62..d652c73 100644 --- a/app/Controllers/Patient.php +++ b/app/Controllers/Patient.php @@ -967,6 +967,43 @@ class Patient extends BaseController return $this->response->setStatusCode(404)->setBody('Patient not found'); } + if (! empty($data['data'][0])) { + $curAbha = trim((string) ($data['data'][0]->abha_address ?? '')); + if ($curAbha === '') { + $foundAbha = ''; + if ($this->db->tableExists('abdm_link_transactions')) { + $alt = $this->db->table('abdm_link_transactions') + ->select('abha_address') + ->where('patient_id', $pno) + ->where('abha_address !=', '') + ->orderBy('id', 'DESC') + ->get(1)->getRowArray(); + if (! empty($alt['abha_address'])) { + $foundAbha = trim((string) $alt['abha_address']); + } + } + if ($foundAbha === '' && $this->db->tableExists('record_links')) { + $rl = $this->db->table('record_links') + ->select('abha_id') + ->where('care_context_reference LIKE', 'OPD-' . $pno . '-%') + ->where('abha_id !=', '') + ->orderBy('id', 'DESC') + ->get(1)->getRowArray(); + if (! empty($rl['abha_id'])) { + $foundAbha = trim((string) $rl['abha_id']); + } + } + if ($foundAbha !== '') { + $data['data'][0]->abha_address = $foundAbha; + if (str_contains($foundAbha, '@')) { + $this->db->table('patient_master')->where('id', $pno)->update(['abha_address' => $foundAbha]); + } else { + $this->db->table('patient_master')->where('id', $pno)->update(['abha_id' => $foundAbha]); + } + } + } + } + $required_age = $data['data'][0]->age; $required_age_in_month = $data['data'][0]->age_in_month; $required_dob = $data['data'][0]->dob; diff --git a/app/Views/billing/Person_profile_V.php b/app/Views/billing/Person_profile_V.php index ed57d27..5e65dad 100644 --- a/app/Views/billing/Person_profile_V.php +++ b/app/Views/billing/Person_profile_V.php @@ -26,13 +26,41 @@ if ($abhaAddress === '' && preg_match('/abha_address\s*:\s*([A-Za-z0-9._-]+@[A-Za-z0-9.-]+)/i', (string) ($data[0]->log ?? ''), $abhaLogMatch) === 1) { $abhaAddress = trim((string) ($abhaLogMatch[1] ?? '')); } + if ($abhaAddress === '') { + $db = \Config\Database::connect(); + if ($db->tableExists('abdm_link_transactions')) { + $alt = $db->table('abdm_link_transactions') + ->select('abha_address') + ->where('patient_id', (int) ($data[0]->id ?? 0)) + ->where('abha_address !=', '') + ->orderBy('id', 'DESC') + ->get(1)->getRowArray(); + if (! empty($alt['abha_address'])) { + $abhaAddress = trim((string) $alt['abha_address']); + } + } + if ($abhaAddress === '' && $db->tableExists('record_links')) { + $rl = $db->table('record_links') + ->select('abha_id') + ->where('care_context_reference LIKE', 'OPD-' . (int) ($data[0]->id ?? 0) . '-%') + ->where('abha_id !=', '') + ->orderBy('id', 'DESC') + ->get(1)->getRowArray(); + if (! empty($rl['abha_id'])) { + $abhaAddress = trim((string) $rl['abha_id']); + } + } + } + if ($patientAbhaId === '' && $abhaAddress !== '') { + $patientAbhaId = $abhaAddress; + } $abhaVerifiedStatus = trim((string) ($data[0]->abha_verified_status ?? '')); $abhaVerificationType = trim((string) ($data[0]->abha_verification_type ?? '')); $abhaKycVerified = (int) ($data[0]->abha_kyc_verified ?? 0) === 1; $abhaMobileVerified = (int) ($data[0]->abha_mobile_verified ?? 0) === 1; $abhaLinkedAt = trim((string) ($data[0]->abdm_linked_at ?? '')); $abhaPhotoAvailable = trim((string) ($data[0]->abha_profile_photo_base64 ?? '')) !== ''; - $isAbhaLinkedAndVerified = $patientAbhaId !== '' && strtoupper($abhaVerifiedStatus) === 'VERIFIED'; + $isAbhaLinkedAndVerified = $patientAbhaId !== '' && in_array(strtoupper($abhaVerifiedStatus), ['VERIFIED', 'LINKED'], true); ?> user(); diff --git a/app/Views/billing/opd_prescription_basic.php b/app/Views/billing/opd_prescription_basic.php index 7324f32..500815c 100644 --- a/app/Views/billing/opd_prescription_basic.php +++ b/app/Views/billing/opd_prescription_basic.php @@ -558,7 +558,21 @@ if ($relationLabel !== '') { $patientNameWithRelation .= ' (' . $relationLabel . ')'; } - $patientAbhaAddress = trim((string) ($patient_master[0]->abha_address ?? $patient_master[0]->abha ?? '')); + $patientAbhaAddress = trim((string) ($patient_master[0]->abha_address ?? $patient_master[0]->abha_id ?? $patient_master[0]->abha ?? '')); + if ($patientAbhaAddress === '') { + $db = \Config\Database::connect(); + if ($db->tableExists('record_links')) { + $rl = $db->table('record_links') + ->select('abha_id') + ->where('care_context_reference LIKE', 'OPD-' . (int) ($patient_master[0]->id ?? 0) . '-%') + ->where('abha_id !=', '') + ->orderBy('id', 'DESC') + ->get(1)->getRowArray(); + if (! empty($rl['abha_id'])) { + $patientAbhaAddress = trim((string) $rl['abha_id']); + } + } + } ?>