fix(abdm): deduplicate chief complaints and medical history, fix medication coding to NRCES spec, and resilient instruction parsing

This commit is contained in:
DevSoftTech Server 2026-10-17 03:24:22 +05:30
parent 0af2d557b2
commit 0344561808
4 changed files with 345 additions and 56 deletions

View File

@ -72,6 +72,14 @@ class ResyncDischarge extends BaseCommand
}
}
CLI::write("\nConditions:", 'cyan');
foreach ($entries as $e) {
if (($e['resource']['resourceType'] ?? '') === 'Condition') {
$c = $e['resource'];
CLI::write(" - ID: " . ($c['id'] ?? '') . " | Text: " . ($c['code']['text'] ?? ''));
}
}
CLI::write("\nMedicationRequests:", 'cyan');
foreach ($entries as $e) {
if (($e['resource']['resourceType'] ?? '') === 'MedicationRequest') {

View File

@ -3429,13 +3429,6 @@ class AbdmGateway extends BaseController
? date('Y-m-d', strtotime($dischargeDate))
: ($admissionDate !== '' ? date('Y-m-d', strtotime($admissionDate)) : date('Y-m-d'));
$diagnoses = [];
foreach ($this->ipdRows('ipd_discharge_diagnosis', ['comp_report'], $ipdId) as $row) {
$text = trim((string) ($row['comp_report'] ?? ''));
if ($text !== '') {
$diagnoses[] = ['text' => $text, 'code' => ''];
}
}
$chiefComplaints = [];
foreach ($this->ipdRows('ipd_discharge_complaint', ['comp_report'], $ipdId) as $row) {
$text = trim((string) ($row['comp_report'] ?? ''));
@ -3443,19 +3436,95 @@ class AbdmGateway extends BaseController
$chiefComplaints[] = ['text' => $text, 'code' => ''];
}
}
$chiefComplaintNarrative = trim((string) (($this->ipdRows('ipd_discharge_complaint_remark', ['comp_remark'], $ipdId)[0]['comp_remark'] ?? '')));
// Build normalized lookup of chief complaints
$complaintLookup = [];
foreach ($chiefComplaints as $cc) {
$t = strtolower(trim((string) ($cc['text'] ?? '')));
if ($t !== '') {
$complaintLookup[$t] = true;
if (str_contains($t, ' - ')) {
$parts = explode(' - ', $t);
$complaintLookup[trim($parts[0])] = true;
}
}
}
// Fetch instructions early for comorbidities
$instructionRows = $this->ipdRows('ipd_discharge_instructions', ['comp_report', 'comp_remark', 'review_after'], $ipdId);
$instructionRow = ! empty($instructionRows) ? $instructionRows[0] : [];
$instructionMeta = ! empty($instructionRow) ? $this->parseJsonArray((string) ($instructionRow['comp_report'] ?? '')) : [];
$nabhMeta = is_array($instructionMeta['nabh'] ?? null) ? $instructionMeta['nabh'] : [];
// Build diagnoses / medical history
$diagnoses = [];
foreach ($this->ipdRows('ipd_discharge_diagnosis', ['comp_report'], $ipdId) as $row) {
$text = trim((string) ($row['comp_report'] ?? ''));
if ($text !== '' && ! isset($complaintLookup[strtolower($text)])) {
$diagnoses[] = ['text' => $text, 'code' => ''];
}
}
// Add comorbidities from NABH
$coMorbRaw = trim((string) ($nabhMeta['co_morbidities'] ?? ''));
if ($coMorbRaw !== '') {
$coMorbParts = preg_split('/[,;\n]+/', $coMorbRaw);
foreach ($coMorbParts as $cPart) {
$cText = trim((string) $cPart);
if ($cText !== '' && $this->isMeaningfulClinicalValue($cText) && ! isset($complaintLookup[strtolower($cText)])) {
$diagnoses[] = ['text' => $cText, 'code' => ''];
}
}
}
// Add chronic conditions from patient_master
if ($patientId > 0 && $this->db && $this->db->tableExists('patient_master')) {
$historyFields = [
'is_smoking' => 'Smoking',
'is_alcohol' => 'Alcohol',
'is_drug_abuse' => 'Drug Abuse',
'is_tobacoo' => 'Tobacco',
'is_hypertesion' => 'Hypertension',
'is_niddm' => 'Diabetes',
'is_diabetes' => 'Diabetes',
'is_ischaemic_heart_ds' => 'Ischemic Heart Disease',
'is_asthma_copd' => 'Asthma/COPD',
'is_bleeding_disorder' => 'Bleeding Disorder',
'is_heapatitis_type' => 'Hepatitis',
'is_blood_transfusion' => 'Blood Transfusion',
'is_hiv_std' => 'HIV/STD',
'is_hiv_I_II' => 'HIV/STD',
'is_epilepsy' => 'Epilepsy',
'is_tb' => 'TB',
'is_psychiatric_illness' => 'Psychiatric Illness',
'is_thyroid' => 'Thyroid',
];
foreach ($historyFields as $f => $label) {
if ((int) ($patientRow[$f] ?? 0) === 1 && ! isset($complaintLookup[strtolower($label)])) {
$diagnoses[] = ['text' => $label, 'code' => ''];
}
}
}
if (empty($diagnoses)) {
$problem = trim((string) ($ipdRow['problem'] ?? ''));
if ($problem !== '') {
if ($problem !== '' && $this->isMeaningfulClinicalValue($problem) && ! isset($complaintLookup[strtolower($problem)])) {
$diagnoses[] = ['text' => $problem, 'code' => ''];
}
}
if (empty($diagnoses) && ! empty($chiefComplaints)) {
$diagnoses = $chiefComplaints;
// Deduplicate diagnoses
$uniqueDiag = [];
$seenDiag = [];
foreach ($diagnoses as $dRow) {
$dt = strtolower(trim((string) ($dRow['text'] ?? '')));
if ($dt !== '' && empty($seenDiag[$dt])) {
$seenDiag[$dt] = true;
$uniqueDiag[] = $dRow;
}
}
if (empty($diagnoses) && empty($procedures)) {
$diagnoses[] = ['text' => 'Medical care and evaluation', 'code' => ''];
}
$chiefComplaintNarrative = trim((string) (($this->ipdRows('ipd_discharge_complaint_remark', ['comp_remark'], $ipdId)[0]['comp_remark'] ?? '')));
$diagnoses = $uniqueDiag;
$diagnosisNarrative = trim((string) (($this->ipdRows('ipd_discharge_diagnosis_remark', ['comp_remark'], $ipdId)[0]['comp_remark'] ?? '')));
$procedures = [];
@ -4300,7 +4369,34 @@ class AbdmGateway extends BaseController
}
$decoded = json_decode($json, true);
return is_array($decoded) ? $decoded : [];
if (is_array($decoded)) {
return $decoded;
}
// Resilient fallback for truncated JSON strings
$cleanIds = [];
if (preg_match('/"food_ids"\s*:\s*\[([^\]]*)\]/', $json, $m)) {
$cleanIds = array_values(array_filter(array_map('intval', explode(',', $m[1]))));
}
$extractKey = function (string $key) use ($json): string {
if (preg_match('/"' . preg_quote($key, '/') . '"\s*:\s*"([^"]*)/', $json, $m)) {
return trim($m[1]);
}
return '';
};
return [
'food_ids' => $cleanIds,
'other_text' => $extractKey('other_text'),
'nabh' => [
'drug_allergy_status' => $extractKey('drug_allergy_status'),
'drug_allergy_details' => $extractKey('drug_allergy_details'),
'adr_history' => $extractKey('adr_history'),
'current_medications' => $extractKey('current_medications'),
'co_morbidities' => $extractKey('co_morbidities'),
'hpi_note' => $extractKey('hpi_note'),
],
];
}
private function isMeaningfulClinicalValue(string $value): bool

View File

@ -5472,7 +5472,32 @@ class Ipd_discharge extends BaseController
$decoded = json_decode($raw, true);
if (! is_array($decoded)) {
return ['food_ids' => [], 'other_text' => '', 'nabh' => []];
$cleanIds = [];
if (preg_match('/"food_ids"\s*:\s*\[([^\]]*)\]/', $raw, $m)) {
$cleanIds = array_values(array_filter(array_map('intval', explode(',', $m[1]))));
}
$extractKey = function (string $key) use ($raw): string {
if (preg_match('/"' . preg_quote($key, '/') . '"\s*:\s*"([^"]*)/', $raw, $m)) {
return trim($m[1]);
}
return '';
};
return [
'food_ids' => $cleanIds,
'other_text' => $extractKey('other_text'),
'nabh' => [
'drug_allergy_status' => $extractKey('drug_allergy_status'),
'drug_allergy_details' => $extractKey('drug_allergy_details'),
'adr_history' => $extractKey('adr_history'),
'current_medications' => $extractKey('current_medications'),
'co_morbidities' => $extractKey('co_morbidities'),
'hpi_note' => $extractKey('hpi_note'),
'women_lmp' => $extractKey('women_lmp'),
'women_last_baby' => $extractKey('women_last_baby'),
'women_pregnancy_related' => $extractKey('women_pregnancy_related'),
'women_related_problems' => $extractKey('women_related_problems'),
],
];
}
$ids = $decoded['food_ids'] ?? [];
@ -9401,21 +9426,6 @@ class Ipd_discharge extends BaseController
? date('Y-m-d', strtotime($dischargeDateRaw))
: ($admissionDateRaw !== '' ? date('Y-m-d', strtotime($admissionDateRaw)) : date('Y-m-d'));
$diagnosisRows = $this->byIpdRows('ipd_discharge_diagnosis', ['comp_report'], 'id ASC', $ipdId);
$conditionRows = [];
foreach ($diagnosisRows as $row) {
$text = trim((string) ($row['comp_report'] ?? ''));
if ($text !== '') {
$conditionRows[] = ['text' => $text, 'code' => ''];
}
}
if (empty($conditionRows) && ! empty($ipdRow['problem'])) {
$prob = trim((string) $ipdRow['problem']);
if ($this->isMeaningfulDischargeValue($prob)) {
$conditionRows[] = ['text' => $prob, 'code' => ''];
}
}
$chiefComplaintsList = [];
if ($this->tableHasColumns('ipd_discharge_complaint', ['ipd_id', 'comp_report'])) {
$complaintRows = $this->byIpdRows('ipd_discharge_complaint', ['comp_report', 'comp_remark'], 'id ASC', $ipdId);
@ -9439,13 +9449,96 @@ class Ipd_discharge extends BaseController
$chiefComplaintsList[] = ['text' => $complaintRemarkText, 'code' => ''];
}
if (empty($conditionRows) && ! empty($chiefComplaintsList)) {
$conditionRows = $chiefComplaintsList;
// Build normalized lookup of chief complaints to avoid duplicating them into medical history
$complaintLookup = [];
foreach ($chiefComplaintsList as $cc) {
$t = strtolower(trim((string) ($cc['text'] ?? '')));
if ($t !== '') {
$complaintLookup[$t] = true;
if (str_contains($t, ' - ')) {
$parts = explode(' - ', $t);
$complaintLookup[trim($parts[0])] = true;
}
}
}
if (empty($conditionRows)) {
$conditionRows[] = ['text' => 'Medical care and evaluation', 'code' => ''];
// Fetch discharge instructions early for comorbidities
$instructionRows = $this->byIpdRows('ipd_discharge_instructions', ['comp_report', 'comp_remark', 'review_after'], 'id DESC', $ipdId);
$instructionRow = ! empty($instructionRows) ? $instructionRows[0] : [];
$instructionMeta = ! empty($instructionRow) ? $this->parseInstructionMetaPayload((string) ($instructionRow['comp_report'] ?? '')) : ['food_ids' => [], 'other_text' => '', 'nabh' => []];
$nabhMeta = is_array($instructionMeta['nabh'] ?? null) ? $instructionMeta['nabh'] : [];
// Build Medical History / Diagnoses (discharge diagnoses + comorbidities + chronic illness, NEVER chief complaints)
$medicalHistoryRows = [];
$diagnosisRows = $this->byIpdRows('ipd_discharge_diagnosis', ['comp_report'], 'id ASC', $ipdId);
foreach ($diagnosisRows as $row) {
$text = trim((string) ($row['comp_report'] ?? ''));
if ($text !== '' && ! isset($complaintLookup[strtolower($text)])) {
$medicalHistoryRows[] = ['text' => $text, 'code' => ''];
}
}
// Comorbidities from NABH metadata
$coMorbRaw = trim((string) ($nabhMeta['co_morbidities'] ?? ''));
if ($coMorbRaw !== '') {
$coMorbParts = preg_split('/[,;\n]+/', $coMorbRaw);
foreach ($coMorbParts as $cPart) {
$cText = trim((string) $cPart);
if ($cText !== '' && $this->isMeaningfulDischargeValue($cText) && ! isset($complaintLookup[strtolower($cText)])) {
$medicalHistoryRows[] = ['text' => $cText, 'code' => ''];
}
}
}
// Chronic illness from patient_master
if ($patientId > 0 && $this->db->tableExists('patient_master')) {
$historyFields = [
'is_smoking' => 'Smoking',
'is_alcohol' => 'Alcohol',
'is_drug_abuse' => 'Drug Abuse',
'is_tobacoo' => 'Tobacco',
'is_hypertesion' => 'Hypertension',
'is_niddm' => 'Diabetes',
'is_diabetes' => 'Diabetes',
'is_ischaemic_heart_ds' => 'Ischemic Heart Disease',
'is_asthma_copd' => 'Asthma/COPD',
'is_bleeding_disorder' => 'Bleeding Disorder',
'is_heapatitis_type' => 'Hepatitis',
'is_blood_transfusion' => 'Blood Transfusion',
'is_hiv_std' => 'HIV/STD',
'is_hiv_I_II' => 'HIV/STD',
'is_epilepsy' => 'Epilepsy',
'is_tb' => 'TB',
'is_psychiatric_illness' => 'Psychiatric Illness',
'is_thyroid' => 'Thyroid',
];
foreach ($historyFields as $f => $label) {
if ((int) ($patientRow[$f] ?? 0) === 1 && ! isset($complaintLookup[strtolower($label)])) {
$medicalHistoryRows[] = ['text' => $label, 'code' => ''];
}
}
}
// Problem from IPD record if non-blank and not duplicate of complaints
if (empty($medicalHistoryRows) && ! empty($ipdRow['problem'])) {
$prob = trim((string) $ipdRow['problem']);
if ($this->isMeaningfulDischargeValue($prob) && ! isset($complaintLookup[strtolower($prob)])) {
$medicalHistoryRows[] = ['text' => $prob, 'code' => ''];
}
}
// Deduplicate medical history rows by text
$uniqueMedHistory = [];
$seenMedHistory = [];
foreach ($medicalHistoryRows as $mRow) {
$mt = strtolower(trim((string) ($mRow['text'] ?? '')));
if ($mt !== '' && empty($seenMedHistory[$mt])) {
$seenMedHistory[$mt] = true;
$uniqueMedHistory[] = $mRow;
}
}
$conditionRows = $uniqueMedHistory;
$procedureRows = [];
foreach ($this->byIpdRows('ipd_discharge_surgery', ['surgery_name', 'surgery_date'], 'id ASC', $ipdId) as $row) {
$text = trim((string) ($row['surgery_name'] ?? ''));

View File

@ -94,12 +94,35 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
$patientRef = 'urn:uuid:patient-' . $patientId;
$encounterRef = is_array($encounter) ? 'urn:uuid:' . (string) ($encounter['id'] ?? '') : null;
$diagnosesList = (array) ($source['diagnoses'] ?? $source['conditions'] ?? $source['ui_final_diagnosis'] ?? []);
$chiefComplaintsList = (array) ($source['chief_complaints'] ?? $source['ui_complaints'] ?? []);
$rawDiagnosesList = (array) ($source['diagnoses'] ?? $source['conditions'] ?? $source['ui_final_diagnosis'] ?? []);
// Build normalized lookup of chief complaints to ensure Medical History never duplicates Chief Complaints
$complaintLookup = [];
foreach ($chiefComplaintsList as $cc) {
$t = strtolower(trim((string) ($cc['text'] ?? $cc['name'] ?? '')));
if ($t !== '') {
$complaintLookup[$t] = true;
if (str_contains($t, ' - ')) {
$parts = explode(' - ', $t);
$complaintLookup[trim($parts[0])] = true;
}
}
}
$diagnosesList = [];
$seenDiag = [];
foreach ($rawDiagnosesList as $diag) {
$dt = strtolower(trim((string) ($diag['text'] ?? $diag['name'] ?? '')));
if ($dt !== '' && ! isset($complaintLookup[$dt]) && empty($seenDiag[$dt])) {
$seenDiag[$dt] = true;
$diagnosesList[] = $diag;
}
}
if (empty($diagnosesList) && empty($chiefComplaintsList)) {
$problem = trim((string) ($source['problem'] ?? ''));
if ($problem !== '') {
if ($problem !== '' && ! isset($complaintLookup[strtolower($problem)])) {
$diagnosesList = [['text' => $problem, 'code' => '']];
} else {
$diagnosesList = [['text' => 'Medical care and evaluation', 'code' => '']];
@ -594,41 +617,81 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
$chiefComplaintsList = (array) ($source['ui_complaints'] ?? $source['chief_complaints'] ?? []);
$complaintsNarrative = trim((string) ($source['ui_complaint_narrative'] ?? $source['chief_complaint_narrative'] ?? ''));
$diagnosesList = (array) ($source['ui_final_diagnosis'] ?? $source['diagnoses'] ?? $source['conditions'] ?? []);
$rawDiagnosesList = (array) ($source['ui_final_diagnosis'] ?? $source['diagnoses'] ?? $source['conditions'] ?? []);
$clinicalHistoryNarrative = trim((string) ($source['ui_clinical_history'] ?? ''));
// Build normalized lookup of chief complaints
$complaintLookup = [];
foreach ($chiefComplaintsList as $cc) {
$t = strtolower(trim((string) ($cc['text'] ?? $cc['name'] ?? '')));
if ($t !== '') {
$complaintLookup[$t] = true;
if (str_contains($t, ' - ')) {
$parts = explode(' - ', $t);
$complaintLookup[trim($parts[0])] = true;
}
}
}
$diagnosesList = [];
$seenDiag = [];
foreach ($rawDiagnosesList as $diag) {
$dt = strtolower(trim((string) ($diag['text'] ?? $diag['name'] ?? '')));
if ($dt !== '' && ! isset($complaintLookup[$dt]) && empty($seenDiag[$dt])) {
$seenDiag[$dt] = true;
$diagnosesList[] = $diag;
}
}
$sectionDefinitions = [];
// Chief complaints
if (! empty($chiefComplaintsList) || $complaintsNarrative !== '') {
$sectionDefinitions[] = ['title' => 'Chief complaints', 'code' => '422843007', 'display' => 'Chief complaint section', 'prefix' => 'discharge-chief-complaint-', 'items' => $chiefComplaintsList, 'narrative' => $complaintsNarrative];
$sectionDefinitions[] = [
'title' => 'Chief complaints',
'code' => '422843007',
'display' => 'Chief complaint section',
'prefix' => 'discharge-chief-complaint-',
'items' => $chiefComplaintsList,
'narrative' => $complaintsNarrative,
'entry_display' => 'Condition',
];
}
// Discharge Diagnosis (Medical History)
if (! empty($diagnosesList) || $clinicalHistoryNarrative !== '') {
$sectionDefinitions[] = ['title' => 'Medical History', 'code' => '1003642006', 'display' => 'Past medical history section', 'prefix' => 'discharge-diagnosis-', 'items' => $diagnosesList, 'narrative' => $clinicalHistoryNarrative];
$sectionDefinitions[] = [
'title' => 'Medical History',
'code' => '1003642006',
'display' => 'Past medical history section',
'prefix' => 'discharge-diagnosis-',
'items' => $diagnosesList,
'narrative' => $clinicalHistoryNarrative,
'entry_display' => 'Condition',
];
}
// Surgery / Procedure
$surgeries = (array) ($source['ui_surgeries'] ?? $source['procedures'] ?? []);
if (! empty($surgeries)) {
$sectionDefinitions[] = ['title' => 'Procedures', 'code' => '1003640003', 'display' => 'History of past procedure section', 'prefix' => 'discharge-procedure-', 'items' => $surgeries];
$sectionDefinitions[] = [
'title' => 'Procedures',
'code' => '1003640003',
'display' => 'History of past procedure section',
'prefix' => 'discharge-procedure-',
'items' => $surgeries,
'entry_display' => 'Procedure',
];
}
// Medications
// Medications - NRCES DischargeSummaryRecord profile specifies SNOMED 1003606003 (Medication history section)
if (! empty($validMeds)) {
$sectionDefinitions[] = [
'title' => 'Medications',
'code' => '721912009',
'display' => 'Medication summary document',
'code' => '1003606003',
'display' => 'Medication history section',
'prefix' => 'discharge-medication-',
'items' => $validMeds,
'additional_coding' => [
[
'system' => 'http://snomed.info/sct',
'code' => '1003606003',
'display' => 'Medication history section',
],
],
'entry_display' => 'MedicationRequest',
];
}
@ -636,23 +699,52 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
$investigations = (array) ($source['ui_investigations'] ?? $source['investigations'] ?? []);
$invNarrative = trim((string) ($source['ui_summary_investigation'] ?? ''));
if (! empty($investigations) || $invNarrative !== '') {
$sectionDefinitions[] = ['title' => 'Investigations', 'code' => '721981007', 'display' => 'Diagnostic studies report', 'prefix' => 'discharge-investigation-', 'items' => $investigations, 'narrative' => $invNarrative];
$sectionDefinitions[] = [
'title' => 'Investigations',
'code' => '721981007',
'display' => 'Diagnostic studies report',
'prefix' => 'discharge-investigation-',
'items' => $investigations,
'narrative' => $invNarrative,
'entry_display' => 'Diagnostic Report',
];
}
// Care plans / Discharge advice & hospital course
if (! empty($validCarePlans)) {
$sectionDefinitions[] = ['title' => 'Care Plan', 'code' => '734163000', 'display' => 'Care plan', 'prefix' => 'discharge-careplan-', 'items' => $validCarePlans];
$sectionDefinitions[] = [
'title' => 'Care Plan',
'code' => '734163000',
'display' => 'Care plan',
'prefix' => 'discharge-careplan-',
'items' => $validCarePlans,
'entry_display' => 'Care plan',
];
}
// Document Reference
if (! empty($documentsList)) {
$sectionDefinitions[] = ['title' => 'Document Reference', 'code' => '373942005', 'display' => 'Discharge summary', 'prefix' => 'discharge-document-', 'items' => $documentsList];
$sectionDefinitions[] = [
'title' => 'Document Reference',
'code' => '373942005',
'display' => 'Discharge summary',
'prefix' => 'discharge-document-',
'items' => $documentsList,
'entry_display' => 'Discharge Summary',
];
}
// Allergies
$allergies = (array) ($source['allergies'] ?? []);
if (! empty($allergies)) {
$sectionDefinitions[] = ['title' => 'Allergies', 'code' => '722446000', 'display' => 'Allergy record', 'prefix' => 'discharge-allergy-', 'items' => $allergies];
$sectionDefinitions[] = [
'title' => 'Allergies',
'code' => '722446000',
'display' => 'Allergy record',
'prefix' => 'discharge-allergy-',
'items' => $allergies,
'entry_display' => 'AllergyIntolerance',
];
}
$sections = [];
@ -660,7 +752,7 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac
$references = [];
$items = isset($definition['items_indexed']) ? $definition['items_indexed'] : (array) ($definition['items'] ?? []);
foreach ($items as $idx => $item) {
$itemDisplay = $this->cleanPlainText((string) ($item['name'] ?? $item['text'] ?? $item['title'] ?? $definition['title']));
$itemDisplay = $definition['entry_display'] ?? $this->cleanPlainText((string) ($item['name'] ?? $item['text'] ?? $item['title'] ?? $definition['title']));
if ($itemDisplay === '') {
$itemDisplay = $definition['title'];
}