diff --git a/docs/ctc-adverse-event.csv b/docs/ctc-adverse-event.csv new file mode 100644 index 00000000..75800433 --- /dev/null +++ b/docs/ctc-adverse-event.csv @@ -0,0 +1,4 @@ +mrn,adverseEventId,adverseEventCode,adverseEventCodeSystem,adverseEventDisplayText,suspectedCauseId,suspectedCauseType,seriousness,seriousnessCodeSystem,seriousnessDisplayText,category,categoryCodeSystem,categoryDisplayText,severity,actuality,studyId,effectiveDate,recordedDate +mrn-full-example,example-id-1,event-code,code-system,code-display,cause-id,resourceType,seriousness-code,code-system,seriousness-display,category-code,code-system,category-dislpay,mild,actual,id,1994-12-09,1994-12-09 +mrn-two-category-example,example-id-2,event-code,code-system,code-display,cause-id,resourceType,seriousness-code,code-system,seriousness-display,category-code|category-code,code-system|code-system,category-display|category-display,mild,actual,id,1994-12-09,1994-12-09 +mrn-minimal-example,,code-from-default-system,,,,,,,,,,,,,,1994-12-09, diff --git a/src/client/MCODEClient.js b/src/client/MCODEClient.js index db41ec82..bcdea46d 100644 --- a/src/client/MCODEClient.js +++ b/src/client/MCODEClient.js @@ -6,6 +6,7 @@ const { CSVCancerRelatedMedicationRequestExtractor, CSVClinicalTrialInformationExtractor, CSVConditionExtractor, + CSVCTCAdverseEventExtractor, CSVObservationExtractor, CSVPatientExtractor, CSVProcedureExtractor, @@ -35,6 +36,7 @@ class MCODEClient extends BaseClient { CSVCancerRelatedMedicationRequestExtractor, CSVClinicalTrialInformationExtractor, CSVConditionExtractor, + CSVCTCAdverseEventExtractor, CSVObservationExtractor, CSVPatientExtractor, CSVProcedureExtractor, @@ -67,6 +69,7 @@ class MCODEClient extends BaseClient { { type: 'CSVProcedureExtractor', dependencies: ['CSVPatientExtractor'] }, { type: 'CSVObservationExtractor', dependencies: ['CSVPatientExtractor'] }, { type: 'CSVAdverseEventExtractor', dependencies: ['CSVPatientExtractor'] }, + { type: 'CSVCTCAdverseEventExtractor', dependencies: ['CSVPatientExtractor'] }, ]; // Sort extractors based on order and dependencies this.extractorConfig = sortExtractors(this.extractorConfig, dependencyInfo); diff --git a/src/extractors/CSVCTCAdverseEventExtractor.js b/src/extractors/CSVCTCAdverseEventExtractor.js new file mode 100644 index 00000000..dbea8e1e --- /dev/null +++ b/src/extractors/CSVCTCAdverseEventExtractor.js @@ -0,0 +1,99 @@ +const { BaseCSVExtractor } = require('./BaseCSVExtractor'); +const { generateMcodeResources } = require('../templates'); +const { getEmptyBundle } = require('../helpers/fhirUtils'); +const { getPatientFromContext } = require('../helpers/contextUtils'); +const { formatDateTime } = require('../helpers/dateUtils'); +const logger = require('../helpers/logger'); + +// Formats data to be passed into template-friendly format +function formatData(adverseEventData, patientId) { + logger.debug('Reformatting adverse event data from CSV into template format'); + return adverseEventData.map((data) => { + const { + adverseeventid: adverseEventId, + adverseeventcode: adverseEventCode, + adverseeventcodesystem: adverseEventCodeSystem, + adverseeventdisplaytext: adverseEventDisplayText, + suspectedcauseid: suspectedCauseId, + suspectedcausetype: suspectedCauseType, + seriousness, + seriousnesscodesystem: seriousnessCodeSystem, + seriousnessdisplaytext: seriousnessDisplayText, + category, + categorycodesystem: categoryCodeSystem, + categorydisplaytext: categoryDisplayText, + severity, + actuality, + studyid: studyId, + effectivedate: effectiveDate, + recordeddate: recordedDate, + } = data; + + if (!(adverseEventCode && effectiveDate)) { + throw new Error('The adverse event is missing an expected attribute. Adverse event code and effective date are all required.'); + } + + const categoryCodes = category.split('|'); + const categorySystems = categoryCodeSystem.split('|'); + const categoryDisplays = categoryDisplayText.split('|'); + + if (!(categoryCodes.length === categorySystems.length && categoryCodes.length === categoryDisplays.length)) { + throw new Error('A category attribute on the adverse event is missing a corresponding categoryCodeSystem or categoryDisplayText value.'); + } + + + return { + ...(adverseEventId && { id: adverseEventId }), + subjectId: patientId, + code: adverseEventCode, + system: !adverseEventCodeSystem ? 'http://snomed.info/sct' : adverseEventCodeSystem, + display: adverseEventDisplayText, + suspectedCauseId, + suspectedCauseType, + seriousnessCode: seriousness, + seriousnessCodeSystem: !seriousnessCodeSystem ? 'http://terminology.hl7.org/CodeSystem/adverse-event-seriousness' : seriousnessCodeSystem, + seriousnessDisplayText, + category: categoryCodes.map((categoryCode, index) => { + if (!categoryCode) return null; + const categoryCoding = { code: categoryCode, system: categorySystems[index] ? categorySystems[index] : 'http://terminology.hl7.org/CodeSystem/adverse-event-category' }; + if (categoryDisplays[index]) categoryCoding.display = categoryDisplays[index]; + return categoryCoding; + }), + severity, + actuality: !actuality ? 'actual' : actuality, + studyId, + effectiveDateTime: formatDateTime(effectiveDate), + recordedDateTime: !recordedDate ? null : formatDateTime(recordedDate), + }; + }); +} + +class CSVCTCAdverseEventExtractor extends BaseCSVExtractor { + constructor({ filePath, url }) { + super({ filePath, url }); + } + + async getAdverseEventData(mrn) { + logger.debug('Getting Adverse Event Data'); + return this.csvModule.get('mrn', mrn); + } + + async get({ mrn, context }) { + const adverseEventData = await this.getAdverseEventData(mrn); + if (adverseEventData.length === 0) { + logger.warn('No adverse event data found for patient'); + return getEmptyBundle(); + } + const patientId = getPatientFromContext(context).id; + + // Reformat data + const formattedData = formatData(adverseEventData, patientId); + + // Fill templates + return generateMcodeResources('CTCAdverseEvent', formattedData); + } +} + +module.exports = { + CSVCTCAdverseEventExtractor, +}; diff --git a/src/extractors/index.js b/src/extractors/index.js index 7cb6f00a..49c5aefb 100644 --- a/src/extractors/index.js +++ b/src/extractors/index.js @@ -5,6 +5,7 @@ const { CSVCancerRelatedMedicationAdministrationExtractor } = require('./CSVCanc const { CSVCancerRelatedMedicationRequestExtractor } = require('./CSVCancerRelatedMedicationRequestExtractor'); const { CSVClinicalTrialInformationExtractor } = require('./CSVClinicalTrialInformationExtractor'); const { CSVConditionExtractor } = require('./CSVConditionExtractor'); +const { CSVCTCAdverseEventExtractor } = require('./CSVCTCAdverseEventExtractor'); const { CSVObservationExtractor } = require('./CSVObservationExtractor'); const { CSVPatientExtractor } = require('./CSVPatientExtractor'); const { CSVProcedureExtractor } = require('./CSVProcedureExtractor'); @@ -32,6 +33,7 @@ module.exports = { CSVCancerRelatedMedicationRequestExtractor, CSVClinicalTrialInformationExtractor, CSVConditionExtractor, + CSVCTCAdverseEventExtractor, CSVObservationExtractor, CSVPatientExtractor, CSVProcedureExtractor, diff --git a/src/templates/CTCAdverseEventTemplate.js b/src/templates/CTCAdverseEventTemplate.js new file mode 100644 index 00000000..fe257710 --- /dev/null +++ b/src/templates/CTCAdverseEventTemplate.js @@ -0,0 +1,101 @@ +const { coding, reference } = require('./snippets'); +const { ifAllArgsObj, ifSomeArgsObj, ifAllArgs, ifSomeArgsArr } = require('../helpers/templateUtils'); + +function eventTemplate(eventCoding) { + return { + event: { + coding: [ + coding(eventCoding), + ], + }, + }; +} + +function suspectedCauseTemplate({ suspectedCauseId, suspectedCauseType }) { + return { + suspectEntity: [ + { + instance: + reference({ id: suspectedCauseId, resourceType: suspectedCauseType }), + }, + ], + }; +} + + +function seriousnessTemplate(seriousnessCoding) { + return { + seriousness: { + coding: [ + coding(seriousnessCoding), + ], + }, + }; +} + +function individualCategoryTemplate(category) { + return { + coding: [coding(category), + ], + }; +} + +function categoryArrayTemplate(categoryArr) { + const category = categoryArr.map(individualCategoryTemplate); + return { category }; +} + +function severityTemplate(severityCode) { + return { + severity: { + coding: [ + coding({ + code: severityCode, + system: 'http://terminology.hl7.org/CodeSystem/adverse-event-severity', + }), + ], + }, + }; +} + +function studyTemplate(studyId) { + return { + study: [ + reference({ id: studyId, resourceType: 'ResearchStudy' }), + ], + }; +} + +function recordedDateTemplate(recordedDateTime) { + return { + recordedDate: recordedDateTime, + }; +} + +function CTCAdverseEventTemplate({ + id, subjectId, code, system, display, suspectedCauseId, suspectedCauseType, seriousnessCode, seriousnessCodeSystem, seriousnessDisplayText, category, + severity, actuality, studyId, effectiveDateTime, recordedDateTime, +}) { + if (!(subjectId && code && system && effectiveDateTime && actuality)) { + throw Error('Trying to render an AdverseEventTemplate, but a required argument is messing; ensure that subjectId, code, system, actuality, and effectiveDateTime are all present'); + } + + return { + resourceType: 'AdverseEvent', + id, + subject: reference({ id: subjectId, resourceType: 'Patient' }), + ...ifSomeArgsObj(eventTemplate)({ code, system, display }), + ...ifAllArgsObj(suspectedCauseTemplate)({ suspectedCauseId, suspectedCauseType }), + ...ifSomeArgsObj(seriousnessTemplate)({ code: seriousnessCode, system: seriousnessCodeSystem, display: seriousnessDisplayText }), + ...ifSomeArgsArr(categoryArrayTemplate)(category), + ...ifAllArgs(severityTemplate)(severity), + actuality, + ...ifAllArgs(studyTemplate)(studyId), + date: effectiveDateTime, + ...ifAllArgs(recordedDateTemplate)(recordedDateTime), + }; +} + +module.exports = { + CTCAdverseEventTemplate, +}; diff --git a/src/templates/ResourceGenerator.js b/src/templates/ResourceGenerator.js index 01fadee3..1bc4036c 100644 --- a/src/templates/ResourceGenerator.js +++ b/src/templates/ResourceGenerator.js @@ -8,6 +8,7 @@ const { cancerRelatedMedicationAdministrationTemplate } = require('./CancerRelat const { cancerRelatedMedicationRequestTemplate } = require('./CancerRelatedMedicationRequestTemplate'); const { carePlanWithReviewTemplate } = require('./CarePlanWithReviewTemplate'); const { conditionTemplate } = require('./ConditionTemplate'); +const { CTCAdverseEventTemplate } = require('./CTCAdverseEventTemplate'); const { observationTemplate } = require('./ObservationTemplate'); const { patientTemplate } = require('./PatientTemplate'); const { procedureTemplate } = require('./ProcedureTemplate'); @@ -23,6 +24,7 @@ const fhirTemplateLookup = { CancerRelatedMedicationRequest: cancerRelatedMedicationRequestTemplate, CarePlanWithReview: carePlanWithReviewTemplate, Condition: conditionTemplate, + CTCAdverseEvent: CTCAdverseEventTemplate, Observation: observationTemplate, Patient: patientTemplate, Procedure: procedureTemplate, diff --git a/test/sample-client-data/ctc-adverse-event-information.csv b/test/sample-client-data/ctc-adverse-event-information.csv new file mode 100644 index 00000000..a2d07aa9 --- /dev/null +++ b/test/sample-client-data/ctc-adverse-event-information.csv @@ -0,0 +1,4 @@ +mrn,adverseEventId,adverseEventCode,adverseEventCodeSystem,adverseEventDisplayText,suspectedCauseId,suspectedCauseType,seriousness,seriousnessCodeSystem,seriousnessDisplayText,category,categoryCodeSystem,categoryDisplayText,severity,actuality,studyId,effectiveDate,recordedDate +123,adverseEventId-1,109006,code-system,Anxiety disorder of childhood OR adolescence,procedure-id,Procedure,serious,http://terminology.hl7.org/CodeSystem/adverse-event-seriousness,Serious,product-use-error|product-quality|wrong-rate,http://terminology.hl7.org/CodeSystem/adverse-event-category|http://snomed.info/sct|http://terminology.hl7.org/CodeSystem/adverse-event-category,Product Use Error|Product Quality|Wrong Rate,severe,actual,researchId-1,12-09-1994,12-09-1994 +456,adverseEventId-2,134006,http://snomed.info/sct,Decreased hair growth,medicationId-1,Medication,non-serious,http://terminology.hl7.org/CodeSystem/adverse-event-seriousness,Non-serious,product-quality|wrong-rate,http://terminology.hl7.org/CodeSystem/adverse-event-category|,Product Quality|,mild,potential,researchId-2,12-10-1995,12-10-1995 +789,adverseEventId-3,150003,,,,,,,,product-use-error,,,,,,12-09-1994, \ No newline at end of file