@peopl-health/nexus 5.13.0-dev.1142 → 5.13.0-dev.1145

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.
@@ -1,18 +1,36 @@
1
- const { fhirId } = require('../helpers/fhirHelper');
2
- const { Procedure } = require('../resources/Procedure');
3
- const { Communication } = require('../resources/Communication');
4
- const { Task } = require('../resources/Task');
5
- const { CommunicationRequest } = require('../resources/CommunicationRequest');
6
- const { Provenance } = require('../resources/Provenance');
7
-
8
- const PROJECTOR_NAME = 'intervention';
1
+ const { ZodError } = require('zod');
2
+
3
+ const { getDeviceId } = require('../config/fhirConfig');
4
+ const { fhirId, codeSystemUrl } = require('../helpers/fhirHelper');
5
+ const { identifier, patientReference, reference, recipientReference, codeableConcept, extension, notes, toIso } = require('../helpers/elementHelper');
6
+ const { baseResource, provenance } = require('../helpers/resourceHelper');
7
+ const { identifierValue, refToId, extValues } = require('../helpers/fhirReadHelper');
8
+ const { INTERVENTION_SLUG, INTERVENTION_KIND_SLUG, INTERVENTION_OUTCOME_SLUG } = require('../constants/projectionSlugs');
9
+ const { Intervention } = require('../../shared/dtos/Intervention');
10
+ const { logger } = require('../../utils/logger');
11
+
9
12
  const ACTIVITY_TEXT = 'intervention-record';
10
13
  const PROCEDURE_KINDS = new Set(['otc', 'self_care', 'other']);
11
14
 
12
- function projectIntervention(intervention) {
13
- const primary = primaryResource(intervention);
15
+ const EXT = {
16
+ RATIONALE: 'intervention-rationale',
17
+ OUTCOME_NOTES: 'intervention-outcome-notes',
18
+ OUTCOME: 'intervention-outcome',
19
+ RECIPIENT_KIND: 'intervention-recipient-kind',
20
+ RECIPIENT_REF: 'intervention-recipient-ref',
21
+ };
22
+
23
+ const RESOURCE_TYPES = ['Procedure', 'Communication', 'Task', 'CommunicationRequest'];
14
24
 
15
- return [primary, Provenance.fromSymptomCase({
25
+ const interventionReason = (caseId) => [{ reference: reference('Condition', caseId) }];
26
+ const withNote = (payload, intervention) => {
27
+ const note = notes(intervention.rationale, intervention.outcomeNotes);
28
+ return note ? { ...payload, note } : payload;
29
+ };
30
+
31
+ function toFhir(intervention) {
32
+ const primary = primaryResource(intervention);
33
+ return [primary, provenance({
16
34
  provenanceId: `${intervention.interventionId}-prov`,
17
35
  targetRefs: [{ reference: `${primary.resourceType}/${fhirId(intervention.interventionId)}` }],
18
36
  recordedAt: intervention.deliveredAt,
@@ -21,22 +39,206 @@ function projectIntervention(intervention) {
21
39
  }
22
40
 
23
41
  function primaryResource(intervention) {
24
- if (PROCEDURE_KINDS.has(intervention.kind)) {
25
- return Procedure.fromIntervention({ intervention });
42
+ if (PROCEDURE_KINDS.has(intervention.kind)) return procedureResource(intervention);
43
+ if (intervention.kind === 'education') return communicationResource(intervention);
44
+ if (intervention.kind === 'team_escalation') return taskResource(intervention);
45
+ if (intervention.kind === 'follow_up_check') return communicationRequestResource(intervention);
46
+ throw new Error(`unknown intervention variant: ${intervention.kind}`);
47
+ }
48
+
49
+ function procedureResource(intervention) {
50
+ return withNote({
51
+ ...baseResource('Procedure', intervention.interventionId, INTERVENTION_SLUG, intervention.patientId),
52
+ status: intervention.outcome === 'pending' ? 'in-progress' : 'completed',
53
+ code: codeableConcept(intervention.interventionLabel),
54
+ reason: interventionReason(intervention.caseId),
55
+ occurrenceDateTime: toIso(intervention.deliveredAt),
56
+ outcome: codeableConcept(intervention.outcome, { code: intervention.outcome, system: codeSystemUrl(INTERVENTION_OUTCOME_SLUG) }),
57
+ category: [codeableConcept(intervention.kind, { code: intervention.kind, system: codeSystemUrl(INTERVENTION_KIND_SLUG) })],
58
+ extension: interventionExtensions(intervention, { withRecipient: true }),
59
+ }, intervention);
60
+ }
61
+
62
+ function communicationResource(intervention) {
63
+ return withNote({
64
+ ...baseResource('Communication', intervention.interventionId, INTERVENTION_SLUG, intervention.patientId),
65
+ status: 'completed',
66
+ category: [codeableConcept(intervention.kind, { code: intervention.kind, system: codeSystemUrl(INTERVENTION_KIND_SLUG) })],
67
+ reason: interventionReason(intervention.caseId),
68
+ sent: toIso(intervention.deliveredAt),
69
+ payload: [{ contentCodeableConcept: codeableConcept(intervention.interventionLabel) }],
70
+ recipient: [recipientReference({
71
+ patientId: intervention.patientId,
72
+ recipientKind: intervention.recipientKind,
73
+ recipientRefId: intervention.recipientRefId,
74
+ })],
75
+ extension: interventionExtensions(intervention),
76
+ }, intervention);
77
+ }
78
+
79
+ function taskResource(intervention) {
80
+ const payload = withNote({
81
+ resourceType: 'Task',
82
+ id: fhirId(intervention.interventionId),
83
+ identifier: [identifier(INTERVENTION_SLUG, intervention.interventionId)],
84
+ status: intervention.outcome === 'pending' ? 'requested' : 'completed',
85
+ intent: 'order',
86
+ code: codeableConcept(intervention.interventionLabel, {
87
+ code: intervention.kind,
88
+ system: codeSystemUrl(INTERVENTION_KIND_SLUG),
89
+ display: intervention.interventionLabel,
90
+ }),
91
+ authoredOn: toIso(intervention.deliveredAt),
92
+ for: patientReference(intervention.patientId),
93
+ requester: reference('Device', getDeviceId()),
94
+ reason: interventionReason(intervention.caseId),
95
+ extension: interventionExtensions(intervention, { withRecipient: true }),
96
+ }, intervention);
97
+ if (intervention.outcome !== 'pending') {
98
+ const outcomeCc = codeableConcept(intervention.outcome, { code: intervention.outcome, system: codeSystemUrl(INTERVENTION_OUTCOME_SLUG) });
99
+ payload.output = [{ type: codeableConcept(INTERVENTION_OUTCOME_SLUG), valueCodeableConcept: outcomeCc }];
26
100
  }
27
- if (intervention.kind === 'education') {
28
- return Communication.fromIntervention({ intervention });
101
+ return payload;
102
+ }
103
+
104
+ function communicationRequestResource(intervention) {
105
+ return withNote({
106
+ ...baseResource('CommunicationRequest', intervention.interventionId, INTERVENTION_SLUG, intervention.patientId),
107
+ status: intervention.outcome === 'pending' ? 'active' : 'completed',
108
+ intent: 'plan',
109
+ category: [codeableConcept(intervention.kind, { code: intervention.kind, system: codeSystemUrl(INTERVENTION_KIND_SLUG) })],
110
+ reason: interventionReason(intervention.caseId),
111
+ payload: [{ contentCodeableConcept: codeableConcept(intervention.interventionLabel) }],
112
+ recipient: [recipientReference({
113
+ patientId: intervention.patientId,
114
+ recipientKind: intervention.recipientKind,
115
+ recipientRefId: intervention.recipientRefId,
116
+ })],
117
+ occurrenceDateTime: toIso(intervention.deliveredAt),
118
+ authoredOn: toIso(intervention.deliveredAt),
119
+ extension: interventionExtensions(intervention),
120
+ }, intervention);
121
+ }
122
+
123
+ function interventionExtensions(intervention, { withRecipient = false } = {}) {
124
+ const out = [];
125
+ if (intervention.rationale) out.push(extension(EXT.RATIONALE, { valueString: intervention.rationale }));
126
+ if (intervention.outcomeNotes) out.push(extension(EXT.OUTCOME_NOTES, { valueString: intervention.outcomeNotes }));
127
+ if (intervention.outcome) out.push(extension(EXT.OUTCOME, { valueString: intervention.outcome }));
128
+ if (withRecipient && intervention.recipientKind === 'related_person' && intervention.recipientRefId) {
129
+ out.push(extension(EXT.RECIPIENT_KIND, { valueString: 'related_person' }));
130
+ out.push(extension(EXT.RECIPIENT_REF, { valueString: intervention.recipientRefId }));
29
131
  }
30
- if (intervention.kind === 'team_escalation') {
31
- return Task.fromIntervention({ intervention });
132
+ return out;
133
+ }
134
+
135
+ function fromFhir(resources, { patientId }) {
136
+ return resources
137
+ .filter((r) => RESOURCE_TYPES.includes(r.resourceType) && identifierValue(r, INTERVENTION_SLUG))
138
+ .map((resource) => reconstruct(resource, patientId))
139
+ .filter(Boolean);
140
+ }
141
+
142
+ function reconstruct(resource, patientId) {
143
+ const reasonRef = resource.reason && resource.reason[0] && resource.reason[0].reference;
144
+ const base = {
145
+ interventionId: identifierValue(resource, INTERVENTION_SLUG),
146
+ patientId,
147
+ caseId: refToId(reasonRef && reasonRef.reference),
148
+ rationale: extValues(resource, EXT.RATIONALE)[0] ?? null,
149
+ outcomeNotes: extValues(resource, EXT.OUTCOME_NOTES)[0] ?? null,
150
+ outcomeExt: extValues(resource, EXT.OUTCOME)[0] ?? null,
151
+ recipientKind: 'patient',
152
+ recipientRefId: null,
153
+ };
154
+ let reconstructed;
155
+ if (resource.resourceType === 'Procedure') reconstructed = reconstructProcedure(resource, base);
156
+ else if (resource.resourceType === 'Communication') reconstructed = reconstructCommunication(resource, base);
157
+ else if (resource.resourceType === 'Task') reconstructed = reconstructTask(resource, base);
158
+ else reconstructed = reconstructCommunicationRequest(resource, base);
159
+ try {
160
+ return new Intervention(reconstructed);
161
+ } catch (error) {
162
+ if (error instanceof ZodError) {
163
+ logger.warn({ interventionId: reconstructed.interventionId }, 'skipping malformed intervention resource');
164
+ return null;
165
+ }
166
+ throw error;
32
167
  }
33
- if (intervention.kind === 'follow_up_check') {
34
- return CommunicationRequest.fromIntervention({ intervention });
168
+ }
169
+
170
+ function reconstructProcedure(resource, base) {
171
+ const { outcomeExt, ...dto } = base;
172
+ return {
173
+ ...dto,
174
+ kind: categoryCode(resource),
175
+ interventionLabel: (resource.code && resource.code.text) || null,
176
+ deliveredAt: resource.occurrenceDateTime,
177
+ outcome: outcomeExt ?? codeableConceptCode(resource.outcome),
178
+ ...recipientFromExtension(resource),
179
+ };
180
+ }
181
+
182
+ function reconstructCommunication(resource, base) {
183
+ const { outcomeExt, ...dto } = base;
184
+ const content = resource.payload && resource.payload[0] && resource.payload[0].contentCodeableConcept;
185
+ const outcome = outcomeExt ?? undefined;
186
+ return {
187
+ ...dto,
188
+ kind: categoryCode(resource),
189
+ interventionLabel: (content && content.text) || null,
190
+ deliveredAt: resource.sent,
191
+ ...(outcome ? { outcome } : {}),
192
+ ...recipientFromReference(resource),
193
+ };
194
+ }
195
+
196
+ function reconstructTask(resource, base) {
197
+ const { outcomeExt, ...dto } = base;
198
+ const output = resource.output && resource.output[0];
199
+ return {
200
+ ...dto,
201
+ kind: codingCode(resource.code),
202
+ interventionLabel: (resource.code && resource.code.text) || null,
203
+ deliveredAt: resource.authoredOn,
204
+ outcome: outcomeExt ?? (output ? codeableConceptCode(output.valueCodeableConcept) : 'pending'),
205
+ ...recipientFromExtension(resource),
206
+ };
207
+ }
208
+
209
+ function reconstructCommunicationRequest(resource, base) {
210
+ const { outcomeExt, ...dto } = base;
211
+ const content = resource.payload && resource.payload[0] && resource.payload[0].contentCodeableConcept;
212
+ return {
213
+ ...dto,
214
+ kind: categoryCode(resource),
215
+ interventionLabel: (content && content.text) || null,
216
+ deliveredAt: resource.occurrenceDateTime,
217
+ outcome: outcomeExt ?? (resource.status === 'active' ? 'pending' : 'unknown'),
218
+ ...recipientFromReference(resource),
219
+ };
220
+ }
221
+
222
+ function recipientFromExtension(resource) {
223
+ const kind = extValues(resource, EXT.RECIPIENT_KIND)[0];
224
+ const refId = extValues(resource, EXT.RECIPIENT_REF)[0];
225
+ if (kind === 'related_person' && refId) return { recipientKind: 'related_person', recipientRefId: refId };
226
+ return { recipientKind: 'patient', recipientRefId: null };
227
+ }
228
+
229
+ function recipientFromReference(resource) {
230
+ const recipientRef = resource.recipient && resource.recipient[0] && resource.recipient[0].reference;
231
+ if (recipientRef && recipientRef.startsWith('RelatedPerson/')) {
232
+ return { recipientKind: 'related_person', recipientRefId: refToId(recipientRef) };
35
233
  }
36
- throw new Error(`unknown intervention variant: ${intervention.kind}`);
234
+ return { recipientKind: 'patient', recipientRefId: null };
37
235
  }
38
236
 
237
+ const codingCode = (concept) => (concept && concept.coding && concept.coding[0] && concept.coding[0].code) || null;
238
+ const categoryCode = (resource) => codingCode(resource.category && resource.category[0]);
239
+ const codeableConceptCode = (concept) => codingCode(concept) || (concept && concept.text) || 'pending';
240
+
39
241
  module.exports = {
40
- projectIntervention,
41
- PROJECTOR_NAME,
242
+ toFhir,
243
+ fromFhir,
42
244
  };
@@ -6,7 +6,6 @@ const { projectSymptomCase, PROJECTOR_NAME: SYMPTOM_CASE_PROJECTOR_NAME } = requ
6
6
  const { projectPatientRisk, PROJECTOR_NAME: RISK_PROJECTOR_NAME } = require('./riskProjection');
7
7
  const { projectSafetyGate, PROJECTOR_NAME: SAFETY_GATE_PROJECTOR_NAME } = require('./safetyGateProjection');
8
8
  const { projectRecommendationPlan, PROJECTOR_NAME: RECOMMENDATION_PLAN_PROJECTOR_NAME } = require('./recommendationPlanProjection');
9
- const { projectIntervention, PROJECTOR_NAME: INTERVENTION_PROJECTOR_NAME } = require('./interventionProjection');
10
9
  const { projectCluster, PROJECTOR_NAME: CLUSTER_PROJECTOR_NAME } = require('./clusterProjection');
11
10
  const { projectRoutingDecision, PROJECTOR_NAME: ROUTING_PROJECTOR_NAME } = require('./routingProjection');
12
11
  const { projectPalliativeAssessment, PROJECTOR_NAME: PALLIATIVE_PROJECTOR_NAME } = require('./palliativeProjection');
@@ -20,7 +19,6 @@ const PROJECTORS = [
20
19
  { name: RISK_PROJECTOR_NAME, project: projectPatientRisk },
21
20
  { name: SAFETY_GATE_PROJECTOR_NAME, project: projectSafetyGate },
22
21
  { name: RECOMMENDATION_PLAN_PROJECTOR_NAME, project: projectRecommendationPlan },
23
- { name: INTERVENTION_PROJECTOR_NAME, project: projectIntervention },
24
22
  { name: CLUSTER_PROJECTOR_NAME, project: projectCluster },
25
23
  { name: ROUTING_PROJECTOR_NAME, project: projectRoutingDecision },
26
24
  { name: PALLIATIVE_PROJECTOR_NAME, project: projectPalliativeAssessment },
@@ -1,48 +1,12 @@
1
- const { fhirId, codeSystemUrl } = require('../helpers/fhirHelper');
2
- const {
3
- identifier,
4
- reference,
5
- patientReference,
6
- recipientReference,
7
- codeableConcept,
8
- notes,
9
- toIso,
10
- } = require('../helpers/elementHelper');
11
- const {
12
- INTERVENTION_SLUG,
13
- INTERVENTION_KIND_SLUG,
14
- ROUTING_DISPOSITION_SLUG,
15
- } = require('../constants/projectionSlugs');
16
- const { interventionExtensions } = require('./interventionExtensions');
1
+ const { fhirId } = require('../helpers/fhirHelper');
2
+ const { identifier, reference, patientReference, codeableConcept, toIso } = require('../helpers/elementHelper');
3
+ const { ROUTING_DISPOSITION_SLUG } = require('../constants/projectionSlugs');
17
4
 
18
5
  class Communication {
19
6
  constructor(payload) {
20
7
  Object.assign(this, payload);
21
8
  }
22
9
 
23
- static fromIntervention({ intervention }) {
24
- const payload = {
25
- resourceType: 'Communication',
26
- id: fhirId(intervention.interventionId),
27
- identifier: [identifier(INTERVENTION_SLUG, intervention.interventionId)],
28
- status: 'completed',
29
- category: [codeableConcept(intervention.kind, { code: intervention.kind, system: codeSystemUrl(INTERVENTION_KIND_SLUG) })],
30
- subject: patientReference(intervention.patientId),
31
- reason: [{ reference: reference('Condition', intervention.caseId) }],
32
- sent: toIso(intervention.deliveredAt),
33
- payload: [{ contentCodeableConcept: codeableConcept(intervention.interventionLabel) }],
34
- recipient: [recipientReference({
35
- patientId: intervention.patientId,
36
- recipientKind: intervention.recipientKind,
37
- recipientRefId: intervention.recipientRefId,
38
- })],
39
- extension: interventionExtensions(intervention),
40
- };
41
- const note = notes(intervention.rationale, intervention.outcomeNotes);
42
- if (note) payload.note = note;
43
- return new Communication(payload);
44
- }
45
-
46
10
  static fromRoutingDisposition({ disposition }) {
47
11
  const payload = {
48
12
  resourceType: 'Communication',
@@ -15,12 +15,9 @@ const {
15
15
  REMINDER_SLUG,
16
16
  REMINDER_KIND_SLUG,
17
17
  COMMUNICATION_CHANNEL_SLUG,
18
- INTERVENTION_SLUG,
19
- INTERVENTION_KIND_SLUG,
20
18
  ROUTING_DISPOSITION_SLUG,
21
19
  } = require('../constants/projectionSlugs');
22
20
  const { SEVERITY_TO_PRIORITY } = require('../constants/severityPriority');
23
- const { interventionExtensions } = require('./interventionExtensions');
24
21
 
25
22
  const NOTIFY_STATUS_TO_CR_STATUS = {
26
23
  queued: 'active',
@@ -91,31 +88,6 @@ class CommunicationRequest {
91
88
  return new CommunicationRequest(payload);
92
89
  }
93
90
 
94
- static fromIntervention({ intervention }) {
95
- const payload = {
96
- resourceType: 'CommunicationRequest',
97
- id: fhirId(intervention.interventionId),
98
- identifier: [identifier(INTERVENTION_SLUG, intervention.interventionId)],
99
- status: intervention.outcome === 'pending' ? 'active' : 'completed',
100
- intent: 'plan',
101
- category: [codeableConcept(intervention.kind, { code: intervention.kind, system: codeSystemUrl(INTERVENTION_KIND_SLUG) })],
102
- subject: patientReference(intervention.patientId),
103
- reason: [{ reference: reference('Condition', intervention.caseId) }],
104
- payload: [{ contentCodeableConcept: codeableConcept(intervention.interventionLabel) }],
105
- recipient: [recipientReference({
106
- patientId: intervention.patientId,
107
- recipientKind: intervention.recipientKind,
108
- recipientRefId: intervention.recipientRefId,
109
- })],
110
- occurrenceDateTime: toIso(intervention.deliveredAt),
111
- authoredOn: toIso(intervention.deliveredAt),
112
- extension: interventionExtensions(intervention),
113
- };
114
- const note = notes(intervention.rationale, intervention.outcomeNotes);
115
- if (note) payload.note = note;
116
- return new CommunicationRequest(payload);
117
- }
118
-
119
91
  static fromRoutingDisposition({ disposition }) {
120
92
  const payload = {
121
93
  resourceType: 'CommunicationRequest',
@@ -1,43 +1,11 @@
1
- const { fhirId, codeSystemUrl } = require('../helpers/fhirHelper');
2
- const {
3
- identifier,
4
- patientReference,
5
- reference,
6
- codeableConcept,
7
- notes,
8
- toIso,
9
- } = require('../helpers/elementHelper');
10
- const {
11
- INTERVENTION_SLUG,
12
- INTERVENTION_KIND_SLUG,
13
- INTERVENTION_OUTCOME_SLUG,
14
- } = require('../constants/projectionSlugs');
15
- const { interventionExtensions } = require('./interventionExtensions');
1
+ const { fhirId } = require('../helpers/fhirHelper');
2
+ const { identifier, patientReference } = require('../helpers/elementHelper');
16
3
 
17
4
  class Procedure {
18
5
  constructor(payload) {
19
6
  Object.assign(this, payload);
20
7
  }
21
8
 
22
- static fromIntervention({ intervention }) {
23
- const payload = {
24
- resourceType: 'Procedure',
25
- id: fhirId(intervention.interventionId),
26
- identifier: [identifier(INTERVENTION_SLUG, intervention.interventionId)],
27
- status: intervention.outcome === 'pending' ? 'in-progress' : 'completed',
28
- code: codeableConcept(intervention.interventionLabel),
29
- subject: patientReference(intervention.patientId),
30
- reason: [{ reference: reference('Condition', intervention.caseId) }],
31
- occurrenceDateTime: toIso(intervention.deliveredAt),
32
- outcome: codeableConcept(intervention.outcome, { code: intervention.outcome, system: codeSystemUrl(INTERVENTION_OUTCOME_SLUG) }),
33
- category: [codeableConcept(intervention.kind, { code: intervention.kind, system: codeSystemUrl(INTERVENTION_KIND_SLUG) })],
34
- extension: interventionExtensions(intervention, { withRecipient: true }),
35
- };
36
- const note = notes(intervention.rationale, intervention.outcomeNotes);
37
- if (note) payload.note = note;
38
- return new Procedure(payload);
39
- }
40
-
41
9
  static fromMentionProjection({ fact, provenance, code, extensions, slug }) {
42
10
  const assertion = fact.assertion;
43
11
  const notes = provenance.verbatimQuotes && provenance.verbatimQuotes.length
@@ -12,9 +12,6 @@ const {
12
12
  const {
13
13
  ESCALATION_SLUG,
14
14
  ESCALATION_KIND_SLUG,
15
- INTERVENTION_SLUG,
16
- INTERVENTION_KIND_SLUG,
17
- INTERVENTION_OUTCOME_SLUG,
18
15
  ROUTING_DISPOSITION_SLUG,
19
16
  RECOMMENDATION_REC_SLUG,
20
17
  PALLIATIVE_ESCALATION_SLUG,
@@ -22,7 +19,6 @@ const {
22
19
  } = require('../constants/projectionSlugs');
23
20
  const { PALLIATIVE_EXT } = require('../constants/extensionSlugs');
24
21
  const { SEVERITY_TO_PRIORITY } = require('../constants/severityPriority');
25
- const { interventionExtensions } = require('./interventionExtensions');
26
22
 
27
23
  const TICKET_STATUS_TO_TASK_STATUS = {
28
24
  queued: 'requested',
@@ -60,33 +56,6 @@ class Task {
60
56
  return new Task(payload);
61
57
  }
62
58
 
63
- static fromIntervention({ intervention }) {
64
- const payload = {
65
- resourceType: 'Task',
66
- id: fhirId(intervention.interventionId),
67
- identifier: [identifier(INTERVENTION_SLUG, intervention.interventionId)],
68
- status: intervention.outcome === 'pending' ? 'requested' : 'completed',
69
- intent: 'order',
70
- code: codeableConcept(intervention.interventionLabel, {
71
- code: intervention.kind,
72
- system: codeSystemUrl(INTERVENTION_KIND_SLUG),
73
- display: intervention.interventionLabel,
74
- }),
75
- authoredOn: toIso(intervention.deliveredAt),
76
- for: patientReference(intervention.patientId),
77
- requester: reference('Device', getDeviceId()),
78
- reason: [{ reference: reference('Condition', intervention.caseId) }],
79
- extension: interventionExtensions(intervention, { withRecipient: true }),
80
- };
81
- if (intervention.outcome !== 'pending') {
82
- const outcomeCc = codeableConcept(intervention.outcome, { code: intervention.outcome, system: codeSystemUrl(INTERVENTION_OUTCOME_SLUG) });
83
- payload.output = [{ type: codeableConcept(INTERVENTION_OUTCOME_SLUG), valueCodeableConcept: outcomeCc }];
84
- }
85
- const note = notes(intervention.rationale, intervention.outcomeNotes);
86
- if (note) payload.note = note;
87
- return new Task(payload);
88
- }
89
-
90
59
  static fromRecommendationPlan({ plan, element, taskId }) {
91
60
  const { recommendation } = element;
92
61
  const { patient } = recommendation;
@@ -1,148 +1,24 @@
1
- const { ZodError } = require('zod');
2
-
3
- const { fhirId, identifierSystem } = require('../helpers/fhirHelper');
4
- const { identifierValue, refToId, extValues } = require('../helpers/fhirReadHelper');
5
- const { INTERVENTION_SLUG } = require('../constants/projectionSlugs');
6
- const {
7
- RATIONALE_EXT,
8
- OUTCOME_NOTES_EXT,
9
- OUTCOME_EXT,
10
- RECIPIENT_KIND_EXT,
11
- RECIPIENT_REF_EXT,
12
- } = require('../resources/interventionExtensions');
13
- const { PROJECTOR_NAME } = require('../projections/interventionProjection');
1
+ const { fhirId } = require('../helpers/fhirHelper');
14
2
  const { getFhirStore } = require('../stores/fhirStore');
15
- const { Intervention } = require('../../shared/dtos/Intervention');
16
- const { logger } = require('../../utils/logger');
17
- const { project, storeResources } = require('./fhirService');
18
-
19
- const VARIANT_TYPES = new Set(['Procedure', 'Communication', 'Task', 'CommunicationRequest']);
3
+ const { toFhir, fromFhir } = require('../projections/interventionProjection');
4
+ const { storeResources } = require('./fhirService');
20
5
 
21
6
  async function storeIntervention({ patientId, intervention }) {
22
7
  if (intervention.patientId !== patientId) {
23
8
  throw new Error(`storeIntervention patientId mismatch: expected ${patientId}`);
24
9
  }
25
- const bundle = project([{ name: PROJECTOR_NAME, aggregate: intervention }]);
26
- const resources = bundle.entry.map((entry) => entry.resource);
27
- const { stored } = await storeResources(resources);
10
+ const { stored } = await storeResources(toFhir(intervention));
28
11
  return { count: stored.length, ids: stored };
29
12
  }
30
13
 
31
14
  async function readInterventions({ patientId, caseId = null }) {
32
15
  const store = getFhirStore();
33
16
  const patient = `Patient/${fhirId(patientId)}`;
34
- const interventionSystem = identifierSystem(INTERVENTION_SLUG);
35
- const resources = (await store.find({ patient }))
36
- .filter((resource) => VARIANT_TYPES.has(resource.resourceType)
37
- && (resource.identifier || []).some((id) => id.system === interventionSystem));
38
- const interventions = [];
39
- for (const resource of resources) {
40
- if (!identifierValue(resource, INTERVENTION_SLUG)) continue;
41
- const reconstructed = reconstructIntervention(resource, patientId);
42
- if (!reconstructed.caseId) continue;
43
- if (caseId && reconstructed.caseId !== caseId) continue;
44
- try {
45
- interventions.push(new Intervention(reconstructed));
46
- } catch (error) {
47
- if (error instanceof ZodError) {
48
- logger.warn({ interventionId: reconstructed.interventionId }, 'skipping malformed intervention resource');
49
- continue;
50
- }
51
- throw error;
52
- }
53
- }
54
- return interventions;
55
- }
56
-
57
- function reconstructIntervention(resource, patientId) {
58
- const reasonRef = resource.reason && resource.reason[0] && resource.reason[0].reference;
59
- const base = {
60
- interventionId: identifierValue(resource, INTERVENTION_SLUG),
61
- patientId,
62
- caseId: refToId(reasonRef && reasonRef.reference),
63
- rationale: extValues(resource, RATIONALE_EXT)[0] ?? null,
64
- outcomeNotes: extValues(resource, OUTCOME_NOTES_EXT)[0] ?? null,
65
- outcomeExt: extValues(resource, OUTCOME_EXT)[0] ?? null,
66
- recipientKind: 'patient',
67
- recipientRefId: null,
68
- };
69
- if (resource.resourceType === 'Procedure') return reconstructProcedure(resource, base);
70
- if (resource.resourceType === 'Communication') return reconstructCommunication(resource, base);
71
- if (resource.resourceType === 'Task') return reconstructTask(resource, base);
72
- return reconstructCommunicationRequest(resource, base);
17
+ const resources = await store.find({ patient });
18
+ return fromFhir(resources, { patientId })
19
+ .filter((intervention) => !caseId || intervention.caseId === caseId);
73
20
  }
74
21
 
75
- function reconstructProcedure(resource, base) {
76
- const { outcomeExt, ...dto } = base;
77
- return {
78
- ...dto,
79
- kind: categoryCode(resource),
80
- interventionLabel: (resource.code && resource.code.text) || null,
81
- deliveredAt: resource.occurrenceDateTime,
82
- outcome: outcomeExt ?? codeableConceptCode(resource.outcome),
83
- ...recipientFromExtension(resource),
84
- };
85
- }
86
-
87
- function reconstructCommunication(resource, base) {
88
- const { outcomeExt, ...dto } = base;
89
- const content = resource.payload && resource.payload[0] && resource.payload[0].contentCodeableConcept;
90
- const outcome = outcomeExt ?? undefined;
91
- return {
92
- ...dto,
93
- kind: categoryCode(resource),
94
- interventionLabel: (content && content.text) || null,
95
- deliveredAt: resource.sent,
96
- ...(outcome ? { outcome } : {}),
97
- ...recipientFromReference(resource),
98
- };
99
- }
100
-
101
- function reconstructTask(resource, base) {
102
- const { outcomeExt, ...dto } = base;
103
- const output = resource.output && resource.output[0];
104
- return {
105
- ...dto,
106
- kind: codingCode(resource.code),
107
- interventionLabel: (resource.code && resource.code.text) || null,
108
- deliveredAt: resource.authoredOn,
109
- outcome: outcomeExt ?? (output ? codeableConceptCode(output.valueCodeableConcept) : 'pending'),
110
- ...recipientFromExtension(resource),
111
- };
112
- }
113
-
114
- function reconstructCommunicationRequest(resource, base) {
115
- const { outcomeExt, ...dto } = base;
116
- const content = resource.payload && resource.payload[0] && resource.payload[0].contentCodeableConcept;
117
- return {
118
- ...dto,
119
- kind: categoryCode(resource),
120
- interventionLabel: (content && content.text) || null,
121
- deliveredAt: resource.occurrenceDateTime,
122
- outcome: outcomeExt ?? (resource.status === 'active' ? 'pending' : 'unknown'),
123
- ...recipientFromReference(resource),
124
- };
125
- }
126
-
127
- function recipientFromExtension(resource) {
128
- const kind = extValues(resource, RECIPIENT_KIND_EXT)[0];
129
- const refId = extValues(resource, RECIPIENT_REF_EXT)[0];
130
- if (kind === 'related_person' && refId) return { recipientKind: 'related_person', recipientRefId: refId };
131
- return { recipientKind: 'patient', recipientRefId: null };
132
- }
133
-
134
- function recipientFromReference(resource) {
135
- const recipientRef = resource.recipient && resource.recipient[0] && resource.recipient[0].reference;
136
- if (recipientRef && recipientRef.startsWith('RelatedPerson/')) {
137
- return { recipientKind: 'related_person', recipientRefId: refToId(recipientRef) };
138
- }
139
- return { recipientKind: 'patient', recipientRefId: null };
140
- }
141
-
142
- const codingCode = (concept) => (concept && concept.coding && concept.coding[0] && concept.coding[0].code) || null;
143
- const categoryCode = (resource) => codingCode(resource.category && resource.category[0]);
144
- const codeableConceptCode = (concept) => codingCode(concept) || (concept && concept.text) || 'pending';
145
-
146
22
  module.exports = {
147
23
  storeIntervention,
148
24
  readInterventions,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "5.13.0-dev.1142",
3
+ "version": "5.13.0-dev.1145",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",
@@ -1,28 +0,0 @@
1
- const { extension } = require('../helpers/elementHelper');
2
-
3
- const RATIONALE_EXT = 'intervention-rationale';
4
- const OUTCOME_NOTES_EXT = 'intervention-outcome-notes';
5
- const OUTCOME_EXT = 'intervention-outcome';
6
- const RECIPIENT_KIND_EXT = 'intervention-recipient-kind';
7
- const RECIPIENT_REF_EXT = 'intervention-recipient-ref';
8
-
9
- function interventionExtensions(intervention, { withRecipient = false } = {}) {
10
- const out = [];
11
- if (intervention.rationale) out.push(extension(RATIONALE_EXT, { valueString: intervention.rationale }));
12
- if (intervention.outcomeNotes) out.push(extension(OUTCOME_NOTES_EXT, { valueString: intervention.outcomeNotes }));
13
- if (intervention.outcome) out.push(extension(OUTCOME_EXT, { valueString: intervention.outcome }));
14
- if (withRecipient && intervention.recipientKind === 'related_person' && intervention.recipientRefId) {
15
- out.push(extension(RECIPIENT_KIND_EXT, { valueString: 'related_person' }));
16
- out.push(extension(RECIPIENT_REF_EXT, { valueString: intervention.recipientRefId }));
17
- }
18
- return out;
19
- }
20
-
21
- module.exports = {
22
- interventionExtensions,
23
- RATIONALE_EXT,
24
- OUTCOME_NOTES_EXT,
25
- OUTCOME_EXT,
26
- RECIPIENT_KIND_EXT,
27
- RECIPIENT_REF_EXT,
28
- };