Skip to content

Commit 6ac5f70

Browse files
authored
Merge pull request #165 from mcode/ctc-ae-extension
CTC Adverse Event Grade Extension
2 parents 882188b + d32d0bb commit 6ac5f70

13 files changed

+486
-15
lines changed

config/csv.config.example.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,13 @@
8989
"constructorArgs": {
9090
"filePath": "./test/sample-client-data/adverse-event-information.csv"
9191
}
92+
},
93+
{
94+
"label": "ctcAdverseEvent",
95+
"type": "CSVCTCAdverseEventExtractor",
96+
"constructorArgs": {
97+
"filePath": "./test/sample-client-data/ctc-adverse-event-information.csv"
98+
}
9299
}
93100
]
94-
}
101+
}

docs/CSV_Templates.xlsx

612 KB
Binary file not shown.

docs/ctc-adverse-event.csv

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
mrn,adverseEventId,adverseEventCode,adverseEventCodeSystem,adverseEventDisplayText,suspectedCauseId,suspectedCauseType,seriousness,seriousnessCodeSystem,seriousnessDisplayText,category,categoryCodeSystem,categoryDisplayText,severity,actuality,studyId,effectiveDate,recordedDate
2-
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
3-
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
4-
mrn-minimal-example,,code-from-default-system,,,,,,,,,,,,,,1994-12-09,
1+
mrn,adverseEventId,adverseEventCode,adverseEventCodeSystem,adverseEventDisplayText,suspectedCauseId,suspectedCauseType,seriousness,seriousnessCodeSystem,seriousnessDisplayText,category,categoryCodeSystem,categoryDisplayText,severity,actuality,studyId,effectiveDate,recordedDate,grade
2+
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,1,
3+
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,3
4+
mrn-minimal-example,,code-from-default-system,,,,,,,,,,,,,,1994-12-09,,1

src/extractors/CSVCTCAdverseEventExtractor.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ const { generateMcodeResources } = require('../templates');
33
const { getEmptyBundle } = require('../helpers/fhirUtils');
44
const { getPatientFromContext } = require('../helpers/contextUtils');
55
const { formatDateTime } = require('../helpers/dateUtils');
6+
const { ctcAEGradeCodeToTextLookup } = require('../helpers/lookups/ctcAdverseEventLookup');
67
const logger = require('../helpers/logger');
78

89
// Formats data to be passed into template-friendly format
@@ -27,10 +28,11 @@ function formatData(adverseEventData, patientId) {
2728
studyid: studyId,
2829
effectivedate: effectiveDate,
2930
recordeddate: recordedDate,
31+
grade,
3032
} = data;
3133

32-
if (!(adverseEventCode && effectiveDate)) {
33-
throw new Error('The adverse event is missing an expected attribute. Adverse event code and effective date are all required.');
34+
if (!(adverseEventCode && effectiveDate && grade)) {
35+
throw new Error('The adverse event is missing an expected attribute. Adverse event code, effective date, and grade are all required.');
3436
}
3537

3638
const categoryCodes = category.split('|');
@@ -64,6 +66,7 @@ function formatData(adverseEventData, patientId) {
6466
studyId,
6567
effectiveDateTime: formatDateTime(effectiveDate),
6668
recordedDateTime: !recordedDate ? null : formatDateTime(recordedDate),
69+
grade: { code: grade, display: ctcAEGradeCodeToTextLookup[grade] },
6770
};
6871
});
6972
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
const { createInvertedLookup, createLowercaseLookup } = require('../lookupUtils');
2+
3+
const ctcAEGradeTextToCodeLookup = {
4+
'Absent Adverse Event': '0',
5+
'Mild Adverse Event': '1',
6+
'Moderate Adverse Event': '2',
7+
'Severe Adverse Event': '3',
8+
'Life Threatening or Disabling Adverse Event': '4',
9+
'Death Related to Adverse Event': '5',
10+
};
11+
12+
const ctcAEGradeCodeToTextLookup = createInvertedLookup(ctcAEGradeTextToCodeLookup);
13+
14+
module.exports = {
15+
ctcAEGradeCodeToTextLookup: createLowercaseLookup(ctcAEGradeCodeToTextLookup),
16+
ctcAEGradeTextToCodeLookup: createLowercaseLookup(ctcAEGradeTextToCodeLookup),
17+
};

src/templates/CTCAdverseEventTemplate.js

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
const { coding, reference } = require('./snippets');
1+
const { coding, reference, extensionArr } = require('./snippets');
22
const { ifAllArgsObj, ifSomeArgsObj, ifAllArgs, ifSomeArgsArr } = require('../helpers/templateUtils');
33

44
function eventTemplate(eventCoding) {
@@ -72,17 +72,29 @@ function recordedDateTemplate(recordedDateTime) {
7272
};
7373
}
7474

75+
function gradeTemplate(grade) {
76+
return {
77+
url: 'http://hl7.org/fhir/us/ctcae/StructureDefinition/ctcae-grade',
78+
valueCodeableConcept: {
79+
coding: [
80+
coding({ ...grade, system: 'http://hl7.org/fhir/us/ctcae/CodeSystem/ctcae-grade-code-system' }),
81+
],
82+
},
83+
};
84+
}
85+
7586
function CTCAdverseEventTemplate({
7687
id, subjectId, code, system, display, suspectedCauseId, suspectedCauseType, seriousnessCode, seriousnessCodeSystem, seriousnessDisplayText, category,
77-
severity, actuality, studyId, effectiveDateTime, recordedDateTime,
88+
severity, actuality, studyId, effectiveDateTime, recordedDateTime, grade,
7889
}) {
79-
if (!(subjectId && code && system && effectiveDateTime && actuality)) {
80-
throw Error('Trying to render an AdverseEventTemplate, but a required argument is messing; ensure that subjectId, code, system, actuality, and effectiveDateTime are all present');
90+
if (!(subjectId && code && system && effectiveDateTime && actuality && grade)) {
91+
throw Error('Trying to render an AdverseEventTemplate, but a required argument is messing; ensure that subjectId, code, system, actuality, grade, and effectiveDateTime are all present');
8192
}
8293

8394
return {
8495
resourceType: 'AdverseEvent',
8596
id,
97+
...extensionArr(gradeTemplate(grade)),
8698
subject: reference({ id: subjectId, resourceType: 'Patient' }),
8799
...ifSomeArgsObj(eventTemplate)({ code, system, display }),
88100
...ifAllArgsObj(suspectedCauseTemplate)({ suspectedCauseId, suspectedCauseType }),
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
const path = require('path');
2+
const rewire = require('rewire');
3+
const _ = require('lodash');
4+
const { CSVCTCAdverseEventExtractor } = require('../../src/extractors');
5+
const exampleCTCCSVAdverseEventModuleResponse = require('./fixtures/csv-ctc-adverse-event-module-response.json');
6+
const exampleCTCCSVAdverseEventBundle = require('./fixtures/csv-ctc-adverse-event-bundle.json');
7+
const { getPatientFromContext } = require('../../src/helpers/contextUtils');
8+
const MOCK_CONTEXT = require('./fixtures/context-with-patient.json');
9+
10+
// Constants for tests
11+
const MOCK_PATIENT_MRN = 'mrn-1'; // linked to values in example-module-response and context-with-patient above
12+
const MOCK_CSV_PATH = path.join(__dirname, 'fixtures/example.csv'); // need a valid path/csv here to avoid parse error
13+
14+
// Instantiate module with parameters
15+
const csvCTCAdverseEventExtractor = new CSVCTCAdverseEventExtractor({
16+
filePath: MOCK_CSV_PATH,
17+
});
18+
19+
// Destructure all modules
20+
const { csvModule } = csvCTCAdverseEventExtractor;
21+
22+
// Spy on csvModule
23+
const csvModuleSpy = jest.spyOn(csvModule, 'get');
24+
25+
// Creating an example bundle with two medication statements
26+
const exampleEntry = exampleCTCCSVAdverseEventModuleResponse[0];
27+
const expandedExampleBundle = _.cloneDeep(exampleCTCCSVAdverseEventBundle);
28+
expandedExampleBundle.entry.push(exampleCTCCSVAdverseEventBundle.entry[0]);
29+
30+
// Rewired extractor for helper tests
31+
const CSVCTCAdverseEventExtractorRewired = rewire('../../src/extractors/CSVCTCAdverseEventExtractor.js');
32+
33+
describe('CSVCTCAdverseEventExtractor', () => {
34+
describe('formatData', () => {
35+
const formatData = CSVCTCAdverseEventExtractorRewired.__get__('formatData');
36+
test('should join data appropriately and throw errors when missing required properties', () => {
37+
const expectedErrorString = 'The adverse event is missing an expected attribute. Adverse event code, effective date, and grade are all required.';
38+
const expectedCategoryErrorString = 'A category attribute on the adverse event is missing a corresponding categoryCodeSystem or categoryDisplayText value.';
39+
const localData = _.cloneDeep(exampleCTCCSVAdverseEventModuleResponse);
40+
const patientId = getPatientFromContext(MOCK_CONTEXT).id;
41+
42+
// Test that valid maximal data works fine
43+
expect(formatData(exampleCTCCSVAdverseEventModuleResponse, patientId)).toEqual(expect.anything());
44+
45+
// Test that deleting an optional value works fine
46+
delete localData[0].actuality;
47+
expect(formatData(exampleCTCCSVAdverseEventModuleResponse, patientId)).toEqual(expect.anything());
48+
49+
// Test that adding another category but not adding a corresponding categoryCodeSystem throws an error
50+
localData[0].category = 'product-use-error|product-problem';
51+
expect(() => formatData(localData, patientId)).toThrow(new Error(expectedCategoryErrorString));
52+
53+
// Test that adding another category but adding a corresponding categoryCodeSystem and categoryDisplayText works fine
54+
localData[0].categorycodesystem = 'http://terminology.hl7.org/CodeSystem/adverse-event-category|http://snomed.info/sct';
55+
localData[0].categorydisplaytext = 'Product Use Error|Product Problem';
56+
expect(formatData(localData, patientId)).toEqual(expect.anything());
57+
58+
// Test that adding another category but including syntax for default categoryCodeSystem and categoryDisplayText values works fine
59+
localData[0].categorycodesystem = 'http://terminology.hl7.org/CodeSystem/adverse-event-category|';
60+
localData[0].categorydisplaytext = 'Product Use Error|';
61+
expect(formatData(localData, patientId)).toEqual(expect.anything());
62+
63+
// Test that deleting a mandatory value throws an error
64+
delete localData[0].grade;
65+
expect(() => formatData(localData, patientId)).toThrow(new Error(expectedErrorString));
66+
});
67+
});
68+
69+
describe('get', () => {
70+
test('should return bundle with a CTCAdverseEvent resource', async () => {
71+
csvModuleSpy.mockReturnValue(exampleCTCCSVAdverseEventModuleResponse);
72+
const data = await csvCTCAdverseEventExtractor.get({ mrn: MOCK_PATIENT_MRN, context: MOCK_CONTEXT });
73+
expect(data.resourceType).toEqual('Bundle');
74+
expect(data.type).toEqual('collection');
75+
expect(data.entry).toBeDefined();
76+
expect(data.entry.length).toEqual(1);
77+
expect(data.entry).toEqual(exampleCTCCSVAdverseEventBundle.entry);
78+
});
79+
80+
test('should return empty bundle when no data available from module', async () => {
81+
csvModuleSpy.mockReturnValue([]);
82+
const data = await csvCTCAdverseEventExtractor.get({ mrn: MOCK_PATIENT_MRN, context: MOCK_CONTEXT });
83+
expect(data.resourceType).toEqual('Bundle');
84+
expect(data.type).toEqual('collection');
85+
expect(data.entry).toBeDefined();
86+
expect(data.entry.length).toEqual(0);
87+
});
88+
89+
test('get() should return an array of 2 when two adverse event resources are tied to a single patient', async () => {
90+
exampleCTCCSVAdverseEventModuleResponse.push(exampleEntry);
91+
csvModuleSpy.mockReturnValue(exampleCTCCSVAdverseEventModuleResponse);
92+
const data = await csvCTCAdverseEventExtractor.get({ mrn: MOCK_PATIENT_MRN, context: MOCK_CONTEXT });
93+
94+
expect(data.resourceType).toEqual('Bundle');
95+
expect(data.type).toEqual('collection');
96+
expect(data.entry).toBeDefined();
97+
expect(data.entry.length).toEqual(2);
98+
expect(data).toEqual(expandedExampleBundle);
99+
});
100+
});
101+
});
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
{
2+
"resourceType": "Bundle",
3+
"type": "collection",
4+
"entry": [
5+
{
6+
"fullUrl": "urn:uuid:adverseEventId-1",
7+
"resource": {
8+
"resourceType": "AdverseEvent",
9+
"id": "adverseEventId-1",
10+
"extension" : [
11+
{
12+
"url" : "http://hl7.org/fhir/us/ctcae/StructureDefinition/ctcae-grade",
13+
"valueCodeableConcept" : {
14+
"coding" : [
15+
{
16+
"system" : "http://hl7.org/fhir/us/ctcae/CodeSystem/ctcae-grade-code-system",
17+
"code" : "1",
18+
"display" : "Mild Adverse Event"
19+
}
20+
]
21+
}
22+
}
23+
],
24+
"subject": {
25+
"reference": "urn:uuid:mrn-1",
26+
"type": "Patient"
27+
},
28+
"event": {
29+
"coding": [
30+
{
31+
"system": "code-system",
32+
"code": "109006",
33+
"display": "Anxiety disorder of childhood OR adolescence"
34+
}
35+
]
36+
},
37+
"suspectEntity": [
38+
{
39+
"instance": {
40+
"reference": "urn:uuid:procedure-id",
41+
"type": "Procedure"
42+
}
43+
}
44+
],
45+
"seriousness": {
46+
"coding": [
47+
{
48+
"system": "http://terminology.hl7.org/CodeSystem/adverse-event-seriousness",
49+
"code": "serious",
50+
"display": "Serious"
51+
}
52+
]
53+
},
54+
"category": [
55+
{
56+
"coding": [
57+
{
58+
"system": "http://terminology.hl7.org/CodeSystem/adverse-event-category",
59+
"code": "product-use-error",
60+
"display": "Product Use Error"
61+
}
62+
]
63+
}
64+
],
65+
"severity": {
66+
"coding": [
67+
{
68+
"system": "http://terminology.hl7.org/CodeSystem/adverse-event-severity",
69+
"code": "severe"
70+
}
71+
]
72+
},
73+
"actuality": "actual",
74+
"study": [
75+
{
76+
"reference": "urn:uuid:researchId-1",
77+
"type": "ResearchStudy"
78+
}
79+
],
80+
"date": "1994-12-09",
81+
"recordedDate": "1994-12-09"
82+
}
83+
}
84+
]
85+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
[
2+
{
3+
"mrn": "mrn-1",
4+
"adverseeventid": "adverseEventId-1",
5+
"adverseeventcode": "109006",
6+
"adverseeventcodesystem": "code-system",
7+
"adverseeventdisplaytext": "Anxiety disorder of childhood OR adolescence",
8+
"suspectedcauseid": "procedure-id",
9+
"suspectedcausetype": "Procedure",
10+
"seriousness": "serious",
11+
"seriousnesscodesystem": "http://terminology.hl7.org/CodeSystem/adverse-event-seriousness",
12+
"seriousnessdisplaytext": "Serious",
13+
"category": "product-use-error",
14+
"categorycodesystem": "http://terminology.hl7.org/CodeSystem/adverse-event-category",
15+
"categorydisplaytext": "Product Use Error",
16+
"severity": "severe",
17+
"actuality": "actual",
18+
"studyid": "researchId-1",
19+
"effectivedate": "12-09-1994",
20+
"recordeddate": "12-09-1994",
21+
"grade": "1"
22+
}
23+
]
Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
mrn,adverseEventId,adverseEventCode,adverseEventCodeSystem,adverseEventDisplayText,suspectedCauseId,suspectedCauseType,seriousness,seriousnessCodeSystem,seriousnessDisplayText,category,categoryCodeSystem,categoryDisplayText,severity,actuality,studyId,effectiveDate,recordedDate
2-
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
3-
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
4-
789,adverseEventId-3,150003,,,,,,,,product-use-error,,,,,,12-09-1994,
1+
mrn,adverseEventId,adverseEventCode,adverseEventCodeSystem,adverseEventDisplayText,suspectedCauseId,suspectedCauseType,seriousness,seriousnessCodeSystem,seriousnessDisplayText,category,categoryCodeSystem,categoryDisplayText,severity,actuality,studyId,effectiveDate,recordedDate,grade
2+
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,1
3+
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,2
4+
789,adverseEventId-3,150003,,,,,,,,product-use-error,,,,,,12-09-1994,,3

0 commit comments

Comments
 (0)