Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adding CSV Appointment Extractor #198

Merged
merged 1 commit into from
Oct 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading