-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #198 from mcode/appointment-extractor
Adding CSV Appointment Extractor
- Loading branch information
Showing
14 changed files
with
499 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 { CSVAppointmentSchema } = require('../helpers/schemas/csv'); | ||
const logger = require('../helpers/logger'); | ||
|
||
// Formats data to be passed into template-friendly format | ||
function formatData(appointmentData, patientId) { | ||
logger.debug('Reformatting appointment data from CSV into template format'); | ||
return appointmentData.map((data) => { | ||
const { | ||
appointmentid: appointmentId, | ||
status, | ||
servicecategory: serviceCategory, | ||
servicetype: serviceType, | ||
appointmenttype: appointmentType, | ||
specialty, | ||
start, | ||
end, | ||
cancelationcode: cancelationCode, | ||
description, | ||
} = data; | ||
|
||
if (!(appointmentId && status)) { | ||
throw Error('Missing required field for Appointment CSV Extraction: appointmentId or status'); | ||
} | ||
|
||
return { | ||
...(appointmentId && { id: appointmentId }), | ||
patientParticipant: { | ||
id: patientId, | ||
}, | ||
status, | ||
serviceCategory, | ||
serviceType, | ||
appointmentType, | ||
specialty, | ||
start: !start ? null : formatDateTime(start), | ||
end: !end ? null : formatDateTime(end), | ||
cancelationCode, | ||
description, | ||
}; | ||
}); | ||
} | ||
|
||
class CSVAppointmentExtractor extends BaseCSVExtractor { | ||
constructor({ | ||
filePath, url, fileName, dataDirectory, csvParse, | ||
}) { | ||
super({ filePath, url, fileName, dataDirectory, csvSchema: CSVAppointmentSchema, csvParse }); | ||
} | ||
|
||
async getAppointmentData(mrn) { | ||
logger.debug('Getting Appointment Data'); | ||
return this.csvModule.get('mrn', mrn); | ||
} | ||
|
||
async get({ mrn, context }) { | ||
const appointmentData = await this.getAppointmentData(mrn); | ||
if (appointmentData.length === 0) { | ||
logger.warn('No appointment data found for patient'); | ||
return getEmptyBundle(); | ||
} | ||
const patientId = getPatientFromContext(context).id; | ||
|
||
// Reformat data | ||
const formattedData = formatData(appointmentData, patientId); | ||
|
||
// Fill templates | ||
return generateMcodeResources('Appointment', formattedData); | ||
} | ||
} | ||
|
||
module.exports = { | ||
CSVAppointmentExtractor, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
const { ifAllArgsObj } = require('../helpers/templateUtils'); | ||
const { reference, coding } = require('./snippets'); | ||
|
||
function patientParticipantTemplate({ patientParticipant }) { | ||
return { | ||
participant: [ | ||
{ | ||
actor: reference({ ...patientParticipant, resourceType: 'Patient' }), | ||
status: 'tentative', | ||
}, | ||
], | ||
}; | ||
} | ||
|
||
function cancelationReasonTemplate({ cancelationCode }) { | ||
return { | ||
cancelationReason: { | ||
coding: [coding({ code: cancelationCode, system: 'http://terminology.hl7.org/CodeSystem/appointment-cancellation-reason' })], | ||
}, | ||
}; | ||
} | ||
|
||
function serviceCategoryTemplate({ serviceCategory }) { | ||
return { | ||
serviceCategory: [{ | ||
coding: [coding({ code: serviceCategory, system: 'http://terminology.hl7.org/CodeSystem/service-category' })], | ||
}], | ||
}; | ||
} | ||
|
||
function serviceTypeTemplate({ serviceType }) { | ||
return { | ||
serviceType: [{ | ||
coding: [coding({ code: serviceType, system: 'http://terminology.hl7.org/CodeSystem/service-type' })], | ||
}], | ||
}; | ||
} | ||
|
||
|
||
function appointmentTypeTemplate({ appointmentType }) { | ||
return { | ||
appointmentType: { | ||
coding: [coding({ code: appointmentType, system: 'http://terminology.hl7.org/CodeSystem/v2-0276' })], | ||
}, | ||
}; | ||
} | ||
|
||
function specialtyTemplate({ specialty }) { | ||
return { | ||
specialty: [{ | ||
coding: [coding({ code: specialty, system: 'http://snomed.info/sct' })], | ||
}], | ||
}; | ||
} | ||
|
||
|
||
function appointmentTemplate({ | ||
id, patientParticipant, status, serviceCategory, serviceType, appointmentType, specialty, start, end, cancelationCode, description, | ||
}) { | ||
if (!(id && status)) { | ||
throw Error('Trying to render an AppointmentTemplate, but a required argument is missing; ensure that id and status are all present'); | ||
} | ||
|
||
return { | ||
resourceType: 'Appointment', | ||
id, | ||
status, | ||
...ifAllArgsObj(serviceCategoryTemplate)({ serviceCategory }), | ||
...ifAllArgsObj(serviceTypeTemplate)({ serviceType }), | ||
...ifAllArgsObj(appointmentTypeTemplate)({ appointmentType }), | ||
...ifAllArgsObj(specialtyTemplate)({ specialty }), | ||
...patientParticipantTemplate({ patientParticipant }), | ||
...(start && { start }), | ||
...(end && { end }), | ||
...ifAllArgsObj(cancelationReasonTemplate)({ cancelationCode }), | ||
...(description && { description }), | ||
}; | ||
} | ||
|
||
module.exports = { | ||
appointmentTemplate, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
const path = require('path'); | ||
const rewire = require('rewire'); | ||
const _ = require('lodash'); | ||
const { CSVAppointmentExtractor } = require('../../src/extractors'); | ||
const exampleCSVAppointmentModuleResponse = require('./fixtures/csv-appointment-module-response.json'); | ||
const exampleCSVAppointmentBundle = require('./fixtures/csv-appointment-bundle.json'); | ||
const { getPatientFromContext } = require('../../src/helpers/contextUtils'); | ||
const MOCK_CONTEXT = require('./fixtures/context-with-patient.json'); | ||
|
||
// 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 | ||
const IMPLEMENTATION = 'mcode'; | ||
|
||
// Rewired extractor for helper tests | ||
const CSVAppointmentExtractorRewired = rewire('../../src/extractors/CSVAppointmentExtractor.js'); | ||
|
||
const formatData = CSVAppointmentExtractorRewired.__get__('formatData'); | ||
|
||
// Instantiate module with parameters | ||
const csvAppointmentExtractor = new CSVAppointmentExtractor({ | ||
filePath: MOCK_CSV_PATH, | ||
implementation: IMPLEMENTATION, | ||
}); | ||
|
||
// Destructure all modules | ||
const { csvModule } = csvAppointmentExtractor; | ||
|
||
// Spy on csvModule | ||
const csvModuleSpy = jest.spyOn(csvModule, 'get'); | ||
|
||
|
||
describe('CSVAppointmentExtractor', () => { | ||
describe('formatData', () => { | ||
test('should format data appropriately and throw errors when missing required properties', () => { | ||
const expectedErrorString = 'Missing required field for Appointment CSV Extraction: appointmentId or status'; | ||
const localData = _.cloneDeep(exampleCSVAppointmentModuleResponse); | ||
const patientId = getPatientFromContext(MOCK_CONTEXT).id; | ||
|
||
// Test that valid data works fine | ||
expect(formatData(exampleCSVAppointmentModuleResponse, patientId)).toEqual(expect.anything()); | ||
|
||
localData[0].start = ''; | ||
localData[0].cancelationcode = ''; | ||
|
||
// Only including required properties is valid | ||
expect(formatData(localData, patientId)).toEqual(expect.anything()); | ||
|
||
const requiredProperties = ['appointmentid', 'status']; | ||
|
||
// Removing each required property should throw an error | ||
requiredProperties.forEach((key) => { | ||
const clonedData = _.cloneDeep(localData); | ||
clonedData[0][key] = ''; | ||
expect(() => formatData(clonedData, patientId)).toThrow(new Error(expectedErrorString)); | ||
}); | ||
}); | ||
}); | ||
|
||
describe('get', () => { | ||
test('should return bundle with Appointment', async () => { | ||
csvModuleSpy.mockReturnValue(exampleCSVAppointmentModuleResponse); | ||
const data = await csvAppointmentExtractor.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(exampleCSVAppointmentBundle.entry); | ||
}); | ||
|
||
test('should return empty bundle when no data available from module', async () => { | ||
csvModuleSpy.mockReturnValue([]); | ||
const data = await csvAppointmentExtractor.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); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.