@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.
Files changed (124) hide show
  1. package/lib/cjs/sdk.d.ts +4 -0
  2. package/lib/cjs/sdk.d.ts.map +1 -1
  3. package/lib/cjs/tests/api_tests/_tmp_bc_verify.test.d.ts.map +1 -0
  4. package/lib/cjs/tests/api_tests/_tmp_blank_status_verify.test.d.ts.map +1 -0
  5. package/lib/cjs/tests/api_tests/_tmp_followup_verify.test.d.ts.map +1 -0
  6. package/lib/cjs/tests/api_tests/_tmp_nonmatch_verify.test.d.ts.map +1 -0
  7. package/lib/cjs/tests/api_tests/ai_decision_summary_config.test.d.ts +16 -0
  8. package/lib/cjs/tests/api_tests/ai_decision_summary_config.test.d.ts.map +1 -0
  9. package/lib/cjs/tests/api_tests/ai_decision_summary_config.test.js +187 -0
  10. package/lib/cjs/tests/api_tests/ai_decision_summary_config.test.js.map +1 -0
  11. package/lib/cjs/tests/api_tests/field_redaction.test.d.ts.map +1 -1
  12. package/lib/cjs/tests/api_tests/field_redaction.test.js +112 -99
  13. package/lib/cjs/tests/api_tests/field_redaction.test.js.map +1 -1
  14. package/lib/cjs/tests/api_tests/journey_error_branching.test.js +7 -2
  15. package/lib/cjs/tests/api_tests/journey_error_branching.test.js.map +1 -1
  16. package/lib/cjs/tests/api_tests/mdi_case_offerings.test.d.ts +6 -0
  17. package/lib/cjs/tests/api_tests/mdi_case_offerings.test.d.ts.map +1 -0
  18. package/lib/cjs/tests/api_tests/mdi_case_offerings.test.js +443 -0
  19. package/lib/cjs/tests/api_tests/mdi_case_offerings.test.js.map +1 -0
  20. package/lib/cjs/tests/api_tests/resource_access_tags.test.d.ts +22 -0
  21. package/lib/cjs/tests/api_tests/resource_access_tags.test.d.ts.map +1 -0
  22. package/lib/cjs/tests/api_tests/resource_access_tags.test.js +488 -0
  23. package/lib/cjs/tests/api_tests/resource_access_tags.test.js.map +1 -0
  24. package/lib/cjs/tests/api_tests/security/F-0116-F-0119-enduser-cross-create.test.d.ts +21 -0
  25. package/lib/cjs/tests/api_tests/security/F-0116-F-0119-enduser-cross-create.test.d.ts.map +1 -0
  26. package/lib/cjs/tests/api_tests/security/F-0116-F-0119-enduser-cross-create.test.js +362 -0
  27. package/lib/cjs/tests/api_tests/security/F-0116-F-0119-enduser-cross-create.test.js.map +1 -0
  28. package/lib/cjs/tests/api_tests/security/F-0145-globalunique-leak.test.d.ts +20 -0
  29. package/lib/cjs/tests/api_tests/security/F-0145-globalunique-leak.test.d.ts.map +1 -0
  30. package/lib/cjs/tests/api_tests/security/F-0145-globalunique-leak.test.js +139 -0
  31. package/lib/cjs/tests/api_tests/security/F-0145-globalunique-leak.test.js.map +1 -0
  32. package/lib/cjs/tests/api_tests/security/F-0151-load-inbox-redaction.test.d.ts +18 -0
  33. package/lib/cjs/tests/api_tests/security/F-0151-load-inbox-redaction.test.d.ts.map +1 -0
  34. package/lib/cjs/tests/api_tests/security/F-0151-load-inbox-redaction.test.js +156 -0
  35. package/lib/cjs/tests/api_tests/security/F-0151-load-inbox-redaction.test.js.map +1 -0
  36. package/lib/cjs/tests/api_tests/security/F-0155-webhook-timeout.test.d.ts +20 -0
  37. package/lib/cjs/tests/api_tests/security/F-0155-webhook-timeout.test.d.ts.map +1 -0
  38. package/lib/cjs/tests/api_tests/security/F-0155-webhook-timeout.test.js +157 -0
  39. package/lib/cjs/tests/api_tests/security/F-0155-webhook-timeout.test.js.map +1 -0
  40. package/lib/cjs/tests/api_tests/time_tracks.test.d.ts +8 -0
  41. package/lib/cjs/tests/api_tests/time_tracks.test.d.ts.map +1 -1
  42. package/lib/cjs/tests/api_tests/time_tracks.test.js +638 -1
  43. package/lib/cjs/tests/api_tests/time_tracks.test.js.map +1 -1
  44. package/lib/cjs/tests/setup.d.ts.map +1 -1
  45. package/lib/cjs/tests/setup.js +20 -1
  46. package/lib/cjs/tests/setup.js.map +1 -1
  47. package/lib/cjs/tests/tests.d.ts.map +1 -1
  48. package/lib/cjs/tests/tests.js +606 -178
  49. package/lib/cjs/tests/tests.js.map +1 -1
  50. package/lib/esm/sdk.d.ts +6 -2
  51. package/lib/esm/sdk.d.ts.map +1 -1
  52. package/lib/esm/tests/api_tests/_tmp_bc_verify.test.d.ts +2 -0
  53. package/lib/esm/tests/api_tests/_tmp_bc_verify.test.d.ts.map +1 -0
  54. package/lib/esm/tests/api_tests/_tmp_bc_verify.test.js +120 -0
  55. package/lib/esm/tests/api_tests/_tmp_bc_verify.test.js.map +1 -0
  56. package/lib/esm/tests/api_tests/_tmp_blank_status_verify.test.d.ts +2 -0
  57. package/lib/esm/tests/api_tests/_tmp_blank_status_verify.test.d.ts.map +1 -0
  58. package/lib/esm/tests/api_tests/_tmp_blank_status_verify.test.js +200 -0
  59. package/lib/esm/tests/api_tests/_tmp_blank_status_verify.test.js.map +1 -0
  60. package/lib/esm/tests/api_tests/_tmp_followup_verify.test.d.ts +2 -0
  61. package/lib/esm/tests/api_tests/_tmp_followup_verify.test.d.ts.map +1 -0
  62. package/lib/esm/tests/api_tests/_tmp_followup_verify.test.js +194 -0
  63. package/lib/esm/tests/api_tests/_tmp_followup_verify.test.js.map +1 -0
  64. package/lib/esm/tests/api_tests/_tmp_nonmatch_verify.test.d.ts +2 -0
  65. package/lib/esm/tests/api_tests/_tmp_nonmatch_verify.test.d.ts.map +1 -0
  66. package/lib/esm/tests/api_tests/_tmp_nonmatch_verify.test.js +115 -0
  67. package/lib/esm/tests/api_tests/_tmp_nonmatch_verify.test.js.map +1 -0
  68. package/lib/esm/tests/api_tests/ai_decision_summary_config.test.d.ts +16 -0
  69. package/lib/esm/tests/api_tests/ai_decision_summary_config.test.d.ts.map +1 -0
  70. package/lib/esm/tests/api_tests/ai_decision_summary_config.test.js +183 -0
  71. package/lib/esm/tests/api_tests/ai_decision_summary_config.test.js.map +1 -0
  72. package/lib/esm/tests/api_tests/field_redaction.test.d.ts.map +1 -1
  73. package/lib/esm/tests/api_tests/field_redaction.test.js +112 -99
  74. package/lib/esm/tests/api_tests/field_redaction.test.js.map +1 -1
  75. package/lib/esm/tests/api_tests/journey_error_branching.test.js +7 -2
  76. package/lib/esm/tests/api_tests/journey_error_branching.test.js.map +1 -1
  77. package/lib/esm/tests/api_tests/mdi_case_offerings.test.d.ts +6 -0
  78. package/lib/esm/tests/api_tests/mdi_case_offerings.test.d.ts.map +1 -0
  79. package/lib/esm/tests/api_tests/mdi_case_offerings.test.js +439 -0
  80. package/lib/esm/tests/api_tests/mdi_case_offerings.test.js.map +1 -0
  81. package/lib/esm/tests/api_tests/resource_access_tags.test.d.ts +22 -0
  82. package/lib/esm/tests/api_tests/resource_access_tags.test.d.ts.map +1 -0
  83. package/lib/esm/tests/api_tests/resource_access_tags.test.js +484 -0
  84. package/lib/esm/tests/api_tests/resource_access_tags.test.js.map +1 -0
  85. package/lib/esm/tests/api_tests/security/F-0116-F-0119-enduser-cross-create.test.d.ts +21 -0
  86. package/lib/esm/tests/api_tests/security/F-0116-F-0119-enduser-cross-create.test.d.ts.map +1 -0
  87. package/lib/esm/tests/api_tests/security/F-0116-F-0119-enduser-cross-create.test.js +358 -0
  88. package/lib/esm/tests/api_tests/security/F-0116-F-0119-enduser-cross-create.test.js.map +1 -0
  89. package/lib/esm/tests/api_tests/security/F-0145-globalunique-leak.test.d.ts +20 -0
  90. package/lib/esm/tests/api_tests/security/F-0145-globalunique-leak.test.d.ts.map +1 -0
  91. package/lib/esm/tests/api_tests/security/F-0145-globalunique-leak.test.js +135 -0
  92. package/lib/esm/tests/api_tests/security/F-0145-globalunique-leak.test.js.map +1 -0
  93. package/lib/esm/tests/api_tests/security/F-0151-load-inbox-redaction.test.d.ts +18 -0
  94. package/lib/esm/tests/api_tests/security/F-0151-load-inbox-redaction.test.d.ts.map +1 -0
  95. package/lib/esm/tests/api_tests/security/F-0151-load-inbox-redaction.test.js +152 -0
  96. package/lib/esm/tests/api_tests/security/F-0151-load-inbox-redaction.test.js.map +1 -0
  97. package/lib/esm/tests/api_tests/security/F-0155-webhook-timeout.test.d.ts +20 -0
  98. package/lib/esm/tests/api_tests/security/F-0155-webhook-timeout.test.d.ts.map +1 -0
  99. package/lib/esm/tests/api_tests/security/F-0155-webhook-timeout.test.js +150 -0
  100. package/lib/esm/tests/api_tests/security/F-0155-webhook-timeout.test.js.map +1 -0
  101. package/lib/esm/tests/api_tests/time_tracks.test.d.ts +8 -0
  102. package/lib/esm/tests/api_tests/time_tracks.test.d.ts.map +1 -1
  103. package/lib/esm/tests/api_tests/time_tracks.test.js +635 -0
  104. package/lib/esm/tests/api_tests/time_tracks.test.js.map +1 -1
  105. package/lib/esm/tests/setup.d.ts.map +1 -1
  106. package/lib/esm/tests/setup.js +20 -1
  107. package/lib/esm/tests/setup.js.map +1 -1
  108. package/lib/esm/tests/tests.d.ts.map +1 -1
  109. package/lib/esm/tests/tests.js +607 -179
  110. package/lib/esm/tests/tests.js.map +1 -1
  111. package/lib/tsconfig.tsbuildinfo +1 -1
  112. package/package.json +10 -10
  113. package/src/tests/api_tests/field_redaction.test.ts +13 -0
  114. package/src/tests/api_tests/journey_error_branching.test.ts +5 -1
  115. package/src/tests/api_tests/mdi_case_offerings.test.ts +414 -0
  116. package/src/tests/api_tests/resource_access_tags.test.ts +303 -0
  117. package/src/tests/api_tests/security/F-0116-F-0119-enduser-cross-create.test.ts +221 -0
  118. package/src/tests/api_tests/security/F-0145-globalunique-leak.test.ts +78 -0
  119. package/src/tests/api_tests/security/F-0151-load-inbox-redaction.test.ts +81 -0
  120. package/src/tests/api_tests/security/F-0155-webhook-timeout.test.ts +85 -0
  121. package/src/tests/api_tests/time_tracks.test.ts +420 -0
  122. package/src/tests/setup.ts +9 -0
  123. package/src/tests/tests.ts +308 -5
  124. 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
+ }