Data Management with FHIR
0.1.0 - ci-build France flag

Data Management with FHIR - Local Development build (v0.1.0) built by the FHIR (HL7® FHIR® Standard) Build Tools. See the Directory of published versions

StructureMap: Transforms EHR logical model data to FHIR Semantic Layer resources using Bundle as container

Official URL: https://interop.aphp.fr/ig/fhir/dm/StructureMap/EHR2FSL Version: 0.1.0
Draft as of 2025-10-23 Computable Name: EHR2FSL

Transforms EHR logical model data to FHIR Semantic Layer resources using Bundle as container

map "https://interop.aphp.fr/ig/fhir/dm/StructureMap/EHR2FSL" = "EHR2FSL"

// Transforms EHR logical model data to FHIR Semantic Layer resources using Bundle as container

uses "https://interop.aphp.fr/ig/fhir/dm/StructureDefinition/ehr" alias EHR as source
uses "http://hl7.org/fhir/StructureDefinition/Bundle" alias Bundle as target

group EHR2FSL(source src : EHR, target bundle : Bundle) {
  // Initialize Bundle
  src -> bundle.id = uuid() "bundleid";
  src -> bundle.type = 'collection' "bundletype";
  src -> (now()) as timestamp then {
    src -> bundle.timestamp = timestamp "affectTimestamp";
  } "setTimestamp";
  // Transform Patient
  src.patient as patient -> bundle.entry as patientEntry then {
    patient -> patientEntry.resource = create('Patient') as dmPatient then {
      patient then TransformPatient(patient, dmPatient) "transformpatient";
      patient then setEntryRequestAndFullUrl(dmPatient, patientEntry) "setRequestAndFullUrl";
      src.patientAdresse as patientAdresse where patientAdresse.patientId = %patient.patientId then setAddress(patientAdresse, dmPatient) "transformpatientAdresse";
      // Transform PMSI Encounters
      src.donneesPmsi as pmsi where pmsi.patientId = %patient.patientId -> bundle.entry as encounterEntry then {
        pmsi -> encounterEntry.resource = create('Encounter') as dmEncounter then {
          pmsi then TransformEncounter(pmsi, dmEncounter, dmPatient) "transformencounter";
          pmsi then setEntryRequestAndFullUrl(dmEncounter, encounterEntry) "setRequestAndFullUrl";
          // Transform Conditions (Diagnostics)
          src.diagnostics as diag where (diag.patientId = %patient.patientId) and (diag.pmsiId = %pmsi.pmsiId) -> bundle.entry as conditionEntry then {
            diag -> conditionEntry.resource = create('Condition') as dmCondition then {
              diag then TransformCondition(diag, dmCondition, dmPatient, dmEncounter) "transformcondition";
              diag then setEntryRequestAndFullUrl(dmCondition, conditionEntry) "setRequestAndFullUrl";
            } "createcondition";
          } "conditionentries";
          // Transform Procedures (Actes)
          src.actes as acte where (acte.patientId = %patient.patientId) and (acte.pmsiId = %pmsi.pmsiId) -> bundle.entry as procedureEntry then {
            acte -> procedureEntry.resource = create('Procedure') as dmProcedure then {
              acte then TransformProcedure(acte, dmProcedure, dmPatient, dmEncounter) "transformprocedure";
              acte then setEntryRequestAndFullUrl(dmProcedure, procedureEntry) "setRequestAndFullUrl";
            } "createprocedure";
          } "procedureentries";
        } "createencounter";
      } "encounterentries";
      // Transform Laboratory Observations
      src.biologie as lab where lab.patientId = %patient.patientId -> bundle.entry as labEntry then {
        lab -> labEntry.resource = create('Observation') as dmLab then {
          lab then TransformLabObservation(lab, dmLab, dmPatient) "transformLabObs";
          lab then setEntryRequestAndFullUrl(dmLab, labEntry) "setRequestAndFullUrl";
        } "createlabobs";
      } "labentries";
      // Transform Medication Requests
      src.prescription as pres where pres.patientId = %patient.patientId -> bundle.entry as medReqEntry then {
        pres -> medReqEntry.resource = create('MedicationRequest') as dmMedReq then {
          src then TransformMedicationRequest(pres, dmMedReq, dmPatient) "createmedrequest";
          pres then setEntryRequestAndFullUrl(dmMedReq, medReqEntry) "setRequestAndFullUrl";
          src.posologie as poso where %poso.prescriptionId = %pres.prescriptionId then setPoso(poso, pres, dmMedReq) "transformPoso";
          // Transform Administration (W prescription)
          src.administration as admin where (admin.patientId = %patient.patientId) and (admin.prescriptionId = pres.prescriptionId) -> bundle.entry as adminEntry then {
            admin -> adminEntry.resource = create('MedicationAdministration') as dmAdmin then {
              admin then TransformMedicationAdministrationWOrder(admin, dmAdmin, dmPatient, dmMedReq) "createmedadmin";
              admin then setEntryRequestAndFullUrl(dmAdmin, adminEntry) "setRequestAndFullUrl";
            } "createmedadmin";
          } "administrationentries";
        } "createmedreq";
      } "medicationentries";
      // Transform Lifestyle Observations - separate observation for each lifestyle element
      src.styleVie as lifestyle where lifestyle.patientId = %patient.patientId then {
        // Tobacco consumption observation
        lifestyle.consommationTabac as tobacco where tobacco.exists() -> bundle.entry as tobaccoEntry then {
          tobacco -> tobaccoEntry.resource = create('Observation') as tobaccoObs then {
            lifestyle then TransformTobaccoObservation(lifestyle, tobaccoObs, dmPatient) "createtobaccoobs";
            lifestyle then setEntryRequestAndFullUrl(tobaccoObs, tobaccoEntry) "setRequestAndFullUrl";
          } "createtobaccoobs";
        } "tobaccoentries";
        // Alcohol consumption observation
        lifestyle.consommationAlcool as alcohol where alcohol.exists() -> bundle.entry as alcoholEntry then {
          alcohol -> alcoholEntry.resource = create('Observation') as alcoholObs then {
            lifestyle then TransformAlcoholObservation(lifestyle, alcoholObs, dmPatient) "createalcoholobs";
            lifestyle then setEntryRequestAndFullUrl(alcoholObs, alcoholEntry) "setRequestAndFullUrl";
          } "createalcoholobs";
        } "alcoholentries";
        // Drug consumption observation
        lifestyle.consommationAutresDrogues as drugs where drugs.exists() -> bundle.entry as drugEntry then {
          drugs -> drugEntry.resource = create('Observation') as drugObs then {
            lifestyle then TransformDrugObservation(lifestyle, drugObs, dmPatient) "createdrugobs";
            lifestyle then setEntryRequestAndFullUrl(drugObs, drugEntry) "setRequestAndFullUrl";
          } "createdrugobs";
        } "drugentries";
        // Physical activity observation
        lifestyle.activitePhysique as physical where physical.exists() -> bundle.entry as physicalEntry then {
          physical -> physicalEntry.resource = create('Observation') as physicalObs then {
            lifestyle then TransformPhysicalActivityObservation(lifestyle, physicalObs, dmPatient) "createphysicalobs";
            lifestyle then setEntryRequestAndFullUrl(physicalObs, physicalEntry) "setRequestAndFullUrl";
          } "createphysicalobs";
        } "physicalentries";
      } "lifestyleentries";
      // Transform Care Observations (s)
      src.dossierSoins as soin where soin.patientId = %patient.patientId -> bundle.entry as vitalEntry then {
        soin -> vitalEntry.resource = create('Observation') as dmObs then {
          soin then TransformVitalSigns(soin, dmObs, dmPatient) "createmesureobs";
          soin then setEntryRequestAndFullUrl(dmObs, vitalEntry) "setRequestAndFullUrl";
        } "createmesureobs";
      } "vitalentries";
      // Transform Administration (WO prescription)
      src.administration as admin where (admin.patientId = %patient.patientId) and admin.prescriptionId.empty() -> bundle.entry as adminEntry then {
        admin -> adminEntry.resource = create('MedicationAdministration') as dmAdmin then {
          admin then TransformMedicationAdministrationWOOrder(admin, dmAdmin, dmPatient) "createmedadmin";
          admin then setEntryRequestAndFullUrl(dmAdmin, adminEntry) "setRequestAndFullUrl";
        } "createmedadmin";
      } "administrationentries";
    } "createpatient";
  } "patiententry";
}

// ---------------------------------------------------------
// PATIENT TRANSFORMATION
// ---------------------------------------------------------
group TransformPatient(source src, target tgt : Patient) {
  src -> tgt.id = uuid() "setId";
  // src.patientId as id -> tgt.id = id "patient-id";
  // Identifiers
  src.patientId as pid -> tgt.identifier as identifier then {
    pid -> identifier.use = 'usual' "iduse";
    pid -> identifier.type = cc('http://terminology.hl7.org/CodeSystem/v2-0203', 'PI', 'Patient Identifier') "idtype";
    pid -> identifier.system = 'https://hospital.eu/ehr/patient-id' "idsystem";
    pid -> identifier.value = pid "idvalue";
  } "patientidentifier";
  // NIR identifier <- C'est pas terrible côté expression de besoin : qu'est ce qu'ils veulent quand ils disent NIR ? ça peut être le NSS, mais du coup c'est pas un identifier (un NSS peut correspondre à plusieurs ayant droits), et ce serait plutôt une info qui irait côté claim. ça peut aussi être l'ins-nir, mais du coup qu'est ce qu'ils attendent dans l'ins ? Bref, à mon avis, faut discuter cette variable au niveau du GT.
  // src.nir as nir where nir.exists() -> tgt.identifier as insIdentifier then {     nir -> insIdentifier.use = 'official' "ins-use";     nir -> insIdentifier.type = cc('http://hl7.fr/fhir/CodeSystem/fr-v2-0203', 'INS-NIR') "ins-type";     nir -> insIdentifier.system = 'urn:oid:1.2.250.1.213.1.4.8' "ins-system";     nir -> insIdentifier.value = nir "ins-value";   } "ins-identifier";
  // Alternative INS from ins field
  src.ins as ins where ins.exists() -> tgt.identifier as insIdentifier then {
    ins -> insIdentifier.use = 'official' "insuse"; // On part du principe qu'on n'est pas sur un old.
    ins -> insIdentifier.type = cc('https://hl7.fr/ig/fhir/core/CodeSystem/fr-core-cs-v2-0203', 'INS-NIR', 'NIR définitif') "instype";
    ins -> insIdentifier.system = 'urn:oid:1.2.250.1.213.1.4.8' "inssystem";
    ins -> insIdentifier.value = ins "insvalue";
  } "insidentifier";
  // Name
  src where src.nom.exists() or src.prenom.exists() -> tgt.name as name then {
    src.nom as lastName -> name.family = lastName "familyname";
    src.prenom as firstName -> name.given = firstName "givenname";
    src -> name.use = 'official' "nameuse"; // hypothèse un peu forte.
  } "patientname";
  // Demographics On aurait pu faire plus élégant avec un ConceptMap
  src.dateNaissance as birthDate -> tgt.birthDate = birthDate "birthdate";
  src.sexe as gender where gender = 'h' -> tgt.gender = 'male' "gendermale";
  src.sexe as gender where gender = 'f' -> tgt.gender = 'female' "genderfemale";
  src.sexe as gender where gender.exists() and (gender != 'h') and (gender != 'f') -> tgt.gender = 'unknown' "genderunknown"; // n'a pas grand sens en l'état des contraintes SQL
  // Death information
  src.dateDeces as deathDate where deathDate.exists() -> tgt.deceased = cast(deathDate, 'dateTime') as deceasedDate then {
    src.sourceDeces as deathSource where deathSource.exists() -> deceasedDate.extension as DeathSourceExtension then {
      deathSource -> DeathSourceExtension.url = 'https://interop.aphp.fr/ig/fhir/dm/StructureDefinition/DeathSource' "deathSourceUrl";
      deathSource -> DeathSourceExtension.value = cast(deathSource, 'code') "deathSourceValue";
    } "deathSource";
  } "deathDate";
  // Multiple birth
  src.rangGemellaire as twin where twin.exists() -> tgt.multipleBirth = twin "multiplebirth";
}

// ========================================================================
// PATIENT_LOCATION TRANSFORMATION
// ========================================================================
group setAddress(source src, target tgtPat : Patient) {
  src where src.latitude.exists() or src.longitude.exists() or src.codeIris.exists() or src.libelleIris.exists() or src.codeGeographiqueResidence.exists() then {
    src -> tgtPat.address as newAddress then {
      src where src.latitude.exists() or src.longitude.exists() ->  newAddress.extension as geolocationExtension,  geolocationExtension.url = 'http://hl7.org/fhir/StructureDefinition/geolocation' then {
        src.latitude as srcLat ->  geolocationExtension.extension as tgtLat,  tgtLat.url = 'latitude',  tgtLat.value = srcLat "setLat";
        src.longitude as srcLong ->  geolocationExtension.extension as tgtLong,  tgtLong.url = 'longitude',  tgtLong.value = srcLong "setLong";
      } "setCoordinates";
      src where src.codeIris.exists() or src.libelleIris.exists() ->  newAddress.line = (%src.libelleIris) as irisLine,  irisLine.extension = create('Extension') as irisExtension,  irisExtension.url = 'http://hl7.org/fhir/StructureDefinition/iso21090-ADXP-censusTract',  irisExtension.value = (%src.codeIris) "setIris";
      // src where src.codeIris.exists() or src.libelleIris.exists() -> newAddress.extension as irisExtension,
      // irisExtension.url = 'http://hl7.org/fhir/StructureDefinition/iso21090-ADXP-censusTract',
      // irisExtension.value = (iif(%src.codeIris.exists() and %src.libelleIris.exists(), %src.codeIris & ' - ' & %src.libelleIris, %src.codeIris & %src.libelleIris)) "setIris";
      src.codeGeographiqueResidence as srcCodeGeographiqueResidence where src.codeGeographiqueResidence.exists() ->  newAddress.extension as residencePmsiExtension,  residencePmsiExtension.url = 'https://interop.aphp.fr/ig/fhir/dm/StructureDefinition/PmsiCodeGeo',  residencePmsiExtension.value = cast(srcCodeGeographiqueResidence, 'code') "SetResidencePmsi";
      src.dateRecueil as dateRecueil ->  newAddress.period as newAddressPeriod,  newAddressPeriod.start = dateRecueil "setDateRecueil";
    } "createAddress";
  } "checkSource";
}

// ========================================================================
// ENCOUNTER TRANSFORMATION
// ========================================================================
group TransformEncounter(source src, target tgtEnc : Encounter, source patient) {
  src -> tgtEnc.id = uuid() "setId";
  // src.pmsiId as id -> tgt.id = id "encounter-id";
  // Identifier
  src -> tgtEnc.identifier as identifier then {
    src -> identifier.type = cc('https://hl7.fr/ig/fhir/core/CodeSystem/fr-core-cs-identifier-type', 'VN', 'Visit Number') "idtype";
    src -> identifier.system = 'https://hospital.eu/ehr/pmsi-id' "encounteridsystem";
    src.pmsiId as srcEncoutnerId -> identifier.value = srcEncoutnerId "encounteridvalue";
  } "encounteridentifier";
  // Status - default to finished for historical data
  src -> tgtEnc.status = 'finished' "encounterstatus";
  // Class - assume inpatient for PMSI data
  src -> tgtEnc.class = c('http://terminology.hl7.org/CodeSystem/v3-ActCode', 'IMP', 'inpatient encounter') "encounterclass";
  // Reference patient
  src -> tgtEnc.subject = create('Reference') as ref then {
    patient.id as patientId ->  ref.reference = ('Patient/' + %patientId),  ref.type = 'Patient' "subjectreference";
  } "encountersubject";
  // Period
  src where src.dateDebutSejour.exists() or src.dateFinSejour.exists() -> tgtEnc.period as period then {
    src.dateDebutSejour as startDate -> period.start = startDate "periodstart";
    src.dateFinSejour as endDate -> period.end = endDate "periodend";
  } "encounterperiod";
  // Désactivé dans le modèle physique   // Duration   src.dureeSejour as duration -> tgtEnc.length as length then {     duration -> length.value = duration "length-value";     duration -> length.unit = 'd' "length-unit";     duration -> length.system = 'http://unitsofmeasure.org' "length-system";     duration -> length.code = 'd' "length-code";   } "encounter-length";
  // Hospitalization
  src where src.modeEntree.exists() or src.modeSortie.exists() -> tgtEnc.hospitalization as hosp then {
    src.modeEntree as admitSource -> hosp.admitSource as admitSrc then {
      admitSource -> admitSrc.text = admitSource "admitsourcetext";
    } "admitsource";
    src.modeSortie as dischargeDisp -> hosp.dischargeDisposition as dischargeDsp then {
      dischargeDisp -> dischargeDsp.text = dischargeDisp "dischargedisptext";
    } "dischargedisposition";
  } "encounterhospitalization";
  // Service Provider
  src where src.uniteFonctionnelle.exists() or src.service.exists() or src.etablissement.exists() -> tgtEnc.serviceProvider as provider then {
    src -> provider.display = (iif(%src.uniteFonctionnelle.exists(), %src.uniteFonctionnelle, iif(%src.service.exists(), %src.service, %src.etablissement))) "providerdisplay";
  } "provider";
// // Location - On n'a pas de location à gérer à ce stade (établissement, service et UF, halluciné par l'IA, sont des Organization que j'ai du coup mi dans Provider. )   src.service as service -> tgt.location as location then {     service -> location.location as loc then {       service -> loc.display = service "location-display";     } "location-ref";   } "encounter-location";
}

// ========================================================================
// CONDITION TRANSFORMATION
// ========================================================================
group TransformCondition(source src, target tgtCond : Condition, source patient, source encounter) {
  src -> tgtCond.id = uuid() "setId";
  // Identifier
  src.diagnosticId as diagId -> tgtCond.identifier as identifier then {
    diagId -> identifier.system = 'https://hospital.eu/ehr/diagnostic-id' "conditionidsystem";
    diagId -> identifier.value = diagId "conditionidvalue";
  } "conditionidentifier";
  // Category
  // Représentation des codes diagnostics du PMSI dans condition , plusieurs options :
  // - ce sont des infos de claim, pas de condition
  // - utiliser la catégorie 'encounter-diagnosis'
  // - utiliser les catégories du PMSI : DP, DR, DAS, DAD
  // - mettre les deux précédent <- j'ai fait ça.
  src -> tgtCond.category = cc('http://terminology.hl7.org/CodeSystem/condition-category', 'encounter-diagnosis', 'Encounter Diagnosis') "categoryencounterDiag";
  src.typeDiagnostic as diagType -> tgtCond.category as category then {
    diagType -> category.text = diagType "categorytext";
  } "conditioncategory";
  // Code (ICD-10)
  src.codeDiagnostic as srcCode -> tgtCond.code as conditionCode then {
    // la fonction cc ne peuple pas le display...
    src.libelleDiagnostic as label ->  conditionCode.coding as tgtCoding,  tgtCoding.system = 'http://hl7.org/fhir/sid/icd-10',  tgtCoding.code = srcCode "conditioncode";
    src.libelleDiagnostic as text -> conditionCode.text = text "codetext";
  } "varcode";
  // Subject
  src -> tgtCond.subject = create('Reference') as ref then {
    patient.id as patientId ->  ref.reference = ('Patient/' + %patientId),  ref.type = 'Patient' "subjectref";
  } "conditionsubject";
  // Encounter reference
  src -> tgtCond.encounter = create('Reference') as ref then {
    encounter.id as encounterId ->  ref.reference = ('Encounter/' + %encounterId),  ref.type = 'Encounter' "encounterref";
  } "conditionencounter";
  // Recorded date
  src.dateRecueil as entryDate -> tgtCond.recordedDate = entryDate "conditionrecordeddate";
  // Clinical status - assume active for recorded diagnoses
  src -> tgtCond.clinicalStatus = cc('http://terminology.hl7.org/CodeSystem/condition-clinical', 'active') "conditionclinicalstatus";
  // Verification status - assume confirmed for coded diagnoses
  src -> tgtCond.verificationStatus = cc('http://terminology.hl7.org/CodeSystem/condition-ver-status', 'confirmed') "conditionverificationstatus";
}

// ========================================================================
// PROCEDURE TRANSFORMATION
// ========================================================================
group TransformProcedure(source src, target tgtProc : Procedure, source patient, source encounter) {
  src -> tgtProc.id = uuid() "setId";
  // Identifier
  src.acteId as acteId -> tgtProc.identifier as identifier then {
    acteId -> identifier.system = 'https://hospital.eu/ehr/acte-id' "procedureidsystem";
    acteId -> identifier.value = acteId "procedureidvalue";
  } "procedureidentifier";
  // Status - assume completed for historical data
  src -> tgtProc.status = 'completed' "procedurestatus";
  // Code (CCAM)
  src.codeActe as code -> tgtProc.code as procedureCode then {
    code ->  procedureCode.coding as coding,  coding.system = 'https://interop.aphp.fr/ig/fhir/dm/CodeSystem/Ccam',  coding.code = code "procedurecoding";
    src.libelleActe as text -> procedureCode.text = text "codetext";
  } "procedurecode";
  // Subject
  src -> tgtProc.subject = create('Reference') as ref then {
    patient.id as patientId ->  ref.reference = ('Patient/' + %patientId),  ref.type = 'Patient' "subjectref";
  } "proceduresubject";
  // Encounter reference
  src -> tgtProc.encounter = create('Reference') as ref then {
    encounter.id as encounterId ->  ref.reference = ('Encounter/' + %encounterId),  ref.type = 'Encounter' "encounterref";
  } "procedureencounter";
  // Performed date/time
  src.dateActe as performedDate where %performedDate.exists() -> tgtProc.performed = performedDate "procedureperformed";
  // Performer
  src.executant as performer -> tgtProc.performer as perf then {
    performer -> perf.actor as actor then {
      performer -> actor.display = performer "performerdisplay";
      performer -> actor.type = 'Practitioner' "performerType";
    } "performeractor";
    performer ->  perf.function as function,  function.text = 'Exécutant' "performerFunction";
  } "procedureperformer";
}

// ========================================================================
// LABORATORY OBSERVATION TRANSFORMATION
// ========================================================================
group TransformLabObservation(source src, target tgtObs : Observation, source patient) {
  src -> tgtObs.id = uuid() "setId";
  // Identifier
  src.biologieId as bioId -> tgtObs.identifier as identifier then {
    bioId -> identifier.system = 'https://hospital.eu/ehr/biologie-id' "labidsystem";
    bioId -> identifier.value = bioId "labidvalue";
  } "labidentifier";
  // Status - map validation status or default to final
  src.statutValidation as validation where validation = 'VALIDE' -> tgtObs.status = 'final' "statusvalidated";
  src where src.statutValidation.exists().not() -> tgtObs.status = 'final' "statusdefault";
  // Category
  src -> tgtObs.category as category then {
    src -> category.coding as coding then {
      src -> coding.system = 'http://terminology.hl7.org/CodeSystem/observation-category' "categorysystem";
      src -> coding.code = 'laboratory' "categorycode";
      src -> coding.display = 'Laboratory' "categorydisplay";
    } "categorycoding";
  } "labcategory";
  // Code (LOINC)
  src.codeLoinc as loinc -> tgtObs.code as code then {
    loinc -> code.coding as coding then {
      loinc -> coding.system = 'http://loinc.org' "codesystem";
      loinc -> coding.code = loinc "codevalue";
    } "labcoding";
    src.libelleTest as text -> code.text = text "codetext";
  } "labcode";
  // Subject
  src -> tgtObs.subject = create('Reference') as ref then {
    patient.id as patientId ->  ref.reference = ('Patient/' + %patientId),  ref.type = 'Patient' "subjectref";
  } "labsubject";
  // Effective date/time
  src.datePrelevement as collectionDate where %collectionDate.exists() -> tgtObs.effective = cast(collectionDate, 'dateTime') "labeffective";
  // Value (numeric or text)
  src.valeur as numValue where numValue.exists() -> tgtObs.value = create('Quantity') as qty then {
    numValue -> qty.value = numValue "quantityvalue";
    src.unite as unit -> qty.unit = unit "quantityunit";
    src.unite as unit -> qty.code = unit "quantitycode";
    src -> qty.system = 'http://unitsofmeasure.org' "quantitysystem";
  } "labvaluequantity";
  // On n'a pas de résultats textuels de prévu
  // src.valeurTexte as textValue where textValue.exists() and src.valeur.exists().not() -> tgtObs.value = textValue "lab-value-string";
  // Reference range
  src where src.borneInfNormale.exists() or src.borneSupNormale.exists() -> tgtObs.referenceRange as range then {
    src.borneInfNormale as low -> range.low as lowQty then {
      low -> lowQty.value = low "reflowvalue";
      src.unite as unit -> lowQty.unit = unit "reflowunit";
      src.unite as unit -> lowQty.code = unit "reflowcode";
      src -> lowQty.system = 'http://unitsofmeasure.org' "reflowsystem";
    } "referencelow";
    src.borneSupNormale as high -> range.high as highQty then {
      high -> highQty.value = high "refhighvalue";
      src.unite as unit -> highQty.unit = unit "refhighunit";
      src.unite as unit -> highQty.code = unit "refhighcode";
      src -> highQty.system = 'http://unitsofmeasure.org' "refhighsystem";
    } "referencehigh";
  } "labreferencerange";
  // Performer
  src.laboratoire as lab -> tgtObs.performer as performer then {
    lab -> performer.display = lab "performerdisplay";
    lab -> performer.type = 'Organization' "performerType";
  } "labperformer";
}

// ========================================================================
// MEDICATION REQUEST TRANSFORMATION
// ========================================================================
group TransformMedicationRequest(source src, target tgtMedReq : MedicationRequest, source patient) {
  src -> tgtMedReq.id = uuid() "setId";
  // Identifier
  src.prescriptionId as presId -> tgtMedReq.identifier as identifier then {
    presId -> identifier.system = 'https://hospital.eu/ehr/prescription-id' "medrequestidsystem";
    presId -> identifier.value = presId "medrequestidvalue";
  } "medrequestidentifier";
  // Status - assume unknown for historical prescriptions
  src -> tgtMedReq.status = 'unknown' "medrequeststatus";
  // Intent
  src -> tgtMedReq.intent = 'order' "medrequestintent";
  // Subject
  src -> tgtMedReq.subject = create('Reference') as ref then {
    patient.id as patientId ->  ref.reference = ('Patient/' + %patientId),  ref.type = 'Patient' "subjectref";
  } "medrequestsubject";
  // Medication
  src.denomination as denomination -> tgtMedReq.medication = create('CodeableConcept') as medication then {
    denomination as text -> medication.text = text "medicationtext";
    src.codeAtc as atc -> medication.coding as coding then {
      atc -> coding.system = 'http://www.whocc.no/atc' "atcsystem";
      atc -> coding.code = atc "atccode";
    } "atccoding";
  } "medicationdenomination";
  // Authored date
  src.datePrescription as prescDate -> tgtMedReq.authoredOn = prescDate "medauthored";
  // Requester
  src.prescripteur as prescriber -> tgtMedReq.requester as requesterRef then {
    prescriber -> requesterRef.display = prescriber "requesterdisplay";
    prescriber -> requesterRef.type = 'Practitioner' "requesterType";
  } "medrequester";
}

// ========================================================================
// POSOLOGIE TRANSFORMATION
// ========================================================================
group setPoso(source srcPoso, source srcPres, target tgtMedReq) {
  srcPres -> tgtMedReq.dosageInstruction as dosageInstruction then {
    // Human-readable dosage text - construct from available fields
    srcPoso -> dosageInstruction.text = ('Prendre' + iif(%srcPoso.quantite.exists(), ' ' + %srcPoso.quantite.toString(), '1') + iif(%srcPoso.uniteQuantite.exists(), ' ' + %srcPoso.uniteQuantite, ' comprimé') + iif(%srcPoso.nombrePrisesParJour.exists(), ' ' + %srcPoso.nombrePrisesParJour.toString() + ' fois par jour', ' selon prescription')) "dosagetext";
    // Route of administration from prescription
    srcPres.voieAdministration as route -> dosageInstruction.route as routeCC then {
      route -> routeCC.coding as routeCoding then {
        route -> routeCoding.code = route "routecode";
        route -> routeCoding.system = 'https://smt.esante.gouv.fr/terminologie-standardterms' "routesystem";
      } "routecoding";
    } "dosageroute";
    // Timing
    srcPoso -> dosageInstruction.timing as timing then {
      srcPoso -> timing.repeat as timingRepeat then {
        // Set prescription period as bounds if available
        srcPres where srcPres.dateDebutPrescription.exists() or srcPres.dateFinPrescription.exists() -> timingRepeat.bounds = create('Period') as medReqPeriod then {
          srcPres.dateDebutPrescription as startDate -> medReqPeriod.start = startDate "periodstart";
          srcPres.dateFinPrescription as endDate -> medReqPeriod.end = endDate "periodend";
        } "prescriptionperiod";
        // Frequency from posology
        srcPoso.nombrePrisesParJour as frequency -> timingRepeat.frequency = frequency "timingfrequency";
        srcPoso.nombrePrisesParJour as frequency -> timingRepeat.period = '1' "timingperiod";
        srcPoso.nombrePrisesParJour as frequency -> timingRepeat.periodUnit = 'd' "timingperiodunit";
      } "timingrepeat";
    } "dosagetiming";
    // Dose and rate
    srcPoso.quantite as quantity -> dosageInstruction.doseAndRate as doseRate then {
      quantity -> doseRate.dose = create('Quantity') as doseQuantity then {
        quantity -> doseQuantity.value = quantity "dosevalue";
        srcPoso.uniteQuantite as unit -> doseQuantity.unit = unit "doseunit";
        srcPoso.uniteQuantite as unit -> doseQuantity.code = unit "dosecode";
        srcPoso -> doseQuantity.system = 'http://unitsofmeasure.org' "dosesystem";
      } "dosequantity";
    } "doseandrate";
  } "dosageinstruction";
}

// ========================================================================
// VITAL SIGNS TRANSFORMATION
// ========================================================================
group TransformVitalSigns(source src, target tgtObs : Observation, source patient) {
  src -> tgtObs.id = uuid() "setId";
  // Identifier
  src.soinId as soinId -> tgtObs.identifier as identifier then {
    soinId -> identifier.system = 'https://hospital.eu/ehr/soin-id' "soinidsystem";
    soinId -> identifier.value = soinId "soinidvalue";
  } "medrequestidentifier";
  // Status - assume final
  src -> tgtObs.status = 'final' "status";
  // Category - vital-signs
  src -> tgtObs.category as category then {
    src -> category.coding as coding then {
      src -> coding.system = 'http://terminology.hl7.org/CodeSystem/observation-category' "categorysystem";
      src -> coding.code = 'vital-signs' "categorycode";
    } "categorycoding";
  } "category";
  // code (LOINC)
  src -> tgtObs.code as code then {
    src -> code.coding as coding then {
      src -> coding.system = 'http://loinc.org' "codesystem";
      src.codeLoinc as codeLoinc -> coding.code = codeLoinc "codevalue";
    } "coding";
    src.libelleTest as label -> code.text = label "codetext";
  } "code";
  // Subject
  src -> tgtObs.subject = create('Reference') as ref then {
    patient.id as patientId ->  ref.reference = ('Patient/' + %patientId),  ref.type = 'Patient' "subjectref";
  } "obssubject";
  // Date de l'observation
  src.dateMesure as date where %date.exists() -> tgtObs.effective = cast(date, 'dateTime') "effective";
  // Valeur (quantitiative)
  src -> tgtObs.value = create('Quantity') as qty then {
    src.valeur as mesure -> qty.value = mesure "value";
    src.unite as unit -> qty.unit = unit "unit";
    src.unite as unit -> qty.code = unit "code";
    src -> qty.system = 'http://unitsofmeasure.org' "system";
  } "quantity";
// Blood pressure observation - would need separate entry with components
}

// ========================================================================
// LIFESTYLE TRANSFORMATION - SEPARATE OBSERVATIONS
// ========================================================================
group TransformTobaccoObservation(source src, target tgtObs : Observation, source patient) {
  src -> tgtObs.id = uuid() "setId";
  // Identifier
  src.styleVieId as lsId -> tgtObs.identifier as identifier then {
    lsId -> identifier.system = 'https://hospital.eu/ehr/lifestyle-id' "lifestyleidsystem";
    lsId -> identifier.value = (%lsId & '-tobacco') "lifestyleidvalue";
  } "lifestyleidentifier";
  // Status
  src -> tgtObs.status = 'final' "lifestylestatus";
  // Category - social-history
  src -> tgtObs.category as category then {
    src -> category.coding as coding then {
      src -> coding.system = 'http://terminology.hl7.org/CodeSystem/observation-category' "categorysystem";
      src -> coding.code = 'social-history' "categorycode";
      src -> coding.display = 'Social History' "categorydisplay";
    } "categorycoding";
  } "lifestylecategory";
  // Code - tobacco use status
  src -> tgtObs.code as code then {
    src -> code.coding as coding then {
      src -> coding.system = 'http://loinc.org' "codesystem";
      src -> coding.code = '72166-2' "codevalue";
      src -> coding.display = 'Tobacco smoking status' "codedisplay";
    } "tobaccocoding";
  } "tobaccocode";
  // Subject
  src -> tgtObs.subject = create('Reference') as ref then {
    patient.id as patientId ->  ref.reference = ('Patient/' + %patientId),  ref.type = 'Patient' "subjectref";
  } "lifestylesubject";
  // Effective date
  src.dateRecueil as collectDate where %collectDate.exists() -> tgtObs.effective = cast(collectDate, 'dateTime') "lifestyleeffective";
  // Value
  src.consommationTabac as tobacco -> tgtObs.value = create('CodeableConcept') as valueCC then {
    tobacco -> valueCC.text = tobacco "tobaccovaluetext";
  } "tobaccovalue";
}

group TransformAlcoholObservation(source src, target tgtObs : Observation, source patient) {
  src -> tgtObs.id = uuid() "setId";
  // Identifier
  src.styleVieId as lsId -> tgtObs.identifier as identifier then {
    lsId -> identifier.system = 'https://hospital.eu/ehr/lifestyle-id' "lifestyleidsystem";
    lsId -> identifier.value = (%lsId & '-alcohol') "lifestyleidvalue";
  } "lifestyleidentifier";
  // Status
  src -> tgtObs.status = 'final' "lifestylestatus";
  // Category - social-history
  src -> tgtObs.category as category then {
    src -> category.coding as coding then {
      src -> coding.system = 'http://terminology.hl7.org/CodeSystem/observation-category' "categorysystem";
      src -> coding.code = 'social-history' "categorycode";
      src -> coding.display = 'Social History' "categorydisplay";
    } "categorycoding";
  } "lifestylecategory";
  // Code - alcohol use
  src -> tgtObs.code as code then {
    src -> code.coding as coding then {
      src -> coding.system = 'http://loinc.org' "codesystem";
      src -> coding.code = '11331-6' "codevalue";
      src -> coding.display = 'History of alcohol use' "codedisplay";
    } "alcoholcoding";
  } "alcoholcode";
  // Subject
  src -> tgtObs.subject = create('Reference') as ref then {
    patient.id as patientId ->  ref.reference = ('Patient/' + %patientId),  ref.type = 'Patient' "subjectref";
  } "lifestylesubject";
  // Effective date
  src.dateRecueil as collectDate where %collectDate.exists() -> tgtObs.effective = cast(collectDate, 'dateTime') "lifestyleeffective";
  // Value
  src.consommationAlcool as alcohol -> tgtObs.value = create('CodeableConcept') as valueCC then {
    alcohol -> valueCC.text = alcohol "alcoholvaluetext";
  } "alcoholvalue";
}

group TransformDrugObservation(source src, target tgtObs : Observation, source patient) {
  src -> tgtObs.id = uuid() "setId";
  // Identifier
  src.styleVieId as lsId -> tgtObs.identifier as identifier then {
    lsId -> identifier.system = 'https://hospital.eu/ehr/lifestyle-id' "lifestyleidsystem";
    lsId -> identifier.value = (%lsId & '-drugs') "lifestyleidvalue";
  } "lifestyleidentifier";
  // Status
  src -> tgtObs.status = 'final' "lifestylestatus";
  // Category - social-history
  src -> tgtObs.category as category then {
    src -> category.coding as coding then {
      src -> coding.system = 'http://terminology.hl7.org/CodeSystem/observation-category' "categorysystem";
      src -> coding.code = 'social-history' "categorycode";
      src -> coding.display = 'Social History' "categorydisplay";
    } "categorycoding";
  } "lifestylecategory";
  // Code - drug use
  src -> tgtObs.code as code then {
    src -> code.coding as coding then {
      src -> coding.system = 'http://loinc.org' "codesystem";
      src -> coding.code = '11342-3' "codevalue";
      src -> coding.display = 'History of drug use' "codedisplay";
    } "drugcoding";
  } "drugcode";
  // Subject
  src -> tgtObs.subject = create('Reference') as ref then {
    patient.id as patientId ->  ref.reference = ('Patient/' + %patientId),  ref.type = 'Patient' "subjectref";
  } "lifestylesubject";
  // Effective date
  src.dateRecueil as collectDate where %collectDate.exists() -> tgtObs.effective = cast(collectDate, 'dateTime') "lifestyleeffective";
  // Value
  src.consommationAutresDrogues as drugs -> tgtObs.value = create('CodeableConcept') as valueCC then {
    drugs -> valueCC.text = drugs "drugvaluetext";
  } "drugvalue";
}

group TransformPhysicalActivityObservation(source src, target tgtObs : Observation, source patient) {
  src -> tgtObs.id = uuid() "setId";
  // Identifier
  src.styleVieId as lsId -> tgtObs.identifier as identifier then {
    lsId -> identifier.system = 'https://hospital.eu/ehr/lifestyle-id' "lifestyleidsystem";
    lsId -> identifier.value = (%lsId & '-physical') "lifestyleidvalue";
  } "lifestyleidentifier";
  // Status
  src -> tgtObs.status = 'final' "lifestylestatus";
  // Category - social-history
  src -> tgtObs.category as category then {
    src -> category.coding as coding then {
      src -> coding.system = 'http://terminology.hl7.org/CodeSystem/observation-category' "categorysystem";
      src -> coding.code = 'social-history' "categorycode";
      src -> coding.display = 'Social History' "categorydisplay";
    } "categorycoding";
  } "lifestylecategory";
  // Code - physical activity
  src -> tgtObs.code as code then {
    src -> code.coding as coding then {
      src -> coding.system = 'http://loinc.org' "codesystem";
      src -> coding.code = '67504-6' "codevalue";
      src -> coding.display = 'Exercise activity' "codedisplay";
    } "physicalcoding";
  } "physicalcode";
  // Subject
  src -> tgtObs.subject = create('Reference') as ref then {
    patient.id as patientId ->  ref.reference = ('Patient/' + %patientId),  ref.type = 'Patient' "subjectref";
  } "lifestylesubject";
  // Effective date
  src.dateRecueil as collectDate where %collectDate.exists() -> tgtObs.effective = cast(collectDate, 'dateTime') "lifestyleeffective";
  // Value
  src.activitePhysique as physical -> tgtObs.value = create('CodeableConcept') as valueCC then {
    physical -> valueCC.text = physical "physicalvaluetext";
  } "physicalvalue";
}

// ========================================================================
// MEDICATION ADMINISTRATION TRANSFORMATION
// ========================================================================
group TransformMedicationAdministrationWOOrder(source src, target tgtMedAdmin : MedicationAdministration, source patient) {
  src -> tgtMedAdmin.id = uuid() "setId";
  // Identifier
  src.administrationId as adminId -> tgtMedAdmin.identifier as identifier then {
    adminId -> identifier.system = 'https://hospital.eu/ehr/administration-id' "adminidsystem";
    adminId -> identifier.value = adminId "adminidvalue";
  } "adminidentifier";
  // Status - assume completed
  src -> tgtMedAdmin.status = 'completed' "adminstatus";
  // Subject
  src -> tgtMedAdmin.subject = create('Reference') as ref then {
    patient.id as patientId ->  ref.reference = ('Patient/' + %patientId),  ref.type = 'Patient' "subjectref";
  } "adminsubject";
  // Medication
  src.denomination as denomination -> tgtMedAdmin.medication = create('CodeableConcept') as medication then {
    denomination -> medication.text = denomination "medicationtext";
    src.codeAtc as atc -> medication.coding as coding then {
      atc -> coding.system = 'http://www.whocc.no/atc' "atcsystem";
      atc -> coding.code = atc "atccode";
    } "atccoding";
  } "adminmedication";
  // Effective period
  src where src.dateHeureDebut.exists() or src.dateHeureFin.exists() -> tgtMedAdmin.effective = create('Period') as period then {
    src.dateHeureDebut as startDate -> period.start = startDate "periodstart";
    src.dateHeureFin as endDate -> period.end = endDate "periodend";
  } "admineffective";
  // Dosage
  src where src.quantite.exists() or src.voieAdministration.exists() -> tgtMedAdmin.dosage as dosage then {
    src.quantite as quantity -> dosage.dose = create('Quantity') as dose then {
      quantity -> dose.value = quantity "dosevalue";
      src.uniteQuantite as unit -> dose.unit = unit "doseunit";
    } "admindose";
    src.voieAdministration as route -> dosage.route as routeCC then {
      route -> routeCC.coding as routeCoding then {
        route -> routeCoding.code = route "routecode";
        route -> routeCoding.system = 'https://smt.esante.gouv.fr/terminologie-standardterms' "routesystem";
      } "routecoding";
    } "dosageroute";
  } "admindosage";
}

group TransformMedicationAdministrationWOrder(source src, target tgtMedAdmin : MedicationAdministration, source patient, source order) extends TransformMedicationAdministrationWOOrder {
  // Context (encounter) reference
  src -> tgtMedAdmin.request = create('Reference') as ref then {
    order.id as orderId ->  ref.reference = ('MedicationRequest/' + %orderId),  ref.type = 'MedicationRequest' "encounterref";
  } "admincontext";
}

group setEntryRequestAndFullUrl(source newRes, target tgtEntry) {
  newRes.id as newResId then {
    newResId -> tgtEntry.fullUrl = ('urn:ehr:' + %newRes.type().name + '/' + %newResId) "setFullUrl";
  } "setResourceType";
}