Skip to content

Commit 264c42e

Browse files
authored
Merge pull request #75 from mcode/careplan-extensions
Update careplan to create multiple extensions
2 parents dd5cfa5 + 2d3f905 commit 264c42e

8 files changed

+191
-47
lines changed

src/extractors/CSVTreatmentPlanChangeExtractor.js

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,30 @@
11
const path = require('path');
2+
const _ = require('lodash');
23
const { Extractor } = require('./Extractor');
34
const { CSVModule } = require('../modules');
4-
const { formatDate, formatDateTime } = require('../helpers/dateUtils');
5+
const { formatDate } = require('../helpers/dateUtils');
56
const { generateMcodeResources } = require('../templates');
67
const { getEmptyBundle } = require('../helpers/fhirUtils');
78
const logger = require('../helpers/logger');
89

910
// Formats data to be passed into template-friendly format
1011
function formatData(tpcData) {
11-
logger.debug('Reformatting disease status data from CSV into template format');
12-
return tpcData.map((data) => {
13-
const { mrn, dateOfCarePlan, changed, reasonCode } = data;
12+
logger.debug('Reformatting treatment plan change data from CSV into template format');
13+
14+
// Nothing to format in empty array
15+
if (_.isEmpty(tpcData)) {
16+
return [];
17+
}
18+
19+
// Newly combined data has mrn and list of reviews to map to an extension
20+
const combinedFormat = { mrn: tpcData[0].mrn, reviews: [] };
21+
22+
// If there are multiple entries, combine them into one object with multiple reviews
23+
const combinedData = _.reduce(tpcData, (res, currentDataEntry) => {
24+
const {
25+
mrn, dateOfCarePlan, changed, reasonCode, reasonDisplayText,
26+
} = currentDataEntry;
27+
1428
if (!mrn || !dateOfCarePlan || !changed) {
1529
throw new Error('Treatment Plan Change Data missing an expected property: mrn, dateOfCarePlan, changed are required');
1630
}
@@ -20,20 +34,38 @@ function formatData(tpcData) {
2034
throw new Error('reasonCode is required when changed flag is true');
2135
}
2236

37+
res.reviews.push({
38+
dateOfCarePlan,
39+
reasonCode,
40+
reasonDisplayText,
41+
changed,
42+
});
43+
return res;
44+
}, combinedFormat);
45+
46+
// Format each entry in the reviews array
47+
combinedData.reviews = combinedData.reviews.map((review) => {
48+
const { dateOfCarePlan, changed, reasonCode, reasonDisplayText } = review;
49+
2350
const formattedData = {
2451
effectiveDate: formatDate(dateOfCarePlan),
25-
effectiveDateTime: formatDateTime(dateOfCarePlan),
2652
hasChanged: changed,
27-
mrn,
2853
};
2954

3055
// Add reasonCode to formattedData if available
3156
if (reasonCode) {
3257
formattedData.reasonCode = reasonCode;
58+
59+
if (reasonDisplayText) {
60+
formattedData.reasonDisplayText = reasonDisplayText;
61+
}
3362
}
3463

3564
return formattedData;
3665
});
66+
67+
// Array will contain one element to generate one FHIR resource with multiple extensions for reviews
68+
return [combinedData];
3769
}
3870

3971
class CSVTreatmentPlanChangeExtractor extends Extractor {

src/templates/CarePlanWithReviewTemplate.js

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
const {
22
coding, extensionArr, meta, narrative, reference, valueX,
33
} = require('./snippets');
4-
const { ifAllArgs } = require('../helpers/templateUtils');
54

65
function metaTemplate() {
76
return {
@@ -17,12 +16,6 @@ function textTemplate() {
1716
};
1817
}
1918

20-
function createdTemplate({ effectiveDateTime }) {
21-
return {
22-
created: effectiveDateTime,
23-
};
24-
}
25-
2619
// R4 change reasons valueset: http://standardhealthrecord.org/guides/icare/ValueSet-icare-care-plan-change-reason-vs.html
2720
function carePlanReasonTemplate({ reasonCode, reasonDisplayText }) {
2821
return {
@@ -37,6 +30,12 @@ function carePlanReasonTemplate({ reasonCode, reasonDisplayText }) {
3730
}
3831

3932
function carePlanChangeReasonExtensionTemplate({ hasChanged, reasonCode, reasonDisplayText, effectiveDate }) {
33+
if (hasChanged === undefined || !effectiveDate) {
34+
const errorMessage = 'Trying to render a CarePlanWithReviewTemplate, but a review was missing required fields; '
35+
+ 'ensure that hasChanged and effectiveDate are present on all reviews';
36+
throw new Error(errorMessage);
37+
}
38+
4039
let hasChangedBoolean = hasChanged;
4140
if (hasChanged === 'true') {
4241
hasChangedBoolean = true;
@@ -83,12 +82,10 @@ function categoryTemplate() {
8382
// Treatment Plan Change modeled with CarePlanWithReview Template
8483
// Uses the ICARE R4 Care Plan profile which is not published yet
8584
// For reference, ICARE R4 Care Plan profile: http://standardhealthrecord.org/guides/icare/StructureDefinition-icare-care-plan-with-review.html
86-
function carePlanWithReviewTemplate({
87-
id, mrn, name, effectiveDate, effectiveDateTime, hasChanged, reasonCode, reasonDisplayText,
88-
}) {
89-
if (!(id && mrn && effectiveDate && hasChanged != null)) {
85+
function carePlanWithReviewTemplate({ id, mrn, name, reviews }) {
86+
if (!(id && mrn && reviews)) {
9087
const errorMessage = 'Trying to render a CarePlanWithReviewTemplate, but a required argument was missing; '
91-
+ 'ensure that id, mrn, effectiveDate, hasChanged are all present';
88+
+ 'ensure that id, mrn, reviews are all present';
9289
throw new Error(errorMessage);
9390
}
9491
return {
@@ -97,12 +94,11 @@ function carePlanWithReviewTemplate({
9794
...metaTemplate(),
9895
...textTemplate(),
9996
...extensionArr(
100-
carePlanChangeReasonExtensionTemplate({ hasChanged, reasonCode, reasonDisplayText, effectiveDate }),
97+
...reviews.map(carePlanChangeReasonExtensionTemplate),
10198
),
10299
...subjectTemplate({ id: mrn, name }),
103100
status: 'draft',
104101
intent: 'proposal',
105-
...ifAllArgs(createdTemplate)({ effectiveDateTime }),
106102
...categoryTemplate(),
107103
};
108104
}

test/extractors/CSVTreatmentPlanChangeExtractor.test.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ describe('CSVTreatmentPlanChangeExtractor', () => {
2626
changed: 'false',
2727
mrn: 'id',
2828
},
29+
{
30+
dateOfCarePlan: '2020-04-30',
31+
changed: 'true',
32+
reasonCode: 'example code',
33+
mrn: 'id',
34+
},
2935
];
3036

3137
test('should join data appropriately and throw errors when missing required properties', () => {
@@ -54,6 +60,29 @@ describe('CSVTreatmentPlanChangeExtractor', () => {
5460
exampleData[0].reasonCode = 'example code';
5561
expect(() => formatData(exampleData)).not.toThrowError();
5662
});
63+
64+
test('should join multiple entries into one', () => {
65+
const expectedFormattedData = [
66+
{
67+
mrn: 'mrn-1',
68+
reviews: [
69+
{
70+
effectiveDate: '2020-04-15',
71+
hasChanged: 'true',
72+
reasonCode: '281647001',
73+
reasonDisplayText: 'Adverse reaction (disorder)',
74+
},
75+
{
76+
effectiveDate: '2020-04-30',
77+
reasonCode: '405613005',
78+
hasChanged: 'true',
79+
},
80+
],
81+
},
82+
];
83+
84+
expect(formatData(exampleCSVTPCModuleResponse)).toEqual(expectedFormattedData);
85+
});
5786
});
5887

5988
describe('get', () => {

test/extractors/fixtures/csv-treatment-plan-change-bundle.json

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
"type": "collection",
44
"entry": [
55
{
6-
"fullUrl": "urn:uuid:4d2afb8f74b6950922b0ae5979edbcaf59cecf959ddf80cce63940b45597a29c",
6+
"fullUrl": "urn:uuid:3005b559bb913fdfb23756a442645c6c37bbdafa42ad4be0bd1a6b698274f466",
77
"resource": {
88
"resourceType": "CarePlan",
9-
"id": "4d2afb8f74b6950922b0ae5979edbcaf59cecf959ddf80cce63940b45597a29c",
9+
"id": "3005b559bb913fdfb23756a442645c6c37bbdafa42ad4be0bd1a6b698274f466",
1010
"meta": {
1111
"profile": [
1212
"http://mcodeinitiative.org/codex/us/icare/StructureDefinition/icare-care-plan-with-review"
@@ -24,19 +24,34 @@
2424
"url": "CarePlanChangeReason",
2525
"valueCodeableConcept": {
2626
"coding": [
27-
{ "system": "http://snomed.info/sct", "code": "281647001" }
28-
]
27+
{ "system": "http://snomed.info/sct", "code": "281647001", "display": "Adverse reaction (disorder)" }
28+
],
29+
"text": "Adverse reaction (disorder)"
2930
}
3031
},
3132
{ "url": "ReviewDate", "valueDate": "2020-04-15" },
3233
{ "url": "ChangedFlag", "valueBoolean": true }
3334
]
35+
},
36+
{
37+
"url": "http://mcodeinitiative.org/codex/us/icare/StructureDefinition/icare-care-plan-review",
38+
"extension": [
39+
{
40+
"url": "CarePlanChangeReason",
41+
"valueCodeableConcept": {
42+
"coding": [
43+
{ "system": "http://snomed.info/sct", "code": "405613005" }
44+
]
45+
}
46+
},
47+
{ "url": "ReviewDate", "valueDate": "2020-04-30" },
48+
{ "url": "ChangedFlag", "valueBoolean": true }
49+
]
3450
}
3551
],
3652
"subject": { "reference": "urn:uuid:mrn-1" },
3753
"status": "draft",
3854
"intent": "proposal",
39-
"created": "2020-04-15",
4055
"category": [
4156
{
4257
"coding": [

test/extractors/fixtures/csv-treatment-plan-change-module-response.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@
33
"mrn": "mrn-1",
44
"dateOfCarePlan": "2020-04-15",
55
"reasonCode": "281647001",
6+
"reasonDisplayText": "Adverse reaction (disorder)",
7+
"changed": "true"
8+
},
9+
{
10+
"mrn": "mrn-1",
11+
"dateOfCarePlan": "2020-04-30",
12+
"reasonCode": "405613005",
613
"changed": "true"
714
}
815
]

test/templates/carePlanWithReview.test.js

Lines changed: 61 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,16 @@ describe('JavaScript render CarePlan template', () => {
88
test('minimal required data passed into template should generate FHIR resource', () => {
99
const CARE_PLAN_VALID_DATA = {
1010
id: 'test-id',
11-
effectiveDateTime: '2020-01-23T09:07:00Z',
12-
effectiveDate: '2020-01-23',
13-
hasChanged: 'false',
1411
mrn: 'abc-def',
15-
reasonCode: null,
16-
reasonDisplayText: null,
1712
name: null,
13+
reviews: [
14+
{
15+
effectiveDate: '2020-01-23',
16+
hasChanged: 'false',
17+
reasonCode: null,
18+
reasonDisplayText: null,
19+
},
20+
],
1821
};
1922

2023
const generatedCarePlan = carePlanWithReviewTemplate(CARE_PLAN_VALID_DATA);
@@ -25,48 +28,86 @@ describe('JavaScript render CarePlan template', () => {
2528
test('maximal data passed into template should generate FHIR resource', () => {
2629
const MAX_CARE_PLAN_DATA = {
2730
id: 'test-id',
28-
effectiveDateTime: '2020-01-23T09:07:00Z',
29-
effectiveDate: '2020-01-23',
30-
hasChanged: 'true',
31-
reasonCode: '281647001',
32-
reasonDisplayText: 'Adverse reaction (disorder)',
3331
mrn: 'abc-def',
3432
name: 'Sample Text',
33+
reviews: [
34+
{
35+
effectiveDate: '2020-01-23',
36+
hasChanged: 'true',
37+
reasonCode: '281647001',
38+
reasonDisplayText: 'Adverse reaction (disorder)',
39+
},
40+
{
41+
effectiveDate: '2020-01-30',
42+
hasChanged: 'true',
43+
reasonCode: '405613005',
44+
reasonDisplayText: 'Planned Procedure (situation)',
45+
},
46+
],
3547
};
3648

3749
const generatedCarePlan = carePlanWithReviewTemplate(MAX_CARE_PLAN_DATA);
3850
expect(generatedCarePlan).toEqual(maximalCarePlan);
3951
expect(isValidFHIR(generatedCarePlan)).toBeTruthy();
4052
});
4153

42-
test('missing non-required data should not throw an error', () => {
54+
test('missing non-required data at care plan object level should not throw an error', () => {
4355
const NECESSARY_DATA = {
4456
id: 'test-id',
45-
effectiveDateTime: '2020-01-23T09:07:00Z',
46-
effectiveDate: '2020-01-23',
47-
hasChanged: 'false',
4857
mrn: 'abc-def',
58+
reviews: [],
4959
};
5060

5161
const OPTIONAL_DATA = {
52-
reasonCode: '281647001',
53-
reasonDisplayText: 'Adverse reaction (disorder)',
5462
name: 'Sample Text',
5563
};
5664

5765
allOptionalKeyCombinationsNotThrow(OPTIONAL_DATA, carePlanWithReviewTemplate, NECESSARY_DATA);
5866
});
5967

68+
test('missing non-required data at review object level should not throw an error', () => {
69+
const NECESSARY_DATA = {
70+
id: 'test-id',
71+
mrn: 'abc-def',
72+
reviews: [
73+
{
74+
effectiveDate: '2020-01-23',
75+
hasChanged: 'false',
76+
},
77+
],
78+
};
79+
80+
const OPTIONAL_DATA = ['reasonCode', 'reasonDisplayText'];
81+
82+
// Test presence of each optional key individually
83+
OPTIONAL_DATA.forEach((key) => {
84+
const r = NECESSARY_DATA.reviews[0];
85+
r[key] = 'sample value';
86+
87+
expect(() => carePlanWithReviewTemplate(NECESSARY_DATA)).not.toThrow();
88+
89+
delete r[key];
90+
});
91+
});
92+
6093
test('missing required data should throw a reference error', () => {
6194
const INVALID_DATA = {
62-
// Omitting 'hasChanged' field which is a required property
95+
// omitting 'mrn' field which is a required property
96+
id: 'test-id',
97+
reviews: [],
98+
};
99+
100+
const INVALID_REVIEW_DATA = {
101+
// Omitting 'hasChanged' field on the review which is a required property
63102
id: 'test-id',
64-
effectiveDateTime: '2020-01-23T09:07:00Z',
65-
effectiveDate: '2020-01-23',
66103
mrn: 'abc-def',
67-
haschanged: null,
104+
reviews: [{
105+
effectiveDate: '2020-01-23',
106+
haschanged: null,
107+
}],
68108
};
69109

70110
expect(() => carePlanWithReviewTemplate(INVALID_DATA)).toThrow(Error);
111+
expect(() => carePlanWithReviewTemplate(INVALID_REVIEW_DATA)).toThrow(Error);
71112
});
72113
});

0 commit comments

Comments
 (0)