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: Blood Requisition Extraction

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

The requisition is the whole transfusion chain on one sheet, and it extracts as the chain: a ServiceRequest per product asked for, coded SNOMED 116859006 and carrying its own quantity and urgency, so two units of packed cells and one of platelets are two orders and not one; a Consent recording that the patient agreed; Observations for the recipient's ABO and Rh under LOINC 883-9 and 10331-7 and for each issued unit's group under the blood-product-unit codes 14578-9 and 14907-0, with the bag number as the specimen identity; the cross-match result per unit, interpreted abnormal when incompatible; and any transfusion reaction as an Observation coded SNOMED 82545002 and flagged abnormal. The two abnormal flags are the point: an incompatible cross-match and a reaction are the two findings on this form that have to be impossible to miss.

/// url = 'https://fhir.slade360.co.ke/fhir/StructureMap/ExtractBloodRequisition'
/// name = 'ExtractBloodRequisition'
/// title = 'Blood Requisition Extraction'
/// status = 'active'
/// description = 'The requisition is the whole transfusion chain on one sheet, and it extracts as the chain: a ServiceRequest per product asked for, coded SNOMED 116859006 and carrying its own quantity and urgency, so two units of packed cells and one of platelets are two orders and not one; a Consent recording that the patient agreed; Observations for the recipient\'s ABO and Rh under LOINC 883-9 and 10331-7 and for each issued unit\'s group under the blood-product-unit codes 14578-9 and 14907-0, with the bag number as the specimen identity; the cross-match result per unit, interpreted abnormal when incompatible; and any transfusion reaction as an Observation coded SNOMED 82545002 and flagged abnormal. The two abnormal flags are the point: an incompatible cross-match and a reaction are the two findings on this form that have to be impossible to miss.'

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

group ExtractBloodRequisition(source qr : QR, target bundle : Bundle) {
  qr -> bundle.type = 'transaction' "setBundleType";
  // ── One order per product ───────────────────────────────────────────────
  // Per product row and not one order for the sheet. The units, the urgency
  // and the date required differ by product, and a single order could only
  // carry one set of them.
  qr.item as p where ((linkId = 'product') and item.where(linkId = 'product/type').answer.value.exists()) ->  bundle.entry as entry,  entry.resource = create('ServiceRequest') as sr then {
    qr ->  sr,  entry then BuildServiceRequestBase(qr, sr, entry) "base";
    p -> sr.status = 'active' "status";
    p -> sr.intent = 'order' "intent";
    qr.encounter as e -> sr.encounter = e;
    qr.authored as t -> sr.authoredOn = t "authoredOn";
    p -> sr.category as cat then {
      p -> cat.coding as cg then {
        p -> cg.system = 'http://snomed.info/sct' "sys";
        p -> cg.code = '116859006' "cd";
        p -> cg.display = 'Transfusion of blood product' "dsp";
      } "coding";
    } "category";
    // The product's own coding travels, so a request for fresh frozen plasma
    // is coded as that and not as "blood".
    p.item as it where (linkId = 'product/type') then {
      it.answer first as a then {
        a.value as cv ->  sr.code as cr,  cr.concept as cc then {
          cv -> cc.coding = cv "coding";
          cv.display as d -> cc.text = d "text";
        } "codeFromAnswer";
      };
    } "productTypeRule";
    // Units as the order quantity. A SimpleQuantity, and the unit is 'unit'
    // in the sense the blood bank means -- one bag -- not a UCUM measure.
    p.item as it where ((linkId = 'product/units') and answer.value.exists()) then {
      it.answer first as a then {
        a.value as v -> sr.quantity = create('Quantity') as q then {
          v -> q.value = v "qValue";
          v -> q.unit = 'unit of blood product' "qUnit";
        } "quantity";
      };
    } "unitsRule";
    p.item as it where ((linkId = 'product/date-required') and answer.value.exists()) then {
      it.answer first as a then {
        a.value as v -> sr.occurrence = cast(v, 'dateTime') "occurrence";
      };
    } "dateRequiredRule";
    qr -> sr then SetBloodRequestCommon(qr, sr) "common";
  } "productRequest";
  // ── Consent to transfusion ──────────────────────────────────────────────
  // decision follows the answer: 'permit' when the patient agreed, 'deny'
  // when they did not. A refused transfusion recorded as an absent consent
  // would be indistinguishable from one nobody asked about.
  qr.item as g where (linkId = 'consent') then {
    g.item as it where ((linkId = 'consent/given') and answer.value.exists()) ->  bundle.entry as entry,  entry.resource = create('Consent') as con then {
      qr ->  con,  entry then BuildConsentBase(qr, con, entry) "base";
      it -> con.status = 'active' "status";
      it -> con.category as cat then {
        it -> cat.coding as cg then {
          it -> cg.system = 'http://loinc.org' "sys";
          it -> cg.code = '64293-4' "cd";
          it -> cg.display = 'Procedure consent Document' "dsp";
        } "coding";
      } "category";
      qr.authored as t ->  con.period as per,  per.start = t "periodStart";
      it.answer first as a then {
        a.value as cv where (code = 'Y') -> con.decision = 'permit' "permit";
      } "permitRule";
      it.answer first as a then {
        a.value as cv where (code = 'N') -> con.decision = 'deny' "deny";
      } "denyRule";
      it -> con.provision as prov then {
        it -> prov.code as pc then {
          it -> pc.coding as cg then {
            it -> cg.system = 'http://snomed.info/sct' "sys";
            it -> cg.code = '116859006' "cd";
            it -> cg.display = 'Transfusion of blood product' "dsp";
          } "coding";
          it -> pc.text = 'Transfusion of the blood products requested on this form' "provText";
        } "provCode";
      } "provision";
    } "transfusionConsent";
  } "consentGroup";
  qr.item as it where ((linkId = 'reason-for-transfusion') 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 = 'transfusion-indication-for-transfusion' "cd";
        it -> cg.display = 'Indication for transfusion' "dsp";
      } "coding";
    } "code";
    it.answer first as a then {
      a.value as txt ->  obs.note as n,  n.text = txt "text";
    };
  } "narr_transfusion_indication_for_transfusion";
  // ── The recipient's group ───────────────────────────────────────────────
  qr.item as g where (linkId = 'recipient-group') then {
    g.item as it where ((linkId = 'recipient-group/abo') 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 = '883-9' "cd";
          it -> cg.display = 'ABO group [Type] in Blood' "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_883_9";
    g.item as it where ((linkId = 'recipient-group/rhesus') 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 = '10331-7' "cd";
          it -> cg.display = 'Rh [Type] in Blood' "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_10331_7";
  } "recipientGroupGroup";
  // ── Each issued unit ────────────────────────────────────────────────────
  // Per bag. The bag number is the identity a look-back investigation starts
  // from, so it rides on every Observation about that unit rather than only on
  // the row -- an Observation that says "incompatible" and cannot say which bag
  // is not usable evidence.
  qr.item as u where ((linkId = 'issued-unit') and item.where(linkId = 'issued-unit/bag-number').answer.value.exists()) then {
    u.item as it where ((linkId = 'issued-unit/donor-abo') 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 = '14578-9' "cd";
          it -> cg.display = 'ABO group [Type] in Blood from Blood product unit' "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";
      };
      u -> obs then SetUnitIdentity(u, obs) "bagIdentity";
    } "unit_donor_abo";
    u.item as it where ((linkId = 'issued-unit/donor-rhesus') 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 = '14907-0' "cd";
          it -> cg.display = 'Rh [Type] in Blood from Blood product unit' "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";
      };
      u -> obs then SetUnitIdentity(u, obs) "bagIdentity";
    } "unit_donor_rhesus";
    // ── The cross-match result ────────────────────────────────────────────
    // Interpreted, not just recorded. An incompatible cross-match is the one
    // result on this sheet that must stop a transfusion, and leaving a reader
    // to notice the word is not good enough.
    u.item as it where ((linkId = 'issued-unit/cross-match-result') 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 = 'https://fhir.slade360.co.ke/fhir/CodeSystem/concept-codesystem' "sys";
          it -> cg.code = 'transfusion-cross-match-result' "cd";
          it -> cg.display = 'Cross-match result' "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";
      };
      it.answer first as a then {
        a.value as cv where (code = 'xmatch-incompatible') -> obs.interpretation as interp then {
          cv -> interp.coding as icg then {
            cv -> icg.system = 'http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation' "sys";
            cv -> icg.code = 'A' "cd";
            cv -> icg.display = 'Abnormal' "dsp";
          } "coding";
        } "incompatibleInterp";
      } "interpretationRule";
      u -> obs then SetUnitIdentity(u, obs) "bagIdentity";
      u.item as cb where ((linkId = 'issued-unit/collected-by') and answer.value.exists()) then {
        cb.answer first as a then {
          a.value as ref -> obs.performer = ref "collectedBy";
        };
      } "collectedByRule";
      qr.item as lab where (linkId = 'laboratory') then {
        lab.item as xb where ((linkId = 'laboratory/cross-match-done-by') and answer.value.exists()) then {
          xb.answer first as a then {
            a.value as ref -> obs.performer = ref "crossMatchedBy";
          };
        } "doneByRule";
        lab.item as xa where ((linkId = 'laboratory/cross-match-done-at') and answer.value.exists()) then {
          xa.answer first as a then {
            a.value as v -> obs.effective = v "crossMatchedAt";
          };
        } "doneAtRule";
      } "labGroup";
    } "crossMatchObservation";
  } "issuedUnitGroup";
  // ── A transfusion reaction ──────────────────────────────────────────────
  // Flagged abnormal whenever one was observed. This is a haemovigilance
  // record: it has to be findable by anyone querying reactions across the
  // hospital, not only by whoever opens this form.
  qr.item as g where (linkId = 'reaction') then {
    g.item as it where ((linkId = 'reaction/observed') 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 SetExamCategory(qr, obs) "category";
      it -> obs.code as cc then {
        it -> cc.coding as cg then {
          it -> cg.system = 'http://snomed.info/sct' "sys";
          it -> cg.code = '82545002' "cd";
          it -> cg.display = 'Blood transfusion reaction' "dsp";
        } "coding";
      } "code";
      it.answer first as a then {
        a.value as cv -> obs.value = create('CodeableConcept') as vc then {
          cv -> vc.coding = cv "valueCoding";
          cv.display as d -> vc.text = d "valueText";
        } "codedValue";
      };
      it.answer first as a then {
        a.value as cv where (code = 'Y') -> obs.interpretation as interp then {
          cv -> interp.coding as icg then {
            cv -> icg.system = 'http://terminology.hl7.org/CodeSystem/v3-ObservationInterpretation' "sys";
            cv -> icg.code = 'A' "cd";
            cv -> icg.display = 'Abnormal' "dsp";
          } "coding";
        } "reactionInterp";
      } "interpretationRule";
      g.item as oa where ((linkId = 'reaction/observed-at') and answer.value.exists()) then {
        oa.answer first as a then {
          a.value as v -> obs.effective = v "observedAt";
        };
      } "observedAtRule";
      g.item as ob where ((linkId = 'reaction/observed-by') and answer.value.exists()) then {
        ob.answer first as a then {
          a.value as ref -> obs.performer = ref "observedBy";
        };
      } "observedByRule";
      g.item as it where (linkId = 'reaction/description') then {
        it.answer first as a then {
          a.value as txt ->  obs.note as n,  n.text = txt "text";
        };
      } "note";
    } "reactionObservation";
  } "reactionGroup";
}

// The bag number, and how long the unit is good for, as identifiers and a note
// on every Observation about that unit. Factored out because it is identical
// for the ABO, the Rh and the cross-match, and a look-back investigation that
// finds two of the three tagged is a look-back that failed.
group SetUnitIdentity(source u, target obs : Observation) {
  u.item as it where ((linkId = 'issued-unit/bag-number') and answer.value.exists()) then {
    it.answer first as a then {
      a.value as txt -> obs.identifier as ident then {
        txt -> ident.system = 'https://fhir.slade360.co.ke/fhir/identifier/blood-bag-number' "identSystem";
        txt -> ident.value = txt "identValue";
      } "bagIdentifier";
      a.value as txt ->  obs.specimen = create('Reference') as sref,  sref.display = txt "specimenDisplay";
    };
  } "bagNumberRule";
  u.item as it where ((linkId = 'issued-unit/expiry-date') and answer.value.exists()) then {
    it.answer first as a then {
      a.value as v ->  obs.note as n,  n.text = 'Unit expiry date recorded on the requisition' "expiryNote";
    };
  } "expiryRule";
}

// Everything every product order on the sheet shares: who asked, from where,
// how urgently, and when the blood is reserved until.
group SetBloodRequestCommon(source qr : QR, target sr : ServiceRequest) {
  qr.item as g where (linkId = 'request') then {
    g.item as it where ((linkId = 'request/requester') and answer.value.exists()) then {
      it.answer first as a then {
        a.value as ref -> sr.requester = ref "requester";
      };
    } "requesterRule";
    // Priority is a code, not a Coding. The form's urgency codes are local --
    // desperate, urgent, elective, as printed -- so they are mapped onto the
    // three request-priority codes rather than passed through.
    g.item as it where ((linkId = 'request/urgency') and answer.value.exists()) then {
      it.answer first as a then {
        a.value as cv where (code = 'urgency-desperate') -> sr.priority = 'stat' "stat";
      } "desperateRule";
      it.answer first as a then {
        a.value as cv where (code = 'urgency-urgent') -> sr.priority = 'urgent' "urgent";
      } "urgentRule";
      it.answer first as a then {
        a.value as cv where (code = 'urgency-elective') -> sr.priority = 'routine' "routine";
      } "electiveRule";
    } "urgencyRule";
    g.item as it where ((linkId = 'request/department') and answer.value.exists()) then {
      it.answer first as a then {
        a.value as txt ->  sr.note as n,  n.text = txt "departmentNote";
      };
    } "departmentRule";
    g.item as it where ((linkId = 'request/room-number') and answer.value.exists()) then {
      it.answer first as a then {
        a.value as txt ->  sr.note as n,  n.text = txt "roomNote";
      };
    } "roomRule";
    g.item as it where ((linkId = 'request/bed-number') and answer.value.exists()) then {
      it.answer first as a then {
        a.value as txt ->  sr.note as n,  n.text = txt "bedNote";
      };
    } "bedRule";
  } "requestGroup";
  qr.item as lab where (linkId = 'laboratory') then {
    lab.item as it where ((linkId = 'laboratory/lab-registration-number') and answer.value.exists()) then {
      it.answer first as a then {
        a.value as txt -> sr.identifier as ident then {
          txt -> ident.system = 'https://fhir.slade360.co.ke/fhir/identifier/blood-bank-registration' "identSystem";
          txt -> ident.value = txt "identValue";
        } "labIdentifier";
      };
    } "labRegRule";
  } "labGroup";
}

// ─────────────────────────────────────────────────────────────────────────────
// 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 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 SetExamCategory(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 = 'exam' "categoryCode";
      qr -> coding.display = 'Exam' "categoryDisplay";
    } "categoryCoding";
  } "category";
}

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";
}

group BuildConsentBase(source qr : QR, target r : Consent, target entry) {
  qr -> r then SetResponseProvenance(qr, r) "responseProvenance";
  qr -> r.id = uuid() then SetConsentFullUrl(r, entry) "idAndFullUrl";
  qr -> entry.request as request then {
    qr -> request.method = 'POST' "requestMethod";
    qr -> request.url = 'Consent' "requestUrl";
  } "entryRequest";
  qr.subject as s -> r.subject = s;
}

group SetConsentFullUrl(source r : Consent, target entry) {
  r.id as id -> entry.fullUrl = append('https://fhir.slade360.co.ke/fhir/Consent/', 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 = 'blood-requisition' "provenanceCode";
      qr -> t.display = 'Blood requisition form' "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";
}