Store ABDM bundles in health records

This commit is contained in:
DevSoftTech Server 2026-06-03 11:05:40 +05:30
parent 45abd3ca76
commit c692340bb2
3 changed files with 130 additions and 55 deletions

View File

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

View File

@ -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=\'<span class="spinner-border spinner-border-sm me-1"></span>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(),
]);

View File

@ -0,0 +1,81 @@
<?php
namespace App\Database\Migrations;
use App\Libraries\FhirEncryptionService;
use CodeIgniter\Database\Migration;
class AddRecordDataToHealthRecords extends Migration
{
public function up(): void
{
if (! $this->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');
}
}
}