From c692340bb2880e213fb9d1cc3ff80fb92764fa59 Mon Sep 17 00:00:00 2001 From: DevSoftTech Server Date: Wed, 3 Jun 2026 11:05:40 +0530 Subject: [PATCH] Store ABDM bundles in health records --- app/Controllers/AbdmGateway.php | 27 ++++--- app/Controllers/Opd_prescription.php | 77 ++++++++---------- ...02-000608_AddRecordDataToHealthRecords.php | 81 +++++++++++++++++++ 3 files changed, 130 insertions(+), 55 deletions(-) create mode 100644 app/Database/Migrations/2026-06-02-000608_AddRecordDataToHealthRecords.php diff --git a/app/Controllers/AbdmGateway.php b/app/Controllers/AbdmGateway.php index 8eee7ab..1dac163 100644 --- a/app/Controllers/AbdmGateway.php +++ b/app/Controllers/AbdmGateway.php @@ -1735,16 +1735,21 @@ class AbdmGateway extends BaseController $abhaId = trim((string) $this->request->getPost('abha_id')); } - // Decrypt the stored payload (JSON encoded full push payload, may contain 'bundle' key) + // Prefer the plain stored bundle; fall back to the encrypted copy for older rows. $storedPayload = []; - $encPayload = trim((string) ($hr['fhir_bundle_enc'] ?? '')); - if ($encPayload !== '') { - try { - $enc = new \App\Libraries\FhirEncryptionService(); - $decrypted = $enc->decrypt($encPayload); - $storedPayload = json_decode($decrypted, true) ?? []; - } catch (\Throwable $e) { - return $this->response->setStatusCode(500)->setJSON(['ok' => 0, 'error_text' => 'Could not decrypt FHIR bundle: ' . $e->getMessage()]); + $plainPayload = trim((string) ($hr['record_data'] ?? '')); + if ($plainPayload !== '') { + $storedPayload = json_decode($plainPayload, true) ?? []; + } else { + $encPayload = trim((string) ($hr['fhir_bundle_enc'] ?? '')); + if ($encPayload !== '') { + try { + $enc = new \App\Libraries\FhirEncryptionService(); + $decrypted = $enc->decrypt($encPayload); + $storedPayload = json_decode($decrypted, true) ?? []; + } catch (\Throwable $e) { + return $this->response->setStatusCode(500)->setJSON(['ok' => 0, 'error_text' => 'Could not decrypt FHIR bundle: ' . $e->getMessage()]); + } } } @@ -2378,7 +2383,8 @@ class AbdmGateway extends BaseController // ========================================================================= /** - * Persist an encrypted FHIR bundle to health_records and return the new row id. + * Persist a FHIR bundle to health_records and return the new row id. + * Stores both a plain-text copy in record_data and an encrypted copy in fhir_bundle_enc. * Returns 0 when the table is absent or on any DB error (fail-open). * * @param array{patient_id: int, abha_id: string, hi_type: string, entity_type: string, @@ -2407,6 +2413,7 @@ class AbdmGateway extends BaseController 'hi_type' => (string) ($data['hi_type'] ?? 'unknown'), 'entity_type' => (string) ($data['entity_type'] ?? ''), 'entity_id' => (string) ($data['entity_id'] ?? ''), + 'record_data' => $rawBundle !== '' ? $rawBundle : null, 'fhir_bundle_enc' => $encPayload !== '' ? $encPayload : null, 'push_status' => 'queued', 'push_at' => $now, diff --git a/app/Controllers/Opd_prescription.php b/app/Controllers/Opd_prescription.php index 7cc83f8..902e1de 100644 --- a/app/Controllers/Opd_prescription.php +++ b/app/Controllers/Opd_prescription.php @@ -5189,7 +5189,7 @@ class Opd_prescription extends BaseController // Submit button . 'document.getElementById("btnSubmit").addEventListener("click",function(){' . ' const btn=this,origHtml=btn.innerHTML;' - . ' if(!confirm("Submit FHIR bundle for OPD #' . $opdId . ' to ABDM bridge?"))return;' + . ' if(!confirm("Submit FHIR bundle for OPD #' . $opdId . ' to ABDM gateway?"))return;' . ' btn.disabled=true;btn.innerHTML=\'Submitting\u2026\';' . ' const body=new URLSearchParams({opd_id:CTX.opdId,opd_session_id:CTX.sessionId});' . ' body.append(CSRF_NAME,CSRF_HASH);' @@ -5337,54 +5337,41 @@ class Opd_prescription extends BaseController } try { - $connector = new \App\Libraries\Abdm\EAtriaBridgeConnector(); + $gatewayUrl = base_url('AbdmGateway/share_prescription_bundle'); + $cookieHeader = (string) $this->request->getHeaderLine('Cookie'); - $visitLabel = $visitDate !== '' ? date('d M Y', strtotime($visitDate)) : date('d M Y'); - $displayLabel = 'OPD Visit ' . $visitLabel . ($doctorName !== '' ? ' - Dr. ' . $doctorName : ''); + $client = service('curlrequest', [ + 'timeout' => 60, + 'http_errors' => false, + ]); - // Keep a stable OPD visit reference keyed by visit date for ABDM care-context linkage. - $ccRef = 'OPD-' . $opdId . '-' . $visitDate; + $response = $client->post($gatewayUrl, [ + 'headers' => array_filter([ + 'X-Requested-With' => 'XMLHttpRequest', + 'Cookie' => $cookieHeader !== '' ? $cookieHeader : null, + ]), + 'form_params' => [ + 'opd_id' => $opdId, + 'opd_session_id' => $sessionId, + 'patient_id' => $patientId, + 'abha_id' => $abhaId, + 'consent_handle' => (string) ($this->request->getPost('consent_handle') ?? ''), + csrf_token() => csrf_hash(), + ], + ]); - $pushData = [ - 'patient_id' => (string) $patientId, - 'patient_name' => $patientName, - 'record_type' => 'OPConsultRecord', - 'hi_type' => 'OPConsultRecord', - 'visit_date' => $visitDate, - 'care_context_reference' => $ccRef, - 'care_context_display' => $displayLabel, - 'notes' => $displayLabel, - 'queue_id' => $ccRef, - 'abha_address' => $abhaId, - 'record_data' => $bundle, - ]; - if ($doctorName !== '') { - $pushData['doctor_name'] = $doctorName; + $httpCode = (int) $response->getStatusCode(); + $result = json_decode((string) $response->getBody(), true); + if (! is_array($result)) { + $result = []; } - $result = $connector->pushRecord($pushData); - $httpCode = (int) ($result['http_code'] ?? 0); - - // 409 = duplicate queue_id — record already stored in bridge - if ($httpCode === 409) { - return $this->response->setJSON([ - 'ok' => 1, - 'duplicate' => 1, - 'bridge_id' => (int) ($result['id'] ?? 0), - 'queue_id' => (string) ($result['queue_id'] ?? ''), - 'message' => 'Record already stored in bridge (duplicate queue_id). Use existing record.', - 'http_code' => $httpCode, - 'csrfName' => csrf_token(), - 'csrfHash' => csrf_hash(), - ]); - } - - $ok = (int) ($result['ok'] ?? 0); - $queueId = (string) ($result['queue_id'] ?? ''); - $bridgeId = (int) ($result['id'] ?? 0); - $msg = (string) ($result['message'] ?? ($ok === 1 - ? 'FHIR bundle submitted to ABDM bridge successfully.' - : ($result['error_text'] ?? 'Submission failed. Check bridge configuration.'))); + $ok = (int) ($result['ok'] ?? 0); + $queueId = (string) ($result['queue_id'] ?? ''); + $bridgeId = (int) ($result['bridge_id'] ?? $result['id'] ?? 0); + $msg = (string) ($result['message'] ?? $result['error_text'] ?? ($ok === 1 + ? 'FHIR bundle submitted to ABDM gateway successfully.' + : 'Submission failed. Check gateway configuration.')); return $this->response->setJSON([ 'ok' => $ok, @@ -5399,7 +5386,7 @@ class Opd_prescription extends BaseController log_message('error', '[fhir_bundle_submit] OPD#' . $opdId . ': ' . $e->getMessage()); return $this->response->setJSON([ 'ok' => 0, - 'message' => 'Bridge connection failed: ' . $e->getMessage(), + 'message' => 'Gateway handoff failed: ' . $e->getMessage(), 'csrfName' => csrf_token(), 'csrfHash' => csrf_hash(), ]); diff --git a/app/Database/Migrations/2026-06-02-000608_AddRecordDataToHealthRecords.php b/app/Database/Migrations/2026-06-02-000608_AddRecordDataToHealthRecords.php new file mode 100644 index 0000000..730dfc7 --- /dev/null +++ b/app/Database/Migrations/2026-06-02-000608_AddRecordDataToHealthRecords.php @@ -0,0 +1,81 @@ +db->tableExists('health_records')) { + return; + } + + $fields = $this->db->getFieldNames('health_records'); + if (! in_array('record_data', $fields, true)) { + $this->forge->addColumn('health_records', [ + 'record_data' => [ + 'type' => 'LONGTEXT', + 'null' => true, + 'after' => 'fhir_bundle_enc', + ], + ]); + } + + $fields = $this->db->getFieldNames('health_records'); + if (! in_array('fhir_bundle_enc', $fields, true) || ! in_array('record_data', $fields, true)) { + return; + } + + if (! FhirEncryptionService::isSupported()) { + return; + } + + try { + $enc = new FhirEncryptionService(); + $rows = $this->db->table('health_records') + ->select('id, fhir_bundle_enc, record_data') + ->where('record_data IS NULL', null, false) + ->orWhere('record_data', '') + ->get() + ->getResultArray(); + + foreach ($rows as $row) { + $encoded = trim((string) ($row['fhir_bundle_enc'] ?? '')); + if ($encoded === '') { + continue; + } + + try { + $plaintext = $enc->decrypt($encoded); + } catch (\Throwable) { + continue; + } + + if ($plaintext === '') { + continue; + } + + $this->db->table('health_records') + ->where('id', (int) ($row['id'] ?? 0)) + ->update(['record_data' => $plaintext]); + } + } catch (\Throwable) { + // Fail-open: the new column is the important part; backfill is best-effort. + } + } + + public function down(): void + { + if (! $this->db->tableExists('health_records')) { + return; + } + + $fields = $this->db->getFieldNames('health_records'); + if (in_array('record_data', $fields, true)) { + $this->forge->dropColumn('health_records', 'record_data'); + } + } +} \ No newline at end of file