@tellescope/sdk 1.250.1 → 1.251.0
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 +9 -0
- package/lib/cjs/sdk.d.ts.map +1 -1
- package/lib/cjs/sdk.js +3 -0
- package/lib/cjs/sdk.js.map +1 -1
- package/lib/cjs/tests/api_tests/account_switcher.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/account_switcher.test.js +1700 -306
- package/lib/cjs/tests/api_tests/account_switcher.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/enduser_cross_access_isolation.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/enduser_cross_access_isolation.test.js +28 -15
- package/lib/cjs/tests/api_tests/enduser_cross_access_isolation.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/enduser_login.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/enduser_login.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/enduser_login.test.js +315 -0
- package/lib/cjs/tests/api_tests/enduser_login.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/medication_added_trigger.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/medication_added_trigger.test.js +556 -105
- package/lib/cjs/tests/api_tests/medication_added_trigger.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/outbound_chat_sent_trigger.test.d.ts +7 -0
- package/lib/cjs/tests/api_tests/outbound_chat_sent_trigger.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/outbound_chat_sent_trigger.test.js +436 -0
- package/lib/cjs/tests/api_tests/outbound_chat_sent_trigger.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/push_forms_to_portal_group_completion.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/push_forms_to_portal_group_completion.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/push_forms_to_portal_group_completion.test.js +370 -0
- package/lib/cjs/tests/api_tests/push_forms_to_portal_group_completion.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/set_fields_order_templates.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/set_fields_order_templates.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/set_fields_order_templates.test.js +373 -0
- package/lib/cjs/tests/api_tests/set_fields_order_templates.test.js.map +1 -0
- package/lib/cjs/tests/setup.d.ts.map +1 -1
- package/lib/cjs/tests/setup.js +47 -32
- 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 +190 -161
- package/lib/cjs/tests/tests.js.map +1 -1
- package/lib/cjs/tests/unit_tests/conditional_logic_medication.test.d.ts +3 -0
- package/lib/cjs/tests/unit_tests/conditional_logic_medication.test.d.ts.map +1 -0
- package/lib/cjs/tests/unit_tests/conditional_logic_medication.test.js +114 -0
- package/lib/cjs/tests/unit_tests/conditional_logic_medication.test.js.map +1 -0
- package/lib/esm/sdk.d.ts +9 -0
- package/lib/esm/sdk.d.ts.map +1 -1
- package/lib/esm/sdk.js +3 -0
- package/lib/esm/sdk.js.map +1 -1
- package/lib/esm/tests/api_tests/account_switcher.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/account_switcher.test.js +1702 -305
- package/lib/esm/tests/api_tests/account_switcher.test.js.map +1 -1
- package/lib/esm/tests/api_tests/enduser_cross_access_isolation.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/enduser_cross_access_isolation.test.js +28 -15
- package/lib/esm/tests/api_tests/enduser_cross_access_isolation.test.js.map +1 -1
- package/lib/esm/tests/api_tests/enduser_login.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/enduser_login.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/enduser_login.test.js +308 -0
- package/lib/esm/tests/api_tests/enduser_login.test.js.map +1 -0
- package/lib/esm/tests/api_tests/enduser_login_phi_disclosure.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/enduser_login_phi_disclosure.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/enduser_login_phi_disclosure.test.js +268 -0
- package/lib/esm/tests/api_tests/enduser_login_phi_disclosure.test.js.map +1 -0
- package/lib/esm/tests/api_tests/medication_added_trigger.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/medication_added_trigger.test.js +556 -105
- package/lib/esm/tests/api_tests/medication_added_trigger.test.js.map +1 -1
- package/lib/esm/tests/api_tests/outbound_chat_sent_trigger.test.d.ts +7 -0
- package/lib/esm/tests/api_tests/outbound_chat_sent_trigger.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/outbound_chat_sent_trigger.test.js +432 -0
- package/lib/esm/tests/api_tests/outbound_chat_sent_trigger.test.js.map +1 -0
- package/lib/esm/tests/api_tests/push_forms_to_portal_group_completion.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/push_forms_to_portal_group_completion.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/push_forms_to_portal_group_completion.test.js +366 -0
- package/lib/esm/tests/api_tests/push_forms_to_portal_group_completion.test.js.map +1 -0
- package/lib/esm/tests/api_tests/set_fields_order_templates.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/set_fields_order_templates.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/set_fields_order_templates.test.js +369 -0
- package/lib/esm/tests/api_tests/set_fields_order_templates.test.js.map +1 -0
- package/lib/esm/tests/setup.d.ts.map +1 -1
- package/lib/esm/tests/setup.js +47 -32
- 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 +190 -161
- package/lib/esm/tests/tests.js.map +1 -1
- package/lib/esm/tests/unit_tests/conditional_logic_medication.test.d.ts +3 -0
- package/lib/esm/tests/unit_tests/conditional_logic_medication.test.d.ts.map +1 -0
- package/lib/esm/tests/unit_tests/conditional_logic_medication.test.js +111 -0
- package/lib/esm/tests/unit_tests/conditional_logic_medication.test.js.map +1 -0
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +10 -10
- package/src/sdk.ts +12 -0
- package/src/tests/api_tests/account_switcher.test.ts +1283 -0
- package/src/tests/api_tests/enduser_cross_access_isolation.test.ts +26 -0
- package/src/tests/api_tests/enduser_login.test.ts +215 -0
- package/src/tests/api_tests/medication_added_trigger.test.ts +345 -4
- package/src/tests/api_tests/outbound_chat_sent_trigger.test.ts +339 -0
- package/src/tests/api_tests/push_forms_to_portal_group_completion.test.ts +198 -0
- package/src/tests/api_tests/set_fields_order_templates.test.ts +258 -0
- package/src/tests/setup.ts +8 -1
- package/src/tests/tests.ts +23 -6
- package/src/tests/unit_tests/conditional_logic_medication.test.ts +133 -0
- package/test_generated.pdf +0 -0
|
@@ -339,6 +339,32 @@ export const enduser_cross_access_isolation_tests = async (
|
|
|
339
339
|
await sdkA.authenticate(enduserA.email!, password)
|
|
340
340
|
await sdkB.authenticate(enduserB.email!, password)
|
|
341
341
|
|
|
342
|
+
// Regression guard: hashedPassword must never appear in updateOne responses,
|
|
343
|
+
// for either enduser self-update or admin-side update of an enduser.
|
|
344
|
+
const selfUpdateResponse = await sdkA.api.endusers.updateOne(enduserA.id, { fname: 'RedactionProbe' })
|
|
345
|
+
assert(
|
|
346
|
+
!!selfUpdateResponse && (selfUpdateResponse as any).fname === 'RedactionProbe',
|
|
347
|
+
'enduser self-update: response should reflect the applied update',
|
|
348
|
+
'enduser self-update: update applied (sanity)',
|
|
349
|
+
)
|
|
350
|
+
assert(
|
|
351
|
+
(selfUpdateResponse as any).hashedPassword === undefined,
|
|
352
|
+
'enduser self-update: hashedPassword leaked in response',
|
|
353
|
+
'enduser self-update: hashedPassword redacted from response',
|
|
354
|
+
)
|
|
355
|
+
|
|
356
|
+
const adminUpdateResponse = await sdk.api.endusers.updateOne(enduserA.id, { fname: 'AdminProbe' })
|
|
357
|
+
assert(
|
|
358
|
+
!!adminUpdateResponse && (adminUpdateResponse as any).fname === 'AdminProbe',
|
|
359
|
+
'admin update of enduser: response should reflect the applied update',
|
|
360
|
+
'admin update of enduser: update applied (sanity)',
|
|
361
|
+
)
|
|
362
|
+
assert(
|
|
363
|
+
(adminUpdateResponse as any).hashedPassword === undefined,
|
|
364
|
+
'admin update of enduser: hashedPassword leaked in response',
|
|
365
|
+
'admin update of enduser: hashedPassword redacted from response',
|
|
366
|
+
)
|
|
367
|
+
|
|
342
368
|
for (const c of MODEL_CASES) {
|
|
343
369
|
const sublog = (variant: string) => `${c.model}: ${variant}`
|
|
344
370
|
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
require('source-map-support').install();
|
|
2
|
+
|
|
3
|
+
import axios from "axios"
|
|
4
|
+
import { Session } from "../../sdk"
|
|
5
|
+
import {
|
|
6
|
+
async_test,
|
|
7
|
+
log_header,
|
|
8
|
+
} from "@tellescope/testing"
|
|
9
|
+
import { setup_tests } from "../setup"
|
|
10
|
+
|
|
11
|
+
const host = process.env.API_URL || 'http://localhost:8080' as const
|
|
12
|
+
|
|
13
|
+
// Coverage for the /v1/login-enduser response surface:
|
|
14
|
+
// - When an enduser has no password set, the error response must not include
|
|
15
|
+
// enduser fields (PHI, hashedPassword, etc.).
|
|
16
|
+
// - The "enduser not found" and "wrong password" cases must be indistinguishable
|
|
17
|
+
// (same status code, same message) to prevent account enumeration.
|
|
18
|
+
// - verify_otp invalid-code error must not include enduser fields.
|
|
19
|
+
|
|
20
|
+
const post_login = async (body: any) => {
|
|
21
|
+
try {
|
|
22
|
+
const res = await axios.post(`${host}/v1/login-enduser`, body, { validateStatus: () => true })
|
|
23
|
+
return { status: res.status, data: res.data }
|
|
24
|
+
} catch (err: any) {
|
|
25
|
+
return { status: err?.response?.status, data: err?.response?.data }
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const enduser_login_tests = async ({ sdk, sdkNonAdmin } : { sdk: Session, sdkNonAdmin: Session }) => {
|
|
30
|
+
log_header("Enduser Login Tests")
|
|
31
|
+
|
|
32
|
+
const ts = Date.now()
|
|
33
|
+
// Distinctive markers we can grep the response body for.
|
|
34
|
+
const MARKER_FNAME = `LoginTestFname${ts}`
|
|
35
|
+
const MARKER_ADDRESS = `${ts} Test Way`
|
|
36
|
+
const MARKER_DOB = '01-01-1990'
|
|
37
|
+
|
|
38
|
+
const noPasswordEnduser = await sdk.api.endusers.createOne({
|
|
39
|
+
fname: MARKER_FNAME,
|
|
40
|
+
lname: 'LoginTest',
|
|
41
|
+
email: `login-test-no-password-${ts}@tellescope.com`,
|
|
42
|
+
dateOfBirth: MARKER_DOB,
|
|
43
|
+
addressLineOne: MARKER_ADDRESS,
|
|
44
|
+
addressLineTwo: 'Apt 4B',
|
|
45
|
+
city: 'Springfield',
|
|
46
|
+
state: 'IL',
|
|
47
|
+
zipCode: '62701',
|
|
48
|
+
gender: 'Female',
|
|
49
|
+
assignedTo: [sdk.userInfo.id],
|
|
50
|
+
fields: { secretField: `should-not-leak-${ts}` },
|
|
51
|
+
tags: ['vip', 'sensitive'],
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const withPasswordEnduser = await sdk.api.endusers.createOne({
|
|
55
|
+
fname: 'PasswordedEnduser',
|
|
56
|
+
lname: 'LoginTest',
|
|
57
|
+
email: `login-test-with-password-${ts}@tellescope.com`,
|
|
58
|
+
})
|
|
59
|
+
await sdk.api.endusers.set_password({ id: withPasswordEnduser.id, password: 'CorrectPassword123!' })
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
// Login response for an enduser without a password set must not include
|
|
63
|
+
// enduser fields.
|
|
64
|
+
const noPasswordResp = await post_login({
|
|
65
|
+
email: noPasswordEnduser.email,
|
|
66
|
+
password: 'arbitrary-password',
|
|
67
|
+
businessId: sdk.userInfo.businessId,
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
const noPasswordBody = JSON.stringify(noPasswordResp.data ?? {})
|
|
71
|
+
|
|
72
|
+
await async_test(
|
|
73
|
+
'No-password login response does not include fname marker',
|
|
74
|
+
async () => noPasswordBody.includes(MARKER_FNAME) ? 'leaked' : 'safe',
|
|
75
|
+
{ expectedResult: 'safe' }
|
|
76
|
+
)
|
|
77
|
+
await async_test(
|
|
78
|
+
'No-password login response does not include dateOfBirth',
|
|
79
|
+
async () => noPasswordBody.includes(MARKER_DOB) ? 'leaked' : 'safe',
|
|
80
|
+
{ expectedResult: 'safe' }
|
|
81
|
+
)
|
|
82
|
+
await async_test(
|
|
83
|
+
'No-password login response does not include addressLineOne marker',
|
|
84
|
+
async () => noPasswordBody.includes(MARKER_ADDRESS) ? 'leaked' : 'safe',
|
|
85
|
+
{ expectedResult: 'safe' }
|
|
86
|
+
)
|
|
87
|
+
for (const sensitiveKey of ['hashedPassword', 'assignedTo', 'fields', 'tags', 'insurance', 'customFields']) {
|
|
88
|
+
await async_test(
|
|
89
|
+
`No-password login response does not include "${sensitiveKey}" key`,
|
|
90
|
+
async () => noPasswordBody.includes(`"${sensitiveKey}"`) ? 'leaked' : 'safe',
|
|
91
|
+
{ expectedResult: 'safe' }
|
|
92
|
+
)
|
|
93
|
+
}
|
|
94
|
+
await async_test(
|
|
95
|
+
'No-password login response info field is absent or empty',
|
|
96
|
+
async () => {
|
|
97
|
+
const info = (noPasswordResp.data ?? {}).info
|
|
98
|
+
if (info === undefined) return 'safe'
|
|
99
|
+
if (typeof info === 'object' && info !== null && Object.keys(info).length === 0) return 'safe'
|
|
100
|
+
return 'leaked'
|
|
101
|
+
},
|
|
102
|
+
{ expectedResult: 'safe' }
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
// All three failure modes (no-password-set, wrong-password, unknown-email)
|
|
106
|
+
// must return an identical 401 + identical message — no enumeration of
|
|
107
|
+
// which case actually applies.
|
|
108
|
+
const wrongPasswordResp = await post_login({
|
|
109
|
+
email: withPasswordEnduser.email,
|
|
110
|
+
password: 'WrongPassword!2025',
|
|
111
|
+
businessId: sdk.userInfo.businessId,
|
|
112
|
+
})
|
|
113
|
+
const unknownEmailResp = await post_login({
|
|
114
|
+
email: `does-not-exist-${ts}@tellescope.com`,
|
|
115
|
+
password: 'AnyPassword!2025',
|
|
116
|
+
businessId: sdk.userInfo.businessId,
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
await async_test(
|
|
120
|
+
'Login returns 401 for no-password account (indistinguishable from wrong password)',
|
|
121
|
+
async () => `${noPasswordResp.status}:${noPasswordResp.data?.message ?? ''}`,
|
|
122
|
+
{ expectedResult: '401:Login details are invalid' }
|
|
123
|
+
)
|
|
124
|
+
await async_test(
|
|
125
|
+
'Login returns same status for no-password vs wrong-password vs unknown-email',
|
|
126
|
+
async () => {
|
|
127
|
+
const statuses = [noPasswordResp.status, wrongPasswordResp.status, unknownEmailResp.status]
|
|
128
|
+
const allSame = statuses.every(s => s === statuses[0])
|
|
129
|
+
return allSame ? `same:${statuses[0]}` : `diff:${statuses.join(',')}`
|
|
130
|
+
},
|
|
131
|
+
{ expectedResult: 'same:401' }
|
|
132
|
+
)
|
|
133
|
+
await async_test(
|
|
134
|
+
'Login returns same message for no-password vs wrong-password vs unknown-email',
|
|
135
|
+
async () => {
|
|
136
|
+
const messages = [noPasswordResp.data?.message, wrongPasswordResp.data?.message, unknownEmailResp.data?.message]
|
|
137
|
+
const allSame = messages.every(m => m === messages[0])
|
|
138
|
+
return allSame ? 'same' : `diff:${JSON.stringify(messages)}`
|
|
139
|
+
},
|
|
140
|
+
{ expectedResult: 'same' }
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
// verify_otp invalid-code response must not include enduser fields.
|
|
144
|
+
const verifyOtpInvalidResp = await axios.post(
|
|
145
|
+
`${host}/v1/verify-otp-code`,
|
|
146
|
+
{ token: 'not-a-real-token', code: '000000', businessId: sdk.userInfo.businessId },
|
|
147
|
+
{ validateStatus: () => true }
|
|
148
|
+
)
|
|
149
|
+
const verifyOtpBody = JSON.stringify(verifyOtpInvalidResp.data ?? {})
|
|
150
|
+
await async_test(
|
|
151
|
+
'verify_otp invalid-code response does not include any enduser fields',
|
|
152
|
+
async () => (
|
|
153
|
+
verifyOtpBody.includes('"hashedPassword"')
|
|
154
|
+
|| verifyOtpBody.includes('"assignedTo"')
|
|
155
|
+
|| verifyOtpBody.includes(MARKER_FNAME)
|
|
156
|
+
? 'leaked' : 'safe'
|
|
157
|
+
),
|
|
158
|
+
{ expectedResult: 'safe' }
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
// Regression: admin creating a duplicate enduser (same email) must still
|
|
162
|
+
// receive the existing record's id in the error info — this is an
|
|
163
|
+
// authenticated, intentional API-aid pattern via uniquenessError and must
|
|
164
|
+
// not be regressed by the public-endpoint sanitizer.
|
|
165
|
+
let duplicateError: any = null
|
|
166
|
+
try {
|
|
167
|
+
await sdk.api.endusers.createOne({ email: noPasswordEnduser.email })
|
|
168
|
+
} catch (err: any) {
|
|
169
|
+
duplicateError = err
|
|
170
|
+
}
|
|
171
|
+
await async_test(
|
|
172
|
+
'Admin duplicate enduser create rejects with 409 Uniqueness Violation',
|
|
173
|
+
async () => duplicateError?.message ?? 'no-error',
|
|
174
|
+
{ expectedResult: 'Uniqueness Violation' }
|
|
175
|
+
)
|
|
176
|
+
await async_test(
|
|
177
|
+
'Admin duplicate enduser create returns the existing record id in info',
|
|
178
|
+
async () => {
|
|
179
|
+
const info = duplicateError?.info
|
|
180
|
+
if (!Array.isArray(info) || info.length === 0) return `no-info:${JSON.stringify(info)}`
|
|
181
|
+
const existing = info[0]?.existingRecord
|
|
182
|
+
const existingId = existing?._id ?? existing?.id
|
|
183
|
+
return existingId === noPasswordEnduser.id ? 'matched' : `mismatched:${existingId}`
|
|
184
|
+
},
|
|
185
|
+
{ expectedResult: 'matched' }
|
|
186
|
+
)
|
|
187
|
+
} finally {
|
|
188
|
+
await Promise.all([
|
|
189
|
+
sdk.api.endusers.deleteOne(noPasswordEnduser.id).catch(() => null),
|
|
190
|
+
sdk.api.endusers.deleteOne(withPasswordEnduser.id).catch(() => null),
|
|
191
|
+
])
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Allow running this test file independently
|
|
196
|
+
if (require.main === module) {
|
|
197
|
+
console.log(`🌐 Using API URL: ${host}`)
|
|
198
|
+
const sdk = new Session({ host })
|
|
199
|
+
const sdkNonAdmin = new Session({ host })
|
|
200
|
+
|
|
201
|
+
const runTests = async () => {
|
|
202
|
+
await setup_tests(sdk, sdkNonAdmin)
|
|
203
|
+
await enduser_login_tests({ sdk, sdkNonAdmin })
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
runTests()
|
|
207
|
+
.then(() => {
|
|
208
|
+
console.log("✅ Enduser login test suite completed successfully")
|
|
209
|
+
process.exit(0)
|
|
210
|
+
})
|
|
211
|
+
.catch((error) => {
|
|
212
|
+
console.error("❌ Enduser login test suite failed:", error)
|
|
213
|
+
process.exit(1)
|
|
214
|
+
})
|
|
215
|
+
}
|
|
@@ -182,7 +182,7 @@ export const medication_added_trigger_tests = async ({ sdk, sdkNonAdmin } : { sd
|
|
|
182
182
|
title: "Medication - Beluga Protocol Test"
|
|
183
183
|
})
|
|
184
184
|
|
|
185
|
-
// Simulate the Beluga RX_WRITTEN webhook
|
|
185
|
+
// Simulate the Beluga RX_WRITTEN webhook with two meds covering category present (typical) and "N/A"
|
|
186
186
|
const webhookResponse = await fetch(`${host}/v1/webhooks/beluga`, {
|
|
187
187
|
method: 'POST',
|
|
188
188
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -197,6 +197,15 @@ export const medication_added_trigger_tests = async ({ sdk, sdkNonAdmin } : { sd
|
|
|
197
197
|
quantity: '1',
|
|
198
198
|
medId: 'test-ndc-123',
|
|
199
199
|
rxId: 'test-rx-456',
|
|
200
|
+
category: 'weightloss1',
|
|
201
|
+
}, {
|
|
202
|
+
name: 'Metformin',
|
|
203
|
+
strength: '500mg',
|
|
204
|
+
refills: '2',
|
|
205
|
+
quantity: '1',
|
|
206
|
+
medId: 'test-ndc-789',
|
|
207
|
+
rxId: 'test-rx-789',
|
|
208
|
+
category: 'N/A',
|
|
200
209
|
}],
|
|
201
210
|
}),
|
|
202
211
|
})
|
|
@@ -216,18 +225,83 @@ export const medication_added_trigger_tests = async ({ sdk, sdkNonAdmin } : { sd
|
|
|
216
225
|
|
|
217
226
|
// Verify the medication was created with correct protocol and source
|
|
218
227
|
const belugaMeds = await sdk.api.enduser_medications.getSome({ filter: { enduserId: belugaEnduser.id } })
|
|
219
|
-
const
|
|
228
|
+
const belugaMedSemaglutide = belugaMeds.find(m => m.title === 'Semaglutide')
|
|
229
|
+
const belugaMedMetformin = belugaMeds.find(m => m.title === 'Metformin')
|
|
230
|
+
if (!belugaMedSemaglutide || !belugaMedMetformin) {
|
|
231
|
+
throw new Error(`Beluga RX_WRITTEN - expected medications missing. Got: ${belugaMeds.map(m => m.title).join(', ')}`)
|
|
232
|
+
}
|
|
220
233
|
await async_test(
|
|
221
234
|
"Beluga RX_WRITTEN - Medication has protocol from form belugaVisitType",
|
|
222
|
-
async () =>
|
|
223
|
-
{ onResult: (m: EnduserMedication) => m
|
|
235
|
+
async () => belugaMedSemaglutide,
|
|
236
|
+
{ onResult: (m: EnduserMedication) => m?.protocol === 'Weight Loss' && m?.source === 'Beluga' && m?.title === 'Semaglutide' }
|
|
237
|
+
)
|
|
238
|
+
await async_test(
|
|
239
|
+
"Beluga RX_WRITTEN - Medication has category from webhook",
|
|
240
|
+
async () => belugaMedSemaglutide,
|
|
241
|
+
{ onResult: (m: EnduserMedication) => m?.category === 'weightloss1' }
|
|
242
|
+
)
|
|
243
|
+
await async_test(
|
|
244
|
+
"Beluga RX_WRITTEN - Medication preserves N/A category verbatim",
|
|
245
|
+
async () => belugaMedMetformin,
|
|
246
|
+
{ onResult: (m: EnduserMedication) => m?.category === 'N/A' }
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
// Backwards-compatibility: a webhook with no category on any med should result in undefined category
|
|
250
|
+
const belugaFormNoCategory = await sdk.api.forms.createOne({
|
|
251
|
+
title: 'Beluga Protocol Test Form (no category)',
|
|
252
|
+
belugaVisitType: 'Weight Loss',
|
|
253
|
+
})
|
|
254
|
+
const belugaEnduserNoCategory = await sdk.api.endusers.createOne({})
|
|
255
|
+
const belugaFormResponseNoCategory = await sdk.api.form_responses.createOne({
|
|
256
|
+
formId: belugaFormNoCategory.id,
|
|
257
|
+
enduserId: belugaEnduserNoCategory.id,
|
|
258
|
+
formTitle: belugaFormNoCategory.title,
|
|
259
|
+
responses: [{
|
|
260
|
+
fieldId: PLACEHOLDER_ID,
|
|
261
|
+
fieldTitle: 'placeholder',
|
|
262
|
+
answer: { type: 'string', value: 'test' },
|
|
263
|
+
}],
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
const webhookResponseNoCategory = await fetch(`${host}/v1/webhooks/beluga`, {
|
|
267
|
+
method: 'POST',
|
|
268
|
+
headers: { 'Content-Type': 'application/json' },
|
|
269
|
+
body: JSON.stringify({
|
|
270
|
+
masterId: `tellescope_${belugaFormResponseNoCategory.id}`,
|
|
271
|
+
event: 'RX_WRITTEN',
|
|
272
|
+
docName: 'Dr. Test',
|
|
273
|
+
medsPrescribed: [{
|
|
274
|
+
name: 'Ibuprofen',
|
|
275
|
+
strength: '200mg',
|
|
276
|
+
refills: '1',
|
|
277
|
+
quantity: '1',
|
|
278
|
+
medId: 'test-ndc-nocat',
|
|
279
|
+
rxId: 'test-rx-nocat',
|
|
280
|
+
}],
|
|
281
|
+
}),
|
|
282
|
+
})
|
|
283
|
+
|
|
284
|
+
if (!webhookResponseNoCategory.ok) {
|
|
285
|
+
throw new Error(`Beluga webhook (no category) failed with status ${webhookResponseNoCategory.status}: ${await webhookResponseNoCategory.text()}`)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
await wait(undefined, 500)
|
|
289
|
+
|
|
290
|
+
const belugaMedsNoCategory = await sdk.api.enduser_medications.getSome({ filter: { enduserId: belugaEnduserNoCategory.id } })
|
|
291
|
+
const belugaMedNoCategory = belugaMedsNoCategory[0]
|
|
292
|
+
await async_test(
|
|
293
|
+
"Beluga RX_WRITTEN - Medication omits category when not provided",
|
|
294
|
+
async () => belugaMedNoCategory,
|
|
295
|
+
{ onResult: (m: EnduserMedication) => m?.category === undefined || m?.category === null }
|
|
224
296
|
)
|
|
225
297
|
|
|
226
298
|
try {
|
|
227
299
|
// Clean up Beluga test data
|
|
228
300
|
await sdk.api.automation_triggers.deleteOne(belugaTrigger.id)
|
|
229
301
|
await sdk.api.forms.deleteOne(belugaForm.id)
|
|
302
|
+
await sdk.api.forms.deleteOne(belugaFormNoCategory.id)
|
|
230
303
|
await sdk.api.endusers.deleteOne(belugaEnduser.id) // also cleans up form response and medications
|
|
304
|
+
await sdk.api.endusers.deleteOne(belugaEnduserNoCategory.id) // also cleans up form response and medications
|
|
231
305
|
} finally {}
|
|
232
306
|
|
|
233
307
|
// ---- Set Fields with {{medication.name}} Test ----
|
|
@@ -276,11 +350,278 @@ export const medication_added_trigger_tests = async ({ sdk, sdkNonAdmin } : { sd
|
|
|
276
350
|
{ onResult: (e: Enduser) => e.fields?.['Specific Med'] === 'Metformin 500mg' }
|
|
277
351
|
)
|
|
278
352
|
|
|
353
|
+
// ---- Set Fields with {{medication.category}} Test ----
|
|
354
|
+
log_header("Medication Added - Set Fields with {{medication.category}}")
|
|
355
|
+
|
|
356
|
+
const setFieldsCategoryTrigger = await sdk.api.automation_triggers.createOne({
|
|
357
|
+
event: { type: 'Medication Added', info: { titles: [], protocols: [] } },
|
|
358
|
+
action: { type: 'Set Fields', info: { fields: [{ name: 'Medication Category', type: 'Custom Value' as const, value: '{{medication.category}}' }] }},
|
|
359
|
+
status: 'Active',
|
|
360
|
+
title: "Medication - Set Fields medication.category"
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
const setFieldsCategoryEnduser = await sdk.api.endusers.createOne({})
|
|
364
|
+
|
|
365
|
+
const setFieldsCategoryMed = await sdk.api.enduser_medications.createOne({
|
|
366
|
+
enduserId: setFieldsCategoryEnduser.id,
|
|
367
|
+
title: 'Ozempic 0.5mg',
|
|
368
|
+
category: 'weightloss1',
|
|
369
|
+
})
|
|
370
|
+
await wait(undefined, 500)
|
|
371
|
+
|
|
372
|
+
await async_test(
|
|
373
|
+
"Medication Added - Set Fields copies medication.category to enduser field",
|
|
374
|
+
() => sdk.api.endusers.getOne(setFieldsCategoryEnduser.id),
|
|
375
|
+
{ onResult: (e: Enduser) => e.fields?.['Medication Category'] === 'weightloss1' }
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
// Category absent on medication → placeholder resolves to empty string
|
|
379
|
+
const setFieldsCategoryEnduserEmpty = await sdk.api.endusers.createOne({})
|
|
380
|
+
const setFieldsCategoryMedEmpty = await sdk.api.enduser_medications.createOne({
|
|
381
|
+
enduserId: setFieldsCategoryEnduserEmpty.id,
|
|
382
|
+
title: 'Lisinopril 20mg',
|
|
383
|
+
})
|
|
384
|
+
await wait(undefined, 500)
|
|
385
|
+
|
|
386
|
+
await async_test(
|
|
387
|
+
"Medication Added - Set Fields with no category resolves to empty string",
|
|
388
|
+
() => sdk.api.endusers.getOne(setFieldsCategoryEnduserEmpty.id),
|
|
389
|
+
{ onResult: (e: Enduser) => e.fields?.['Medication Category'] === '' }
|
|
390
|
+
)
|
|
391
|
+
|
|
279
392
|
try {
|
|
280
393
|
await sdk.api.automation_triggers.deleteOne(setFieldsTrigger.id)
|
|
281
394
|
await sdk.api.automation_triggers.deleteOne(setFieldsTriggerFiltered.id)
|
|
395
|
+
await sdk.api.automation_triggers.deleteOne(setFieldsCategoryTrigger.id)
|
|
282
396
|
await sdk.api.endusers.deleteOne(setFieldsEnduser.id)
|
|
283
397
|
await sdk.api.endusers.deleteOne(setFieldsEnduser2.id)
|
|
398
|
+
await sdk.api.endusers.deleteOne(setFieldsCategoryEnduser.id)
|
|
399
|
+
await sdk.api.endusers.deleteOne(setFieldsCategoryEnduserEmpty.id)
|
|
400
|
+
} finally {}
|
|
401
|
+
|
|
402
|
+
// ---- titleCondition (compound conditional logic on title) ----
|
|
403
|
+
log_header("Medication Added - titleCondition compound logic")
|
|
404
|
+
|
|
405
|
+
// C1: simple $contains — fires for medications whose title contains 'GLP' (case-sensitive)
|
|
406
|
+
const c1Trigger = await sdk.api.automation_triggers.createOne({
|
|
407
|
+
event: {
|
|
408
|
+
type: 'Medication Added',
|
|
409
|
+
info: {
|
|
410
|
+
titles: [],
|
|
411
|
+
protocols: [],
|
|
412
|
+
titleCondition: { condition: { title: { $contains: 'GLP' } } },
|
|
413
|
+
},
|
|
414
|
+
},
|
|
415
|
+
action: { type: 'Add Tags', info: { tags: ['Med-Cond-Contains-GLP'] }},
|
|
416
|
+
status: 'Active',
|
|
417
|
+
title: "Medication - titleCondition contains GLP"
|
|
418
|
+
})
|
|
419
|
+
|
|
420
|
+
const c1MatchEnduser = await sdk.api.endusers.createOne({})
|
|
421
|
+
await sdk.api.enduser_medications.createOne({ enduserId: c1MatchEnduser.id, title: 'Semaglutide GLP-1' })
|
|
422
|
+
await wait(undefined, 500)
|
|
423
|
+
await async_test(
|
|
424
|
+
"titleCondition $contains - matching medication fires",
|
|
425
|
+
() => sdk.api.endusers.getOne(c1MatchEnduser.id),
|
|
426
|
+
{ onResult: (e: Enduser) => !!e.tags?.includes('Med-Cond-Contains-GLP') }
|
|
427
|
+
)
|
|
428
|
+
|
|
429
|
+
const c1MissEnduser = await sdk.api.endusers.createOne({})
|
|
430
|
+
await sdk.api.enduser_medications.createOne({ enduserId: c1MissEnduser.id, title: 'Aspirin' })
|
|
431
|
+
await wait(undefined, 500)
|
|
432
|
+
await async_test(
|
|
433
|
+
"titleCondition $contains - non-matching medication does NOT fire",
|
|
434
|
+
() => sdk.api.endusers.getOne(c1MissEnduser.id),
|
|
435
|
+
{ onResult: (e: Enduser) => !e.tags?.includes('Med-Cond-Contains-GLP') }
|
|
436
|
+
)
|
|
437
|
+
|
|
438
|
+
// C2: $ne — fires for everything except 'Placebo'
|
|
439
|
+
const c2Trigger = await sdk.api.automation_triggers.createOne({
|
|
440
|
+
event: {
|
|
441
|
+
type: 'Medication Added',
|
|
442
|
+
info: {
|
|
443
|
+
titles: [],
|
|
444
|
+
protocols: [],
|
|
445
|
+
titleCondition: { condition: { title: { $ne: 'Placebo' } } },
|
|
446
|
+
},
|
|
447
|
+
},
|
|
448
|
+
action: { type: 'Add Tags', info: { tags: ['Med-Cond-Ne-Placebo'] }},
|
|
449
|
+
status: 'Active',
|
|
450
|
+
title: "Medication - titleCondition ne Placebo"
|
|
451
|
+
})
|
|
452
|
+
|
|
453
|
+
const c2MatchEnduser = await sdk.api.endusers.createOne({})
|
|
454
|
+
await sdk.api.enduser_medications.createOne({ enduserId: c2MatchEnduser.id, title: 'Metformin' })
|
|
455
|
+
await wait(undefined, 500)
|
|
456
|
+
await async_test(
|
|
457
|
+
"titleCondition $ne - non-Placebo medication fires",
|
|
458
|
+
() => sdk.api.endusers.getOne(c2MatchEnduser.id),
|
|
459
|
+
{ onResult: (e: Enduser) => !!e.tags?.includes('Med-Cond-Ne-Placebo') }
|
|
460
|
+
)
|
|
461
|
+
|
|
462
|
+
const c2MissEnduser = await sdk.api.endusers.createOne({})
|
|
463
|
+
await sdk.api.enduser_medications.createOne({ enduserId: c2MissEnduser.id, title: 'Placebo' })
|
|
464
|
+
await wait(undefined, 500)
|
|
465
|
+
await async_test(
|
|
466
|
+
"titleCondition $ne - Placebo medication does NOT fire",
|
|
467
|
+
() => sdk.api.endusers.getOne(c2MissEnduser.id),
|
|
468
|
+
{ onResult: (e: Enduser) => !e.tags?.includes('Med-Cond-Ne-Placebo') }
|
|
469
|
+
)
|
|
470
|
+
|
|
471
|
+
// C3: compound $and — contains 'mg' AND does not contain 'Placebo' (case-sensitive)
|
|
472
|
+
const c3Trigger = await sdk.api.automation_triggers.createOne({
|
|
473
|
+
event: {
|
|
474
|
+
type: 'Medication Added',
|
|
475
|
+
info: {
|
|
476
|
+
titles: [],
|
|
477
|
+
protocols: [],
|
|
478
|
+
titleCondition: {
|
|
479
|
+
$and: [
|
|
480
|
+
{ condition: { title: { $contains: 'mg' } } },
|
|
481
|
+
{ condition: { title: { $doesNotContain: 'Placebo' } } },
|
|
482
|
+
],
|
|
483
|
+
},
|
|
484
|
+
},
|
|
485
|
+
},
|
|
486
|
+
action: { type: 'Add Tags', info: { tags: ['Med-Cond-And'] }},
|
|
487
|
+
status: 'Active',
|
|
488
|
+
title: "Medication - titleCondition compound AND"
|
|
489
|
+
})
|
|
490
|
+
|
|
491
|
+
const c3MatchEnduser = await sdk.api.endusers.createOne({})
|
|
492
|
+
await sdk.api.enduser_medications.createOne({ enduserId: c3MatchEnduser.id, title: 'Lisinopril 10mg' })
|
|
493
|
+
await wait(undefined, 500)
|
|
494
|
+
await async_test(
|
|
495
|
+
"titleCondition $and - both pass fires",
|
|
496
|
+
() => sdk.api.endusers.getOne(c3MatchEnduser.id),
|
|
497
|
+
{ onResult: (e: Enduser) => !!e.tags?.includes('Med-Cond-And') }
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
const c3MissEnduser = await sdk.api.endusers.createOne({})
|
|
501
|
+
await sdk.api.enduser_medications.createOne({ enduserId: c3MissEnduser.id, title: 'Placebo 5mg' })
|
|
502
|
+
await wait(undefined, 500)
|
|
503
|
+
await async_test(
|
|
504
|
+
"titleCondition $and - second branch fails does NOT fire",
|
|
505
|
+
() => sdk.api.endusers.getOne(c3MissEnduser.id),
|
|
506
|
+
{ onResult: (e: Enduser) => !e.tags?.includes('Med-Cond-And') }
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
// C4: compound $or — equals 'Aspirin' OR contains 'pril'
|
|
510
|
+
const c4Trigger = await sdk.api.automation_triggers.createOne({
|
|
511
|
+
event: {
|
|
512
|
+
type: 'Medication Added',
|
|
513
|
+
info: {
|
|
514
|
+
titles: [],
|
|
515
|
+
protocols: [],
|
|
516
|
+
titleCondition: {
|
|
517
|
+
$or: [
|
|
518
|
+
{ condition: { title: 'Aspirin' } },
|
|
519
|
+
{ condition: { title: { $contains: 'pril' } } },
|
|
520
|
+
],
|
|
521
|
+
},
|
|
522
|
+
},
|
|
523
|
+
},
|
|
524
|
+
action: { type: 'Add Tags', info: { tags: ['Med-Cond-Or'] }},
|
|
525
|
+
status: 'Active',
|
|
526
|
+
title: "Medication - titleCondition compound OR"
|
|
527
|
+
})
|
|
528
|
+
|
|
529
|
+
const c4MatchA = await sdk.api.endusers.createOne({})
|
|
530
|
+
await sdk.api.enduser_medications.createOne({ enduserId: c4MatchA.id, title: 'Aspirin' })
|
|
531
|
+
await wait(undefined, 500)
|
|
532
|
+
await async_test(
|
|
533
|
+
"titleCondition $or - first branch matches fires",
|
|
534
|
+
() => sdk.api.endusers.getOne(c4MatchA.id),
|
|
535
|
+
{ onResult: (e: Enduser) => !!e.tags?.includes('Med-Cond-Or') }
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
const c4MatchB = await sdk.api.endusers.createOne({})
|
|
539
|
+
await sdk.api.enduser_medications.createOne({ enduserId: c4MatchB.id, title: 'Lisinopril' })
|
|
540
|
+
await wait(undefined, 500)
|
|
541
|
+
await async_test(
|
|
542
|
+
"titleCondition $or - second branch matches fires",
|
|
543
|
+
() => sdk.api.endusers.getOne(c4MatchB.id),
|
|
544
|
+
{ onResult: (e: Enduser) => !!e.tags?.includes('Med-Cond-Or') }
|
|
545
|
+
)
|
|
546
|
+
|
|
547
|
+
const c4Miss = await sdk.api.endusers.createOne({})
|
|
548
|
+
await sdk.api.enduser_medications.createOne({ enduserId: c4Miss.id, title: 'Metformin' })
|
|
549
|
+
await wait(undefined, 500)
|
|
550
|
+
await async_test(
|
|
551
|
+
"titleCondition $or - neither branch matches does NOT fire",
|
|
552
|
+
() => sdk.api.endusers.getOne(c4Miss.id),
|
|
553
|
+
{ onResult: (e: Enduser) => !e.tags?.includes('Med-Cond-Or') }
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
// C5: backwards compatibility — no titleCondition, titles array still works
|
|
557
|
+
const c5Trigger = await sdk.api.automation_triggers.createOne({
|
|
558
|
+
event: { type: 'Medication Added', info: { titles: ['Atorvastatin'], protocols: [] } },
|
|
559
|
+
action: { type: 'Add Tags', info: { tags: ['Med-Cond-BackCompat'] }},
|
|
560
|
+
status: 'Active',
|
|
561
|
+
title: "Medication - titles array back-compat"
|
|
562
|
+
})
|
|
563
|
+
|
|
564
|
+
const c5Match = await sdk.api.endusers.createOne({})
|
|
565
|
+
await sdk.api.enduser_medications.createOne({ enduserId: c5Match.id, title: 'Atorvastatin' })
|
|
566
|
+
await wait(undefined, 500)
|
|
567
|
+
await async_test(
|
|
568
|
+
"titles back-compat - title match still fires without titleCondition",
|
|
569
|
+
() => sdk.api.endusers.getOne(c5Match.id),
|
|
570
|
+
{ onResult: (e: Enduser) => !!e.tags?.includes('Med-Cond-BackCompat') }
|
|
571
|
+
)
|
|
572
|
+
|
|
573
|
+
const c5Miss = await sdk.api.endusers.createOne({})
|
|
574
|
+
await sdk.api.enduser_medications.createOne({ enduserId: c5Miss.id, title: 'Aspirin' })
|
|
575
|
+
await wait(undefined, 500)
|
|
576
|
+
await async_test(
|
|
577
|
+
"titles back-compat - non-match does NOT fire without titleCondition",
|
|
578
|
+
() => sdk.api.endusers.getOne(c5Miss.id),
|
|
579
|
+
{ onResult: (e: Enduser) => !e.tags?.includes('Med-Cond-BackCompat') }
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
// C6: combined — titles array AND titleCondition compose (both must pass)
|
|
583
|
+
const c6Trigger = await sdk.api.automation_triggers.createOne({
|
|
584
|
+
event: {
|
|
585
|
+
type: 'Medication Added',
|
|
586
|
+
info: {
|
|
587
|
+
titles: ['Lisinopril'],
|
|
588
|
+
protocols: [],
|
|
589
|
+
titleCondition: { condition: { title: { $contains: 'Lisin' } } },
|
|
590
|
+
},
|
|
591
|
+
},
|
|
592
|
+
action: { type: 'Add Tags', info: { tags: ['Med-Cond-Combined'] }},
|
|
593
|
+
status: 'Active',
|
|
594
|
+
title: "Medication - titles + titleCondition combined"
|
|
595
|
+
})
|
|
596
|
+
|
|
597
|
+
const c6Match = await sdk.api.endusers.createOne({})
|
|
598
|
+
await sdk.api.enduser_medications.createOne({ enduserId: c6Match.id, title: 'Lisinopril' })
|
|
599
|
+
await wait(undefined, 500)
|
|
600
|
+
await async_test(
|
|
601
|
+
"titleCondition combined - both pass fires",
|
|
602
|
+
() => sdk.api.endusers.getOne(c6Match.id),
|
|
603
|
+
{ onResult: (e: Enduser) => !!e.tags?.includes('Med-Cond-Combined') }
|
|
604
|
+
)
|
|
605
|
+
|
|
606
|
+
try {
|
|
607
|
+
await sdk.api.automation_triggers.deleteOne(c1Trigger.id)
|
|
608
|
+
await sdk.api.automation_triggers.deleteOne(c2Trigger.id)
|
|
609
|
+
await sdk.api.automation_triggers.deleteOne(c3Trigger.id)
|
|
610
|
+
await sdk.api.automation_triggers.deleteOne(c4Trigger.id)
|
|
611
|
+
await sdk.api.automation_triggers.deleteOne(c5Trigger.id)
|
|
612
|
+
await sdk.api.automation_triggers.deleteOne(c6Trigger.id)
|
|
613
|
+
await sdk.api.endusers.deleteOne(c1MatchEnduser.id)
|
|
614
|
+
await sdk.api.endusers.deleteOne(c1MissEnduser.id)
|
|
615
|
+
await sdk.api.endusers.deleteOne(c2MatchEnduser.id)
|
|
616
|
+
await sdk.api.endusers.deleteOne(c2MissEnduser.id)
|
|
617
|
+
await sdk.api.endusers.deleteOne(c3MatchEnduser.id)
|
|
618
|
+
await sdk.api.endusers.deleteOne(c3MissEnduser.id)
|
|
619
|
+
await sdk.api.endusers.deleteOne(c4MatchA.id)
|
|
620
|
+
await sdk.api.endusers.deleteOne(c4MatchB.id)
|
|
621
|
+
await sdk.api.endusers.deleteOne(c4Miss.id)
|
|
622
|
+
await sdk.api.endusers.deleteOne(c5Match.id)
|
|
623
|
+
await sdk.api.endusers.deleteOne(c5Miss.id)
|
|
624
|
+
await sdk.api.endusers.deleteOne(c6Match.id)
|
|
284
625
|
} finally {}
|
|
285
626
|
}
|
|
286
627
|
|