@peopl-health/nexus 5.13.0-dev.1149 → 5.13.0-dev.1151
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/clinical/tools/submitRoutingDecisionTool.js +2 -2
- package/lib/fhir/helpers/resourceHelper.js +5 -2
- package/lib/fhir/index.js +2 -2
- package/lib/fhir/models/envelopeModel.js +15 -0
- package/lib/fhir/projections/registerProjectors.js +0 -2
- package/lib/fhir/projections/routingProjection.js +276 -40
- package/lib/fhir/resources/CarePlan.js +1 -18
- package/lib/fhir/resources/ClinicalImpression.js +2 -43
- package/lib/fhir/resources/CommunicationRequest.js +0 -20
- package/lib/fhir/resources/Provenance.js +0 -11
- package/lib/fhir/resources/Task.js +0 -19
- package/lib/fhir/services/routingService.js +18 -130
- package/lib/fhir/stores/fhirStore.js +2 -1
- package/package.json +1 -1
- package/lib/fhir/resources/Communication.js +0 -29
- package/lib/fhir/resources/RequestOrchestration.js +0 -46
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const crypto = require('node:crypto');
|
|
2
2
|
|
|
3
|
-
const { storeRoutingDecision,
|
|
3
|
+
const { storeRoutingDecision, readRoutingDecisions } = require('../../fhir');
|
|
4
4
|
const { RoutingDecision } = require('../../shared/dtos/RoutingDecision');
|
|
5
5
|
|
|
6
6
|
const _ROUTING_ACTIONS = [
|
|
@@ -273,7 +273,7 @@ async function handler(args = {}, context = {}) {
|
|
|
273
273
|
const patientId = runtime.patientCode;
|
|
274
274
|
const turnId = runtime.turnId;
|
|
275
275
|
|
|
276
|
-
const existing = await
|
|
276
|
+
const [existing] = await readRoutingDecisions({ patientId, turnId });
|
|
277
277
|
if (existing) {
|
|
278
278
|
return fail('decision_already_committed_for_turn', { existing_decision_id: existing.decisionId, committed_at: existing.committedAt });
|
|
279
279
|
}
|
|
@@ -11,13 +11,16 @@ function baseResource(resourceType, id, slug, patientId) {
|
|
|
11
11
|
};
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
function provenance({ provenanceId, targetRefs, recordedAt, activityText }) {
|
|
14
|
+
function provenance({ provenanceId, targetRefs, recordedAt, activityText, agentDevice = null, onBehalfOf = null }) {
|
|
15
15
|
return {
|
|
16
16
|
resourceType: 'Provenance',
|
|
17
17
|
id: fhirId(provenanceId),
|
|
18
18
|
target: targetRefs,
|
|
19
19
|
recorded: toIso(recordedAt),
|
|
20
|
-
agent: [{
|
|
20
|
+
agent: [{
|
|
21
|
+
who: reference('Device', agentDevice || getDeviceId()),
|
|
22
|
+
onBehalfOf: reference('Organization', onBehalfOf || getOrganizationId()),
|
|
23
|
+
}],
|
|
21
24
|
activity: codeableConcept(activityText),
|
|
22
25
|
};
|
|
23
26
|
}
|
package/lib/fhir/index.js
CHANGED
|
@@ -12,7 +12,7 @@ const {
|
|
|
12
12
|
const { storeContingency, readContingencies } = require('./services/contingencyService');
|
|
13
13
|
const { storeIntervention, readInterventions } = require('./services/interventionService');
|
|
14
14
|
const { storeCluster, readClusters } = require('./services/clusterService');
|
|
15
|
-
const { storeRoutingDecision,
|
|
15
|
+
const { storeRoutingDecision, readRoutingDecisions } = require('./services/routingService');
|
|
16
16
|
const {
|
|
17
17
|
storePalliativeAssessment,
|
|
18
18
|
readPalliativeByTurn,
|
|
@@ -55,7 +55,7 @@ module.exports = {
|
|
|
55
55
|
storeCluster,
|
|
56
56
|
readClusters,
|
|
57
57
|
storeRoutingDecision,
|
|
58
|
-
|
|
58
|
+
readRoutingDecisions,
|
|
59
59
|
storePalliativeAssessment,
|
|
60
60
|
readPalliativeByTurn,
|
|
61
61
|
storePalliativeCarePlan,
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const mongoose = require('mongoose');
|
|
2
2
|
|
|
3
3
|
const SYMPTOM_SYSTEM_SUFFIX = '/pro-ctcae';
|
|
4
|
+
const TURN_ID_EXT_SUFFIX = '-turn-id';
|
|
4
5
|
|
|
5
6
|
const envelopeSchema = new mongoose.Schema(
|
|
6
7
|
{
|
|
@@ -11,6 +12,7 @@ const envelopeSchema = new mongoose.Schema(
|
|
|
11
12
|
date: { type: Date, default: null },
|
|
12
13
|
reference: { type: String, default: null },
|
|
13
14
|
symptomTerm: { type: String, default: null },
|
|
15
|
+
turnId: { type: String, default: null },
|
|
14
16
|
},
|
|
15
17
|
resource: { type: mongoose.Schema.Types.Mixed, required: true },
|
|
16
18
|
meta: {
|
|
@@ -27,6 +29,10 @@ envelopeSchema.index(
|
|
|
27
29
|
{ 'index.patient': 1, 'resource.identifier.system': 1, 'index.date': -1 },
|
|
28
30
|
{ partialFilterExpression: { 'meta.deleted': false } },
|
|
29
31
|
);
|
|
32
|
+
envelopeSchema.index(
|
|
33
|
+
{ 'index.patient': 1, 'index.turnId': 1, 'index.date': -1 },
|
|
34
|
+
{ partialFilterExpression: { 'meta.deleted': false, 'index.turnId': { $type: 'string' } } },
|
|
35
|
+
);
|
|
30
36
|
envelopeSchema.index({ 'index.reference': 1 }, { partialFilterExpression: { 'index.reference': { $type: 'string' } } });
|
|
31
37
|
envelopeSchema.index({ 'resource.status': 1 }, { sparse: true });
|
|
32
38
|
|
|
@@ -67,6 +73,7 @@ envelopeSchema.statics.deriveIndex = function deriveIndex(resource) {
|
|
|
67
73
|
date: instantOf(date),
|
|
68
74
|
reference: resource.resourceType && resource.id ? `${resource.resourceType}/${resource.id}` : null,
|
|
69
75
|
symptomTerm: symptomTermOf(resource),
|
|
76
|
+
turnId: turnIdOf(resource),
|
|
70
77
|
};
|
|
71
78
|
};
|
|
72
79
|
|
|
@@ -93,6 +100,14 @@ function symptomTermOf(resource) {
|
|
|
93
100
|
return null;
|
|
94
101
|
}
|
|
95
102
|
|
|
103
|
+
function turnIdOf(resource) {
|
|
104
|
+
for (const ext of resource.extension || []) {
|
|
105
|
+
const url = (ext || {}).url;
|
|
106
|
+
if (typeof url === 'string' && url.endsWith(TURN_ID_EXT_SUFFIX)) return ext.valueString || null;
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
96
111
|
function businessIdentifier(resource) {
|
|
97
112
|
for (const identifier of resource.identifier || []) {
|
|
98
113
|
const system = (identifier || {}).system;
|
|
@@ -6,7 +6,6 @@ const { projectPatientRisk, PROJECTOR_NAME: RISK_PROJECTOR_NAME } = require('./r
|
|
|
6
6
|
const { projectSafetyGate, PROJECTOR_NAME: SAFETY_GATE_PROJECTOR_NAME } = require('./safetyGateProjection');
|
|
7
7
|
const { projectRecommendationPlan, PROJECTOR_NAME: RECOMMENDATION_PLAN_PROJECTOR_NAME } = require('./recommendationPlanProjection');
|
|
8
8
|
const { projectCluster, PROJECTOR_NAME: CLUSTER_PROJECTOR_NAME } = require('./clusterProjection');
|
|
9
|
-
const { projectRoutingDecision, PROJECTOR_NAME: ROUTING_PROJECTOR_NAME } = require('./routingProjection');
|
|
10
9
|
const { projectPalliativeAssessment, PROJECTOR_NAME: PALLIATIVE_PROJECTOR_NAME } = require('./palliativeProjection');
|
|
11
10
|
const { projectPalliativeCarePlan, PROJECTOR_NAME: PALLIATIVE_CAREPLAN_PROJECTOR_NAME } = require('./palliativeCarePlanProjection');
|
|
12
11
|
|
|
@@ -18,7 +17,6 @@ const PROJECTORS = [
|
|
|
18
17
|
{ name: SAFETY_GATE_PROJECTOR_NAME, project: projectSafetyGate },
|
|
19
18
|
{ name: RECOMMENDATION_PLAN_PROJECTOR_NAME, project: projectRecommendationPlan },
|
|
20
19
|
{ name: CLUSTER_PROJECTOR_NAME, project: projectCluster },
|
|
21
|
-
{ name: ROUTING_PROJECTOR_NAME, project: projectRoutingDecision },
|
|
22
20
|
{ name: PALLIATIVE_PROJECTOR_NAME, project: projectPalliativeAssessment },
|
|
23
21
|
{ name: PALLIATIVE_CAREPLAN_PROJECTOR_NAME, project: projectPalliativeCarePlan },
|
|
24
22
|
];
|
|
@@ -1,48 +1,284 @@
|
|
|
1
|
-
const {
|
|
2
|
-
|
|
3
|
-
const {
|
|
4
|
-
const {
|
|
5
|
-
const {
|
|
6
|
-
const {
|
|
7
|
-
const {
|
|
8
|
-
const {
|
|
9
|
-
|
|
10
|
-
const
|
|
1
|
+
const { ZodError } = require('zod');
|
|
2
|
+
|
|
3
|
+
const { getDeviceId } = require('../config/fhirConfig');
|
|
4
|
+
const { reference, patientReference, codeableConcept, extension, toIso } = require('../helpers/elementHelper');
|
|
5
|
+
const { baseResource, provenance } = require('../helpers/resourceHelper');
|
|
6
|
+
const { extMap, extValues, identifierValue, refToId } = require('../helpers/fhirReadHelper');
|
|
7
|
+
const { ROUTING_ASSESSMENT_SLUG, ROUTING_DISPOSITION_SLUG } = require('../constants/projectionSlugs');
|
|
8
|
+
const { ROUTING_EXT } = require('../constants/extensionSlugs');
|
|
9
|
+
const { RoutingDecision } = require('../../shared/dtos/RoutingDecision');
|
|
10
|
+
const { logger } = require('../../utils/logger');
|
|
11
|
+
|
|
11
12
|
const ACTIVITY_TEXT = 'routing-decision';
|
|
13
|
+
const COMPOSE_AS_PREFIX = 'compose_as: ';
|
|
14
|
+
|
|
15
|
+
const ccText = (concept) => concept?.text || null;
|
|
16
|
+
const caseReason = (caseId) => [{ reference: reference('Condition', caseId) }];
|
|
17
|
+
|
|
18
|
+
function dispositionBase(resourceType, disposition) {
|
|
19
|
+
return baseResource(resourceType, disposition.dispositionId, ROUTING_DISPOSITION_SLUG, disposition.patientId);
|
|
20
|
+
}
|
|
12
21
|
|
|
13
|
-
const
|
|
14
|
-
communication:
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
22
|
+
const DISPOSITION_CODECS = {
|
|
23
|
+
communication: {
|
|
24
|
+
resourceType: 'Communication',
|
|
25
|
+
build: (d) => ({
|
|
26
|
+
...dispositionBase('Communication', d),
|
|
27
|
+
status: 'completed',
|
|
28
|
+
category: [codeableConcept(d.primaryAction)],
|
|
29
|
+
sent: toIso(d.authoredAt),
|
|
30
|
+
payload: [{ contentCodeableConcept: codeableConcept(d.primaryReason) }],
|
|
31
|
+
recipient: [patientReference(d.patientId)],
|
|
32
|
+
...(d.primaryCaseId ? { reason: caseReason(d.primaryCaseId) } : {}),
|
|
33
|
+
}),
|
|
34
|
+
actionOf: (r) => ccText(r.category?.[0]),
|
|
35
|
+
reasonOf: (r) => ccText(r.payload?.[0]?.contentCodeableConcept),
|
|
36
|
+
authoredOf: (r) => r.sent,
|
|
37
|
+
},
|
|
38
|
+
communication_request: {
|
|
39
|
+
resourceType: 'CommunicationRequest',
|
|
40
|
+
build: (d) => ({
|
|
41
|
+
...dispositionBase('CommunicationRequest', d),
|
|
42
|
+
status: 'active',
|
|
43
|
+
intent: 'plan',
|
|
44
|
+
category: [codeableConcept(d.primaryAction)],
|
|
45
|
+
authoredOn: toIso(d.authoredAt),
|
|
46
|
+
payload: [{ contentCodeableConcept: codeableConcept(d.primaryReason) }],
|
|
47
|
+
recipient: [patientReference(d.patientId)],
|
|
48
|
+
...(d.primaryCaseId ? { reason: caseReason(d.primaryCaseId) } : {}),
|
|
49
|
+
}),
|
|
50
|
+
actionOf: (r) => ccText(r.category?.[0]),
|
|
51
|
+
reasonOf: (r) => ccText(r.payload?.[0]?.contentCodeableConcept),
|
|
52
|
+
authoredOf: (r) => r.authoredOn,
|
|
53
|
+
},
|
|
54
|
+
task: {
|
|
55
|
+
resourceType: 'Task',
|
|
56
|
+
build: (d) => {
|
|
57
|
+
const { subject, ...rest } = dispositionBase('Task', d);
|
|
58
|
+
return {
|
|
59
|
+
...rest,
|
|
60
|
+
status: 'requested',
|
|
61
|
+
intent: 'order',
|
|
62
|
+
code: codeableConcept(d.primaryAction),
|
|
63
|
+
description: d.primaryReason,
|
|
64
|
+
for: subject,
|
|
65
|
+
authoredOn: toIso(d.authoredAt),
|
|
66
|
+
requester: reference('Device', getDeviceId()),
|
|
67
|
+
...(d.primaryCaseId ? { reason: caseReason(d.primaryCaseId) } : {}),
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
actionOf: (r) => ccText(r.code),
|
|
71
|
+
reasonOf: (r) => r.description,
|
|
72
|
+
authoredOf: (r) => r.authoredOn,
|
|
73
|
+
},
|
|
74
|
+
orchestration: {
|
|
75
|
+
resourceType: 'RequestOrchestration',
|
|
76
|
+
build: (d) => ({
|
|
77
|
+
...dispositionBase('RequestOrchestration', d),
|
|
78
|
+
status: 'active',
|
|
79
|
+
intent: 'plan',
|
|
80
|
+
authoredOn: toIso(d.authoredAt),
|
|
81
|
+
action: [
|
|
82
|
+
{ title: d.primaryAction, description: d.primaryReason, code: [codeableConcept(d.primaryAction)] },
|
|
83
|
+
...d.secondaryActions.map((secondary) => ({
|
|
84
|
+
title: secondary.action,
|
|
85
|
+
description: `${COMPOSE_AS_PREFIX}${secondary.composeAs}`,
|
|
86
|
+
code: [codeableConcept(secondary.action)],
|
|
87
|
+
})),
|
|
88
|
+
],
|
|
89
|
+
...(d.primaryCaseId ? { reason: caseReason(d.primaryCaseId) } : {}),
|
|
90
|
+
}),
|
|
91
|
+
actionOf: (r) => r.action?.[0]?.title,
|
|
92
|
+
reasonOf: (r) => r.action?.[0]?.description,
|
|
93
|
+
authoredOf: (r) => r.authoredOn,
|
|
94
|
+
},
|
|
95
|
+
care_plan: {
|
|
96
|
+
resourceType: 'CarePlan',
|
|
97
|
+
build: (d) => ({
|
|
98
|
+
...dispositionBase('CarePlan', d),
|
|
99
|
+
status: 'active',
|
|
100
|
+
intent: 'plan',
|
|
101
|
+
category: [codeableConcept(d.primaryAction)],
|
|
102
|
+
created: toIso(d.authoredAt),
|
|
103
|
+
description: d.primaryReason,
|
|
104
|
+
...(d.primaryCaseId ? { addresses: caseReason(d.primaryCaseId) } : {}),
|
|
105
|
+
}),
|
|
106
|
+
actionOf: (r) => ccText(r.category?.[0]),
|
|
107
|
+
reasonOf: (r) => r.description,
|
|
108
|
+
authoredOf: (r) => r.created,
|
|
109
|
+
},
|
|
19
110
|
};
|
|
20
111
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
112
|
+
const KIND_BY_RESOURCE_TYPE = Object.fromEntries(
|
|
113
|
+
Object.entries(DISPOSITION_CODECS).map(([kind, codec]) => [codec.resourceType, kind]),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
function toFhir(routingDecision) {
|
|
117
|
+
const { decisionId, committedAt, assessment, disposition, provenance: prov } = routingDecision;
|
|
118
|
+
const codec = DISPOSITION_CODECS[disposition.kind];
|
|
119
|
+
if (!codec) throw new Error(`unknown routing-disposition kind: ${disposition.kind}`);
|
|
120
|
+
const dispositionResource = codec.build(disposition);
|
|
121
|
+
return [
|
|
122
|
+
assessmentResource(assessment, { decisionId, committedAt, disposition }),
|
|
123
|
+
dispositionResource,
|
|
124
|
+
provenance({
|
|
125
|
+
provenanceId: `${assessment.assessmentId}-prov`,
|
|
126
|
+
targetRefs: [
|
|
127
|
+
reference('ClinicalImpression', assessment.assessmentId),
|
|
128
|
+
reference(dispositionResource.resourceType, disposition.dispositionId),
|
|
129
|
+
],
|
|
130
|
+
recordedAt: prov.recordedAt,
|
|
131
|
+
activityText: ACTIVITY_TEXT,
|
|
132
|
+
agentDevice: prov.agentDevice,
|
|
133
|
+
onBehalfOf: prov.onBehalfOf,
|
|
134
|
+
}),
|
|
135
|
+
];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function assessmentResource(assessment, { decisionId, committedAt, disposition }) {
|
|
139
|
+
const payload = {
|
|
140
|
+
...baseResource('ClinicalImpression', assessment.assessmentId, ROUTING_ASSESSMENT_SLUG, assessment.patientId),
|
|
141
|
+
status: 'completed',
|
|
142
|
+
effectiveDateTime: toIso(assessment.performedAt),
|
|
143
|
+
finding: assessment.findings.map((finding) => {
|
|
144
|
+
const entry = {};
|
|
145
|
+
const extensions = [];
|
|
146
|
+
if (finding.caseId) {
|
|
147
|
+
entry.item = reference('Condition', finding.caseId);
|
|
148
|
+
extensions.push(extension(ROUTING_EXT.FINDING_CASE_ID, { valueString: finding.caseId }));
|
|
149
|
+
}
|
|
150
|
+
extensions.push(
|
|
151
|
+
extension(ROUTING_EXT.FINDING_REASONING, { valueString: finding.clinicalReasoning }),
|
|
152
|
+
extension(ROUTING_EXT.FINDING_ACTION, { valueString: finding.decisionAction }),
|
|
153
|
+
);
|
|
154
|
+
entry.extension = extensions;
|
|
155
|
+
return entry;
|
|
156
|
+
}),
|
|
157
|
+
extension: assessmentExtensions(assessment, { decisionId, committedAt, disposition }),
|
|
158
|
+
};
|
|
159
|
+
if (assessment.summary) payload.summary = assessment.summary;
|
|
160
|
+
if (assessment.previousId) payload.previous = reference('ClinicalImpression', assessment.previousId);
|
|
161
|
+
if (assessment.notes.length) payload.note = assessment.notes.map((text) => ({ text }));
|
|
162
|
+
return payload;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function assessmentExtensions(assessment, { decisionId, committedAt, disposition }) {
|
|
166
|
+
const out = [
|
|
167
|
+
extension(ROUTING_EXT.TURN_ID, { valueString: assessment.turnId }),
|
|
168
|
+
extension(ROUTING_EXT.DECISION_ID, { valueString: decisionId }),
|
|
169
|
+
extension(ROUTING_EXT.COMMITTED_AT, { valueDateTime: toIso(committedAt) }),
|
|
170
|
+
];
|
|
171
|
+
if (disposition.primaryCaseId) {
|
|
172
|
+
out.push(extension(ROUTING_EXT.PRIMARY_CASE_ID, { valueString: disposition.primaryCaseId }));
|
|
173
|
+
}
|
|
174
|
+
for (const deferred of disposition.deferredItems) {
|
|
175
|
+
out.push(
|
|
176
|
+
extension(ROUTING_EXT.DEFERRED_ITEM, { valueString: deferred.item }),
|
|
177
|
+
extension(ROUTING_EXT.DEFERRED_REASON, { valueString: deferred.deferReason }),
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
return out;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function fromFhir(resources, { patientId }) {
|
|
184
|
+
const assessments = new Map();
|
|
185
|
+
const dispositions = new Map();
|
|
186
|
+
const provenanceByTarget = new Map();
|
|
187
|
+
for (const resource of resources) {
|
|
188
|
+
if (resource.resourceType === 'ClinicalImpression' && identifierValue(resource, ROUTING_ASSESSMENT_SLUG)) {
|
|
189
|
+
assessments.set(`ClinicalImpression/${resource.id}`, resource);
|
|
190
|
+
} else if (resource.resourceType === 'Provenance') {
|
|
191
|
+
for (const target of resource.target || []) provenanceByTarget.set(target.reference, resource);
|
|
192
|
+
} else if (KIND_BY_RESOURCE_TYPE[resource.resourceType] && identifierValue(resource, ROUTING_DISPOSITION_SLUG)) {
|
|
193
|
+
dispositions.set(`${resource.resourceType}/${resource.id}`, resource);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const out = [];
|
|
198
|
+
for (const [assessmentRef, assessment] of assessments) {
|
|
199
|
+
const prov = provenanceByTarget.get(assessmentRef);
|
|
200
|
+
if (!prov) continue;
|
|
201
|
+
const dispositionRef = (prov.target || []).find((target) => dispositions.has(target.reference));
|
|
202
|
+
if (!dispositionRef) continue;
|
|
203
|
+
const disposition = dispositions.get(dispositionRef.reference);
|
|
204
|
+
try {
|
|
205
|
+
out.push(new RoutingDecision(reconstruct(assessment, disposition, prov, patientId)));
|
|
206
|
+
} catch (error) {
|
|
207
|
+
if (error instanceof ZodError) {
|
|
208
|
+
logger.warn({ assessmentId: assessment.id }, 'skipping malformed routing ClinicalImpression');
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
return out;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function reconstruct(assessment, disposition, prov, patientId) {
|
|
218
|
+
const ext = extMap(assessment);
|
|
219
|
+
const turnId = ext[ROUTING_EXT.TURN_ID];
|
|
220
|
+
const agent = prov.agent?.[0] || {};
|
|
221
|
+
return {
|
|
222
|
+
schemaVersion: '1',
|
|
223
|
+
decisionId: ext[ROUTING_EXT.DECISION_ID],
|
|
224
|
+
patientId,
|
|
225
|
+
turnId,
|
|
226
|
+
committedAt: ext[ROUTING_EXT.COMMITTED_AT],
|
|
227
|
+
assessment: reconstructAssessment(assessment, patientId, turnId),
|
|
228
|
+
disposition: reconstructDisposition(disposition, assessment, patientId),
|
|
229
|
+
provenance: {
|
|
230
|
+
agentDevice: refToId(agent.who?.reference),
|
|
231
|
+
onBehalfOf: refToId(agent.onBehalfOf?.reference),
|
|
232
|
+
recordedAt: prov.recorded,
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function reconstructAssessment(resource, patientId, turnId) {
|
|
238
|
+
return {
|
|
239
|
+
assessmentId: identifierValue(resource, ROUTING_ASSESSMENT_SLUG),
|
|
240
|
+
patientId,
|
|
241
|
+
turnId,
|
|
242
|
+
performedAt: resource.effectiveDateTime,
|
|
243
|
+
previousId: refToId(resource.previous?.reference),
|
|
244
|
+
summary: resource.summary || '',
|
|
245
|
+
notes: (resource.note || []).map((note) => note.text),
|
|
246
|
+
findings: (resource.finding || []).map((finding) => {
|
|
247
|
+
const findingExt = extMap(finding);
|
|
248
|
+
return {
|
|
249
|
+
caseId: findingExt[ROUTING_EXT.FINDING_CASE_ID] || refToId(finding.item?.reference),
|
|
250
|
+
clinicalReasoning: findingExt[ROUTING_EXT.FINDING_REASONING] || '',
|
|
251
|
+
decisionAction: findingExt[ROUTING_EXT.FINDING_ACTION] || '',
|
|
252
|
+
};
|
|
253
|
+
}),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function reconstructDisposition(resource, assessment, patientId) {
|
|
258
|
+
const kind = KIND_BY_RESOURCE_TYPE[resource.resourceType];
|
|
259
|
+
const codec = DISPOSITION_CODECS[kind];
|
|
260
|
+
const deferReasons = extValues(assessment, ROUTING_EXT.DEFERRED_REASON);
|
|
261
|
+
return {
|
|
262
|
+
dispositionId: identifierValue(resource, ROUTING_DISPOSITION_SLUG),
|
|
263
|
+
patientId,
|
|
264
|
+
kind,
|
|
265
|
+
primaryAction: codec.actionOf(resource),
|
|
266
|
+
primaryReason: codec.reasonOf(resource),
|
|
267
|
+
primaryCaseId: extMap(assessment)[ROUTING_EXT.PRIMARY_CASE_ID] || null,
|
|
268
|
+
secondaryActions: (resource.action || []).slice(1).map((action) => {
|
|
269
|
+
const description = action.description || '';
|
|
270
|
+
return {
|
|
271
|
+
action: action.title,
|
|
272
|
+
composeAs: description.startsWith(COMPOSE_AS_PREFIX) ? description.slice(COMPOSE_AS_PREFIX.length) : description,
|
|
273
|
+
};
|
|
274
|
+
}),
|
|
275
|
+
deferredItems: extValues(assessment, ROUTING_EXT.DEFERRED_ITEM)
|
|
276
|
+
.map((item, i) => ({ item, deferReason: deferReasons[i] })),
|
|
277
|
+
authoredAt: codec.authoredOf(resource),
|
|
278
|
+
};
|
|
43
279
|
}
|
|
44
280
|
|
|
45
281
|
module.exports = {
|
|
46
|
-
|
|
47
|
-
|
|
282
|
+
toFhir,
|
|
283
|
+
fromFhir,
|
|
48
284
|
};
|
|
@@ -7,7 +7,7 @@ const {
|
|
|
7
7
|
extension,
|
|
8
8
|
toIso,
|
|
9
9
|
} = require('../helpers/elementHelper');
|
|
10
|
-
const {
|
|
10
|
+
const { RECOMMENDATION_PLAN_SLUG, PALLIATIVE_CAREPLAN_SLUG } = require('../constants/projectionSlugs');
|
|
11
11
|
const { PALLIATIVE_EXT } = require('../constants/extensionSlugs');
|
|
12
12
|
|
|
13
13
|
class CarePlan {
|
|
@@ -60,23 +60,6 @@ class CarePlan {
|
|
|
60
60
|
return new CarePlan(payload);
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
static fromRoutingDisposition({ disposition }) {
|
|
64
|
-
const payload = {
|
|
65
|
-
resourceType: 'CarePlan',
|
|
66
|
-
id: fhirId(disposition.dispositionId),
|
|
67
|
-
identifier: [identifier(ROUTING_DISPOSITION_SLUG, disposition.dispositionId)],
|
|
68
|
-
status: 'active',
|
|
69
|
-
intent: 'plan',
|
|
70
|
-
category: [codeableConcept(disposition.primaryAction)],
|
|
71
|
-
subject: patientReference(disposition.patientId),
|
|
72
|
-
created: toIso(disposition.authoredAt),
|
|
73
|
-
description: disposition.primaryReason,
|
|
74
|
-
};
|
|
75
|
-
if (disposition.primaryCaseId) {
|
|
76
|
-
payload.addresses = [{ reference: reference('Condition', disposition.primaryCaseId) }];
|
|
77
|
-
}
|
|
78
|
-
return new CarePlan(payload);
|
|
79
|
-
}
|
|
80
63
|
}
|
|
81
64
|
|
|
82
65
|
module.exports = {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const { fhirId } = require('../helpers/fhirHelper');
|
|
2
2
|
const { identifier, reference, patientReference, codeableConcept, extension, toIso } = require('../helpers/elementHelper');
|
|
3
|
-
const { CLUSTER_IMPRESSION_SLUG, CLUSTER_LABEL_SLUG,
|
|
4
|
-
const {
|
|
3
|
+
const { CLUSTER_IMPRESSION_SLUG, CLUSTER_LABEL_SLUG, RECOMMENDATION_ELEMENTS_SLUG, PALLIATIVE_ASSESSMENT_SLUG } = require('../constants/projectionSlugs');
|
|
4
|
+
const { CLUSTER_EXT, PALLIATIVE_EXT } = require('../constants/extensionSlugs');
|
|
5
5
|
|
|
6
6
|
const CLUSTER_STATUS_TO_FHIR = { active: 'in-progress', dissolved: 'completed' };
|
|
7
7
|
const CLUSTER_LIST_SEPARATOR = '; ';
|
|
@@ -75,47 +75,6 @@ class ClinicalImpression {
|
|
|
75
75
|
return new ClinicalImpression(payload);
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
static fromRoutingAssessment({ assessment, decisionId, committedAt, deferredItems = [], primaryCaseId = null }) {
|
|
79
|
-
const extensions = [
|
|
80
|
-
extension(ROUTING_EXT.TURN_ID, { valueString: assessment.turnId }),
|
|
81
|
-
extension(ROUTING_EXT.DECISION_ID, { valueString: decisionId }),
|
|
82
|
-
extension(ROUTING_EXT.COMMITTED_AT, { valueDateTime: toIso(committedAt) }),
|
|
83
|
-
];
|
|
84
|
-
if (primaryCaseId) extensions.push(extension(ROUTING_EXT.PRIMARY_CASE_ID, { valueString: primaryCaseId }));
|
|
85
|
-
for (const deferred of deferredItems) {
|
|
86
|
-
extensions.push(extension(ROUTING_EXT.DEFERRED_ITEM, { valueString: deferred.item }));
|
|
87
|
-
extensions.push(extension(ROUTING_EXT.DEFERRED_REASON, { valueString: deferred.deferReason }));
|
|
88
|
-
}
|
|
89
|
-
const payload = {
|
|
90
|
-
resourceType: 'ClinicalImpression',
|
|
91
|
-
id: fhirId(assessment.assessmentId),
|
|
92
|
-
identifier: [identifier(ROUTING_ASSESSMENT_SLUG, assessment.assessmentId)],
|
|
93
|
-
status: 'completed',
|
|
94
|
-
subject: patientReference(assessment.patientId),
|
|
95
|
-
effectiveDateTime: toIso(assessment.performedAt),
|
|
96
|
-
finding: assessment.findings.map((finding) => {
|
|
97
|
-
const findingExts = [];
|
|
98
|
-
const projected = {};
|
|
99
|
-
// A caseless mention (caseId null) has no case to point at: no Condition item and no case-id extension, so absence stays absence in the FHIR.
|
|
100
|
-
if (finding.caseId) {
|
|
101
|
-
projected.item = reference('Condition', finding.caseId);
|
|
102
|
-
findingExts.push(extension(ROUTING_EXT.FINDING_CASE_ID, { valueString: finding.caseId }));
|
|
103
|
-
}
|
|
104
|
-
findingExts.push(
|
|
105
|
-
extension(ROUTING_EXT.FINDING_REASONING, { valueString: finding.clinicalReasoning }),
|
|
106
|
-
extension(ROUTING_EXT.FINDING_ACTION, { valueString: finding.decisionAction }),
|
|
107
|
-
);
|
|
108
|
-
projected.extension = findingExts;
|
|
109
|
-
return projected;
|
|
110
|
-
}),
|
|
111
|
-
extension: extensions,
|
|
112
|
-
};
|
|
113
|
-
if (assessment.summary) payload.summary = assessment.summary;
|
|
114
|
-
if (assessment.previousId) payload.previous = reference('ClinicalImpression', assessment.previousId);
|
|
115
|
-
if (assessment.notes.length) payload.note = assessment.notes.map((text) => ({ text }));
|
|
116
|
-
return new ClinicalImpression(payload);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
78
|
static fromPalliativeAssessment({ assessment }) {
|
|
120
79
|
const payload = {
|
|
121
80
|
resourceType: 'ClinicalImpression',
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
const { fhirId, codeSystemUrl } = require('../helpers/fhirHelper');
|
|
2
2
|
const {
|
|
3
3
|
identifier,
|
|
4
|
-
reference,
|
|
5
4
|
patientReference,
|
|
6
5
|
recipientReference,
|
|
7
6
|
codeableConcept,
|
|
@@ -15,7 +14,6 @@ const {
|
|
|
15
14
|
REMINDER_SLUG,
|
|
16
15
|
REMINDER_KIND_SLUG,
|
|
17
16
|
COMMUNICATION_CHANNEL_SLUG,
|
|
18
|
-
ROUTING_DISPOSITION_SLUG,
|
|
19
17
|
} = require('../constants/projectionSlugs');
|
|
20
18
|
const { SEVERITY_TO_PRIORITY } = require('../constants/severityPriority');
|
|
21
19
|
|
|
@@ -88,24 +86,6 @@ class CommunicationRequest {
|
|
|
88
86
|
return new CommunicationRequest(payload);
|
|
89
87
|
}
|
|
90
88
|
|
|
91
|
-
static fromRoutingDisposition({ disposition }) {
|
|
92
|
-
const payload = {
|
|
93
|
-
resourceType: 'CommunicationRequest',
|
|
94
|
-
id: fhirId(disposition.dispositionId),
|
|
95
|
-
identifier: [identifier(ROUTING_DISPOSITION_SLUG, disposition.dispositionId)],
|
|
96
|
-
status: 'active',
|
|
97
|
-
intent: 'plan',
|
|
98
|
-
category: [codeableConcept(disposition.primaryAction)],
|
|
99
|
-
subject: patientReference(disposition.patientId),
|
|
100
|
-
authoredOn: toIso(disposition.authoredAt),
|
|
101
|
-
payload: [{ contentCodeableConcept: codeableConcept(disposition.primaryReason) }],
|
|
102
|
-
recipient: [patientReference(disposition.patientId)],
|
|
103
|
-
};
|
|
104
|
-
if (disposition.primaryCaseId) {
|
|
105
|
-
payload.reason = [{ reference: reference('Condition', disposition.primaryCaseId) }];
|
|
106
|
-
}
|
|
107
|
-
return new CommunicationRequest(payload);
|
|
108
|
-
}
|
|
109
89
|
}
|
|
110
90
|
|
|
111
91
|
module.exports = { CommunicationRequest };
|
|
@@ -56,17 +56,6 @@ class Provenance {
|
|
|
56
56
|
});
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
static fromRoutingDecision({ provenanceId, targetRefs, provenance, activityText }) {
|
|
60
|
-
return new Provenance({
|
|
61
|
-
resourceType: 'Provenance',
|
|
62
|
-
id: fhirId(provenanceId),
|
|
63
|
-
target: targetRefs,
|
|
64
|
-
recorded: toIso(provenance.recordedAt),
|
|
65
|
-
agent: [{ who: reference('Device', provenance.agentDevice), onBehalfOf: reference('Organization', provenance.onBehalfOf) }],
|
|
66
|
-
activity: codeableConcept(activityText),
|
|
67
|
-
});
|
|
68
|
-
}
|
|
69
|
-
|
|
70
59
|
static fromSymptomCase({ provenanceId, targetRefs, recordedAt, activityText }) {
|
|
71
60
|
return new Provenance({
|
|
72
61
|
resourceType: 'Provenance',
|
|
@@ -12,7 +12,6 @@ const {
|
|
|
12
12
|
const {
|
|
13
13
|
ESCALATION_SLUG,
|
|
14
14
|
ESCALATION_KIND_SLUG,
|
|
15
|
-
ROUTING_DISPOSITION_SLUG,
|
|
16
15
|
RECOMMENDATION_REC_SLUG,
|
|
17
16
|
PALLIATIVE_ESCALATION_SLUG,
|
|
18
17
|
PALLIATIVE_CAREPLAN_REC_SLUG,
|
|
@@ -148,24 +147,6 @@ class Task {
|
|
|
148
147
|
return new Task(payload);
|
|
149
148
|
}
|
|
150
149
|
|
|
151
|
-
static fromRoutingDisposition({ disposition }) {
|
|
152
|
-
const payload = {
|
|
153
|
-
resourceType: 'Task',
|
|
154
|
-
id: fhirId(disposition.dispositionId),
|
|
155
|
-
identifier: [identifier(ROUTING_DISPOSITION_SLUG, disposition.dispositionId)],
|
|
156
|
-
status: 'requested',
|
|
157
|
-
intent: 'order',
|
|
158
|
-
code: codeableConcept(disposition.primaryAction),
|
|
159
|
-
description: disposition.primaryReason,
|
|
160
|
-
for: patientReference(disposition.patientId),
|
|
161
|
-
authoredOn: toIso(disposition.authoredAt),
|
|
162
|
-
requester: reference('Device', getDeviceId()),
|
|
163
|
-
};
|
|
164
|
-
if (disposition.primaryCaseId) {
|
|
165
|
-
payload.reason = [{ reference: reference('Condition', disposition.primaryCaseId) }];
|
|
166
|
-
}
|
|
167
|
-
return new Task(payload);
|
|
168
|
-
}
|
|
169
150
|
}
|
|
170
151
|
|
|
171
152
|
module.exports = { Task };
|
|
@@ -1,148 +1,36 @@
|
|
|
1
|
-
const { ZodError } = require('zod');
|
|
2
|
-
|
|
3
1
|
const { fhirId } = require('../helpers/fhirHelper');
|
|
4
|
-
const {
|
|
5
|
-
const { ROUTING_ASSESSMENT_SLUG, ROUTING_DISPOSITION_SLUG } = require('../constants/projectionSlugs');
|
|
6
|
-
const { ROUTING_EXT } = require('../constants/extensionSlugs');
|
|
7
|
-
const { PROJECTOR_NAME } = require('../projections/routingProjection');
|
|
2
|
+
const { ROUTING_ASSESSMENT_SLUG } = require('../constants/projectionSlugs');
|
|
8
3
|
const { getFhirStore } = require('../stores/fhirStore');
|
|
9
|
-
const {
|
|
10
|
-
const {
|
|
11
|
-
const { project, storeResources } = require('./fhirService');
|
|
12
|
-
|
|
13
|
-
const DISPOSITION_KIND_BY_RESOURCE = {
|
|
14
|
-
Communication: 'communication',
|
|
15
|
-
CommunicationRequest: 'communication_request',
|
|
16
|
-
Task: 'task',
|
|
17
|
-
RequestOrchestration: 'orchestration',
|
|
18
|
-
CarePlan: 'care_plan',
|
|
19
|
-
};
|
|
4
|
+
const { toFhir, fromFhir } = require('../projections/routingProjection');
|
|
5
|
+
const { storeResources } = require('./fhirService');
|
|
20
6
|
|
|
21
7
|
async function storeRoutingDecision({ patientId, routingDecision }) {
|
|
22
8
|
if (routingDecision.patientId !== patientId) {
|
|
23
9
|
throw new Error(`storeRoutingDecision patientId mismatch: expected ${patientId}`);
|
|
24
10
|
}
|
|
25
|
-
const
|
|
26
|
-
const resources = bundle.entry.map((entry) => entry.resource);
|
|
27
|
-
const { stored } = await storeResources(resources);
|
|
11
|
+
const { stored } = await storeResources(toFhir(routingDecision));
|
|
28
12
|
return { count: stored.length, ids: stored };
|
|
29
13
|
}
|
|
30
14
|
|
|
31
|
-
async function
|
|
15
|
+
async function readRoutingDecisions({ patientId, turnId = null }) {
|
|
32
16
|
const store = getFhirStore();
|
|
33
17
|
const patient = `Patient/${fhirId(patientId)}`;
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
if (!assessment) return null;
|
|
38
|
-
const provenance = await store.getByReference(`Provenance/${fhirId(`${assessment.id}-prov`)}`);
|
|
39
|
-
if (!provenance) return null;
|
|
40
|
-
const disposition = await findDisposition(store, assessment, provenance);
|
|
41
|
-
if (!disposition) return null;
|
|
42
|
-
try {
|
|
43
|
-
return new RoutingDecision(reconstructRouting(assessment, disposition, provenance, patientId, turnId));
|
|
44
|
-
} catch (error) {
|
|
45
|
-
if (error instanceof ZodError) {
|
|
46
|
-
logger.warn({ assessmentId: assessment.id }, 'skipping malformed routing ClinicalImpression');
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
|
-
throw error;
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
async function findDisposition(store, assessment, provenance) {
|
|
54
|
-
const assessmentRef = `ClinicalImpression/${fhirId(assessment.id)}`;
|
|
55
|
-
const dispositionRef = (provenance.target || []).find((target) => target.reference !== assessmentRef);
|
|
56
|
-
if (!dispositionRef) return null;
|
|
57
|
-
const resource = await store.getByReference(dispositionRef.reference);
|
|
58
|
-
return resource && identifierValue(resource, ROUTING_DISPOSITION_SLUG) ? resource : null;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function reconstructRouting(assessment, disposition, provenance, patientId, turnId) {
|
|
62
|
-
const ext = extMap(assessment);
|
|
63
|
-
const agent = (provenance.agent || [])[0] || {};
|
|
64
|
-
return {
|
|
65
|
-
schemaVersion: '1',
|
|
66
|
-
decisionId: ext[ROUTING_EXT.DECISION_ID],
|
|
67
|
-
patientId,
|
|
68
|
-
turnId,
|
|
69
|
-
committedAt: ext[ROUTING_EXT.COMMITTED_AT],
|
|
70
|
-
assessment: reconstructAssessment(assessment, patientId, turnId),
|
|
71
|
-
disposition: reconstructDisposition(disposition, assessment, patientId),
|
|
72
|
-
provenance: {
|
|
73
|
-
agentDevice: refToId(agent.who && agent.who.reference),
|
|
74
|
-
onBehalfOf: refToId(agent.onBehalfOf && agent.onBehalfOf.reference),
|
|
75
|
-
recordedAt: provenance.recorded,
|
|
76
|
-
},
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function reconstructAssessment(resource, patientId, turnId) {
|
|
81
|
-
return {
|
|
82
|
-
assessmentId: identifierValue(resource, ROUTING_ASSESSMENT_SLUG),
|
|
83
|
-
patientId,
|
|
18
|
+
const assessments = await store.find({
|
|
19
|
+
patient,
|
|
20
|
+
resourceType: 'ClinicalImpression',
|
|
84
21
|
turnId,
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const ext = extMap(finding);
|
|
95
|
-
return {
|
|
96
|
-
caseId: ext[ROUTING_EXT.FINDING_CASE_ID] || refToId(finding.item && finding.item.reference),
|
|
97
|
-
clinicalReasoning: ext[ROUTING_EXT.FINDING_REASONING] || '',
|
|
98
|
-
decisionAction: ext[ROUTING_EXT.FINDING_ACTION] || '',
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function reconstructDisposition(resource, assessment, patientId) {
|
|
103
|
-
return {
|
|
104
|
-
dispositionId: identifierValue(resource, ROUTING_DISPOSITION_SLUG),
|
|
105
|
-
patientId,
|
|
106
|
-
kind: DISPOSITION_KIND_BY_RESOURCE[resource.resourceType],
|
|
107
|
-
primaryAction: dispositionAction(resource),
|
|
108
|
-
primaryReason: dispositionReason(resource),
|
|
109
|
-
primaryCaseId: extMap(assessment)[ROUTING_EXT.PRIMARY_CASE_ID] || null,
|
|
110
|
-
secondaryActions: (resource.action || []).slice(1).map(reconstructSecondary),
|
|
111
|
-
deferredItems: reconstructDeferredItems(assessment),
|
|
112
|
-
authoredAt: resource.sent || resource.authoredOn || resource.created,
|
|
113
|
-
};
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
function reconstructDeferredItems(assessment) {
|
|
117
|
-
const items = extValues(assessment, ROUTING_EXT.DEFERRED_ITEM);
|
|
118
|
-
const reasons = extValues(assessment, ROUTING_EXT.DEFERRED_REASON);
|
|
119
|
-
return items.map((item, i) => ({ item, deferReason: reasons[i] }));
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function reconstructSecondary(action) {
|
|
123
|
-
const description = action.description || '';
|
|
124
|
-
return {
|
|
125
|
-
action: action.title,
|
|
126
|
-
composeAs: description.startsWith('compose_as: ') ? description.slice('compose_as: '.length) : description,
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function dispositionAction(resource) {
|
|
131
|
-
if (resource.resourceType === 'Task') return ccText(resource.code);
|
|
132
|
-
if (resource.resourceType === 'RequestOrchestration') return (resource.action || [])[0] && resource.action[0].title;
|
|
133
|
-
return ccText((resource.category || [])[0]);
|
|
22
|
+
identifierSlug: ROUTING_ASSESSMENT_SLUG,
|
|
23
|
+
});
|
|
24
|
+
const bundles = await Promise.all(assessments.map(async (assessment) => {
|
|
25
|
+
const provenance = await store.getByReference(`Provenance/${fhirId(`${assessment.id}-prov`)}`);
|
|
26
|
+
if (!provenance) return [];
|
|
27
|
+
const targets = await Promise.all((provenance.target || []).map((target) => store.getByReference(target.reference)));
|
|
28
|
+
return [assessment, provenance, ...targets.filter(Boolean)];
|
|
29
|
+
}));
|
|
30
|
+
return fromFhir(bundles.flat(), { patientId });
|
|
134
31
|
}
|
|
135
32
|
|
|
136
|
-
function dispositionReason(resource) {
|
|
137
|
-
if (resource.resourceType === 'Task') return resource.description;
|
|
138
|
-
if (resource.resourceType === 'CarePlan') return resource.description;
|
|
139
|
-
if (resource.resourceType === 'RequestOrchestration') return (resource.action || [])[0] && resource.action[0].description;
|
|
140
|
-
return ccText((resource.payload || [])[0] && resource.payload[0].contentCodeableConcept);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
const ccText = (concept) => (concept && concept.text) || null;
|
|
144
|
-
|
|
145
33
|
module.exports = {
|
|
146
34
|
storeRoutingDecision,
|
|
147
|
-
|
|
35
|
+
readRoutingDecisions,
|
|
148
36
|
};
|
|
@@ -82,10 +82,11 @@ class FhirStore {
|
|
|
82
82
|
return envelope ? envelope.resource : null;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
async find({ patient = null, resourceType = null, symptomTerm = null, identifierSlug = null } = {}) {
|
|
85
|
+
async find({ patient = null, resourceType = null, symptomTerm = null, identifierSlug = null, turnId = null } = {}) {
|
|
86
86
|
const query = { 'meta.deleted': false };
|
|
87
87
|
if (patient) query['index.patient'] = patient;
|
|
88
88
|
if (symptomTerm) query['index.symptomTerm'] = symptomTerm;
|
|
89
|
+
if (turnId) query['index.turnId'] = turnId;
|
|
89
90
|
if (identifierSlug) query['resource.identifier.system'] = identifierSystem(identifierSlug);
|
|
90
91
|
if (resourceType) {
|
|
91
92
|
const model = await this._modelFor(resourceType);
|
package/package.json
CHANGED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
const { fhirId } = require('../helpers/fhirHelper');
|
|
2
|
-
const { identifier, reference, patientReference, codeableConcept, toIso } = require('../helpers/elementHelper');
|
|
3
|
-
const { ROUTING_DISPOSITION_SLUG } = require('../constants/projectionSlugs');
|
|
4
|
-
|
|
5
|
-
class Communication {
|
|
6
|
-
constructor(payload) {
|
|
7
|
-
Object.assign(this, payload);
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
static fromRoutingDisposition({ disposition }) {
|
|
11
|
-
const payload = {
|
|
12
|
-
resourceType: 'Communication',
|
|
13
|
-
id: fhirId(disposition.dispositionId),
|
|
14
|
-
identifier: [identifier(ROUTING_DISPOSITION_SLUG, disposition.dispositionId)],
|
|
15
|
-
status: 'completed',
|
|
16
|
-
category: [codeableConcept(disposition.primaryAction)],
|
|
17
|
-
subject: patientReference(disposition.patientId),
|
|
18
|
-
sent: toIso(disposition.authoredAt),
|
|
19
|
-
payload: [{ contentCodeableConcept: codeableConcept(disposition.primaryReason) }],
|
|
20
|
-
recipient: [patientReference(disposition.patientId)],
|
|
21
|
-
};
|
|
22
|
-
if (disposition.primaryCaseId) {
|
|
23
|
-
payload.reason = [{ reference: reference('Condition', disposition.primaryCaseId) }];
|
|
24
|
-
}
|
|
25
|
-
return new Communication(payload);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
module.exports = { Communication };
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
const { fhirId } = require('../helpers/fhirHelper');
|
|
2
|
-
const {
|
|
3
|
-
identifier,
|
|
4
|
-
patientReference,
|
|
5
|
-
reference,
|
|
6
|
-
codeableConcept,
|
|
7
|
-
toIso,
|
|
8
|
-
} = require('../helpers/elementHelper');
|
|
9
|
-
const { ROUTING_DISPOSITION_SLUG } = require('../constants/projectionSlugs');
|
|
10
|
-
|
|
11
|
-
class RequestOrchestration {
|
|
12
|
-
constructor(payload) {
|
|
13
|
-
Object.assign(this, payload);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
static fromRoutingDisposition({ disposition }) {
|
|
17
|
-
const action = [{
|
|
18
|
-
title: disposition.primaryAction,
|
|
19
|
-
description: disposition.primaryReason,
|
|
20
|
-
code: [codeableConcept(disposition.primaryAction)],
|
|
21
|
-
}];
|
|
22
|
-
for (const secondary of disposition.secondaryActions) {
|
|
23
|
-
action.push({
|
|
24
|
-
title: secondary.action,
|
|
25
|
-
description: `compose_as: ${secondary.composeAs}`,
|
|
26
|
-
code: [codeableConcept(secondary.action)],
|
|
27
|
-
});
|
|
28
|
-
}
|
|
29
|
-
const payload = {
|
|
30
|
-
resourceType: 'RequestOrchestration',
|
|
31
|
-
id: fhirId(disposition.dispositionId),
|
|
32
|
-
identifier: [identifier(ROUTING_DISPOSITION_SLUG, disposition.dispositionId)],
|
|
33
|
-
status: 'active',
|
|
34
|
-
intent: 'plan',
|
|
35
|
-
subject: patientReference(disposition.patientId),
|
|
36
|
-
authoredOn: toIso(disposition.authoredAt),
|
|
37
|
-
action,
|
|
38
|
-
};
|
|
39
|
-
if (disposition.primaryCaseId) {
|
|
40
|
-
payload.reason = [{ reference: reference('Condition', disposition.primaryCaseId) }];
|
|
41
|
-
}
|
|
42
|
-
return new RequestOrchestration(payload);
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
module.exports = { RequestOrchestration };
|