Search FHIR

POC - Mapping CDA to FHIR
0.1.0 - ci-build France flag

POC - Mapping CDA to FHIR - Local Development build (v0.1.0) built by the FHIR (HL7® FHIR® Standard) Build Tools. See the Directory of published versions

StructureMap: Mapping CSE-MDE vers FHIR Bundle - Contexte Français

Official URL: https://interop.esante.gouv.fr/ig/fhir/mappingcdafhir/StructureMap/CdaFrMDEToBundle Version: 0.1.0
Draft as of 2025-11-07 Computable Name: CdaFrMDEToBundle

Mapping CSE-MDE vers FHIR Bundle - Contexte Français

map "https://interop.esante.gouv.fr/ig/fhir/mappingcdafhir/StructureMap/CdaFrMDEToBundle" = "CdaFrMDEToBundle"

// Mapping CSE-MDE vers FHIR Bundle - Contexte Français

uses "http://hl7.org/cda/stds/core/StructureDefinition/ClinicalDocument" alias ClinicalDocument as source
uses "http://hl7.org/fhir/StructureDefinition/Bundle" alias Bundle as target
uses "http://hl7.org/fhir/StructureDefinition/Composition" alias Composition as target
uses "http://hl7.org/fhir/StructureDefinition/Patient" alias Patient as target
uses "http://hl7.org/fhir/StructureDefinition/Observation" alias Observation as target

imports "https://interop.esante.gouv.fr/ig/fhir/mappingcdafhir/StructureMap/CdaToFHIRTypes"
imports "https://interop.esante.gouv.fr/ig/fhir/mappingcdafhir/StructureMap/CdaToBundle"
imports "https://interop.esante.gouv.fr/ig/fhir/mappingcdafhir/StructureMap/CdaFrToBundle"

// Entry point
group CdaFrMDEToBundle(source cda : ClinicalDocument, target bundle : Bundle) {
  cda ->  bundle.entry as e,  e.resource = create('Composition') as composition,  composition.id = uuid() as uuid,  e.fullUrl = append('urn:uuid:', uuid),  bundle.entry as e2,  e2.resource = create('Patient') as patient,  patient.id = uuid() as uuid2,  e2.fullUrl = append('urn:uuid:', uuid2) then CdaFrMDEMapping(cda, patient, composition, bundle) "main";
}

// Main mapping group
group CdaFrMDEMapping(source cda : ClinicalDocument, target patient : Patient, target composition : Composition, target bundle : Bundle) {
  // Bundle metadata
  cda -> bundle.id = uuid() "bundleId";
  cda.id as cdaId -> bundle.identifier as identifier then {
    cdaId.root as root where cdaId.extension.exists() -> identifier.system = translate(root, '#oid2uri', 'uri') "system";
    cdaId.extension as extension -> identifier.value = extension "value";
    cdaId.root as root where cdaId.extension.empty() -> identifier.system = 'urn:ietf:rfc:3986' "systemOid";
    cdaId.root as root where cdaId.extension.empty() -> identifier.value = append('urn:oid:', root) "valueOid";
  } "bundleIdentifier";
  cda -> bundle.type = 'document' "bundleType";
  cda.effectiveTime as effectiveTime -> bundle.timestamp = create('instant') as timestamp then TSInstant(effectiveTime, timestamp) "bundleTimestamp";
  // Composition using CdaToBundle function (includes patient mapping)
  cda -> composition then ClinicalDocumentComposition(cda, composition, patient, bundle) "composition";
  // Add gender mapping after ClinicalDocumentComposition (which calls ClinicalDocumentPatientRole)
  cda.recordTarget as recordTarget then {
    recordTarget.patientRole as patientRole then {
      patientRole.patient as cdaPatient then {
        cdaPatient.administrativeGenderCode as gender then MapGender(gender, patient) "patientGender";
      } "cdaPatient";
    };
  } "recordTargetGender";
  // Process sections with observations
  cda.component as cdaComp then {
    cdaComp.structuredBody as body then {
      body.component as bodyComp then {
        bodyComp.section as section then {
          // Create composition section
          section -> composition.section as compSection then {
            section.title as sectionTitle -> compSection.title = (sectionTitle.xmlText) "sectionTitle";
            section.code as sectionCode -> compSection.code = create('CodeableConcept') as cc then CDCodeableConcept(sectionCode, cc) "sectionCode";
            section.text as sectionText -> compSection.text = sectionText "sectionText";
            // Process observations in organizers
            section.entry as entry then {
              entry.organizer as organizer then {
                organizer.component as orgComp then {
                  orgComp.observation as obs then {
                    obs ->  bundle.entry as obsEntry,  obsEntry.resource = create('Observation') as observation,  observation.id = uuid() as obsUuid,  obsEntry.fullUrl = append('urn:uuid:', obsUuid),  compSection.entry = create('Reference') as obsRef,  obsRef.reference = append('urn:uuid:', obsUuid) then ProcessObservation(cda, obs, observation, patient, composition) "processObs";
                  };
                } "orgComponent";
              };
            };
          } "compSection";
        };
      } "bodyComponent";
    };
  } "cdaComponent";
}

// Process individual observation using CdaToFHIRTypes functions
group ProcessObservation(source cda : ClinicalDocument, source obs, target observation : Observation, target patient : Patient, target composition : Composition) {
  // Meta profile - ANS profiles based on LOINC code
  obs.code as obsCode where code = '29463-7' -> observation.meta = create('Meta') as meta then {
    obsCode -> meta.profile = 'https://interop.esante.gouv.fr/ig/fhir/mesures/StructureDefinition/mesures-fr-observation-body-weight' "profileWeight";
  } "metaWeight";
  obs.code as obsCode where code = '8302-2' -> observation.meta = create('Meta') as meta then {
    obsCode -> meta.profile = 'https://interop.esante.gouv.fr/ig/fhir/mesures/StructureDefinition/mesures-fr-observation-bodyheight' "profileHeight";
  } "metaHeight";
  obs.code as obsCode where code = '8287-5' -> observation.meta = create('Meta') as meta then {
    obsCode -> meta.profile = 'https://interop.esante.gouv.fr/ig/fhir/mesures/StructureDefinition/mesures-observation-head-circumference' "profileHeadCirc";
  } "metaHeadCirc";
  // Status - map CDA "completed" to FHIR "final"
  obs.statusCode as statusCode where code = 'completed' -> observation.status = 'final' "statusCompleted";
  obs.statusCode as statusCode where code != 'completed' -> observation.status = create('code') as status then CSCode(statusCode, status) "statusOther";
  // Category - vital-signs for all observations in CSE-MDE
  obs -> observation.category = create('CodeableConcept') as category then {
    obs -> category.coding = create('Coding') as coding then {
      obs -> coding.system = 'http://terminology.hl7.org/CodeSystem/observation-category' "system";
      obs -> coding.code = 'vital-signs' "code";
    } "coding";
  } "category";
  // Code using CDCodeableConcept from CdaToFHIRTypes
  obs.code as code -> observation.code = create('CodeableConcept') as cc then CDCodeableConcept(code, cc) "obsCode";
  // EffectiveDateTime - use effectiveTime from observation
  obs.effectiveTime as effectiveTime -> observation.effective = create('dateTime') as dt then TSDateTime(effectiveTime, dt) "effectiveDateTime";
  // Value as Quantity using PQQuantity from CdaToFHIRTypes
  obs.value as value -> observation.value = create('Quantity') as qty then PQQuantity(value, qty) "obsValue";
  // Subject reference
  obs ->  observation.subject = create('Reference') as ref,  ref.reference = ('urn:uuid:' + %patient.id) "obsSubject";
}

// Map CDA v3 administrative gender to FHIR gender
group MapGender(source src, target patient : Patient) {
  src where code = 'M' -> patient.gender = 'male' "genderMale";
  src where code = 'F' -> patient.gender = 'female' "genderFemale";
  src where code = 'UN' -> patient.gender = 'other' "genderOther";
  src where code = 'UNK' -> patient.gender = 'unknown' "genderUnknown";
}