UNPKG

@claudebernard/fhir-mapper

Version:

A simple FHIR / BCB resource mapper to help stay interoperable while still using the Claude Bernard intelligence

1,196 lines (1,187 loc) 61.5 kB
const unitsOfTime = { 's': { value: 0, label: 'seconde' }, 'min': { value: 10, label: 'minute' }, 'h': { value: 20, label: 'heure' }, 'd': { value: 40, label: 'jour' }, 'wk': { value: 50, label: 'semaine' }, 'mo': { value: 70, label: 'mois' }, 'a': { value: 100, label: 'an' } }; /** * Converts a CB Posology Bean to BCB Posologie Structuree2 format * Based on the Java constructor BCBPosologieStructuree2(PosologyBean poso) * Maps all fields from the Java constructor */ function convertCBToBCB(poso) { return { // Direct mappings from the Java constructor - exact field mapping idProduit: poso.productId, codeTerrain: poso.code, codeIndication: poso.indicationCode, codeNature: poso.typeCode, codeVoie: poso.routeCode, codeProfil: poso.profilCode, quantite1: poso.quantity1, quantite2: poso.quantity2, codeUnitePrise: poso.intakeUnitCode, parKilo: poso.perKilo, adequationUP: poso.adequacy, codePar: poso.byCode, combien1: poso.howMuch1, combien2: poso.howMuch2, tousLes: poso.every, codeDuree1: poso.duration1Code, codeDuree2: poso.duration2Code, codeDuree3: poso.duration3Code, codeMoment: poso.momentCode, pendant1: poso.during1, pendant2: poso.during2, maximum: poso.maximum, maximumPoids: poso.weightMaximum, codeSpecifPrise1: poso.intakeSpecification1Code, codeSpecifPrise2: poso.intakeSpecification2Code, nbUnites: poso.numberOfUnits, coeffMoment: poso.momentCoeff, implicite: poso.implicit, // Additional label fields for completeness (not in Java constructor but useful) noPosologie: poso.patientPosology, libellePosologie: poso.posologyLabel, libIndication: poso.indicationLabel, libVoie: poso.routeLabel, libUnitePrise: poso.intakeUnitLabel, libDuree1: poso.duration1Label, libDuree2: poso.duration2Label, libDuree3: poso.duration3Label, libSpecifPrise1: poso.intakeSpecification1Label }; } /** * Converts BCB Posologie Structuree2 back to CB Posology Bean format. * This is the reverse of the convertCBToBCB function. * @param bcbPosology BCB Posologie Structuree2 object * @returns CB Posology Bean object */ function convertBCBToCB(bcbPosology) { return { // Reverse mapping from BCB to CB (Java constructor mapping in reverse) productId: bcbPosology.idProduit ?? undefined, code: bcbPosology.codeTerrain ?? undefined, indicationCode: bcbPosology.codeIndication ?? undefined, typeCode: bcbPosology.codeNature ?? undefined, routeCode: bcbPosology.codeVoie ?? undefined, profilCode: bcbPosology.codeProfil ?? undefined, quantity1: bcbPosology.quantite1 ?? undefined, quantity2: bcbPosology.quantite2 ?? undefined, intakeUnitCode: bcbPosology.codeUnitePrise ?? undefined, perKilo: bcbPosology.parKilo ?? undefined, adequacy: bcbPosology.adequationUP ?? undefined, byCode: bcbPosology.codePar ?? undefined, howMuch1: bcbPosology.combien1 ?? undefined, howMuch2: bcbPosology.combien2 ?? undefined, every: bcbPosology.tousLes ?? undefined, duration1Code: bcbPosology.codeDuree1 ?? undefined, duration2Code: bcbPosology.codeDuree2 ?? undefined, duration3Code: bcbPosology.codeDuree3 ?? undefined, momentCode: bcbPosology.codeMoment ?? undefined, during1: bcbPosology.pendant1 ?? undefined, during2: bcbPosology.pendant2 ?? undefined, maximum: bcbPosology.maximum ?? undefined, weightMaximum: bcbPosology.maximumPoids ?? undefined, intakeSpecification1Code: bcbPosology.codeSpecifPrise1 ?? undefined, intakeSpecification2Code: bcbPosology.codeSpecifPrise2 ?? undefined, numberOfUnits: bcbPosology.nbUnites ?? undefined, momentCoeff: bcbPosology.coeffMoment ?? undefined, implicit: bcbPosology.implicite ?? undefined, // Label fields (reverse mapping) patientPosology: bcbPosology.noPosologie ?? undefined, posologyLabel: bcbPosology.libellePosologie ?? undefined, indicationLabel: bcbPosology.libIndication ?? undefined, routeLabel: bcbPosology.libVoie ?? undefined, intakeUnitLabel: bcbPosology.libUnitePrise ?? undefined, duration1Label: bcbPosology.libDuree1 ?? undefined, duration2Label: bcbPosology.libDuree2 ?? undefined, duration3Label: bcbPosology.libDuree3 ?? undefined, intakeSpecification1Label: bcbPosology.libSpecifPrise1 ?? undefined }; } function buildDoseAndRate(dosage) { const coding = { system: dosage.codeUnitePrise ? 'https://platform.claudebernard.fr/fhir/CodeSystem/dosage-intake-units' : undefined, code: dosage.codeUnitePrise ?? undefined, unit: dosage.codeUnitePrise ? dosage.libUnitePrise : undefined }; const doseAndRate = dosage.quantite2 ? { doseRange: { low: { value: dosage.quantite1, ...coding }, high: { value: dosage.quantite2, ...coding } } } : { doseQuantity: { value: dosage.quantite1, ...coding } }; return [doseAndRate]; } // The formula used here is count = dose * frequency * (duration / period) function calculateCount(dose, frequency, period, periodUnit, duration, durationUnit) { // Provide default values for null or undefined parameters const actualDose = dose ?? 0; const actualFrequency = frequency ?? 0; const actualPeriod = period ?? 1; const actualDuration = duration ?? 0; const actualPeriodUnit = periodUnit || 'd'; // Default to 'd' (days) if not provided const actualDurationUnit = durationUnit || 'd'; // Default to 'd' (days) if not provided // Convert duration to the same unit as period let convertedDuration = actualDuration; if (actualDurationUnit !== actualPeriodUnit && actualDuration && actualDurationUnit && actualPeriodUnit) { convertedDuration = convertTime(actualDuration, actualDurationUnit, actualPeriodUnit); } // Check if dose, frequency, convertedDuration, or period is null or 0 and compute count accordingly if (!actualDose) { return actualFrequency * (convertedDuration / actualPeriod); } else if (!actualFrequency) { return actualDose * (convertedDuration / actualPeriod); } else if (!convertedDuration) { return actualDose * actualFrequency * actualPeriod; } else if (!actualPeriod) { return actualDose * actualFrequency * convertedDuration; } else { return actualDose * actualFrequency * (convertedDuration / actualPeriod); } } function convertTime(value, fromUnit, toUnit) { const conversionFactors = { 's': 1, 'min': 60, 'h': 3600, 'd': 86400, 'wk': 604800, 'mo': 2628000, 'a': 31536000 }; if (!conversionFactors[fromUnit]) { throw new Error(`Invalid fromUnit: ${fromUnit}`); } if (!conversionFactors[toUnit]) { throw new Error(`Invalid toUnit: ${toUnit}`); } // Convert the input value to seconds const valueInSeconds = value * conversionFactors[fromUnit]; // Convert from seconds to the desired unit const convertedValue = valueInSeconds / conversionFactors[toUnit]; return convertedValue; } async function handleCodifications(dosage, indicationMapper, routeMapper, intakeMapper) { let conditionCode; let conditionLabel; let intakeCode; let intakeLabel; let routeCode; let routeLabel; let errors = []; if (dosage.asNeededFor && dosage.asNeededFor[0].coding && dosage.asNeededFor[0].coding.length > 0) { if (dosage.asNeededFor[0]?.coding?.[0]?.system === 'https://platform.claudebernard.fr/fhir/CodeSystem/amm-pathologies') { conditionCode = dosage.asNeededFor?.[0]?.coding?.[0]?.code; conditionLabel = dosage.asNeededFor?.[0]?.coding?.[0]?.display; } else if (indicationMapper) { const result = await indicationMapper(dosage?.asNeededFor?.[0]?.coding?.[0]); let indication = undefined; if (Array.isArray(result)) { if (result.length > 0) indication = result[0]; } else if (result) { indication = result; } if (indication) { conditionCode = indication.code; conditionLabel = indication.label; } else { conditionCode = undefined; conditionLabel = undefined; errors.push({ field: 'codeIndication', message: 'No mapping found for indication code' }); } } else { errors.push({ field: 'codeIndication', message: 'The system used in the asNeededFor coding field is not supported and no custom mapper was provided' }); } } if (dosage.route && dosage.route.coding && dosage.route.coding.length > 0) { if (dosage.route?.coding?.[0]?.system === 'https://platform.claudebernard.fr/fhir/CodeSystem/dosage-routes') { routeCode = parseInt(dosage?.route?.coding?.[0]?.code ?? ''); routeLabel = dosage.route?.coding?.[0]?.display; } else if (dosage.route && routeMapper) { const result = await routeMapper(dosage.route?.coding?.[0]); let route = undefined; if (Array.isArray(result)) { if (result.length > 0) route = result[0]; } else if (result) { route = result; } if (route) { routeCode = parseInt(route.code); routeLabel = route.label; } else { routeCode = undefined; routeLabel = undefined; errors.push({ field: 'codeVoie', message: 'No mapping found for route code' }); } } else { errors.push({ field: 'codeVoie', message: 'The system used in the route coding field is not supported and no custom mapper was provided' }); } } if (dosage.doseAndRate && dosage.doseAndRate.length > 0) { if (dosage.doseAndRate?.[0]?.doseQuantity?.system === 'https://platform.claudebernard.fr/fhir/CodeSystem/dosage-intake-units') { intakeCode = parseInt(dosage.doseAndRate?.[0]?.doseQuantity.code ?? ''); intakeLabel = dosage.doseAndRate?.[0]?.doseQuantity.unit; } else if (dosage.doseAndRate && intakeMapper) { const result = await intakeMapper(dosage.doseAndRate?.[0]?.doseQuantity); let intake = undefined; if (Array.isArray(result)) { if (result.length > 0) intake = result[0]; } else if (result) { intake = result; } if (intake) { intakeCode = parseInt(intake.code); intakeLabel = intake.label; } else { intakeCode = undefined; intakeLabel = undefined; errors.push({ field: 'codeUnitePrise', message: 'No mapping found for intake code' }); } } else { errors.push({ field: 'codeUnitePrise', message: 'The system used in the doseQuantity coding field is not supported and no custom mapper was provided' }); } } return { conditionCode, conditionLabel, intakeCode, intakeLabel, routeCode, routeLabel, codificationErrors: errors }; } // adding this function to support new naming convention without breaking changes async function fhirToBcb$2(dosageInstructions, indicationMapper, routeMapper, intakeMapper) { return await mapToBcb(dosageInstructions, indicationMapper, routeMapper, intakeMapper); } async function mapToBcb(dosageInstructions, indicationMapper, routeMapper, intakeMapper) { if (!dosageInstructions || dosageInstructions.length === 0) { return [ { result: undefined, errors: [{ field: 'root', message: 'Invalid Dosage array' }] } ]; } const safeIndicationMapper = wrapMaybeAsync$1(indicationMapper); const safeRouteMapper = wrapMaybeAsync$1(routeMapper); const safeIntakeMapper = wrapMaybeAsync$1(intakeMapper); const results = []; for (const dosage of dosageInstructions) { let errors = []; const periodUnit = dosage.timing?.repeat?.periodUnit; const quantite1 = dosage?.doseAndRate?.[0]?.doseQuantity?.value ?? dosage?.doseAndRate?.[0]?.doseRange?.low?.value; const quantite2 = dosage?.doseAndRate?.[0]?.doseRange?.high?.value ?? null; let maximum = null; let codeDuree3 = null; let maxDosePerDay = null; if (dosage.maxDosePerPeriod) { maxDosePerDay = dosage.maxDosePerPeriod.find(period => period?.denominator?.unit === 'd' && period?.denominator?.value === 1); if (maxDosePerDay) { maximum = maxDosePerDay?.numerator?.value; codeDuree3 = maxDosePerDay?.denominator?.unit; } } let libSpecifPrise1 = dosage.asNeeded ? 'selon besoin' : null; const doseSpacing = dosage.maxDosePerPeriod?.find(period => period?.denominator?.unit === 'h'); if (doseSpacing) { const spacingText = `en espaçant les prises de ${doseSpacing?.denominator?.value}h minimum`; libSpecifPrise1 = libSpecifPrise1 ? `${libSpecifPrise1}, ${spacingText}` : spacingText; } // handleCodifications must be updated to support async mappers const { conditionCode, conditionLabel, intakeCode, intakeLabel, routeCode, routeLabel, codificationErrors } = await handleCodifications(dosage, safeIndicationMapper, safeRouteMapper, safeIntakeMapper); const codeDuree2 = dosage?.timing?.repeat?.boundsDuration?.code; const bcbDosage = { noPosologie: dosage.sequence ?? undefined, libellePosologie: dosage.text ?? undefined, codeIndication: conditionCode, libIndication: conditionLabel, codeDuree1: periodUnit ? unitsOfTime[periodUnit]?.value : undefined, libDuree1: periodUnit ? unitsOfTime[periodUnit]?.label : undefined, quantite1: quantite1 ?? undefined, quantite2: quantite2 ?? undefined, tousLes: dosage.timing?.repeat?.period ?? undefined, combien1: dosage.timing?.repeat?.frequency ?? undefined, combien2: dosage.timing?.repeat?.frequencyMax ?? undefined, pendant1: dosage.timing?.repeat?.boundsDuration?.value ?? undefined, codeUnitePrise: intakeCode, libUnitePrise: intakeLabel, codeVoie: routeCode, libVoie: routeLabel, codeDuree2: codeDuree2 ? unitsOfTime[codeDuree2]?.value : undefined, libDuree2: codeDuree2 ? unitsOfTime[codeDuree2]?.label : undefined, maximum: maximum ?? undefined, codeDuree3: codeDuree3 ? unitsOfTime[codeDuree3]?.value : undefined, libDuree3: codeDuree3 ? unitsOfTime[codeDuree3]?.label : undefined, libSpecifPrise1: libSpecifPrise1 ?? undefined }; results.push({ result: bcbDosage, errors: codificationErrors.concat(errors) }); } return results; } // adding this function to support new naming convention without breaking changes function bcbToFhir$2(bcbDosages) { return mapToFhir(bcbDosages); } function mapToFhir(bcbDosages) { if (!bcbDosages || bcbDosages.length === 0) { throw new Error('Invalid BCBPosologieStructuree2 array'); } return bcbDosages.map((dosage) => { const keys = Object.keys(unitsOfTime); const periodUnit = keys.find(key => unitsOfTime[key].value === dosage.codeDuree1) ?? undefined; const durationUnit = keys.find(key => unitsOfTime[key].value === dosage.codeDuree2) ?? undefined; const maximumUnit = keys.find(key => unitsOfTime[key].value === dosage.codeDuree3) ?? undefined; const route = dosage.codeVoie ? { coding: [{ system: 'https://platform.claudebernard.fr/fhir/CodeSystem/dosage-routes', code: dosage.codeVoie.toString(), display: dosage.libVoie }] } : undefined; const intake = dosage.codeUnitePrise ? { coding: [{ system: 'https://platform.claudebernard.fr/fhir/CodeSystem/dosage-intake-units', code: dosage.codeUnitePrise.toString(), display: dosage.libUnitePrise }] } : undefined; const indication = dosage.codeIndication ? [{ coding: [{ system: 'https://platform.claudebernard.fr/fhir/CodeSystem/amm-pathologies', code: dosage.codeIndication, display: dosage.libIndication }] }] : undefined; const maxDosePerPeriod = dosage.maximum ? [{ numerator: { value: dosage.maximum, system: 'https://platform.claudebernard.fr/fhir/CodeSystem/dosage-intake-units', code: intake?.coding?.[0]?.code, unit: intake?.coding?.[0]?.display }, denominator: { value: 1, unit: maximumUnit } }] : undefined; const doseAndRate = buildDoseAndRate(dosage); const boundsDuration = dosage.pendant1 ? { value: dosage.pendant1, unit: durationUnit } : undefined; const asNeeded = dosage.libSpecifPrise1?.includes('besoin'); return { result: { text: dosage.libellePosologie, sequence: dosage.noPosologie, doseAndRate: doseAndRate, maxDosePerPeriod: dosage.maximum ? maxDosePerPeriod : undefined, route: route, asNeeded: asNeeded, asNeededFor: indication, timing: { repeat: { frequency: dosage.combien1, frequencyMax: dosage.combien2, period: dosage.tousLes, periodUnit: periodUnit, boundsDuration: boundsDuration, count: calculateCount(dosage.quantite1, dosage.combien1, dosage.tousLes, periodUnit, dosage.pendant1, durationUnit), countMax: calculateCount(dosage.quantite2 ?? dosage.quantite1, dosage.combien2 ?? dosage.combien1, dosage.tousLes, periodUnit, dosage.pendant2 ?? dosage.pendant1, durationUnit) } } }, errors: [] }; }); } // Utility to wrap a possibly sync or async function so it always returns a Promise function wrapMaybeAsync$1(fn) { if (!fn) return undefined; return ((...args) => Promise.resolve(fn(...args))); } /** * Converts FHIR Dosage instructions to CB Posology Bean format. * Uses the existing fhirToBcb function and then converts BCB to CB format. * @param dosageInstructions Array of FHIR Dosage resources * @param indicationMapper Optional mapper function for indication codes * @param routeMapper Optional mapper function for route codes * @param intakeMapper Optional mapper function for intake codes * @returns Promise of array of CB Posology Beans with mapping responses */ async function fhirToCb$1(dosageInstructions, indicationMapper, routeMapper, intakeMapper) { // First convert FHIR to BCB using existing function const bcbResults = await fhirToBcb$2(dosageInstructions, indicationMapper, routeMapper, intakeMapper); // Convert each BCB result to CB format const cbResults = []; for (const bcbResult of bcbResults) { if (bcbResult.result) { // Convert BCB to CB format (reverse conversion) const cbPosology = convertBCBToCB(bcbResult.result); cbResults.push({ result: cbPosology, errors: bcbResult.errors || [] }); } else { // If BCB conversion failed, pass through the errors cbResults.push({ result: undefined, errors: bcbResult.errors || [{ field: 'conversion', message: 'Failed to convert FHIR to BCB format' }] }); } } return cbResults; } /** * Converts CB Posology Bean format to FHIR Dosage instructions. * Converts CB to BCB format first, then uses existing bcbToFhir function. * @param cbPosologies Array of CB Posology Beans * @returns Array of FHIR Dosage resources with mapping responses */ function cbToFhir$1(cbPosologies) { if (!cbPosologies || cbPosologies.length === 0) { return [{ result: undefined, errors: [{ field: 'root', message: 'Invalid CB Posology array' }] }]; } // Convert each CB to BCB format using the converter const bcbPosologies = []; const conversionErrors = []; for (const [index, cbPosology] of cbPosologies.entries()) { try { const bcbPosology = convertCBToBCB(cbPosology); bcbPosologies.push(bcbPosology); } catch (error) { conversionErrors.push({ field: `cbPosology[${index}]`, message: `Failed to convert CB to BCB format: ${error}` }); } } // If there were conversion errors, return them if (conversionErrors.length > 0) { return [{ result: undefined, errors: conversionErrors }]; } // Use existing BCB to FHIR conversion return bcbToFhir$2(bcbPosologies); } var dosageMapper = /*#__PURE__*/Object.freeze({ __proto__: null, bcbToFhir: bcbToFhir$2, cbToFhir: cbToFhir$1, fhirToBcb: fhirToBcb$2, fhirToCb: fhirToCb$1, mapToBcb: mapToBcb, mapToFhir: mapToFhir }); /** * Simple hash function compatible with both Node.js and browser environments * This replaces the Node.js crypto module to avoid browser compatibility issues */ const simpleHash = (str) => { let hash = 0; if (str.length === 0) return hash.toString(16).padStart(8, '0'); for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; // Convert to 32-bit integer } return Math.abs(hash).toString(16).padStart(8, '0').substring(0, 8); }; /** * Generate a deterministic 8-character hash from input data * Compatible with browser and Node.js environments */ const generateHash = (data) => { const dataString = typeof data === 'string' ? data : JSON.stringify(data); return simpleHash(dataString); }; const genderMap = { male: 'M', female: 'F' }; const fhirGenderMap = { M: 'male', F: 'female' }; const hepaticInsufficiencyMap = { 'A': { code: 'K72.90', display: 'Mild hepatic insufficiency' }, 'B': { code: 'K72.91', display: 'Moderate hepatic insufficiency' }, 'C': { code: 'K72.92', display: 'Severe hepatic insufficiency' } }; const hepaticInsufficiencyReverseMap = { 'K72.90': 'A', 'K72.91': 'B', 'K72.92': 'C' }; const calculateAgeInMonths = (birthDate) => { const birth = new Date(birthDate); const today = new Date(); const yearsDifference = today.getFullYear() - birth.getFullYear(); const monthsDifference = today.getMonth() - birth.getMonth(); const daysDifference = today.getDate() - birth.getDate(); let ageInMonths = yearsDifference * 12 + monthsDifference; if (daysDifference < 0) { ageInMonths--; } return ageInMonths; }; const calculateBirthDateFromAgeInMonths = (ageInMonths) => { const today = new Date(); const birthDate = new Date(today.setMonth(today.getMonth() - ageInMonths)); const year = birthDate.getFullYear(); const month = (birthDate.getMonth() + 1).toString().padStart(2, '0'); const day = birthDate.getDate().toString().padStart(2, '0'); return `${year}-${month}-${day}`; }; const generatePatientId = (patientData) => { // Create a consistent hash based on patient data const dataToHash = { age: patientData.age, gender: 'sexe' in patientData ? patientData.sexe : patientData.gender, weight: 'poids' in patientData ? patientData.poids : patientData.weight, height: 'taille' in patientData ? patientData.taille : patientData.height, pregnancy: 'grossesse' in patientData ? patientData.grossesse : patientData.pregnancy, breastfeeding: 'allaitement' in patientData ? patientData.allaitement : patientData.breastfeeding }; return generateHash(dataToHash); }; const convertPatient = (bcbPatient) => { return { firstName: '', lastName: '', age: bcbPatient.age, gender: bcbPatient.sexe, weight: bcbPatient.poids, pregnancy: bcbPatient.grossesse === 1, breastfeeding: bcbPatient.allaitement === 1, ammPathologies: bcbPatient.lstPathologiesAMM.map(code => ({ type: 'AMM', code: code, label: '' })), allergies: bcbPatient.lstIdComposantAllergie.map(code => ({ code: code, label: '' })), hepaticStage: bcbPatient.insuffisanceHepatique, height: bcbPatient.taille, weeksOfPregnancy: 0, creatinineClearance: bcbPatient.clairanceCreatinine, molCreatinine: bcbPatient.creatininemieMol, mglCreatinine: bcbPatient.creatininemieMg, gfr: 0, medicalTeam: { firstNameDoctor: "", lastNameDoctor: "", firstNameSpecialist: "", lastNameSpecialist: "", firstNamePharmacist: "", lastNamePharmacist: "", firstNameNurse: "", lastNameNurse: "" } }; }; const sortEntries = async (target, entries, allergiesMapper, snomedPathologiesMapper, errors = []) => { const patientEntry = entries.find(entry => entry?.resource?.resourceType === "Patient")?.resource; const observationEntries = entries.filter(entry => entry?.resource?.resourceType === "Observation").map(entry => entry?.resource); const conditionEntries = entries.filter(entry => entry?.resource?.resourceType === "Condition").map(entry => entry?.resource); const allergyIntoleranceEntries = entries.filter(entry => entry?.resource?.resourceType === "AllergyIntolerance").map(entry => entry?.resource); const weightObservation = observationEntries.find(entry => entry?.code?.coding?.[0]?.code === '29463-7'); const heightObservation = observationEntries.find(entry => entry?.code?.coding?.[0]?.code === '8302-2'); const pregnancyObservation = observationEntries.find(entry => entry?.code?.coding?.some(coding => coding.code === '82810-3')); const breastfeedingObservation = observationEntries.find(entry => entry?.code?.coding?.some(coding => coding.code === '63895-7')); const hepaticCondition = conditionEntries.find(entry => entry?.code?.coding?.some(coding => coding.code === '2164-2' || coding.code === '14682-9' || coding.code === '2160-0')); let lstIdComposantAllergie = []; let errorField = target === 'bcb' ? 'lstIdComposantAllergie' : 'allergies'; for (const allergyEntry of allergyIntoleranceEntries) { if (allergiesMapper) { const mappingResult = await allergiesMapper(allergyEntry?.code?.coding?.[0]); if (Array.isArray(mappingResult)) { if (mappingResult.length > 0) { lstIdComposantAllergie.push(...mappingResult); } else { errors.push({ field: errorField, message: `No mapping found for allergy code ${allergyEntry?.code?.coding?.[0]?.code}` }); } } else if (mappingResult) { lstIdComposantAllergie.push(mappingResult); } else { errors.push({ field: errorField, message: `No mapping found for allergy code ${allergyEntry?.code?.coding?.[0]?.code}` }); } } else { errors.push({ field: errorField, message: 'Allergy Intolerance values were found but no allergies mapper was provided, the output field is thus left empty.' }); } } const cim10PathologiesEntries = conditionEntries.filter(entry => entry?.code?.coding?.some(coding => coding.system === 'http://hl7.org/fhir/sid/icd-10')).map(pathology => ({ code: pathology?.code?.coding?.[0]?.code || '', label: '' })); const snomedPathologiesEntries = conditionEntries.filter(entry => entry?.code?.coding?.some(coding => coding.system === 'http://snomed.info/sct')); const ammPathologiesEntries = conditionEntries.filter(entry => entry?.code?.coding?.some(coding => coding.system === 'https://platform.claudebernard.fr/fhir/CodeSystem/amm-pathologies')).map(pathology => ({ code: pathology?.code?.coding?.[0]?.code || '', label: '' })); let lstPathologies = []; lstPathologies.push(...ammPathologiesEntries); let lstCim10Pathologies = []; lstCim10Pathologies.push(...cim10PathologiesEntries); errorField = target === 'bcb' ? 'lstPathologiesAMM' : 'ammPathologies'; for (const pathology of snomedPathologiesEntries) { if (snomedPathologiesMapper) { const mappingResult = await snomedPathologiesMapper(pathology?.code?.coding?.[0]); if (Array.isArray(mappingResult)) { if (mappingResult.length > 0) { lstPathologies.push(...mappingResult); } else { errors.push({ field: errorField, message: `No mapping found for snomed code ${pathology?.code?.coding?.[0]?.code}` }); } } else if (mappingResult) { lstPathologies.push(mappingResult); } else { errors.push({ field: errorField, message: `No mapping found for snomed code ${pathology?.code?.coding?.[0]?.code}` }); } } else { errors.push({ field: errorField, message: "Snomed pathologies were found but no mapper was provided, the output field is thus left empty." }); } } const clairanceCreatinineObservation = observationEntries.find(entry => entry?.code?.coding?.some(coding => coding.code === '2164-2')); const creatininemieMolObservation = observationEntries.find(entry => entry?.code?.coding?.some(coding => coding.code === '14682-9')); const creatininemieMgObservation = observationEntries.find(entry => entry?.code?.coding?.some(coding => coding.code === '2160-0')); return { patientEntry, weightObservation, heightObservation, pregnancyObservation, breastfeedingObservation, hepaticCondition, lstIdComposantAllergie, lstCim10Pathologies, lstPathologies, clairanceCreatinineObservation, creatininemieMolObservation, creatininemieMgObservation }; }; const patientToFhir = (patientData) => { if (!patientData) { return { result: undefined, errors: [{ field: 'root', message: 'Invalid patient object' }] }; } let errors = []; const bundleEntries = []; // Generate a consistent patient ID based on patient data const patientId = generatePatientId(patientData); const patientReference = `Patient/${patientId}`; const patient = { resourceType: 'Patient', id: patientId, birthDate: calculateBirthDateFromAgeInMonths(patientData.age), gender: fhirGenderMap[patientData.gender] || 'unknown' }; const weightObservation = { resourceType: 'Observation', id: `${patientId}-weight`, code: { coding: [{ code: '29463-7', display: 'Body Weight' }] }, status: 'final', valueQuantity: { value: patientData.weight, unit: 'kg' }, subject: { reference: patientReference } }; const heightObservation = { resourceType: 'Observation', id: `${patientId}-height`, code: { coding: [{ code: '8302-2', display: 'Body Height' }] }, status: 'final', valueQuantity: { value: patientData.height, unit: 'cm' }, subject: { reference: patientReference } }; let pregnancyObservation = undefined; if (patientData.pregnancy) { pregnancyObservation = { resourceType: 'Observation', id: `${patientId}-pregnancy`, code: { coding: [{ code: '82810-3', display: 'Pregnancy Status' }] }, status: 'final', subject: { reference: patientReference } }; } if (pregnancyObservation && patientData.weeksOfPregnancy > 0) { pregnancyObservation.valueQuantity = { value: patientData.weeksOfPregnancy, unit: 'weeks', system: 'http://unitsofmeasure.org', code: 'wk' }; } let breastfeedingObservation = undefined; if (patientData.breastfeeding) { breastfeedingObservation = { resourceType: 'Observation', id: `${patientId}-breastfeeding`, code: { coding: [{ code: '63895-7', display: 'Breastfeeding Status' }] }, status: 'final', subject: { reference: patientReference } }; } let creatinineClearanceObservation; if (patientData.creatinineClearance > 0) { creatinineClearanceObservation = { resourceType: 'Observation', id: `${patientId}-creatinine-clearance`, code: { coding: [{ system: 'http://loinc.org', code: '2164-2', display: 'Creatinine clearance' }] }, status: 'final', valueQuantity: { value: patientData.creatinineClearance, unit: 'mL/min' }, subject: { reference: patientReference } }; } let creatinineMolObservation; if (patientData.molCreatinine > 0) { creatinineMolObservation = { resourceType: 'Observation', id: `${patientId}-creatinine-mol`, code: { coding: [{ system: 'http://loinc.org', code: '14682-9', display: 'Creatinine [Moles/volume] in Serum or Plasma' }] }, status: 'final', valueQuantity: { value: patientData.molCreatinine, unit: 'mmol/L' }, subject: { reference: patientReference } }; } let creatinineMgObservation; if (patientData.mglCreatinine > 0) { creatinineMgObservation = { resourceType: 'Observation', id: `${patientId}-creatinine-mg`, code: { coding: [{ system: 'http://loinc.org', code: '2160-0', display: 'Creatinine [Mass/volume] in Serum or Plasma' }] }, status: 'final', valueQuantity: { value: patientData.mglCreatinine, unit: 'mg/dL' }, subject: { reference: patientReference } }; } let conditions = []; if (patientData.ammPathologies) { conditions = patientData.ammPathologies.map((pathology, index) => { return { resourceType: 'Condition', id: `${patientId}-condition-${index}`, code: { coding: [{ system: pathology.type === 'AMM' ? 'https://platform.claudebernard.fr/fhir/CodeSystem/amm-pathologies' : 'http://hl7.org/fhir/sid/icd-10', code: pathology.code, display: pathology.label || pathology.code }] }, clinicalStatus: { coding: [{ system: 'http://terminology.hl7.org/CodeSystem/condition-clinical', code: 'active', display: 'Active' }] }, subject: { reference: patientReference } }; }); } let hepaticInsufficiencyCondition; if (patientData.hepaticStage && hepaticInsufficiencyMap[patientData.hepaticStage]) { const stage = hepaticInsufficiencyMap[patientData.hepaticStage]; hepaticInsufficiencyCondition = { resourceType: 'Condition', id: `${patientId}-hepatic-insufficiency`, code: { coding: [{ system: 'http://hl7.org/fhir/sid/icd-10', code: stage.code, display: stage.display }] }, clinicalStatus: { coding: [{ system: 'http://terminology.hl7.org/CodeSystem/condition-clinical', code: 'active', display: 'Active' }] }, verificationStatus: { coding: [{ system: 'http://terminology.hl7.org/CodeSystem/condition-ver-status', code: 'confirmed', display: 'Confirmed' }] }, subject: { reference: patientReference } }; } let allergies = []; if (patientData.allergies) { allergies = patientData.allergies.map((allergy, index) => { return { resourceType: 'AllergyIntolerance', id: `${patientId}-allergy-${index}`, patient: { reference: patientReference }, code: { coding: [{ system: 'https://platform.claudebernard.fr/fhir/CodeSystem/products-ingredients', code: allergy.code.toString(), display: allergy.label?.toString() || allergy.code.toString() }] }, clinicalStatus: { coding: [{ system: 'http://terminology.hl7.org/CodeSystem/condition-clinical', code: 'active', display: 'Active' }] }, }; }); } bundleEntries.push(...[ { resource: patient }, ...(weightObservation ? [{ resource: weightObservation }] : []), ...(heightObservation ? [{ resource: heightObservation }] : []), ...(pregnancyObservation ? [{ resource: pregnancyObservation }] : []), ...(breastfeedingObservation ? [{ resource: breastfeedingObservation }] : []), ...(creatinineClearanceObservation ? [{ resource: creatinineClearanceObservation }] : []), ...(creatinineMolObservation ? [{ resource: creatinineMolObservation }] : []), ...(creatinineMgObservation ? [{ resource: creatinineMgObservation }] : []), ...conditions.map(condition => ({ resource: condition })), ...allergies.map(allergy => ({ resource: allergy })), ...(hepaticInsufficiencyCondition ? [{ resource: hepaticInsufficiencyCondition }] : []), ]); const bundle = { resourceType: "Bundle", id: "patient-bundle", type: "collection", entry: bundleEntries }; return { result: bundle, errors: errors }; }; /* ================== FHIR <=> BCB ================== */ function bcbToFhir$1(bcbPatient) { return patientToFhir(convertPatient(bcbPatient)); } async function fhirToBcb$1(fhirBundle, allergiesMapper, snomedPathologiesMapper) { if (!fhirBundle || !fhirBundle.entry || fhirBundle.entry.length === 0) { return { result: undefined, errors: [{ field: 'root', message: 'Invalid Bundle or empty entries' }] }; } let errors = []; const { patientEntry, weightObservation, heightObservation, pregnancyObservation, breastfeedingObservation, hepaticCondition, lstIdComposantAllergie, lstCim10Pathologies, lstPathologies, clairanceCreatinineObservation, creatininemieMolObservation, creatininemieMgObservation } = await sortEntries('bcb', fhirBundle.entry, wrapMaybeAsync(allergiesMapper), wrapMaybeAsync(snomedPathologiesMapper), errors); const bcbPatient = { lstIdComposantAllergie: lstIdComposantAllergie.map(codification => Number(codification.code)), lstPathologiesCIM10: lstCim10Pathologies.map(codification => codification.code), lstPathologiesAMM: lstPathologies.map(codification => codification.code), age: patientEntry && patientEntry.birthDate ? calculateAgeInMonths(patientEntry.birthDate) : 0, poids: weightObservation && weightObservation?.valueQuantity?.value ? weightObservation?.valueQuantity?.value : 0, taille: heightObservation && heightObservation?.valueQuantity?.value ? heightObservation?.valueQuantity?.value : 0, grossesse: pregnancyObservation ? 1 : 0, allaitement: breastfeedingObservation ? 1 : 0, sexe: patientEntry?.gender ? genderMap[patientEntry.gender] || '' : '', clairanceCreatinine: clairanceCreatinineObservation && clairanceCreatinineObservation?.valueQuantity?.value ? clairanceCreatinineObservation?.valueQuantity?.value : 0, creatininemieMol: creatininemieMolObservation && creatininemieMolObservation?.valueQuantity?.value ? creatininemieMolObservation?.valueQuantity?.value : 0, creatininemieMg: creatininemieMgObservation && creatininemieMgObservation?.valueQuantity?.value ? creatininemieMgObservation?.valueQuantity?.value : 0, insuffisanceHepatique: hepaticCondition?.code?.coding?.[0]?.code ? hepaticInsufficiencyReverseMap[hepaticCondition.code.coding[0].code] : '', }; return { result: bcbPatient, errors: errors }; } /* ================== FHIR <=> CB ================== */ function cbToFhir(cbPatient) { return patientToFhir(cbPatient); } async function fhirToCb(fhirBundle, allergiesMapper, snomedPathologiesMapper) { if (!fhirBundle?.entry?.length) { return { result: undefined, errors: [{ field: 'root', message: 'Invalid Bundle or empty entries' }] }; } let errors = []; const { patientEntry, weightObservation, heightObservation, pregnancyObservation, breastfeedingObservation, hepaticCondition, lstIdComposantAllergie, lstCim10Pathologies, lstPathologies, clairanceCreatinineObservation, creatininemieMolObservation, creatininemieMgObservation } = await sortEntries('cb', fhirBundle.entry, wrapMaybeAsync(allergiesMapper), wrapMaybeAsync(snomedPathologiesMapper), errors); const pathologies = [ ...lstPathologies.map(codification => ({ type: 'AMM', code: codification.code, label: codification.label })), ...lstCim10Pathologies.map(codification => ({ type: 'CIM10', code: codification.code, label: codification.label })) ]; return { result: { firstName: '', lastName: '', age: patientEntry && patientEntry.birthDate ? calculateAgeInMonths(patientEntry.birthDate) : 0, gender: patientEntry?.gender ? genderMap[patientEntry.gender] || '' : '', weight: weightObservation && weightObservation?.valueQuantity?.value ? weightObservation?.valueQuantity?.value : 0, pregnancy: pregnancyObservation ? true : false, breastfeeding: breastfeedingObservation ? true : false, ammPathologies: pathologies, allergies: lstIdComposantAllergie.map(codification => ({ code: Number(codification.code), label: codification.label })), hepaticStage: hepaticCondition?.code?.coding?.[0]?.code || '', height: heightObservation && heightObservation?.valueQuantity?.value ? heightObservation?.valueQuantity?.value : 0, weeksOfPregnancy: pregnancyObservation && pregnancyObservation?.valueQuantity?.value ? pregnancyObservation?.valueQuantity?.value : 0, creatinineClearance: clairanceCreatinineObservation && clairanceCreatinineObservation?.valueQuantity?.value ? clairanceCreatinineObservation?.valueQuantity?.value : 0, molCreatinine: creatininemieMolObservation && creatininemieMolObservation?.valueQuantity?.value ? creatininemieMolObservation?.valueQuantity?.value : 0, mglCreatinine: creatininemieMgObservation && creatininemieMgObservation?.valueQuantity?.value ? creatininemieMgObservation?.valueQuantity?.value : 0, gfr: 0, medicalTeam: { firstNameDoctor: "", lastNameDoctor: "", firstNameSpecialist: "", lastNameSpecialist: "", firstNamePharmacist: "", lastNamePharmacist: "", firstNameNurse: "", lastNameNurse: "" } }, errors: errors }; } // Utility to wrap a possibly sync or async function so it always returns a Promise function wrapMaybeAsync(fn) { if (!fn) return undefined; return ((...args) => Promise.resolve(fn(...args))); } var patientMapper = /*#__PURE__*/Object.freeze({ __proto__: null, bcbToFhir: bcbToFhir$1, cbToFhir: cbToFhir, fhirToBcb: fhirToBcb$1, fhirToCb: fhirToCb }); /** * Generate a hash-based ID for a medication based on its codes */ const generateMedicationId = (codes, system) => { const sortedCodes = [...codes].sort((a, b) => a.localeCompare(b)); const dataToHash = { codes: sortedCodes, system }; return generateHash(dataToHash); }; /** * Helper function to extract codes from a single medication */ const extractCodesFromMedication = (medication, system) => { const codes = []; if (medication.code?.coding) { for (const coding of medication.code.coding) { if (coding.system === system && coding.code) { codes.push(coding.code); } } } return codes; }; /** * Extracts a list of codes from a Bundle containing Medication resources based on the specified system. * @param bundle Bundle containing Medication resources to extract codes from * @param system The coding system to filter by * @returns List of codes that match the specified system */ function extractCodesFromMedications(bundle, system) { const codes = []; if (!bundle.entry) { return codes; } for (const entry of bundle.entry) { if (entry.resource?.resourceType === 'Medication') { const medication = entry.resource; codes.push(...extractCodesFromMedication(medication, system)); } } return codes; } /** * Extracts a list of bcb-code strings from a Bundle containing Medication resources. * Only codes with system "https://platform.claudebernard.fr/fhir/CodeSystem/bcb-code" are included. * @param bundle Bundle containing Medication resources * @returns List of BCB codes */ function extractBcbCodesFromMedications(bundle) { return extractCodesFromMedications(bundle, "https://platform.claudebernard.fr/fhir/CodeSystem/bcb-code"); } /** * Extracts a list of cip13-code strings from a Bundle containing Medi