diff --git a/openfn-69066751-5f2c-459c-b42e-feba1c802383-state.json b/openfn-69066751-5f2c-459c-b42e-feba1c802383-state.json index d26dd28..89b08f9 100644 --- a/openfn-69066751-5f2c-459c-b42e-feba1c802383-state.json +++ b/openfn-69066751-5f2c-459c-b42e-feba1c802383-state.json @@ -231,8 +231,8 @@ "wf2-omrs-dhis2": { "id": "c886121c-0a91-45b5-ae30-7d0922b693be", "name": "wf2-omrs-dhis2", - "inserted_at": "2024-11-12T16:16:40.298307Z", - "lock_version": 104, + "inserted_at": "2024-11-12T17:40:15.020397Z", + "lock_version": 114, "triggers": { "cron": { "enabled": false, @@ -266,7 +266,7 @@ "Get-TEIs-and-Map-Answers": { "id": "793f4fe1-d717-4caa-a617-2b5e2b19880d", "name": "Get TEIs and Map Answers", - "body": "const delay = ms => new Promise(resolve => setTimeout(resolve, ms));\n\neach(\n $.encounters,\n get(\n 'tracker/trackedEntities',\n {\n orgUnit: $.orgUnit,\n program: $.program,\n filter: [`AYbfTPYMNJH:Eq:${$.data.patient.uuid}`],\n fields: '*',\n },\n {},\n async state => {\n const encounter = state.references.at(-1);\n console.log(encounter.patient.uuid, 'Encounter patient uuid');\n\n const { trackedEntity, enrollments } = state.data?.instances?.[0] || {};\n if (trackedEntity && enrollments) {\n state.TEIs ??= {};\n state.TEIs[encounter.patient.uuid] = {\n trackedEntity,\n enrollments,\n enrollment: enrollments[0]?.enrollment,\n };\n }\n\n await delay(2000);\n return state;\n }\n )\n);\n\nconst processAnswer = (\n answer,\n conceptUuid,\n dataElement,\n optsMap,\n optionSetKey\n) => {\n // console.log('Has answer', conceptUuid, dataElement);\n return typeof answer.value === 'object'\n ? processObjectAnswer(\n answer,\n conceptUuid,\n dataElement,\n optsMap,\n optionSetKey\n )\n : processOtherAnswer(answer, conceptUuid, dataElement);\n};\n\nconst processObjectAnswer = (\n answer,\n conceptUuid,\n dataElement,\n optsMap,\n optionSetKey\n) => {\n if (isDiagnosisByPsychologist(conceptUuid, dataElement)) {\n return '' + answer.value.uuid === '278401ee-3d6f-4c65-9455-f1c16d0a7a98';\n }\n return findMatchingOption(answer, optsMap, optionSetKey);\n};\n\nconst processOtherAnswer = (answer, conceptUuid, dataElement) => {\n if (isPhq9Score(answer.value, conceptUuid, dataElement)) {\n return getRangePhq(answer.value);\n }\n return answer.value;\n};\n\nconst processNoAnswer = (data, conceptUuid, dataElement) => {\n // console.log('No answer', conceptUuid, dataElement);\n if (isEncounterDate(conceptUuid, dataElement)) {\n return data.encounterDatetime.replace('+0000', '');\n }\n return '';\n};\n\nconst findMatchingOption = (answer, optsMap, optionSetKey) => {\n const optionKey = `${answer.formUuid}-${answer.concept.uuid}`; \n\n //const matchingOptionSet = optionSetKey[answer.concept.uuid];\n const matchingOptionSet = optionSetKey[optionKey];\n console.log('optionKey', optionKey);\n console.log('conceptUid', answer.concept.uuid);\n console.log('value uid', answer.value.uuid);\n console.log('value', answer.value.display);\n console.log('matchingOptionSet', matchingOptionSet);\n\n //const answerKey = answerMappingUid\n\n const matchingOption = optsMap.find(\n o =>\n o['value.uuid - External ID'] === answer.value.uuid &&\n o['DHIS2 Option Set UID'] === matchingOptionSet\n )?.['DHIS2 Option Code'] || answer.value.display; //TODO: revisit this logic if optionSet not found\n\n console.log('matchingOption value', matchingOption)\n\n // to convert ALL caps to lowercase per DHIS2 api\n if (matchingOption === 'FALSE') {\n console.log('false option', matchingOption)\n return 'false';\n }\n if (matchingOption === 'TRUE') {\n console.log('true option', matchingOption)\n return 'true';\n }\n ////=========================================//\n\n return matchingOption || '';\n};\n\nconst isEncounterDate = (conceptUuid, dataElement) => {\n return (\n conceptUuid === 'encounter-date' &&\n ['CXS4qAJH2qD', 'I7phgLmRWQq', 'yUT7HyjWurN'].includes(dataElement)\n );\n};\n\nconst isDiagnosisByPsychologist = (conceptUuid, dataElement) =>\n conceptUuid === '722dd83a-c1cf-48ad-ac99-45ac131ccc96' &&\n dataElement === 'pN4iQH4AEzk';\n\nconst isPhq9Score = (value, conceptUuid, dataElement) =>\n typeof value === 'number' &&\n conceptUuid === '5f3d618e-5c89-43bd-8c79-07e4e98c2f23' &&\n dataElement === 'tsFOVnlc6lz';\n\nconst getRangePhq = input => {\n if (input >= 20) return '>20';\n if (input >= 15) return '15_19';\n if (input >= 10) return '10_14';\n if (input >= 5) return '5_9';\n return '0_4';\n};\n\nconst dataValuesMapping = (data, dataValueMap, optsMap, optionSetKey) => {\n return Object.keys(dataValueMap)\n .map(dataElement => {\n const conceptUuid = dataValueMap[dataElement];\n const obsAnswer = data.obs.find(o => o.concept.uuid === conceptUuid);\n const answer = {\n ...obsAnswer,\n formUuid: data.form.uuid\n };\n const value = answer\n ? processAnswer(answer, conceptUuid, dataElement, optsMap, optionSetKey)\n : processNoAnswer(data, conceptUuid, dataElement);\n\n return { dataElement, value };\n })\n .filter(d => d);\n};\n\n// Prepare DHIS2 data model for create events\nfn(state => {\n const createEvent = (data, state) => {\n const { trackedEntity, enrollment } = state.TEIs[data.patient.uuid] || {};\n\n if (!trackedEntity || !enrollment) {\n return null;\n }\n\n return {\n program: state.program,\n orgUnit: state.orgUnit,\n trackedEntity,\n enrollment,\n occurredAt: data.encounterDatetime.replace('+0000', ''),\n };\n };\n\n const handleMissingRecord = (data, state) => {\n const { uuid, display } = data.patient;\n\n console.log(uuid, 'Patient is missing trackedEntity && enrollment');\n\n state.missingRecords ??= {};\n state.missingRecords[uuid] ??= {\n encounters: [],\n patient: display,\n };\n\n state.missingRecords[uuid].encounters.push(data);\n };\n\n const processEncounter = (data, state) => {\n const event = createEvent(data, state);\n if (!event) {\n handleMissingRecord(data, state);\n return null;\n }\n\n const form = state.formMaps[data.form.uuid];\n if (!form?.dataValueMap) {\n return null;\n }\n\n return {\n ...event,\n programStage: form.programStage,\n dataValues: dataValuesMapping(\n data,\n form.dataValueMap,\n state.optsMap,\n state.optionSetKey\n ),\n };\n };\n\n state.encountersMapping = state.encounters\n .map(data => processEncounter(data, state))\n .filter(Boolean);\n\n return state;\n});\n// const findMatchingOption = (answer, optsMap, optionSetKey) => {\n// const answerKeyUid = optionSetKey[answer.concept.uuid];\n\n// const matchingOption = optsMap.find(\n// (o) => o[\"DHIS2 answerKeyUid\"] === answerKeyUid\n// )?.[\"DHIS2 Option Code\"];\n\n// //TBD if we want this.. TODO: revisit this logic\n// if (matchingOption === \"no\") {\n// return \"FALSE\";\n// }\n// if (matchingOption === \"yes\") {\n// return \"TRUE\";\n// }\n// //======//\n// return matchingOption || \"\";\n// };\n\n//=== Original logic modified on Nov 11 =========//\n// const findMatchingOption = (answer, optsMap) => {\n// const matchingOption = optsMap.find(\n// o => o['value.uuid - External ID'] === answer.value.uuid\n// )?.['DHIS2 Option Code'];\n\n// if (matchingOption === 'no') {\n// return 'FALSE';\n// }\n// if (matchingOption === 'yes') {\n// return 'TRUE';\n// }\n// return matchingOption || '';\n// };\n", + "body": "const delay = ms => new Promise(resolve => setTimeout(resolve, ms));\n\neach(\n $.encounters,\n get(\n 'tracker/trackedEntities',\n {\n orgUnit: $.orgUnit,\n program: $.program,\n filter: [`AYbfTPYMNJH:Eq:${$.data.patient.uuid}`],\n fields: '*',\n },\n {},\n async state => {\n const encounter = state.references.at(-1);\n console.log(encounter.patient.uuid, 'Encounter patient uuid');\n\n const { trackedEntity, enrollments } = state.data?.instances?.[0] || {};\n if (trackedEntity && enrollments) {\n state.TEIs ??= {};\n state.TEIs[encounter.patient.uuid] = {\n trackedEntity,\n enrollments,\n enrollment: enrollments[0]?.enrollment,\n };\n }\n\n await delay(2000);\n return state;\n }\n )\n);\n\nconst processAnswer = (\n answer,\n conceptUuid,\n dataElement,\n optsMap,\n optionSetKey\n) => {\n // console.log('Has answer', conceptUuid, dataElement);\n return typeof answer.value === 'object'\n ? processObjectAnswer(\n answer,\n conceptUuid,\n dataElement,\n optsMap,\n optionSetKey\n )\n : processOtherAnswer(answer, conceptUuid, dataElement);\n};\n\nconst processObjectAnswer = (\n answer,\n conceptUuid,\n dataElement,\n optsMap,\n optionSetKey\n) => {\n if (isDiagnosisByPsychologist(conceptUuid, dataElement)) {\n console.log('Yes done by psychologist..')\n return '' + answer.value.uuid === '278401ee-3d6f-4c65-9455-f1c16d0a7a98';\n }\n //return 'true'; \n return findMatchingOption(answer, optsMap, optionSetKey);\n};\n\nconst processOtherAnswer = (answer, conceptUuid, dataElement) => {\n if (isPhq9Score(answer.value, conceptUuid, dataElement)) {\n console.log('isPhq9Score', isPhq9Score);\n return getRangePhq(answer.value);\n }\n return answer.value;\n};\n\nconst processNoAnswer = (data, conceptUuid, dataElement) => {\n // console.log('No answer', conceptUuid, dataElement);\n if (isEncounterDate(conceptUuid, dataElement)) {\n return data.encounterDatetime.replace('+0000', '');\n }\n return '';\n};\n\nconst findMatchingOption = (answer, optsMap, optionSetKey) => {\n const optionKey = `${answer.formUuid}-${answer.concept.uuid}`; \n\n //const matchingOptionSet = optionSetKey[answer.concept.uuid];\n const matchingOptionSet = optionSetKey[optionKey];\n console.log('optionKey', optionKey);\n console.log('conceptUid', answer.concept.uuid);\n console.log('value uid', answer.value.uuid);\n console.log('value', answer.value.display);\n console.log('matchingOptionSet', matchingOptionSet);\n\n //const answerKey = answerMappingUid\n\n const matchingOption = optsMap.find(\n o =>\n o['value.uuid - External ID'] === answer.value.uuid &&\n o['DHIS2 Option Set UID'] === matchingOptionSet\n )?.['DHIS2 Option Code'] || answer.value.display; //TODO: revisit this logic if optionSet not found\n\n console.log('matchingOption value', matchingOption)\n\n // to convert ALL caps to lowercase per DHIS2 api\n if (matchingOption === 'FALSE') {\n console.log('false option', matchingOption)\n return 'false';\n }\n if (matchingOption === 'TRUE') {\n console.log('true option', matchingOption)\n return 'true';\n }\n ////=========================================//\n\n return matchingOption || '';\n};\n\nconst isEncounterDate = (conceptUuid, dataElement) => {\n return (\n conceptUuid === 'encounter-date' &&\n ['CXS4qAJH2qD', 'I7phgLmRWQq', 'yUT7HyjWurN'].includes(dataElement)\n );\n};\n\nconst isDiagnosisByPsychologist = (conceptUuid, dataElement) =>\n conceptUuid === '722dd83a-c1cf-48ad-ac99-45ac131ccc96' &&\n dataElement === 'pN4iQH4AEzk';\n\nconst isPhq9Score = (value, conceptUuid, dataElement) =>\n typeof value === 'number' &&\n conceptUuid === '5f3d618e-5c89-43bd-8c79-07e4e98c2f23' &&\n dataElement === 'tsFOVnlc6lz';\n\nconst getRangePhq = input => {\n if (input >= 20) return '>20';\n if (input >= 15) return '15_19';\n if (input >= 10) return '10_14';\n if (input >= 5) return '5_9';\n return '0_4';\n};\n\nconst dataValuesMapping = (data, dataValueMap, optsMap, optionSetKey) => {\n return Object.keys(dataValueMap)\n .map(dataElement => {\n const conceptUuid = dataValueMap[dataElement];\n const obsAnswer = data.obs.find(o => o.concept.uuid === conceptUuid);\n const answer = {\n ...obsAnswer,\n formUuid: data.form.uuid\n };\n const value = answer\n ? processAnswer(answer, conceptUuid, dataElement, optsMap, optionSetKey)\n : processNoAnswer(data, conceptUuid, dataElement);\n\n return { dataElement, value };\n })\n .filter(d => d);\n};\n\n// Prepare DHIS2 data model for create events\nfn(state => {\n const createEvent = (data, state) => {\n const { trackedEntity, enrollment } = state.TEIs[data.patient.uuid] || {};\n\n if (!trackedEntity || !enrollment) {\n return null;\n }\n\n return {\n program: state.program,\n orgUnit: state.orgUnit,\n trackedEntity,\n enrollment,\n occurredAt: data.encounterDatetime.replace('+0000', ''),\n };\n };\n\n const handleMissingRecord = (data, state) => {\n const { uuid, display } = data.patient;\n\n console.log(uuid, 'Patient is missing trackedEntity && enrollment');\n\n state.missingRecords ??= {};\n state.missingRecords[uuid] ??= {\n encounters: [],\n patient: display,\n };\n\n state.missingRecords[uuid].encounters.push(data);\n };\n\n const processEncounter = (data, state) => {\n const event = createEvent(data, state);\n if (!event) {\n handleMissingRecord(data, state);\n return null;\n }\n\n const form = state.formMaps[data.form.uuid];\n if (!form?.dataValueMap) {\n return null;\n }\n\n return {\n ...event,\n programStage: form.programStage,\n dataValues: dataValuesMapping(\n data,\n form.dataValueMap,\n state.optsMap,\n state.optionSetKey\n ),\n };\n };\n\n state.encountersMapping = state.encounters\n .map(data => processEncounter(data, state))\n .filter(Boolean);\n\n return state;\n});\n// const findMatchingOption = (answer, optsMap, optionSetKey) => {\n// const answerKeyUid = optionSetKey[answer.concept.uuid];\n\n// const matchingOption = optsMap.find(\n// (o) => o[\"DHIS2 answerKeyUid\"] === answerKeyUid\n// )?.[\"DHIS2 Option Code\"];\n\n// //TBD if we want this.. TODO: revisit this logic\n// if (matchingOption === \"no\") {\n// return \"FALSE\";\n// }\n// if (matchingOption === \"yes\") {\n// return \"TRUE\";\n// }\n// //======//\n// return matchingOption || \"\";\n// };\n\n//=== Original logic modified on Nov 11 =========//\n// const findMatchingOption = (answer, optsMap) => {\n// const matchingOption = optsMap.find(\n// o => o['value.uuid - External ID'] === answer.value.uuid\n// )?.['DHIS2 Option Code'];\n\n// if (matchingOption === 'no') {\n// return 'FALSE';\n// }\n// if (matchingOption === 'yes') {\n// return 'TRUE';\n// }\n// return matchingOption || '';\n// };\n", "adaptor": "@openfn/language-dhis2@5.0.1", "project_credential_id": "9716980f-1b35-4295-adde-b05bc47002da" }, @@ -280,7 +280,7 @@ "Mappings": { "id": "f022ca80-7da0-4fdb-9728-38e34280c516", "name": "Mappings", - "body": "get(\n 'https://raw.githubusercontent.com/OpenFn/openfn-lime-pilot/refs/heads/collections/metadata/collections.json',\n { parseAs: 'json' },\n state => {\n const { cursor, lastRunDateTime, patients, data } = state;\n\n return {\n ...data,\n cursor,\n patients,\n lastRunDateTime,\n };\n }\n);\n\n// Validates if a string matches UUID v4 format\nconst isValidUUID = id => {\n if (!id || typeof id !== 'string') return false;\n\n const UUID_PATTERN =\n /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n return UUID_PATTERN.test(id);\n};\n\nfn(state => {\n const { formMetadata, identifiers, ...rest } = state;\n\n rest.formUuids = formMetadata\n .filter(form => isValidUUID(form['OMRS form.uuid']))\n .map(form => form['OMRS form.uuid']);\n\n rest.orgUnit = identifiers.find(i => i.type === 'ORG_UNIT')?.[\n 'dhis2 attribute id'\n ];\n rest.program = identifiers.find(i => i.type === 'PROGRAM')?.[\n 'dhis2 attribute id'\n ];\n\n rest.patientProgramStage = state.formMaps.patient.programStage;\n\n rest.dhis2PatientNumber = identifiers.find(\n i => i.type === 'DHIS2_PATIENT_NUMBER'\n )?.['omrs identifierType']; //DHIS2 ID or DHIS2 Patient Number\n\n rest.openmrsAutoId = identifiers.find(i => i.type === 'OPENMRS_AUTO_ID')?.[\n 'omrs identifierType'\n ]; //MSF ID or OpenMRS Patient Number\n\n return rest;\n});\n\nfn(state => {\n state.genderOptions = {\n M: 'male',\n F: 'female',\n U: 'unknown',\n O: 'prefer_not_to_answer',\n };\n\n state.placeOflivingMap = state.optsMap\n .filter(o => o['OptionSet name'] === 'Place of Living')\n .reduce((acc, value) => {\n acc[value['value.display - Answers']] = value['DHIS2 Option Code'];\n return acc;\n }, {});\n\n return state;\n});\n", + "body": "get(\n 'https://raw.githubusercontent.com/OpenFn/openfn-lime-pilot/refs/heads/collections/metadata/collections.json',\n { parseAs: 'json' },\n state => {\n const { cursor, lastRunDateTime, patients, data } = state;\n\n return {\n ...data,\n cursor,\n patients,\n lastRunDateTime,\n };\n }\n);\n\n// Validates if a string matches UUID v4 format\nconst isValidUUID = id => {\n if (!id || typeof id !== 'string') return false;\n\n const UUID_PATTERN =\n /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;\n return UUID_PATTERN.test(id);\n};\n\nfn(state => {\n const { formMetadata, identifiers, ...rest } = state;\n\n rest.formUuids = formMetadata\n .filter(form => isValidUUID(form['OMRS form.uuid']))\n .map(form => form['OMRS form.uuid']);\n\n rest.orgUnit = identifiers.find(i => i.type === 'ORG_UNIT')?.[\n 'dhis2 attribute id'\n ];\n rest.program = identifiers.find(i => i.type === 'PROGRAM')?.[\n 'dhis2 attribute id'\n ];\n\n rest.patientProgramStage = state.formMaps.patient.programStage;\n\n rest.dhis2PatientNumber = identifiers.find(\n i => i.type === 'DHIS2_PATIENT_NUMBER'\n )?.['omrs identifierType']; //DHIS2 ID or DHIS2 Patient Number\n\n rest.openmrsAutoId = identifiers.find(i => i.type === 'OPENMRS_AUTO_ID')?.[\n 'omrs identifierType'\n ]; //MSF ID or OpenMRS Patient Number\n\n return rest;\n});\n\nfn(state => {\n //TODO: Update dynamically like placeOfLivingMap?\n state.genderOptions = {\n M: 'male',\n F: 'female',\n U: 'unknown',\n O: 'prefer_not_to_answer',\n };\n\n state.placeOflivingMap = state.optsMap\n .filter(o => o['OptionSet name'] === 'Place of Living')\n .reduce((acc, value) => {\n acc[value['value.display - Answers']] = value['DHIS2 Option Code'];\n return acc;\n }, {});\n\n return state;\n});\n", "adaptor": "@openfn/language-http@latest", "project_credential_id": null }, diff --git a/workflows/wf2/2-mappings.js b/workflows/wf2/2-mappings.js index 261004d..1ac8e89 100644 --- a/workflows/wf2/2-mappings.js +++ b/workflows/wf2/2-mappings.js @@ -50,6 +50,7 @@ fn(state => { }); fn(state => { + //TODO: Update dynamically like placeOfLivingMap? state.genderOptions = { M: 'male', F: 'female', diff --git a/workflows/wf2/5-get-teis.js b/workflows/wf2/5-get-teis.js index 95a0d08..0d2d160 100644 --- a/workflows/wf2/5-get-teis.js +++ b/workflows/wf2/5-get-teis.js @@ -58,13 +58,16 @@ const processObjectAnswer = ( optionSetKey ) => { if (isDiagnosisByPsychologist(conceptUuid, dataElement)) { + console.log('Yes done by psychologist..') return '' + answer.value.uuid === '278401ee-3d6f-4c65-9455-f1c16d0a7a98'; } + //return 'true'; return findMatchingOption(answer, optsMap, optionSetKey); }; const processOtherAnswer = (answer, conceptUuid, dataElement) => { if (isPhq9Score(answer.value, conceptUuid, dataElement)) { + console.log('isPhq9Score', isPhq9Score); return getRangePhq(answer.value); } return answer.value;