diff --git a/config/csv.config.example.json b/config/csv.config.example.json index 66862dda..840bb7e3 100644 --- a/config/csv.config.example.json +++ b/config/csv.config.example.json @@ -56,10 +56,17 @@ } }, { - "label": "cancerRelatedMedication", - "type": "CSVCancerRelatedMedicationExtractor", + "label": "cancerRelatedMedicationAdministration", + "type": "CSVCancerRelatedMedicationAdministrationExtractor", "constructorArgs": { - "filePath": "./data/cancer-related-medication-information.csv" + "filePath": "./data/cancer-related-medication-administration-information.csv" + } + }, + { + "label": "cancerRelatedMedicationRequest", + "type": "CSVCancerRelatedMedicationRequestExtractor", + "constructorArgs": { + "filePath": "./data/cancer-related-medication-request-information.csv" } }, { diff --git a/docs/CSV_Templates.xlsx b/docs/CSV_Templates.xlsx old mode 100644 new mode 100755 index 326a483f..8e808c72 Binary files a/docs/CSV_Templates.xlsx and b/docs/CSV_Templates.xlsx differ diff --git a/docs/cancer-related-medication.csv b/docs/cancer-related-medication-administration.csv similarity index 100% rename from docs/cancer-related-medication.csv rename to docs/cancer-related-medication-administration.csv diff --git a/docs/cancer-related-medication-request.csv b/docs/cancer-related-medication-request.csv new file mode 100644 index 00000000..44741bdd --- /dev/null +++ b/docs/cancer-related-medication-request.csv @@ -0,0 +1,3 @@ +mrn,requestId,code,codeSystem,displayText,treatmentReasonCode,treatmentReasonCodeSystem,treatmentReasonDisplayText,procedureIntent,status,intent,authoredOn,requesterId +mrn-1,requestId-1,code,code-system,code-text,code,code-system,display-text,procedure-intent-code,status-code,intent-code,YYY-MM-DD,requesterId-1 +mrn-2,requestId-2,code,code-system,code-text,code,code-system,display-text,procedure-intent-code,status-code,intent-code,YYY-MM-DD,requesterId-2 \ No newline at end of file diff --git a/src/client/MCODEClient.js b/src/client/MCODEClient.js index 50456e59..db41ec82 100644 --- a/src/client/MCODEClient.js +++ b/src/client/MCODEClient.js @@ -2,7 +2,8 @@ const { BaseClient } = require('./BaseClient'); const { CSVAdverseEventExtractor, CSVCancerDiseaseStatusExtractor, - CSVCancerRelatedMedicationExtractor, + CSVCancerRelatedMedicationAdministrationExtractor, + CSVCancerRelatedMedicationRequestExtractor, CSVClinicalTrialInformationExtractor, CSVConditionExtractor, CSVObservationExtractor, @@ -30,7 +31,8 @@ class MCODEClient extends BaseClient { this.registerExtractors( CSVAdverseEventExtractor, CSVCancerDiseaseStatusExtractor, - CSVCancerRelatedMedicationExtractor, + CSVCancerRelatedMedicationAdministrationExtractor, + CSVCancerRelatedMedicationRequestExtractor, CSVClinicalTrialInformationExtractor, CSVConditionExtractor, CSVObservationExtractor, @@ -60,7 +62,8 @@ class MCODEClient extends BaseClient { { type: 'CSVClinicalTrialInformationExtractor', dependencies: ['CSVPatientExtractor'] }, { type: 'CSVTreatmentPlanChangeExtractor', dependencies: ['CSVPatientExtractor'] }, { type: 'CSVStagingExtractor', dependencies: ['CSVPatientExtractor'] }, - { type: 'CSVCancerRelatedMedicationExtractor', dependencies: ['CSVPatientExtractor'] }, + { type: 'CSVCancerRelatedMedicationAdministrationExtractor', dependencies: ['CSVPatientExtractor'] }, + { type: 'CSVCancerRelatedMedicationRequestExtractor', dependencies: ['CSVPatientExtractor'] }, { type: 'CSVProcedureExtractor', dependencies: ['CSVPatientExtractor'] }, { type: 'CSVObservationExtractor', dependencies: ['CSVPatientExtractor'] }, { type: 'CSVAdverseEventExtractor', dependencies: ['CSVPatientExtractor'] }, diff --git a/src/extractors/CSVCancerRelatedMedicationExtractor.js b/src/extractors/CSVCancerRelatedMedicationAdministrationExtractor.js similarity index 74% rename from src/extractors/CSVCancerRelatedMedicationExtractor.js rename to src/extractors/CSVCancerRelatedMedicationAdministrationExtractor.js index a2780096..4763e216 100644 --- a/src/extractors/CSVCancerRelatedMedicationExtractor.js +++ b/src/extractors/CSVCancerRelatedMedicationAdministrationExtractor.js @@ -7,7 +7,7 @@ const logger = require('../helpers/logger'); function formatData(medicationData, patientId) { - logger.debug('Reformatting cancer-related medication data from CSV into template format'); + logger.debug('Reformatting cancer-related medication administration data from CSV into template format'); return medicationData.map((medication) => { const { @@ -25,7 +25,7 @@ function formatData(medicationData, patientId) { } = medication; if (!(code && codeSystem && status)) { - throw new Error('The cancer-related medication is missing an expected element; code, code system, and status are all required values.'); + throw new Error('The cancer-related medication administration is missing an expected element; code, code system, and status are all required values.'); } return { @@ -45,20 +45,20 @@ function formatData(medicationData, patientId) { }); } -class CSVCancerRelatedMedicationExtractor extends BaseCSVExtractor { +class CSVCancerRelatedMedicationAdministrationExtractor extends BaseCSVExtractor { constructor({ filePath, url }) { super({ filePath, url }); } async getMedicationData(mrn) { - logger.debug('Getting Cancer Related Medication Data'); + logger.debug('Getting Cancer Related Medication Administration Data'); return this.csvModule.get('mrn', mrn); } async get({ mrn, context }) { const medicationData = await this.getMedicationData(mrn); if (medicationData.length === 0) { - logger.warn('No medication data found for patient'); + logger.warn('No medication administration data found for patient'); return getEmptyBundle(); } const patientId = getPatientFromContext(context).id; @@ -67,10 +67,10 @@ class CSVCancerRelatedMedicationExtractor extends BaseCSVExtractor { const formattedData = formatData(medicationData, patientId); // Fill templates - return generateMcodeResources('CancerRelatedMedication', formattedData); + return generateMcodeResources('CancerRelatedMedicationAdministration', formattedData); } } module.exports = { - CSVCancerRelatedMedicationExtractor, + CSVCancerRelatedMedicationAdministrationExtractor, }; diff --git a/src/extractors/CSVCancerRelatedMedicationRequestExtractor.js b/src/extractors/CSVCancerRelatedMedicationRequestExtractor.js new file mode 100644 index 00000000..74e66fdf --- /dev/null +++ b/src/extractors/CSVCancerRelatedMedicationRequestExtractor.js @@ -0,0 +1,78 @@ +const { BaseCSVExtractor } = require('./BaseCSVExtractor'); +const { generateMcodeResources } = require('../templates'); +const { getPatientFromContext } = require('../helpers/contextUtils'); +const { getEmptyBundle } = require('../helpers/fhirUtils'); +const { formatDateTime } = require('../helpers/dateUtils'); +const logger = require('../helpers/logger'); + + +function formatData(medicationData, patientId) { + logger.debug('Reformatting cancer-related medication request data from CSV into template format'); + + return medicationData.map((medication) => { + const { + requestid: requestId, + code, + codesystem: codeSystem, + displaytext: displayText, + treatmentreasoncode: treatmentReasonCode, + treatmentreasoncodesystem: treatmentReasonCodeSystem, + treatmentreasondisplaytext: treatmentReasonDisplayText, + procedureintent: procedureIntent, + status, + intent, + authoredon: authoredOn, + requesterid: requesterId, + } = medication; + + if (!(code && codeSystem && status && intent && requesterId && authoredOn)) { + throw new Error('The cancer-related medication request is missing an expected element; code, code system, status, authoredOn, requesterId, and intent are all required values.'); + } + + return { + ...(requestId && { id: requestId }), + subjectId: patientId, + code, + codeSystem, + displayText, + treatmentReasonCode, + treatmentReasonCodeSystem, + treatmentReasonDisplayText, + procedureIntent, + status, + intent, + authoredOn: formatDateTime(authoredOn), + requesterId, + }; + }); +} + +class CSVCancerRelatedMedicationRequestExtractor extends BaseCSVExtractor { + constructor({ filePath, url }) { + super({ filePath, url }); + } + + async getMedicationData(mrn) { + logger.debug('Getting Cancer Related Medication Request Data'); + return this.csvModule.get('mrn', mrn); + } + + async get({ mrn, context }) { + const medicationData = await this.getMedicationData(mrn); + if (medicationData.length === 0) { + logger.warn('No medication request data found for patient'); + return getEmptyBundle(); + } + const patientId = getPatientFromContext(context).id; + + // Reformat data + const formattedData = formatData(medicationData, patientId); + + // Fill templates + return generateMcodeResources('CancerRelatedMedicationRequest', formattedData); + } +} + +module.exports = { + CSVCancerRelatedMedicationRequestExtractor, +}; diff --git a/src/extractors/index.js b/src/extractors/index.js index ce2417fc..7cb6f00a 100644 --- a/src/extractors/index.js +++ b/src/extractors/index.js @@ -1,7 +1,8 @@ const { BaseFHIRExtractor } = require('./BaseFHIRExtractor'); const { CSVAdverseEventExtractor } = require('./CSVAdverseEventExtractor'); const { CSVCancerDiseaseStatusExtractor } = require('./CSVCancerDiseaseStatusExtractor'); -const { CSVCancerRelatedMedicationExtractor } = require('./CSVCancerRelatedMedicationExtractor'); +const { CSVCancerRelatedMedicationAdministrationExtractor } = require('./CSVCancerRelatedMedicationAdministrationExtractor'); +const { CSVCancerRelatedMedicationRequestExtractor } = require('./CSVCancerRelatedMedicationRequestExtractor'); const { CSVClinicalTrialInformationExtractor } = require('./CSVClinicalTrialInformationExtractor'); const { CSVConditionExtractor } = require('./CSVConditionExtractor'); const { CSVObservationExtractor } = require('./CSVObservationExtractor'); @@ -27,7 +28,8 @@ module.exports = { BaseFHIRExtractor, CSVAdverseEventExtractor, CSVCancerDiseaseStatusExtractor, - CSVCancerRelatedMedicationExtractor, + CSVCancerRelatedMedicationAdministrationExtractor, + CSVCancerRelatedMedicationRequestExtractor, CSVClinicalTrialInformationExtractor, CSVConditionExtractor, CSVObservationExtractor, diff --git a/src/index.js b/src/index.js index cb5c5406..040cad44 100644 --- a/src/index.js +++ b/src/index.js @@ -12,7 +12,8 @@ const { BaseFHIRExtractor, CSVAdverseEventExtractor, CSVCancerDiseaseStatusExtractor, - CSVCancerRelatedMedicationExtractor, + CSVCancerRelatedMedicationAdministrationExtractor, + CSVCancerRelatedMedicationRequestExtractor, CSVClinicalTrialInformationExtractor, CSVConditionExtractor, CSVObservationExtractor, @@ -85,7 +86,8 @@ module.exports = { BaseFHIRModule, CSVAdverseEventExtractor, CSVCancerDiseaseStatusExtractor, - CSVCancerRelatedMedicationExtractor, + CSVCancerRelatedMedicationAdministrationExtractor, + CSVCancerRelatedMedicationRequestExtractor, CSVClinicalTrialInformationExtractor, CSVConditionExtractor, CSVFileModule, diff --git a/src/templates/CancerRelatedMedicationTemplate.js b/src/templates/CancerRelatedMedicationAdministrationTemplate.js similarity index 61% rename from src/templates/CancerRelatedMedicationTemplate.js rename to src/templates/CancerRelatedMedicationAdministrationTemplate.js index f29abf59..2d6d37da 100644 --- a/src/templates/CancerRelatedMedicationTemplate.js +++ b/src/templates/CancerRelatedMedicationAdministrationTemplate.js @@ -1,9 +1,10 @@ const { - coding, dataAbsentReasonExtension, extensionArr, - reference, valueX, + medicationTemplate, + subjectTemplate, + treatmentReasonTemplate, } = require('./snippets'); const { ifAllArgsObj } = require('../helpers/templateUtils'); @@ -14,21 +15,6 @@ function treatmentIntentTemplate({ treatmentIntent }) { }; } -function medicationTemplate({ code, codeSystem, displayText }) { - return { - medicationCodeableConcept: { - coding: [coding({ system: codeSystem, code, display: displayText }), - ], - }, - }; -} - -function subjectTemplate({ id }) { - return { - subject: reference({ id, resourceType: 'Patient' }), - }; -} - function periodTemplate({ startDate, endDate }) { // If start and end date are not provided, indicate data absent with extension. if (!startDate && !endDate) { @@ -45,19 +31,7 @@ function periodTemplate({ startDate, endDate }) { }; } -function treatmentReasonTemplate({ treatmentReasonCode, treatmentReasonCodeSystem, treatmentReasonDisplayText }) { - return { - reasonCode: [ - { - coding: [coding({ system: treatmentReasonCodeSystem, code: treatmentReasonCode, display: treatmentReasonDisplayText }), - ], - }, - ], - }; -} - - -function cancerRelatedMedicationTemplate({ +function cancerRelatedMedicationAdministrationTemplate({ subjectId, id, code, @@ -72,15 +46,15 @@ function cancerRelatedMedicationTemplate({ status, }) { if (!(subjectId && code && codeSystem && status)) { - throw Error('Trying to render a CancerRelatedMedicationTemplate, but a required argument is missing; ensure that subjectId, code, code system, and status are all present'); + throw Error('Trying to render a CancerRelatedMedicationAdministrationTemplate, but a required argument is missing; ensure that subjectId, code, code system, and status are all present'); } return { - resourceType: 'MedicationStatement', + resourceType: 'MedicationAdministration', id, meta: { profile: [ - 'http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-cancer-related-medication-statement', + 'http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-cancer-related-medication-administration', ], }, ...extensionArr(ifAllArgsObj(treatmentIntentTemplate)({ treatmentIntent })), @@ -93,5 +67,5 @@ function cancerRelatedMedicationTemplate({ } module.exports = { - cancerRelatedMedicationTemplate, + cancerRelatedMedicationAdministrationTemplate, }; diff --git a/src/templates/CancerRelatedMedicationRequestTemplate.js b/src/templates/CancerRelatedMedicationRequestTemplate.js new file mode 100644 index 00000000..4d37d357 --- /dev/null +++ b/src/templates/CancerRelatedMedicationRequestTemplate.js @@ -0,0 +1,66 @@ +const { + extensionArr, + reference, + valueX, + medicationTemplate, + subjectTemplate, + treatmentReasonTemplate, +} = require('./snippets'); +const { ifAllArgsObj } = require('../helpers/templateUtils'); + +function procedureIntentTemplate({ procedureIntent }) { + return { + url: 'http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-procedure-intent', + ...valueX({ code: procedureIntent, system: 'http://snomed.info/sct' }, 'valueCodeableConcept'), + }; +} + +function requesterTemplate({ id }) { + return { + requester: reference({ id }), + }; +} + +function cancerRelatedMedicationRequestTemplate({ + subjectId, + id, + code, + codeSystem, + displayText, + treatmentReasonCode, + treatmentReasonCodeSystem, + treatmentReasonDisplayText, + procedureIntent, + status, + intent, + authoredOn, + requesterId, +}) { + if (!(subjectId && code && codeSystem && status && intent && requesterId && authoredOn)) { + const e1 = 'Trying to render a CancerRelatedMedicationRequestTemplate, but a required argument is missing; '; + const e2 = 'ensure that subjectId, code, codeSystem, intent, requesterId, authoredOn, and status are all present'; + throw Error(e1 + e2); + } + + return { + resourceType: 'MedicationRequest', + id, + meta: { + profile: [ + 'http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-cancer-related-medication-request', + ], + }, + ...extensionArr(ifAllArgsObj(procedureIntentTemplate)({ procedureIntent })), + status, + intent, + ...medicationTemplate({ code, codeSystem, displayText }), + ...ifAllArgsObj(subjectTemplate)({ id: subjectId }), + authoredOn, + ...ifAllArgsObj(requesterTemplate)({ id: requesterId }), + ...ifAllArgsObj(treatmentReasonTemplate)({ treatmentReasonCode, treatmentReasonCodeSystem, treatmentReasonDisplayText }), + }; +} + +module.exports = { + cancerRelatedMedicationRequestTemplate, +}; diff --git a/src/templates/ResourceGenerator.js b/src/templates/ResourceGenerator.js index d73cc9eb..01fadee3 100644 --- a/src/templates/ResourceGenerator.js +++ b/src/templates/ResourceGenerator.js @@ -4,7 +4,8 @@ const logger = require('../helpers/logger'); const { adverseEventTemplate } = require('./AdverseEventTemplate'); const { cancerDiseaseStatusTemplate } = require('./CancerDiseaseStatusTemplate'); -const { cancerRelatedMedicationTemplate } = require('./CancerRelatedMedicationTemplate'); +const { cancerRelatedMedicationAdministrationTemplate } = require('./CancerRelatedMedicationAdministrationTemplate'); +const { cancerRelatedMedicationRequestTemplate } = require('./CancerRelatedMedicationRequestTemplate'); const { carePlanWithReviewTemplate } = require('./CarePlanWithReviewTemplate'); const { conditionTemplate } = require('./ConditionTemplate'); const { observationTemplate } = require('./ObservationTemplate'); @@ -18,7 +19,8 @@ const { tnmCategoryTemplate } = require('./TNMCategoryTemplate'); const fhirTemplateLookup = { AdverseEvent: adverseEventTemplate, CancerDiseaseStatus: cancerDiseaseStatusTemplate, - CancerRelatedMedication: cancerRelatedMedicationTemplate, + CancerRelatedMedicationAdministration: cancerRelatedMedicationAdministrationTemplate, + CancerRelatedMedicationRequest: cancerRelatedMedicationRequestTemplate, CarePlanWithReview: carePlanWithReviewTemplate, Condition: conditionTemplate, Observation: observationTemplate, diff --git a/src/templates/snippets/index.js b/src/templates/snippets/index.js index 1c5eac41..f21199cb 100644 --- a/src/templates/snippets/index.js +++ b/src/templates/snippets/index.js @@ -7,6 +7,9 @@ const { effectiveX } = require('./effectiveX'); const { identifier, identifierArr } = require('./identifier'); const { bodySiteTemplate } = require('./bodySiteTemplate'); const { stagingMethodTemplate } = require('./cancerStaging'); +const { medicationTemplate } = require('./medication'); +const { subjectTemplate } = require('./subject'); +const { treatmentReasonTemplate } = require('./treatmentReason'); module.exports = { bodySiteTemplate, @@ -16,9 +19,12 @@ module.exports = { extensionArr, identifier, identifierArr, + medicationTemplate, meta, narrative, reference, stagingMethodTemplate, + subjectTemplate, + treatmentReasonTemplate, valueX, }; diff --git a/src/templates/snippets/medication.js b/src/templates/snippets/medication.js new file mode 100644 index 00000000..af1fe22c --- /dev/null +++ b/src/templates/snippets/medication.js @@ -0,0 +1,14 @@ +const { coding } = require('./coding'); + +function medicationTemplate({ code, codeSystem, displayText }) { + return { + medicationCodeableConcept: { + coding: [coding({ system: codeSystem, code, display: displayText }), + ], + }, + }; +} + +module.exports = { + medicationTemplate, +}; diff --git a/src/templates/snippets/subject.js b/src/templates/snippets/subject.js new file mode 100644 index 00000000..4a23147b --- /dev/null +++ b/src/templates/snippets/subject.js @@ -0,0 +1,11 @@ +const { reference } = require('./reference'); + +function subjectTemplate({ id }) { + return { + subject: reference({ id, resourceType: 'Patient' }), + }; +} + +module.exports = { + subjectTemplate, +}; diff --git a/src/templates/snippets/treatmentReason.js b/src/templates/snippets/treatmentReason.js new file mode 100644 index 00000000..5d2570b1 --- /dev/null +++ b/src/templates/snippets/treatmentReason.js @@ -0,0 +1,16 @@ +const { coding } = require('./coding'); + +function treatmentReasonTemplate({ treatmentReasonCode, treatmentReasonCodeSystem, treatmentReasonDisplayText }) { + return { + reasonCode: [ + { + coding: [coding({ system: treatmentReasonCodeSystem, code: treatmentReasonCode, display: treatmentReasonDisplayText }), + ], + }, + ], + }; +} + +module.exports = { + treatmentReasonTemplate, +}; diff --git a/test/extractors/CSVCancerRelatedMedicationAdministrationExtractor.test.js b/test/extractors/CSVCancerRelatedMedicationAdministrationExtractor.test.js new file mode 100644 index 00000000..3b3d2230 --- /dev/null +++ b/test/extractors/CSVCancerRelatedMedicationAdministrationExtractor.test.js @@ -0,0 +1,86 @@ +const path = require('path'); +const rewire = require('rewire'); +const _ = require('lodash'); +const { CSVCancerRelatedMedicationAdministrationExtractor } = require('../../src/extractors'); +const exampleCSVMedicationModuleResponse = require('./fixtures/csv-medication-administration-module-response.json'); +const exampleCSVMedicationBundle = require('./fixtures/csv-medication-administration-bundle.json'); +const { getPatientFromContext } = require('../../src/helpers/contextUtils'); +const MOCK_CONTEXT = require('./fixtures/context-with-patient.json'); + +// Rewired extractor for helper tests +const CSVCancerRelatedMedicationExtractorRewired = rewire('../../src/extractors/CSVCancerRelatedMedicationAdministrationExtractor.js'); + +// Constants for tests +const MOCK_PATIENT_MRN = 'mrn-1'; // linked to values in example-module-response and context-with-patient above +const MOCK_CSV_PATH = path.join(__dirname, 'fixtures/example.csv'); // need a valid path/csv here to avoid parse error + +// Instantiate module with parameters +const csvCancerRelatedMedicationAdministrationExtractor = new CSVCancerRelatedMedicationAdministrationExtractor({ + filePath: MOCK_CSV_PATH, +}); + +// Destructure all modules +const { csvModule } = csvCancerRelatedMedicationAdministrationExtractor; + +// Spy on csvModule +const csvModuleSpy = jest.spyOn(csvModule, 'get'); + +const formatData = CSVCancerRelatedMedicationExtractorRewired.__get__('formatData'); + +// Creating an example bundle with two medication statements +const exampleEntry = exampleCSVMedicationModuleResponse[0]; +const expandedExampleBundle = _.cloneDeep(exampleCSVMedicationBundle); +expandedExampleBundle.entry.push(exampleCSVMedicationBundle.entry[0]); + +describe('CSVCancerRelatedMedicationAdministrationExtractor', () => { + describe('formatData', () => { + test('should join data appropriately and throw errors when missing required properties', () => { + const expectedErrorString = 'The cancer-related medication administration is missing an expected element; code, code system, and status are all required values.'; + const localData = _.cloneDeep(exampleCSVMedicationModuleResponse); + const patientId = getPatientFromContext(MOCK_CONTEXT).id; + + // Test that valid maximal data works fine + expect(formatData(localData, patientId)).toEqual(expect.anything()); + + // Test that deleting an optional value works fine + delete localData[0].treatmentIntent; + expect(formatData(localData, patientId)).toEqual(expect.anything()); + + // Test that deleting a mandatory value throws an error + delete localData[0].code; + expect(() => formatData(localData, patientId)).toThrow(new Error(expectedErrorString)); + }); + }); + + describe('get', () => { + test('should return bundle with a CancerRelatedMedicationAdministration', async () => { + csvModuleSpy.mockReturnValue(exampleCSVMedicationModuleResponse); + const data = await csvCancerRelatedMedicationAdministrationExtractor.get({ mrn: MOCK_PATIENT_MRN, context: MOCK_CONTEXT }); + expect(data.resourceType).toEqual('Bundle'); + expect(data.type).toEqual('collection'); + expect(data.entry).toBeDefined(); + expect(data.entry.length).toEqual(1); + expect(data.entry).toEqual(exampleCSVMedicationBundle.entry); + }); + + test('should return empty bundle when no data available from module', async () => { + csvModuleSpy.mockReturnValue([]); + const data = await csvCancerRelatedMedicationAdministrationExtractor.get({ mrn: MOCK_PATIENT_MRN, context: MOCK_CONTEXT }); + expect(data.resourceType).toEqual('Bundle'); + expect(data.type).toEqual('collection'); + expect(data.entry).toBeDefined(); + expect(data.entry.length).toEqual(0); + }); + + test('get() should return an array of 2 when two medication administrations are tied to a single patient', async () => { + exampleCSVMedicationModuleResponse.push(exampleEntry); + csvModuleSpy.mockReturnValue(exampleCSVMedicationModuleResponse); + const data = await csvCancerRelatedMedicationAdministrationExtractor.get({ mrn: MOCK_PATIENT_MRN, context: MOCK_CONTEXT }); + expect(data.resourceType).toEqual('Bundle'); + expect(data.type).toEqual('collection'); + expect(data.entry).toBeDefined(); + expect(data.entry.length).toEqual(2); + expect(data).toEqual(expandedExampleBundle); + }); + }); +}); diff --git a/test/extractors/CSVCancerRelatedMedicationExtractor.test.js b/test/extractors/CSVCancerRelatedMedicationRequestExtractor.test.js similarity index 74% rename from test/extractors/CSVCancerRelatedMedicationExtractor.test.js rename to test/extractors/CSVCancerRelatedMedicationRequestExtractor.test.js index 0d65bb14..c368cf17 100644 --- a/test/extractors/CSVCancerRelatedMedicationExtractor.test.js +++ b/test/extractors/CSVCancerRelatedMedicationRequestExtractor.test.js @@ -1,26 +1,26 @@ const path = require('path'); const rewire = require('rewire'); const _ = require('lodash'); -const { CSVCancerRelatedMedicationExtractor } = require('../../src/extractors'); -const exampleCSVMedicationModuleResponse = require('./fixtures/csv-medication-module-response.json'); -const exampleCSVMedicationBundle = require('./fixtures/csv-medication-bundle.json'); +const { CSVCancerRelatedMedicationRequestExtractor } = require('../../src/extractors'); +const exampleCSVMedicationModuleResponse = require('./fixtures/csv-medication-request-module-response.json'); +const exampleCSVMedicationBundle = require('./fixtures/csv-medication-request-bundle.json'); const { getPatientFromContext } = require('../../src/helpers/contextUtils'); const MOCK_CONTEXT = require('./fixtures/context-with-patient.json'); // Rewired extractor for helper tests -const CSVCancerRelatedMedicationExtractorRewired = rewire('../../src/extractors/CSVCancerRelatedMedicationExtractor.js'); +const CSVCancerRelatedMedicationExtractorRewired = rewire('../../src/extractors/CSVCancerRelatedMedicationRequestExtractor.js'); // Constants for tests const MOCK_PATIENT_MRN = 'mrn-1'; // linked to values in example-module-response and context-with-patient above const MOCK_CSV_PATH = path.join(__dirname, 'fixtures/example.csv'); // need a valid path/csv here to avoid parse error // Instantiate module with parameters -const csvCancerRelatedMedicationExtractor = new CSVCancerRelatedMedicationExtractor({ +const csvCancerRelatedMedicationRequestExtractor = new CSVCancerRelatedMedicationRequestExtractor({ filePath: MOCK_CSV_PATH, }); // Destructure all modules -const { csvModule } = csvCancerRelatedMedicationExtractor; +const { csvModule } = csvCancerRelatedMedicationRequestExtractor; // Spy on csvModule const csvModuleSpy = jest.spyOn(csvModule, 'get'); @@ -32,10 +32,10 @@ const exampleEntry = exampleCSVMedicationModuleResponse[0]; const expandedExampleBundle = _.cloneDeep(exampleCSVMedicationBundle); expandedExampleBundle.entry.push(exampleCSVMedicationBundle.entry[0]); -describe('CSVCancerRelatedMedicationExtractor', () => { +describe('CSVCancerRelatedMedicationRequestExtractor', () => { describe('formatData', () => { test('should join data appropriately and throw errors when missing required properties', () => { - const expectedErrorString = 'The cancer-related medication is missing an expected element; code, code system, and status are all required values.'; + const expectedErrorString = 'The cancer-related medication request is missing an expected element; code, code system, status, authoredOn, requesterId, and intent are all required values.'; const localData = _.cloneDeep(exampleCSVMedicationModuleResponse); const patientId = getPatientFromContext(MOCK_CONTEXT).id; @@ -53,9 +53,9 @@ describe('CSVCancerRelatedMedicationExtractor', () => { }); describe('get', () => { - test('should return bundle with a CancerRelatedMedication', async () => { + test('should return bundle with a CancerRelatedMedicationRequest', async () => { csvModuleSpy.mockReturnValue(exampleCSVMedicationModuleResponse); - const data = await csvCancerRelatedMedicationExtractor.get({ mrn: MOCK_PATIENT_MRN, context: MOCK_CONTEXT }); + const data = await csvCancerRelatedMedicationRequestExtractor.get({ mrn: MOCK_PATIENT_MRN, context: MOCK_CONTEXT }); expect(data.resourceType).toEqual('Bundle'); expect(data.type).toEqual('collection'); expect(data.entry).toBeDefined(); @@ -65,17 +65,17 @@ describe('CSVCancerRelatedMedicationExtractor', () => { test('should return empty bundle when no data available from module', async () => { csvModuleSpy.mockReturnValue([]); - const data = await csvCancerRelatedMedicationExtractor.get({ mrn: MOCK_PATIENT_MRN, context: MOCK_CONTEXT }); + const data = await csvCancerRelatedMedicationRequestExtractor.get({ mrn: MOCK_PATIENT_MRN, context: MOCK_CONTEXT }); expect(data.resourceType).toEqual('Bundle'); expect(data.type).toEqual('collection'); expect(data.entry).toBeDefined(); expect(data.entry.length).toEqual(0); }); - test('get() should return an array of 2 when two medication statements are tied to a single patient', async () => { + test('get() should return an array of 2 when two medication requests are tied to a single patient', async () => { exampleCSVMedicationModuleResponse.push(exampleEntry); csvModuleSpy.mockReturnValue(exampleCSVMedicationModuleResponse); - const data = await csvCancerRelatedMedicationExtractor.get({ mrn: MOCK_PATIENT_MRN, context: MOCK_CONTEXT }); + const data = await csvCancerRelatedMedicationRequestExtractor.get({ mrn: MOCK_PATIENT_MRN, context: MOCK_CONTEXT }); expect(data.resourceType).toEqual('Bundle'); expect(data.type).toEqual('collection'); expect(data.entry).toBeDefined(); diff --git a/test/extractors/fixtures/csv-medication-bundle.json b/test/extractors/fixtures/csv-medication-administration-bundle.json similarity index 93% rename from test/extractors/fixtures/csv-medication-bundle.json rename to test/extractors/fixtures/csv-medication-administration-bundle.json index cfaf5dfc..d68ffcba 100644 --- a/test/extractors/fixtures/csv-medication-bundle.json +++ b/test/extractors/fixtures/csv-medication-administration-bundle.json @@ -5,11 +5,11 @@ { "fullUrl": "urn:uuid:medicationId-1", "resource": { - "resourceType": "MedicationStatement", + "resourceType": "MedicationAdministration", "id": "medicationId-1", "meta": { "profile": [ - "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-cancer-related-medication-statement" + "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-cancer-related-medication-administration" ] }, "extension": [ diff --git a/test/extractors/fixtures/csv-medication-module-response.json b/test/extractors/fixtures/csv-medication-administration-module-response.json similarity index 100% rename from test/extractors/fixtures/csv-medication-module-response.json rename to test/extractors/fixtures/csv-medication-administration-module-response.json diff --git a/test/extractors/fixtures/csv-medication-request-bundle.json b/test/extractors/fixtures/csv-medication-request-bundle.json new file mode 100644 index 00000000..5f9615ec --- /dev/null +++ b/test/extractors/fixtures/csv-medication-request-bundle.json @@ -0,0 +1,61 @@ +{ + "resourceType": "Bundle", + "type": "collection", + "entry": [ + { + "fullUrl": "urn:uuid:requestId-1", + "resource": { + "resourceType": "MedicationRequest", + "id": "requestId-1", + "meta": { + "profile": [ + "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-cancer-related-medication-request" + ] + }, + "extension": [ + { + "url": "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-procedure-intent", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "example-code" + } + ] + } + } + ], + "status": "example-status", + "intent": "example-intent", + "medicationCodeableConcept": { + "coding": [ + { + "system": "example-code-system", + "code": "example-code", + "display": "Example Text" + } + ] + }, + "subject": { + "reference": "urn:uuid:mrn-1", + "type": "Patient" + }, + "authoredOn": "YYYY-MM-DD", + "requester": { + "reference": "urn:uuid:example-requester" + }, + "reasonCode": [ + { + "coding": [ + { + "system": "example-code-system", + "code": "example-reason", + "display": "Example Text" + } + ] + } + ] + } + } + ] +} \ No newline at end of file diff --git a/test/extractors/fixtures/csv-medication-request-module-response.json b/test/extractors/fixtures/csv-medication-request-module-response.json new file mode 100644 index 00000000..3c046757 --- /dev/null +++ b/test/extractors/fixtures/csv-medication-request-module-response.json @@ -0,0 +1,17 @@ +[ + { + "mrn": "mrn-1", + "requestid": "requestId-1", + "code": "example-code", + "codesystem": "example-code-system", + "displaytext": "Example Text", + "authoredon": "YYYY-MM-DD", + "treatmentreasoncode": "example-reason", + "treatmentreasoncodesystem": "example-code-system", + "treatmentreasondisplaytext": "Example Text", + "procedureintent": "example-code", + "status": "example-status", + "requesterid": "example-requester", + "intent": "example-intent" + } +] diff --git a/test/sample-client-data/cancer-related-medication-information.csv b/test/sample-client-data/cancer-related-medication-administration-information.csv similarity index 52% rename from test/sample-client-data/cancer-related-medication-information.csv rename to test/sample-client-data/cancer-related-medication-administration-information.csv index 4cd3fc3e..125ed46a 100644 --- a/test/sample-client-data/cancer-related-medication-information.csv +++ b/test/sample-client-data/cancer-related-medication-administration-information.csv @@ -1,4 +1,4 @@ mrn,medicationId,code,codeSystem,displayText,startDate,endDate,treatmentReasonCode,treatmentReasonCodeSystem,treatmentReasonDisplayText,treatmentIntent,status -123,medicationId-1,10760,http://www.nlm.nih.gov/research/umls/rxnorm,Triamcinolone Oral Paste,2020-01-01,2020-07-05,134006,http://snomed.info/sct,Decreased hair growth,373808002,active -456,medicationId-2,91318,http://www.nlm.nih.gov/research/umls/rxnorm,Coal Tar Topical Solution,2020-02-17,2020-08-13,188001,http://snomed.info/sct,Intercostal artery injury,373808002,completed -789,medicationId-3,91833,http://www.nlm.nih.gov/research/umls/rxnorm,Vitamin K1 Injectable Solution [Aquamephyton],2020-01-12,2020-10-01,228007,http://snomed.info/sct,Lucio phenomenon,363676003,intended \ No newline at end of file +123,medicationId-1,10760,http://www.nlm.nih.gov/research/umls/rxnorm,Triamcinolone Oral Paste,2020-01-01,2020-07-05,999000,http://snomed.info/sct,Mixed islet cell and exocrine adenocarcinoma,373808002,on-hold +456,medicationId-2,91318,http://www.nlm.nih.gov/research/umls/rxnorm,Coal Tar Topical Solution,2020-02-17,2020-08-13,915007,http://snomed.info/sct,Malignant melanoma in junctional nevus,373808002,completed +789,medicationId-3,91833,http://www.nlm.nih.gov/research/umls/rxnorm,Vitamin K1 Injectable Solution [Aquamephyton],2020-01-12,2020-10-01,900006,http://snomed.info/sct,Mucin-producing adenocarcinoma,363676003,stopped \ No newline at end of file diff --git a/test/sample-client-data/cancer-related-medication-request-information.csv b/test/sample-client-data/cancer-related-medication-request-information.csv new file mode 100644 index 00000000..1561a494 --- /dev/null +++ b/test/sample-client-data/cancer-related-medication-request-information.csv @@ -0,0 +1,4 @@ +mrn,requestId,code,codeSystem,displayText,treatmentReasonCode,treatmentReasonCodeSystem,treatmentReasonDisplayText,procedureIntent,status,intent,authoredOn,requesterId +123,requestId-1,10760,http://www.nlm.nih.gov/research/umls/rxnorm,Triamcinolone Oral Paste,999000,http://snomed.info/sct,Mixed islet cell and exocrine adenocarcinoma,373808002,active,order,2020-01-01,requester-1 +456,requestId-2,91318,http://www.nlm.nih.gov/research/umls/rxnorm,Coal Tar Topical Solution,915007,http://snomed.info/sct,Malignant melanoma in junctional nevus,373808002,on-hold,proposal,2019-02-02,requester-2 +789,requestId-3,91833,http://www.nlm.nih.gov/research/umls/rxnorm,Vitamin K1 Injectable Solution [Aquamephyton],900006,http://snomed.info/sct,Mucin-producing adenocarcinoma,363676003,cancelled,plan,2021-06-12,requester-3 diff --git a/test/templates/fixtures/maximal-medication-request.json b/test/templates/fixtures/maximal-medication-request.json new file mode 100644 index 00000000..0d1cf9ed --- /dev/null +++ b/test/templates/fixtures/maximal-medication-request.json @@ -0,0 +1,52 @@ +{ + "resourceType": "MedicationRequest", + "id": "medicationId-1", + "meta": { + "profile": [ + "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-cancer-related-medication-request" + ] + }, + "extension": [ + { + "url": "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-procedure-intent", + "valueCodeableConcept": { + "coding": [ + { + "system": "http://snomed.info/sct", + "code": "example-code" + } + ] + } + } + ], + "status": "example-status", + "intent": "example-intent", + "medicationCodeableConcept": { + "coding": [ + { + "system": "example-code-system", + "code": "example-code", + "display": "Example Text" + } + ] + }, + "subject": { + "reference": "urn:uuid:mrn-1", + "type": "Patient" + }, + "authoredOn": "2020-01-01", + "requester": { + "reference": "urn:uuid:example-requester" + }, + "reasonCode": [ + { + "coding": [ + { + "system": "example-code-system", + "code": "example-reason", + "display": "Example Text" + } + ] + } + ] +} \ No newline at end of file diff --git a/test/templates/fixtures/maximal-medication-resource.json b/test/templates/fixtures/maximal-medication-resource.json index 0ff3e383..e392ca7e 100644 --- a/test/templates/fixtures/maximal-medication-resource.json +++ b/test/templates/fixtures/maximal-medication-resource.json @@ -1,9 +1,9 @@ { - "resourceType": "MedicationStatement", + "resourceType": "MedicationAdministration", "id": "medicationId-1", "meta": { "profile": [ - "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-cancer-related-medication-statement" + "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-cancer-related-medication-administration" ] }, "extension": [ diff --git a/test/templates/fixtures/minimal-medication-request.json b/test/templates/fixtures/minimal-medication-request.json new file mode 100644 index 00000000..06ee89cd --- /dev/null +++ b/test/templates/fixtures/minimal-medication-request.json @@ -0,0 +1,26 @@ +{ + "resourceType": "MedicationRequest", + "meta": { + "profile": [ + "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-cancer-related-medication-request" + ] + }, + "status": "example-status", + "intent": "example-intent", + "medicationCodeableConcept": { + "coding": [ + { + "system": "example-code-system", + "code": "example-code" + } + ] + }, + "subject": { + "reference": "urn:uuid:mrn-1", + "type": "Patient" + }, + "authoredOn": "2020-01-01", + "requester": { + "reference": "urn:uuid:example-requester" + } +} \ No newline at end of file diff --git a/test/templates/fixtures/minimal-medication-resource.json b/test/templates/fixtures/minimal-medication-resource.json index 51ef612e..de6bc7a7 100644 --- a/test/templates/fixtures/minimal-medication-resource.json +++ b/test/templates/fixtures/minimal-medication-resource.json @@ -1,8 +1,8 @@ { - "resourceType": "MedicationStatement", + "resourceType": "MedicationAdministration", "meta": { "profile": [ - "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-cancer-related-medication-statement" + "http://hl7.org/fhir/us/mcode/StructureDefinition/mcode-cancer-related-medication-administration" ] }, "status": "example-status", diff --git a/test/templates/medication.test.js b/test/templates/medicationAdministration.test.js similarity index 84% rename from test/templates/medication.test.js rename to test/templates/medicationAdministration.test.js index fad7ac7a..ef88ee72 100644 --- a/test/templates/medication.test.js +++ b/test/templates/medicationAdministration.test.js @@ -1,7 +1,7 @@ const { isValidFHIR } = require('../../src/helpers/fhirUtils'); const maximalValidExampleMedication = require('./fixtures/maximal-medication-resource.json'); const minimalValidExampleMedication = require('./fixtures/minimal-medication-resource.json'); -const { cancerRelatedMedicationTemplate } = require('../../src/templates/CancerRelatedMedicationTemplate.js'); +const { cancerRelatedMedicationAdministrationTemplate } = require('../../src/templates/CancerRelatedMedicationAdministrationTemplate.js'); const { allOptionalKeyCombinationsNotThrow } = require('../utils'); const MEDICATION_VALID_DATA = { @@ -51,16 +51,16 @@ const MEDICATION_INVALID_DATA = { treatmentIntent: 'example-code', }; -describe('test Medication template', () => { +describe('test Medication Administration template', () => { test('valid data passed into template should generate FHIR resource', () => { - const generatedMedication = cancerRelatedMedicationTemplate(MEDICATION_VALID_DATA); + const generatedMedication = cancerRelatedMedicationAdministrationTemplate(MEDICATION_VALID_DATA); expect(generatedMedication).toEqual(maximalValidExampleMedication); expect(isValidFHIR(generatedMedication)).toBeTruthy(); }); test('minimal data passed into template should generate FHIR resource', () => { - const generatedMedication = cancerRelatedMedicationTemplate(MEDICATION_MINIMAL_DATA); + const generatedMedication = cancerRelatedMedicationAdministrationTemplate(MEDICATION_MINIMAL_DATA); expect(generatedMedication).toEqual(minimalValidExampleMedication); @@ -96,10 +96,10 @@ describe('test Medication template', () => { status: 'example-status', }; - allOptionalKeyCombinationsNotThrow(OPTIONAL_DATA, cancerRelatedMedicationTemplate, NECESSARY_DATA); + allOptionalKeyCombinationsNotThrow(OPTIONAL_DATA, cancerRelatedMedicationAdministrationTemplate, NECESSARY_DATA); }); test('invalid data should throw an error', () => { - expect(() => cancerRelatedMedicationTemplate(MEDICATION_INVALID_DATA)).toThrow(Error); + expect(() => cancerRelatedMedicationAdministrationTemplate(MEDICATION_INVALID_DATA)).toThrow(Error); }); }); diff --git a/test/templates/medicationRequest.test.js b/test/templates/medicationRequest.test.js new file mode 100644 index 00000000..f2112ca9 --- /dev/null +++ b/test/templates/medicationRequest.test.js @@ -0,0 +1,97 @@ +const { isValidFHIR } = require('../../src/helpers/fhirUtils'); +const maximalValidExampleRequest = require('./fixtures/maximal-medication-request.json'); +const minimalValidExampleRequest = require('./fixtures/minimal-medication-request.json'); +const { cancerRelatedMedicationRequestTemplate } = require('../../src/templates/CancerRelatedMedicationRequestTemplate.js'); +const { allOptionalKeyCombinationsNotThrow } = require('../utils'); + +const REQUEST_VALID_DATA = { + subjectId: 'mrn-1', + id: 'medicationId-1', + code: 'example-code', + codeSystem: 'example-code-system', + displayText: 'Example Text', + authoredOn: '2020-01-01', + treatmentReasonCode: 'example-reason', + treatmentReasonCodeSystem: 'example-code-system', + treatmentReasonDisplayText: 'Example Text', + procedureIntent: 'example-code', + status: 'example-status', + requesterId: 'example-requester', + intent: 'example-intent', +}; + +const REQUEST_MINIMAL_DATA = { + subjectId: 'mrn-1', + code: 'example-code', + codeSystem: 'example-code-system', + displayText: null, + authoredOn: '2020-01-01', + treatmentReasonCode: null, + treatmentReasonCodeSystem: null, + treatmentReasonDisplayText: null, + procedureIntent: null, + status: 'example-status', + requesterId: 'example-requester', + intent: 'example-intent', +}; + + +const REQUEST_INVALID_DATA = { + subjectId: null, + id: 'medicationId-1', + code: null, + codeSystem: null, + displayText: 'Example Text', + authoredOn: '2020-01-01', + treatmentReasonCode: 'example-reason', + treatmentReasonCodeSystem: 'example-code-system', + treatmentReasonDisplayText: 'Example Text', + procedureIntent: 'example-code', + status: null, + requesterId: 'example-requester', + intent: 'example-intent', +}; + +describe('test Medication Request template', () => { + test('valid data passed into template should generate FHIR resource', () => { + const generatedRequest = cancerRelatedMedicationRequestTemplate(REQUEST_VALID_DATA); + + expect(generatedRequest).toEqual(maximalValidExampleRequest); + expect(isValidFHIR(generatedRequest)).toBeTruthy(); + }); + + test('minimal data passed into template should generate FHIR resource', () => { + const generatedRequest = cancerRelatedMedicationRequestTemplate(REQUEST_MINIMAL_DATA); + + expect(generatedRequest).toEqual(minimalValidExampleRequest); + + expect(isValidFHIR(generatedRequest)).toBeTruthy(); + }); + + test('missing non-required data should not throw an error', () => { + const OPTIONAL_DATA = { + id: 'medicationId-1', + displayText: 'Example Text', + treatmentReasonCode: 'example-reason', + treatmentReasonCodeSystem: 'example-code-system', + treatmentReasonDisplayText: 'Example Text', + procedureIntent: 'example-code', + }; + + const NECESSARY_DATA = { + subjectId: 'mrn-1', + code: 'example-code', + codeSystem: 'example-code-system', + status: 'example-status', + requesterId: 'example-requester', + intent: 'example-intent', + authoredOn: '2020-01-01', + }; + + allOptionalKeyCombinationsNotThrow(OPTIONAL_DATA, cancerRelatedMedicationRequestTemplate, NECESSARY_DATA); + }); + + test('invalid data should throw an error', () => { + expect(() => cancerRelatedMedicationRequestTemplate(REQUEST_INVALID_DATA)).toThrow(Error); + }); +});