@tellescope/sdk 1.253.1 → 1.254.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.
- package/lib/cjs/sdk.d.ts +4 -0
- package/lib/cjs/sdk.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/_tmp_bc_verify.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/_tmp_blank_status_verify.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/_tmp_followup_verify.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/_tmp_nonmatch_verify.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/ai_decision_summary_config.test.d.ts +16 -0
- package/lib/cjs/tests/api_tests/ai_decision_summary_config.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/ai_decision_summary_config.test.js +187 -0
- package/lib/cjs/tests/api_tests/ai_decision_summary_config.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/field_redaction.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/field_redaction.test.js +112 -99
- package/lib/cjs/tests/api_tests/field_redaction.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/journey_error_branching.test.js +7 -2
- package/lib/cjs/tests/api_tests/journey_error_branching.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/mdi_case_offerings.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/mdi_case_offerings.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/mdi_case_offerings.test.js +443 -0
- package/lib/cjs/tests/api_tests/mdi_case_offerings.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/resource_access_tags.test.d.ts +22 -0
- package/lib/cjs/tests/api_tests/resource_access_tags.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/resource_access_tags.test.js +488 -0
- package/lib/cjs/tests/api_tests/resource_access_tags.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/F-0116-F-0119-enduser-cross-create.test.d.ts +21 -0
- package/lib/cjs/tests/api_tests/security/F-0116-F-0119-enduser-cross-create.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/F-0116-F-0119-enduser-cross-create.test.js +362 -0
- package/lib/cjs/tests/api_tests/security/F-0116-F-0119-enduser-cross-create.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/F-0145-globalunique-leak.test.d.ts +20 -0
- package/lib/cjs/tests/api_tests/security/F-0145-globalunique-leak.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/F-0145-globalunique-leak.test.js +139 -0
- package/lib/cjs/tests/api_tests/security/F-0145-globalunique-leak.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/F-0151-load-inbox-redaction.test.d.ts +18 -0
- package/lib/cjs/tests/api_tests/security/F-0151-load-inbox-redaction.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/F-0151-load-inbox-redaction.test.js +156 -0
- package/lib/cjs/tests/api_tests/security/F-0151-load-inbox-redaction.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/F-0155-webhook-timeout.test.d.ts +20 -0
- package/lib/cjs/tests/api_tests/security/F-0155-webhook-timeout.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/F-0155-webhook-timeout.test.js +157 -0
- package/lib/cjs/tests/api_tests/security/F-0155-webhook-timeout.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/time_tracks.test.d.ts +8 -0
- package/lib/cjs/tests/api_tests/time_tracks.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/time_tracks.test.js +638 -1
- package/lib/cjs/tests/api_tests/time_tracks.test.js.map +1 -1
- package/lib/cjs/tests/setup.d.ts.map +1 -1
- package/lib/cjs/tests/setup.js +20 -1
- package/lib/cjs/tests/setup.js.map +1 -1
- package/lib/cjs/tests/tests.d.ts.map +1 -1
- package/lib/cjs/tests/tests.js +606 -178
- package/lib/cjs/tests/tests.js.map +1 -1
- package/lib/esm/sdk.d.ts +6 -2
- package/lib/esm/sdk.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/_tmp_bc_verify.test.d.ts +2 -0
- package/lib/esm/tests/api_tests/_tmp_bc_verify.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/_tmp_bc_verify.test.js +120 -0
- package/lib/esm/tests/api_tests/_tmp_bc_verify.test.js.map +1 -0
- package/lib/esm/tests/api_tests/_tmp_blank_status_verify.test.d.ts +2 -0
- package/lib/esm/tests/api_tests/_tmp_blank_status_verify.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/_tmp_blank_status_verify.test.js +200 -0
- package/lib/esm/tests/api_tests/_tmp_blank_status_verify.test.js.map +1 -0
- package/lib/esm/tests/api_tests/_tmp_followup_verify.test.d.ts +2 -0
- package/lib/esm/tests/api_tests/_tmp_followup_verify.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/_tmp_followup_verify.test.js +194 -0
- package/lib/esm/tests/api_tests/_tmp_followup_verify.test.js.map +1 -0
- package/lib/esm/tests/api_tests/_tmp_nonmatch_verify.test.d.ts +2 -0
- package/lib/esm/tests/api_tests/_tmp_nonmatch_verify.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/_tmp_nonmatch_verify.test.js +115 -0
- package/lib/esm/tests/api_tests/_tmp_nonmatch_verify.test.js.map +1 -0
- package/lib/esm/tests/api_tests/ai_decision_summary_config.test.d.ts +16 -0
- package/lib/esm/tests/api_tests/ai_decision_summary_config.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/ai_decision_summary_config.test.js +183 -0
- package/lib/esm/tests/api_tests/ai_decision_summary_config.test.js.map +1 -0
- package/lib/esm/tests/api_tests/field_redaction.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/field_redaction.test.js +112 -99
- package/lib/esm/tests/api_tests/field_redaction.test.js.map +1 -1
- package/lib/esm/tests/api_tests/journey_error_branching.test.js +7 -2
- package/lib/esm/tests/api_tests/journey_error_branching.test.js.map +1 -1
- package/lib/esm/tests/api_tests/mdi_case_offerings.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/mdi_case_offerings.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/mdi_case_offerings.test.js +439 -0
- package/lib/esm/tests/api_tests/mdi_case_offerings.test.js.map +1 -0
- package/lib/esm/tests/api_tests/resource_access_tags.test.d.ts +22 -0
- package/lib/esm/tests/api_tests/resource_access_tags.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/resource_access_tags.test.js +484 -0
- package/lib/esm/tests/api_tests/resource_access_tags.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/F-0116-F-0119-enduser-cross-create.test.d.ts +21 -0
- package/lib/esm/tests/api_tests/security/F-0116-F-0119-enduser-cross-create.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/F-0116-F-0119-enduser-cross-create.test.js +358 -0
- package/lib/esm/tests/api_tests/security/F-0116-F-0119-enduser-cross-create.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/F-0145-globalunique-leak.test.d.ts +20 -0
- package/lib/esm/tests/api_tests/security/F-0145-globalunique-leak.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/F-0145-globalunique-leak.test.js +135 -0
- package/lib/esm/tests/api_tests/security/F-0145-globalunique-leak.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/F-0151-load-inbox-redaction.test.d.ts +18 -0
- package/lib/esm/tests/api_tests/security/F-0151-load-inbox-redaction.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/F-0151-load-inbox-redaction.test.js +152 -0
- package/lib/esm/tests/api_tests/security/F-0151-load-inbox-redaction.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/F-0155-webhook-timeout.test.d.ts +20 -0
- package/lib/esm/tests/api_tests/security/F-0155-webhook-timeout.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/F-0155-webhook-timeout.test.js +150 -0
- package/lib/esm/tests/api_tests/security/F-0155-webhook-timeout.test.js.map +1 -0
- package/lib/esm/tests/api_tests/time_tracks.test.d.ts +8 -0
- package/lib/esm/tests/api_tests/time_tracks.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/time_tracks.test.js +635 -0
- package/lib/esm/tests/api_tests/time_tracks.test.js.map +1 -1
- package/lib/esm/tests/setup.d.ts.map +1 -1
- package/lib/esm/tests/setup.js +20 -1
- package/lib/esm/tests/setup.js.map +1 -1
- package/lib/esm/tests/tests.d.ts.map +1 -1
- package/lib/esm/tests/tests.js +607 -179
- package/lib/esm/tests/tests.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +10 -10
- package/src/tests/api_tests/field_redaction.test.ts +13 -0
- package/src/tests/api_tests/journey_error_branching.test.ts +5 -1
- package/src/tests/api_tests/mdi_case_offerings.test.ts +414 -0
- package/src/tests/api_tests/resource_access_tags.test.ts +303 -0
- package/src/tests/api_tests/security/F-0116-F-0119-enduser-cross-create.test.ts +221 -0
- package/src/tests/api_tests/security/F-0145-globalunique-leak.test.ts +78 -0
- package/src/tests/api_tests/security/F-0151-load-inbox-redaction.test.ts +81 -0
- package/src/tests/api_tests/security/F-0155-webhook-timeout.test.ts +85 -0
- package/src/tests/api_tests/time_tracks.test.ts +420 -0
- package/src/tests/setup.ts +9 -0
- package/src/tests/tests.ts +308 -5
- package/test_generated.pdf +0 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
require('source-map-support').install();
|
|
2
|
+
|
|
3
|
+
import { Session } from "../../sdk"
|
|
4
|
+
import {
|
|
5
|
+
async_test,
|
|
6
|
+
log_header,
|
|
7
|
+
} from "@tellescope/testing"
|
|
8
|
+
import { setup_tests } from "../setup"
|
|
9
|
+
import { evaluate_conditional_logic, evaluate_string_field_comparison } from "@tellescope/utilities"
|
|
10
|
+
import { CompoundFilter } from "@tellescope/types-models"
|
|
11
|
+
|
|
12
|
+
const host = process.env.API_URL || 'http://localhost:8080' as const
|
|
13
|
+
|
|
14
|
+
type MDIOffering = { offering_id: string, conditions?: CompoundFilter<string> }
|
|
15
|
+
|
|
16
|
+
// Local replica of the API's resolve_case_offerings (md_integrations.ts), built
|
|
17
|
+
// from the same shared @tellescope/utilities helpers it uses, so we can assert
|
|
18
|
+
// the resolution behavior from the SDK test harness.
|
|
19
|
+
const is_empty_conditions = (conditions: CompoundFilter<string>): boolean => {
|
|
20
|
+
if (!conditions || !Object.keys(conditions).length) return true
|
|
21
|
+
const condition = (conditions as { condition?: Record<string, unknown> }).condition
|
|
22
|
+
if (condition !== undefined) return !Object.keys(condition).length
|
|
23
|
+
return false
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const resolve_case_offerings = (
|
|
27
|
+
offerings: MDIOffering[],
|
|
28
|
+
responses: { externalId?: string, value?: string }[],
|
|
29
|
+
): MDIOffering[] => (
|
|
30
|
+
offerings.filter(o => {
|
|
31
|
+
if (!o.conditions || is_empty_conditions(o.conditions)) return true
|
|
32
|
+
return evaluate_conditional_logic(o.conditions, (key, value) =>
|
|
33
|
+
evaluate_string_field_comparison(responses.find(r => r.externalId === key)?.value, value)
|
|
34
|
+
)
|
|
35
|
+
})
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
// Local replicas of the API's patient-mapping helpers (md_integrations.ts).
|
|
39
|
+
// SDK tests cannot import the private API package, so we mirror the conversion
|
|
40
|
+
// helpers + exclusion set here to assert the same behavior.
|
|
41
|
+
type GenericQuantityWithUnit = { value: number | string, unit: string }
|
|
42
|
+
|
|
43
|
+
const to_number = (v: unknown): number | undefined => {
|
|
44
|
+
const n = typeof v === 'number' ? v : parseFloat(String(v ?? ''))
|
|
45
|
+
return Number.isFinite(n) ? n : undefined
|
|
46
|
+
}
|
|
47
|
+
const enduser_height_to_cm = (h?: GenericQuantityWithUnit): number | undefined => {
|
|
48
|
+
const inches = to_number(h?.value); if (inches === undefined) return undefined
|
|
49
|
+
return Math.round(inches * 2.54)
|
|
50
|
+
}
|
|
51
|
+
const enduser_weight_to_kg = (w?: GenericQuantityWithUnit): number | undefined => {
|
|
52
|
+
const lbs = to_number(w?.value); if (lbs === undefined) return undefined
|
|
53
|
+
return Math.round(lbs * 0.45359237 * 100) / 100
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const PATIENT_MAPPED_EXTERNAL_IDS = new Set([
|
|
57
|
+
'fname', 'first_name', 'lname', 'last_name',
|
|
58
|
+
'email', 'dateOfBirth', 'gender', 'phone', 'address',
|
|
59
|
+
'height', 'weight',
|
|
60
|
+
'allergies', 'current_medications', 'medical_conditions', 'special_necessities', 'pregnancy',
|
|
61
|
+
'intro_video_id',
|
|
62
|
+
])
|
|
63
|
+
|
|
64
|
+
// Mirrors the externalId exclusion step in form_response_to_case_questions:
|
|
65
|
+
// patient-mapped fields are dropped, everything else survives.
|
|
66
|
+
const case_question_external_ids = (responses: { externalId?: string }[]): string[] => (
|
|
67
|
+
responses
|
|
68
|
+
.filter(r => !(r.externalId && PATIENT_MAPPED_EXTERNAL_IDS.has(r.externalId)))
|
|
69
|
+
.map(r => r.externalId)
|
|
70
|
+
.filter((id): id is string => id !== undefined)
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
// Local replica of form_response_to_case_questions' displayed_options logic
|
|
74
|
+
// (md_integrations.ts). SDK tests cannot import the private API package, so we
|
|
75
|
+
// mirror the field lookup (_id first, externalId fallback) + the multiple_choice
|
|
76
|
+
// / Dropdown gate, and assert displayed_options is populated only when applicable.
|
|
77
|
+
type ReplicaField = { _id: string, externalId?: string, options?: { choices?: string[] } }
|
|
78
|
+
type ReplicaResponse = { fieldId?: string, externalId?: string, answer: { type?: string } }
|
|
79
|
+
|
|
80
|
+
const resolve_displayed_options = (
|
|
81
|
+
response: ReplicaResponse,
|
|
82
|
+
formFields: ReplicaField[] = [],
|
|
83
|
+
): string[] | undefined => {
|
|
84
|
+
const field = (
|
|
85
|
+
formFields.find(f => f._id.toString() === response.fieldId)
|
|
86
|
+
|| formFields.find(f => response.externalId && f.externalId === response.externalId)
|
|
87
|
+
)
|
|
88
|
+
return (
|
|
89
|
+
(response.answer.type === 'multiple_choice' || response.answer.type === 'Dropdown')
|
|
90
|
+
&& field?.options?.choices?.length
|
|
91
|
+
) ? field.options.choices : undefined
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export const mdi_case_offerings_tests = async ({ sdk, sdkNonAdmin }: { sdk: Session, sdkNonAdmin: Session }) => {
|
|
95
|
+
log_header("MDI Case Offerings Tests")
|
|
96
|
+
|
|
97
|
+
let testFormId: string | undefined
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
// --- Schema round-trip tests ---
|
|
101
|
+
|
|
102
|
+
await async_test(
|
|
103
|
+
"Create form with mdiCaseOfferings mixing unconditional and conditional entries",
|
|
104
|
+
async () => {
|
|
105
|
+
const offerings: MDIOffering[] = [
|
|
106
|
+
{ offering_id: "offering-always" }, // unconditional
|
|
107
|
+
{ offering_id: "offering-ca", conditions: { condition: { state: "CA" } } },
|
|
108
|
+
{
|
|
109
|
+
offering_id: "offering-ca-glp",
|
|
110
|
+
conditions: { $and: [{ condition: { state: "CA" } }, { condition: { med_type: "weightLoss" } }] },
|
|
111
|
+
},
|
|
112
|
+
]
|
|
113
|
+
|
|
114
|
+
const form = await sdk.api.forms.createOne({
|
|
115
|
+
title: 'MDI Case Offerings Test Form',
|
|
116
|
+
mdiCaseOfferings: offerings,
|
|
117
|
+
})
|
|
118
|
+
testFormId = form.id
|
|
119
|
+
|
|
120
|
+
const fetched = await sdk.api.forms.getOne(form.id)
|
|
121
|
+
return (
|
|
122
|
+
fetched.mdiCaseOfferings?.length === 3
|
|
123
|
+
&& fetched.mdiCaseOfferings[0].offering_id === "offering-always"
|
|
124
|
+
&& fetched.mdiCaseOfferings[0].conditions === undefined
|
|
125
|
+
&& !!fetched.mdiCaseOfferings[1].conditions?.condition
|
|
126
|
+
&& !!fetched.mdiCaseOfferings[2].conditions?.$and
|
|
127
|
+
)
|
|
128
|
+
},
|
|
129
|
+
{ expectedResult: true }
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
await async_test(
|
|
133
|
+
"Backwards-compat: existing flat { offering_id } config still validates",
|
|
134
|
+
async () => {
|
|
135
|
+
if (!testFormId) throw new Error("No test form")
|
|
136
|
+
|
|
137
|
+
// mirrors a pre-existing config with no conditions
|
|
138
|
+
const flat: MDIOffering[] = [
|
|
139
|
+
{ offering_id: "legacy-1" },
|
|
140
|
+
{ offering_id: "legacy-2" },
|
|
141
|
+
]
|
|
142
|
+
await sdk.api.forms.updateOne(testFormId, { mdiCaseOfferings: flat }, { replaceObjectFields: true })
|
|
143
|
+
const fetched = await sdk.api.forms.getOne(testFormId)
|
|
144
|
+
return (
|
|
145
|
+
fetched.mdiCaseOfferings?.length === 2
|
|
146
|
+
&& fetched.mdiCaseOfferings.every(o => o.conditions === undefined)
|
|
147
|
+
)
|
|
148
|
+
},
|
|
149
|
+
{ expectedResult: true }
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
await async_test(
|
|
153
|
+
"Save nested CompoundFilter conditions on an offering and read back",
|
|
154
|
+
async () => {
|
|
155
|
+
if (!testFormId) throw new Error("No test form")
|
|
156
|
+
|
|
157
|
+
const nested: MDIOffering[] = [
|
|
158
|
+
{
|
|
159
|
+
offering_id: "offering-nested",
|
|
160
|
+
conditions: {
|
|
161
|
+
$and: [
|
|
162
|
+
{ $or: [{ condition: { state: "CA" } }, { condition: { state: "NY" } }] },
|
|
163
|
+
{ condition: { med_type: "weightLoss" } },
|
|
164
|
+
],
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
]
|
|
168
|
+
await sdk.api.forms.updateOne(testFormId, { mdiCaseOfferings: nested }, { replaceObjectFields: true })
|
|
169
|
+
const fetched = await sdk.api.forms.getOne(testFormId)
|
|
170
|
+
return (
|
|
171
|
+
fetched.mdiCaseOfferings?.length === 1
|
|
172
|
+
&& !!fetched.mdiCaseOfferings[0].conditions?.$and
|
|
173
|
+
)
|
|
174
|
+
},
|
|
175
|
+
{ expectedResult: true }
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
// --- resolve_case_offerings behavior tests ---
|
|
179
|
+
|
|
180
|
+
const MIXED: MDIOffering[] = [
|
|
181
|
+
{ offering_id: "always" }, // unconditional → always sent
|
|
182
|
+
{ offering_id: "ca-only", conditions: { condition: { state: "CA" } } },
|
|
183
|
+
{ offering_id: "ny-only", conditions: { condition: { state: "NY" } } },
|
|
184
|
+
]
|
|
185
|
+
|
|
186
|
+
await async_test(
|
|
187
|
+
"Resolver returns exactly the matching subset plus unconditional offerings",
|
|
188
|
+
async () => {
|
|
189
|
+
const resolved = resolve_case_offerings(MIXED, [{ externalId: "state", value: "CA" }])
|
|
190
|
+
return (
|
|
191
|
+
resolved.length === 2
|
|
192
|
+
&& resolved.some(o => o.offering_id === "always")
|
|
193
|
+
&& resolved.some(o => o.offering_id === "ca-only")
|
|
194
|
+
&& !resolved.some(o => o.offering_id === "ny-only")
|
|
195
|
+
)
|
|
196
|
+
},
|
|
197
|
+
{ expectedResult: true }
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
await async_test(
|
|
201
|
+
"Send none: when conditions match nothing, only unconditional offerings remain",
|
|
202
|
+
async () => {
|
|
203
|
+
// a config where every offering carries a condition that fails
|
|
204
|
+
const allConditional: MDIOffering[] = [
|
|
205
|
+
{ offering_id: "ca-only", conditions: { condition: { state: "CA" } } },
|
|
206
|
+
{ offering_id: "ny-only", conditions: { condition: { state: "NY" } } },
|
|
207
|
+
]
|
|
208
|
+
const resolved = resolve_case_offerings(allConditional, [{ externalId: "state", value: "TX" }])
|
|
209
|
+
return resolved.length === 0
|
|
210
|
+
},
|
|
211
|
+
{ expectedResult: true }
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
await async_test(
|
|
215
|
+
"Backwards compat: all-unconditional config sends everything",
|
|
216
|
+
async () => {
|
|
217
|
+
const allUnconditional: MDIOffering[] = [
|
|
218
|
+
{ offering_id: "a" },
|
|
219
|
+
{ offering_id: "b" },
|
|
220
|
+
{ offering_id: "c" },
|
|
221
|
+
]
|
|
222
|
+
const resolved = resolve_case_offerings(allUnconditional, [{ externalId: "state", value: "TX" }])
|
|
223
|
+
return resolved.length === 3
|
|
224
|
+
},
|
|
225
|
+
{ expectedResult: true }
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
await async_test(
|
|
229
|
+
"Empty/blank conditions object is treated as unconditional (always sent)",
|
|
230
|
+
async () => {
|
|
231
|
+
const withBlank: MDIOffering[] = [
|
|
232
|
+
{ offering_id: "blank", conditions: { condition: {} } },
|
|
233
|
+
]
|
|
234
|
+
// even with no matching response, the blank-condition offering is sent
|
|
235
|
+
const resolved = resolve_case_offerings(withBlank, [])
|
|
236
|
+
return resolved.length === 1 && resolved[0].offering_id === "blank"
|
|
237
|
+
},
|
|
238
|
+
{ expectedResult: true }
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
await async_test(
|
|
242
|
+
"$or conditions: offering sent when any branch matches",
|
|
243
|
+
async () => {
|
|
244
|
+
const offerings: MDIOffering[] = [
|
|
245
|
+
{ offering_id: "ca-or-ny", conditions: { $or: [{ condition: { state: "CA" } }, { condition: { state: "NY" } }] } },
|
|
246
|
+
]
|
|
247
|
+
const resolved = resolve_case_offerings(offerings, [{ externalId: "state", value: "NY" }])
|
|
248
|
+
return resolved.length === 1
|
|
249
|
+
},
|
|
250
|
+
{ expectedResult: true }
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
await async_test(
|
|
254
|
+
"$contains operator narrows offerings by substring match",
|
|
255
|
+
async () => {
|
|
256
|
+
const offerings: MDIOffering[] = [
|
|
257
|
+
{ offering_id: "glp", conditions: { condition: { meds: { $contains: "GLP" } as any } } },
|
|
258
|
+
]
|
|
259
|
+
const matches = resolve_case_offerings(offerings, [{ externalId: "meds", value: "GLP-1 agonist" }])
|
|
260
|
+
const misses = resolve_case_offerings(offerings, [{ externalId: "meds", value: "Semaglutide" }])
|
|
261
|
+
return matches.length === 1 && misses.length === 0
|
|
262
|
+
},
|
|
263
|
+
{ expectedResult: true }
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
// --- height/weight conversion tests ---
|
|
267
|
+
|
|
268
|
+
await async_test(
|
|
269
|
+
"enduser_height_to_cm converts Inches → integer centimeters",
|
|
270
|
+
async () => (
|
|
271
|
+
enduser_height_to_cm({ value: 70, unit: 'Inches' }) === 178
|
|
272
|
+
&& enduser_height_to_cm({ value: '70', unit: 'Inches' }) === 178 // string value parsed
|
|
273
|
+
&& enduser_height_to_cm(undefined) === undefined // absent → omitted
|
|
274
|
+
&& enduser_height_to_cm({ value: '', unit: 'Inches' }) === undefined
|
|
275
|
+
),
|
|
276
|
+
{ expectedResult: true }
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
await async_test(
|
|
280
|
+
"enduser_weight_to_kg converts Pounds → kilograms (float, 2dp)",
|
|
281
|
+
async () => (
|
|
282
|
+
enduser_weight_to_kg({ value: 200, unit: 'Pounds' }) === 90.72
|
|
283
|
+
&& enduser_weight_to_kg({ value: '200', unit: 'Pounds' }) === 90.72 // string value parsed
|
|
284
|
+
&& enduser_weight_to_kg(undefined) === undefined // absent → omitted
|
|
285
|
+
&& enduser_weight_to_kg({ value: 'not-a-number', unit: 'Pounds' }) === undefined
|
|
286
|
+
),
|
|
287
|
+
{ expectedResult: true }
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
// --- case_questions exclusion tests ---
|
|
291
|
+
|
|
292
|
+
await async_test(
|
|
293
|
+
"case_questions exclude patient-mapped fields but keep non-mapped clinical answers",
|
|
294
|
+
async () => {
|
|
295
|
+
const responses = [
|
|
296
|
+
{ externalId: 'fname' },
|
|
297
|
+
{ externalId: 'email' },
|
|
298
|
+
{ externalId: 'dateOfBirth' },
|
|
299
|
+
{ externalId: 'gender' },
|
|
300
|
+
{ externalId: 'phone' },
|
|
301
|
+
{ externalId: 'address' },
|
|
302
|
+
{ externalId: 'height' },
|
|
303
|
+
{ externalId: 'weight' },
|
|
304
|
+
{ externalId: 'allergies' },
|
|
305
|
+
{ externalId: 'special_necessities' },
|
|
306
|
+
{ externalId: 'symptoms' }, // non-mapped clinical field → survives
|
|
307
|
+
{ externalId: 'duration' }, // non-mapped clinical field → survives
|
|
308
|
+
]
|
|
309
|
+
const kept = case_question_external_ids(responses)
|
|
310
|
+
const excluded = ['fname', 'email', 'dateOfBirth', 'gender', 'phone', 'address', 'height', 'weight', 'allergies', 'special_necessities']
|
|
311
|
+
return (
|
|
312
|
+
kept.length === 2
|
|
313
|
+
&& kept.includes('symptoms')
|
|
314
|
+
&& kept.includes('duration')
|
|
315
|
+
&& excluded.every(id => !kept.includes(id))
|
|
316
|
+
)
|
|
317
|
+
},
|
|
318
|
+
{ expectedResult: true }
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
// --- displayed_options tests ---
|
|
322
|
+
|
|
323
|
+
await async_test(
|
|
324
|
+
"multiple_choice response + matching field by _id → displayed_options equals field choices",
|
|
325
|
+
async () => {
|
|
326
|
+
const fields: ReplicaField[] = [
|
|
327
|
+
{ _id: 'field-1', options: { choices: ['Yes', 'No', 'Maybe'] } },
|
|
328
|
+
]
|
|
329
|
+
const response: ReplicaResponse = { fieldId: 'field-1', answer: { type: 'multiple_choice' } }
|
|
330
|
+
const opts = resolve_displayed_options(response, fields)
|
|
331
|
+
return (
|
|
332
|
+
!!opts
|
|
333
|
+
&& opts.length === 3
|
|
334
|
+
&& opts[0] === 'Yes' && opts[1] === 'No' && opts[2] === 'Maybe'
|
|
335
|
+
)
|
|
336
|
+
},
|
|
337
|
+
{ expectedResult: true }
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
await async_test(
|
|
341
|
+
"Dropdown response + matching field by externalId → displayed_options equals field choices",
|
|
342
|
+
async () => {
|
|
343
|
+
const fields: ReplicaField[] = [
|
|
344
|
+
{ _id: 'field-2', externalId: 'state', options: { choices: ['CA', 'NY', 'TX'] } },
|
|
345
|
+
]
|
|
346
|
+
// fieldId does not match any field; externalId fallback resolves the field
|
|
347
|
+
const response: ReplicaResponse = { fieldId: '', externalId: 'state', answer: { type: 'Dropdown' } }
|
|
348
|
+
const opts = resolve_displayed_options(response, fields)
|
|
349
|
+
return !!opts && opts.length === 3 && opts.join(',') === 'CA,NY,TX'
|
|
350
|
+
},
|
|
351
|
+
{ expectedResult: true }
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
await async_test(
|
|
355
|
+
"string/number response → displayed_options undefined even with field choices present",
|
|
356
|
+
async () => {
|
|
357
|
+
const fields: ReplicaField[] = [
|
|
358
|
+
{ _id: 'field-3', options: { choices: ['Yes', 'No'] } },
|
|
359
|
+
]
|
|
360
|
+
const stringResponse: ReplicaResponse = { fieldId: 'field-3', answer: { type: 'string' } }
|
|
361
|
+
const numberResponse: ReplicaResponse = { fieldId: 'field-3', answer: { type: 'number' } }
|
|
362
|
+
return (
|
|
363
|
+
resolve_displayed_options(stringResponse, fields) === undefined
|
|
364
|
+
&& resolve_displayed_options(numberResponse, fields) === undefined
|
|
365
|
+
)
|
|
366
|
+
},
|
|
367
|
+
{ expectedResult: true }
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
await async_test(
|
|
371
|
+
"multiple_choice response with no matching field → displayed_options undefined (question still kept)",
|
|
372
|
+
async () => {
|
|
373
|
+
const fields: ReplicaField[] = [
|
|
374
|
+
{ _id: 'field-4', externalId: 'other', options: { choices: ['A', 'B'] } },
|
|
375
|
+
]
|
|
376
|
+
// neither fieldId nor externalId matches any field
|
|
377
|
+
const noMatch: ReplicaResponse = { fieldId: 'field-unknown', externalId: 'unknown', answer: { type: 'multiple_choice' } }
|
|
378
|
+
// matching field but empty choices → also undefined
|
|
379
|
+
const emptyChoices: ReplicaResponse = { fieldId: 'field-5', answer: { type: 'multiple_choice' } }
|
|
380
|
+
const fieldsEmpty: ReplicaField[] = [{ _id: 'field-5', options: { choices: [] } }]
|
|
381
|
+
return (
|
|
382
|
+
resolve_displayed_options(noMatch, fields) === undefined
|
|
383
|
+
&& resolve_displayed_options(emptyChoices, fieldsEmpty) === undefined
|
|
384
|
+
)
|
|
385
|
+
},
|
|
386
|
+
{ expectedResult: true }
|
|
387
|
+
)
|
|
388
|
+
} finally {
|
|
389
|
+
if (testFormId) {
|
|
390
|
+
await sdk.api.forms.deleteOne(testFormId).catch(console.error)
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
if (require.main === module) {
|
|
396
|
+
console.log(`Using API URL: ${host}`)
|
|
397
|
+
const sdk = new Session({ host })
|
|
398
|
+
const sdkNonAdmin = new Session({ host })
|
|
399
|
+
|
|
400
|
+
const runTests = async () => {
|
|
401
|
+
await setup_tests(sdk, sdkNonAdmin)
|
|
402
|
+
await mdi_case_offerings_tests({ sdk, sdkNonAdmin })
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
runTests()
|
|
406
|
+
.then(() => {
|
|
407
|
+
console.log("MDI case offerings test suite completed successfully")
|
|
408
|
+
process.exit(0)
|
|
409
|
+
})
|
|
410
|
+
.catch((error) => {
|
|
411
|
+
console.error("MDI case offerings test suite failed:", error)
|
|
412
|
+
process.exit(1)
|
|
413
|
+
})
|
|
414
|
+
}
|