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: Inpatient Paediatric Admission Assessment Extraction

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

Weight, immunisation status and feeding become Observations; the guardian at the bedside becomes a RelatedPerson. Immunisation status is an Observation rather than an Immunization because the form records what the guardian says, not a dose given with a vaccine, a date and a batch – an Immunization built from Partially immunised would be a fiction. The guardian is a new RelatedPerson rather than a patch to Patient.contact: repeated assessments would append a duplicate contact each time with nothing to deduplicate on, and moh-510-extractor already mints guardian RelatedPersons this way.

/// url = 'https://fhir.slade360.co.ke/fhir/StructureMap/ExtractInpatientPaediatricAdmissionAssessment'
/// name = 'ExtractInpatientPaediatricAdmissionAssessment'
/// title = 'Inpatient Paediatric Admission Assessment Extraction'
/// status = 'active'
/// description = 'Weight, immunisation status and feeding become Observations; the guardian at the bedside becomes a RelatedPerson. Immunisation status is an Observation rather than an Immunization because the form records what the guardian says, not a dose given with a vaccine, a date and a batch -- an Immunization built from `Partially immunised` would be a fiction. The guardian is a new RelatedPerson rather than a patch to Patient.contact: repeated assessments would append a duplicate contact each time with nothing to deduplicate on, and moh-510-extractor already mints guardian RelatedPersons this way.'

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/RelatedPerson" alias RelatedPerson as target

group ExtractInpatientPaediatricAdmissionAssessment(source qr : QR, target bundle : Bundle) {
  qr -> bundle.type = 'transaction' "setBundleType";
  qr.item as itW where ((linkId = 'weight') and answer.value.exists()) then {
    itW.answer first as aW then {
      aW ->  bundle.entry as entryW,  entryW.resource = create('Observation') as obsW then {
        qr ->  obsW,  entryW then BuildObsBase(qr, obsW, entryW) "weightBase";
        qr -> obsW then SetVitalSignsCategory(qr, obsW) "weightCategory";
        qr -> obsW.code as code then {
          qr -> code.coding as coding then {
            qr -> coding.system = 'http://loinc.org' "codeSystem";
            qr -> coding.code = '29463-7' "codeCode";
            qr -> coding.display = 'Body weight' "codeDisplay";
          } "codeCoding";
        } "weightCode";
        // Already a Quantity: the item is type `quantity` with a unitOption
        // pinning kg.
        aW.value as v -> obsW.value = v "weightValue";
      } "weightObservation";
    } "weightAnswer";
  } "weightRule";
  qr.item as itI where ((linkId = 'immunisation') and answer.value.exists()) then {
    itI.answer first as aI then {
      aI ->  bundle.entry as entryI,  entryI.resource = create('Observation') as obsI then {
        qr ->  obsI,  entryI then BuildObsBase(qr, obsI, entryI) "immunisationBase";
        qr -> obsI then SetSocialHistoryCategory(qr, obsI) "immunisationCategory";
        qr -> obsI.code as code then {
          qr -> code.coding as coding then {
            qr -> coding.system = 'http://loinc.org' "codeSystem";
            qr -> coding.code = '11370-4' "codeCode";
            qr -> coding.display = 'Immunization status - Reported' "codeDisplay";
          } "codeCoding";
        } "immunisationCode";
        aI -> obsI then SetCodedValue(aI, obsI) "immunisationValue";
      } "immunisationObservation";
    } "immunisationAnswer";
  } "immunisationRule";
  qr.item as itF where ((linkId = 'feeding') and answer.value.exists()) then {
    itF.answer first as aF then {
      aF ->  bundle.entry as entryF,  entryF.resource = create('Observation') as obsF then {
        qr ->  obsF,  entryF then BuildObsBase(qr, obsF, entryF) "feedingBase";
        qr -> obsF then SetSocialHistoryCategory(qr, obsF) "feedingCategory";
        // No LOINC concept for a free-text account of how a child is fed, so a
        // local code. See SGHIInpatientClinicalConceptCodeSystem.
        qr -> obsF.code as code then {
          qr -> code.coding as coding then {
            qr -> coding.system = 'https://fhir.slade360.co.ke/fhir/CodeSystem/inpatient-clinical-concept-codesystem' "codeSystem";
            qr -> coding.code = 'feeding' "codeCode";
            qr -> coding.display = 'Feeding' "codeDisplay";
          } "codeCoding";
          qr -> code.text = 'Feeding' "codeText";
        } "feedingCode";
        aF.value as v -> obsF.value = v "feedingValue";
      } "feedingObservation";
    } "feedingAnswer";
  } "feedingRule";
  // ── Guardian ───────────────────────────────────────────────────────────────
  // RelatedPerson has no element that points back at the response it came from,
  // so this resource is the one thing the map emits with no link home. A
  // Provenance would carry it, but that is a wider change than this form.
  qr.item as itG where ((linkId = 'guardianName') and answer.value.exists()) then {
    itG.answer first as aG then {
      aG ->  bundle.entry as entryG,  entryG.resource = create('RelatedPerson') as rp then {
        qr -> rp.id = uuid() then SetRelatedPersonFullUrl(rp, entryG) "idAndFullUrl";
        qr -> entryG.request as request then {
          qr -> request.method = 'POST' "requestMethod";
          qr -> request.url = 'RelatedPerson' "requestUrl";
        } "entryRequest";
        qr -> rp.active = true "active";
        qr.subject as s -> rp.patient = s "patient";
        aG.value as v -> rp.name as name then {
          v -> name.text = v "nameText";
        } "guardianName";
        // The relationship is written as the guardian described it on the day.
        // v3-RoleCode has concepts for mother and father, but the form accepts
        // anything from "Aunt" to "Neighbour", so matching free text against a
        // code system here would guess.
        qr.item as itR where ((linkId = 'guardianRel') and answer.value.exists()) then {
          itR.answer first as aR then {
            aR.value as rv -> rp.relationship as rel then {
              rv -> rel.text = rv "relationshipText";
            } "guardianRelationship";
          } "guardianRelAnswer";
        } "guardianRelRule";
      } "relatedPerson";
    } "guardianNameAnswer";
  } "guardianRule";
}

// ─────────────────────────────────────────────────────────────────────────────
// 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.
// ─────────────────────────────────────────────────────────────────────────────
// Source parameters are left untyped on purpose. Writing `: Element` makes the
// publisher's StructureMap validator compare QuestionnaireResponse.item against
// the literal 'Element' and report them as incompatible with each other, which is
// noise, not a finding. Untyped, it says only that it cannot check the paths.
group BuildObsBase(source qr : QR, target obs : Observation, target entry) {
  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";
  qr -> obs.derivedFrom = create('Reference') as ref then {
    qr.id as qid -> ref.reference = qid "derivedFromRef";
  } "linkQuestionnaireResponse";
}

// A coded answer becomes the Observation's value, coding and display intact, so
// the record says both what was picked and how it read on screen.
group SetCodedValue(source a, target obs : Observation) {
  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";
}

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 SetSocialHistoryCategory(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 = 'social-history' "categoryCode";
      qr -> coding.display = 'Social History' "categoryDisplay";
    } "categoryCoding";
  } "category";
}

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

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