@peopl-health/nexus 5.12.2 → 5.12.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/fhir/constants/extensionSlugs.js +15 -0
- package/lib/fhir/helpers/elementHelper.js +5 -0
- package/lib/fhir/helpers/fhirReadHelper.js +11 -0
- package/lib/fhir/helpers/resourceHelper.js +8 -2
- package/lib/fhir/projections/clinicalMentionProjection.js +175 -45
- package/lib/fhir/projections/clusterProjection.js +3 -2
- package/lib/fhir/projections/contingencyProjection.js +7 -3
- package/lib/fhir/projections/interventionProjection.js +6 -6
- package/lib/fhir/projections/registerProjectors.js +0 -2
- package/lib/fhir/projections/routingProjection.js +15 -17
- package/lib/fhir/projections/symptomCaseProjection.js +2 -2
- package/lib/fhir/resources/Condition.js +2 -24
- package/lib/fhir/resources/Observation.js +0 -64
- package/lib/fhir/resources/Provenance.js +1 -18
- package/lib/fhir/services/clinicalMentionService.js +3 -5
- package/lib/fhir/utils/normalizeUtils.js +12 -0
- package/package.json +1 -1
- package/lib/fhir/resources/Appointment.js +0 -28
- package/lib/fhir/resources/MedicationStatement.js +0 -42
- package/lib/fhir/resources/Procedure.js +0 -30
|
@@ -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',
|
|
@@ -35,6 +47,7 @@ const ROUTING_EXT = {
|
|
|
35
47
|
FINDING_CASE_ID: 'routing-finding-case-id',
|
|
36
48
|
FINDING_REASONING: 'routing-finding-reasoning',
|
|
37
49
|
FINDING_ACTION: 'routing-finding-action',
|
|
50
|
+
PREVIOUS_ID: 'routing-previous-id',
|
|
38
51
|
};
|
|
39
52
|
|
|
40
53
|
const CLUSTER_EXT = {
|
|
@@ -46,6 +59,7 @@ const CLUSTER_EXT = {
|
|
|
46
59
|
STATUS: 'cluster-status',
|
|
47
60
|
CONSULTATION_ID: 'cluster-consultation-id',
|
|
48
61
|
DISSOLVE_REASON: 'cluster-dissolve-reason',
|
|
62
|
+
MEMBER_CASE_ID: 'cluster-member-case-id',
|
|
49
63
|
};
|
|
50
64
|
|
|
51
65
|
const PALLIATIVE_EXT = {
|
|
@@ -87,6 +101,7 @@ const RISK_EXT = {
|
|
|
87
101
|
|
|
88
102
|
module.exports = {
|
|
89
103
|
SYMPTOM_CASE_EXT,
|
|
104
|
+
MENTION_EXT,
|
|
90
105
|
ASSESSMENT_EXT,
|
|
91
106
|
GRADE_EXT,
|
|
92
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
|
};
|
|
@@ -34,9 +34,20 @@ function extValues(resource, name) {
|
|
|
34
34
|
return values;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
+
function referencedId(node, slug, reference) {
|
|
38
|
+
return extValues(node, slug)[0] || refToId(reference);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function referencedIds(node, slug, references) {
|
|
42
|
+
const values = extValues(node, slug);
|
|
43
|
+
return values.length ? values : references.map(refToId).filter(Boolean);
|
|
44
|
+
}
|
|
45
|
+
|
|
37
46
|
module.exports = {
|
|
38
47
|
extMap,
|
|
39
48
|
identifierValue,
|
|
40
49
|
refToId,
|
|
41
50
|
extValues,
|
|
51
|
+
referencedId,
|
|
52
|
+
referencedIds,
|
|
42
53
|
};
|
|
@@ -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 {
|
|
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 {
|
|
5
|
-
|
|
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
|
-
|
|
17
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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
|
|
41
|
-
const assertion = fact
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
|
|
50
|
-
|
|
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
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
|
190
|
+
return components;
|
|
60
191
|
}
|
|
61
192
|
|
|
62
193
|
module.exports = {
|
|
63
|
-
|
|
64
|
-
PROJECTOR_NAME,
|
|
194
|
+
toFhir,
|
|
65
195
|
};
|
|
@@ -3,7 +3,7 @@ const { ZodError } = require('zod');
|
|
|
3
3
|
const { codeSystemUrl } = require('../helpers/fhirHelper');
|
|
4
4
|
const { identifier, reference, codeableConcept, extension, toIso } = require('../helpers/elementHelper');
|
|
5
5
|
const { baseResource, provenance } = require('../helpers/resourceHelper');
|
|
6
|
-
const { extMap, identifierValue,
|
|
6
|
+
const { extMap, identifierValue, referencedIds } = require('../helpers/fhirReadHelper');
|
|
7
7
|
const { CLUSTER_IMPRESSION_SLUG, CLUSTER_LABEL_SLUG } = require('../constants/projectionSlugs');
|
|
8
8
|
const { CLUSTER_EXT } = require('../constants/extensionSlugs');
|
|
9
9
|
const { ClusterImpression } = require('../../shared/dtos/ClusterImpression');
|
|
@@ -54,6 +54,7 @@ function clusterExtensions(cluster) {
|
|
|
54
54
|
if (cluster.weakensIf.length) {
|
|
55
55
|
out.push(extension(CLUSTER_EXT.WEAKENS_IF, { valueString: cluster.weakensIf.join(LIST_SEPARATOR) }));
|
|
56
56
|
}
|
|
57
|
+
for (const caseId of cluster.memberCaseIds) out.push(extension(CLUSTER_EXT.MEMBER_CASE_ID, { valueString: caseId }));
|
|
57
58
|
out.push(extension(CLUSTER_EXT.STATUS, { valueString: cluster.status }));
|
|
58
59
|
if (cluster.consultationId) out.push(extension(CLUSTER_EXT.CONSULTATION_ID, { valueString: cluster.consultationId }));
|
|
59
60
|
if (cluster.dissolveReason) out.push(extension(CLUSTER_EXT.DISSOLVE_REASON, { valueString: cluster.dissolveReason }));
|
|
@@ -104,7 +105,7 @@ function reconstruct(resource, patientId) {
|
|
|
104
105
|
status: ext[CLUSTER_EXT.STATUS],
|
|
105
106
|
confidence: ext[CLUSTER_EXT.CONFIDENCE],
|
|
106
107
|
clinicalSignificance: ext[CLUSTER_EXT.CLINICAL_SIGNIFICANCE],
|
|
107
|
-
memberCaseIds: (resource.problem || []).map((
|
|
108
|
+
memberCaseIds: referencedIds(resource, CLUSTER_EXT.MEMBER_CASE_ID, (resource.problem || []).map((p) => p.reference)),
|
|
108
109
|
consultationId: ext[CLUSTER_EXT.CONSULTATION_ID] || null,
|
|
109
110
|
relationshipModel: resource.summary || '',
|
|
110
111
|
strengthensIf: splitList(ext[CLUSTER_EXT.STRENGTHENS_IF]),
|
|
@@ -4,7 +4,7 @@ const { getOrganizationId } = require('../config/fhirConfig');
|
|
|
4
4
|
const { fhirId } = require('../helpers/fhirHelper');
|
|
5
5
|
const { patientReference, reference, codeableConcept, extension, toIso } = require('../helpers/elementHelper');
|
|
6
6
|
const { baseResource, provenance } = require('../helpers/resourceHelper');
|
|
7
|
-
const { extMap, identifierValue,
|
|
7
|
+
const { extMap, identifierValue, referencedId } = require('../helpers/fhirReadHelper');
|
|
8
8
|
const { CONTINGENCY_PLAN_SLUG, CONTINGENCY_STEP_SLUG } = require('../constants/projectionSlugs');
|
|
9
9
|
const { ContingencySafetyNet } = require('../../shared/dtos/ContingencySafetyNet');
|
|
10
10
|
const { logger } = require('../../utils/logger');
|
|
@@ -18,6 +18,8 @@ const EXT = {
|
|
|
18
18
|
WHY_THIS_MATTERS: 'contingency-why-this-matters',
|
|
19
19
|
CREATION_REASONING: 'contingency-creation-reasoning',
|
|
20
20
|
RESOLVED_REASON: 'contingency-resolved-reason',
|
|
21
|
+
RELATED_CASE_ID: 'contingency-related-case-id',
|
|
22
|
+
RELATED_CLUSTER_ID: 'contingency-related-cluster-id',
|
|
21
23
|
STEP_PRIORITY: 'contingency-step-priority',
|
|
22
24
|
ACTION_REQUESTED: 'contingency-action-requested',
|
|
23
25
|
FIRES_AFTER_SILENCE_MINUTES: 'fires-after-silence-minutes',
|
|
@@ -89,6 +91,8 @@ function carePlanPeriod(carePlan) {
|
|
|
89
91
|
|
|
90
92
|
function carePlanExtensions(carePlan) {
|
|
91
93
|
const out = [extension(EXT.STATUS, { valueString: carePlan.status })];
|
|
94
|
+
if (carePlan.relatedCaseId) out.push(extension(EXT.RELATED_CASE_ID, { valueString: carePlan.relatedCaseId }));
|
|
95
|
+
if (carePlan.relatedClusterId) out.push(extension(EXT.RELATED_CLUSTER_ID, { valueString: carePlan.relatedClusterId }));
|
|
92
96
|
if (carePlan.awaitingCharacterization) out.push(extension(EXT.AWAITING_CHARACTERIZATION, { valueString: carePlan.awaitingCharacterization }));
|
|
93
97
|
if (carePlan.whyThisMatters) out.push(extension(EXT.WHY_THIS_MATTERS, { valueString: carePlan.whyThisMatters }));
|
|
94
98
|
if (carePlan.creationReasoning) out.push(extension(EXT.CREATION_REASONING, { valueString: carePlan.creationReasoning }));
|
|
@@ -148,8 +152,8 @@ function reconstruct(plan, requests, patientId) {
|
|
|
148
152
|
carePlan: {
|
|
149
153
|
planId,
|
|
150
154
|
patientId,
|
|
151
|
-
relatedCaseId:
|
|
152
|
-
relatedClusterId:
|
|
155
|
+
relatedCaseId: referencedId(plan, EXT.RELATED_CASE_ID, address?.reference),
|
|
156
|
+
relatedClusterId: referencedId(plan, EXT.RELATED_CLUSTER_ID, plan.supportingInfo?.[0]?.reference),
|
|
153
157
|
concern: plan.description || '',
|
|
154
158
|
awaitingCharacterization: ext[EXT.AWAITING_CHARACTERIZATION] || '',
|
|
155
159
|
whyThisMatters: ext[EXT.WHY_THIS_MATTERS] || '',
|
|
@@ -2,9 +2,9 @@ const { ZodError } = require('zod');
|
|
|
2
2
|
|
|
3
3
|
const { getDeviceId } = require('../config/fhirConfig');
|
|
4
4
|
const { fhirId, codeSystemUrl } = require('../helpers/fhirHelper');
|
|
5
|
-
const {
|
|
5
|
+
const { patientReference, reference, recipientReference, codeableConcept, extension, notes, toIso } = require('../helpers/elementHelper');
|
|
6
6
|
const { baseResource, provenance } = require('../helpers/resourceHelper');
|
|
7
|
-
const { identifierValue, refToId, extValues } = require('../helpers/fhirReadHelper');
|
|
7
|
+
const { identifierValue, refToId, extValues, referencedId } = require('../helpers/fhirReadHelper');
|
|
8
8
|
const { INTERVENTION_SLUG, INTERVENTION_KIND_SLUG, INTERVENTION_OUTCOME_SLUG } = require('../constants/projectionSlugs');
|
|
9
9
|
const { Intervention } = require('../../shared/dtos/Intervention');
|
|
10
10
|
const { logger } = require('../../utils/logger');
|
|
@@ -13,6 +13,7 @@ const ACTIVITY_TEXT = 'intervention-record';
|
|
|
13
13
|
const PROCEDURE_KINDS = new Set(['otc', 'self_care', 'other']);
|
|
14
14
|
|
|
15
15
|
const EXT = {
|
|
16
|
+
CASE_ID: 'intervention-case-id',
|
|
16
17
|
RATIONALE: 'intervention-rationale',
|
|
17
18
|
OUTCOME_NOTES: 'intervention-outcome-notes',
|
|
18
19
|
OUTCOME: 'intervention-outcome',
|
|
@@ -78,9 +79,7 @@ function communicationResource(intervention) {
|
|
|
78
79
|
|
|
79
80
|
function taskResource(intervention) {
|
|
80
81
|
const payload = withNote({
|
|
81
|
-
|
|
82
|
-
id: fhirId(intervention.interventionId),
|
|
83
|
-
identifier: [identifier(INTERVENTION_SLUG, intervention.interventionId)],
|
|
82
|
+
...baseResource('Task', intervention.interventionId, INTERVENTION_SLUG),
|
|
84
83
|
status: intervention.outcome === 'pending' ? 'requested' : 'completed',
|
|
85
84
|
intent: 'order',
|
|
86
85
|
code: codeableConcept(intervention.interventionLabel, {
|
|
@@ -122,6 +121,7 @@ function communicationRequestResource(intervention) {
|
|
|
122
121
|
|
|
123
122
|
function interventionExtensions(intervention, { withRecipient = false } = {}) {
|
|
124
123
|
const out = [];
|
|
124
|
+
if (intervention.caseId) out.push(extension(EXT.CASE_ID, { valueString: intervention.caseId }));
|
|
125
125
|
if (intervention.rationale) out.push(extension(EXT.RATIONALE, { valueString: intervention.rationale }));
|
|
126
126
|
if (intervention.outcomeNotes) out.push(extension(EXT.OUTCOME_NOTES, { valueString: intervention.outcomeNotes }));
|
|
127
127
|
if (intervention.outcome) out.push(extension(EXT.OUTCOME, { valueString: intervention.outcome }));
|
|
@@ -144,7 +144,7 @@ function reconstruct(resource, patientId) {
|
|
|
144
144
|
const base = {
|
|
145
145
|
interventionId: identifierValue(resource, INTERVENTION_SLUG),
|
|
146
146
|
patientId,
|
|
147
|
-
caseId:
|
|
147
|
+
caseId: referencedId(resource, EXT.CASE_ID, reasonRef?.reference),
|
|
148
148
|
rationale: extValues(resource, EXT.RATIONALE)[0] ?? null,
|
|
149
149
|
outcomeNotes: extValues(resource, EXT.OUTCOME_NOTES)[0] ?? null,
|
|
150
150
|
outcomeExt: extValues(resource, EXT.OUTCOME)[0] ?? null,
|
|
@@ -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 },
|
|
@@ -3,7 +3,7 @@ const { ZodError } = require('zod');
|
|
|
3
3
|
const { getDeviceId } = require('../config/fhirConfig');
|
|
4
4
|
const { reference, patientReference, codeableConcept, extension, toIso } = require('../helpers/elementHelper');
|
|
5
5
|
const { baseResource, provenance } = require('../helpers/resourceHelper');
|
|
6
|
-
const { extMap, extValues, identifierValue, refToId } = require('../helpers/fhirReadHelper');
|
|
6
|
+
const { extMap, extValues, identifierValue, refToId, referencedId } = require('../helpers/fhirReadHelper');
|
|
7
7
|
const { ROUTING_ASSESSMENT_SLUG, ROUTING_DISPOSITION_SLUG } = require('../constants/projectionSlugs');
|
|
8
8
|
const { ROUTING_EXT } = require('../constants/extensionSlugs');
|
|
9
9
|
const { RoutingDecision } = require('../../shared/dtos/RoutingDecision');
|
|
@@ -53,20 +53,17 @@ const DISPOSITION_CODECS = {
|
|
|
53
53
|
},
|
|
54
54
|
task: {
|
|
55
55
|
resourceType: 'Task',
|
|
56
|
-
build: (d) => {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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,
|
|
@@ -171,6 +168,7 @@ function assessmentExtensions(assessment, { decisionId, committedAt, disposition
|
|
|
171
168
|
if (disposition.primaryCaseId) {
|
|
172
169
|
out.push(extension(ROUTING_EXT.PRIMARY_CASE_ID, { valueString: disposition.primaryCaseId }));
|
|
173
170
|
}
|
|
171
|
+
if (assessment.previousId) out.push(extension(ROUTING_EXT.PREVIOUS_ID, { valueString: assessment.previousId }));
|
|
174
172
|
for (const deferred of disposition.deferredItems) {
|
|
175
173
|
out.push(
|
|
176
174
|
extension(ROUTING_EXT.DEFERRED_ITEM, { valueString: deferred.item }),
|
|
@@ -240,13 +238,13 @@ function reconstructAssessment(resource, patientId, turnId) {
|
|
|
240
238
|
patientId,
|
|
241
239
|
turnId,
|
|
242
240
|
performedAt: resource.effectiveDateTime,
|
|
243
|
-
previousId:
|
|
241
|
+
previousId: referencedId(resource, ROUTING_EXT.PREVIOUS_ID, resource.previous?.reference),
|
|
244
242
|
summary: resource.summary || '',
|
|
245
243
|
notes: (resource.note || []).map((note) => note.text),
|
|
246
244
|
findings: (resource.finding || []).map((finding) => {
|
|
247
245
|
const findingExt = extMap(finding);
|
|
248
246
|
return {
|
|
249
|
-
caseId:
|
|
247
|
+
caseId: referencedId(finding, ROUTING_EXT.FINDING_CASE_ID, finding.item?.reference),
|
|
250
248
|
clinicalReasoning: findingExt[ROUTING_EXT.FINDING_REASONING] || '',
|
|
251
249
|
decisionAction: findingExt[ROUTING_EXT.FINDING_ACTION] || '',
|
|
252
250
|
};
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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 {
|
|
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 {
|
|
2
|
-
const {
|
|
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
|
|
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/package.json
CHANGED
|
@@ -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 };
|