fix(abdm): auto-link ABHA address on discovery/confirm, resolve FHIR by patient_id on-demand, and display in profile

This commit is contained in:
DevSoftTech Server 2026-09-24 09:33:19 +05:30
parent 2f6b346e25
commit 24089f36a6
5 changed files with 360 additions and 10 deletions

View File

@ -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
// =========================================================================

View File

@ -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;
}

View File

@ -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;

View File

@ -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);
?>
<?php
$user = auth()->user();

View File

@ -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']);
}
}
}
?>
<a href="javascript:load_form('<?= esc($historyUrl, 'js') ?>','Consult History');"
class="btn btn-info btn-sm"