feat(abdm): enable proactive ABDM bridge push and gateway linking for nursing wellness records

This commit is contained in:
DevSoftTech Server 2026-10-07 11:50:51 +05:30
parent 0441ebe08a
commit a3537c0b29
5 changed files with 341 additions and 18 deletions

View File

@ -6469,6 +6469,25 @@ class AbdmGateway extends BaseController
}
}
} elseif ($taskType === 'wellness_record_publish') {
if ($this->db->tableExists('patient_wellness_records')) {
$wRec = $this->db->table('patient_wellness_records')->where('id', $taskEntityId)->get(1)->getRowArray();
if (! empty($wRec)) {
$vDate = ! empty($wRec['recorded_at']) ? $wRec['recorded_at'] : date('Y-m-d');
$dateStr = date('d M Y', strtotime((string) $vDate));
$cleanDate = date('Ymd', strtotime((string) $vDate));
$ccRef = ! empty($wRec['care_context_reference']) ? $wRec['care_context_reference'] : ('WELLNESS-' . $patientId . '-W' . $taskEntityId . '-' . $cleanDate);
$display = 'Wellness Record - ' . $dateStr;
$addContext([
'careContextId' => $ccRef,
'referenceNumber' => $ccRef,
'display' => $display,
'record_type' => 'WellnessRecord',
'patient_id' => $patientId,
'is_fhir_ready' => true,
'is_primary' => true,
]);
}
}
if ($this->db->tableExists('opd_prescription')) {
$wopd = $this->db->table('opd_prescription')->where('id', $taskEntityId)->get(1)->getRowArray();
if (! empty($wopd)) {
@ -9244,7 +9263,7 @@ class AbdmGateway extends BaseController
};
}
private function pushAdditionalHiRecord(array $payload, int $patientId, string $abhaId, string $consentHandle)
public function pushAdditionalHiRecord(array $payload, int $patientId, string $abhaId, string $consentHandle = '')
{
$abhaIdentity = $this->resolvePatientAbhaIdentity($patientId, $abhaId);
$abhaNumber = $abhaIdentity['abha_id'];
@ -9394,6 +9413,42 @@ class AbdmGateway extends BaseController
return null;
}
// 1. Check patient_wellness_records first (New M2 Comprehensive Wellness & Vitals)
if ($this->db->tableExists('patient_wellness_records')) {
$wBuilder = $this->db->table('patient_wellness_records')->where('patient_id', $patientId);
if ($opdId > 0) {
$wBuilder->groupStart()
->where('id', $opdId)
->orWhere('patient_id', $patientId)
->groupEnd();
}
$wRow = $wBuilder->orderBy('id', 'DESC')->get(1)->getRowArray();
if (! empty($wRow)) {
$bundle = ! empty($wRow['fhir_bundle_json']) ? json_decode((string) $wRow['fhir_bundle_json'], true) : [];
$visitDate = date('Y-m-d', strtotime((string) ($wRow['recorded_at'] ?? 'now')));
$entityId = (string) $wRow['id'];
$cleanDate = date('Ymd', strtotime($visitDate));
$ccRef = ! empty($wRow['care_context_reference'])
? (string) $wRow['care_context_reference']
: ('WELLNESS-' . $patientId . '-W' . $entityId . '-' . $cleanDate);
$ccDisplay = 'Wellness Record - ' . date('d/m/Y', strtotime($visitDate));
if (! empty($bundle)) {
return [
'hi_type' => 'WellnessRecord',
'entity_type' => 'wellness',
'entity_id' => $entityId,
'patient_id' => $patientId,
'patient_name' => $this->patientDisplayName($patientRow),
'visit_date' => $visitDate,
'care_context_reference' => $ccRef,
'care_context_display' => $ccDisplay,
'bundle' => $bundle,
];
}
}
}
$row = [];
if ($this->db->tableExists('opd_prescription')) {
$builder = $this->db->table('opd_prescription')->where('p_id', $patientId);

View File

@ -1776,6 +1776,71 @@ class AbdmTaskBoard extends BaseController
private function backfillWellnessTasks(): void
{
// 1. Backfill from patient_wellness_records (ABDM M2 Comprehensive Wellness & Vitals)
if ($this->db->tableExists('patient_wellness_records') && $this->db->tableExists('patient_master')) {
$patientFields = $this->db->getFieldNames('patient_master') ?? [];
$abhaCol = $this->resolveExistingColumn($patientFields, ['abha_address', 'abha_id', 'abha_no', 'abha']);
if ($abhaCol !== null) {
$wellnessRows = $this->db->table('patient_wellness_records w')
->select('w.id, w.patient_id, w.recorded_at, w.care_context_reference, w.bridge_record_id, w.abdm_status, p.p_fname, p.p_lname, p.' . $abhaCol . ' as abha_id', false)
->join('patient_master p', 'p.id = w.patient_id', 'inner')
->where('p.' . $abhaCol . ' !=', '')
->orderBy('w.id', 'DESC')
->limit(200)
->get()
->getResultArray();
foreach ($wellnessRows as $wRow) {
$patientId = (int) ($wRow['patient_id'] ?? 0);
$wellnessId = (int) ($wRow['id'] ?? 0);
$abhaId = trim((string) ($wRow['abha_id'] ?? ''));
if ($patientId <= 0 || $wellnessId <= 0 || ! $this->isValidAbhaNumber($abhaId)) {
continue;
}
$exists = $this->db->table('abdm_work_tasks')
->select('id, status')
->where('task_type', 'wellness_record_publish')
->where('entity_type', 'wellness')
->where('entity_id', (string) $wellnessId)
->get(1)
->getRowArray();
if (! empty($exists)) {
continue;
}
$patientName = trim(($wRow['p_fname'] ?? '') . ' ' . ($wRow['p_lname'] ?? ''));
$isPushed = (! empty($wRow['bridge_record_id']) || in_array($wRow['abdm_status'] ?? '', ['queued', 'pushed', 'linked'], true));
$taskId = $this->taskService->createOrRefreshTask(
'wellness_record_publish',
'patient_wellness_records',
'wellness',
(string) $wellnessId,
$patientId,
$patientName,
$abhaId,
'submit',
[
'wellness_id' => $wellnessId,
'care_context_reference' => (string) ($wRow['care_context_reference'] ?? ''),
'bridge_record_id' => (int) ($wRow['bridge_record_id'] ?? 0),
'clinical_timestamp' => (string) ($wRow['recorded_at'] ?? ''),
'trigger' => 'wellness_records.backfilled',
]
);
if ($isPushed && $taskId > 0) {
$this->db->table('abdm_work_tasks')->where('id', $taskId)->update([
'status' => 'completed',
'result_summary' => 'Pushed to ABDM Bridge (ID #' . ($wRow['bridge_record_id'] ?? '') . ')',
'updated_at' => date('Y-m-d H:i:s'),
]);
}
}
}
}
// 2. Also backfill from opd_prescription (for legacy OPD vitals)
if (! $this->db->tableExists('opd_prescription') || ! $this->db->tableExists('patient_master')) {
return;
}

View File

@ -1068,17 +1068,22 @@ class NursingApi extends BaseController
$fhirBundle = $genResult['fhir_bundle'] ?? [];
if (! empty($fhirBundle)) {
$fhirBundleJson = json_encode($fhirBundle, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
$db->table('patient_wellness_records')->where('id', $wellnessId)->update(['fhir_bundle_json' => $fhirBundleJson]);
$db->table('patient_wellness_records')->where('id', $wellnessId)->update([
'fhir_bundle_json' => $fhirBundleJson,
'care_context_reference' => $careContextRef,
]);
}
} catch (\Throwable $e) {
log_message('warning', '[savePatientWellness] FHIR generation error: ' . $e->getMessage());
}
// Also register in health_records for ABDM discovery & push
// Register in health_records for ABDM discovery & push
$healthRecordId = 0;
if ($db->tableExists('health_records')) {
$effectiveAbha = trim((string) ($patient['abha_address'] ?? $patient['abha_id'] ?? ''));
$db->table('health_records')->insert([
'patient_id' => $patientId,
'abha_id' => $patient['abha_address'] ?? $patient['abha_id'] ?? null,
'abha_id' => $effectiveAbha !== '' ? $effectiveAbha : null,
'hi_type' => 'WellnessRecord',
'entity_type' => 'wellness',
'entity_id' => (string) $wellnessId,
@ -1089,13 +1094,113 @@ class NursingApi extends BaseController
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
$healthRecordId = (int) $db->insertID();
}
// Proactive push to ABDM Bridge when patient has ABHA ID or Address
$pushStatus = 'local_discovery_ready';
$bridgeRecordId = null;
$queueId = null;
$pushMsg = '';
$effectiveAbha = trim((string) ($patient['abha_address'] ?? $patient['abha_id'] ?? ''));
if ($effectiveAbha !== '') {
try {
$connector = \App\Libraries\Abdm\AbdmConnectorFactory::make();
$pushPayload = [
'patient_id' => (string) $patientId,
'patient_name' => trim(($patient['p_fname'] ?? '') . ' ' . ($patient['p_lname'] ?? '')),
'abha_id' => (string) ($patient['abha_id'] ?? ''),
'abha_address' => (string) ($patient['abha_address'] ?? ''),
'year_of_birth' => ! empty($patient['dob']) ? date('Y', strtotime((string) $patient['dob'])) : null,
'hi_type' => 'WellnessRecord',
'record_type' => 'WellnessRecord',
'visit_date' => date('Y-m-d', strtotime($recordedAt)),
'care_context_reference' => $careContextRef,
'care_context_display' => 'Wellness Record - ' . date('d/m/Y', strtotime($recordedAt)),
'notes' => 'Wellness & Vitals Record',
'queue_id' => $careContextRef,
'record_data' => ! empty($fhirBundle) ? $fhirBundle : json_decode((string) ($fhirBundleJson ?? '{}'), true),
];
$pushRes = $connector->pushRecord($pushPayload);
$pushOk = (int) ($pushRes['ok'] ?? 0);
$httpCode = (int) ($pushRes['http_code'] ?? 0);
$statusVal = strtolower((string) ($pushRes['status'] ?? ''));
if ($pushOk === 1 || in_array($httpCode, [200, 201, 202, 409], true) || in_array($statusVal, ['queued', 'pushed', 'linked'], true)) {
$pushStatus = 'queued';
$bridgeRecordId = (int) ($pushRes['record_id'] ?? 0);
$queueId = (string) ($pushRes['queue_id'] ?? '');
$pushMsg = ' & pushed to ABDM Bridge' . ($bridgeRecordId > 0 ? " (Record #{$bridgeRecordId})" : '');
} else {
$pushStatus = 'failed';
$pushMsg = ' (ABDM Bridge push pending: ' . ($pushRes['error_text'] ?? 'Connection error') . ')';
}
} catch (\Throwable $pe) {
log_message('warning', '[savePatientWellness] Bridge push error: ' . $pe->getMessage());
$pushStatus = 'failed';
}
}
// Update patient_wellness_records with push status & references
$db->table('patient_wellness_records')->where('id', $wellnessId)->update([
'care_context_reference' => $careContextRef,
'bridge_record_id' => $bridgeRecordId > 0 ? $bridgeRecordId : null,
'queue_id' => $queueId !== '' ? $queueId : null,
'abdm_status' => $pushStatus,
]);
// Update health_records
if ($healthRecordId > 0) {
$db->table('health_records')->where('id', $healthRecordId)->update([
'push_status' => $pushStatus,
'bridge_record_id' => $bridgeRecordId > 0 ? $bridgeRecordId : null,
'abdm_txn_id' => $queueId !== '' ? $queueId : null,
'push_at' => $pushStatus === 'queued' ? date('Y-m-d H:i:s') : null,
'updated_at' => date('Y-m-d H:i:s'),
]);
}
// Create or refresh task in abdm_work_tasks
if ($db->tableExists('abdm_work_tasks')) {
try {
$taskService = new \App\Libraries\AbdmWorkTaskService();
$tId = $taskService->createOrRefreshTask(
'wellness_record_publish',
'patient_wellness_records',
'wellness',
(string) $wellnessId,
$patientId,
trim(($patient['p_fname'] ?? '') . ' ' . ($patient['p_lname'] ?? '')),
$effectiveAbha,
'submit',
[
'wellness_id' => $wellnessId,
'care_context_reference' => $careContextRef,
'bridge_record_id' => $bridgeRecordId,
'clinical_timestamp' => $recordedAt,
'trigger' => 'nursing.wellness_saved',
]
);
if ($pushStatus === 'queued' && $tId > 0) {
$db->table('abdm_work_tasks')->where('id', $tId)->update([
'status' => 'completed',
'result_summary' => 'Pushed to ABDM Bridge (ID #' . $bridgeRecordId . ', Queue: ' . $queueId . ')',
'updated_at' => date('Y-m-d H:i:s'),
]);
}
} catch (\Throwable $te) {
log_message('warning', '[savePatientWellness] Task create error: ' . $te->getMessage());
}
}
return $this->response->setJSON([
'status' => 1,
'message' => 'ABDM M2 Wellness & Vitals recorded successfully',
'message' => 'ABDM M2 Wellness & Vitals recorded' . $pushMsg,
'wellness_id' => $wellnessId,
'care_context_reference' => $careContextRef,
'abdm_push_status' => $pushStatus,
'bridge_record_id' => $bridgeRecordId,
'bmi' => $bmi,
]);
}

View File

@ -659,6 +659,36 @@ class DoctorDocument extends BaseController
return $this->response->setStatusCode(400)->setJSON(['ok' => 0, 'error_text' => 'patient_id is required']);
}
// Check if there is an existing patient_wellness_records row with fhir_bundle_json
if ($this->db->tableExists('patient_wellness_records')) {
$wBuilder = $this->db->table('patient_wellness_records')->where('patient_id', $patientId);
if ($opdSessionId > 0) {
$wBuilder->groupStart()
->where('id', $opdSessionId)
->orWhere('patient_id', $patientId)
->groupEnd();
}
$wRow = $wBuilder->orderBy('id', 'DESC')->get(1)->getRowArray();
if (! empty($wRow['fhir_bundle_json'])) {
$decoded = json_decode((string) $wRow['fhir_bundle_json'], true);
if (is_array($decoded) && ! empty($decoded)) {
$cleanDate = date('Ymd', strtotime((string) ($wRow['recorded_at'] ?? 'now')));
$ccRef = ! empty($wRow['care_context_reference'])
? (string) $wRow['care_context_reference']
: ('WELLNESS-' . $patientId . '-W' . $wRow['id'] . '-' . $cleanDate);
return $this->response->setJSON([
'ok' => 1,
'source' => $wRow,
'bundle' => $decoded,
'fhir_bundle' => $decoded,
'validation' => ['valid' => true, 'errors' => []],
'care_context_reference' => $ccRef,
'care_context_display' => 'Wellness Record - ' . date('d/m/Y', strtotime((string) ($wRow['recorded_at'] ?? 'now'))),
]);
}
}
}
$source = $this->buildWellnessRecordSource($patientId, $opdSessionId);
if (empty($source)) {
return $this->response->setStatusCode(404)->setJSON(['ok' => 0, 'error_text' => 'Wellness source data (vitals/prescription) not found for patient #' . $patientId]);
@ -744,6 +774,40 @@ class DoctorDocument extends BaseController
$addVital('59408-5', 'Oxygen saturation in Arterial blood by Pulse oximetry', $rxRow['spo2'] ?? null, '%', '%');
$addVital('2339-0', 'Blood Glucose', $rxRow['glucose'] ?? null, 'mg/dL', 'mg/dL');
// If no vitals found in opd_prescription, check patient_wellness_records
if (empty($vitals) && $this->db->tableExists('patient_wellness_records')) {
$wBuilder = $this->db->table('patient_wellness_records')->where('patient_id', $patientId);
if ($opdSessionId > 0) {
$wBuilder->groupStart()
->where('id', $opdSessionId)
->orWhere('patient_id', $patientId)
->groupEnd();
}
$wRow = $wBuilder->orderBy('id', 'DESC')->get(1)->getRowArray();
if (! empty($wRow)) {
$addVital('8480-6', 'Systolic blood pressure', $wRow['bp_systolic'] ?? null, 'mmHg', 'mm[Hg]');
$addVital('8462-4', 'Diastolic blood pressure', $wRow['bp_diastolic'] ?? null, 'mmHg', 'mm[Hg]');
$addVital('8867-4', 'Heart rate', $wRow['pulse_rate'] ?? null, '/min', '/min');
$addVital('8302-2', 'Body height', $wRow['height_cm'] ?? null, 'cm', 'cm');
$addVital('29463-7', 'Body weight', $wRow['weight_kg'] ?? null, 'kg', 'kg');
$addVital('39156-5', 'Body Mass Index', $wRow['bmi'] ?? null, 'kg/m2', 'kg/m2');
$tC = $wRow['temperature_c'] ?? null;
if ($tC === null && ! empty($wRow['temperature_f'])) {
$tC = round((((float) $wRow['temperature_f'] - 32) * 5) / 9, 1);
}
$addVital('8310-5', 'Body temperature', $tC, 'Cel', 'Cel');
$addVital('9279-1', 'Respiratory rate', $wRow['resp_rate'] ?? null, '/min', '/min');
$addVital('59408-5', 'Oxygen saturation in Arterial blood by Pulse oximetry', $wRow['spo2'] ?? null, '%', '%');
$addVital('2339-0', 'Random Blood Glucose', $wRow['sugar_random'] ?? null, 'mg/dL', 'mg/dL');
$addVital('1558-6', 'Fasting Blood Glucose', $wRow['sugar_fasting'] ?? null, 'mg/dL', 'mg/dL');
$addVital('4548-4', 'Hemoglobin A1c', $wRow['hba1c'] ?? null, '%', '%');
$addVital('718-7', 'Hemoglobin', $wRow['hemoglobin'] ?? null, 'g/dL', 'g/dL');
$addVital('8280-0', 'Waist Circumference', $wRow['waist_circumference_cm'] ?? null, 'cm', 'cm');
$addVital('98337-9', 'Hip Circumference', $wRow['hip_circumference_cm'] ?? null, 'cm', 'cm');
$addVital('72287-6', 'Waist-hip ratio', $wRow['waist_hip_ratio'] ?? null, '{ratio}', '{ratio}');
}
}
$physicalExam = [];
foreach (['complaints', 'diagnosis', 'investigation'] as $f) {
$val = trim((string) ($rxRow[$f] ?? ''));

View File

@ -835,6 +835,16 @@ class AbdmTaskBoardSyncService
$careContextRef = trim((string) $immRec['abdm_care_context_reference']);
}
}
if ($taskType === 'wellness_record_publish' && $this->db->tableExists('patient_wellness_records')) {
$wRec = $this->db->table('patient_wellness_records')->where('id', (int) $entityId)->get(1)->getRowArray();
if (! empty($wRec)) {
if (! empty($wRec['care_context_reference'])) {
$careContextRef = trim((string) $wRec['care_context_reference']);
} else {
$careContextRef = 'WELLNESS-' . $patientId . '-W' . $entityId . '-' . $cleanVisitDate;
}
}
}
$careContextDisplay = $hiType . ' ' . $visitDate;
$pushData = [
@ -854,21 +864,36 @@ class AbdmTaskBoardSyncService
];
$bundleJson = null;
if ($taskType === 'wellness_record_publish' && class_exists('\App\Controllers\DoctorDocument')) {
try {
$docCtrl = new \App\Controllers\DoctorDocument();
$wSource = $docCtrl->buildWellnessRecordSource($patientId, $entityId);
if (! empty($wSource)) {
$wFactory = new \App\Libraries\Abdm\Fhir\FhirGeneratorFactory();
$wGen = $wFactory->wellness()->generate($wSource);
if (! empty($wGen['bundle'])) {
$pushData['bundle'] = $wGen['bundle'];
$pushData['fhir_bundle'] = $wGen['bundle'];
$bundleJson = (string) json_encode($wGen['bundle'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
if ($taskType === 'wellness_record_publish') {
if ($this->db->tableExists('patient_wellness_records')) {
$wRec = $this->db->table('patient_wellness_records')->where('id', (int) $entityId)->get(1)->getRowArray();
if (! empty($wRec['fhir_bundle_json'])) {
$decodedBundle = json_decode((string) $wRec['fhir_bundle_json'], true);
if (is_array($decodedBundle) && ! empty($decodedBundle)) {
$pushData['bundle'] = $decodedBundle;
$pushData['fhir_bundle'] = $decodedBundle;
$pushData['record_data'] = $decodedBundle;
$bundleJson = (string) $wRec['fhir_bundle_json'];
}
}
} catch (\Throwable $we) {
log_message('warning', '[processIndividualWorkTask] Wellness bundle generation error: ' . $we->getMessage());
}
if (empty($pushData['bundle']) && class_exists('\App\Controllers\DoctorDocument')) {
try {
$docCtrl = new \App\Controllers\DoctorDocument();
$wSource = $docCtrl->buildWellnessRecordSource($patientId, $entityId);
if (! empty($wSource)) {
$wFactory = new \App\Libraries\Abdm\Fhir\FhirGeneratorFactory();
$wGen = $wFactory->wellness()->generate($wSource);
if (! empty($wGen['bundle'])) {
$pushData['bundle'] = $wGen['bundle'];
$pushData['fhir_bundle'] = $wGen['bundle'];
$pushData['record_data'] = $wGen['bundle'];
$bundleJson = (string) json_encode($wGen['bundle'], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}
}
} catch (\Throwable $we) {
log_message('warning', '[processIndividualWorkTask] Wellness bundle generation error: ' . $we->getMessage());
}
}
}
@ -894,6 +919,15 @@ class AbdmTaskBoardSyncService
$bundleJson
);
if ($taskType === 'wellness_record_publish' && $this->db->tableExists('patient_wellness_records')) {
$this->db->table('patient_wellness_records')->where('id', (int) $entityId)->update([
'care_context_reference' => $careContextRef,
'bridge_record_id' => (int) ($result['record_id'] ?? 0) > 0 ? (int) $result['record_id'] : null,
'queue_id' => $queueId,
'abdm_status' => 'queued',
]);
}
return ['ok' => 1, 'queue_id' => $queueId];
}