From d09822167693ec1c0bbb7f12f4168f19d53f8b45 Mon Sep 17 00:00:00 2001 From: DevSoftTech Server Date: Mon, 19 Oct 2026 23:06:12 +0530 Subject: [PATCH] fix(abdm): unify medication request FHIR representation across OPD and discharge (generic salt subtitle, SNOMED food timing, and natural dosage instructions) --- .../Generators/DischargeFhirGenerator.php | 29 ++- .../Abdm/Fhir/Generators/OpdFhirGenerator.php | 9 + .../Generators/PrescriptionFhirGenerator.php | 9 + app/Libraries/FhirR4Builder.php | 187 +++++++++++++----- 4 files changed, 186 insertions(+), 48 deletions(-) diff --git a/app/Libraries/Abdm/Fhir/Generators/DischargeFhirGenerator.php b/app/Libraries/Abdm/Fhir/Generators/DischargeFhirGenerator.php index 6c358c9..effcc0f 100644 --- a/app/Libraries/Abdm/Fhir/Generators/DischargeFhirGenerator.php +++ b/app/Libraries/Abdm/Fhir/Generators/DischargeFhirGenerator.php @@ -280,7 +280,7 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac $medCoding = []; if ($isSaltDistinct) { - // Primary coding represents the generic substance / salt so PHR displays it in the subtitle line instead of "NA" + // Primary coding represents the generic substance / salt $medCoding[] = [ 'system' => 'http://snomed.info/sct', 'code' => $saltCode, @@ -344,6 +344,23 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac ], ]; + // Reason / Indication: Map salt composition if available (matches OPD Consult pattern so PHR displays salt instead of "NA") + $saltReasonText = $salt; + if ($saltReasonText === '' || ! $this->isMeaningfulValue($saltReasonText)) { + $saltReasonText = $this->tryResolveGenericSalt($name, $medId); + } + if ($saltReasonText === '' || ! $this->isMeaningfulValue($saltReasonText)) { + $firstCond = $diagnosesList[0]['name'] ?? ($diagnosesList[0]['text'] ?? ($source['problem'] ?? '')); + $saltReasonText = is_string($firstCond) && trim($firstCond) !== '' ? trim($firstCond) : ''; + } + if ($saltReasonText !== '') { + $medResource['reasonCode'] = [ + [ + 'text' => $saltReasonText, + ], + ]; + } + if (is_array($practitioner)) { $medResource['requester'] = [ 'reference' => 'urn:uuid:' . (string) $practitioner['id'], @@ -1800,18 +1817,22 @@ class DischargeFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\Abstrac // Dose unit detection $doseUnit = 'Tablet'; - $doseValue = 1; + $doseValue = '1'; if (preg_match('/\b(SACHET|POWDER)\b/', $raw)) { $doseUnit = 'Sachet'; } elseif (preg_match('/\b(SYRUP|SUSPENSION|ML)\b/', $raw)) { $doseUnit = 'mL'; - $doseValue = 5; + $doseValue = '5'; } elseif (preg_match('/\b(CAPSULE|CAP)\b/', $raw)) { $doseUnit = 'Capsule'; } elseif (preg_match('/\b(DROPS?)\b/', $raw)) { $doseUnit = 'drops'; - $doseValue = 2; + $doseValue = '2'; + } + + if (preg_match('/([0-9]+(?:\/[0-9]+|\.[0-9]+)?)\s*(?:Tab|Cap|Tablet|Capsule|Sachet|ml|drops)/i', $dosageText, $vm)) { + $doseValue = $vm[1]; } // Duration detection diff --git a/app/Libraries/Abdm/Fhir/Generators/OpdFhirGenerator.php b/app/Libraries/Abdm/Fhir/Generators/OpdFhirGenerator.php index 073bf4e..54dffb4 100644 --- a/app/Libraries/Abdm/Fhir/Generators/OpdFhirGenerator.php +++ b/app/Libraries/Abdm/Fhir/Generators/OpdFhirGenerator.php @@ -170,6 +170,15 @@ class OpdFhirGenerator extends \App\Libraries\Abdm\Fhir\Generators\AbstractModul 'dosageInstruction' => $dosageInstruction, ]; + $salt = trim((string) ($med['salt'] ?? $med['generic'] ?? $med['generic_name'] ?? '')); + if ($salt !== '') { + $medResource['reasonCode'] = [ + [ + 'text' => $salt, + ], + ]; + } + $builder->addMedicationRequest($medResource); } diff --git a/app/Libraries/Abdm/Fhir/Generators/PrescriptionFhirGenerator.php b/app/Libraries/Abdm/Fhir/Generators/PrescriptionFhirGenerator.php index 62807eb..e91c634 100644 --- a/app/Libraries/Abdm/Fhir/Generators/PrescriptionFhirGenerator.php +++ b/app/Libraries/Abdm/Fhir/Generators/PrescriptionFhirGenerator.php @@ -141,6 +141,15 @@ class PrescriptionFhirGenerator extends AbstractModuleFhirGenerator 'dosageInstruction' => $dosageInstruction, ]; + $salt = trim((string) ($med['salt'] ?? $med['generic'] ?? $med['generic_name'] ?? '')); + if ($salt !== '') { + $medResource['reasonCode'] = [ + [ + 'text' => $salt, + ], + ]; + } + if ($encounterRef !== null) { $medResource['encounter'] = ['reference' => $encounterRef]; } diff --git a/app/Libraries/FhirR4Builder.php b/app/Libraries/FhirR4Builder.php index b7c6b50..c3aa0f3 100644 --- a/app/Libraries/FhirR4Builder.php +++ b/app/Libraries/FhirR4Builder.php @@ -400,58 +400,157 @@ class FhirR4Builder ]]; } - $dosageInstruction = ['text' => (string) ($medication['dosage'] ?? '')]; - $routeText = trim((string) ($medication['route_text'] ?? '')); - $routeCode = trim((string) ($medication['route_code'] ?? '')); - if (! $this->isMeaningfulValue($routeText)) { - $medTypeUpper = strtoupper($medType); - if (in_array($medTypeUpper, ['INJ', 'INJECTION', 'IV', 'IM'], true)) { - $routeText = 'Injection'; - $routeCode = '47625008'; - } elseif (in_array($medTypeUpper, ['CREAM', 'OINT', 'OINTMENT', 'GEL', 'LOTION'], true)) { - $routeText = 'Topical'; - $routeCode = '6064005'; - } else { - $routeText = 'Oral'; - $routeCode = '260548002'; - } - } elseif ($routeCode === '') { - $routeCode = $this->resolveRouteSnomedCode($routeText); + $rawDosageText = trim((string) ($medication['dosage'] ?? '')); + $freqText = trim((string) ($medication['frequency_text'] ?? '')); + $timingResult = $this->buildFhirTimingStructure($freqText, $rawDosageText); + + $combinedDosageContext = strtoupper($rawDosageText . ' ' . $freqText . ' ' . $medType . ' ' . $drugName); + + // 1. Food relation / Additional Instruction (Point 2: standard SNOMED 307166007 / 307165006) + $addInstruct = null; + $foodSuffix = ''; + if (preg_match('/\b(EMPTY STOMACH|BEFORE FOOD|BEFORE MEALS?|BF|AC)\b/', $combinedDosageContext)) { + $addInstruct = [[ + 'coding' => [[ + 'system' => 'http://snomed.info/sct', + 'code' => '307165006', + 'display' => 'Before meals', + ]], + 'text' => 'Before food / Empty stomach', + ]]; + $foodSuffix = 'before food / empty stomach'; + } elseif (preg_match('/\b(AFTER FOOD|AFTER MEALS?|AF|PC)\b/', $combinedDosageContext)) { + $addInstruct = [[ + 'coding' => [[ + 'system' => 'http://snomed.info/sct', + 'code' => '307166007', + 'display' => 'After meals', + ]], + 'text' => 'After food', + ]]; + $foodSuffix = 'after food'; + } elseif (preg_match('/\b(WITH FOOD|WITH MEALS?|WF)\b/', $combinedDosageContext)) { + $addInstruct = [[ + 'coding' => [[ + 'system' => 'http://snomed.info/sct', + 'code' => '311504000', + 'display' => 'With or after food', + ]], + 'text' => 'With food', + ]]; + $foodSuffix = 'with food'; } - $routeEntry = ['text' => $routeText]; - if ($this->isMeaningfulValue($routeCode) && $routeCode !== '371530004') { - $routeEntry['coding'] = [[ - 'system' => 'http://snomed.info/sct', - 'code' => $routeCode, - 'display' => $routeText, - ]]; - } else { - $routeEntry['coding'] = [[ - 'system' => 'http://snomed.info/sct', - 'code' => '260548002', - 'display' => $routeText, - ]]; + // 2. Timing display & frequency + $timingDisplay = 'Once daily'; + if (! empty($timingResult['timing']['code']['coding'][0]['display'])) { + $timingDisplay = (string) $timingResult['timing']['code']['coding'][0]['display']; + } elseif (preg_match('/\b(TDS|TID|THRICE|3 TIMES)\b/', $combinedDosageContext)) { + $timingDisplay = 'Three times daily'; + } elseif (preg_match('/\b(QID|4 TIMES)\b/', $combinedDosageContext)) { + $timingDisplay = 'Four times daily'; + } elseif (preg_match('/\b(BD|BID|TWICE|2 TIMES)\b/', $combinedDosageContext)) { + $timingDisplay = 'Twice daily'; + } elseif (preg_match('/\b(SOS|PRN|AS NEEDED)\b/', $combinedDosageContext)) { + $timingDisplay = 'As needed'; + } elseif (preg_match('/\b(HS|BEDTIME|AT NIGHT)\b/', $combinedDosageContext)) { + $timingDisplay = 'At bedtime'; + } elseif (preg_match('/\b(STAT|ONCE|NOW)\b/', $combinedDosageContext)) { + $timingDisplay = 'Immediately'; } - $dosageInstruction['route'] = $routeEntry; - $methodText = trim((string) ($medication['method_text'] ?? $medType)); - $methodCode = trim((string) ($medication['method_code'] ?? '')); - if ($this->isMeaningfulValue($methodText)) { - $methodEntry = ['text' => $methodText]; - if ($this->isMeaningfulValue($methodCode) && $methodCode !== '371530004') { - $methodEntry['coding'] = [[ + // 3. Route & Method detection (Standard SNOMED: Oral Route 26643006 / Swallow 421521009) + $medTypeUpper = strtoupper($medType); + $routeCode = '26643006'; + $routeDisplay = 'Oral Route'; + $routeText = 'Oral Route'; + $methodCode = '421521009'; // Swallow + $methodDisplay = 'Swallow'; + + if (preg_match('/\b(INJ|IV|INTRAVENOUS|INFUSION)\b/', $combinedDosageContext) || in_array($medTypeUpper, ['INJ', 'INJECTION', 'IV', 'INFUSION'], true)) { + $routeCode = '47625008'; + $routeDisplay = 'Intravenous route'; + $routeText = 'Intravenous (IV)'; + $methodCode = '422145002'; + $methodDisplay = 'Inject'; + } elseif (preg_match('/\b(IM|INTRAMUSCULAR)\b/', $combinedDosageContext) || $medTypeUpper === 'IM') { + $routeCode = '78421000'; + $routeDisplay = 'Intramuscular route'; + $routeText = 'Intramuscular (IM)'; + $methodCode = '422145002'; + $methodDisplay = 'Inject'; + } elseif (preg_match('/\b(DROPS?|EYE|EAR|OPHTHALMIC)\b/', $combinedDosageContext) || in_array($medTypeUpper, ['DROPS', 'DROP', 'EYE DROP', 'EAR DROP'], true)) { + $routeCode = '54485002'; + $routeDisplay = 'Ophthalmic route'; + $routeText = 'Drops'; + $methodCode = '421553004'; + $methodDisplay = 'Instill'; + } elseif (preg_match('/\b(CREAM|OINTMENT|GEL|TOPICAL|LOTION)\b/', $combinedDosageContext) || in_array($medTypeUpper, ['CREAM', 'OINT', 'OINTMENT', 'GEL', 'LOTION'], true)) { + $routeCode = '6064005'; + $routeDisplay = 'Topical route'; + $routeText = 'Topical'; + $methodCode = '417924000'; + $methodDisplay = 'Apply'; + } + + // 4. Dose value & unit + $doseUnit = 'Tablet'; + $doseValue = '1'; + if (preg_match('/\b(SACHET|POWDER)\b/', $combinedDosageContext) || in_array($medTypeUpper, ['SACHET', 'POWDER'], true)) { + $doseUnit = 'Sachet'; + } elseif (preg_match('/\b(SYRUP|SUSPENSION|ML)\b/', $combinedDosageContext) || in_array($medTypeUpper, ['SYP', 'SYRUP', 'SUSP', 'SUSPENSION', 'ML'], true)) { + $doseUnit = 'mL'; + $doseValue = '5'; + } elseif (preg_match('/\b(CAPSULE|CAP)\b/', $combinedDosageContext) || in_array($medTypeUpper, ['CAP', 'CAPSULE'], true)) { + $doseUnit = 'Capsule'; + } elseif (preg_match('/\b(DROPS?)\b/', $combinedDosageContext) || in_array($medTypeUpper, ['DROPS', 'DROP'], true)) { + $doseUnit = 'drops'; + $doseValue = '2'; + } + + if (preg_match('/([0-9]+(?:\/[0-9]+|\.[0-9]+)?)\s*(?:Tab|Cap|Tablet|Capsule|Sachet|ml|drops)/i', $rawDosageText, $vm)) { + $doseValue = $vm[1]; + } + + // 5. Duration + $durationDays = 0; + if (preg_match('/(?:FOR\s*)?(\d+)\s*DAYS?/i', $rawDosageText, $dm)) { + $durationDays = (int) $dm[1]; + } + + // 6. Build natural, understandable sentence (Point 3) + $instructionText = 'Take ' . $doseValue . ' ' . $doseUnit . ' ' . $timingDisplay; + if ($foodSuffix !== '') { + $instructionText .= ' ' . $foodSuffix; + } + if ($durationDays > 0) { + $instructionText .= ' for ' . $durationDays . ' days'; + } + + $dosageInstruction = [ + 'text' => $instructionText, + 'route' => [ + 'coding' => [[ + 'system' => 'http://snomed.info/sct', + 'code' => $routeCode, + 'display' => $routeDisplay, + ]], + 'text' => $routeText, + ], + 'method' => [ + 'coding' => [[ 'system' => 'http://snomed.info/sct', 'code' => $methodCode, - 'display' => $methodText, - ]]; - } - $dosageInstruction['method'] = $methodEntry; + 'display' => $methodDisplay, + ]], + 'text' => $methodDisplay, + ], + ]; + + if ($addInstruct !== null) { + $dosageInstruction['additionalInstruction'] = $addInstruct; } - $freqText = trim((string) ($medication['frequency_text'] ?? '')); - $dosageText = (string) ($medication['dosage'] ?? ''); - $timingResult = $this->buildFhirTimingStructure($freqText, $dosageText); if ($timingResult['timing'] !== null) { $dosageInstruction['timing'] = $timingResult['timing']; }