From f5e1649895fea03763d0a9baf5d206eec83418e2 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 24 Mar 2026 10:54:37 +0000
Subject: [PATCH 1/5] Initial plan
From 2730184c874165454c3e4f851f8b52101b7f5da0 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 24 Mar 2026 11:10:14 +0000
Subject: [PATCH 2/5] feat(fhir): add FHIR implementation plan script with
scenarios 6.1-6.6
Co-authored-by: halcwb <683631+halcwb@users.noreply.github.com>
Agent-Logs-Url: https://github.com/informedica/GenPRES/sessions/2414b85b-c4e0-4b07-bb7a-4f2ded4a8294
---
.../Informedica.FHIR.Lib.fsproj | 4 +
.../Scripts/ImplementationPlan.fsx | 894 ++++++++++++++++++
src/Informedica.FHIR.Lib/Scripts/load.fsx | 25 +
3 files changed, 923 insertions(+)
create mode 100644 src/Informedica.FHIR.Lib/Scripts/ImplementationPlan.fsx
create mode 100644 src/Informedica.FHIR.Lib/Scripts/load.fsx
diff --git a/src/Informedica.FHIR.Lib/Informedica.FHIR.Lib.fsproj b/src/Informedica.FHIR.Lib/Informedica.FHIR.Lib.fsproj
index ca01233b..aa25026c 100644
--- a/src/Informedica.FHIR.Lib/Informedica.FHIR.Lib.fsproj
+++ b/src/Informedica.FHIR.Lib/Informedica.FHIR.Lib.fsproj
@@ -8,6 +8,10 @@
+
+
+
+
diff --git a/src/Informedica.FHIR.Lib/Scripts/ImplementationPlan.fsx b/src/Informedica.FHIR.Lib/Scripts/ImplementationPlan.fsx
new file mode 100644
index 00000000..318eda78
--- /dev/null
+++ b/src/Informedica.FHIR.Lib/Scripts/ImplementationPlan.fsx
@@ -0,0 +1,894 @@
+/// # FHIR Medication Interface Implementation Plan
+///
+/// This script implements the plan described in docs/mdr/interface/genpres_interface_specification.md
+/// and follows the steps outlined in the GenPRES FHIR integration issue.
+///
+/// ## Steps
+///
+/// 1. Define the FHIR scenarios from the specification
+/// 2. Look up product information from GPK product codes via ZIndex
+/// 3. Parse quantitative variables to ValueUnits
+/// 4. Translate each scenario to the Medication string representation
+/// 5. Run each scenario through Medication.fromString as an order
+/// 6. Print the results
+/// 7. Sketch a FHIR-based translation approach
+
+#load "load.fsx"
+
+open System
+open MathNet.Numerics
+open Informedica.Utils.Lib.BCL
+open Informedica.GenCore.Lib.Ranges
+open Informedica.GenUnits.Lib
+open Informedica.GenOrder.Lib
+
+
+// =============================================================================
+// STEP 1: Define the FHIR scenarios from the specification
+// =============================================================================
+//
+// Source: docs/mdr/interface/genpres_interface_specification.md, sections 6.1–6.6
+//
+// NOTE: The GPK codes in the specification are PLACEHOLDERS (e.g. "2345678").
+// Real codes must come from the G-Standard database. See Step 2 for product lookup.
+
+/// Represents a product as described in the FHIR scenario YAML
+type ScenarioProduct =
+ {
+ // Placeholder GPK code from the spec (not a real G-Standard code)
+ GpkPlaceholder: string
+ // Amount of product used
+ Quantity: decimal
+ Unit: string
+ // Human-readable description from the spec comment
+ Description: string
+ }
+
+
+/// Represents the administration schedule from the FHIR YAML schema block
+type AdministrationSchema =
+ {
+ // Number of times per period (null for continuous)
+ Frequency: int option
+ // Duration of the period (e.g. 1 for per day, 36 for per 36 hours)
+ TimePeriod: decimal option
+ // Unit of the period (e.g. "dag", "uur")
+ TimeUnit: string option
+ // Infusion rate quantity
+ RateQuantity: decimal option
+ // Rate unit numerator (e.g. "mL")
+ RateUnit1: string option
+ // Rate unit denominator (e.g. "uur")
+ RateUnit2: string option
+ // Exact administration times (for timed orders)
+ ExactTimes: string list
+ }
+
+
+/// Represents a FHIR scenario as described in section 6 of the specification
+type FhirScenario =
+ {
+ // Scenario identifier (matches section number)
+ ScenarioId: string
+ // Short descriptive name
+ Description: string
+ // Patient weight in kg
+ WeightKg: decimal
+ // Patient height in cm
+ HeightCm: decimal
+ // Gender ("male" / "female")
+ Gender: string
+ // Clinical indication
+ Indication: string
+ // Generic medication name
+ MedicationName: string
+ // Administration route (Dutch G-Standard term)
+ Route: string
+ // Pharmaceutical form (Dutch)
+ Shape: string
+ // Order type: "Once", "OnceTimed", "Discontinuous", "Timed", "Continuous"
+ DoseType: string
+ // Products from the YAML products block
+ Products: ScenarioProduct list
+ // Administration quantity and unit
+ AdminQuantity: decimal
+ AdminUnit: string
+ // Scheduling
+ Schema: AdministrationSchema
+ }
+
+
+// --- Scenario 6.1: Single-Product Once (Paracetamol rectal suppository) ---
+let scenario61 =
+ {
+ ScenarioId = "6.1"
+ Description = "Single-Product Once – paracetamol suppository"
+ WeightKg = 19.5m
+ HeightCm = 109m
+ Gender = "male"
+ Indication = "Pijn, acuut/post-operatief"
+ MedicationName = "paracetamol"
+ Route = "RECTAAL"
+ Shape = "zetpil"
+ DoseType = "Once"
+ Products =
+ [
+ {
+ GpkPlaceholder = "2345678"
+ Quantity = 1m
+ Unit = "stuk"
+ Description = "paracetamol 750 mg/stuk zetpil (placeholder GPK)"
+ }
+ ]
+ AdminQuantity = 1m
+ AdminUnit = "stuk"
+ Schema =
+ {
+ Frequency = Some 1
+ TimePeriod = None
+ TimeUnit = None
+ RateQuantity = None
+ RateUnit1 = None
+ RateUnit2 = None
+ ExactTimes = []
+ }
+ }
+
+
+// --- Scenario 6.2: Single-Product Once-Timed (Paracetamol IV) ---
+let scenario62 =
+ {
+ ScenarioId = "6.2"
+ Description = "Single-Product OnceTimed – paracetamol IV infusion"
+ WeightKg = 11m
+ HeightCm = 79m
+ Gender = "male"
+ Indication = "Pijn, acuut/post-operatief"
+ MedicationName = "paracetamol"
+ Route = "INTRAVENEUS"
+ Shape = "infusievloeistof"
+ DoseType = "OnceTimed"
+ Products =
+ [
+ {
+ GpkPlaceholder = "3456789"
+ Quantity = 22m
+ Unit = "mL"
+ Description = "paracetamol 10 mg/mL infusievloeistof (placeholder GPK)"
+ }
+ ]
+ AdminQuantity = 22m
+ AdminUnit = "mL"
+ Schema =
+ {
+ Frequency = Some 1
+ TimePeriod = None
+ TimeUnit = None
+ RateQuantity = Some 85m
+ RateUnit1 = Some "mL"
+ RateUnit2 = Some "uur"
+ ExactTimes = []
+ }
+ }
+
+
+// --- Scenario 6.3: Single-Product Discontinuous (Paracetamol rectal, 4x/day) ---
+let scenario63 =
+ {
+ ScenarioId = "6.3"
+ Description = "Single-Product Discontinuous – paracetamol rectal 4x/dag"
+ WeightKg = 11m
+ HeightCm = 79m
+ Gender = "male"
+ Indication = "Milde tot matige pijn; koorts"
+ MedicationName = "paracetamol"
+ Route = "RECTAAL"
+ Shape = "zetpil"
+ DoseType = "Discontinuous"
+ Products =
+ [
+ {
+ GpkPlaceholder = "2345678"
+ Quantity = 1m
+ Unit = "stuk"
+ Description = "paracetamol 180 mg/stuk zetpil (placeholder GPK)"
+ }
+ ]
+ AdminQuantity = 1m
+ AdminUnit = "stuk"
+ Schema =
+ {
+ Frequency = Some 4
+ TimePeriod = Some 1m
+ TimeUnit = Some "dag"
+ RateQuantity = None
+ RateUnit1 = None
+ RateUnit2 = None
+ ExactTimes = []
+ }
+ }
+
+
+// --- Scenario 6.3.1: Single-Product Discontinuous Specific Time (Gentamicine neonatal) ---
+let scenario631 =
+ {
+ ScenarioId = "6.3.1"
+ Description = "Single-Product Discontinuous – gentamicine 1x/36h neonatal"
+ WeightKg = 1.8m
+ HeightCm = 42m
+ Gender = "female"
+ Indication = "Ernstige infectie, gram negatieve microorganismen"
+ MedicationName = "gentamicine"
+ Route = "INTRAVENEUS"
+ Shape = "infusievloeistof"
+ DoseType = "Discontinuous"
+ Products =
+ [
+ {
+ GpkPlaceholder = "2345678"
+ Quantity = 9m
+ Unit = "mL"
+ Description = "gentamicine 1 mg/mL infusievloeistof (placeholder GPK)"
+ }
+ ]
+ AdminQuantity = 9m
+ AdminUnit = "mL"
+ Schema =
+ {
+ Frequency = Some 1
+ TimePeriod = Some 36m
+ TimeUnit = Some "uur"
+ RateQuantity = None
+ RateUnit1 = None
+ RateUnit2 = None
+ ExactTimes = []
+ }
+ }
+
+
+// --- Scenario 6.4: Single-Product Timed (Paracetamol IV with exact times) ---
+let scenario64 =
+ {
+ ScenarioId = "6.4"
+ Description = "Single-Product Timed – paracetamol IV 4x/dag with exact times"
+ WeightKg = 11m
+ HeightCm = 79m
+ Gender = "male"
+ Indication = "Pijn, acuut/post-operatief"
+ MedicationName = "paracetamol"
+ Route = "INTRAVENEUS"
+ Shape = "infusievloeistof"
+ DoseType = "Timed"
+ Products =
+ [
+ {
+ GpkPlaceholder = "2345678"
+ Quantity = 17.5m
+ Unit = "mL"
+ Description = "paracetamol 10 mg/mL infusievloeistof (placeholder GPK)"
+ }
+ ]
+ AdminQuantity = 17.5m
+ AdminUnit = "mL"
+ Schema =
+ {
+ Frequency = Some 4
+ TimePeriod = Some 1m
+ TimeUnit = Some "dag"
+ RateQuantity = Some 70m
+ RateUnit1 = Some "mL"
+ RateUnit2 = Some "uur"
+ ExactTimes = [ "10:00"; "16:00"; "22:00"; "04:00" ]
+ }
+ }
+
+
+// --- Scenario 6.5: Single-Product Continuous (Propofol sedation) ---
+let scenario65 =
+ {
+ ScenarioId = "6.5"
+ Description = "Single-Product Continuous – propofol sedation"
+ WeightKg = 11m
+ HeightCm = 79m
+ Gender = "male"
+ Indication = "Sedatie"
+ MedicationName = "propofol"
+ Route = "INTRAVENEUS"
+ Shape = "emulsie voor injectie"
+ DoseType = "Continuous"
+ Products =
+ [
+ {
+ GpkPlaceholder = "2345678"
+ Quantity = 50m
+ Unit = "mL"
+ Description = "propofol 10 mg/mL emulsie voor injectie (placeholder GPK)"
+ }
+ ]
+ AdminQuantity = 50m
+ AdminUnit = "mL"
+ Schema =
+ {
+ Frequency = None
+ TimePeriod = None
+ TimeUnit = None
+ RateQuantity = Some 3.3m
+ RateUnit1 = Some "mL"
+ RateUnit2 = Some "uur"
+ ExactTimes = []
+ }
+ }
+
+
+// --- Scenario 6.6: Multi-Product Continuous (Noradrenaline + glucose 10%) ---
+let scenario66 =
+ {
+ ScenarioId = "6.6"
+ Description = "Multi-Product Continuous – noradrenaline with glucose 10% diluent"
+ WeightKg = 11m
+ HeightCm = 79m
+ Gender = "male"
+ Indication = "Circulatoire insufficiëntie"
+ MedicationName = "noradrenaline"
+ Route = "INTRAVENEUS"
+ Shape = "concentraat voor oplossing voor infusie"
+ DoseType = "Continuous"
+ Products =
+ [
+ {
+ GpkPlaceholder = "2345678"
+ Quantity = 5m
+ Unit = "mL"
+ Description = "noradrenaline 1 mg/mL concentraat voor oplossing voor infusie (placeholder GPK)"
+ }
+ {
+ GpkPlaceholder = "3456789"
+ Quantity = 45m
+ Unit = "mL"
+ Description = "glucose 10% vloeistof/diluent (placeholder GPK)"
+ }
+ ]
+ AdminQuantity = 50m
+ AdminUnit = "mL"
+ Schema =
+ {
+ Frequency = None
+ TimePeriod = None
+ TimeUnit = None
+ RateQuantity = Some 1m
+ RateUnit1 = Some "mL"
+ RateUnit2 = Some "uur"
+ ExactTimes = []
+ }
+ }
+
+
+let allScenarios =
+ [
+ scenario61
+ scenario62
+ scenario63
+ scenario631
+ scenario64
+ scenario65
+ scenario66
+ ]
+
+
+// =============================================================================
+// STEP 2: Look up product information from GPK codes via ZIndex
+// =============================================================================
+//
+// NOTE: The GPK codes in the spec are PLACEHOLDERS. A real implementation would
+// use ZIndex.GenericProduct.get [gpkCode] to resolve a product by its actual
+// G-Standard GPK code. Here we demonstrate the lookup API by searching for
+// products by their generic name, which is what the spec intends.
+
+open Informedica.ZIndex.Lib
+
+
+/// Print a summary of a GenericProduct record
+let printGenericProduct (gp: Types.GenericProduct) =
+ printfn " GPK %i | %-30s | Form: %-30s | Routes: %s"
+ gp.Id
+ gp.Name
+ gp.Form
+ (gp.Route |> String.concat ", ")
+
+ for sub in gp.Substances do
+ printfn " Substance: %-25s | Qty: %g %s | GenericQty: %g %s"
+ sub.SubstanceName
+ sub.SubstanceQuantity
+ sub.SubstanceUnit
+ sub.GenericQuantity
+ sub.GenericUnit
+
+
+/// Lookup all products for a given generic medication name using ZIndex
+let lookupByGenericName (name: string) =
+ GenericProduct.get []
+ |> Array.filter (fun gp -> gp.Name |> String.equalsCapInsens name)
+
+
+/// Demonstrate GPK lookup for the medications in our scenarios
+let demonstrateProductLookup () =
+ printfn "\n=== STEP 2: Product Lookup via ZIndex ==="
+ printfn "NOTE: Spec uses placeholder GPK codes (e.g. '2345678')."
+ printfn "Real lookup uses ZIndex.GenericProduct.get [gpkCode].\n"
+
+ let medicationNames =
+ [
+ "paracetamol"
+ "gentamicine"
+ "propofol"
+ "noradrenaline"
+ ]
+
+ for name in medicationNames do
+ printfn "--- %s ---" name
+ let products = lookupByGenericName name
+
+ if products |> Array.isEmpty then
+ printfn " (no products found – run after loading G-Standard data)"
+ else
+ products |> Array.truncate 3 |> Array.iter printGenericProduct
+ if products.Length > 3 then
+ printfn " ... and %i more products" (products.Length - 3)
+
+ printfn ""
+
+
+demonstrateProductLookup ()
+
+
+// =============================================================================
+// STEP 3: Parse quantitative variables to ValueUnits
+// =============================================================================
+//
+// Demonstrate how the scenario quantities translate to GenUnits ValueUnit values.
+// These are the same unit types used in the Medication text format.
+
+let demonstrateValueUnitParsing () =
+ printfn "\n=== STEP 3: Parsing Quantitative Variables to ValueUnits ==="
+
+ // Weight adjust unit
+ let weight19_5kg = 19.5m |> BigRational.fromDecimal |> ValueUnit.singleWithUnit Units.Weight.kiloGram
+ printfn "Patient weight: %s" (weight19_5kg |> ValueUnit.toStringDecimalDutchShort)
+
+ // Suppository quantity
+ let stuk = Units.General.general "stuk"
+ let qty1stuk = 1N |> ValueUnit.singleWithUnit stuk
+ printfn "1 stuk: %s" (qty1stuk |> ValueUnit.toStringDecimalDutchShort)
+
+ // Concentration: 750 mg/stuk
+ let conc750mgStuk =
+ 750N
+ |> ValueUnit.singleWithUnit (Units.Mass.milliGram |> Units.per stuk)
+
+ printfn "750 mg/stuk: %s" (conc750mgStuk |> ValueUnit.toStringDecimalDutchShort)
+
+ // IV concentration: 10 mg/mL
+ let conc10mgMl =
+ 10N
+ |> ValueUnit.singleWithUnit (Units.Mass.milliGram |> Units.per Units.Volume.milliLiter)
+
+ printfn "10 mg/mL: %s" (conc10mgMl |> ValueUnit.toStringDecimalDutchShort)
+
+ // Infusion rate: 85 mL/uur
+ let rate85mlHr =
+ 85N
+ |> ValueUnit.singleWithUnit (Units.Volume.milliLiter |> Units.per Units.Time.hour)
+
+ printfn "85 mL/uur: %s" (rate85mlHr |> ValueUnit.toStringDecimalDutchShort)
+
+ // Dose adjust: 40 mg/kg/dose (quantity-adjust, no time)
+ let doseAdj40 =
+ 40N
+ |> ValueUnit.singleWithUnit (
+ Units.Mass.milliGram
+ |> Units.per Units.Weight.kiloGram
+ )
+
+ printfn "40 mg/kg/dose: %s" (doseAdj40 |> ValueUnit.toStringDecimalDutchShort)
+
+ // Dose adjust: 3 mg/kg/uur (rate-adjust, with time)
+ let rateAdj3 =
+ 3N
+ |> ValueUnit.singleWithUnit (
+ Units.Mass.milliGram
+ |> Units.per Units.Weight.kiloGram
+ |> Units.per Units.Time.hour
+ )
+
+ printfn "3 mg/kg/uur: %s" (rateAdj3 |> ValueUnit.toStringDecimalDutchShort)
+
+ // Frequency: 4 x/dag
+ let freq4xDay =
+ [| 4N |] |> ValueUnit.withUnit (Units.Count.times |> Units.per Units.Time.day)
+
+ printfn "4 x/dag: %s" (freq4xDay |> ValueUnit.toStringDecimalDutchShort)
+
+ // Per-time dose: 10–20 mg/kg/dose (quantity-adjust MinMax)
+ let doseMin = 10N |> ValueUnit.singleWithUnit (Units.Mass.milliGram |> Units.per Units.Weight.kiloGram)
+ let doseMax = 20N |> ValueUnit.singleWithUnit (Units.Mass.milliGram |> Units.per Units.Weight.kiloGram)
+ let doseRange = MinMax.createInclIncl doseMin doseMax
+
+ printfn "10–20 mg/kg: %s" (doseRange |> MinMax.toString ValueUnit.toStringDecimalDutchShort ValueUnit.toStringDecimalDutchShort "min " "min " "max " "max ")
+
+ printfn ""
+
+
+demonstrateValueUnitParsing ()
+
+
+// =============================================================================
+// STEP 4-5: Translate each scenario to the Medication string representation
+// =============================================================================
+//
+// The Medication string format is the canonical text representation used by
+// Medication.fromString / Medication.toString.
+// See: src/Informedica.GenORDER.Lib/Medication.fs
+//
+// This step shows how each FHIR scenario maps to that format.
+
+/// Map FHIR DoseType string to the GenPRES OrderType token used in the Medication text format
+let doseTypeToOrderType =
+ function
+ | "Once" -> "OnceOrder"
+ | "OnceTimed" -> "OnceTimedOrder"
+ | "Discontinuous" -> "DiscontinuousOrder"
+ | "Timed" -> "TimedOrder"
+ | "Continuous" -> "ContinuousOrder"
+ | other -> failwith $"Unknown DoseType: {other}"
+
+
+/// Build the Frequencies field string for a scenario schema
+let buildFrequenciesString (schema: AdministrationSchema) =
+ match schema.Frequency, schema.TimeUnit with
+ | Some freq, Some unit ->
+ let period = schema.TimePeriod |> Option.defaultValue 1m
+
+ if period = 1m then
+ $"%i{freq} x/{unit}"
+ else
+ $"%i{freq} x/{period} {unit}"
+ | _ -> ""
+
+
+/// Build the Time (infusion duration) field string for an OnceTimed or Timed scenario.
+/// Computes duration = adminQuantity / rate when both are available.
+let buildTimeString (adminQuantityMl: decimal) (schema: AdministrationSchema) =
+ match schema.RateQuantity, schema.RateUnit1, schema.RateUnit2 with
+ | Some rate, Some _, Some _ when rate > 0m ->
+ // Duration in hours = volume / rate; convert to minutes
+ let durationMin = adminQuantityMl / rate * 60m
+ let durationMin = System.Math.Ceiling(float durationMin) |> int
+ // Provide a small time window around the computed duration (±25%)
+ let minTime = max 1 (durationMin - durationMin / 4)
+ let maxTime = durationMin + durationMin / 4
+ $"%i{minTime} min - %i{maxTime} min"
+ | _ -> ""
+
+
+/// Translate a FhirScenario into the Medication text string that can be parsed by Medication.fromString.
+///
+/// This function creates a Medication text template based on the scenario metadata.
+/// The concentrations and dose limits are populated with example values that reflect
+/// the clinical intent described in the scenario.
+let scenarioToMedicationText (id: string) (scenario: FhirScenario) : string =
+ let orderType = scenario.DoseType |> doseTypeToOrderType
+
+ let adjustStr =
+ $"{scenario.WeightKg} kg"
+
+ let frequenciesStr = buildFrequenciesString scenario.Schema
+ let timeStr = buildTimeString scenario.AdminQuantity scenario.Schema
+
+ // Build component text based on the products in the scenario
+ let componentText =
+ scenario.Products
+ |> List.mapi (fun i product ->
+ // Determine concentration and dose fields from clinical context
+ let (concentrationStr, doseStr) =
+ match scenario.MedicationName, scenario.DoseType with
+ | "paracetamol", "Once" when scenario.Route = "RECTAAL" ->
+ "120;240;500;1000;125;250;60;30;360;90;750;180 mg/stuk",
+ "paracetamol, [dun] mg, [qty-adj] 40 mg/kg/dosis, [qty] max 1000 mg/dosis"
+
+ | "paracetamol", "OnceTimed" ->
+ "10 mg/ml",
+ "paracetamol, [dun] mg, [qty-adj] 20 mg/kg/dosis, [qty] max 1000 mg/dosis"
+
+ | "paracetamol", "Discontinuous" when scenario.Route = "RECTAAL" ->
+ "120;240;500;1000;125;250;60;30;360;90;750;180 mg/stuk",
+ "paracetamol, [dun] mg, [qty-adj] 10 mg/kg - 20 mg/kg/dosis, [qty] max 1000 mg/dosis"
+
+ | "paracetamol", ("Timed" | "Discontinuous") ->
+ "10 mg/ml",
+ "paracetamol, [dun] mg, [per-time-adj] 60 mg/kg/dag, [qty] max 1000 mg/dosis"
+
+ | "gentamicine", _ ->
+ "1;2;4 mg/ml",
+ "gentamicine, [dun] mg, [qty-adj] 5 mg/kg/dosis, [qty] max 500 mg/dosis"
+
+ | "propofol", _ ->
+ "10 mg/ml",
+ "propofol, [dun] mg, [rate-adj] 1 mg/kg/uur - 4 mg/kg/uur"
+
+ | "noradrenaline", _ when i = 0 ->
+ "1 mg/ml",
+ "noradrenaline, [dun] microg, [rate-adj] 0,05 microg/kg/min - 2 microg/kg/min"
+
+ | "noradrenaline", _ ->
+ // Second component: glucose 10% diluent (no active substance dose)
+ "100 mg/ml", ""
+
+ | _ ->
+ "1 mg/ml", ""
+
+ // Component names
+ let compName =
+ if scenario.Products.Length > 1 && i = 1 then
+ "gluc 10%"
+ else
+ scenario.MedicationName
+
+ let quantityStr =
+ match product.Unit with
+ | "stuk" -> $"1 stuk"
+ | "mL" ->
+ // Show available container sizes (from known product catalogue)
+ match scenario.MedicationName with
+ | "paracetamol" -> "50;100 ml"
+ | "gentamicine" -> "1 ml"
+ | "propofol" -> "50;200 ml"
+ | "noradrenaline" when i = 0 -> "5;10;20 ml"
+ | _ -> "50 ml"
+ | u -> $"1 {u}"
+
+ let divisibleStr =
+ match product.Unit with
+ | "stuk" -> "1"
+ | "mL" -> "10"
+ | _ -> "1"
+
+ let substanceDoseStr =
+ if doseStr = "" then "Dose:" else $"Dose: %s{doseStr}"
+
+ $"""
+\tName: %s{compName}
+\tForm: %s{scenario.Shape}
+\tQuantities: %s{quantityStr}
+\tDivisible: %s{divisibleStr}
+\tDose:
+\tSolution:
+\tSubstances:
+
+\t\tName: %s{compName}
+\t\tQuantities:
+\t\tConcentrations: %s{concentrationStr}
+\t\t%s{substanceDoseStr}
+\t\tSolution:"""
+ )
+ |> String.concat "\n"
+
+ // Determine the top-level Dose field
+ let topDoseStr =
+ match scenario.DoseType with
+ | "Once" when scenario.Route = "RECTAAL" ->
+ "[dun], [qty] 1 stuk/dosis"
+ | "OnceTimed" ->
+ "[dun], [qty-adj] max 20 ml/kg/dosis, [qty] max 1000 ml/dosis"
+ | "Continuous" ->
+ ""
+ | _ -> ""
+
+ $"""Id: %s{id}
+Name: %s{scenario.MedicationName}
+Quantity:
+Quantities:
+Route: %s{scenario.Route}
+OrderType: %s{orderType}
+Adjust: %s{adjustStr}
+Frequencies: %s{frequenciesStr}
+Time: %s{timeStr}
+Dose: %s{topDoseStr}
+Div:
+DoseCount: 1 x
+Components:%s{componentText}"""
+
+
+// Build all scenario texts
+let scenarioMedicationTexts =
+ allScenarios
+ |> List.map (fun scenario ->
+ let guid = Guid.NewGuid().ToString()
+ scenario, scenarioToMedicationText guid scenario
+ )
+
+
+printfn "\n=== STEP 4: Medication Text Representations ==="
+
+for scenario, text in scenarioMedicationTexts do
+ printfn "\n--- Scenario %s: %s ---" scenario.ScenarioId scenario.Description
+ printfn "%s" text
+
+
+// =============================================================================
+// STEP 6: Run each scenario through Medication.fromString
+// =============================================================================
+
+printfn "\n=== STEP 5-6: Running Scenarios through Medication.fromString ==="
+
+let runScenario (scenario: FhirScenario) (medText: string) =
+ printfn "\n--- Scenario %s: %s ---" scenario.ScenarioId scenario.Description
+ printfn "Patient: %M kg, %M cm, %s" scenario.WeightKg scenario.HeightCm scenario.Gender
+ printfn "Indication: %s" scenario.Indication
+
+ match Medication.fromString medText with
+ | Error errors ->
+ printfn "PARSE ERROR:"
+ errors |> List.iter (printfn " - %s")
+
+ | Ok med ->
+ printfn "Parsed OK: %s (%A)" med.Name med.OrderType
+
+ // Convert to order DTO and build the order
+ match med |> Medication.toOrderDto |> Order.Dto.fromDto with
+ | Error exn ->
+ printfn "ORDER BUILD ERROR: %A" exn
+
+ | Ok order ->
+ printfn "Order built OK."
+
+ // Solve the order to propagate constraints
+ let solved =
+ order
+ |> Order.solveMinMax "ImplementationPlan" true OrderLogging.noOp
+
+ match solved with
+ | Error(_, msg) ->
+ printfn "SOLVE WARNING: %s (partial result may still be valid)" msg
+ // Print the partial order anyway
+ order |> Order.printTable ConsoleTables.Format.Minimal
+
+ | Ok solvedOrder ->
+ printfn "Solved OK."
+ solvedOrder |> Order.printTable ConsoleTables.Format.Minimal
+
+
+for scenario, medText in scenarioMedicationTexts do
+ runScenario scenario medText
+
+
+// =============================================================================
+// STEP 7: Print summary of results
+// =============================================================================
+
+printfn "\n=== STEP 7: Summary ==="
+printfn "Processed %i FHIR scenarios from the interface specification." (List.length allScenarios)
+printfn ""
+
+for scenario, medText in scenarioMedicationTexts do
+ let result =
+ medText
+ |> Medication.fromString
+ |> Result.bind (fun med ->
+ med
+ |> Medication.toOrderDto
+ |> Order.Dto.fromDto
+ |> Result.mapError (fun exn -> [ $"{exn}" ])
+ )
+
+ let status =
+ match result with
+ | Ok _ -> "OK"
+ | Error errs -> $"ERROR: {errs |> String.concat '; '}"
+
+ printfn " Scenario %-6s %-50s %s" scenario.ScenarioId scenario.Description status
+
+
+// =============================================================================
+// STEP 8: Sketch a FHIR-based translation approach
+// =============================================================================
+//
+// This section outlines how the GenPRES Medication model would be represented
+// in FHIR R4 MedicationRequest resources.
+//
+// The FHIR MedicationRequest resource encodes the same information as the
+// Medication text format, but in a structured, interoperable form.
+//
+// Key FHIR resources used:
+// - MedicationRequest : the prescription order (one per scenario)
+// - Medication : the medication product identified by GPK code
+// - Dosage : frequency, timing, route, and dose quantity
+// - DosageInstruction : rate for continuous/timed infusion orders
+//
+// Mapping outline:
+//
+// FhirScenario.MedicationName → MedicationRequest.medication.coding (GPK system)
+// FhirScenario.Route → MedicationRequest.dosageInstruction.route (G-Standard thesaurus)
+// FhirScenario.WeightKg → MedicationRequest.dosageInstruction.doseAndRate (weight-based)
+// FhirScenario.Schema.Frequency → MedicationRequest.dosageInstruction.timing.repeat.frequency
+// FhirScenario.Schema.TimeUnit → MedicationRequest.dosageInstruction.timing.repeat.periodUnit
+// FhirScenario.Schema.RateQuantity → MedicationRequest.dosageInstruction.doseAndRate.rateRatio
+// FhirScenario.Schema.ExactTimes → MedicationRequest.dosageInstruction.timing.repeat.timeOfDay
+//
+// Example FHIR JSON skeleton for scenario 6.3 (Paracetamol 4x/dag rectal):
+//
+// {
+// "resourceType": "MedicationRequest",
+// "id": "",
+// "status": "active",
+// "intent": "order",
+// "medicationCodeableConcept": {
+// "coding": [{
+// "system": "urn:oid:2.16.840.1.113883.2.4.4.7", // G-Standard GPK
+// "code": "",
+// "display": "paracetamol 180 mg/stuk zetpil"
+// }]
+// },
+// "subject": { "reference": "Patient/123456" },
+// "dosageInstruction": [{
+// "route": {
+// "coding": [{ "system": "urn:oid:2.16.840.1.113883.2.4.4.9", "code": "12" }],
+// "text": "RECTAAL"
+// },
+// "doseAndRate": [{
+// "doseQuantity": { "value": 1, "unit": "stuk" }
+// }],
+// "timing": {
+// "repeat": {
+// "frequency": 4,
+// "period": 1,
+// "periodUnit": "d"
+// }
+// }
+// }]
+// }
+//
+// Reverse mapping (FHIR → GenPRES Medication text):
+//
+// The fromFhirRequest function (to be implemented in Informedica.FHIR.Lib) would:
+// 1. Extract the GPK code from medicationCodeableConcept.coding
+// 2. Call ZIndex.GenericProduct.get [gpkCode] to resolve concentration data
+// 3. Map timing.repeat.frequency + periodUnit → Frequencies ValueUnit
+// 4. Map doseAndRate.rateRatio → Rate ValueUnit (for continuous orders)
+// 5. Map subject.weight (from Patient resource) → Adjust ValueUnit
+// 6. Construct a Medication record and call Medication.toOrderDto
+// 7. Return the solved order
+
+printfn "\n=== STEP 8: FHIR-based Translation Approach ==="
+printfn """
+The Informedica.FHIR.Lib library will provide two main functions:
+
+ toFhirRequest : Medication → FHIR MedicationRequest JSON
+ fromFhirRequest : FHIR MedicationRequest JSON → Result
+
+The mapping between the GenPRES Medication text format and FHIR R4 MedicationRequest
+is defined as follows:
+
+ GenPRES field FHIR field
+ ─────────────────────────────────────────────────────────────────────────────
+ Name medicationCodeableConcept.coding[GPK].display
+ Route dosageInstruction.route.coding[G-Standard thesaurus]
+ OrderType dosageInstruction.timing + intent
+ Adjust (weight) dosageInstruction.doseAndRate.doseQuantity (per kg)
+ Frequencies dosageInstruction.timing.repeat.frequency + periodUnit
+ Time (infusion) dosageInstruction.timing.repeat.duration + durationUnit
+ Component.Concentration Medication.ingredient.strength.numerator
+ Substance.Dose[qty-adj] dosageInstruction.doseAndRate.doseRange (weight-based)
+ Substance.Dose[rate-adj] dosageInstruction.doseAndRate.rateRatio
+ Schema.ExactTimes dosageInstruction.timing.repeat.timeOfDay
+
+For multi-product scenarios (6.6+), each component maps to a separate
+Medication.ingredient entry, with the primary product identified by GPK code and
+diluents identified by their own GPK codes.
+
+Next implementation steps for Informedica.FHIR.Lib:
+ 1. Add a FHIR R4 serialization dependency (e.g. Hl7.Fhir.R4 NuGet package)
+ 2. Implement toFhirRequest using the mapping table above
+ 3. Implement fromFhirRequest using ZIndex GPK lookup + Medication.fromString
+ 4. Validate round-trip: fromFhirRequest (toFhirRequest medication) = Ok medication
+ 5. Write Expecto tests for each scenario defined in this script
+"""
diff --git a/src/Informedica.FHIR.Lib/Scripts/load.fsx b/src/Informedica.FHIR.Lib/Scripts/load.fsx
new file mode 100644
index 00000000..91e4370e
--- /dev/null
+++ b/src/Informedica.FHIR.Lib/Scripts/load.fsx
@@ -0,0 +1,25 @@
+#I __SOURCE_DIRECTORY__
+
+let stopWatch = System.Diagnostics.Stopwatch()
+stopWatch.Start()
+
+fsi.AddPrinter _.ToShortDateString()
+
+#load "../../../scripts/load-dependencies.fsx"
+
+#r "../../Informedica.Utils.Lib/bin/Debug/net10.0/Informedica.Utils.Lib.dll"
+#r "../../Informedica.Logging.Lib/bin/Debug/net10.0/Informedica.Logging.Lib.dll"
+#r "../../Informedica.GenUnits.Lib/bin/Debug/net10.0/Informedica.GenUnits.Lib.dll"
+#r "../../Informedica.GenCore.Lib/bin/Debug/net10.0/Informedica.GenCore.Lib.dll"
+#r "../../Informedica.GenSolver.Lib/bin/Debug/net10.0/Informedica.GenSolver.Lib.dll"
+#r "../../Informedica.GenFORM.Lib/bin/Debug/net10.0/Informedica.GenFORM.Lib.dll"
+#r "../../Informedica.ZIndex.Lib/bin/Debug/net10.0/Informedica.ZIndex.Lib.dll"
+#r "../../Informedica.GenORDER.Lib/bin/Debug/net10.0/Informedica.GenOrder.Lib.dll"
+
+open System
+open Informedica.Utils.Lib
+
+let zindexPath = __SOURCE_DIRECTORY__ |> Path.combineWith "../../../"
+Environment.CurrentDirectory <- zindexPath
+
+printfn $"elapsed time: {stopWatch.ElapsedMilliseconds / 1000L}"
From b19bb5f0322a30e5007e60a16b9c1e5f0005e986 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 24 Mar 2026 11:11:34 +0000
Subject: [PATCH 3/5] fix(fhir): use correct uppercase DLL names in load.fsx
Co-authored-by: halcwb <683631+halcwb@users.noreply.github.com>
Agent-Logs-Url: https://github.com/informedica/GenPRES/sessions/2414b85b-c4e0-4b07-bb7a-4f2ded4a8294
---
src/Informedica.FHIR.Lib/Scripts/load.fsx | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/Informedica.FHIR.Lib/Scripts/load.fsx b/src/Informedica.FHIR.Lib/Scripts/load.fsx
index 91e4370e..6e23a3a5 100644
--- a/src/Informedica.FHIR.Lib/Scripts/load.fsx
+++ b/src/Informedica.FHIR.Lib/Scripts/load.fsx
@@ -9,12 +9,12 @@ fsi.AddPrinter _.ToShortDateString()
#r "../../Informedica.Utils.Lib/bin/Debug/net10.0/Informedica.Utils.Lib.dll"
#r "../../Informedica.Logging.Lib/bin/Debug/net10.0/Informedica.Logging.Lib.dll"
-#r "../../Informedica.GenUnits.Lib/bin/Debug/net10.0/Informedica.GenUnits.Lib.dll"
-#r "../../Informedica.GenCore.Lib/bin/Debug/net10.0/Informedica.GenCore.Lib.dll"
-#r "../../Informedica.GenSolver.Lib/bin/Debug/net10.0/Informedica.GenSolver.Lib.dll"
+#r "../../Informedica.GenUNITS.Lib/bin/Debug/net10.0/Informedica.GenUNITS.Lib.dll"
+#r "../../Informedica.GenCORE.Lib/bin/Debug/net10.0/Informedica.GenCORE.Lib.dll"
+#r "../../Informedica.GenSOLVER.Lib/bin/Debug/net10.0/Informedica.GenSOLVER.Lib.dll"
#r "../../Informedica.GenFORM.Lib/bin/Debug/net10.0/Informedica.GenFORM.Lib.dll"
#r "../../Informedica.ZIndex.Lib/bin/Debug/net10.0/Informedica.ZIndex.Lib.dll"
-#r "../../Informedica.GenORDER.Lib/bin/Debug/net10.0/Informedica.GenOrder.Lib.dll"
+#r "../../Informedica.GenORDER.Lib/bin/Debug/net10.0/Informedica.GenORDER.Lib.dll"
open System
open Informedica.Utils.Lib
From b2ad01b2894d4a82c92141607ab80a1824f738ac Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 24 Mar 2026 11:35:14 +0000
Subject: [PATCH 4/5] refactor(fhir): rename RateUnit1/2 to
RateFormUnit/RateTimeUnit; remove hardcoded dosing knowledge
Co-authored-by: halcwb <683631+halcwb@users.noreply.github.com>
Agent-Logs-Url: https://github.com/informedica/GenPRES/sessions/88fb1713-6692-41d3-aaa6-a4b2fd7fb287
---
.../Scripts/ImplementationPlan.fsx | 747 +++++++++---------
1 file changed, 358 insertions(+), 389 deletions(-)
diff --git a/src/Informedica.FHIR.Lib/Scripts/ImplementationPlan.fsx b/src/Informedica.FHIR.Lib/Scripts/ImplementationPlan.fsx
index 318eda78..9c1183d9 100644
--- a/src/Informedica.FHIR.Lib/Scripts/ImplementationPlan.fsx
+++ b/src/Informedica.FHIR.Lib/Scripts/ImplementationPlan.fsx
@@ -3,15 +3,31 @@
/// This script implements the plan described in docs/mdr/interface/genpres_interface_specification.md
/// and follows the steps outlined in the GenPRES FHIR integration issue.
///
+/// ## Key principle
+///
+/// A FHIR scenario provides only:
+/// 1. Patient data (weight, height, age, gender)
+/// 2. Indication
+/// 3. Route
+/// 4. Shape (pharmaceutical form)
+/// 5. Dose type
+/// 6. Component Orderable Quantities (from the products block)
+/// 7. Orderable Dose Quantity and/or Rate (from administration + schema)
+/// 8. Schedule Frequency (from schema)
+///
+/// Everything else (concentrations, dose limits) is derived by lookup
+/// via ZIndex/GenFORM or by calculation.
+///
/// ## Steps
///
/// 1. Define the FHIR scenarios from the specification
/// 2. Look up product information from GPK product codes via ZIndex
/// 3. Parse quantitative variables to ValueUnits
-/// 4. Translate each scenario to the Medication string representation
-/// 5. Run each scenario through Medication.fromString as an order
-/// 6. Print the results
-/// 7. Sketch a FHIR-based translation approach
+/// 4. Reconstruct an OrderScenario from the scenario context via OrderContext lookup
+/// 5. Apply orderable quantities and schedule from the FHIR scenario
+/// 6. Run the scenarios through the order pipeline
+/// 7. Print the results
+/// 8. Sketch a FHIR-based solution
#load "load.fsx"
@@ -20,6 +36,7 @@ open MathNet.Numerics
open Informedica.Utils.Lib.BCL
open Informedica.GenCore.Lib.Ranges
open Informedica.GenUnits.Lib
+open Informedica.GenForm.Lib
open Informedica.GenOrder.Lib
@@ -32,43 +49,49 @@ open Informedica.GenOrder.Lib
// NOTE: The GPK codes in the specification are PLACEHOLDERS (e.g. "2345678").
// Real codes must come from the G-Standard database. See Step 2 for product lookup.
-/// Represents a product as described in the FHIR scenario YAML
+/// Represents a single product component as described in the FHIR scenario YAML products block
type ScenarioProduct =
{
// Placeholder GPK code from the spec (not a real G-Standard code)
GpkPlaceholder: string
- // Amount of product used
+ // Orderable quantity of this component
Quantity: decimal
+ // Unit for the orderable quantity (e.g. "mL", "stuk")
Unit: string
// Human-readable description from the spec comment
Description: string
}
-/// Represents the administration schedule from the FHIR YAML schema block
+/// Represents the administration schedule from the FHIR YAML schema block.
+/// Only contains what is directly present in the FHIR scenario —
+/// concentrations and dose limits are NOT included here.
type AdministrationSchema =
{
- // Number of times per period (null for continuous)
+ // Number of administrations per period (None for continuous orders)
Frequency: int option
- // Duration of the period (e.g. 1 for per day, 36 for per 36 hours)
+ // Duration of the frequency period (e.g. 1 for "per day", 36 for "per 36 hours")
TimePeriod: decimal option
- // Unit of the period (e.g. "dag", "uur")
+ // Unit of the frequency period (e.g. "dag", "uur")
TimeUnit: string option
- // Infusion rate quantity
+ // Infusion rate quantity (e.g. 85 for "85 mL/uur")
RateQuantity: decimal option
- // Rate unit numerator (e.g. "mL")
- RateUnit1: string option
- // Rate unit denominator (e.g. "uur")
- RateUnit2: string option
- // Exact administration times (for timed orders)
+ // Rate form unit — the numerator unit (e.g. "mL" for "mL/uur")
+ RateFormUnit: string option
+ // Rate time unit — the denominator unit (e.g. "uur" for "mL/uur")
+ RateTimeUnit: string option
+ // Exact administration times (for Timed orders)
ExactTimes: string list
}
-/// Represents a FHIR scenario as described in section 6 of the specification
+/// Represents a FHIR treatment scenario as described in section 6 of the specification.
+/// Contains only the data that can be directly derived from the FHIR resource —
+/// specifically: patient context, indication, route, shape, dose type,
+/// orderable quantities, and schedule.
type FhirScenario =
{
- // Scenario identifier (matches section number)
+ // Scenario identifier (matches the spec section number)
ScenarioId: string
// Short descriptive name
Description: string
@@ -76,24 +99,25 @@ type FhirScenario =
WeightKg: decimal
// Patient height in cm
HeightCm: decimal
- // Gender ("male" / "female")
+ // Patient gender: "male" or "female"
Gender: string
- // Clinical indication
+ // Clinical indication (maps to Filter.Indication)
Indication: string
- // Generic medication name
+ // Generic medication name (maps to Filter.Generic)
MedicationName: string
- // Administration route (Dutch G-Standard term)
+ // Administration route, Dutch G-Standard term (maps to Filter.Route)
Route: string
- // Pharmaceutical form (Dutch)
+ // Pharmaceutical form, Dutch (maps to Filter.Form)
Shape: string
- // Order type: "Once", "OnceTimed", "Discontinuous", "Timed", "Continuous"
+ // Dose type: "Once", "OnceTimed", "Discontinuous", "Timed", "Continuous"
+ // (maps to Filter.DoseType)
DoseType: string
- // Products from the YAML products block
+ // Component orderable quantities from the YAML products block
Products: ScenarioProduct list
- // Administration quantity and unit
+ // Total administration quantity and unit
AdminQuantity: decimal
AdminUnit: string
- // Scheduling
+ // Schedule from the YAML schema block
Schema: AdministrationSchema
}
@@ -128,8 +152,8 @@ let scenario61 =
TimePeriod = None
TimeUnit = None
RateQuantity = None
- RateUnit1 = None
- RateUnit2 = None
+ RateFormUnit = None
+ RateTimeUnit = None
ExactTimes = []
}
}
@@ -165,8 +189,8 @@ let scenario62 =
TimePeriod = None
TimeUnit = None
RateQuantity = Some 85m
- RateUnit1 = Some "mL"
- RateUnit2 = Some "uur"
+ RateFormUnit = Some "mL"
+ RateTimeUnit = Some "uur"
ExactTimes = []
}
}
@@ -202,8 +226,8 @@ let scenario63 =
TimePeriod = Some 1m
TimeUnit = Some "dag"
RateQuantity = None
- RateUnit1 = None
- RateUnit2 = None
+ RateFormUnit = None
+ RateTimeUnit = None
ExactTimes = []
}
}
@@ -239,8 +263,8 @@ let scenario631 =
TimePeriod = Some 36m
TimeUnit = Some "uur"
RateQuantity = None
- RateUnit1 = None
- RateUnit2 = None
+ RateFormUnit = None
+ RateTimeUnit = None
ExactTimes = []
}
}
@@ -276,8 +300,8 @@ let scenario64 =
TimePeriod = Some 1m
TimeUnit = Some "dag"
RateQuantity = Some 70m
- RateUnit1 = Some "mL"
- RateUnit2 = Some "uur"
+ RateFormUnit = Some "mL"
+ RateTimeUnit = Some "uur"
ExactTimes = [ "10:00"; "16:00"; "22:00"; "04:00" ]
}
}
@@ -313,8 +337,8 @@ let scenario65 =
TimePeriod = None
TimeUnit = None
RateQuantity = Some 3.3m
- RateUnit1 = Some "mL"
- RateUnit2 = Some "uur"
+ RateFormUnit = Some "mL"
+ RateTimeUnit = Some "uur"
ExactTimes = []
}
}
@@ -356,8 +380,8 @@ let scenario66 =
TimePeriod = None
TimeUnit = None
RateQuantity = Some 1m
- RateUnit1 = Some "mL"
- RateUnit2 = Some "uur"
+ RateFormUnit = Some "mL"
+ RateTimeUnit = Some "uur"
ExactTimes = []
}
}
@@ -382,7 +406,10 @@ let allScenarios =
// NOTE: The GPK codes in the spec are PLACEHOLDERS. A real implementation would
// use ZIndex.GenericProduct.get [gpkCode] to resolve a product by its actual
// G-Standard GPK code. Here we demonstrate the lookup API by searching for
-// products by their generic name, which is what the spec intends.
+// products by their generic name.
+//
+// From these results the concentrations and dose limits are derived, NOT
+// hardcoded. See Step 4 for how this feeds into OrderScenario reconstruction.
open Informedica.ZIndex.Lib
@@ -404,7 +431,7 @@ let printGenericProduct (gp: Types.GenericProduct) =
sub.GenericUnit
-/// Lookup all products for a given generic medication name using ZIndex
+/// Lookup all GenericProducts for a given medication name using ZIndex
let lookupByGenericName (name: string) =
GenericProduct.get []
|> Array.filter (fun gp -> gp.Name |> String.equalsCapInsens name)
@@ -414,7 +441,8 @@ let lookupByGenericName (name: string) =
let demonstrateProductLookup () =
printfn "\n=== STEP 2: Product Lookup via ZIndex ==="
printfn "NOTE: Spec uses placeholder GPK codes (e.g. '2345678')."
- printfn "Real lookup uses ZIndex.GenericProduct.get [gpkCode].\n"
+ printfn "Real lookup uses ZIndex.GenericProduct.get [gpkCode]."
+ printfn "Concentrations and dose limits are derived from these results, not hardcoded.\n"
let medicationNames =
[
@@ -432,6 +460,7 @@ let demonstrateProductLookup () =
printfn " (no products found – run after loading G-Standard data)"
else
products |> Array.truncate 3 |> Array.iter printGenericProduct
+
if products.Length > 3 then
printfn " ... and %i more products" (products.Length - 3)
@@ -445,75 +474,66 @@ demonstrateProductLookup ()
// STEP 3: Parse quantitative variables to ValueUnits
// =============================================================================
//
-// Demonstrate how the scenario quantities translate to GenUnits ValueUnit values.
-// These are the same unit types used in the Medication text format.
+// Demonstrate how the FHIR scenario quantities (the only values present in the
+// FHIR resources) translate to GenUnits ValueUnit values.
+// These correspond to the four directly available data categories:
+// 1. Component Orderable Quantities
+// 2. Orderable Dose Quantity
+// 3. Orderable Dose Rate
+// 4. Schedule Frequency
let demonstrateValueUnitParsing () =
printfn "\n=== STEP 3: Parsing Quantitative Variables to ValueUnits ==="
- // Weight adjust unit
+ // --- Patient context (for adjust calculations) ---
+
let weight19_5kg = 19.5m |> BigRational.fromDecimal |> ValueUnit.singleWithUnit Units.Weight.kiloGram
- printfn "Patient weight: %s" (weight19_5kg |> ValueUnit.toStringDecimalDutchShort)
+ printfn "Patient weight: %s" (weight19_5kg |> ValueUnit.toStringDecimalDutchShort)
+
+ // --- 1. Component Orderable Quantities (from FHIR products block) ---
- // Suppository quantity
let stuk = Units.General.general "stuk"
let qty1stuk = 1N |> ValueUnit.singleWithUnit stuk
- printfn "1 stuk: %s" (qty1stuk |> ValueUnit.toStringDecimalDutchShort)
+ printfn "Orderable qty (stuk): %s" (qty1stuk |> ValueUnit.toStringDecimalDutchShort)
- // Concentration: 750 mg/stuk
- let conc750mgStuk =
- 750N
- |> ValueUnit.singleWithUnit (Units.Mass.milliGram |> Units.per stuk)
+ let qty22mL = 22N |> ValueUnit.singleWithUnit Units.Volume.milliLiter
+ printfn "Orderable qty (mL): %s" (qty22mL |> ValueUnit.toStringDecimalDutchShort)
- printfn "750 mg/stuk: %s" (conc750mgStuk |> ValueUnit.toStringDecimalDutchShort)
+ // --- 2. Orderable Dose Quantity (from FHIR administration block) ---
- // IV concentration: 10 mg/mL
- let conc10mgMl =
- 10N
- |> ValueUnit.singleWithUnit (Units.Mass.milliGram |> Units.per Units.Volume.milliLiter)
+ // Dose quantity (absolute) — from admin quantity
+ let adminQty1stuk = 1N |> ValueUnit.singleWithUnit stuk
+ printfn "Admin qty: %s" (adminQty1stuk |> ValueUnit.toStringDecimalDutchShort)
- printfn "10 mg/mL: %s" (conc10mgMl |> ValueUnit.toStringDecimalDutchShort)
+ // --- 3. Orderable Dose Rate (from FHIR schema rate) ---
- // Infusion rate: 85 mL/uur
+ // Rate: 85 mL/uur
let rate85mlHr =
85N
|> ValueUnit.singleWithUnit (Units.Volume.milliLiter |> Units.per Units.Time.hour)
- printfn "85 mL/uur: %s" (rate85mlHr |> ValueUnit.toStringDecimalDutchShort)
+ printfn "Rate (85 mL/uur): %s" (rate85mlHr |> ValueUnit.toStringDecimalDutchShort)
- // Dose adjust: 40 mg/kg/dose (quantity-adjust, no time)
- let doseAdj40 =
- 40N
- |> ValueUnit.singleWithUnit (
- Units.Mass.milliGram
- |> Units.per Units.Weight.kiloGram
- )
-
- printfn "40 mg/kg/dose: %s" (doseAdj40 |> ValueUnit.toStringDecimalDutchShort)
+ // Rate: 3.3 mL/uur (propofol, stored as BigRational fraction)
+ let rate33mlHr =
+ BigRational.fromDecimal 3.3m
+ |> ValueUnit.singleWithUnit (Units.Volume.milliLiter |> Units.per Units.Time.hour)
- // Dose adjust: 3 mg/kg/uur (rate-adjust, with time)
- let rateAdj3 =
- 3N
- |> ValueUnit.singleWithUnit (
- Units.Mass.milliGram
- |> Units.per Units.Weight.kiloGram
- |> Units.per Units.Time.hour
- )
+ printfn "Rate (3.3 mL/uur): %s" (rate33mlHr |> ValueUnit.toStringDecimalDutchShort)
- printfn "3 mg/kg/uur: %s" (rateAdj3 |> ValueUnit.toStringDecimalDutchShort)
+ // --- 4. Schedule Frequency (from FHIR schema pattern) ---
// Frequency: 4 x/dag
let freq4xDay =
[| 4N |] |> ValueUnit.withUnit (Units.Count.times |> Units.per Units.Time.day)
- printfn "4 x/dag: %s" (freq4xDay |> ValueUnit.toStringDecimalDutchShort)
+ printfn "Frequency (4 x/dag): %s" (freq4xDay |> ValueUnit.toStringDecimalDutchShort)
- // Per-time dose: 10–20 mg/kg/dose (quantity-adjust MinMax)
- let doseMin = 10N |> ValueUnit.singleWithUnit (Units.Mass.milliGram |> Units.per Units.Weight.kiloGram)
- let doseMax = 20N |> ValueUnit.singleWithUnit (Units.Mass.milliGram |> Units.per Units.Weight.kiloGram)
- let doseRange = MinMax.createInclIncl doseMin doseMax
+ // Frequency: 1 x/36 uur (gentamicine neonatal)
+ let freq1x36h =
+ [| 1N |] |> ValueUnit.withUnit (Units.Count.times |> Units.per Units.Time.hour)
- printfn "10–20 mg/kg: %s" (doseRange |> MinMax.toString ValueUnit.toStringDecimalDutchShort ValueUnit.toStringDecimalDutchShort "min " "min " "max " "max ")
+ printfn "Frequency (1x/36 uur): %s" (freq1x36h |> ValueUnit.toStringDecimalDutchShort)
printfn ""
@@ -522,243 +542,195 @@ demonstrateValueUnitParsing ()
// =============================================================================
-// STEP 4-5: Translate each scenario to the Medication string representation
+// STEP 4: Reconstruct an OrderScenario from FHIR context via OrderContext lookup
// =============================================================================
//
-// The Medication string format is the canonical text representation used by
-// Medication.fromString / Medication.toString.
-// See: src/Informedica.GenORDER.Lib/Medication.fs
+// A FHIR scenario provides patient data, indication, route, shape, and dose type.
+// These are exactly the inputs needed to reconstruct an OrderScenario via the
+// OrderContext lookup mechanism.
//
-// This step shows how each FHIR scenario maps to that format.
-
-/// Map FHIR DoseType string to the GenPRES OrderType token used in the Medication text format
-let doseTypeToOrderType =
- function
- | "Once" -> "OnceOrder"
- | "OnceTimed" -> "OnceTimedOrder"
- | "Discontinuous" -> "DiscontinuousOrder"
- | "Timed" -> "TimedOrder"
- | "Continuous" -> "ContinuousOrder"
- | other -> failwith $"Unknown DoseType: {other}"
-
-
-/// Build the Frequencies field string for a scenario schema
-let buildFrequenciesString (schema: AdministrationSchema) =
- match schema.Frequency, schema.TimeUnit with
- | Some freq, Some unit ->
- let period = schema.TimePeriod |> Option.defaultValue 1m
-
- if period = 1m then
- $"%i{freq} x/{unit}"
- else
- $"%i{freq} x/{period} {unit}"
- | _ -> ""
-
-
-/// Build the Time (infusion duration) field string for an OnceTimed or Timed scenario.
-/// Computes duration = adminQuantity / rate when both are available.
-let buildTimeString (adminQuantityMl: decimal) (schema: AdministrationSchema) =
- match schema.RateQuantity, schema.RateUnit1, schema.RateUnit2 with
- | Some rate, Some _, Some _ when rate > 0m ->
- // Duration in hours = volume / rate; convert to minutes
- let durationMin = adminQuantityMl / rate * 60m
- let durationMin = System.Math.Ceiling(float durationMin) |> int
- // Provide a small time window around the computed duration (±25%)
- let minTime = max 1 (durationMin - durationMin / 4)
- let maxTime = durationMin + durationMin / 4
- $"%i{minTime} min - %i{maxTime} min"
- | _ -> ""
-
-
-/// Translate a FhirScenario into the Medication text string that can be parsed by Medication.fromString.
-///
-/// This function creates a Medication text template based on the scenario metadata.
-/// The concentrations and dose limits are populated with example values that reflect
-/// the clinical intent described in the scenario.
-let scenarioToMedicationText (id: string) (scenario: FhirScenario) : string =
- let orderType = scenario.DoseType |> doseTypeToOrderType
-
- let adjustStr =
- $"{scenario.WeightKg} kg"
-
- let frequenciesStr = buildFrequenciesString scenario.Schema
- let timeStr = buildTimeString scenario.AdminQuantity scenario.Schema
-
- // Build component text based on the products in the scenario
- let componentText =
- scenario.Products
- |> List.mapi (fun i product ->
- // Determine concentration and dose fields from clinical context
- let (concentrationStr, doseStr) =
- match scenario.MedicationName, scenario.DoseType with
- | "paracetamol", "Once" when scenario.Route = "RECTAAL" ->
- "120;240;500;1000;125;250;60;30;360;90;750;180 mg/stuk",
- "paracetamol, [dun] mg, [qty-adj] 40 mg/kg/dosis, [qty] max 1000 mg/dosis"
-
- | "paracetamol", "OnceTimed" ->
- "10 mg/ml",
- "paracetamol, [dun] mg, [qty-adj] 20 mg/kg/dosis, [qty] max 1000 mg/dosis"
-
- | "paracetamol", "Discontinuous" when scenario.Route = "RECTAAL" ->
- "120;240;500;1000;125;250;60;30;360;90;750;180 mg/stuk",
- "paracetamol, [dun] mg, [qty-adj] 10 mg/kg - 20 mg/kg/dosis, [qty] max 1000 mg/dosis"
-
- | "paracetamol", ("Timed" | "Discontinuous") ->
- "10 mg/ml",
- "paracetamol, [dun] mg, [per-time-adj] 60 mg/kg/dag, [qty] max 1000 mg/dosis"
-
- | "gentamicine", _ ->
- "1;2;4 mg/ml",
- "gentamicine, [dun] mg, [qty-adj] 5 mg/kg/dosis, [qty] max 500 mg/dosis"
-
- | "propofol", _ ->
- "10 mg/ml",
- "propofol, [dun] mg, [rate-adj] 1 mg/kg/uur - 4 mg/kg/uur"
-
- | "noradrenaline", _ when i = 0 ->
- "1 mg/ml",
- "noradrenaline, [dun] microg, [rate-adj] 0,05 microg/kg/min - 2 microg/kg/min"
-
- | "noradrenaline", _ ->
- // Second component: glucose 10% diluent (no active substance dose)
- "100 mg/ml", ""
-
- | _ ->
- "1 mg/ml", ""
-
- // Component names
- let compName =
- if scenario.Products.Length > 1 && i = 1 then
- "gluc 10%"
- else
- scenario.MedicationName
-
- let quantityStr =
- match product.Unit with
- | "stuk" -> $"1 stuk"
- | "mL" ->
- // Show available container sizes (from known product catalogue)
- match scenario.MedicationName with
- | "paracetamol" -> "50;100 ml"
- | "gentamicine" -> "1 ml"
- | "propofol" -> "50;200 ml"
- | "noradrenaline" when i = 0 -> "5;10;20 ml"
- | _ -> "50 ml"
- | u -> $"1 {u}"
-
- let divisibleStr =
- match product.Unit with
- | "stuk" -> "1"
- | "mL" -> "10"
- | _ -> "1"
-
- let substanceDoseStr =
- if doseStr = "" then "Dose:" else $"Dose: %s{doseStr}"
-
- $"""
-\tName: %s{compName}
-\tForm: %s{scenario.Shape}
-\tQuantities: %s{quantityStr}
-\tDivisible: %s{divisibleStr}
-\tDose:
-\tSolution:
-\tSubstances:
-
-\t\tName: %s{compName}
-\t\tQuantities:
-\t\tConcentrations: %s{concentrationStr}
-\t\t%s{substanceDoseStr}
-\t\tSolution:"""
- )
- |> String.concat "\n"
-
- // Determine the top-level Dose field
- let topDoseStr =
- match scenario.DoseType with
- | "Once" when scenario.Route = "RECTAAL" ->
- "[dun], [qty] 1 stuk/dosis"
- | "OnceTimed" ->
- "[dun], [qty-adj] max 20 ml/kg/dosis, [qty] max 1000 ml/dosis"
- | "Continuous" ->
- ""
- | _ -> ""
-
- $"""Id: %s{id}
-Name: %s{scenario.MedicationName}
-Quantity:
-Quantities:
-Route: %s{scenario.Route}
-OrderType: %s{orderType}
-Adjust: %s{adjustStr}
-Frequencies: %s{frequenciesStr}
-Time: %s{timeStr}
-Dose: %s{topDoseStr}
-Div:
-DoseCount: 1 x
-Components:%s{componentText}"""
-
-
-// Build all scenario texts
-let scenarioMedicationTexts =
- allScenarios
- |> List.map (fun scenario ->
- let guid = Guid.NewGuid().ToString()
- scenario, scenarioToMedicationText guid scenario
- )
-
-
-printfn "\n=== STEP 4: Medication Text Representations ==="
-
-for scenario, text in scenarioMedicationTexts do
- printfn "\n--- Scenario %s: %s ---" scenario.ScenarioId scenario.Description
- printfn "%s" text
+// The concentrations, dose limits, and product structure are NOT in the FHIR
+// scenario — they come from the ZIndex/GenFORM database via OrderContext.getScenarios.
+//
+// Workflow:
+// 1. Build a Patient from the FHIR patient context
+// 2. Call OrderContext.create to get the initial context with available filters
+// 3. Set the filter fields from the FHIR scenario (Indication, Generic, Route, Form, DoseType)
+// 4. Call OrderContext.getScenarios (via UpdateOrderContext + evaluate) to look up matching scenarios
+// 5. Apply the orderable quantities and schedule from the FHIR products + schema blocks
+open Patient.Optics
-// =============================================================================
-// STEP 6: Run each scenario through Medication.fromString
-// =============================================================================
-printfn "\n=== STEP 5-6: Running Scenarios through Medication.fromString ==="
+/// Convert a FhirScenario's patient data into a Patient record
+let buildPatient (scenario: FhirScenario) : Patient =
+ let gender =
+ match scenario.Gender with
+ | "male" -> Male
+ | "female" -> Female
+ | _ -> AnyGender
+
+ patient
+ |> setGender gender
+ |> setWeight (scenario.WeightKg |> Kilogram |> Some)
+ |> setHeight (scenario.HeightCm |> decimal |> int |> Centimeter |> Some)
+
+
+/// Convert the scenario DoseType string to the GenFORM DoseType discriminated union
+let parseDoseType (doseTypeStr: string) =
+ // doseText is left empty here; it is filled in by the dose rule
+ DoseType.fromString doseTypeStr ""
+
-let runScenario (scenario: FhirScenario) (medText: string) =
+/// Print a summary of the OrderScenario context after lookup
+let printOrderScenario (scenario: FhirScenario) (ctx: OrderContext) =
printfn "\n--- Scenario %s: %s ---" scenario.ScenarioId scenario.Description
- printfn "Patient: %M kg, %M cm, %s" scenario.WeightKg scenario.HeightCm scenario.Gender
- printfn "Indication: %s" scenario.Indication
+ printfn " Patient: %g kg, %g cm, %s" scenario.WeightKg scenario.HeightCm scenario.Gender
+ printfn " Indication: %s Route: %s Shape: %s DoseType: %s"
+ scenario.Indication scenario.Route scenario.Shape scenario.DoseType
+
+ printfn " Scenarios found: %i" ctx.Scenarios.Length
+
+ if ctx.Scenarios.Length > 0 then
+ let sc = ctx.Scenarios[0]
+ printfn " First scenario: %s | %s | %s | %A"
+ sc.Name sc.Route sc.Form sc.DoseType
+
+ printfn ""
+ printfn " >>> Prescription (from ZIndex/GenFORM — NOT from FHIR scenario):"
+
+ for line in sc.Prescription |> Array.collect id do
+ let text =
+ match line with
+ | Valid s
+ | Caution s
+ | Warning s
+ | Alert s -> s
+
+ if text |> String.isNullOrWhiteSpace |> not then
+ printfn " %s" text
- match Medication.fromString medText with
- | Error errors ->
- printfn "PARSE ERROR:"
- errors |> List.iter (printfn " - %s")
+ printfn ""
+
+ printfn " >>> Available orderable quantities from the FHIR scenario:"
+ printfn " Admin quantity: %g %s" scenario.AdminQuantity scenario.AdminUnit
+
+ for p in scenario.Products do
+ printfn " Component qty: %g %s (%s)" p.Quantity p.Unit p.Description
+
+ match scenario.Schema.RateQuantity, scenario.Schema.RateFormUnit, scenario.Schema.RateTimeUnit with
+ | Some rate, Some fu, Some tu ->
+ printfn " Rate: %g %s/%s" rate fu tu
+ | _ -> ()
- | Ok med ->
- printfn "Parsed OK: %s (%A)" med.Name med.OrderType
+ match scenario.Schema.Frequency, scenario.Schema.TimeUnit with
+ | Some freq, Some unit ->
+ let period = scenario.Schema.TimePeriod |> Option.defaultValue 1m
+ let freqStr = if period = 1m then $"{freq} x/{unit}" else $"{freq} x/{period} {unit}"
+ printfn " Frequency: %s" freqStr
+ | _ -> ()
- // Convert to order DTO and build the order
- match med |> Medication.toOrderDto |> Order.Dto.fromDto with
- | Error exn ->
- printfn "ORDER BUILD ERROR: %A" exn
+ if scenario.Schema.ExactTimes |> List.isEmpty |> not then
+ printfn " Exact times: %s" (scenario.Schema.ExactTimes |> String.concat ", ")
- | Ok order ->
- printfn "Order built OK."
- // Solve the order to propagate constraints
- let solved =
- order
- |> Order.solveMinMax "ImplementationPlan" true OrderLogging.noOp
+printfn "\n=== STEP 4: Reconstructing OrderScenarios via OrderContext Lookup ==="
+printfn """
+The FHIR scenario provides: patient data, indication, route, shape, dose type.
+These are the filter inputs for OrderContext.create + getScenarios.
+Concentrations and dose limits come from ZIndex/GenFORM — not from the FHIR scenario.
+"""
+
+// A resource provider is required for OrderContext operations.
+// In production, use Api.getCachedProviderWithDataUrlId.
+// Here we use the demo cache that is available without authentication.
+let provider : Resources.IResourceProvider =
+ Api.getCachedProviderWithDataUrlId OrderLogging.noOp (Environment.GetEnvironmentVariable("GENPRES_URL_ID"))
+
+
+/// Reconstruct an OrderContext for a given FHIR scenario by:
+/// 1. Building the Patient from scenario patient data
+/// 2. Creating an initial OrderContext
+/// 3. Setting the filter from the scenario's indication, route, shape, and dose type
+/// 4. Evaluating to trigger the ZIndex/GenFORM lookup
+let reconstructOrderContext (scenario: FhirScenario) : Result =
+ let pat = buildPatient scenario
+ let doseType = parseDoseType scenario.DoseType
+
+ let ctx = OrderContext.create OrderLogging.noOp provider pat
+
+ // Set filter fields from the FHIR scenario context
+ let filter =
+ { ctx.Filter with
+ Indication = Some scenario.Indication
+ Generic = Some scenario.MedicationName
+ Route = Some scenario.Route
+ Form = Some scenario.Shape
+ DoseType = Some doseType
+ }
+
+ { ctx with Filter = filter }
+ |> OrderContext.UpdateOrderContext
+ |> OrderContext.evaluate OrderLogging.noOp provider
+ |> function
+ | Ok cmd -> cmd |> OrderContext.Command.get |> Ok
+ | Error(msg, _) -> Error msg
+
+
+for scenario in allScenarios do
+ match reconstructOrderContext scenario with
+ | Error msg ->
+ printfn "\n--- Scenario %s: %s ---" scenario.ScenarioId scenario.Description
+ printfn " Lookup error: %s" msg
+ | Ok ctx ->
+ printOrderScenario scenario ctx
- match solved with
+
+// =============================================================================
+// STEP 5-6: Run through the order pipeline with scenario quantities
+// =============================================================================
+//
+// Once an OrderScenario has been retrieved via lookup, the orderable quantities,
+// dose rate, and schedule frequency from the FHIR scenario can be applied.
+//
+// These are the ONLY values that come from the FHIR scenario and are directly
+// set on the order:
+// - Component orderable quantities (from products block)
+// - Orderable dose quantity or rate (from administration block / schema rate)
+// - Schedule frequency (from schema pattern)
+//
+// The solver then derives all other values from the ZIndex dose rules.
+
+printfn "\n=== STEP 5-6: Running Scenarios through the Order Pipeline ==="
+
+let runOrderScenario (scenario: FhirScenario) =
+ printfn "\n--- Scenario %s: %s ---" scenario.ScenarioId scenario.Description
+
+ match reconstructOrderContext scenario with
+ | Error msg ->
+ printfn " Context error: %s" msg
+ | Ok ctx ->
+ match ctx.Scenarios |> Array.tryHead with
+ | None ->
+ printfn " No scenarios found for this filter combination."
+ printfn " (Check that indication/route/shape/dose type match the GenFORM database)"
+ | Some sc ->
+ printfn " Scenario: %s | %s | %A" sc.Name sc.Route sc.DoseType
+ printfn " Order type: %A" sc.Order.Schedule
+
+ // Solve the order to propagate all constraints from ZIndex dose rules
+ match sc.Order |> Order.solveMinMax "FHIR-ImplementationPlan" true OrderLogging.noOp with
| Error(_, msg) ->
- printfn "SOLVE WARNING: %s (partial result may still be valid)" msg
- // Print the partial order anyway
- order |> Order.printTable ConsoleTables.Format.Minimal
+ printfn " Solve warning: %s" msg
+ sc.Order |> Order.printTable ConsoleTables.Format.Minimal
| Ok solvedOrder ->
- printfn "Solved OK."
+ printfn " Solved OK."
solvedOrder |> Order.printTable ConsoleTables.Format.Minimal
-for scenario, medText in scenarioMedicationTexts do
- runScenario scenario medText
+for scenario in allScenarios do
+ runOrderScenario scenario
// =============================================================================
@@ -766,66 +738,87 @@ for scenario, medText in scenarioMedicationTexts do
// =============================================================================
printfn "\n=== STEP 7: Summary ==="
-printfn "Processed %i FHIR scenarios from the interface specification." (List.length allScenarios)
+printfn "Processed %i FHIR scenarios." (List.length allScenarios)
printfn ""
-for scenario, medText in scenarioMedicationTexts do
- let result =
- medText
- |> Medication.fromString
- |> Result.bind (fun med ->
- med
- |> Medication.toOrderDto
- |> Order.Dto.fromDto
- |> Result.mapError (fun exn -> [ $"{exn}" ])
- )
-
+for scenario in allScenarios do
let status =
- match result with
- | Ok _ -> "OK"
- | Error errs -> $"ERROR: {errs |> String.concat '; '}"
+ match reconstructOrderContext scenario with
+ | Error msg -> $"CONTEXT ERROR: {msg}"
+ | Ok ctx ->
+ let n = ctx.Scenarios.Length
+
+ if n = 0 then
+ "NO SCENARIOS FOUND"
+ else
+ $"OK — {n} scenario(s) found"
- printfn " Scenario %-6s %-50s %s" scenario.ScenarioId scenario.Description status
+ printfn " Scenario %-6s %-52s %s" scenario.ScenarioId scenario.Description status
// =============================================================================
-// STEP 8: Sketch a FHIR-based translation approach
+// STEP 8: Sketch a FHIR-based solution
// =============================================================================
//
-// This section outlines how the GenPRES Medication model would be represented
-// in FHIR R4 MedicationRequest resources.
+// This section outlines how the full FHIR ↔ GenPRES round-trip would work.
//
-// The FHIR MedicationRequest resource encodes the same information as the
-// Medication text format, but in a structured, interoperable form.
+// Key insight: the FHIR scenario provides the FILTER, not the dosing data.
+// The dosing data (concentrations, dose limits) is returned by GenPRES
+// after lookup against ZIndex/GenFORM rules.
//
-// Key FHIR resources used:
-// - MedicationRequest : the prescription order (one per scenario)
-// - Medication : the medication product identified by GPK code
-// - Dosage : frequency, timing, route, and dose quantity
-// - DosageInstruction : rate for continuous/timed infusion orders
+// ── FHIR → GenPRES (import) ─────────────────────────────────────────────────
//
-// Mapping outline:
+// fromFhirRequest (MedicationRequest → OrderScenario):
//
-// FhirScenario.MedicationName → MedicationRequest.medication.coding (GPK system)
-// FhirScenario.Route → MedicationRequest.dosageInstruction.route (G-Standard thesaurus)
-// FhirScenario.WeightKg → MedicationRequest.dosageInstruction.doseAndRate (weight-based)
-// FhirScenario.Schema.Frequency → MedicationRequest.dosageInstruction.timing.repeat.frequency
-// FhirScenario.Schema.TimeUnit → MedicationRequest.dosageInstruction.timing.repeat.periodUnit
-// FhirScenario.Schema.RateQuantity → MedicationRequest.dosageInstruction.doseAndRate.rateRatio
-// FhirScenario.Schema.ExactTimes → MedicationRequest.dosageInstruction.timing.repeat.timeOfDay
+// FHIR field GenPRES field
+// ────────────────────────────────────────────────────────────────────────
+// Patient weight/height/age/gender → Patient record (for adjust calc)
+// dosageInstruction.route → Filter.Route
+// medication.form / ingredient → Filter.Form
+// dosageInstruction.timing intent → Filter.DoseType
+// medicationCodeableConcept (GPK) → Filter.Generic (via ZIndex lookup)
+// indication (condition reference) → Filter.Indication
//
-// Example FHIR JSON skeleton for scenario 6.3 (Paracetamol 4x/dag rectal):
+// After filter setup → call OrderContext.getScenarios → lookup concentrations
+// and dose limits from ZIndex/GenFORM rules
+//
+// Then apply from the FHIR scenario:
+// products[].quantity + unit → Component Orderable Quantities
+// administration quantity + unit → Orderable Dose Quantity
+// schema rate quantity + units → Orderable Dose Rate (RateFormUnit/RateTimeUnit)
+// schema frequency + period + unit → Schedule Frequency
+//
+// ── GenPRES → FHIR (export) ─────────────────────────────────────────────────
+//
+// toFhirRequest (OrderScenario → MedicationRequest):
+//
+// GenPRES field FHIR field
+// ────────────────────────────────────────────────────────────────────────
+// OrderScenario.Name (GPK lookup) → medicationCodeableConcept.coding[GPK]
+// OrderScenario.Route → dosageInstruction.route (G-Standard thesaurus)
+// OrderScenario.Form → medication.form
+// OrderScenario.DoseType → dosageInstruction.timing intent
+// Patient weight/height → Patient.extension (body weight)
+// Filter.Indication → reasonCode
+// Component orderable quantities → ingredient[].amount
+// Orderable Dose Quantity → dosageInstruction.doseAndRate.doseQuantity
+// Orderable Dose Rate (RateFormUnit/ → dosageInstruction.doseAndRate.rateRatio
+// RateTimeUnit)
+// Schedule Frequency + TimePeriod + → dosageInstruction.timing.repeat
+// TimeUnit
+// Schema.ExactTimes → dosageInstruction.timing.repeat.timeOfDay
+//
+// Example FHIR JSON for scenario 6.3 (Paracetamol 4x/dag rectal):
//
// {
// "resourceType": "MedicationRequest",
-// "id": "",
// "status": "active",
// "intent": "order",
// "medicationCodeableConcept": {
// "coding": [{
-// "system": "urn:oid:2.16.840.1.113883.2.4.4.7", // G-Standard GPK
+// "system": "urn:oid:2.16.840.1.113883.2.4.4.7",
// "code": "",
-// "display": "paracetamol 180 mg/stuk zetpil"
+// "display": "paracetamol zetpil"
// }]
// },
// "subject": { "reference": "Patient/123456" },
@@ -838,57 +831,33 @@ for scenario, medText in scenarioMedicationTexts do
// "doseQuantity": { "value": 1, "unit": "stuk" }
// }],
// "timing": {
-// "repeat": {
-// "frequency": 4,
-// "period": 1,
-// "periodUnit": "d"
-// }
+// "repeat": { "frequency": 4, "period": 1, "periodUnit": "d" }
// }
// }]
// }
//
-// Reverse mapping (FHIR → GenPRES Medication text):
-//
-// The fromFhirRequest function (to be implemented in Informedica.FHIR.Lib) would:
-// 1. Extract the GPK code from medicationCodeableConcept.coding
-// 2. Call ZIndex.GenericProduct.get [gpkCode] to resolve concentration data
-// 3. Map timing.repeat.frequency + periodUnit → Frequencies ValueUnit
-// 4. Map doseAndRate.rateRatio → Rate ValueUnit (for continuous orders)
-// 5. Map subject.weight (from Patient resource) → Adjust ValueUnit
-// 6. Construct a Medication record and call Medication.toOrderDto
-// 7. Return the solved order
-
-printfn "\n=== STEP 8: FHIR-based Translation Approach ==="
+// Next implementation steps for Informedica.FHIR.Lib:
+// 1. Add FHIR R4 serialization dependency (e.g. Hl7.Fhir.R4 NuGet package)
+// 2. Implement fromFhirRequest using the mapping above
+// 3. Implement toFhirRequest using the mapping above
+// 4. Validate round-trip: fromFhirRequest (toFhirRequest scenario) ≈ original scenario
+// 5. Write Expecto tests for each scenario defined in this script
+
+printfn "\n=== STEP 8: FHIR-based Solution Approach ==="
printfn """
-The Informedica.FHIR.Lib library will provide two main functions:
-
- toFhirRequest : Medication → FHIR MedicationRequest JSON
- fromFhirRequest : FHIR MedicationRequest JSON → Result
-
-The mapping between the GenPRES Medication text format and FHIR R4 MedicationRequest
-is defined as follows:
-
- GenPRES field FHIR field
- ─────────────────────────────────────────────────────────────────────────────
- Name medicationCodeableConcept.coding[GPK].display
- Route dosageInstruction.route.coding[G-Standard thesaurus]
- OrderType dosageInstruction.timing + intent
- Adjust (weight) dosageInstruction.doseAndRate.doseQuantity (per kg)
- Frequencies dosageInstruction.timing.repeat.frequency + periodUnit
- Time (infusion) dosageInstruction.timing.repeat.duration + durationUnit
- Component.Concentration Medication.ingredient.strength.numerator
- Substance.Dose[qty-adj] dosageInstruction.doseAndRate.doseRange (weight-based)
- Substance.Dose[rate-adj] dosageInstruction.doseAndRate.rateRatio
- Schema.ExactTimes dosageInstruction.timing.repeat.timeOfDay
-
-For multi-product scenarios (6.6+), each component maps to a separate
-Medication.ingredient entry, with the primary product identified by GPK code and
-diluents identified by their own GPK codes.
-
-Next implementation steps for Informedica.FHIR.Lib:
- 1. Add a FHIR R4 serialization dependency (e.g. Hl7.Fhir.R4 NuGet package)
- 2. Implement toFhirRequest using the mapping table above
- 3. Implement fromFhirRequest using ZIndex GPK lookup + Medication.fromString
- 4. Validate round-trip: fromFhirRequest (toFhirRequest medication) = Ok medication
- 5. Write Expecto tests for each scenario defined in this script
+FHIR → GenPRES (import):
+ 1. Extract patient context → build Patient record
+ 2. Extract indication, route, form, dose type → set Filter
+ 3. Look up GPK code in ZIndex to get Filter.Generic
+ 4. Call OrderContext.create + getScenarios → concentrations/dose limits from ZIndex
+ 5. Apply orderable quantities, rate, and frequency from FHIR scenario data
+
+GenPRES → FHIR (export):
+ 1. Map OrderScenario fields to MedicationRequest resource
+ 2. Map orderable quantities → ingredient amounts
+ 3. Map dose quantity → doseAndRate.doseQuantity
+ 4. Map rate (RateFormUnit/RateTimeUnit) → doseAndRate.rateRatio
+ 5. Map frequency/period → timing.repeat
+
+See the comments in Step 8 above for the full field-level mapping.
"""
From 16657700f5ad293f8c524b7f8d4903711258caa8 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Tue, 24 Mar 2026 11:48:44 +0000
Subject: [PATCH 5/5] feat(fhir): expand implementation plan with FHIR R4
types, bidirectional translation, all spec scenarios
Co-authored-by: halcwb <683631+halcwb@users.noreply.github.com>
Agent-Logs-Url: https://github.com/informedica/GenPRES/sessions/88a3340d-6a54-45d9-b098-a445c37c0329
---
.../Scripts/ImplementationPlan.fsx | 1113 +++++++++++++++--
1 file changed, 1022 insertions(+), 91 deletions(-)
diff --git a/src/Informedica.FHIR.Lib/Scripts/ImplementationPlan.fsx b/src/Informedica.FHIR.Lib/Scripts/ImplementationPlan.fsx
index 9c1183d9..8c7eb8af 100644
--- a/src/Informedica.FHIR.Lib/Scripts/ImplementationPlan.fsx
+++ b/src/Informedica.FHIR.Lib/Scripts/ImplementationPlan.fsx
@@ -44,7 +44,7 @@ open Informedica.GenOrder.Lib
// STEP 1: Define the FHIR scenarios from the specification
// =============================================================================
//
-// Source: docs/mdr/interface/genpres_interface_specification.md, sections 6.1–6.6
+// Source: docs/mdr/interface/genpres_interface_specification.md, sections 6.1–6.11
//
// NOTE: The GPK codes in the specification are PLACEHOLDERS (e.g. "2345678").
// Real codes must come from the G-Standard database. See Step 2 for product lookup.
@@ -387,6 +387,228 @@ let scenario66 =
}
+// --- Scenario 6.7: Multi-Product Once-Timed (Amiodaron + glucose 10% diluent) ---
+let scenario67 =
+ {
+ ScenarioId = "6.7"
+ Description = "Multi-Product OnceTimed – amiodaron with glucose 10% diluent"
+ WeightKg = 11m
+ HeightCm = 79m
+ Gender = "male"
+ Indication = "Ernstige therapieresistente hartritmestoornissen"
+ MedicationName = "amiodaron"
+ Route = "INTRAVENEUS"
+ Shape = "injectievloeistof"
+ DoseType = "OnceTimed"
+ Products =
+ [
+ {
+ GpkPlaceholder = "2345678"
+ Quantity = 6m
+ Unit = "mL"
+ Description = "amiodaron 50 mg/mL injectievloeistof (placeholder GPK)"
+ }
+ {
+ GpkPlaceholder = "3456789"
+ Quantity = 44m
+ Unit = "mL"
+ Description = "glucose 10% vloeistof/diluent (placeholder GPK)"
+ }
+ ]
+ AdminQuantity = 9.1m
+ AdminUnit = "mL"
+ Schema =
+ {
+ Frequency = Some 1
+ TimePeriod = None
+ TimeUnit = None
+ RateQuantity = Some 18m
+ RateFormUnit = Some "mL"
+ RateTimeUnit = Some "uur"
+ ExactTimes = []
+ }
+ }
+
+
+// --- Scenario 6.8: Multi-Product with Reconstitution Once (Adrenaline) ---
+// Products listed are AFTER reconstitution; the reconstitution block is in the YAML.
+let scenario68 =
+ {
+ ScenarioId = "6.8"
+ Description = "Multi-Product Reconstitution Once – adrenaline (reconstituted)"
+ WeightKg = 3.9m
+ HeightCm = 54m
+ Gender = "male"
+ Indication = "Reanimatie"
+ MedicationName = "adrenaline"
+ Route = "INTRAVENEUS"
+ Shape = "injectievloeistof"
+ DoseType = "Once"
+ Products =
+ [
+ {
+ GpkPlaceholder = "2345678"
+ Quantity = 10m
+ Unit = "mL"
+ Description = "adrenaline 0,1 mg/mL oplossing voor infusie (na reconstitutie; placeholder GPK)"
+ }
+ ]
+ AdminQuantity = 0.4m
+ AdminUnit = "mL"
+ Schema =
+ {
+ Frequency = Some 1
+ TimePeriod = None
+ TimeUnit = None
+ RateQuantity = None
+ RateFormUnit = None
+ RateTimeUnit = None
+ ExactTimes = []
+ }
+ }
+
+
+// --- Scenario 6.9: Multi-Product with Reconstitution Timed (Vancomycine + glucose 5%) ---
+let scenario69 =
+ {
+ ScenarioId = "6.9"
+ Description = "Multi-Product Reconstitution Timed – vancomycine 4x/dag"
+ WeightKg = 11m
+ HeightCm = 79m
+ Gender = "male"
+ Indication = "Bacteriële infecties"
+ MedicationName = "vancomycine"
+ Route = "INTRAVENEUS"
+ Shape = "poeder voor oplossing voor infusie"
+ DoseType = "Timed"
+ Products =
+ [
+ {
+ GpkPlaceholder = "2345678"
+ Quantity = 10m
+ Unit = "mL"
+ Description = "vancomycine 50 mg/mL oplossing voor infusie (na reconstitutie; placeholder GPK)"
+ }
+ {
+ GpkPlaceholder = "3456789"
+ Quantity = 40m
+ Unit = "mL"
+ Description = "glucose 5% vloeistof/diluent (placeholder GPK)"
+ }
+ ]
+ AdminQuantity = 14.9m
+ AdminUnit = "mL"
+ Schema =
+ {
+ Frequency = Some 4
+ TimePeriod = Some 1m
+ TimeUnit = Some "dag"
+ RateQuantity = Some 59m
+ RateFormUnit = Some "mL"
+ RateTimeUnit = Some "uur"
+ ExactTimes = []
+ }
+ }
+
+
+// --- Scenario 6.10: Multi-Product TPN (Totale Parenterale Voeding) ---
+let scenario610 =
+ {
+ ScenarioId = "6.10"
+ Description = "Multi-Product TPN – Totale Parenterale Voeding 1x/dag"
+ WeightKg = 11m
+ HeightCm = 79m
+ Gender = "male"
+ Indication = "Standaard Totale Parenterale Voeding"
+ MedicationName = "TPV"
+ Route = "INTRAVENEUS"
+ Shape = "vloeistof voor infusie"
+ DoseType = "Timed"
+ Products =
+ [
+ {
+ GpkPlaceholder = "2345678"
+ Quantity = 105m
+ Unit = "mL"
+ Description = "Samenstelling C vloeistof voor infusie (placeholder GPK)"
+ }
+ {
+ GpkPlaceholder = "3456789"
+ Quantity = 59.5m
+ Unit = "mL"
+ Description = "NaCl 3% vloeistof voor infusie (placeholder GPK)"
+ }
+ {
+ GpkPlaceholder = "4567890"
+ Quantity = 20m
+ Unit = "mL"
+ Description = "KCl 7,4% vloeistof voor infusie (placeholder GPK)"
+ }
+ {
+ GpkPlaceholder = "5678901"
+ Quantity = 715.5m
+ Unit = "mL"
+ Description = "glucose 10% vloeistof voor infusie (placeholder GPK)"
+ }
+ ]
+ AdminQuantity = 900m
+ AdminUnit = "mL"
+ Schema =
+ {
+ Frequency = Some 1
+ TimePeriod = Some 1m
+ TimeUnit = Some "dag"
+ RateQuantity = Some 45m
+ RateFormUnit = Some "mL"
+ RateTimeUnit = Some "uur"
+ ExactTimes = [ "17:00" ]
+ }
+ }
+
+
+// --- Scenario 6.11: Multi-Product Enteral Feeding (MM met BMF) ---
+let scenario611 =
+ {
+ ScenarioId = "6.11"
+ Description = "Multi-Product Enteral – MM met BMF 8x/dag"
+ WeightKg = 3.8m
+ HeightCm = 53m
+ Gender = "female"
+ Indication = "Enterale voeding"
+ MedicationName = "MM met BMF"
+ Route = "ORAAL"
+ Shape = "voeding"
+ DoseType = "Timed"
+ Products =
+ [
+ {
+ GpkPlaceholder = "9999978"
+ Quantity = 20m
+ Unit = "mL"
+ Description = "MM vloeistof voor voeding (placeholder GPK)"
+ }
+ {
+ GpkPlaceholder = "99999789"
+ Quantity = 0.1m
+ Unit = "g"
+ Description = "Nutrilon Nenatal BMF poeder voor voeding (placeholder GPK)"
+ }
+ ]
+ AdminQuantity = 20m
+ AdminUnit = "mL"
+ Schema =
+ {
+ Frequency = Some 8
+ TimePeriod = Some 1m
+ TimeUnit = Some "dag"
+ RateQuantity = None
+ RateFormUnit = None
+ RateTimeUnit = None
+ ExactTimes = [ "07:00"; "10:00"; "13:00"; "16:00"; "19:00"; "22:00"; "01:00"; "04:00" ]
+ }
+ }
+
+
let allScenarios =
[
scenario61
@@ -396,6 +618,11 @@ let allScenarios =
scenario64
scenario65
scenario66
+ scenario67
+ scenario68
+ scenario69
+ scenario610
+ scenario611
]
@@ -757,107 +984,811 @@ for scenario in allScenarios do
// =============================================================================
-// STEP 8: Sketch a FHIR-based solution
+// STEP 8: FHIR R4 Bidirectional Translation Investigation
// =============================================================================
//
-// This section outlines how the full FHIR ↔ GenPRES round-trip would work.
-//
-// Key insight: the FHIR scenario provides the FILTER, not the dosing data.
-// The dosing data (concentrations, dose limits) is returned by GenPRES
-// after lookup against ZIndex/GenFORM rules.
+// Official FHIR R4 documentation references:
+// MedicationRequest https://hl7.org/fhir/R4/medicationrequest.html
+// Medication https://hl7.org/fhir/R4/medication.html
+// Dosage (Dosage) https://hl7.org/fhir/R4/dosage.html
+// Timing https://hl7.org/fhir/R4/datatypes.html#Timing
+// Quantity https://hl7.org/fhir/R4/datatypes.html#Quantity
+// Ratio https://hl7.org/fhir/R4/datatypes.html#Ratio
+// CodeableConcept https://hl7.org/fhir/R4/datatypes.html#CodeableConcept
//
-// ── FHIR → GenPRES (import) ─────────────────────────────────────────────────
+// Dutch G-Standard / NL FHIR:
+// GPK codes OID urn:oid:2.16.840.1.113883.2.4.4.7
+// Route thesaurus 9 OID urn:oid:2.16.840.1.113883.2.4.4.9
+// Form thesaurus 10 OID urn:oid:2.16.840.1.113883.2.4.4.10
+// Medicatie-9 https://informatiestandaarden.nictiz.nl/wiki/Landingspagina_Medicatie
//
-// fromFhirRequest (MedicationRequest → OrderScenario):
+// ── Core design principle ────────────────────────────────────────────────────
//
-// FHIR field GenPRES field
-// ────────────────────────────────────────────────────────────────────────
-// Patient weight/height/age/gender → Patient record (for adjust calc)
-// dosageInstruction.route → Filter.Route
-// medication.form / ingredient → Filter.Form
-// dosageInstruction.timing intent → Filter.DoseType
-// medicationCodeableConcept (GPK) → Filter.Generic (via ZIndex lookup)
-// indication (condition reference) → Filter.Indication
+// FHIR resources describe WHAT was ordered (the outcome of clinical decision-making).
+// GenPRES derives HOW to order by looking up the correct dosing constraints from
+// the ZIndex/GenFORM database. The FHIR resource provides only the filter context
+// (patient + indication + route + form + dose type) and the measured/chosen
+// orderable quantities. Everything else (concentrations, dose limits) comes from
+// ZIndex.
//
-// After filter setup → call OrderContext.getScenarios → lookup concentrations
-// and dose limits from ZIndex/GenFORM rules
+// ── FHIR R4 resource types (mirror of the official data model) ───────────────
//
-// Then apply from the FHIR scenario:
-// products[].quantity + unit → Component Orderable Quantities
-// administration quantity + unit → Orderable Dose Quantity
-// schema rate quantity + units → Orderable Dose Rate (RateFormUnit/RateTimeUnit)
-// schema frequency + period + unit → Schedule Frequency
-//
-// ── GenPRES → FHIR (export) ─────────────────────────────────────────────────
+// The types below model exactly the FHIR R4 fields used in GenPRES integration.
+// They are defined here in the script to document the mapping; the actual
+// Informedica.FHIR.Lib library would use Hl7.Fhir.R4 (NuGet) types instead.
+
+/// A single coding within a CodeableConcept
+type FhirCoding =
+ {
+ // Coding system URI, e.g. "urn:oid:2.16.840.1.113883.2.4.4.7" for GPK
+ System: string
+ // Code value within the system
+ Code: string
+ // Human-readable display text
+ Display: string option
+ }
+
+
+/// A concept represented by one or more codings plus an optional display text
+type FhirCodeableConcept =
+ {
+ Coding: FhirCoding list
+ // Free-text representation (used when no coding is available)
+ Text: string option
+ }
+
+
+/// A measured or measurable quantity with unit
+type FhirQuantity =
+ {
+ Value: decimal
+ // Unit display label
+ Unit: string
+ // Unit system URI, e.g. "http://unitsofmeasure.org" for UCUM
+ System: string option
+ // UCUM code for the unit, e.g. "mL", "mg", "h"
+ Code: string option
+ }
+
+
+/// A ratio of two quantities, used for rates and concentrations
+type FhirRatio =
+ {
+ // Numerator (e.g. 85 mL for a rate of 85 mL/uur)
+ Numerator: FhirQuantity
+ // Denominator (e.g. 1 uur)
+ Denominator: FhirQuantity
+ }
+
+
+/// The repeat pattern within a Timing resource
+/// See https://hl7.org/fhir/R4/datatypes.html#Timing
+type FhirTimingRepeat =
+ {
+ // How many times per period (e.g. 4 for "4 x/dag")
+ Frequency: int option
+ // Length of the period (e.g. 1 for "per dag", 36 for "per 36 uur")
+ Period: decimal option
+ // UCUM-based period unit: s | min | h | d | wk | mo | a
+ PeriodUnit: string option
+ // Duration of each administration for timed infusions (e.g. 15 min)
+ Duration: decimal option
+ // UCUM-based duration unit
+ DurationUnit: string option
+ // Exact clock times for Timed orders, format HH:MM:SS
+ TimeOfDay: string list
+ }
+
+
+/// The Timing datatype: describes when an event is to occur
+type FhirTiming =
+ {
+ // Specific event date/times (for OnceTimed with a fixed start)
+ Event: DateTime list
+ // Repeating pattern
+ Repeat: FhirTimingRepeat option
+ }
+
+
+/// Rate expressed as either a ratio or a simple quantity
+type FhirDosageRate =
+ // Ratio: numerator/denominator, e.g. 85 mL / 1 uur
+ | RateRatio of FhirRatio
+ // SimpleQuantity with UCUM composite unit, e.g. "85 mL/h"
+ | RateQuantity of FhirQuantity
+
+
+/// A dose/rate entry within dosageInstruction.doseAndRate[]
+type FhirDosageAndRate =
+ {
+ // Type of dose entry (e.g. "ordered" vs. "calculated"); optional
+ Type: FhirCodeableConcept option
+ // The dose quantity per administration
+ Dose: FhirQuantity option
+ // The infusion rate
+ Rate: FhirDosageRate option
+ }
+
+
+/// The Dosage datatype: instructions for how medication should be taken/given
+/// See https://hl7.org/fhir/R4/dosage.html
+type FhirDosage =
+ {
+ // Free-text dosage instructions (human-readable summary)
+ Text: string option
+ // Timing of administration
+ Timing: FhirTiming option
+ // Route of administration (G-Standard thesaurus 9)
+ Route: FhirCodeableConcept option
+ // Method of administration
+ Method: FhirCodeableConcept option
+ // Dose quantity and rate
+ DoseAndRate: FhirDosageAndRate list
+ }
+
+
+/// A single ingredient within a Medication resource
+type FhirMedicationIngredient =
+ {
+ // The substance identified by GPK code
+ ItemCodeableConcept: FhirCodeableConcept
+ // Whether this is an active ingredient
+ IsActive: bool option
+ // Concentration: e.g. 10 mg / 1 mL
+ Strength: FhirRatio option
+ }
+
+
+/// The Medication resource: describes the medication product
+/// See https://hl7.org/fhir/R4/medication.html
+type FhirMedication =
+ {
+ ResourceType: string // always "Medication"
+ Id: string option
+ // Product identification by GPK code
+ Code: FhirCodeableConcept
+ // Pharmaceutical form (G-Standard thesaurus 10)
+ Form: FhirCodeableConcept option
+ // Ingredient list (supports multi-ingredient products)
+ Ingredient: FhirMedicationIngredient list
+ }
+
+
+/// A reference to another FHIR resource
+type FhirReference =
+ {
+ // Relative or absolute reference, e.g. "Patient/123456"
+ Reference: string
+ Display: string option
+ }
+
+
+/// The MedicationRequest resource: a prescription or medication order
+/// See https://hl7.org/fhir/R4/medicationrequest.html
+type FhirMedicationRequest =
+ {
+ ResourceType: string // always "MedicationRequest"
+ Id: string option
+ // Status: active | draft | on-hold | cancelled | completed | ...
+ Status: string
+ // Intent: proposal | plan | order | original-order | ...
+ Intent: string
+ // Identified medication by GPK code (use medicationCodeableConcept for GPK)
+ MedicationCodeableConcept: FhirCodeableConcept
+ // The patient
+ Subject: FhirReference
+ // When the prescription was written
+ AuthoredOn: DateTime option
+ // Clinical indication (ICD-10 or free text)
+ ReasonCode: FhirCodeableConcept list
+ // Free-text notes
+ Note: string list
+ // Dosage instructions
+ DosageInstruction: FhirDosage list
+ // How much to dispense
+ DispenseRequest: {| Quantity: FhirQuantity option |} option
+ // Contained Medication resources (for inline ingredient detail)
+ Contained: FhirMedication list
+ }
+
+
+// ── G-Standard and FHIR system constants ─────────────────────────────────────
//
-// toFhirRequest (OrderScenario → MedicationRequest):
+// These OIDs and system URIs are used to identify coding systems in FHIR resources.
+
+/// G-Standard and FHIR coding system constants
+module FhirSystems =
+
+ /// OID for the Dutch G-Standard GPK product table
+ let gpk = "urn:oid:2.16.840.1.113883.2.4.4.7"
+
+ /// OID for the G-Standard route thesaurus (Thesaurus 9)
+ let route = "urn:oid:2.16.840.1.113883.2.4.4.9"
+
+ /// OID for the G-Standard pharmaceutical form thesaurus (Thesaurus 10)
+ let form = "urn:oid:2.16.840.1.113883.2.4.4.10"
+
+ /// UCUM unit system URI
+ let ucum = "http://unitsofmeasure.org"
+
+ /// SNOMED CT system URI
+ let snomed = "http://snomed.info/sct"
+
+
+/// G-Standard route code → GenPRES route name and vice versa
+module RouteMapping =
+
+ // G-Standard Thesaurus 9 route codes
+ let codeToName =
+ Map.ofList
+ [
+ "2", "INTRAVENEUS"
+ "9", "ORAAL"
+ "12", "RECTAAL"
+ "14", "SUBCUTAAN"
+ "15", "INTRAMUSCULAIR"
+ "46", "INHALATIE"
+ ]
+
+ let nameToCode = codeToName |> Map.toList |> List.map (fun (k, v) -> v, k) |> Map.ofList
+
+ let toCode name =
+ nameToCode |> Map.tryFind name |> Option.defaultValue ""
+
+ let toName code =
+ codeToName |> Map.tryFind code |> Option.defaultValue ""
+
+
+/// FHIR timing period unit (UCUM-based) ↔ GenPRES time unit
+module PeriodUnitMapping =
+
+ let fhirToGenPres =
+ Map.ofList
+ [
+ "s", "seconde"
+ "min", "minuut"
+ "h", "uur"
+ "d", "dag"
+ "wk", "week"
+ "mo", "maand"
+ "a", "jaar"
+ ]
+
+ let genPresToFhir = fhirToGenPres |> Map.toList |> List.map (fun (k, v) -> v, k) |> Map.ofList
+
+ let toGenPres unit =
+ fhirToGenPres |> Map.tryFind unit |> Option.defaultValue unit
+
+ let toFhir unit =
+ genPresToFhir |> Map.tryFind unit |> Option.defaultValue unit
+
+
+// ── FHIR → GenPRES translation ────────────────────────────────────────────────
//
-// GenPRES field FHIR field
-// ────────────────────────────────────────────────────────────────────────
-// OrderScenario.Name (GPK lookup) → medicationCodeableConcept.coding[GPK]
-// OrderScenario.Route → dosageInstruction.route (G-Standard thesaurus)
-// OrderScenario.Form → medication.form
-// OrderScenario.DoseType → dosageInstruction.timing intent
-// Patient weight/height → Patient.extension (body weight)
-// Filter.Indication → reasonCode
-// Component orderable quantities → ingredient[].amount
-// Orderable Dose Quantity → dosageInstruction.doseAndRate.doseQuantity
-// Orderable Dose Rate (RateFormUnit/ → dosageInstruction.doseAndRate.rateRatio
-// RateTimeUnit)
-// Schedule Frequency + TimePeriod + → dosageInstruction.timing.repeat
-// TimeUnit
-// Schema.ExactTimes → dosageInstruction.timing.repeat.timeOfDay
+// fromFhirMedicationRequest: converts a FhirMedicationRequest resource into a
+// FhirScenario that can drive the GenPRES OrderContext lookup.
//
-// Example FHIR JSON for scenario 6.3 (Paracetamol 4x/dag rectal):
+// Translation logic:
+// - Patient context comes from the FHIR Patient resource (passed as parameters here)
+// - Indication: MedicationRequest.reasonCode[0].text
+// - Generic name: resolved from GPK code via ZIndex.GenericProduct.get [gpkCode]
+// - Route: MedicationRequest.dosageInstruction[0].route.text (or look up from coding)
+// - Shape: Medication.form.text (from contained Medication resource)
+// - DoseType: inferred from timing and rate structure (see below)
+// - Products: from Medication.ingredient[] + MedicationRequest quantities
+// - AdminQuantity: dosageInstruction[0].doseAndRate[0].doseQuantity
+// - Rate: dosageInstruction[0].doseAndRate[0].rateRatio (RateFormUnit/RateTimeUnit)
+// - Frequency: dosageInstruction[0].timing.repeat (frequency, period, periodUnit)
+// - ExactTimes: dosageInstruction[0].timing.repeat.timeOfDay
+
+/// Infer the GenPRES DoseType string from the structure of a FHIR Dosage
+let inferDoseType (dosage: FhirDosage) : string =
+ let hasRate = dosage.DoseAndRate |> List.exists (fun dr -> dr.Rate.IsSome)
+
+ match dosage.Timing with
+ | None -> if hasRate then "Continuous" else "Once"
+ | Some timing ->
+ match timing.Repeat with
+ | None -> if hasRate then "Continuous" else "Once"
+ | Some r ->
+ match r.Frequency, r.Duration with
+ // No frequency → continuous
+ | None, _ -> if hasRate then "Continuous" else "Once"
+ // One-time with duration or rate → OnceTimed
+ | Some 1, Some _ -> "OnceTimed"
+ | Some 1, None when hasRate -> "OnceTimed"
+ // One-time, no rate → Once
+ | Some 1, None -> "Once"
+ // Multiple per period with rate → Timed
+ | Some _, _ when hasRate -> "Timed"
+ // Multiple per period, no rate → Discontinuous
+ | Some _, _ -> "Discontinuous"
+
+
+/// Extract schedule from a FHIR Dosage
+let extractSchema (dosage: FhirDosage) : AdministrationSchema =
+ let rateQty, rateFormUnit, rateTimeUnit =
+ dosage.DoseAndRate
+ |> List.tryHead
+ |> Option.bind _.Rate
+ |> Option.map
+ (function
+ | RateRatio r -> Some r.Numerator.Value, Some r.Numerator.Unit, Some r.Denominator.Unit
+ | RateQuantity q ->
+ // Parse UCUM composite unit like "mL/h"
+ let parts =
+ (q.Code |> Option.defaultValue q.Unit).Split('/')
+
+ match parts with
+ | [| num; den |] -> Some q.Value, Some num, Some(PeriodUnitMapping.toGenPres den)
+ | _ -> Some q.Value, Some q.Unit, None)
+ |> Option.defaultValue (None, None, None)
+
+ let frequency, timePeriod, timeUnit, exactTimes =
+ dosage.Timing
+ |> Option.map (fun t ->
+ let r = t.Repeat
+
+ let freq = r |> Option.bind _.Frequency
+ let period = r |> Option.bind _.Period
+
+ let unit =
+ r
+ |> Option.bind _.PeriodUnit
+ |> Option.map PeriodUnitMapping.toGenPres
+
+ let times = r |> Option.map _.TimeOfDay |> Option.defaultValue []
+ freq, period, unit, times)
+ |> Option.defaultValue (None, None, None, [])
+
+ {
+ Frequency = frequency
+ TimePeriod = timePeriod
+ TimeUnit = timeUnit
+ RateQuantity = rateQty
+ RateFormUnit = rateFormUnit
+ RateTimeUnit = rateTimeUnit
+ ExactTimes = exactTimes
+ }
+
+
+/// Convert a FHIR MedicationRequest + patient parameters into a FhirScenario.
+/// The FhirScenario can then drive GenPRES OrderContext lookup (see Step 4).
+let fromFhirMedicationRequest
+ (weightKg: decimal)
+ (heightCm: decimal)
+ (gender: string)
+ (req: FhirMedicationRequest)
+ : FhirScenario =
+
+ // --- Indication (reasonCode.text) ---
+ let indication =
+ req.ReasonCode
+ |> List.tryHead
+ |> Option.bind _.Text
+ |> Option.defaultValue ""
+
+ // --- GPK code → generic name via ZIndex ---
+ let gpkCode =
+ req.MedicationCodeableConcept.Coding
+ |> List.tryFind (fun c -> c.System = FhirSystems.gpk)
+ |> Option.map _.Code
+ |> Option.defaultValue ""
+
+ let medicationName =
+ if gpkCode |> String.isNullOrWhiteSpace then
+ req.MedicationCodeableConcept.Text |> Option.defaultValue ""
+ else
+ // Try to parse GPK code as int and look up via ZIndex
+ match gpkCode |> System.Int32.TryParse with
+ | true, gpkInt ->
+ match GenericProduct.get [ gpkInt ] |> Array.tryHead with
+ | Some gp -> gp.Name
+ | None -> req.MedicationCodeableConcept.Text |> Option.defaultValue gpkCode
+ | false, _ -> req.MedicationCodeableConcept.Text |> Option.defaultValue gpkCode
+
+ // --- Route (dosageInstruction.route) ---
+ let route =
+ req.DosageInstruction
+ |> List.tryHead
+ |> Option.bind _.Route
+ |> Option.map (fun cc ->
+ match cc.Text with
+ | Some t -> t
+ | None ->
+ cc.Coding
+ |> List.tryFind (fun c -> c.System = FhirSystems.route)
+ |> Option.map (fun c -> RouteMapping.toName c.Code)
+ |> Option.defaultValue "")
+ |> Option.defaultValue ""
+
+ // --- Shape (Medication.form from contained resource) ---
+ let shape =
+ req.Contained
+ |> List.tryHead
+ |> Option.bind _.Form
+ |> Option.bind _.Text
+ |> Option.defaultValue ""
+
+ // --- DoseType (inferred from dosage timing/rate structure) ---
+ let doseType =
+ req.DosageInstruction
+ |> List.tryHead
+ |> Option.map inferDoseType
+ |> Option.defaultValue "Once"
+
+ // --- Admin quantity (dosageInstruction.doseAndRate.doseQuantity) ---
+ let adminQty, adminUnit =
+ req.DosageInstruction
+ |> List.tryHead
+ |> Option.bind (fun d -> d.DoseAndRate |> List.tryHead)
+ |> Option.bind _.Dose
+ |> Option.map (fun q -> q.Value, q.Unit)
+ |> Option.defaultValue (0m, "")
+
+ // --- Schema (timing + rate) ---
+ let schema =
+ req.DosageInstruction
+ |> List.tryHead
+ |> Option.map extractSchema
+ |> Option.defaultValue
+ {
+ Frequency = None
+ TimePeriod = None
+ TimeUnit = None
+ RateQuantity = None
+ RateFormUnit = None
+ RateTimeUnit = None
+ ExactTimes = []
+ }
+
+ // --- Products (from contained Medication.ingredient[]) ---
+ // NOTE: In a real implementation the admin quantity would be distributed
+ // across ingredients according to their proportions.
+ let products =
+ req.Contained
+ |> List.collect (fun med ->
+ med.Ingredient
+ |> List.map (fun ing ->
+ let gpk =
+ ing.ItemCodeableConcept.Coding
+ |> List.tryFind (fun c -> c.System = FhirSystems.gpk)
+ |> Option.map _.Code
+ |> Option.defaultValue ""
+
+ let display =
+ ing.ItemCodeableConcept.Coding
+ |> List.tryHead
+ |> Option.bind _.Display
+ |> Option.defaultValue (ing.ItemCodeableConcept.Text |> Option.defaultValue "")
+
+ {
+ GpkPlaceholder = gpk
+ Quantity = adminQty
+ Unit = adminUnit
+ Description = display
+ }))
+
+ {
+ ScenarioId = req.Id |> Option.defaultValue ""
+ Description = req.Note |> List.tryHead |> Option.defaultValue ""
+ WeightKg = weightKg
+ HeightCm = heightCm
+ Gender = gender
+ Indication = indication
+ MedicationName = medicationName
+ Route = route
+ Shape = shape
+ DoseType = doseType
+ Products = products
+ AdminQuantity = adminQty
+ AdminUnit = adminUnit
+ Schema = schema
+ }
+
+
+// ── GenPRES → FHIR translation ────────────────────────────────────────────────
//
-// {
-// "resourceType": "MedicationRequest",
-// "status": "active",
-// "intent": "order",
-// "medicationCodeableConcept": {
-// "coding": [{
-// "system": "urn:oid:2.16.840.1.113883.2.4.4.7",
-// "code": "",
-// "display": "paracetamol zetpil"
-// }]
-// },
-// "subject": { "reference": "Patient/123456" },
-// "dosageInstruction": [{
-// "route": {
-// "coding": [{ "system": "urn:oid:2.16.840.1.113883.2.4.4.9", "code": "12" }],
-// "text": "RECTAAL"
-// },
-// "doseAndRate": [{
-// "doseQuantity": { "value": 1, "unit": "stuk" }
-// }],
-// "timing": {
-// "repeat": { "frequency": 4, "period": 1, "periodUnit": "d" }
-// }
-// }]
-// }
+// toFhirMedicationRequest: converts a GenPRES FhirScenario (populated after
+// OrderContext lookup and order pipeline) into a FHIR R4 MedicationRequest.
//
-// Next implementation steps for Informedica.FHIR.Lib:
-// 1. Add FHIR R4 serialization dependency (e.g. Hl7.Fhir.R4 NuGet package)
-// 2. Implement fromFhirRequest using the mapping above
-// 3. Implement toFhirRequest using the mapping above
-// 4. Validate round-trip: fromFhirRequest (toFhirRequest scenario) ≈ original scenario
-// 5. Write Expecto tests for each scenario defined in this script
-
-printfn "\n=== STEP 8: FHIR-based Solution Approach ==="
+// Translation logic:
+// - MedicationRequest.status: "active"
+// - MedicationRequest.intent: "order"
+// - MedicationRequest.medicationCodeableConcept: GPK code from ZIndex lookup
+// - MedicationRequest.reasonCode: scenario.Indication
+// - DosageInstruction.route: RouteMapping.toCode scenario.Route
+// - DosageInstruction.timing.repeat: Frequency / TimePeriod / TimeUnit / ExactTimes
+// - DosageInstruction.doseAndRate.doseQuantity: AdminQuantity + AdminUnit
+// - DosageInstruction.doseAndRate.rateRatio: RateFormUnit / RateTimeUnit / RateQuantity
+// - Contained Medication.form: scenario.Shape
+// - Contained Medication.ingredient: one entry per product
+
+/// Build a FHIR Timing resource from an AdministrationSchema
+let toFhirTiming (schema: AdministrationSchema) : FhirTiming option =
+ let repeat =
+ match schema.Frequency, schema.TimePeriod, schema.TimeUnit with
+ | None, None, None when schema.ExactTimes |> List.isEmpty -> None
+ | _ ->
+ Some
+ {
+ Frequency = schema.Frequency
+ Period = schema.TimePeriod
+ PeriodUnit = schema.TimeUnit |> Option.map PeriodUnitMapping.toFhir
+ Duration = None
+ DurationUnit = None
+ TimeOfDay = schema.ExactTimes
+ }
+
+ if repeat.IsSome || schema.ExactTimes |> List.isEmpty |> not then
+ Some { Event = []; Repeat = repeat }
+ else
+ None
+
+
+/// Build a FHIR DosageAndRate from an AdministrationSchema + admin quantity
+let toFhirDosageAndRate (adminQty: decimal) (adminUnit: string) (schema: AdministrationSchema) : FhirDosageAndRate list =
+ let dose =
+ if adminQty > 0m then
+ Some
+ {
+ Value = adminQty
+ Unit = adminUnit
+ System = Some FhirSystems.ucum
+ Code = Some adminUnit
+ }
+ else
+ None
+
+ let rate =
+ match schema.RateQuantity, schema.RateFormUnit, schema.RateTimeUnit with
+ | Some qty, Some fu, Some tu ->
+ Some(
+ RateRatio
+ {
+ Numerator =
+ {
+ Value = qty
+ Unit = fu
+ System = Some FhirSystems.ucum
+ Code = Some fu
+ }
+ Denominator =
+ {
+ Value = 1m
+ Unit = tu
+ System = Some FhirSystems.ucum
+ Code = Some(PeriodUnitMapping.toFhir tu)
+ }
+ }
+ )
+ | _ -> None
+
+ [ { Type = None; Dose = dose; Rate = rate } ]
+
+
+/// Convert a GenPRES FhirScenario into a FHIR R4 MedicationRequest.
+/// GPK code must be resolved beforehand (real GPK, not placeholder).
+let toFhirMedicationRequest
+ (resolvedGpkCode: string)
+ (patientRef: string)
+ (scenario: FhirScenario)
+ : FhirMedicationRequest =
+
+ let routeCode = RouteMapping.toCode scenario.Route
+
+ let dosage =
+ {
+ Text = None
+ Timing = toFhirTiming scenario.Schema
+ Route =
+ Some
+ {
+ Coding =
+ [
+ {
+ System = FhirSystems.route
+ Code = routeCode
+ Display = Some scenario.Route
+ }
+ ]
+ Text = Some scenario.Route
+ }
+ Method = None
+ DoseAndRate = toFhirDosageAndRate scenario.AdminQuantity scenario.AdminUnit scenario.Schema
+ }
+
+ let medicationIngredients =
+ scenario.Products
+ |> List.map (fun p ->
+ {
+ ItemCodeableConcept =
+ {
+ Coding =
+ [
+ {
+ System = FhirSystems.gpk
+ Code = p.GpkPlaceholder
+ Display = Some p.Description
+ }
+ ]
+ Text = Some p.Description
+ }
+ IsActive = Some true
+ // Strength would be populated from ZIndex lookup
+ Strength = None
+ })
+
+ let containedMedication =
+ {
+ ResourceType = "Medication"
+ Id = Some $"med-{scenario.ScenarioId}"
+ Code =
+ {
+ Coding =
+ [
+ {
+ System = FhirSystems.gpk
+ Code = resolvedGpkCode
+ Display = Some scenario.MedicationName
+ }
+ ]
+ Text = Some scenario.MedicationName
+ }
+ Form =
+ Some
+ {
+ Coding = []
+ Text = Some scenario.Shape
+ }
+ Ingredient = medicationIngredients
+ }
+
+ {
+ ResourceType = "MedicationRequest"
+ Id = Some $"req-{scenario.ScenarioId}"
+ Status = "active"
+ Intent = "order"
+ MedicationCodeableConcept =
+ {
+ Coding =
+ [
+ {
+ System = FhirSystems.gpk
+ Code = resolvedGpkCode
+ Display = Some scenario.MedicationName
+ }
+ ]
+ Text = Some scenario.MedicationName
+ }
+ Subject = { Reference = patientRef; Display = None }
+ AuthoredOn = Some DateTime.UtcNow
+ ReasonCode =
+ [
+ {
+ Coding = []
+ Text = Some scenario.Indication
+ }
+ ]
+ Note = [ scenario.Description ]
+ DosageInstruction = [ dosage ]
+ DispenseRequest = None
+ Contained = [ containedMedication ]
+ }
+
+
+// ── Demonstrate round-trip for each scenario ──────────────────────────────────
+
+printfn "\n=== STEP 8: FHIR R4 Bidirectional Translation ==="
printfn """
-FHIR → GenPRES (import):
- 1. Extract patient context → build Patient record
- 2. Extract indication, route, form, dose type → set Filter
- 3. Look up GPK code in ZIndex to get Filter.Generic
- 4. Call OrderContext.create + getScenarios → concentrations/dose limits from ZIndex
- 5. Apply orderable quantities, rate, and frequency from FHIR scenario data
-
-GenPRES → FHIR (export):
- 1. Map OrderScenario fields to MedicationRequest resource
- 2. Map orderable quantities → ingredient amounts
- 3. Map dose quantity → doseAndRate.doseQuantity
- 4. Map rate (RateFormUnit/RateTimeUnit) → doseAndRate.rateRatio
- 5. Map frequency/period → timing.repeat
-
-See the comments in Step 8 above for the full field-level mapping.
+Official FHIR R4 resource docs:
+ MedicationRequest https://hl7.org/fhir/R4/medicationrequest.html
+ Medication https://hl7.org/fhir/R4/medication.html
+ Dosage https://hl7.org/fhir/R4/dosage.html
+
+Translation direction:
+ A) FHIR MedicationRequest → FhirScenario (via fromFhirMedicationRequest)
+ → set Filter on OrderContext → call getScenarios → look up ZIndex rules
+ → apply orderable quantities from FHIR scenario → run order pipeline
+ B) GenPRES OrderScenario → FHIR MedicationRequest (via toFhirMedicationRequest)
+ → serialize to JSON for EHR / medication administration system
+
+Key insight: FHIR provides the filter context and orderable quantities only.
+Concentrations and dose limits are always derived from ZIndex/GenFORM, never
+stored in the FHIR resource.
+"""
+
+printfn "--- Round-trip demonstration (scenario → FHIR → scenario) ---"
+
+for scenario in allScenarios do
+ printfn ""
+ printfn " Scenario %-7s %s" scenario.ScenarioId scenario.Description
+
+ // A) GenPRES → FHIR
+ let gpkPlaceholder =
+ scenario.Products
+ |> List.tryHead
+ |> Option.map _.GpkPlaceholder
+ |> Option.defaultValue ""
+
+ let fhirReq = toFhirMedicationRequest gpkPlaceholder "Patient/DEMO" scenario
+
+ printfn " → FHIR MedicationRequest: id=%A status=%s intent=%s"
+ fhirReq.Id
+ fhirReq.Status
+ fhirReq.Intent
+
+ printfn " medication: %s [GPK: %s]"
+ (fhirReq.MedicationCodeableConcept.Text |> Option.defaultValue "")
+ (fhirReq.MedicationCodeableConcept.Coding |> List.tryHead |> Option.map _.Code |> Option.defaultValue "")
+
+ printfn " indication: %s"
+ (fhirReq.ReasonCode |> List.tryHead |> Option.bind _.Text |> Option.defaultValue "")
+
+ printfn " route: %s"
+ (fhirReq.DosageInstruction |> List.tryHead |> Option.bind _.Route |> Option.bind _.Text |> Option.defaultValue "")
+
+ let dosageText =
+ fhirReq.DosageInstruction
+ |> List.tryHead
+ |> Option.map (fun d ->
+ let doseStr =
+ d.DoseAndRate
+ |> List.tryHead
+ |> Option.bind _.Dose
+ |> Option.map (fun q -> $"{q.Value} {q.Unit}")
+ |> Option.defaultValue "(no dose)"
+
+ let rateStr =
+ d.DoseAndRate
+ |> List.tryHead
+ |> Option.bind _.Rate
+ |> Option.map (function
+ | RateRatio r -> $"{r.Numerator.Value} {r.Numerator.Unit}/{r.Denominator.Unit}"
+ | RateQuantity q -> $"{q.Value} {q.Unit}")
+ |> Option.defaultValue "(no rate)"
+
+ let freqStr =
+ d.Timing
+ |> Option.bind _.Repeat
+ |> Option.map (fun r ->
+ match r.Frequency, r.PeriodUnit with
+ | Some f, Some u -> $"{f} x/{PeriodUnitMapping.toGenPres u}"
+ | _ -> "(continuous)")
+ |> Option.defaultValue "(no schedule)"
+
+ $"dose={doseStr} rate={rateStr} freq={freqStr}")
+ |> Option.defaultValue "(no dosage)"
+
+ printfn " dosage: %s" dosageText
+
+ // B) FHIR → FhirScenario (round-trip)
+ let roundTripped =
+ fromFhirMedicationRequest scenario.WeightKg scenario.HeightCm scenario.Gender fhirReq
+
+ let routeMatch = roundTripped.Route = scenario.Route
+ let doseTypeMatch = roundTripped.DoseType = scenario.DoseType
+ let indicationMatch = roundTripped.Indication = scenario.Indication
+
+ printfn " ← Round-trip: Route=%s DoseType=%s Indication=%s"
+ (if routeMatch then "✓" else $"✗ got '{roundTripped.Route}'")
+ (if doseTypeMatch then "✓" else $"✗ got '{roundTripped.DoseType}'")
+ (if indicationMatch then "✓" else $"✗ got '{roundTripped.Indication}'")
+
+
+// ── Next implementation steps ─────────────────────────────────────────────────
+
+printfn """
+
+=== Next steps for Informedica.FHIR.Lib ===
+ 1. Add Hl7.Fhir.R4 NuGet package (paket: 'nuget Hl7.Fhir.R4')
+ 2. Replace the script FhirMedicationRequest type with the Hl7.Fhir.R4 model type
+ 3. Implement fromFhirMedicationRequest using the FhirScenario approach:
+ MedicationRequest resource → FhirScenario → OrderContext filter → getScenarios
+ 4. Implement toFhirMedicationRequest:
+ OrderScenario + orderable quantities → MedicationRequest resource
+ 5. Add JSON serialization using Hl7.Fhir.Serialization.FhirJsonParser
+ 6. Write Expecto tests for each scenario defined in this script
+ 7. Validate round-trip: fromFhirMedicationRequest (toFhirMedicationRequest scenario)
+ preserves Route, DoseType, Indication, AdminQuantity, Rate, Frequency
"""