Skip to content

Commit 6647149

Browse files
author
Matthew Gramigna
committed
Update careplan to create multiple extensions when multiple data entries are present
1 parent 59682bf commit 6647149

8 files changed

+173
-45
lines changed

src/extractors/CSVTreatmentPlanChangeExtractor.js

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const path = require('path');
2+
const _ = require('lodash');
23
const { Extractor } = require('./Extractor');
34
const { CSVModule } = require('../modules');
45
const { formatDate, formatDateTime } = require('../helpers/dateUtils');
@@ -8,23 +9,36 @@ 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;
14-
if (!mrn || !dateOfCarePlan || !changed) {
12+
logger.debug('Reformatting treatment plan change data from CSV into template format');
13+
14+
// If there are multiple entries, combine them into one object with multiple reviews
15+
const combinedData = _.reduce(tpcData, (res, n) => {
16+
if (!n.mrn || !n.dateOfCarePlan || !n.changed) {
1517
throw new Error('Treatment Plan Change Data missing an expected property: mrn, dateOfCarePlan, changed are required');
1618
}
1719

1820
// reasonCode is required if changed flag is true
19-
if (changed === 'true' && !reasonCode) {
21+
if (n.changed === 'true' && !n.reasonCode) {
2022
throw new Error('reasonCode is required when changed flag is true');
2123
}
2224

25+
if (!res.mrn) res.mrn = n.mrn;
26+
(res.reviews || (res.reviews = [])).push({
27+
dateOfCarePlan: n.dateOfCarePlan,
28+
reasonCode: n.reasonCode,
29+
changed: n.changed,
30+
});
31+
return res;
32+
}, {});
33+
34+
// Format each entry in the reviews array
35+
combinedData.reviews = combinedData.reviews.map((reviews) => {
36+
const { dateOfCarePlan, changed, reasonCode } = reviews;
37+
2338
const formattedData = {
2439
effectiveDate: formatDate(dateOfCarePlan),
2540
effectiveDateTime: formatDateTime(dateOfCarePlan),
2641
hasChanged: changed,
27-
mrn,
2842
};
2943

3044
// Add reasonCode to formattedData if available
@@ -34,6 +48,9 @@ function formatData(tpcData) {
3448

3549
return formattedData;
3650
});
51+
52+
// Array will contain one element to generate one FHIR resource with multiple extensions for reviews
53+
return [combinedData];
3754
}
3855

3956
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: 30 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,30 @@ 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+
effectiveDateTime: '2020-04-15',
72+
hasChanged: 'true',
73+
reasonCode: '281647001',
74+
},
75+
{
76+
effectiveDate: '2020-04-30',
77+
effectiveDateTime: '2020-04-30',
78+
reasonCode: '405613005',
79+
hasChanged: 'true',
80+
},
81+
],
82+
},
83+
];
84+
85+
expect(formatData(exampleCSVTPCModuleResponse)).toEqual(expectedFormattedData);
86+
});
5787
});
5888

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

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

Lines changed: 17 additions & 3 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:a66c9b6d0c06506e28dcaac8fc2ef9126b1039507761c79c6e04f606b5c7b054",
77
"resource": {
88
"resourceType": "CarePlan",
9-
"id": "4d2afb8f74b6950922b0ae5979edbcaf59cecf959ddf80cce63940b45597a29c",
9+
"id": "a66c9b6d0c06506e28dcaac8fc2ef9126b1039507761c79c6e04f606b5c7b054",
1010
"meta": {
1111
"profile": [
1212
"http://mcodeinitiative.org/codex/us/icare/StructureDefinition/icare-care-plan-with-review"
@@ -31,12 +31,26 @@
3131
{ "url": "ReviewDate", "valueDate": "2020-04-15" },
3232
{ "url": "ChangedFlag", "valueBoolean": true }
3333
]
34+
},
35+
{
36+
"url": "http://mcodeinitiative.org/codex/us/icare/StructureDefinition/icare-care-plan-review",
37+
"extension": [
38+
{
39+
"url": "CarePlanChangeReason",
40+
"valueCodeableConcept": {
41+
"coding": [
42+
{ "system": "http://snomed.info/sct", "code": "405613005" }
43+
]
44+
}
45+
},
46+
{ "url": "ReviewDate", "valueDate": "2020-04-30" },
47+
{ "url": "ChangedFlag", "valueBoolean": true }
48+
]
3449
}
3550
],
3651
"subject": { "reference": "urn:uuid:mrn-1" },
3752
"status": "draft",
3853
"intent": "proposal",
39-
"created": "2020-04-15",
4054
"category": [
4155
{
4256
"coding": [

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,5 +4,11 @@
44
"dateOfCarePlan": "2020-04-15",
55
"reasonCode": "281647001",
66
"changed": "true"
7+
},
8+
{
9+
"mrn": "mrn-1",
10+
"dateOfCarePlan": "2020-04-30",
11+
"reasonCode": "405613005",
12+
"changed": "true"
713
}
814
]

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
});

test/templates/fixtures/maximal-careplan-resource.json

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,32 @@
3636
"valueBoolean": true
3737
}
3838
]
39+
},
40+
{
41+
"url": "http://mcodeinitiative.org/codex/us/icare/StructureDefinition/icare-care-plan-review",
42+
"extension": [
43+
{
44+
"url": "CarePlanChangeReason",
45+
"valueCodeableConcept": {
46+
"coding": [
47+
{
48+
"code": "405613005",
49+
"system": "http://snomed.info/sct",
50+
"display": "Planned Procedure (situation)"
51+
}
52+
],
53+
"text": "Planned Procedure (situation)"
54+
}
55+
},
56+
{
57+
"url": "ReviewDate",
58+
"valueDate": "2020-01-30"
59+
},
60+
{
61+
"url": "ChangedFlag",
62+
"valueBoolean": true
63+
}
64+
]
3965
}
4066
],
4167
"subject": {
@@ -44,7 +70,6 @@
4470
},
4571
"status": "draft",
4672
"intent": "proposal",
47-
"created": "2020-01-23T09:07:00Z",
4873
"category": [
4974
{
5075
"coding": [ { "code": "assess-plan", "system": "http://argonaut.hl7.org"}]

test/templates/fixtures/minimal-careplan-resource.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@
3030
},
3131
"status": "draft",
3232
"intent": "proposal",
33-
"created": "2020-01-23T09:07:00Z",
3433
"category": [
3534
{
3635
"coding": [ { "code": "assess-plan", "system": "http://argonaut.hl7.org"}]

0 commit comments

Comments
 (0)