@peopl-health/nexus 5.12.3 → 5.12.5

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.
@@ -216,7 +216,6 @@ const submitForApproval = async (req, res) => {
216
216
  $set: {
217
217
  status: 'PENDING',
218
218
  approvalRequest: {
219
- sid: response.sid,
220
219
  dateSubmitted: validSubmittedDate,
221
220
  dateUpdated: validUpdatedDate,
222
221
  rejectionReason: response.rejection_reason || ''
@@ -270,7 +269,6 @@ const checkApprovalStatus = async (req, res) => {
270
269
  status.approvalRequest.date_created || status.approvalRequest.dateCreated || status.content.dateCreated
271
270
  );
272
271
  dbTemplate.approvalRequest = {
273
- sid: status.approvalRequest.sid,
274
272
  dateSubmitted,
275
273
  dateUpdated: parseDate(
276
274
  status.approvalRequest.date_updated || status.approvalRequest.dateUpdated || status.content.dateUpdated,
@@ -288,7 +286,7 @@ const checkApprovalStatus = async (req, res) => {
288
286
  contentSid,
289
287
  content: status.content,
290
288
  approvalRequest: status.approvalRequest,
291
- template: dbTemplate
289
+ template: dbTemplate ? _formatTemplate(dbTemplate) : null
292
290
  });
293
291
  } catch (err) {
294
292
  // If Twilio check fails but we have DB record, return what we know
@@ -301,7 +299,7 @@ const checkApprovalStatus = async (req, res) => {
301
299
  status: dbTemplate.status
302
300
  },
303
301
  approvalRequest: dbTemplate.approvalRequest,
304
- template: dbTemplate,
302
+ template: _formatTemplate(dbTemplate),
305
303
  warning: 'Could not fetch latest status from Twilio'
306
304
  });
307
305
  }
@@ -412,7 +410,7 @@ const getCompleteTemplate = async (req, res) => {
412
410
 
413
411
  return res.status(200).json({
414
412
  success: true,
415
- template
413
+ template: _formatTemplate(template)
416
414
  });
417
415
  } catch (error) {
418
416
  return handleApiError(res, error, 'Failed to get complete template');
@@ -25,6 +25,18 @@ const GRADE_EXT = {
25
25
  THRESHOLD_CLAUSE: 'grade-threshold-clause',
26
26
  };
27
27
 
28
+ const MENTION_EXT = {
29
+ ASSERTION_NEGATED: 'mention-assertion-negated',
30
+ ASSERTION_HEDGED: 'mention-assertion-hedged',
31
+ ASSERTION_EXPERIENCER: 'mention-assertion-experiencer',
32
+ ASSERTION_TEMPORAL: 'mention-assertion-temporal',
33
+ ASSERTION_CONFIDENCE: 'mention-assertion-confidence',
34
+ TEMPORALITY: 'mention-temporality',
35
+ TURN_ID: 'mention-turn-id',
36
+ SOURCE: 'mention-source',
37
+ EXTRACTOR_CONFIDENCE: 'mention-extractor-confidence',
38
+ };
39
+
28
40
  const ROUTING_EXT = {
29
41
  TURN_ID: 'routing-turn-id',
30
42
  DECISION_ID: 'routing-decision-id',
@@ -89,6 +101,7 @@ const RISK_EXT = {
89
101
 
90
102
  module.exports = {
91
103
  SYMPTOM_CASE_EXT,
104
+ MENTION_EXT,
92
105
  ASSESSMENT_EXT,
93
106
  GRADE_EXT,
94
107
  ROUTING_EXT,
@@ -34,6 +34,10 @@ function notes(...texts) {
34
34
  return present.length ? present : null;
35
35
  }
36
36
 
37
+ function toInstant(value) {
38
+ return new Date(value).toISOString();
39
+ }
40
+
37
41
  function toIso(value) {
38
42
  if (value instanceof Date) return value.toISOString().replace(/\.000Z$/, 'Z');
39
43
  const s = String(value);
@@ -50,5 +54,6 @@ module.exports = {
50
54
  codeableConcept,
51
55
  extension,
52
56
  notes,
57
+ toInstant,
53
58
  toIso,
54
59
  };
@@ -2,12 +2,12 @@ const { getDeviceId, getOrganizationId, getSystems } = require('../config/fhirCo
2
2
  const { fhirId } = require('./fhirHelper');
3
3
  const { identifier, patientReference, reference, codeableConcept, toIso } = require('./elementHelper');
4
4
 
5
- function baseResource(resourceType, id, slug, patientId) {
5
+ function baseResource(resourceType, id, slug, patientId = null) {
6
6
  return {
7
7
  resourceType,
8
8
  id: fhirId(id),
9
9
  identifier: [identifier(slug, id)],
10
- subject: patientReference(patientId),
10
+ ...(patientId ? { subject: patientReference(patientId) } : {}),
11
11
  };
12
12
  }
13
13
 
@@ -25,6 +25,11 @@ function provenance({ provenanceId, targetRefs, recordedAt, activity, agentDevic
25
25
  };
26
26
  }
27
27
 
28
+ function problemListCategory() {
29
+ const code = 'problem-list-item';
30
+ return codeableConcept(code, { code, system: getSystems().conditionCategory });
31
+ }
32
+
28
33
  function observationCategory(code) {
29
34
  const label = code.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
30
35
  return codeableConcept(label, { code, system: getSystems().observationCategory, display: label });
@@ -33,5 +38,6 @@ function observationCategory(code) {
33
38
  module.exports = {
34
39
  baseResource,
35
40
  provenance,
41
+ problemListCategory,
36
42
  observationCategory,
37
43
  };
@@ -1,65 +1,195 @@
1
- const { getSystems } = require('../config/fhirConfig');
2
- const { codeableConcept, extension } = require('../helpers/elementHelper');
1
+ const { getDeviceId, getOrganizationId, getSystems } = require('../config/fhirConfig');
2
+ const { stringifyValue } = require('../utils/normalizeUtils');
3
+ const { fhirId } = require('../helpers/fhirHelper');
4
+ const { reference, patientReference, codeableConcept, extension, toInstant } = require('../helpers/elementHelper');
5
+ const { baseResource, problemListCategory, observationCategory } = require('../helpers/resourceHelper');
3
6
  const { MENTION_SLUG } = require('../constants/projectionSlugs');
4
- const { Observation } = require('../resources/Observation');
5
- const { Condition } = require('../resources/Condition');
6
- const { MedicationStatement } = require('../resources/MedicationStatement');
7
- const { Procedure } = require('../resources/Procedure');
8
- const { Appointment } = require('../resources/Appointment');
9
- const { Provenance } = require('../resources/Provenance');
10
-
11
- const PROJECTOR_NAME = 'clinicalMention';
7
+ const { MENTION_EXT } = require('../constants/extensionSlugs');
8
+
12
9
  const ACTIVITY_TEXT = 'clinical-mention-extraction';
10
+ const OBSERVED_FIELD_SYSTEM_KEY = 'observedField';
13
11
 
14
12
  const DEDICATED_AGGREGATE = new Set(['comorbidity', 'allergy', 'performance_status']);
15
13
 
16
- function projectClinicalMention(mention) {
17
- const { fact, provenance } = mention;
14
+ const CATEGORY_TO_OBSERVATION_CATEGORY = {
15
+ symptom: 'survey',
16
+ lab_value: 'laboratory',
17
+ emotion: 'survey',
18
+ indication: 'exam',
19
+ diagnosis: 'exam',
20
+ };
21
+
22
+ const DIAGNOSIS_CATEGORIES = new Set(['diagnosis', 'indication']);
18
23
 
24
+ function toFhir(mention) {
25
+ const { fact, provenance } = mention;
19
26
  if (DEDICATED_AGGREGATE.has(fact.category)) return [];
27
+ const primary = primaryResource(mention);
28
+ return [primary, mentionProvenance(primary, provenance)];
29
+ }
20
30
 
21
- const assertion = fact.assertion;
22
- const extensions = [
23
- extension('mention-assertion-negated', { valueBoolean: assertion.negated }),
24
- extension('mention-assertion-hedged', { valueBoolean: assertion.hedged }),
25
- extension('mention-assertion-experiencer', { valueString: assertion.experiencer }),
26
- extension('mention-assertion-temporal', { valueString: assertion.temporal }),
27
- extension('mention-assertion-confidence', { valueDecimal: assertion.confidence }),
28
- ];
29
- if (fact.temporality) {
30
- extensions.push(extension('mention-temporality', { valueString: fact.temporality }));
31
- }
31
+ const BUILDER_BY_CATEGORY = {
32
+ medication: medicationStatementResource,
33
+ adherence: medicationStatementResource,
34
+ treatment: procedureResource,
35
+ appointment: appointmentResource,
36
+ };
37
+
38
+ function primaryResource(mention) {
39
+ const { fact } = mention;
40
+ const build = BUILDER_BY_CATEGORY[fact.category];
41
+ if (build) return build(mention);
42
+ const currentAffirmedDiagnosis = DIAGNOSIS_CATEGORIES.has(fact.category)
43
+ && fact.assertion.temporal === 'current'
44
+ && !fact.assertion.negated;
45
+ return currentAffirmedDiagnosis ? conditionResource(mention) : observationResource(mention);
46
+ }
32
47
 
33
- const code = fact.ctcaeTerm
48
+ function mentionCode(fact) {
49
+ return fact.ctcaeTerm
34
50
  ? codeableConcept(fact.canonicalTerm, { code: fact.ctcaeTerm, system: getSystems().ctcae, display: fact.ctcaeTerm })
35
51
  : codeableConcept(fact.canonicalTerm);
36
- const primary = primaryResource({ fact, provenance, code, extensions });
37
- return [primary, Provenance.fromMentionProjection({ target: primary, provenance, activityText: ACTIVITY_TEXT })];
38
52
  }
39
53
 
40
- function primaryResource({ fact, provenance, code, extensions }) {
41
- const assertion = fact.assertion;
42
- const args = { fact, provenance, code, extensions, slug: MENTION_SLUG };
43
- if (fact.category === 'medication' || fact.category === 'adherence') {
44
- return MedicationStatement.fromMentionProjection(args);
45
- }
46
- if (fact.category === 'treatment') {
47
- return Procedure.fromMentionProjection(args);
54
+ function mentionExtensions(fact) {
55
+ const { assertion } = fact;
56
+ const out = [
57
+ extension(MENTION_EXT.ASSERTION_NEGATED, { valueBoolean: assertion.negated }),
58
+ extension(MENTION_EXT.ASSERTION_HEDGED, { valueBoolean: assertion.hedged }),
59
+ extension(MENTION_EXT.ASSERTION_EXPERIENCER, { valueString: assertion.experiencer }),
60
+ extension(MENTION_EXT.ASSERTION_TEMPORAL, { valueString: assertion.temporal }),
61
+ extension(MENTION_EXT.ASSERTION_CONFIDENCE, { valueDecimal: assertion.confidence }),
62
+ ];
63
+ if (fact.temporality) out.push(extension(MENTION_EXT.TEMPORALITY, { valueString: fact.temporality }));
64
+ return out;
65
+ }
66
+
67
+ const quoteNotes = (provenance) => {
68
+ const quotes = provenance.verbatimQuotes || [];
69
+ return quotes.length ? quotes.map((text) => ({ text })) : null;
70
+ };
71
+
72
+ function observationResource({ fact, provenance }) {
73
+ const categoryCode = CATEGORY_TO_OBSERVATION_CATEGORY[fact.category] || 'exam';
74
+ const payload = {
75
+ ...baseResource('Observation', fact.mentionId, MENTION_SLUG, fact.patientId),
76
+ status: 'preliminary',
77
+ category: [observationCategory(categoryCode)],
78
+ code: mentionCode(fact),
79
+ performer: [patientReference(fact.patientId)],
80
+ effectiveDateTime: toInstant(fact.reportedAt),
81
+ issued: toInstant(provenance.recordedAt),
82
+ extension: mentionExtensions(fact),
83
+ };
84
+ if (fact.assertion.negated) {
85
+ payload.interpretation = [codeableConcept('Negative', { code: 'NEG', system: getSystems().observationInterpretation, display: 'Negative' })];
86
+ payload.valueCodeableConcept = codeableConcept('Ausente (negado por el paciente)');
48
87
  }
49
- if (fact.category === 'appointment') {
50
- return Appointment.fromMentionProjection(args);
88
+ const quotes = quoteNotes(provenance);
89
+ const components = observedFieldComponents(fact.observedFields);
90
+ if (quotes) payload.note = quotes;
91
+ if (components.length) payload.component = components;
92
+ return payload;
93
+ }
94
+
95
+ function conditionResource({ fact, provenance }) {
96
+ const systems = getSystems();
97
+ const verification = fact.assertion.hedged ? 'unconfirmed' : 'provisional';
98
+ const payload = {
99
+ ...baseResource('Condition', fact.mentionId, MENTION_SLUG, fact.patientId),
100
+ clinicalStatus: codeableConcept('active', { code: 'active', system: systems.conditionClinical }),
101
+ verificationStatus: codeableConcept(verification, { code: verification, system: systems.conditionVerStatus }),
102
+ category: [problemListCategory()],
103
+ code: mentionCode(fact),
104
+ recordedDate: toInstant(provenance.recordedAt),
105
+ extension: mentionExtensions(fact),
106
+ };
107
+ const quotes = quoteNotes(provenance);
108
+ if (quotes) payload.note = quotes;
109
+ return payload;
110
+ }
111
+
112
+ function medicationStatementResource({ fact, provenance }) {
113
+ const payload = {
114
+ ...baseResource('MedicationStatement', fact.mentionId, MENTION_SLUG, fact.patientId),
115
+ status: 'recorded',
116
+ medication: { concept: mentionCode(fact) },
117
+ effectiveDateTime: toInstant(fact.reportedAt),
118
+ extension: mentionExtensions(fact),
119
+ };
120
+ const adherence = adherenceFor(fact.assertion);
121
+ if (adherence) {
122
+ payload.adherence = {
123
+ code: codeableConcept(adherence, { code: adherence, system: getSystems().medicationStatementAdherence }),
124
+ };
51
125
  }
52
- const currentAffirmedDiagnosis =
53
- (fact.category === 'diagnosis' || fact.category === 'indication') &&
54
- assertion.temporal === 'current' &&
55
- !assertion.negated;
56
- if (currentAffirmedDiagnosis) {
57
- return Condition.fromMentionProjection(args);
126
+ const quotes = quoteNotes(provenance);
127
+ if (quotes) payload.note = quotes;
128
+ return payload;
129
+ }
130
+
131
+ function adherenceFor(assertion) {
132
+ if (assertion.negated) return 'not-taking';
133
+ if (assertion.temporal === 'historical') return 'stopped';
134
+ return null;
135
+ }
136
+
137
+ function procedureResource({ fact, provenance }) {
138
+ const payload = {
139
+ ...baseResource('Procedure', fact.mentionId, MENTION_SLUG, fact.patientId),
140
+ status: fact.assertion.negated ? 'not-done' : 'completed',
141
+ code: mentionCode(fact),
142
+ occurrenceDateTime: toInstant(fact.reportedAt),
143
+ recorder: patientReference(fact.patientId),
144
+ extension: mentionExtensions(fact),
145
+ };
146
+ const quotes = quoteNotes(provenance);
147
+ if (quotes) payload.note = quotes;
148
+ return payload;
149
+ }
150
+
151
+ function appointmentResource({ fact, provenance }) {
152
+ const payload = {
153
+ ...baseResource('Appointment', fact.mentionId, MENTION_SLUG),
154
+ status: fact.assertion.negated ? 'cancelled' : 'proposed',
155
+ description: fact.canonicalTerm,
156
+ participant: [{ actor: patientReference(fact.patientId), status: 'accepted' }],
157
+ extension: mentionExtensions(fact),
158
+ };
159
+ const quotes = quoteNotes(provenance);
160
+ if (quotes) payload.note = quotes;
161
+ return payload;
162
+ }
163
+
164
+ function mentionProvenance(target, provenance) {
165
+ return {
166
+ resourceType: 'Provenance',
167
+ id: fhirId(`${provenance.mentionId}-prov`),
168
+ target: [reference(target.resourceType, target.id)],
169
+ recorded: toInstant(provenance.recordedAt),
170
+ agent: [{ who: reference('Device', getDeviceId()), onBehalfOf: reference('Organization', getOrganizationId()) }],
171
+ activity: codeableConcept(ACTIVITY_TEXT),
172
+ extension: [
173
+ extension(MENTION_EXT.TURN_ID, { valueString: provenance.turnId }),
174
+ extension(MENTION_EXT.SOURCE, { valueString: provenance.source }),
175
+ extension(MENTION_EXT.EXTRACTOR_CONFIDENCE, { valueDecimal: provenance.extractorConfidence }),
176
+ ],
177
+ };
178
+ }
179
+
180
+ function observedFieldComponents(observedFields) {
181
+ const components = [];
182
+ for (const [key, raw] of Object.entries(observedFields || {})) {
183
+ const text = stringifyValue(raw);
184
+ if (!text) continue;
185
+ components.push({
186
+ code: codeableConcept(key, { code: key, system: getSystems()[OBSERVED_FIELD_SYSTEM_KEY] }),
187
+ valueString: text,
188
+ });
58
189
  }
59
- return Observation.fromMentionProjection(args);
190
+ return components;
60
191
  }
61
192
 
62
193
  module.exports = {
63
- projectClinicalMention,
64
- PROJECTOR_NAME,
194
+ toFhir,
65
195
  };
@@ -2,7 +2,7 @@ const { ZodError } = require('zod');
2
2
 
3
3
  const { getDeviceId } = require('../config/fhirConfig');
4
4
  const { fhirId, codeSystemUrl } = require('../helpers/fhirHelper');
5
- const { identifier, patientReference, reference, recipientReference, codeableConcept, extension, notes, toIso } = require('../helpers/elementHelper');
5
+ const { patientReference, reference, recipientReference, codeableConcept, extension, notes, toIso } = require('../helpers/elementHelper');
6
6
  const { baseResource, provenance } = require('../helpers/resourceHelper');
7
7
  const { identifierValue, refToId, extValues, referencedId } = require('../helpers/fhirReadHelper');
8
8
  const { INTERVENTION_SLUG, INTERVENTION_KIND_SLUG, INTERVENTION_OUTCOME_SLUG } = require('../constants/projectionSlugs');
@@ -79,9 +79,7 @@ function communicationResource(intervention) {
79
79
 
80
80
  function taskResource(intervention) {
81
81
  const payload = withNote({
82
- resourceType: 'Task',
83
- id: fhirId(intervention.interventionId),
84
- identifier: [identifier(INTERVENTION_SLUG, intervention.interventionId)],
82
+ ...baseResource('Task', intervention.interventionId, INTERVENTION_SLUG),
85
83
  status: intervention.outcome === 'pending' ? 'requested' : 'completed',
86
84
  intent: 'order',
87
85
  code: codeableConcept(intervention.interventionLabel, {
@@ -1,5 +1,4 @@
1
1
  const { registerProjector } = require('../services/fhirService');
2
- const { projectClinicalMention, PROJECTOR_NAME: MENTION_PROJECTOR_NAME } = require('./clinicalMentionProjection');
3
2
  const { projectDispatchedEscalation, PROJECTOR_NAME: ESCALATION_PROJECTOR_NAME } = require('./dispatchedEscalationProjection');
4
3
  const { projectReminder, PROJECTOR_NAME: REMINDER_PROJECTOR_NAME } = require('./reminderProjection');
5
4
  const { projectPatientRisk, PROJECTOR_NAME: RISK_PROJECTOR_NAME } = require('./riskProjection');
@@ -9,7 +8,6 @@ const { projectPalliativeAssessment, PROJECTOR_NAME: PALLIATIVE_PROJECTOR_NAME }
9
8
  const { projectPalliativeCarePlan, PROJECTOR_NAME: PALLIATIVE_CAREPLAN_PROJECTOR_NAME } = require('./palliativeCarePlanProjection');
10
9
 
11
10
  const PROJECTORS = [
12
- { name: MENTION_PROJECTOR_NAME, project: projectClinicalMention },
13
11
  { name: ESCALATION_PROJECTOR_NAME, project: projectDispatchedEscalation },
14
12
  { name: REMINDER_PROJECTOR_NAME, project: projectReminder },
15
13
  { name: RISK_PROJECTOR_NAME, project: projectPatientRisk },
@@ -53,20 +53,17 @@ const DISPOSITION_CODECS = {
53
53
  },
54
54
  task: {
55
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
- },
56
+ build: (d) => ({
57
+ ...baseResource('Task', d.dispositionId, ROUTING_DISPOSITION_SLUG),
58
+ status: 'requested',
59
+ intent: 'order',
60
+ code: codeableConcept(d.primaryAction),
61
+ description: d.primaryReason,
62
+ for: patientReference(d.patientId),
63
+ authoredOn: toIso(d.authoredAt),
64
+ requester: reference('Device', getDeviceId()),
65
+ ...(d.primaryCaseId ? { reason: caseReason(d.primaryCaseId) } : {}),
66
+ }),
70
67
  actionOf: (r) => ccText(r.code),
71
68
  reasonOf: (r) => r.description,
72
69
  authoredOf: (r) => r.authoredOn,
@@ -3,7 +3,7 @@ const { ZodError } = require('zod');
3
3
  const { getSystems } = require('../config/fhirConfig');
4
4
  const { codeSystemUrl } = require('../helpers/fhirHelper');
5
5
  const { reference, patientReference, codeableConcept, extension, toIso } = require('../helpers/elementHelper');
6
- const { baseResource, provenance, observationCategory } = require('../helpers/resourceHelper');
6
+ const { baseResource, provenance, problemListCategory, observationCategory } = require('../helpers/resourceHelper');
7
7
  const { extMap, extValues, identifierValue } = require('../helpers/fhirReadHelper');
8
8
  const { SYMPTOM_CASE_SLUG, SYMPTOM_ASSESSMENT_SLUG, SYMPTOM_GRADE_SLUG } = require('../constants/projectionSlugs');
9
9
  const { SYMPTOM_CASE_EXT, ASSESSMENT_EXT, GRADE_EXT } = require('../constants/extensionSlugs');
@@ -76,7 +76,7 @@ function conditionResource(condition, verification, headAssessmentId) {
76
76
  clinicalStatus: codeableConcept(clinicalStatusCode, { code: clinicalStatusCode, system: systems.conditionClinical }),
77
77
  verificationStatus: codeableConcept(verificationCode, { code: verificationCode, system: systems.conditionVerStatus }),
78
78
  category: [
79
- codeableConcept('problem-list-item', { code: 'problem-list-item', system: systems.conditionCategory }),
79
+ problemListCategory(),
80
80
  codeableConcept('PRO-CTCAE patient-reported symptom (agent-tracked case)', { code: 'proctcae-symptom', system: codeSystemUrl('condition-category') }),
81
81
  ],
82
82
  code,
@@ -1,35 +1,13 @@
1
1
  const { getSystems } = require('../config/fhirConfig');
2
2
  const { fhirId, codeSystemUrl } = require('../helpers/fhirHelper');
3
3
  const { identifier, reference, patientReference, codeableConcept, extension, toIso } = require('../helpers/elementHelper');
4
+ const { problemListCategory } = require('../helpers/resourceHelper');
4
5
 
5
6
  class Condition {
6
7
  constructor(payload) {
7
8
  Object.assign(this, payload);
8
9
  }
9
10
 
10
- static fromMentionProjection({ fact, provenance, code, extensions, slug }) {
11
- const assertion = fact.assertion;
12
- const notes = provenance.verbatimQuotes && provenance.verbatimQuotes.length
13
- ? provenance.verbatimQuotes.map((text) => ({ text }))
14
- : null;
15
- const verification = assertion.hedged ? 'unconfirmed' : 'provisional';
16
- const systems = getSystems();
17
- const payload = {
18
- resourceType: 'Condition',
19
- id: fhirId(fact.mentionId),
20
- identifier: [identifier(slug, fact.mentionId)],
21
- clinicalStatus: codeableConcept('active', { code: 'active', system: systems.conditionClinical }),
22
- verificationStatus: codeableConcept(verification, { code: verification, system: systems.conditionVerStatus }),
23
- category: [codeableConcept('problem-list-item', { code: 'problem-list-item', system: systems.conditionCategory })],
24
- code,
25
- subject: patientReference(fact.patientId),
26
- recordedDate: new Date(provenance.recordedAt).toISOString(),
27
- extension: extensions,
28
- };
29
- if (notes) payload.note = notes;
30
- return new Condition(payload);
31
- }
32
-
33
11
  static fromTriageProjection({ sym, code }) {
34
12
  const caseId = `${sym.patientId}-${sym.term}`;
35
13
  const systems = getSystems();
@@ -40,7 +18,7 @@ class Condition {
40
18
  clinicalStatus: codeableConcept('active', { code: 'active', system: systems.conditionClinical }),
41
19
  verificationStatus: codeableConcept('provisional', { code: 'provisional', system: systems.conditionVerStatus }),
42
20
  category: [
43
- codeableConcept('problem-list-item', { code: 'problem-list-item', system: systems.conditionCategory }),
21
+ problemListCategory(),
44
22
  codeableConcept('PRO-CTCAE patient-reported symptom (triage-tracked case)', { code: 'proctcae-symptom', system: codeSystemUrl('condition-category') }),
45
23
  ],
46
24
  code,
@@ -1,53 +1,13 @@
1
- const { getSystems } = require('../config/fhirConfig');
2
1
  const { fhirId } = require('../helpers/fhirHelper');
3
2
  const { identifier, reference, patientReference, codeableConcept, extension, toIso } = require('../helpers/elementHelper');
4
3
  const { observationCategory } = require('../helpers/resourceHelper');
5
4
  const { GRADE_EXT } = require('../constants/extensionSlugs');
6
5
 
7
- const OBSERVED_FIELD_SYSTEM_KEY = 'observedField';
8
-
9
- const CATEGORY_TO_OBSERVATION_CATEGORY = {
10
- symptom: 'survey',
11
- lab_value: 'laboratory',
12
- emotion: 'survey',
13
- indication: 'exam',
14
- diagnosis: 'exam',
15
- };
16
-
17
6
  class Observation {
18
7
  constructor(payload) {
19
8
  Object.assign(this, payload);
20
9
  }
21
10
 
22
- static fromMentionProjection({ fact, provenance, code, extensions, slug }) {
23
- const assertion = fact.assertion;
24
- const notes = provenance.verbatimQuotes && provenance.verbatimQuotes.length
25
- ? provenance.verbatimQuotes.map((text) => ({ text }))
26
- : null;
27
- const categoryCode = CATEGORY_TO_OBSERVATION_CATEGORY[fact.category] || 'exam';
28
- const payload = {
29
- resourceType: 'Observation',
30
- id: fhirId(fact.mentionId),
31
- identifier: [identifier(slug, fact.mentionId)],
32
- status: 'preliminary',
33
- category: [observationCategory(categoryCode)],
34
- code,
35
- subject: patientReference(fact.patientId),
36
- performer: [patientReference(fact.patientId)],
37
- effectiveDateTime: new Date(fact.reportedAt).toISOString(),
38
- issued: new Date(provenance.recordedAt).toISOString(),
39
- extension: extensions,
40
- };
41
- if (assertion.negated) {
42
- payload.interpretation = [codeableConcept('Negative', { code: 'NEG', system: getSystems().observationInterpretation, display: 'Negative' })];
43
- payload.valueCodeableConcept = codeableConcept('Ausente (negado por el paciente)');
44
- }
45
- if (notes) payload.note = notes;
46
- const components = observedFieldComponents(fact.observedFields);
47
- if (components.length) payload.component = components;
48
- return new Observation(payload);
49
- }
50
-
51
11
  static fromTriageProjection({ sym, code, aeCategoryConcept, components, gradeConcept, method, deviceId }) {
52
12
  const sid = String(sym.symptomId);
53
13
  const payload = {
@@ -95,28 +55,4 @@ class Observation {
95
55
  }
96
56
  }
97
57
 
98
- function observedFieldComponents(observedFields) {
99
- const components = [];
100
- for (const [key, raw] of Object.entries(observedFields || {})) {
101
- const text = stringifyObserved(raw);
102
- if (!text) continue;
103
- components.push({
104
- code: codeableConcept(key, { code: key, system: getSystems()[OBSERVED_FIELD_SYSTEM_KEY] }),
105
- valueString: text,
106
- });
107
- }
108
- return components;
109
- }
110
-
111
- function stringifyObserved(value) {
112
- if (value === null || value === undefined) return '';
113
- if (Array.isArray(value)) {
114
- return value.filter((v) => v !== null && v !== undefined).map((v) => String(v).trim()).filter(Boolean).join('; ');
115
- }
116
- if (typeof value === 'object') {
117
- return Object.entries(value).filter(([, v]) => v !== null && v !== undefined && String(v).trim()).map(([k, v]) => `${k}: ${v}`).join('; ');
118
- }
119
- return String(value).trim();
120
- }
121
-
122
58
  module.exports = { Observation };
@@ -1,29 +1,12 @@
1
1
  const { getDeviceId, getOrganizationId } = require('../config/fhirConfig');
2
2
  const { fhirId } = require('../helpers/fhirHelper');
3
- const { extension, reference, codeableConcept, toIso } = require('../helpers/elementHelper');
3
+ const { reference, codeableConcept, toIso } = require('../helpers/elementHelper');
4
4
 
5
5
  class Provenance {
6
6
  constructor(payload) {
7
7
  Object.assign(this, payload);
8
8
  }
9
9
 
10
- static fromMentionProjection({ target, provenance, activityText }) {
11
- const payload = {
12
- resourceType: 'Provenance',
13
- id: fhirId(`${provenance.mentionId}-prov`),
14
- target: [{ reference: `${target.resourceType}/${target.id}` }],
15
- recorded: new Date(provenance.recordedAt).toISOString(),
16
- agent: [{ who: reference('Device', getDeviceId()), onBehalfOf: reference('Organization', getOrganizationId()) }],
17
- activity: codeableConcept(activityText),
18
- extension: [
19
- extension('mention-turn-id', { valueString: provenance.turnId }),
20
- extension('mention-source', { valueString: provenance.source }),
21
- extension('mention-extractor-confidence', { valueDecimal: provenance.extractorConfidence }),
22
- ],
23
- };
24
- return new Provenance(payload);
25
- }
26
-
27
10
  static fromDispatchedEscalation({ target, recordedAt, activityText }) {
28
11
  return new Provenance({
29
12
  resourceType: 'Provenance',
@@ -1,5 +1,5 @@
1
- const { PROJECTOR_NAME } = require('../projections/clinicalMentionProjection');
2
- const { project, storeResources } = require('./fhirService');
1
+ const { toFhir } = require('../projections/clinicalMentionProjection');
2
+ const { storeResources } = require('./fhirService');
3
3
 
4
4
  async function storeClinicalMentions({ patientId, mentions }) {
5
5
  for (const mention of mentions) {
@@ -7,9 +7,7 @@ async function storeClinicalMentions({ patientId, mentions }) {
7
7
  throw new Error(`storeClinicalMentions patientId mismatch: expected ${patientId}`);
8
8
  }
9
9
  }
10
- const bundle = project(mentions.map((mention) => ({ name: PROJECTOR_NAME, aggregate: mention })));
11
- const resources = bundle.entry.map((entry) => entry.resource);
12
- const { stored } = await storeResources(resources);
10
+ const { stored } = await storeResources(mentions.flatMap(toFhir));
13
11
  return { stored: stored.length, ids: stored, patientId };
14
12
  }
15
13
 
@@ -15,6 +15,17 @@ const parseList = (value) =>
15
15
  .map((entry) => entry.trim())
16
16
  .filter(Boolean);
17
17
 
18
+ function stringifyValue(value) {
19
+ if (value === null || value === undefined) return '';
20
+ if (Array.isArray(value)) {
21
+ return value.filter((v) => v !== null && v !== undefined).map((v) => String(v).trim()).filter(Boolean).join('; ');
22
+ }
23
+ if (typeof value === 'object') {
24
+ return Object.entries(value).filter(([, v]) => v !== null && v !== undefined && String(v).trim()).map(([k, v]) => `${k}: ${v}`).join('; ');
25
+ }
26
+ return String(value).trim();
27
+ }
28
+
18
29
  function termKey(name) {
19
30
  return String(name || '')
20
31
  .trim()
@@ -29,5 +40,6 @@ module.exports = {
29
40
  lookupValue,
30
41
  integerId,
31
42
  parseList,
43
+ stringifyValue,
32
44
  termKey,
33
45
  };
package/lib/index.d.ts CHANGED
@@ -116,13 +116,13 @@ declare module '@peopl-health/nexus' {
116
116
  name: string;
117
117
  description?: string;
118
118
  parameters?: any;
119
- handler: (args: any) => any;
119
+ handler: (args: any) => string | Promise<string>;
120
120
  }
121
121
 
122
122
  export interface AssistantConfigDefinition {
123
123
  extends?: typeof BaseAssistant;
124
124
  create?: (this: BaseAssistant, code: string, context?: any) => Promise<any>;
125
- tools?: Array<AssistantToolDefinition | { name: string; definition?: any; handler: (args: any) => any }>;
125
+ tools?: Array<AssistantToolDefinition | { name: string; definition?: any; handler: (args: any) => string | Promise<string> }>;
126
126
  setup?: (this: BaseAssistant, context: { assistantId: string; thread?: any; options?: any }) => void;
127
127
  }
128
128
 
@@ -138,7 +138,7 @@ declare module '@peopl-health/nexus' {
138
138
  assistantId?: string;
139
139
  thread?: any;
140
140
  client?: any;
141
- tools?: Array<AssistantToolDefinition | { name: string; definition?: any; handler: (args: any) => any }>;
141
+ tools?: Array<AssistantToolDefinition | { name: string; definition?: any; handler: (args: any) => string | Promise<string> }>;
142
142
  setup?: (context: { assistantId: string; thread?: any; options?: any }) => void;
143
143
  status?: string;
144
144
  } | any);
@@ -146,9 +146,9 @@ declare module '@peopl-health/nexus' {
146
146
  thread: any;
147
147
  status: string;
148
148
  createdAt: Date;
149
- registerTool(definition: AssistantToolDefinition | string, schema?: any, handler?: (args: any) => any): void;
149
+ registerTool(definition: AssistantToolDefinition | string, schema?: any, handler?: (args: any) => string | Promise<string>): void;
150
150
  getToolSchemas(): any[];
151
- executeTool(name: string, args: any): Promise<any>;
151
+ executeTool(name: string, args: any): Promise<string>;
152
152
  getPreviousMessages(thread?: any): Promise<Array<{ role: string; content: any }>>;
153
153
  createThread(code: string, context?: any): Promise<any>;
154
154
  sendMessage(userId: string, message: string, options?: any): Promise<any>;
@@ -36,7 +36,6 @@ const TemplateSchema = new mongoose.Schema({
36
36
  }
37
37
  ],
38
38
  approvalRequest: {
39
- sid: String,
40
39
  dateSubmitted: Date,
41
40
  dateUpdated: Date,
42
41
  rejectionReason: String
@@ -15,7 +15,6 @@ const refreshApprovalStatuses = async (templates) => {
15
15
  const reqData = approvalInfo.approvalRequest;
16
16
  const updateFields = {
17
17
  approvalRequest: {
18
- sid: reqData.sid,
19
18
  dateSubmitted: reqData.dateCreated ? new Date(reqData.dateCreated) : new Date(),
20
19
  dateUpdated: reqData.dateUpdated ? new Date(reqData.dateUpdated) : new Date(),
21
20
  rejectionReason: reqData.rejectionReason || ''
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peopl-health/nexus",
3
- "version": "5.12.3",
3
+ "version": "5.12.5",
4
4
  "description": "Core messaging and assistant library for WhatsApp communication platforms",
5
5
  "keywords": [
6
6
  "whatsapp",
@@ -1,28 +0,0 @@
1
- const { fhirId } = require('../helpers/fhirHelper');
2
- const { identifier, patientReference } = require('../helpers/elementHelper');
3
-
4
- class Appointment {
5
- constructor(payload) {
6
- Object.assign(this, payload);
7
- }
8
-
9
- static fromMentionProjection({ fact, provenance, extensions, slug }) {
10
- const assertion = fact.assertion;
11
- const notes = provenance.verbatimQuotes && provenance.verbatimQuotes.length
12
- ? provenance.verbatimQuotes.map((text) => ({ text }))
13
- : null;
14
- const payload = {
15
- resourceType: 'Appointment',
16
- id: fhirId(fact.mentionId),
17
- identifier: [identifier(slug, fact.mentionId)],
18
- status: assertion.negated ? 'cancelled' : 'proposed',
19
- description: fact.canonicalTerm,
20
- participant: [{ actor: patientReference(fact.patientId), status: 'accepted' }],
21
- extension: extensions,
22
- };
23
- if (notes) payload.note = notes;
24
- return new Appointment(payload);
25
- }
26
- }
27
-
28
- module.exports = { Appointment };
@@ -1,42 +0,0 @@
1
- const { getSystems } = require('../config/fhirConfig');
2
- const { fhirId } = require('../helpers/fhirHelper');
3
- const { identifier, patientReference, codeableConcept } = require('../helpers/elementHelper');
4
-
5
- class MedicationStatement {
6
- constructor(payload) {
7
- Object.assign(this, payload);
8
- }
9
-
10
- static fromMentionProjection({ fact, provenance, code, extensions, slug }) {
11
- const assertion = fact.assertion;
12
- const notes = provenance.verbatimQuotes && provenance.verbatimQuotes.length
13
- ? provenance.verbatimQuotes.map((text) => ({ text }))
14
- : null;
15
- const payload = {
16
- resourceType: 'MedicationStatement',
17
- id: fhirId(fact.mentionId),
18
- identifier: [identifier(slug, fact.mentionId)],
19
- status: 'recorded',
20
- medication: { concept: code },
21
- subject: patientReference(fact.patientId),
22
- effectiveDateTime: new Date(fact.reportedAt).toISOString(),
23
- extension: extensions,
24
- };
25
- const adherence = adherenceFor(assertion);
26
- if (adherence) {
27
- payload.adherence = {
28
- code: codeableConcept(adherence, { code: adherence, system: getSystems().medicationStatementAdherence }),
29
- };
30
- }
31
- if (notes) payload.note = notes;
32
- return new MedicationStatement(payload);
33
- }
34
- }
35
-
36
- function adherenceFor(assertion) {
37
- if (assertion.negated) return 'not-taking';
38
- if (assertion.temporal === 'historical') return 'stopped';
39
- return null;
40
- }
41
-
42
- module.exports = { MedicationStatement };
@@ -1,30 +0,0 @@
1
- const { fhirId } = require('../helpers/fhirHelper');
2
- const { identifier, patientReference } = require('../helpers/elementHelper');
3
-
4
- class Procedure {
5
- constructor(payload) {
6
- Object.assign(this, payload);
7
- }
8
-
9
- static fromMentionProjection({ fact, provenance, code, extensions, slug }) {
10
- const assertion = fact.assertion;
11
- const notes = provenance.verbatimQuotes && provenance.verbatimQuotes.length
12
- ? provenance.verbatimQuotes.map((text) => ({ text }))
13
- : null;
14
- const payload = {
15
- resourceType: 'Procedure',
16
- id: fhirId(fact.mentionId),
17
- identifier: [identifier(slug, fact.mentionId)],
18
- status: assertion.negated ? 'not-done' : 'completed',
19
- code,
20
- subject: patientReference(fact.patientId),
21
- occurrenceDateTime: new Date(fact.reportedAt).toISOString(),
22
- recorder: patientReference(fact.patientId),
23
- extension: extensions,
24
- };
25
- if (notes) payload.note = notes;
26
- return new Procedure(payload);
27
- }
28
- }
29
-
30
- module.exports = { Procedure };