fix(abdm): generate physical exam observations, extract comorbidities, and trigger FHIR sync only on final discharge
This commit is contained in:
parent
0344561808
commit
fdd188df6e
|
|
@ -3466,8 +3466,20 @@ class AbdmGateway extends BaseController
|
|||
}
|
||||
}
|
||||
|
||||
// Add comorbidities from NABH
|
||||
// Add comorbidities from NABH, with fallback to latest opd_prescription / history
|
||||
$coMorbRaw = trim((string) ($nabhMeta['co_morbidities'] ?? ''));
|
||||
if ($coMorbRaw === '' && $patientId > 0 && $this->db && $this->db->tableExists('opd_prescription')) {
|
||||
$latestPresc = $this->db->table('opd_prescription')
|
||||
->select('Prescriber_Remarks')
|
||||
->where('p_id', $patientId)
|
||||
->orderBy('id', 'DESC')
|
||||
->get(1)->getRowArray();
|
||||
if (! empty($latestPresc['Prescriber_Remarks'])) {
|
||||
if (preg_match('/^\s*Co-Morbidities\s*:\s*(.+)$/im', (string) $latestPresc['Prescriber_Remarks'], $m)) {
|
||||
$coMorbRaw = trim($m[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($coMorbRaw !== '') {
|
||||
$coMorbParts = preg_split('/[,;\n]+/', $coMorbRaw);
|
||||
foreach ($coMorbParts as $cPart) {
|
||||
|
|
|
|||
|
|
@ -1074,6 +1074,16 @@ class Ipd extends BaseController
|
|||
->where('id', $caseId)
|
||||
->update(['status' => 1]);
|
||||
}
|
||||
|
||||
// Trigger complete, final FHIR Discharge Summary generation & ABDM sync
|
||||
try {
|
||||
$patientId = (int) ($currentIpd['p_id'] ?? 0);
|
||||
$dischargeCtrl = new \App\Controllers\Ipd_discharge();
|
||||
$dischargeCtrl->initController($this->request, $this->response, service('logger'));
|
||||
$dischargeCtrl->enqueueIpdDischargeSync($ipdId, $patientId, 'final_discharge');
|
||||
} catch (\Throwable $e) {
|
||||
log_message('warning', '[updateDischargeStatus] ABDM discharge sync trigger failed: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return $this->buildDischargeProcessResponse($ipdId, 'Discharge status updated.');
|
||||
|
|
|
|||
|
|
@ -9361,7 +9361,17 @@ class Ipd_discharge extends BaseController
|
|||
// Do not block discharge workflows if queue service is unavailable.
|
||||
}
|
||||
|
||||
// Also persist ABDM-compatible FHIR bundle in sync tables/outbox for M2 push.
|
||||
// Only persist ABDM-compatible FHIR bundle if the patient is officially discharged
|
||||
// (discarge_patient_status > 0 and valid discharge_date) or explicitly finalized.
|
||||
// Intermediate draft saves while patient is still admitted must not create incomplete bundles.
|
||||
$dischargeStatus = (int) ($ipdRow['discarge_patient_status'] ?? 0);
|
||||
$dischargeDate = trim((string) ($ipdRow['discharge_date'] ?? ''));
|
||||
$isDischarged = $dischargeStatus > 0 && $dischargeDate !== '' && $dischargeDate !== '0000-00-00';
|
||||
|
||||
if (! $isDischarged && ! in_array($action, ['final_discharge', 'force', 'auto_push'], true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fail-open: discharge save flow must never fail due to ABDM sync issues.
|
||||
try {
|
||||
$this->enqueueIpdDischargeFhirSync($ipdId, $patientId, $ipdRow, $patientRow);
|
||||
|
|
@ -9478,8 +9488,19 @@ class Ipd_discharge extends BaseController
|
|||
}
|
||||
}
|
||||
|
||||
// Comorbidities from NABH metadata
|
||||
// Comorbidities from NABH metadata with fallback to latest opd_prescription / history
|
||||
$coMorbRaw = trim((string) ($nabhMeta['co_morbidities'] ?? ''));
|
||||
if ($coMorbRaw === '' && $patientId > 0 && $this->db->tableExists('opd_prescription')) {
|
||||
$latestOpdRow = $this->db->table('opd_prescription')
|
||||
->where('p_id', $patientId)
|
||||
->orderBy('id', 'DESC')
|
||||
->get(1)
|
||||
->getRowArray();
|
||||
if (! empty($latestOpdRow)) {
|
||||
$hydratedNabh = $this->hydrateNabhFieldsFromOpdRow($latestOpdRow);
|
||||
$coMorbRaw = trim((string) ($hydratedNabh['co_morbidities'] ?? ''));
|
||||
}
|
||||
}
|
||||
if ($coMorbRaw !== '') {
|
||||
$coMorbParts = preg_split('/[,;\n]+/', $coMorbRaw);
|
||||
foreach ($coMorbParts as $cPart) {
|
||||
|
|
|
|||
|
|
@ -326,6 +326,114 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
|
|||
$builder->addMedicationRequest($medResource);
|
||||
}
|
||||
|
||||
// ── Physical Examination & Vital Signs ──────────────────────────────
|
||||
$rawObservations = (array) ($source['ui_physical_exam'] ?? $source['observations'] ?? []);
|
||||
$validObservations = [];
|
||||
foreach ($rawObservations as $idx => $obs) {
|
||||
if (! is_array($obs)) {
|
||||
continue;
|
||||
}
|
||||
$obsText = trim((string) ($obs['text'] ?? $obs['name'] ?? ''));
|
||||
$obsValue = trim((string) ($obs['value'] ?? ''));
|
||||
if ($obsText === '' || ! $this->isMeaningfulValue($obsText)) {
|
||||
continue;
|
||||
}
|
||||
if ($obsValue === '' || ! $this->isMeaningfulValue($obsValue)) {
|
||||
continue;
|
||||
}
|
||||
$validObservations[$idx] = $obs;
|
||||
}
|
||||
|
||||
foreach ($validObservations as $idx => $obs) {
|
||||
$obsText = trim((string) ($obs['text'] ?? $obs['name'] ?? ''));
|
||||
$obsValue = trim((string) ($obs['value'] ?? ''));
|
||||
$obsCategoryText = trim((string) ($obs['category'] ?? 'Physical Examination'));
|
||||
$obsCategoryCode = trim((string) ($obs['category_code'] ?? ''));
|
||||
$effectiveAt = (string) ($obs['effective_at'] ?? $obs['effective_date_time'] ?? $timestamp);
|
||||
|
||||
$isVital = in_array(strtolower($obsCategoryCode), ['vital-signs', 'vitals'], true)
|
||||
|| preg_match('/^(pulse|heart rate|bp|blood pressure|respiration|respiratory rate|temperature|temp|spo2|oxygen)/i', $obsText);
|
||||
|
||||
$catCode = $isVital ? 'vital-signs' : 'exam';
|
||||
$catDisplay = $isVital ? 'Vital Signs' : 'Exam';
|
||||
|
||||
$observationId = 'discharge-observation-' . $recordId . '-' . $idx;
|
||||
|
||||
$coding = [];
|
||||
$lowerText = strtolower($obsText);
|
||||
if (str_contains($lowerText, 'pulse') || str_contains($lowerText, 'heart rate')) {
|
||||
$coding[] = ['system' => 'http://snomed.info/sct', 'code' => '8499008', 'display' => 'Pulse rate'];
|
||||
} elseif (str_contains($lowerText, 'respiration') || str_contains($lowerText, 'respiratory')) {
|
||||
$coding[] = ['system' => 'http://snomed.info/sct', 'code' => '86290005', 'display' => 'Respiratory rate'];
|
||||
} elseif (str_contains($lowerText, 'pallor')) {
|
||||
$coding[] = ['system' => 'http://snomed.info/sct', 'code' => '271782001', 'display' => 'Pallor'];
|
||||
} elseif (str_contains($lowerText, 'jaundice') || str_contains($lowerText, 'icterus')) {
|
||||
$coding[] = ['system' => 'http://snomed.info/sct', 'code' => '18165001', 'display' => 'Jaundice'];
|
||||
} elseif (str_contains($lowerText, 'cyanosis')) {
|
||||
$coding[] = ['system' => 'http://snomed.info/sct', 'code' => '3415004', 'display' => 'Cyanosis'];
|
||||
} elseif (str_contains($lowerText, 'clubbing')) {
|
||||
$coding[] = ['system' => 'http://snomed.info/sct', 'code' => '30760006', 'display' => 'Clubbing'];
|
||||
} elseif (str_contains($lowerText, 'edema') || str_contains($lowerText, 'oedema')) {
|
||||
$coding[] = ['system' => 'http://snomed.info/sct', 'code' => '267038008', 'display' => 'Edema'];
|
||||
} elseif (str_contains($lowerText, 'jvp') || str_contains($lowerText, 'jugular')) {
|
||||
$coding[] = ['system' => 'http://snomed.info/sct', 'code' => '271649006', 'display' => 'Jugular venous pressure'];
|
||||
} elseif (str_contains($lowerText, 'bp') || str_contains($lowerText, 'blood pressure')) {
|
||||
$coding[] = ['system' => 'http://snomed.info/sct', 'code' => '75367002', 'display' => 'Blood pressure'];
|
||||
} elseif (str_contains($lowerText, 'temp')) {
|
||||
$coding[] = ['system' => 'http://snomed.info/sct', 'code' => '386725007', 'display' => 'Body temperature'];
|
||||
} elseif (str_contains($lowerText, 'spo2') || str_contains($lowerText, 'oxygen saturation')) {
|
||||
$coding[] = ['system' => 'http://snomed.info/sct', 'code' => '431314004', 'display' => 'Oxygen saturation'];
|
||||
} else {
|
||||
$resolved = $this->codingResolver->resolveSnomedForDiagnosisOrFinding((string) ($obs['code'] ?? ''), $obsText);
|
||||
$coding = $this->withoutPlaceholderCoding((array) ($resolved['coding'] ?? []));
|
||||
if ($coding === []) {
|
||||
$coding[] = ['system' => 'http://snomed.info/sct', 'code' => '404684003', 'display' => 'Clinical finding'];
|
||||
}
|
||||
}
|
||||
|
||||
$obsResource = [
|
||||
'resourceType' => 'Observation',
|
||||
'id' => $observationId,
|
||||
'meta' => ['profile' => ['https://nrces.in/ndhm/fhir/r4/StructureDefinition/Observation']],
|
||||
'text' => [
|
||||
'status' => 'generated',
|
||||
'div' => '<div xmlns="http://www.w3.org/1999/xhtml"><p><b>' . htmlspecialchars($obsText) . ' (' . htmlspecialchars($obsCategoryText) . '):</b> ' . htmlspecialchars($obsValue) . '</p></div>',
|
||||
],
|
||||
'status' => 'final',
|
||||
'category' => [[
|
||||
'coding' => [[
|
||||
'system' => 'http://terminology.hl7.org/CodeSystem/observation-category',
|
||||
'code' => $catCode,
|
||||
'display' => $catDisplay,
|
||||
]],
|
||||
'text' => $obsCategoryText !== '' ? $obsCategoryText : $catDisplay,
|
||||
]],
|
||||
'code' => [
|
||||
'coding' => $coding,
|
||||
'text' => $obsText,
|
||||
],
|
||||
'subject' => ['reference' => $patientRef, 'display' => (string) ($source['patient']['name'] ?? $source['patient_name'] ?? 'Patient')],
|
||||
'effectiveDateTime' => $effectiveAt,
|
||||
];
|
||||
|
||||
$unit = trim((string) ($obs['unit'] ?? ''));
|
||||
if ($unit === '' && preg_match('/^([\d\.]+)\s*(\/min|bpm|mmHg|°c|°f|%)$/i', $obsValue, $uParts)) {
|
||||
$obsResource['valueQuantity'] = [
|
||||
'value' => (float) $uParts[1],
|
||||
'unit' => $uParts[2],
|
||||
];
|
||||
} elseif (is_numeric($obsValue)) {
|
||||
$obsResource['valueQuantity'] = [
|
||||
'value' => (float) $obsValue,
|
||||
'unit' => $unit,
|
||||
];
|
||||
} else {
|
||||
$obsResource['valueString'] = $obsValue;
|
||||
}
|
||||
|
||||
$builder->addObservation($obsResource);
|
||||
}
|
||||
|
||||
// ── Investigations ─────────────────────────────────────────────────────
|
||||
foreach ((array) ($source['ui_investigations'] ?? $source['investigations'] ?? []) as $idx => $inv) {
|
||||
$text = trim((string) ($inv['text'] ?? ''));
|
||||
|
|
@ -670,6 +778,18 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
|
|||
];
|
||||
}
|
||||
|
||||
// Physical Examination & Vital Signs
|
||||
if (! empty($validObservations)) {
|
||||
$sectionDefinitions[] = [
|
||||
'title' => 'Physical Examination',
|
||||
'code' => '425044008',
|
||||
'display' => 'Physical examination section',
|
||||
'prefix' => 'discharge-observation-',
|
||||
'items_indexed' => $validObservations,
|
||||
'entry_display' => 'Observation',
|
||||
];
|
||||
}
|
||||
|
||||
// Surgery / Procedure
|
||||
$surgeries = (array) ($source['ui_surgeries'] ?? $source['procedures'] ?? []);
|
||||
if (! empty($surgeries)) {
|
||||
|
|
@ -1078,7 +1198,7 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
|
|||
if (empty($diagnoses) && ! empty($source['conditions'])) {
|
||||
$diagnoses = (array) $source['conditions'];
|
||||
}
|
||||
$vitals = (array) ($source['observations'] ?? []);
|
||||
$vitals = (array) ($source['ui_physical_exam'] ?? $source['observations'] ?? []);
|
||||
$procedures = (array) ($source['procedures'] ?? []);
|
||||
$medications = ! empty($source['medications']) ? (array) $source['medications'] : (array) ($source['ui_discharge_medicine'] ?? []);
|
||||
$carePlans = ! empty($source['care_plans']) ? (array) $source['care_plans'] : (array) ($source['ui_care_plans'] ?? $source['ui_discharge_summary'] ?? []);
|
||||
|
|
|
|||
|
|
@ -988,6 +988,7 @@ final class FhirGeneratorsTest extends CIUnitTestCase
|
|||
$this->assertContains('Patient', $resourceTypes);
|
||||
$this->assertContains('Encounter', $resourceTypes);
|
||||
$this->assertContains('Condition', $resourceTypes);
|
||||
$this->assertContains('Observation', $resourceTypes);
|
||||
$this->assertContains('MedicationRequest', $resourceTypes);
|
||||
$this->assertContains('DiagnosticReport', $resourceTypes);
|
||||
$this->assertContains('CarePlan', $resourceTypes);
|
||||
|
|
@ -996,6 +997,7 @@ final class FhirGeneratorsTest extends CIUnitTestCase
|
|||
$composition = $bundle['entry'][0]['resource'];
|
||||
$sectionTitles = array_column($composition['section'], 'title');
|
||||
$this->assertContains('Chief complaints', $sectionTitles);
|
||||
$this->assertContains('Physical Examination', $sectionTitles);
|
||||
$this->assertContains('Medications', $sectionTitles);
|
||||
$this->assertContains('Care Plan', $sectionTitles);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user