SGHI FHIR Profile Implementation Guide
0.1.0 - ci-build
SGHI FHIR Profile Implementation Guide, published by Kathurima Kimathi. This guide is not an authorized publication; it is the continuous build for version 0.1.0 built by the FHIR (HL7® FHIR® Standard) CI Build. This version is based on the current content of https://github.com/savannahghi/sil_fhir_profile_ig/ and changes regularly. See the Directory of published versions
| Official URL: https://fhir.slade360.co.ke/fhir/StructureMap/ExtractChemotherapyPrescription | Version: 0.1.0 | |||
| Active as of 2026-09-14 | Computable Name: ExtractChemotherapyPrescription | |||
One cycle out as: a MedicationRequest per cytotoxic drug and per named premedication, each carrying the drug as written because the hospital's formulary is not coded and a guessed code on a cytotoxic is a dosing risk; a ServiceRequest per investigation the sheet makes mandatory before every cycle, so the requirement becomes an order the laboratory sees rather than a reminder on paper; the height, this cycle's weight and the body surface area derived from them as Observations, because every dose on the sheet is calculated from that BSA and it has to be auditable; and the pre- and post-chemotherapy hydration as MedicationRequests of their own.
/// url = 'https://fhir.slade360.co.ke/fhir/StructureMap/ExtractChemotherapyPrescription' /// name = 'ExtractChemotherapyPrescription' /// title = 'Chemotherapy Prescription Extraction' /// status = 'active' /// description = 'One cycle out as: a MedicationRequest per cytotoxic drug and per named premedication, each carrying the drug as written because the hospital\'s formulary is not coded and a guessed code on a cytotoxic is a dosing risk; a ServiceRequest per investigation the sheet makes mandatory before every cycle, so the requirement becomes an order the laboratory sees rather than a reminder on paper; the height, this cycle\'s weight and the body surface area derived from them as Observations, because every dose on the sheet is calculated from that BSA and it has to be auditable; and the pre- and post-chemotherapy hydration as MedicationRequests of their own.' uses "http://hl7.org/fhir/StructureDefinition/QuestionnaireResponse" alias QR as source uses "http://hl7.org/fhir/StructureDefinition/Bundle" alias Bundle as target uses "http://hl7.org/fhir/StructureDefinition/Observation" alias Observation as target uses "http://hl7.org/fhir/StructureDefinition/MedicationRequest" alias MedicationRequest as target uses "http://hl7.org/fhir/StructureDefinition/ServiceRequest" alias ServiceRequest as target group ExtractChemotherapyPrescription(source qr : QR, target bundle : Bundle) { qr -> bundle.type = 'transaction' "setBundleType"; // ── The measurements every dose derives from ──────────────────────────── // Height, this cycle's weight, and the body surface area between them. The // BSA is extracted only when the client calculated it: the questionnaire // leaves the field read-only with a Mosteller calculatedExpression, and where // no engine ran it there is nothing to record. Recomputing it here would be // worse than leaving it out -- a BSA on the record that the prescriber never // saw is a number nobody checked the doses against. qr.item as g where (linkId = 'measurements') then { g.item as it where ((linkId = 'measurements/height') and answer.value.exists()) -> bundle.entry as entry, entry.resource = create('Observation') as obs then { qr -> obs, entry then BuildObsBase(qr, obs, entry) "base"; qr -> obs then SetVitalSignsCategory(qr, obs) "category"; it -> obs.code as cc then { it -> cc.coding as cg then { it -> cg.system = 'http://loinc.org' "sys"; it -> cg.code = '8302-2' "cd"; it -> cg.display = 'Body height' "dsp"; } "coding"; } "code"; it.answer first as a then { a.value as v -> obs.value = v "set"; }; } "measure_8302_2"; g.item as it where ((linkId = 'measurements/current-weight') and answer.value.exists()) -> bundle.entry as entry, entry.resource = create('Observation') as obs then { qr -> obs, entry then BuildObsBase(qr, obs, entry) "base"; qr -> obs then SetVitalSignsCategory(qr, obs) "category"; it -> obs.code as cc then { it -> cc.coding as cg then { it -> cg.system = 'http://loinc.org' "sys"; it -> cg.code = '29463-7' "cd"; it -> cg.display = 'Body weight' "dsp"; } "coding"; } "code"; it.answer first as a then { a.value as v -> obs.value = v "set"; }; it -> obs.note as n, n.text = 'The weight this cycle was dosed on.' "note"; } "measure_29463_7"; g.item as it where ((linkId = 'measurements/initial-weight') and answer.value.exists()) -> bundle.entry as entry, entry.resource = create('Observation') as obs then { qr -> obs, entry then BuildObsBase(qr, obs, entry) "base"; qr -> obs then SetVitalSignsCategory(qr, obs) "category"; it -> obs.code as cc then { it -> cc.coding as cg then { it -> cg.system = 'http://loinc.org' "sys"; it -> cg.code = '29463-7' "cd"; it -> cg.display = 'Body weight' "dsp"; } "coding"; } "code"; it.answer first as a then { a.value as v -> obs.value = v "set"; }; it -> obs.note as n, n.text = 'The weight at the start of treatment, for comparison.' "note"; } "measure_29463_72"; g.item as it where ((linkId = 'measurements/body-surface-area') and answer.value.exists()) -> bundle.entry as entry, entry.resource = create('Observation') as obs then { qr -> obs, entry then BuildObsBase(qr, obs, entry) "base"; qr -> obs then SetSurveyCategory(qr, obs) "category"; it -> obs.code as cc then { it -> cc.coding as cg then { it -> cg.system = 'http://loinc.org' "sys"; it -> cg.code = '8277-6' "cd"; it -> cg.display = 'Body surface area' "dsp"; } "coding"; } "code"; it.answer first as a then { a.value as v -> obs.value = create('Quantity') as q then { v -> q.value = v "qValue"; v -> q.unit = 'm2' "qUnit"; v -> q.system = 'http://unitsofmeasure.org' "qSystem"; v -> q.code = 'm2' "qCode"; } "quantity"; }; it -> obs.note as n, n.text = 'Calculated by the Mosteller formula from the height and this cycle\'s weight. Every cytotoxic dose on the prescription derives from it. The unit is attached by the extraction map.' "note"; } "measureq_8277_6"; } "measurementsGroup"; // ── Staging and line of treatment ─────────────────────────────────────── qr.item as g where (linkId = 'context') then { g.item as it where ((linkId = 'context/line-of-treatment') and answer.value.exists()) -> bundle.entry as entry, entry.resource = create('Observation') as obs then { qr -> obs, entry then BuildObsBase(qr, obs, entry) "base"; qr -> obs then SetSurveyCategory(qr, obs) "category"; it -> obs.code as cc then { it -> cc.coding as cg then { it -> cg.system = 'https://fhir.slade360.co.ke/fhir/CodeSystem/concept-codesystem' "sys"; it -> cg.code = 'treatment-line' "cd"; it -> cg.display = 'Line of treatment' "dsp"; } "coding"; } "code"; it.answer first as a then { a.value as cv -> obs.value = create('CodeableConcept') as cc then { cv -> cc.coding = cv "valueCoding"; cv.display as d -> cc.text = d "valueText"; } "codedValue"; }; } "coded_treatment_line"; g.item as it where ((linkId = 'context/cycle-number') and answer.value.exists()) -> bundle.entry as entry, entry.resource = create('Observation') as obs then { qr -> obs, entry then BuildObsBase(qr, obs, entry) "base"; qr -> obs then SetSurveyCategory(qr, obs) "category"; it -> obs.code as cc then { it -> cc.coding as cg then { it -> cg.system = 'https://fhir.slade360.co.ke/fhir/CodeSystem/concept-codesystem' "sys"; it -> cg.code = 'chemotherapy-cycle-number' "cd"; it -> cg.display = 'Cycle number' "dsp"; } "coding"; } "code"; it.answer first as a then { a.value as v -> obs.value = v "set"; }; } "measure_chemotherapy_cycle_number"; g.item as it where ((linkId = 'context/stage') and answer.value.exists()) -> bundle.entry as entry, entry.resource = create('Observation') as obs then { qr -> obs, entry then BuildObsBase(qr, obs, entry) "base"; qr -> obs then SetSurveyCategory(qr, obs) "category"; it -> obs.code as cc then { it -> cc.coding as cg then { it -> cg.system = 'https://fhir.slade360.co.ke/fhir/CodeSystem/concept-codesystem' "sys"; it -> cg.code = 'chemotherapy-regimen' "cd"; it -> cg.display = 'Chemotherapy regimen' "dsp"; } "coding"; it -> cc.text = 'Cancer stage, as staged' "codeText"; } "code"; it.answer first as a then { a.value as txt -> obs.note as n, n.text = txt "text"; }; } "narr_chemotherapy_regimen"; g.item as it where ((linkId = 'context/protocol') and answer.value.exists()) -> bundle.entry as entry, entry.resource = create('Observation') as obs then { qr -> obs, entry then BuildObsBase(qr, obs, entry) "base"; qr -> obs then SetSurveyCategory(qr, obs) "category"; it -> obs.code as cc then { it -> cc.coding as cg then { it -> cg.system = 'https://fhir.slade360.co.ke/fhir/CodeSystem/concept-codesystem' "sys"; it -> cg.code = 'chemotherapy-regimen' "cd"; it -> cg.display = 'Chemotherapy regimen' "dsp"; } "coding"; } "code"; it.answer first as a then { a.value as txt -> obs.note as n, n.text = txt "text"; }; } "narr_chemotherapy_regimen2"; g.item as it where ((linkId = 'context/immunohistochemistry') and answer.value.exists()) -> bundle.entry as entry, entry.resource = create('Observation') as obs then { qr -> obs, entry then BuildObsBase(qr, obs, entry) "base"; qr -> obs then SetLaboratoryCategory(qr, obs) "category"; it -> obs.code as cc then { it -> cc.coding as cg then { it -> cg.system = 'http://loinc.org' "sys"; it -> cg.code = '11502-2' "cd"; it -> cg.display = 'Laboratory report' "dsp"; } "coding"; it -> cc.text = 'Immuno-histochemistry' "codeText"; } "code"; it.answer first as a then { a.value as txt -> obs.note as n, n.text = txt "text"; }; } "narr_11502_2"; } "contextGroup"; // ── The cytotoxic drugs ───────────────────────────────────────────────── // A MedicationRequest each. `status = 'draft'` deliberately: a chemotherapy // prescription is checked by a second clinician and reconstituted by pharmacy // before anything reaches the patient, and a cytotoxic order that arrives // 'active' has skipped both. The oncology system moves it on. // `medication.concept.text` carries the drug as written. The hospital's // formulary is not coded, and a guessed RxNorm or ATC code on a cytotoxic is // the one place in this whole set where being wrong hurts a patient rather // than a report. qr.item as g where ((linkId = 'chemotherapy-drug') and item.where(linkId = 'chemotherapy-drug/name').answer.value.exists()) -> bundle.entry as entry, entry.resource = create('MedicationRequest') as mr then { qr -> mr, entry then BuildMedicationRequestBase(qr, mr, entry) "base"; g -> mr.status = 'draft' "status"; g -> mr.intent = 'order' "intent"; g -> mr.priority = 'routine' "priority"; qr.authored as t -> mr.authoredOn = t "authoredOn"; qr.encounter as e -> mr.encounter = e; qr.source as src -> mr.requester = src "requester"; g -> mr.category as cat then { g -> cat.coding as cg then { g -> cg.system = 'http://terminology.hl7.org/CodeSystem/medicationrequest-admin-location' "sys"; g -> cg.code = 'outpatient' "cd"; g -> cg.display = 'Outpatient' "dsp"; } "coding"; } "category"; g.item as it where (linkId = 'chemotherapy-drug/name') then { it.answer first as a then { a.value as txt -> mr.medication as med, med.concept as mc, mc.text = txt "medicationText"; }; } "drugNameRule"; g.item as it where ((linkId = 'chemotherapy-drug/dose') and answer.value.ofType(Quantity).exists()) then { it.answer first as a then { a.value as v -> mr.dosageInstruction as di, di.doseAndRate as dr, dr.dose = v "doseQuantity"; }; } "doseRule"; g.item as it where ((linkId = 'chemotherapy-drug/route') and answer.value.exists()) then { it.answer first as a then { a.value as txt -> mr.dosageInstruction as di, di.route as rt, rt.text = txt "routeText"; }; } "routeRule"; // The prescribed dose per square metre, kept as written. It is the // provenance of the absolute dose above, and a pharmacist checking the // calculation needs both figures, not just the product. g.item as it where ((linkId = 'chemotherapy-drug/dose-per-bsa') and answer.value.exists()) then { it.answer first as a then { a.value as txt -> mr.note as n, n.text = txt "dosePerBsaNote"; }; } "dosePerBsaRule"; g -> mr.note as n, n.text = 'Prescribed on the chemotherapy prescription sheet. Left as a draft for second-clinician check and pharmacy reconstitution.' "provenanceNote"; } "chemotherapyDrug"; // ── The premedications ────────────────────────────────────────────────── // Six named drugs with a milligram box each on the sheet, plus a repeating // "other" row. A MedicationRequest per drug that has a dose written against // it; a blank box means it was not prescribed, not that it was prescribed at // zero. qr.item as g where (linkId = 'premedication') then { g.item as it where ((linkId = 'premedication/ondansetron') and answer.value.ofType(Quantity).exists()) -> bundle.entry as entry, entry.resource = create('MedicationRequest') as mr then { qr -> mr, entry then BuildMedicationRequestBase(qr, mr, entry) "base"; it -> mr.status = 'active' "status"; it -> mr.intent = 'order' "intent"; it -> mr.medication as med, med.concept as mc, mc.text = 'Ondansetron' "medicationText"; it.answer first as a then { a.value as v -> mr.dosageInstruction as di, di.doseAndRate as dr, dr.dose = v "doseQuantity"; }; qr.authored as t -> mr.authoredOn = t "authoredOn"; qr.encounter as e -> mr.encounter = e; qr.source as src -> mr.requester = src "requester"; it -> mr.note as n, n.text = 'Premedication, to be infused with the pre-chemotherapy hydration.' "premedNote"; } "premed_ondansetron"; } "premedGrp"; qr.item as g where (linkId = 'premedication') then { g.item as it where ((linkId = 'premedication/dexamethasone') and answer.value.ofType(Quantity).exists()) -> bundle.entry as entry, entry.resource = create('MedicationRequest') as mr then { qr -> mr, entry then BuildMedicationRequestBase(qr, mr, entry) "base"; it -> mr.status = 'active' "status"; it -> mr.intent = 'order' "intent"; it -> mr.medication as med, med.concept as mc, mc.text = 'Dexamethasone' "medicationText"; it.answer first as a then { a.value as v -> mr.dosageInstruction as di, di.doseAndRate as dr, dr.dose = v "doseQuantity"; }; qr.authored as t -> mr.authoredOn = t "authoredOn"; qr.encounter as e -> mr.encounter = e; qr.source as src -> mr.requester = src "requester"; it -> mr.note as n, n.text = 'Premedication, to be infused with the pre-chemotherapy hydration.' "premedNote"; } "premed_dexamethasone"; } "premedGrp2"; qr.item as g where (linkId = 'premedication') then { g.item as it where ((linkId = 'premedication/plasil') and answer.value.ofType(Quantity).exists()) -> bundle.entry as entry, entry.resource = create('MedicationRequest') as mr then { qr -> mr, entry then BuildMedicationRequestBase(qr, mr, entry) "base"; it -> mr.status = 'active' "status"; it -> mr.intent = 'order' "intent"; it -> mr.medication as med, med.concept as mc, mc.text = 'Plasil (metoclopramide)' "medicationText"; it.answer first as a then { a.value as v -> mr.dosageInstruction as di, di.doseAndRate as dr, dr.dose = v "doseQuantity"; }; qr.authored as t -> mr.authoredOn = t "authoredOn"; qr.encounter as e -> mr.encounter = e; qr.source as src -> mr.requester = src "requester"; it -> mr.note as n, n.text = 'Premedication, to be infused with the pre-chemotherapy hydration.' "premedNote"; } "premed_plasil"; } "premedGrp3"; qr.item as g where (linkId = 'premedication') then { g.item as it where ((linkId = 'premedication/omeprazole') and answer.value.ofType(Quantity).exists()) -> bundle.entry as entry, entry.resource = create('MedicationRequest') as mr then { qr -> mr, entry then BuildMedicationRequestBase(qr, mr, entry) "base"; it -> mr.status = 'active' "status"; it -> mr.intent = 'order' "intent"; it -> mr.medication as med, med.concept as mc, mc.text = 'Omeprazole' "medicationText"; it.answer first as a then { a.value as v -> mr.dosageInstruction as di, di.doseAndRate as dr, dr.dose = v "doseQuantity"; }; qr.authored as t -> mr.authoredOn = t "authoredOn"; qr.encounter as e -> mr.encounter = e; qr.source as src -> mr.requester = src "requester"; it -> mr.note as n, n.text = 'Premedication, to be infused with the pre-chemotherapy hydration.' "premedNote"; } "premed_omeprazole"; } "premedGrp4"; qr.item as g where (linkId = 'premedication') then { g.item as it where ((linkId = 'premedication/piriton') and answer.value.ofType(Quantity).exists()) -> bundle.entry as entry, entry.resource = create('MedicationRequest') as mr then { qr -> mr, entry then BuildMedicationRequestBase(qr, mr, entry) "base"; it -> mr.status = 'active' "status"; it -> mr.intent = 'order' "intent"; it -> mr.medication as med, med.concept as mc, mc.text = 'Piriton (chlorphenamine)' "medicationText"; it.answer first as a then { a.value as v -> mr.dosageInstruction as di, di.doseAndRate as dr, dr.dose = v "doseQuantity"; }; qr.authored as t -> mr.authoredOn = t "authoredOn"; qr.encounter as e -> mr.encounter = e; qr.source as src -> mr.requester = src "requester"; it -> mr.note as n, n.text = 'Premedication, to be infused with the pre-chemotherapy hydration.' "premedNote"; } "premed_piriton"; } "premedGrp5"; qr.item as g where (linkId = 'premedication') then { g.item as it where ((linkId = 'premedication/paracetamol') and answer.value.ofType(Quantity).exists()) -> bundle.entry as entry, entry.resource = create('MedicationRequest') as mr then { qr -> mr, entry then BuildMedicationRequestBase(qr, mr, entry) "base"; it -> mr.status = 'active' "status"; it -> mr.intent = 'order' "intent"; it -> mr.medication as med, med.concept as mc, mc.text = 'Paracetamol' "medicationText"; it.answer first as a then { a.value as v -> mr.dosageInstruction as di, di.doseAndRate as dr, dr.dose = v "doseQuantity"; }; qr.authored as t -> mr.authoredOn = t "authoredOn"; qr.encounter as e -> mr.encounter = e; qr.source as src -> mr.requester = src "requester"; it -> mr.note as n, n.text = 'Premedication, to be infused with the pre-chemotherapy hydration.' "premedNote"; } "premed_paracetamol"; } "premedGrp6"; qr.item as pg where (linkId = 'premedication') then { pg.item as g where ((linkId = 'premedication/other') and item.where(linkId = 'premedication/other/name').answer.value.exists()) -> bundle.entry as entry, entry.resource = create('MedicationRequest') as mr then { qr -> mr, entry then BuildMedicationRequestBase(qr, mr, entry) "base"; g -> mr.status = 'active' "status"; g -> mr.intent = 'order' "intent"; g.item as it where (linkId = 'premedication/other/name') then { it.answer first as a then { a.value as txt -> mr.medication as med, med.concept as mc, mc.text = txt "medicationText"; }; } "otherNameRule"; g.item as it where ((linkId = 'premedication/other/dose') and answer.value.ofType(Quantity).exists()) then { it.answer first as a then { a.value as v -> mr.dosageInstruction as di, di.doseAndRate as dr, dr.dose = v "doseQuantity"; }; } "otherDoseRule"; qr.authored as t -> mr.authoredOn = t "authoredOn"; qr.encounter as e -> mr.encounter = e; qr.source as src -> mr.requester = src "requester"; g -> mr.note as n, n.text = 'Premedication, to be infused with the pre-chemotherapy hydration.' "premedNote"; } "otherPremedication"; } "otherPremedicationGroup"; // ── Pre- and post-chemotherapy hydration ──────────────────────────────── // MedicationRequests, because a litre of normal saline before a cisplatin is // as much part of the prescription as the cisplatin. Each carries a note // saying which side of the cytotoxic it goes, since both are the same fluid // in the same volume and nothing else would distinguish them. qr.item as g where ((linkId = 'pre-hydration') and item.where(linkId = 'pre-hydration/fluid').answer.value.exists()) -> bundle.entry as entry, entry.resource = create('MedicationRequest') as mr then { qr -> mr, entry then BuildMedicationRequestBase(qr, mr, entry) "base"; g -> mr.status = 'active' "status"; g -> mr.intent = 'order' "intent"; g.item as it where (linkId = 'pre-hydration/fluid') then { it.answer first as a then { a.value as txt -> mr.medication as med, med.concept as mc, mc.text = txt "medicationText"; }; } "fluidName"; g.item as it where ((linkId = 'pre-hydration/volume') and answer.value.ofType(Quantity).exists()) then { it.answer first as a then { a.value as v -> mr.dosageInstruction as di, di.doseAndRate as dr, dr.dose = v "doseQuantity"; }; } "fluidVolume"; qr.authored as t -> mr.authoredOn = t "authoredOn"; qr.encounter as e -> mr.encounter = e; qr.source as src -> mr.requester = src "requester"; g -> mr.note as n, n.text = 'Pre-chemotherapy hydration.' "hydrationNote"; } "hydration_pre_hydration"; qr.item as g where ((linkId = 'post-hydration') and item.where(linkId = 'post-hydration/fluid').answer.value.exists()) -> bundle.entry as entry, entry.resource = create('MedicationRequest') as mr then { qr -> mr, entry then BuildMedicationRequestBase(qr, mr, entry) "base"; g -> mr.status = 'active' "status"; g -> mr.intent = 'order' "intent"; g.item as it where (linkId = 'post-hydration/fluid') then { it.answer first as a then { a.value as txt -> mr.medication as med, med.concept as mc, mc.text = txt "medicationText"; }; } "fluidName2"; g.item as it where ((linkId = 'post-hydration/volume') and answer.value.ofType(Quantity).exists()) then { it.answer first as a then { a.value as v -> mr.dosageInstruction as di, di.doseAndRate as dr, dr.dose = v "doseQuantity"; }; } "fluidVolume2"; qr.authored as t -> mr.authoredOn = t "authoredOn"; qr.encounter as e -> mr.encounter = e; qr.source as src -> mr.requester = src "requester"; g -> mr.note as n, n.text = 'Post-chemotherapy hydration.' "hydrationNote"; } "hydration_post_hydration"; // ── The mandatory pre-cycle investigations ────────────────────────────── // ServiceRequests. The sheet says these are mandatory before every cycle; a // tick that stays on the form is a requirement nobody in the laboratory can // see, and a cycle given without them is exactly what the requirement exists // to prevent. qr.item as g where (linkId = 'pre-cycle-investigations') then { g.item as it where ((linkId = 'pre-cycle-investigations/fhg') and (answer.value.ofType(Coding).code = 'Y')) -> bundle.entry as entry, entry.resource = create('ServiceRequest') as sr then { qr -> sr, entry then BuildServiceRequestBase(qr, sr, entry) "base"; it -> sr.status = 'active' "status"; it -> sr.intent = 'order' "intent"; it -> sr.category as cat then { it -> cat.coding as cg then { it -> cg.system = 'http://snomed.info/sct' "sys"; it -> cg.code = '108252007' "cd"; it -> cg.display = 'Laboratory procedure' "dsp"; } "coding"; } "category"; it -> sr.code as cr, cr.concept as cc, cc.text = 'Full haemogram' "codeText"; qr.authored as t -> sr.authoredOn = t "authoredOn"; qr.encounter as e -> sr.encounter = e; qr.source as src -> sr.requester = src "requester"; it -> sr.note as n, n.text = 'Mandatory before this cycle of chemotherapy.' "mandatoryNote"; } "precycle_fhg"; g.item as it where ((linkId = 'pre-cycle-investigations/uecs') and (answer.value.ofType(Coding).code = 'Y')) -> bundle.entry as entry, entry.resource = create('ServiceRequest') as sr then { qr -> sr, entry then BuildServiceRequestBase(qr, sr, entry) "base"; it -> sr.status = 'active' "status"; it -> sr.intent = 'order' "intent"; it -> sr.category as cat then { it -> cat.coding as cg then { it -> cg.system = 'http://snomed.info/sct' "sys"; it -> cg.code = '108252007' "cd"; it -> cg.display = 'Laboratory procedure' "dsp"; } "coding"; } "category"; it -> sr.code as cr, cr.concept as cc, cc.text = 'Urea, electrolytes and creatinine' "codeText"; qr.authored as t -> sr.authoredOn = t "authoredOn"; qr.encounter as e -> sr.encounter = e; qr.source as src -> sr.requester = src "requester"; it -> sr.note as n, n.text = 'Mandatory before this cycle of chemotherapy.' "mandatoryNote"; } "precycle_uecs"; g.item as it where ((linkId = 'pre-cycle-investigations/lfts') and (answer.value.ofType(Coding).code = 'Y')) -> bundle.entry as entry, entry.resource = create('ServiceRequest') as sr then { qr -> sr, entry then BuildServiceRequestBase(qr, sr, entry) "base"; it -> sr.status = 'active' "status"; it -> sr.intent = 'order' "intent"; it -> sr.category as cat then { it -> cat.coding as cg then { it -> cg.system = 'http://snomed.info/sct' "sys"; it -> cg.code = '108252007' "cd"; it -> cg.display = 'Laboratory procedure' "dsp"; } "coding"; } "category"; it -> sr.code as cr, cr.concept as cc, cc.text = 'Liver function tests' "codeText"; qr.authored as t -> sr.authoredOn = t "authoredOn"; qr.encounter as e -> sr.encounter = e; qr.source as src -> sr.requester = src "requester"; it -> sr.note as n, n.text = 'Mandatory before this cycle of chemotherapy.' "mandatoryNote"; } "precycle_lfts"; g.item as it where ((linkId = 'pre-cycle-investigations/psa') and (answer.value.ofType(Coding).code = 'Y')) -> bundle.entry as entry, entry.resource = create('ServiceRequest') as sr then { qr -> sr, entry then BuildServiceRequestBase(qr, sr, entry) "base"; it -> sr.status = 'active' "status"; it -> sr.intent = 'order' "intent"; it -> sr.category as cat then { it -> cat.coding as cg then { it -> cg.system = 'http://snomed.info/sct' "sys"; it -> cg.code = '108252007' "cd"; it -> cg.display = 'Laboratory procedure' "dsp"; } "coding"; } "category"; it -> sr.code as cr, cr.concept as cc, cc.text = 'PSA' "codeText"; qr.authored as t -> sr.authoredOn = t "authoredOn"; qr.encounter as e -> sr.encounter = e; qr.source as src -> sr.requester = src "requester"; it -> sr.note as n, n.text = 'Mandatory before this cycle of chemotherapy.' "mandatoryNote"; } "precycle_psa"; g.item as it where ((linkId = 'pre-cycle-investigations/ca-marker') and (answer.value.ofType(Coding).code = 'Y')) -> bundle.entry as entry, entry.resource = create('ServiceRequest') as sr then { qr -> sr, entry then BuildServiceRequestBase(qr, sr, entry) "base"; it -> sr.status = 'active' "status"; it -> sr.intent = 'order' "intent"; it -> sr.category as cat then { it -> cat.coding as cg then { it -> cg.system = 'http://snomed.info/sct' "sys"; it -> cg.code = '108252007' "cd"; it -> cg.display = 'Laboratory procedure' "dsp"; } "coding"; } "category"; it -> sr.code as cr, cr.concept as cc, cc.text = 'CA tumour marker' "codeText"; qr.authored as t -> sr.authoredOn = t "authoredOn"; qr.encounter as e -> sr.encounter = e; qr.source as src -> sr.requester = src "requester"; it -> sr.note as n, n.text = 'Mandatory before this cycle of chemotherapy.' "mandatoryNote"; } "precycle_ca_marker"; g.item as it where ((linkId = 'pre-cycle-investigations/imaging') and answer.value.exists()) -> bundle.entry as entry, entry.resource = create('ServiceRequest') as sr then { qr -> sr, entry then BuildServiceRequestBase(qr, sr, entry) "base"; it -> sr.status = 'active' "status"; it -> sr.intent = 'order' "intent"; it -> sr.category as cat then { it -> cat.coding as cg then { it -> cg.system = 'http://snomed.info/sct' "sys"; it -> cg.code = '363679005' "cd"; it -> cg.display = 'Imaging' "dsp"; } "coding"; } "category"; it.answer first as a then { a.value as txt -> sr.code as cr, cr.concept as cc, cc.text = txt "codeText"; }; qr.authored as t -> sr.authoredOn = t "authoredOn"; qr.encounter as e -> sr.encounter = e; qr.source as src -> sr.requester = src "requester"; } "imagingOrder"; g.item as it where (linkId = 'pre-cycle-investigations/other') then { it.answer as a -> bundle.entry as entry, entry.resource = create('ServiceRequest') as sr then { qr -> sr, entry then BuildServiceRequestBase(qr, sr, entry) "base"; a -> sr.status = 'active' "status"; a -> sr.intent = 'order' "intent"; a -> sr.category as cat then { a -> cat.coding as cg then { a -> cg.system = 'http://snomed.info/sct' "sys"; a -> cg.code = '108252007' "cd"; a -> cg.display = 'Laboratory procedure' "dsp"; } "coding"; } "category"; a.value as txt -> sr.code as cr, cr.concept as cc, cc.text = txt "codeText"; qr.authored as t -> sr.authoredOn = t "authoredOn"; qr.encounter as e -> sr.encounter = e; qr.source as src -> sr.requester = src "requester"; } "createOtherOrder"; } "otherInvestigations"; } "preCycleGroup"; // ── Post-chemotherapy treatment ───────────────────────────────────────── // The four numbered lines at the foot of the sheet. MedicationRequests, since // that is what they are in practice -- antiemetics and growth factor to take // home -- but with `status = 'draft'` and no dose parsed out of the free // text, because the sheet writes them as one line each. qr.item as g where ((linkId = 'post-chemo-treatment') and item.where(linkId = 'post-chemo-treatment/name').answer.value.exists()) -> bundle.entry as entry, entry.resource = create('MedicationRequest') as mr then { qr -> mr, entry then BuildMedicationRequestBase(qr, mr, entry) "base"; g -> mr.status = 'draft' "status"; g -> mr.intent = 'order' "intent"; g.item as it where (linkId = 'post-chemo-treatment/name') then { it.answer first as a then { a.value as txt -> mr.medication as med, med.concept as mc, mc.text = txt "medicationText"; }; } "postNameRule"; g.item as it where ((linkId = 'post-chemo-treatment/detail') and answer.value.exists()) then { it.answer first as a then { a.value as txt -> mr.dosageInstruction as di, di.text = txt "dosageText"; }; } "postDetailRule"; qr.authored as t -> mr.authoredOn = t "authoredOn"; qr.encounter as e -> mr.encounter = e; qr.source as src -> mr.requester = src "requester"; g -> mr.note as n, n.text = 'Post-chemotherapy treatment written on the prescription sheet. Left as a draft: the sheet gives no dose or frequency to parse.' "provenanceNote"; } "postChemoTreatment"; // ── Diagnosis ─────────────────────────────────────────────────────────── // Not a Condition. A patient on cycle four already has a coded cancer on the // problem list, and creating a new Condition from every cycle's prescription // sheet would put four of them there by the end of treatment. The diagnosis // travels as a note on the regimen Observation instead. qr.item as g where (linkId = 'context') then { g.item as it where ((linkId = 'context/diagnosis') and answer.value.exists()) -> bundle.entry as entry, entry.resource = create('Observation') as obs then { qr -> obs, entry then BuildObsBase(qr, obs, entry) "base"; qr -> obs then SetSurveyCategory(qr, obs) "category"; it -> obs.code as cc then { it -> cc.coding as cg then { it -> cg.system = 'https://fhir.slade360.co.ke/fhir/CodeSystem/concept-codesystem' "sys"; it -> cg.code = 'chemotherapy-regimen' "cd"; it -> cg.display = 'Chemotherapy regimen' "dsp"; } "coding"; it -> cc.text = 'Diagnosis this cycle was prescribed for' "codeText"; } "code"; it.answer first as a then { a.value as txt -> obs.note as n, n.text = txt "text"; }; } "diagnosisObservation"; } "diagnosisGroup"; } // ───────────────────────────────────────────────────────────────────────────── // Shared scaffolding. Repeated in each map rather than imported: the transform // engine resolves `imports` against whatever StructureMaps happen to be on the // server, and a form that stops extracting because a shared map was not // uploaded is a worse failure than a duplicated group. // Two notes on what the publisher's StructureMap validator says about this file. // Source parameters below are left untyped on purpose. Writing `: Element` makes // the validator compare QuestionnaireResponse.item against the literal 'Element' // and report them as incompatible with each other, which is noise, not a finding. // `evaluate(context, expression)` is flagged as "takes 1 parameter but 2 were // found". Leave it. The FML parser in the same jar refuses the one-argument form // outright -- it wants a parameter, a comma, then the FHIRPath -- so the // publisher is objecting to what its own compiler emits. Two arguments is the // only form that compiles, and it is what the R5 transform engine executes. // ───────────────────────────────────────────────────────────────────────────── group BuildObsBase(source qr : QR, target obs : Observation, target entry) { qr -> obs then SetResponseProvenance(qr, obs) "responseProvenance"; qr -> obs.id = uuid() then SetObservationFullUrl(obs, entry) "idAndFullUrl"; qr -> entry.request as request then { qr -> request.method = 'POST' "requestMethod"; qr -> request.url = 'Observation' "requestUrl"; } "entryRequest"; qr -> obs.status = 'final' "status"; qr.subject as s -> obs.subject = s; qr.encounter as e -> obs.encounter = e; qr.authored as t -> obs.effective = t "effective"; qr.source as src -> obs.performer = src "performerFromSource"; qr where (source.exists().not()) then { qr.author as a -> obs.performer = a "performerFromAuthor"; } "performerFallback"; // A stored response's id already reads "QuestionnaireResponse/<id>", so it is // a relative reference as it stands. The hasValue() guard is what stops an // unsaved response -- one posted straight to $extract rather than saved first // -- from writing "QuestionnaireResponse/null" into the record. qr.id as qid where ($this.hasValue()) -> obs.derivedFrom = create('Reference') as ref, ref.reference = qid "linkQuestionnaireResponse"; } group SetObservationFullUrl(source obs : Observation, target entry) { obs.id as id -> entry.fullUrl = append('https://fhir.slade360.co.ke/fhir/Observation/', id) "assignFullUrl"; } // The two answer shapes a component can carry. Both read the answer through the // untyped `.value` accessor: reading `valueCoding` or `valueQuantity` by its // typed property name fails at transform time on HAPI R5 with "Attempt to read // invalid property ... on QuestionnaireResponse.item.answer". group SetComponentCodedValue(source it, target comp) { it.answer first as a then { a.value as cv -> comp.value = create('CodeableConcept') as cc then { cv -> cc.coding = cv "valueCoding"; cv.display as d -> cc.text = d "valueText"; } "codedValue"; } "componentAnswer"; } group SetComponentValue(source it, target comp) { it.answer first as a then { a.value as v -> comp.value = v "plainValue"; } "componentAnswer"; } group SetSurveyCategory(source qr : QR, target obs : Observation) { qr -> obs.category as cat then { qr -> cat.coding as coding then { qr -> coding.system = 'http://terminology.hl7.org/CodeSystem/observation-category' "categorySystem"; qr -> coding.code = 'survey' "categoryCode"; qr -> coding.display = 'Survey' "categoryDisplay"; } "categoryCoding"; } "category"; } group SetVitalSignsCategory(source qr : QR, target obs : Observation) { qr -> obs.category as cat then { qr -> cat.coding as coding then { qr -> coding.system = 'http://terminology.hl7.org/CodeSystem/observation-category' "categorySystem"; qr -> coding.code = 'vital-signs' "categoryCode"; qr -> coding.display = 'Vital Signs' "categoryDisplay"; } "categoryCoding"; } "category"; } group SetLaboratoryCategory(source qr : QR, target obs : Observation) { qr -> obs.category as cat then { qr -> cat.coding as coding then { qr -> coding.system = 'http://terminology.hl7.org/CodeSystem/observation-category' "categorySystem"; qr -> coding.code = 'laboratory' "categoryCode"; qr -> coding.display = 'Laboratory' "categoryDisplay"; } "categoryCoding"; } "category"; } group BuildMedicationRequestBase(source qr : QR, target r : MedicationRequest, target entry) { qr -> r then SetResponseProvenance(qr, r) "responseProvenance"; qr -> r.id = uuid() then SetMedicationRequestFullUrl(r, entry) "idAndFullUrl"; qr -> entry.request as request then { qr -> request.method = 'POST' "requestMethod"; qr -> request.url = 'MedicationRequest' "requestUrl"; } "entryRequest"; qr.subject as s -> r.subject = s; } group SetMedicationRequestFullUrl(source r : MedicationRequest, target entry) { r.id as id -> entry.fullUrl = append('https://fhir.slade360.co.ke/fhir/MedicationRequest/', id) "assignFullUrl"; } group BuildServiceRequestBase(source qr : QR, target r : ServiceRequest, target entry) { qr -> r then SetResponseProvenance(qr, r) "responseProvenance"; qr -> r.id = uuid() then SetServiceRequestFullUrl(r, entry) "idAndFullUrl"; qr -> entry.request as request then { qr -> request.method = 'POST' "requestMethod"; qr -> request.url = 'ServiceRequest' "requestUrl"; } "entryRequest"; qr.subject as s -> r.subject = s; } group SetServiceRequestFullUrl(source r : ServiceRequest, target entry) { r.id as id -> entry.fullUrl = append('https://fhir.slade360.co.ke/fhir/ServiceRequest/', id) "assignFullUrl"; } // ── Provenance and tenancy ─────────────────────────────────────────────────── // Two tags on everything this map creates. The first says which questionnaire // produced it, so a form that reopens mid-admission recalls its own earlier // entries and not another form's. The second copies the tenant tags off the // response, because nothing else on the path puts them there. // The code is the form's document type, not the questionnaire's id: every // environment mints its own ids, and there is only one set of maps, so an id // here would tie the tag to whichever environment stamped it. // ── Provenance and tenancy ─────────────────────────────────────────────────── group SetResponseProvenance(source qr : QR, target r) { qr -> r.meta as m then { qr -> m.tag as t then { qr -> t.system = 'http://slade360edi.com/questionnaire-provenance' "provenanceSystem"; qr -> t.code = 'chemotherapy-prescription' "provenanceCode"; qr -> t.display = 'Chemotherapy prescription' "provenanceDisplay"; } "provenanceTag"; } "provenanceMeta"; // Every tag the response carries, onto the resource. `meta` is 0..1, so this // lands on the same meta the rule above created rather than a second one. qr.meta as qm then { qm.tag as qt -> r.meta as m, m.tag = qt "copyTag"; } "tenantTags"; }