@peopl-health/nexus 5.12.0 → 5.12.1

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.
Files changed (30) hide show
  1. package/lib/clinical/AssistantProcessor.js +1 -1
  2. package/lib/clinical/tools/submitRoutingDecisionTool.js +2 -2
  3. package/lib/clinical/tools/updateConditionStatusTool.js +1 -2
  4. package/lib/fhir/helpers/resourceHelper.js +13 -4
  5. package/lib/fhir/index.js +2 -2
  6. package/lib/fhir/models/envelopeModel.js +19 -0
  7. package/lib/fhir/projections/clusterProjection.js +113 -14
  8. package/lib/fhir/projections/contingencyProjection.js +1 -1
  9. package/lib/fhir/projections/interventionProjection.js +225 -23
  10. package/lib/fhir/projections/registerProjectors.js +0 -8
  11. package/lib/fhir/projections/routingProjection.js +276 -40
  12. package/lib/fhir/projections/symptomCaseProjection.js +272 -48
  13. package/lib/fhir/resources/CarePlan.js +1 -18
  14. package/lib/fhir/resources/ClinicalImpression.js +3 -115
  15. package/lib/fhir/resources/CommunicationRequest.js +0 -48
  16. package/lib/fhir/resources/Condition.js +1 -85
  17. package/lib/fhir/resources/Observation.js +1 -31
  18. package/lib/fhir/resources/Procedure.js +2 -34
  19. package/lib/fhir/resources/Provenance.js +1 -33
  20. package/lib/fhir/resources/Task.js +0 -50
  21. package/lib/fhir/services/clusterService.js +12 -60
  22. package/lib/fhir/services/interventionService.js +7 -131
  23. package/lib/fhir/services/routingService.js +18 -130
  24. package/lib/fhir/services/symptomCaseService.js +11 -121
  25. package/lib/fhir/stores/fhirStore.js +3 -1
  26. package/lib/shared/dtos/ManagedSymptom.js +4 -0
  27. package/package.json +1 -1
  28. package/lib/fhir/resources/Communication.js +0 -65
  29. package/lib/fhir/resources/RequestOrchestration.js +0 -46
  30. package/lib/fhir/resources/interventionExtensions.js +0 -28
@@ -43,7 +43,7 @@ class AssistantProcessor {
43
43
  });
44
44
  }
45
45
 
46
- if (thread.isInCohort && !runOptions.toolRuntimeContext && runOptions.runId && thread.code) {
46
+ if (!runOptions.toolRuntimeContext && runOptions.runId && thread.code) {
47
47
  try {
48
48
  runOptions.toolRuntimeContext = new ToolRuntimeContext({
49
49
  patientCode: thread.code,
@@ -1,6 +1,6 @@
1
1
  const crypto = require('node:crypto');
2
2
 
3
- const { storeRoutingDecision, readRoutingByTurn } = require('../../fhir');
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 readRoutingByTurn({ patientId, turnId });
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
  }
@@ -1,6 +1,5 @@
1
1
  const { readSymptomCases, storeSymptomCase, readContingencies, storeContingency, readClusters, storeCluster } = require('../../fhir');
2
- const { ManagedSymptom, OPEN_STATUSES } = require('../../shared/dtos/ManagedSymptom');
3
- const { RESOLUTION_CLOSE_REASONS } = require('../../fhir/resources/Condition');
2
+ const { ManagedSymptom, OPEN_STATUSES, RESOLUTION_CLOSE_REASONS } = require('../../shared/dtos/ManagedSymptom');
4
3
  const { ContingencySafetyNet, ACTIVE_CARE_PLAN_STATUSES } = require('../../shared/dtos/ContingencySafetyNet');
5
4
  const { ClusterImpression } = require('../../shared/dtos/ClusterImpression');
6
5
  const { ClusterHistoryRecord } = require('../../shared/dtos/ClusterHistoryRecord');
@@ -1,4 +1,4 @@
1
- const { getDeviceId, getOrganizationId } = require('../config/fhirConfig');
1
+ const { getDeviceId, getOrganizationId, getSystems } = require('../config/fhirConfig');
2
2
  const { fhirId } = require('./fhirHelper');
3
3
  const { identifier, patientReference, reference, codeableConcept, toIso } = require('./elementHelper');
4
4
 
@@ -11,18 +11,27 @@ 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, activity, 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: [{ who: reference('Device', getDeviceId()), onBehalfOf: reference('Organization', getOrganizationId()) }],
21
- activity: codeableConcept(activityText),
20
+ agent: [{
21
+ who: reference('Device', agentDevice || getDeviceId()),
22
+ onBehalfOf: reference('Organization', onBehalfOf || getOrganizationId()),
23
+ }],
24
+ activity,
22
25
  };
23
26
  }
24
27
 
28
+ function observationCategory(code) {
29
+ const label = code.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
30
+ return codeableConcept(label, { code, system: getSystems().observationCategory, display: label });
31
+ }
32
+
25
33
  module.exports = {
26
34
  baseResource,
27
35
  provenance,
36
+ observationCategory,
28
37
  };
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, readRoutingByTurn } = require('./services/routingService');
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
- readRoutingByTurn,
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: {
@@ -23,6 +25,14 @@ const envelopeSchema = new mongoose.Schema(
23
25
 
24
26
  envelopeSchema.index({ 'index.patient': 1, 'index.date': -1 });
25
27
  envelopeSchema.index({ 'index.patient': 1, 'index.symptomTerm': 1, 'index.date': -1 });
28
+ envelopeSchema.index(
29
+ { 'index.patient': 1, 'resource.identifier.system': 1, 'index.date': -1 },
30
+ { partialFilterExpression: { 'meta.deleted': false } },
31
+ );
32
+ envelopeSchema.index(
33
+ { 'index.patient': 1, 'index.turnId': 1, 'index.date': -1 },
34
+ { partialFilterExpression: { 'meta.deleted': false, 'index.turnId': { $type: 'string' } } },
35
+ );
26
36
  envelopeSchema.index({ 'index.reference': 1 }, { partialFilterExpression: { 'index.reference': { $type: 'string' } } });
27
37
  envelopeSchema.index({ 'resource.status': 1 }, { sparse: true });
28
38
 
@@ -63,6 +73,7 @@ envelopeSchema.statics.deriveIndex = function deriveIndex(resource) {
63
73
  date: instantOf(date),
64
74
  reference: resource.resourceType && resource.id ? `${resource.resourceType}/${resource.id}` : null,
65
75
  symptomTerm: symptomTermOf(resource),
76
+ turnId: turnIdOf(resource),
66
77
  };
67
78
  };
68
79
 
@@ -89,6 +100,14 @@ function symptomTermOf(resource) {
89
100
  return null;
90
101
  }
91
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
+
92
111
  function businessIdentifier(resource) {
93
112
  for (const identifier of resource.identifier || []) {
94
113
  const system = (identifier || {}).system;
@@ -1,24 +1,123 @@
1
- const { ClinicalImpression } = require('../resources/ClinicalImpression');
2
- const { Provenance } = require('../resources/Provenance');
1
+ const { ZodError } = require('zod');
3
2
 
4
- const PROJECTOR_NAME = 'cluster';
3
+ const { codeSystemUrl } = require('../helpers/fhirHelper');
4
+ const { identifier, reference, codeableConcept, extension, toIso } = require('../helpers/elementHelper');
5
+ const { baseResource, provenance } = require('../helpers/resourceHelper');
6
+ const { extMap, identifierValue, refToId } = require('../helpers/fhirReadHelper');
7
+ const { CLUSTER_IMPRESSION_SLUG, CLUSTER_LABEL_SLUG } = require('../constants/projectionSlugs');
8
+ const { CLUSTER_EXT } = require('../constants/extensionSlugs');
9
+ const { ClusterImpression } = require('../../shared/dtos/ClusterImpression');
10
+ const { logger } = require('../../utils/logger');
5
11
 
6
- function projectCluster({ cluster, history = [] }) {
7
- const impression = ClinicalImpression.fromClusterImpression({ cluster });
12
+ const LIST_SEPARATOR = '; ';
13
+ const STATUS_TO_FHIR = { active: 'in-progress', dissolved: 'completed' };
14
+ const ACTIVITY_CODESYSTEM = 'clinical-activity';
15
+ const ACTIVITY_CODES = {
16
+ opened: 'cluster-hypothesis-opened',
17
+ updated: 'cluster-hypothesis-updated',
18
+ dissolved: 'cluster-hypothesis-dissolved',
19
+ };
20
+ const DEFAULT_ACTIVITY_CODE = 'cluster-hypothesis';
21
+
22
+ function toFhir({ cluster, history = [] }) {
8
23
  const records = history.length
9
24
  ? [...history].sort((a, b) => Date.parse(a.recordedAt) - Date.parse(b.recordedAt))
10
25
  : [{ turnId: 'opened', action: 'opened', recordedAt: cluster.openedAt, transitionReason: null }];
11
- const provenances = records.map((record) => Provenance.fromClusterImpression({
12
- clusterId: cluster.clusterId,
13
- turnId: record.turnId,
14
- action: record.action,
26
+ return [clusterResource(cluster), ...records.map((record) => historyProvenance(cluster.clusterId, record))];
27
+ }
28
+
29
+ function clusterResource(cluster) {
30
+ const payload = {
31
+ ...baseResource('ClinicalImpression', cluster.clusterId, CLUSTER_IMPRESSION_SLUG, cluster.patientId),
32
+ status: STATUS_TO_FHIR[cluster.status],
33
+ problem: cluster.memberCaseIds.map((caseId) => reference('Condition', caseId)),
34
+ extension: clusterExtensions(cluster),
35
+ };
36
+ if (cluster.label) payload.identifier.push(identifier(CLUSTER_LABEL_SLUG, cluster.label));
37
+ if (cluster.relationshipModel) payload.summary = cluster.relationshipModel;
38
+ if (cluster.openingReasoning) payload.description = cluster.openingReasoning;
39
+ if (cluster.dissolvedAt) payload.effectivePeriod = { start: toIso(cluster.openedAt), end: toIso(cluster.dissolvedAt) };
40
+ else payload.effectiveDateTime = toIso(cluster.openedAt);
41
+ if (cluster.dissolveCategory) payload.statusReason = codeableConcept(cluster.dissolveCategory);
42
+ return payload;
43
+ }
44
+
45
+ function clusterExtensions(cluster) {
46
+ const out = [
47
+ extension(CLUSTER_EXT.HYPOTHESIS_TYPE, { valueString: cluster.hypothesisType }),
48
+ extension(CLUSTER_EXT.CONFIDENCE, { valueString: cluster.confidence }),
49
+ extension(CLUSTER_EXT.CLINICAL_SIGNIFICANCE, { valueString: cluster.clinicalSignificance }),
50
+ ];
51
+ if (cluster.strengthensIf.length) {
52
+ out.push(extension(CLUSTER_EXT.STRENGTHENS_IF, { valueString: cluster.strengthensIf.join(LIST_SEPARATOR) }));
53
+ }
54
+ if (cluster.weakensIf.length) {
55
+ out.push(extension(CLUSTER_EXT.WEAKENS_IF, { valueString: cluster.weakensIf.join(LIST_SEPARATOR) }));
56
+ }
57
+ out.push(extension(CLUSTER_EXT.STATUS, { valueString: cluster.status }));
58
+ if (cluster.consultationId) out.push(extension(CLUSTER_EXT.CONSULTATION_ID, { valueString: cluster.consultationId }));
59
+ if (cluster.dissolveReason) out.push(extension(CLUSTER_EXT.DISSOLVE_REASON, { valueString: cluster.dissolveReason }));
60
+ return out;
61
+ }
62
+
63
+ function historyProvenance(clusterId, record) {
64
+ const code = ACTIVITY_CODES[record.action] || DEFAULT_ACTIVITY_CODE;
65
+ const payload = provenance({
66
+ provenanceId: `${clusterId}-prov-${record.turnId}-${record.action}`,
67
+ targetRefs: [reference('ClinicalImpression', clusterId)],
15
68
  recordedAt: record.recordedAt,
16
- transitionReason: record.transitionReason,
17
- }));
18
- return [impression, ...provenances];
69
+ activity: codeableConcept(code, { code, system: codeSystemUrl(ACTIVITY_CODESYSTEM) }),
70
+ });
71
+ if (record.transitionReason) {
72
+ payload.extension = [extension('transition-reason', { valueString: record.transitionReason })];
73
+ }
74
+ return payload;
75
+ }
76
+
77
+ function fromFhir(resources, { patientId }) {
78
+ const out = [];
79
+ for (const resource of resources) {
80
+ if (resource.resourceType !== 'ClinicalImpression') continue;
81
+ if (!identifierValue(resource, CLUSTER_IMPRESSION_SLUG)) continue;
82
+ try {
83
+ out.push(new ClusterImpression(reconstruct(resource, patientId)));
84
+ } catch (error) {
85
+ if (error instanceof ZodError) {
86
+ logger.warn({ clusterId: resource.id }, 'skipping malformed cluster ClinicalImpression');
87
+ continue;
88
+ }
89
+ throw error;
90
+ }
91
+ }
92
+ return out;
93
+ }
94
+
95
+ function reconstruct(resource, patientId) {
96
+ const ext = extMap(resource);
97
+ const splitList = (value) => (value ? value.split(LIST_SEPARATOR) : []);
98
+ return {
99
+ schemaVersion: '1',
100
+ clusterId: identifierValue(resource, CLUSTER_IMPRESSION_SLUG),
101
+ patientId,
102
+ label: identifierValue(resource, CLUSTER_LABEL_SLUG) || '',
103
+ hypothesisType: ext[CLUSTER_EXT.HYPOTHESIS_TYPE],
104
+ status: ext[CLUSTER_EXT.STATUS],
105
+ confidence: ext[CLUSTER_EXT.CONFIDENCE],
106
+ clinicalSignificance: ext[CLUSTER_EXT.CLINICAL_SIGNIFICANCE],
107
+ memberCaseIds: (resource.problem || []).map((problemRef) => refToId(problemRef.reference)).filter(Boolean),
108
+ consultationId: ext[CLUSTER_EXT.CONSULTATION_ID] || null,
109
+ relationshipModel: resource.summary || '',
110
+ strengthensIf: splitList(ext[CLUSTER_EXT.STRENGTHENS_IF]),
111
+ weakensIf: splitList(ext[CLUSTER_EXT.WEAKENS_IF]),
112
+ openingReasoning: resource.description || '',
113
+ openedAt: resource.effectiveDateTime || resource.effectivePeriod?.start,
114
+ dissolvedAt: resource.effectivePeriod?.end || null,
115
+ dissolveReason: ext[CLUSTER_EXT.DISSOLVE_REASON] || null,
116
+ dissolveCategory: resource.statusReason?.text || null,
117
+ };
19
118
  }
20
119
 
21
120
  module.exports = {
22
- projectCluster,
23
- PROJECTOR_NAME,
121
+ toFhir,
122
+ fromFhir,
24
123
  };
@@ -60,7 +60,7 @@ function toFhir(contingency) {
60
60
  provenanceId: `${carePlan.planId}-prov`,
61
61
  targetRefs,
62
62
  recordedAt: carePlan.createdAt,
63
- activityText: ACTIVITY_TEXT,
63
+ activity: codeableConcept(ACTIVITY_TEXT),
64
64
  }));
65
65
  return resources;
66
66
  }
@@ -1,42 +1,244 @@
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,
19
- activityText: ACTIVITY_TEXT,
37
+ activity: codeableConcept(ACTIVITY_TEXT),
20
38
  })];
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
  };
@@ -2,13 +2,9 @@ const { registerProjector } = require('../services/fhirService');
2
2
  const { projectClinicalMention, PROJECTOR_NAME: MENTION_PROJECTOR_NAME } = require('./clinicalMentionProjection');
3
3
  const { projectDispatchedEscalation, PROJECTOR_NAME: ESCALATION_PROJECTOR_NAME } = require('./dispatchedEscalationProjection');
4
4
  const { projectReminder, PROJECTOR_NAME: REMINDER_PROJECTOR_NAME } = require('./reminderProjection');
5
- const { projectSymptomCase, PROJECTOR_NAME: SYMPTOM_CASE_PROJECTOR_NAME } = require('./symptomCaseProjection');
6
5
  const { projectPatientRisk, PROJECTOR_NAME: RISK_PROJECTOR_NAME } = require('./riskProjection');
7
6
  const { projectSafetyGate, PROJECTOR_NAME: SAFETY_GATE_PROJECTOR_NAME } = require('./safetyGateProjection');
8
7
  const { projectRecommendationPlan, PROJECTOR_NAME: RECOMMENDATION_PLAN_PROJECTOR_NAME } = require('./recommendationPlanProjection');
9
- const { projectIntervention, PROJECTOR_NAME: INTERVENTION_PROJECTOR_NAME } = require('./interventionProjection');
10
- const { projectCluster, PROJECTOR_NAME: CLUSTER_PROJECTOR_NAME } = require('./clusterProjection');
11
- const { projectRoutingDecision, PROJECTOR_NAME: ROUTING_PROJECTOR_NAME } = require('./routingProjection');
12
8
  const { projectPalliativeAssessment, PROJECTOR_NAME: PALLIATIVE_PROJECTOR_NAME } = require('./palliativeProjection');
13
9
  const { projectPalliativeCarePlan, PROJECTOR_NAME: PALLIATIVE_CAREPLAN_PROJECTOR_NAME } = require('./palliativeCarePlanProjection');
14
10
 
@@ -16,13 +12,9 @@ const PROJECTORS = [
16
12
  { name: MENTION_PROJECTOR_NAME, project: projectClinicalMention },
17
13
  { name: ESCALATION_PROJECTOR_NAME, project: projectDispatchedEscalation },
18
14
  { name: REMINDER_PROJECTOR_NAME, project: projectReminder },
19
- { name: SYMPTOM_CASE_PROJECTOR_NAME, project: projectSymptomCase },
20
15
  { name: RISK_PROJECTOR_NAME, project: projectPatientRisk },
21
16
  { name: SAFETY_GATE_PROJECTOR_NAME, project: projectSafetyGate },
22
17
  { name: RECOMMENDATION_PLAN_PROJECTOR_NAME, project: projectRecommendationPlan },
23
- { name: INTERVENTION_PROJECTOR_NAME, project: projectIntervention },
24
- { name: CLUSTER_PROJECTOR_NAME, project: projectCluster },
25
- { name: ROUTING_PROJECTOR_NAME, project: projectRoutingDecision },
26
18
  { name: PALLIATIVE_PROJECTOR_NAME, project: projectPalliativeAssessment },
27
19
  { name: PALLIATIVE_CAREPLAN_PROJECTOR_NAME, project: projectPalliativeCarePlan },
28
20
  ];