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: ExtractDeviceRequest

Official URL: https://fhir.slade360.co.ke/fhir/StructureMap/ExtractDeviceRequest Version: 0.1.0
Draft as of 2026-09-14 Computable Name: ExtractDeviceRequest

/// url = 'https://fhir.slade360.co.ke/fhir/StructureMap/ExtractDeviceRequest'
/// name = 'ExtractDeviceRequest'
/// 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/DeviceRequest" alias DeviceRequest as target

group ExtractDeviceRequest(source qr : QR, target bundle : Bundle) {
  qr -> bundle.type = 'transaction' "setBundleType";
  // Subject, encounter, authored date, requester, identifier and the device extension all
  // live on the response itself, so the whole of it is carried into the build group rather
  // than the answer group alone.
  qr.item as requestGroup where (linkId = 'the-request') then {
    requestGroup ->  bundle.entry as entry,  entry.resource = create('DeviceRequest') as devReq then {
      qr ->  devReq,  entry then BuildDeviceRequest(qr, requestGroup, devReq, entry) "buildDevReq";
    } "createDevReq";
  } "findRequestGroup";
}

group BuildDeviceRequest(source qr : QR, source requestGroup, target devReq : DeviceRequest, target entry) {
  // The form has no sign-off step, so a submitted order is already actionable. If a
  // review step is added later this becomes 'draft', as it is on the medication order.
  qr -> devReq.status = 'active' "setStatus";
  qr.subject as s -> devReq.subject = s "setSubject";
  qr.encounter as enc -> devReq.encounter = enc "setEncounter";
  qr.authored as authored -> devReq.authoredOn = authored "setAuthoredOn";
  qr.source as src -> devReq.requester = src "setRequester";
  qr.identifier first as qrIdent -> devReq.identifier = qrIdent "copyIdentifier";
  // Device as CodeableReference from the QR extension (using concept). The ordering
  // client sets the device there rather than answering a form item, so this reads exactly
  // as the medication order map reads its medication concept. Assigning the whole
  // CodeableConcept carries its codings and their systems, so nothing further is copied.
  // `value`, not `valueCodeableConcept`: this engine resolves the choice element by its
  // base name and rejects the typed one outright --
  // "Attempt to read invalid property 'valueCodeableConcept' on type Extension" -- which
  // fails the whole transform, not just this rule. Same accessor every answer below uses.
  // The url is matched on its tail rather than in full: a map is published once at the
  // production canonical while the extension url is built from each environment's own
  // base, so an equality test would hold in production alone. Matching beats taking the
  // first extension, which silently reads whichever one the client happened to put first.
  qr.extension as ext where (url.endsWith('/StructureDefinition/device-reference')) then {
    ext.value as deviceCC -> devReq.code as code then {
      deviceCC -> code.concept = deviceCC "setDeviceConcept";
    } "accessDevice";
  } "findDeviceExt";
  // Intent. DeviceRequest.intent is a code, not a Coding: it takes the code alone and has
  // nowhere to record the system it came from.
  requestGroup.item as intentItem where (linkId = 'intent') then {
    intentItem.answer first as intentAns then {
      intentAns.value as intentCoding then {
        intentCoding.code as intentCode -> devReq.intent = intentCode "setIntentCode";
      } "extractIntentCode";
    } "extractIntent";
  } "findIntent";
  // Priority. Same shape as intent, and same reason.
  requestGroup.item as priorityItem where (linkId = 'priority') then {
    priorityItem.answer first as priorityAns then {
      priorityAns.value as priorityCoding then {
        priorityCoding.code as priorityCode -> devReq.priority = priorityCode "setPriorityCode";
      } "extractPriorityCode";
    } "extractPriority";
  } "findPriority";
  requestGroup.item as qtyItem where (linkId = 'quantity') then {
    qtyItem.answer first as qtyAns then {
      qtyAns.value as qtyValue -> devReq.quantity = qtyValue "setQuantity";
    } "extractQuantity";
  } "findQuantity";
  // How it is supplied. DeviceRequest has no element for this, so it becomes a parameter:
  // parameter.code names the question, parameter.value carries the answer.
  requestGroup.item as supplyItem where (linkId = 'supply-method') then {
    supplyItem.answer first as supplyAns then {
      supplyAns.value as supplyCoding -> devReq.parameter as param then {
        supplyCoding -> param.code = create('CodeableConcept') as paramCode then {
          supplyCoding -> paramCode.coding = create('Coding') as paramCoding then {
            supplyCoding -> paramCoding.system = 'https://fhir.slade360.co.ke/fhir/CodeSystem/device-order-codesystem' "setParamSystem";
            supplyCoding -> paramCoding.code = 'supply-method' "setParamCode";
            supplyCoding -> paramCoding.display = 'How the device is supplied' "setParamDisplay";
          } "buildParamCoding";
        } "buildParamCode";
        supplyCoding -> param.value = create('CodeableConcept') as paramValue then {
          supplyCoding -> paramValue.coding = supplyCoding "setParamValueCoding";
          supplyCoding.display as supplyDisp -> paramValue.text = supplyDisp "setParamValueText";
        } "buildParamValue";
      } "createParameter";
    } "extractSupply";
  } "findSupply";
  // Reason is free text on the form, so it lands as CodeableReference.concept.text with
  // no coding. A coded reason would need a value set the form does not offer.
  requestGroup.item as reasonItem where (linkId = 'reason') then {
    reasonItem.answer first as reasonAns then {
      reasonAns.value as reasonText ->  devReq.reason as reasonRef,  reasonRef.concept as reasonCC then {
        reasonText -> reasonCC.text = reasonText "setReasonText";
      } "setReason";
    } "extractReason";
  } "findReason";
  // Patient instruction. DeviceRequest has no equivalent of Dosage.patientInstruction, so
  // this rides a local extension rather than collapsing into note alongside the message
  // written for the dispensing team.
  requestGroup.item as instructionItem where (linkId = 'patient-instructions') then {
    instructionItem.answer first as instructionAns then {
      instructionAns.value as instructionText -> devReq.extension as instructionExt then {
        instructionText -> instructionExt.url = 'https://fhir.slade360.co.ke/fhir/StructureDefinition/sghi-device-patient-instruction' "setInstructionExtUrl";
        instructionText -> instructionExt.value = instructionText "setInstructionExtValue";
      } "createInstructionExt";
    } "extractInstruction";
  } "findInstruction";
  requestGroup.item as notesItem where (linkId = 'notes') then {
    notesItem.answer first as notesAns then {
      notesAns.value as notesText -> devReq.note as note then {
        notesText -> note.text = notesText "setNoteText";
      } "createNote";
    } "extractNotes";
  } "findNotes";
  qr -> devReq.id = uuid() then SetDeviceRequestFullUrl(devReq, entry) "setDevReqIdAndFullUrl";
  qr -> entry.request as request then {
    qr -> request.method = 'POST' "reqMethod";
    qr -> request.url = 'DeviceRequest' "reqUrl";
  } "entryRequest";
}

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