Skip to content

Commit

Permalink
Merge pull request #198 from mcode/appointment-extractor
Browse files Browse the repository at this point in the history
Adding CSV Appointment Extractor
  • Loading branch information
rdingwell authored Oct 16, 2023
2 parents c8902b9 + c25422f commit 74ce115
Show file tree
Hide file tree
Showing 14 changed files with 499 additions and 0 deletions.
3 changes: 3 additions & 0 deletions src/client/MCODEClient.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const { BaseClient } = require('./BaseClient');
const {
CSVAdverseEventExtractor,
CSVAppointmentExtractor,
CSVCancerDiseaseStatusExtractor,
CSVCancerRelatedMedicationAdministrationExtractor,
CSVCancerRelatedMedicationRequestExtractor,
Expand Down Expand Up @@ -32,6 +33,7 @@ class MCODEClient extends BaseClient {
super();
this.registerExtractors(
CSVAdverseEventExtractor,
CSVAppointmentExtractor,
CSVCancerDiseaseStatusExtractor,
CSVCancerRelatedMedicationAdministrationExtractor,
CSVCancerRelatedMedicationRequestExtractor,
Expand Down Expand Up @@ -73,6 +75,7 @@ class MCODEClient extends BaseClient {
{ type: 'CSVAdverseEventExtractor', dependencies: ['CSVPatientExtractor'] },
{ type: 'CSVCTCAdverseEventExtractor', dependencies: ['CSVPatientExtractor'] },
{ type: 'CSVEncounterExtractor', dependencies: ['CSVPatientExtractor'] },
{ type: 'CSVAppointmentExtractor', dependencies: ['CSVPatientExtractor'] },
];
// Sort extractors based on order and dependencies
this.extractorConfig = sortExtractors(this.extractorConfig, dependencyInfo);
Expand Down
78 changes: 78 additions & 0 deletions src/extractors/CSVAppointmentExtractor.js
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,
};
2 changes: 2 additions & 0 deletions src/extractors/index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const { BaseCSVExtractor } = require('./BaseCSVExtractor');
const { BaseFHIRExtractor } = require('./BaseFHIRExtractor');
const { CSVAdverseEventExtractor } = require('./CSVAdverseEventExtractor');
const { CSVAppointmentExtractor } = require('./CSVAppointmentExtractor');
const { CSVCancerDiseaseStatusExtractor } = require('./CSVCancerDiseaseStatusExtractor');
const { CSVCancerRelatedMedicationAdministrationExtractor } = require('./CSVCancerRelatedMedicationAdministrationExtractor');
const { CSVCancerRelatedMedicationRequestExtractor } = require('./CSVCancerRelatedMedicationRequestExtractor');
Expand Down Expand Up @@ -31,6 +32,7 @@ module.exports = {
BaseCSVExtractor,
BaseFHIRExtractor,
CSVAdverseEventExtractor,
CSVAppointmentExtractor,
CSVCancerDiseaseStatusExtractor,
CSVCancerRelatedMedicationAdministrationExtractor,
CSVCancerRelatedMedicationRequestExtractor,
Expand Down
17 changes: 17 additions & 0 deletions src/helpers/schemas/csv.js
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,22 @@ const CSVEncounterSchema = {
],
};

const CSVAppointmentSchema = {
headers: [
{ name: 'mrn', required: true },
{ name: 'appointmentId', required: true },
{ name: 'status', required: true },
{ name: 'serviceCategory' },
{ name: 'serviceType' },
{ name: 'appointmentType' },
{ name: 'specialty' },
{ name: 'start' },
{ name: 'end' },
{ name: 'cancelationCode' },
{ name: 'description' },
],
};

module.exports = {
CSVCancerDiseaseStatusSchema,
CSVConditionSchema,
Expand All @@ -235,4 +251,5 @@ module.exports = {
CSVAdverseEventSchema,
CSVCTCAdverseEventSchema,
CSVEncounterSchema,
CSVAppointmentSchema,
};
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const {
const {
BaseFHIRExtractor,
CSVAdverseEventExtractor,
CSVAppointmentExtractor,
CSVCancerDiseaseStatusExtractor,
CSVCancerRelatedMedicationAdministrationExtractor,
CSVCancerRelatedMedicationRequestExtractor,
Expand Down Expand Up @@ -95,6 +96,7 @@ module.exports = {
BaseFHIRExtractor,
BaseFHIRModule,
CSVAdverseEventExtractor,
CSVAppointmentExtractor,
CSVCancerDiseaseStatusExtractor,
CSVCancerRelatedMedicationAdministrationExtractor,
CSVCancerRelatedMedicationRequestExtractor,
Expand Down
82 changes: 82 additions & 0 deletions src/templates/AppointmentTemplate.js
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,
};
2 changes: 2 additions & 0 deletions src/templates/ResourceGenerator.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const crypto = require('crypto');
const logger = require('../helpers/logger');

const { adverseEventTemplate } = require('./AdverseEventTemplate');
const { appointmentTemplate } = require('./AppointmentTemplate');
const { cancerDiseaseStatusTemplate } = require('./CancerDiseaseStatusTemplate');
const { cancerRelatedMedicationAdministrationTemplate } = require('./CancerRelatedMedicationAdministrationTemplate');
const { cancerRelatedMedicationRequestTemplate } = require('./CancerRelatedMedicationRequestTemplate');
Expand All @@ -20,6 +21,7 @@ const { encounterTemplate } = require('./EncounterTemplate');

const fhirTemplateLookup = {
AdverseEvent: adverseEventTemplate,
Appointment: appointmentTemplate,
CancerDiseaseStatus: cancerDiseaseStatusTemplate,
CancerRelatedMedicationAdministration: cancerRelatedMedicationAdministrationTemplate,
CancerRelatedMedicationRequest: cancerRelatedMedicationRequestTemplate,
Expand Down
80 changes: 80 additions & 0 deletions test/extractors/CSVAppointmentExtractor.test.js
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);
});
});
});
Loading

0 comments on commit 74ce115

Please sign in to comment.