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

StructureMap: ExtractMedicationRequest

Official URL: https://fhir.slade360.co.ke/fhir/StructureMap/ExtractMedicationRequest Version: 0.1.0
Draft as of 2026-10-06 Computable Name: ExtractMedicationRequest

/// url = 'https://fhir.slade360.co.ke/fhir/StructureMap/ExtractMedicationRequest'
/// name = 'ExtractMedicationRequest'
/// status = 'draft'

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/MedicationRequest" alias MedicationRequest as target
uses "http://hl7.org/fhir/StructureDefinition/Medication" alias Medication as target

group ExtractMedicationRequest(source qr : QR, target bundle : Bundle) {
  qr -> bundle.type = 'transaction' "setBundleType";
  qr.item as prescriptionGroup where (linkId = 'prescription-form') then {
    prescriptionGroup ->  bundle.entry as entry,  entry.resource = create('MedicationRequest') as medReq then {
      qr -> medReq then CopyResponseTags(qr, medReq) "responseTags";
      qr ->  medReq,  entry then BuildMedicationRequest(qr, prescriptionGroup, medReq, entry) "buildMedReq";
    } "createMedReq";
  } "findPrescriptionGroup";
}

group BuildMedicationRequest(source qr : QR, source prescriptionGroup, target medReq : MedicationRequest, target entry) {
  // status will always be draft until medication has been signed off
  qr -> medReq.status = 'draft' "setStatus";
  qr -> medReq.intent = 'order' "setIntent";
  // Subject from QuestionnaireResponse
  qr.subject as s where ((reference.exists() and (reference.startsWith('Patient/'))) or (reference.empty() and (type.empty() or ('/' + type).endsWith('/Patient')))) -> medReq.subject = s "setSubject";
  // Encounter from QuestionnaireResponse
  qr.encounter as enc where ((reference.exists() and (reference.startsWith('Encounter/'))) or (reference.empty() and (type.empty() or ('/' + type).endsWith('/Encounter')))) -> medReq.encounter = enc "setEncounter";
  // AuthoredOn from QuestionnaireResponse.authored
  qr.authored as authored -> medReq.authoredOn = authored "setAuthoredOn";
  // Requester from QuestionnaireResponse.source (the Organization)
  qr.source as src where ((reference.exists() and (reference.startsWith('Patient/') or reference.startsWith('Organization/'))) or (reference.empty() and (type.empty() or ('/' + type).endsWith('/Patient') or ('/' + type).endsWith('/Organization')))) -> medReq.requester = src "setRequester";
  // Performer from QuestionnaireResponse.source (the Organization)
  qr.source as src where ((reference.exists() and (reference.startsWith('Patient/') or reference.startsWith('Organization/'))) or (reference.empty() and (type.empty() or ('/' + type).endsWith('/Patient') or ('/' + type).endsWith('/Organization')))) -> medReq.performer = create('Reference') as perf then {
    src.reference as ref -> perf.reference = ref "perfRef";
    src.display as disp -> perf.display = disp "perfDisplay";
  } "performer";
  // Copy identifier from QuestionnaireResponse to MedicationRequest, unless its
  // assigner is something an identifier cannot be assigned by.
  qr.identifier first as qrIdent where (assigner.empty() or assigner.where(((reference.exists() and (reference.startsWith('Organization/'))) or (reference.empty() and (type.empty() or ('/' + type).endsWith('/Organization'))))).exists()) -> medReq.identifier = qrIdent "copyIdentifier";
  // Medication as CodeableReference from the medication-reference extension, which
  // the platform writes under either canonical. Assigning the whole CodeableConcept
  // carries its codings and their systems, so there is nothing further to copy.
  // R5's engine reads a choice as `value`: `valueCodeableConcept`, and every
  // `valueX` below, is an invalid property here and failed the whole extraction.
  qr.extension as ext where (url.endsWith('/StructureDefinition/medication-reference')) then {
    ext.value as medCC where ($this is CodeableConcept) -> medReq.medication as med then {
      medCC -> med.concept = medCC "setMedConcept";
    } "accessMedication";
  } "findMedicationExt";
  // Priority
  prescriptionGroup.item as priorityItem where (linkId = 'priority') then {
    priorityItem.answer first as priorityAns then {
      // MedicationRequest.priority is a code, not a Coding: it holds the code
      // alone and has nowhere to put the system it came from.
      priorityAns.value as priorityCoding where ($this is Coding) then {
        priorityCoding.code as pCode -> medReq.priority = pCode "setPriorityCode";
      } "extractPriorityCode";
    } "extractPriority";
  } "findPriority";
  // Dosage Instruction
  qr -> medReq.dosageInstruction = create('Dosage') as dosage then {
    prescriptionGroup -> dosage then BuildDosage(prescriptionGroup, dosage) "buildDosage";
  } "createDosage";
  // Substitution
  prescriptionGroup.item as substItem where (linkId = 'substitutable') then {
    substItem.answer first as substAns then {
      substAns.value as substBool where ($this is boolean) -> medReq.substitution as subst then {
        // allowed[x] is written as `allowed`; the engine picks the type from the value.
        substBool -> subst.allowed = substBool "setSubstValue";
      } "createSubst";
    } "extractSubst";
  } "findSubst";
  // Dispense Request
  qr -> medReq.dispenseRequest as dispReq then {
    prescriptionGroup -> dispReq then BuildDispenseRequest(qr, prescriptionGroup, dispReq) "buildDispReq";
  } "createDispReq";
  // Set ID and FullUrl
  qr -> medReq.id = uuid() then SetMedicationRequestFullUrl(medReq, entry) "setMedReqIdAndFullUrl";
  // Entry request for transaction bundle
  qr -> entry.request as request then {
    qr -> request.method = 'POST' "reqMethod";
    qr -> request.url = 'MedicationRequest' "reqUrl";
  } "entryRequest";
}

group BuildDosage(source prescriptionGroup, target dosage : Dosage) {
  prescriptionGroup -> dosage.timing as timing then {
    prescriptionGroup -> timing.repeat as repeat then {
      // Frequency (OD, BD, TID, QID). A positiveInt, which an integer is not converted
      // to, so each is cast.
      prescriptionGroup.item as freqItem where (linkId = 'frequency') then {
        freqItem.answer first as freqAns then {
          freqAns.value as freqCoding where (($this is Coding) and (code = 'OD')) -> repeat.frequency = cast('1', 'positiveInt') "setFreqOD";
          freqAns.value as freqCoding where (($this is Coding) and (code = 'BD')) -> repeat.frequency = cast('2', 'positiveInt') "setFreqBD";
          freqAns.value as freqCoding where (($this is Coding) and (code = 'TID')) -> repeat.frequency = cast('3', 'positiveInt') "setFreqTID";
          freqAns.value as freqCoding where (($this is Coding) and (code = 'QID')) -> repeat.frequency = cast('4', 'positiveInt') "setFreqQID";
        } "extractFreq";
      } "findFreq";
      // Period (1 day)
      prescriptionGroup -> repeat.period = 1 "setPeriod";
      prescriptionGroup -> repeat.periodUnit = 'd' "setPeriodUnit";
      // Duration
      prescriptionGroup.item as durItem where (linkId = 'duration') then {
        durItem.answer first as durAns then {
          durAns.value as durValue where ($this is integer) -> repeat.duration = durValue "setDuration";
        } "extractDuration";
      } "findDuration";
      // Duration UOM - directly use the code from unitsofmeasure.org (h, d, wk, mo, a)
      prescriptionGroup.item as durUomItem where (linkId = 'duration-uom') then {
        durUomItem.answer first as durUomAns then {
          durUomAns.value as durUomCoding where ($this is Coding) then {
            durUomCoding.code as durUomCode -> repeat.durationUnit = durUomCode "setDurationUnit";
          } "extractDurUomCode";
        } "extractDurUom";
      } "findDurUom";
      // When to take - directly use the code (AC, PC, ACM, ACD, ACV, PCM, PCD, PCV, etc.)
      // Handles both single answer and array of answers
      prescriptionGroup.item as whenItem where (linkId = 'when-to-take') then {
        whenItem.answer as whenAns then {
          whenAns.value as whenCoding where ($this is Coding) then {
            whenCoding.code as whenCode -> repeat.when = whenCode "setWhenCode";
          } "extractWhenCode";
        } "extractWhen";
      } "findWhen";
      // Bounds Period from start-date and end-date
      prescriptionGroup.item as startItem where (linkId = 'start-date') then {
        startItem.answer first as startAns then {
          startAns.value as startDate where ($this is date) -> repeat.bounds = create('Period') as boundsPeriod then {
            startDate -> boundsPeriod.start = startDate "setBoundsStart";
            prescriptionGroup.item as endItem where (linkId = 'end-date') then {
              endItem.answer first as endAns then {
                endAns.value as endDate where ($this is date) -> boundsPeriod.end = endDate "setBoundsEnd";
              } "extractEndDate";
            } "findEndDate";
          } "createBoundsPeriod";
        } "extractStartDate";
      } "findStartDate";
    } "accessRepeat";
  } "accessTiming";
  // Route
  prescriptionGroup.item as routeItem where (linkId = 'route') then {
    routeItem.answer first as routeAns then {
      routeAns.value as routeCoding where ($this is Coding) -> dosage.route = create('CodeableConcept') as routeCC then {
        routeCoding -> routeCC.coding = routeCoding "setRouteCoding";
        routeCoding.display as disp -> routeCC.text = disp "setRouteText";
      } "setRouteCC";
    } "extractRoute";
  } "findRoute";
  // Dose and Rate. A dose can be a fraction (half a tablet, 2.5 ml), so a decimal
  // answer is read as well as the integer ones already stored.
  prescriptionGroup.item as doseItem where (linkId = 'dose') then {
    doseItem.answer first as doseAns then {
      doseAns.value as doseValue where (($this is integer) or ($this is decimal)) ->  dosage.doseAndRate as doseRate,  doseRate.dose = create('Quantity') as doseQty then {
        doseValue -> doseQty.value = doseValue "setDoseValue";
        prescriptionGroup.item as doseUnitItem where (linkId = 'dosage-unit') then {
          doseUnitItem.answer first as doseUnitAns then {
            doseUnitAns.value as unitCoding where ($this is Coding) then {
              unitCoding.display as unitDisp -> doseQty.unit = unitDisp "setDoseUnit";
              unitCoding.code as unitCode -> doseQty.code = unitCode "setDoseCode";
              unitCoding.system as unitSys -> doseQty.system = unitSys "setDoseSystem";
            } "extractUnitCoding";
          } "extractDoseUnitAns";
        } "findDoseUnit";
      } "createDoseAndRate";
    } "extractDoseAns";
  } "findDose";
  // Dosage Instructions (patient instruction)
  prescriptionGroup.item as instrItem where (linkId = 'dosage-instructions') then {
    instrItem.answer first as instrAns then {
      instrAns.value as instrText where ($this is string) -> dosage.patientInstruction = instrText "setPatientInstruction";
    } "extractInstr";
  } "findDosageInstr";
  // Additional Instructions (text)
  prescriptionGroup.item as addInstrItem where (linkId = 'additional-instructions') then {
    addInstrItem.answer first as addInstrAns then {
      addInstrAns.value as addInstrText where ($this is string) -> dosage.text = addInstrText "setDosageText";
    } "extractAddInstr";
  } "findAddInstr";
}

group BuildDispenseRequest(source qr : QR, source prescriptionGroup, target dispReq) {
  // Refills
  prescriptionGroup.item as refillsItem where (linkId = 'refills') then {
    refillsItem.answer first as refillsAns then {
      // An unsignedInt, which an integer is not converted to: cast, and only from zero up.
      refillsAns.value as refillsValue where (($this is integer) and ($this >= 0)) -> dispReq.numberOfRepeatsAllowed = cast(refillsValue, 'unsignedInt') "setRefills";
    } "extractRefills";
  } "findRefills";
  // Validity Period from start-date and end-date
  prescriptionGroup.item as startItem where (linkId = 'start-date') then {
    startItem.answer first as startAns then {
      startAns.value as startDate where ($this is date) -> dispReq.validityPeriod as validPeriod then {
        startDate -> validPeriod.start = startDate "setValidityStart";
        prescriptionGroup.item as endItem where (linkId = 'end-date') then {
          endItem.answer first as endAns then {
            endAns.value as endDate where ($this is date) -> validPeriod.end = endDate "setValidityEnd";
          } "extractValidityEnd";
        } "findValidityEnd";
      } "createValidityPeriod";
    } "extractValidityStart";
  } "findValidityStart";
  // Expected Supply Duration
  prescriptionGroup.item as durItem where (linkId = 'duration') then {
    durItem.answer first as durAns then {
      durAns.value as durValue where ($this is integer) -> dispReq.expectedSupplyDuration as supplyDur then {
        durValue -> supplyDur.value = durValue "setSupplyDurValue";
        prescriptionGroup.item as durUomItem where (linkId = 'duration-uom') then {
          durUomItem.answer first as durUomAns then {
            durUomAns.value as durUomCoding where ($this is Coding) then {
              durUomCoding.code as uomCode -> supplyDur.code = uomCode "setSupplyDurCode";
              durUomCoding.display as uomDisplay -> supplyDur.unit = uomDisplay "setSupplyDurUnit";
              durUomCoding.system as uomSystem -> supplyDur.system = uomSystem "setSupplyDurSystem";
            } "extractSupplyDurUom";
          } "extractSupplyDurUomAns";
        } "findSupplyDurUom";
      } "createSupplyDuration";
    } "extractSupplyDuration";
  } "findSupplyDuration";
  // Dispenser - from QuestionnaireResponse.source (Organization)
  qr.source as src where ((reference.exists() and (reference.startsWith('Organization/'))) or (reference.empty() and (type.empty() or ('/' + type).endsWith('/Organization')))) -> dispReq.dispenser = src "setDispenser";
}

group SetMedicationRequestFullUrl(source medReq : MedicationRequest, target entry) {
  medReq.id as id -> entry.fullUrl = append('https://fhir.slade360.co.ke/fhir/MedicationRequest/', id) "assignFullUrl";
}

// ── Tenancy ──────────────────────────────────────────────────────────────────
// Whose this is. The platform says which tenant a response belongs to with a
// tag on the response, and every tag the response carries is copied onto
// everything this map creates, as it is. Copied rather than named here, so the
// map never has to know which tenant it is running for, nor which system the
// tag is under: that system has moved more than once.
group CopyResponseTags(source qr : QR, target r) {
  qr.meta as qm then {
    qm.tag as qt ->  r.meta as m,  m.tag = qt "copyTag";
  } "tenantTags";
}