@tellescope/sdk 1.244.4 → 1.245.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/tests/api_tests/medication_added_trigger.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/medication_added_trigger.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/medication_added_trigger.test.js +452 -0
- package/lib/cjs/tests/api_tests/medication_added_trigger.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/openloop_webhooks.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/openloop_webhooks.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/openloop_webhooks.test.js +833 -0
- package/lib/cjs/tests/api_tests/openloop_webhooks.test.js.map +1 -0
- package/lib/cjs/tests/tests.d.ts.map +1 -1
- package/lib/cjs/tests/tests.js +142 -134
- package/lib/cjs/tests/tests.js.map +1 -1
- package/lib/esm/tests/api_tests/medication_added_trigger.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/medication_added_trigger.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/medication_added_trigger.test.js +448 -0
- package/lib/esm/tests/api_tests/medication_added_trigger.test.js.map +1 -0
- package/lib/esm/tests/api_tests/openloop_webhooks.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/openloop_webhooks.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/openloop_webhooks.test.js +829 -0
- package/lib/esm/tests/api_tests/openloop_webhooks.test.js.map +1 -0
- package/lib/esm/tests/tests.d.ts.map +1 -1
- package/lib/esm/tests/tests.js +142 -134
- 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/medication_added_trigger.test.ts +306 -0
- package/src/tests/api_tests/openloop_webhooks.test.ts +662 -0
- package/src/tests/tests.ts +5 -1
- package/test_generated.pdf +0 -0
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
require('source-map-support').install();
|
|
2
|
+
|
|
3
|
+
import { Session } from "../../sdk"
|
|
4
|
+
import {
|
|
5
|
+
assert,
|
|
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
|
+
const businessId = '60398b1131a295e64f084ff6'
|
|
13
|
+
|
|
14
|
+
const v1Url = `${host}/v1/webhooks/openloop/${businessId}`
|
|
15
|
+
const v2Url = `${host}/v1/webhooks/openloop-v2/${businessId}`
|
|
16
|
+
|
|
17
|
+
const postJSON = async (url: string, body: object) => {
|
|
18
|
+
const res = await fetch(url, {
|
|
19
|
+
method: 'POST',
|
|
20
|
+
headers: { 'Content-Type': 'application/json' },
|
|
21
|
+
body: JSON.stringify(body),
|
|
22
|
+
})
|
|
23
|
+
let data: any
|
|
24
|
+
try { data = await res.json() } catch { data = null }
|
|
25
|
+
return { status: res.status, data }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const postV1 = (body: object) => postJSON(v1Url, body)
|
|
29
|
+
const postV2 = (body: object) => postJSON(v2Url, body)
|
|
30
|
+
|
|
31
|
+
let counter = 0
|
|
32
|
+
const uid = () => `${Date.now()}-${++counter}`
|
|
33
|
+
|
|
34
|
+
const makeV1Confirmation = (overrides: Record<string, any> = {}) => ({
|
|
35
|
+
type: 'order_confirmation' as const,
|
|
36
|
+
patientID: 'test-healthie-ol-1',
|
|
37
|
+
pharmacy: 'Test Pharmacy',
|
|
38
|
+
medication_instructions: 'Take once daily',
|
|
39
|
+
shipping_address: '123 Test St',
|
|
40
|
+
orderNumber: `ol-conf-${uid()}`,
|
|
41
|
+
weeksOrdered: 'w4',
|
|
42
|
+
fill: '1',
|
|
43
|
+
medicationSKU: 'SKU-100',
|
|
44
|
+
sku_med: 'Test Medication 10mg',
|
|
45
|
+
...overrides,
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const makeV1Shipped = (overrides: Record<string, any> = {}) => ({
|
|
49
|
+
type: 'order_shipped' as const,
|
|
50
|
+
patientID: 'test-healthie-ol-1',
|
|
51
|
+
pharmacy: 'Test Pharmacy',
|
|
52
|
+
shipped_date: '2024-07-09',
|
|
53
|
+
track_number: 'TRACK123',
|
|
54
|
+
status: 'shipped' as const,
|
|
55
|
+
order_date: '2024-07-09',
|
|
56
|
+
orderNumber: `ol-ship-${uid()}`,
|
|
57
|
+
fill: '1',
|
|
58
|
+
medicationSKU: 'SKU-200',
|
|
59
|
+
sku_med: 'Shipped Med 20mg',
|
|
60
|
+
...overrides,
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
const makeV2Payload = (eventType: string, overrides: Record<string, any> = {}) => ({
|
|
64
|
+
id: `v2-${uid()}`,
|
|
65
|
+
client: 'test-client',
|
|
66
|
+
eventId: `evt-${uid()}`,
|
|
67
|
+
eventType,
|
|
68
|
+
chartId: 'chart-1',
|
|
69
|
+
patientId: 'test-healthie-ol-1',
|
|
70
|
+
providerId: 'provider-1',
|
|
71
|
+
medication: 'V2 Test Med',
|
|
72
|
+
medicationSku: 'v2-sku-001',
|
|
73
|
+
fill: '1',
|
|
74
|
+
prescriptionCreatedDate: '2024-01-15',
|
|
75
|
+
...overrides,
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
export const openloop_webhooks_tests = async ({ sdk, sdkNonAdmin }: { sdk: Session, sdkNonAdmin: Session }) => {
|
|
79
|
+
log_header("OpenLoop Webhooks Tests")
|
|
80
|
+
|
|
81
|
+
const healthieId1 = 'test-healthie-ol-1'
|
|
82
|
+
const healthieId2 = 'test-healthie-ol-2'
|
|
83
|
+
|
|
84
|
+
const enduser1 = await sdk.api.endusers.createOne({ source: 'Healthie', externalId: healthieId1 })
|
|
85
|
+
const enduser2 = await sdk.api.endusers.createOne({ source: 'Healthie', externalId: healthieId2 })
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
// ===== SECTION A: V1 Validation =====
|
|
89
|
+
log_header("V1 Validation")
|
|
90
|
+
|
|
91
|
+
await async_test(
|
|
92
|
+
'V1: missing patientID returns 400',
|
|
93
|
+
async () => {
|
|
94
|
+
const res = await postV1({ type: 'order_confirmation', pharmacy: 'x', orderNumber: 'x' })
|
|
95
|
+
return res.status
|
|
96
|
+
},
|
|
97
|
+
{ onResult: (s: number) => s === 400 }
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
await async_test(
|
|
101
|
+
'V1: unknown patient returns 404',
|
|
102
|
+
async () => {
|
|
103
|
+
const res = await postV1(makeV1Confirmation({ patientID: 'nonexistent-patient-id' }))
|
|
104
|
+
return res.status
|
|
105
|
+
},
|
|
106
|
+
{ onResult: (s: number) => s === 404 }
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
// ===== SECTION B: V1 order_confirmation =====
|
|
110
|
+
log_header("V1 order_confirmation")
|
|
111
|
+
|
|
112
|
+
const confOrderNumber = `ol-conf-fields-${uid()}`
|
|
113
|
+
await async_test(
|
|
114
|
+
'V1: order_confirmation creates EnduserOrder with correct fields',
|
|
115
|
+
async () => {
|
|
116
|
+
const res = await postV1(makeV1Confirmation({
|
|
117
|
+
patientID: healthieId1,
|
|
118
|
+
orderNumber: confOrderNumber,
|
|
119
|
+
sku_med: 'Test Med 10mg',
|
|
120
|
+
pharmacy: 'PharmaCo',
|
|
121
|
+
medication_instructions: 'Take daily with food',
|
|
122
|
+
shipping_address: '456 Oak Ave',
|
|
123
|
+
weeksOrdered: 'w12',
|
|
124
|
+
fill: '2',
|
|
125
|
+
medicationSKU: 'SKU-ABC',
|
|
126
|
+
}))
|
|
127
|
+
assert(res.status === 200, `Expected 200, got ${res.status}`)
|
|
128
|
+
|
|
129
|
+
const orders = await sdk.api.enduser_orders.getSome({
|
|
130
|
+
filter: { source: 'OpenLoop', externalId: confOrderNumber }
|
|
131
|
+
})
|
|
132
|
+
assert(orders.length === 1, `Expected 1 order, got ${orders.length}`)
|
|
133
|
+
const order = orders[0]
|
|
134
|
+
assert(order.enduserId === enduser1.id, 'enduserId mismatch')
|
|
135
|
+
assert(order.title === 'Test Med 10mg', `title mismatch: ${order.title}`)
|
|
136
|
+
assert(order.status === 'confirmed', `status mismatch: ${order.status}`)
|
|
137
|
+
assert(order.description === '456 Oak Ave', `description mismatch: ${order.description}`)
|
|
138
|
+
assert(order.instructions === 'Take daily with food', `instructions mismatch: ${order.instructions}`)
|
|
139
|
+
assert(order.frequency === 'w12', `frequency mismatch: ${order.frequency}`)
|
|
140
|
+
assert(order.fill === '2', `fill mismatch: ${order.fill}`)
|
|
141
|
+
assert(order.sku === 'sku-abc', `sku mismatch (should be lowercased): ${order.sku}`)
|
|
142
|
+
return true
|
|
143
|
+
},
|
|
144
|
+
{ onResult: (r: boolean) => r === true }
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
await async_test(
|
|
148
|
+
'V1: order_confirmation idempotency - same order not duplicated',
|
|
149
|
+
async () => {
|
|
150
|
+
// Post same orderNumber again
|
|
151
|
+
const res = await postV1(makeV1Confirmation({
|
|
152
|
+
patientID: healthieId1,
|
|
153
|
+
orderNumber: confOrderNumber,
|
|
154
|
+
sku_med: 'Different Title',
|
|
155
|
+
fill: '99',
|
|
156
|
+
}))
|
|
157
|
+
assert(res.status === 200, `Expected 200, got ${res.status}`)
|
|
158
|
+
|
|
159
|
+
const orders = await sdk.api.enduser_orders.getSome({
|
|
160
|
+
filter: { source: 'OpenLoop', externalId: confOrderNumber }
|
|
161
|
+
})
|
|
162
|
+
assert(orders.length === 1, `Expected 1 order after duplicate, got ${orders.length}`)
|
|
163
|
+
// $setOnInsert means fields should NOT have changed
|
|
164
|
+
assert(orders[0].title === 'Test Med 10mg', `title should not change on duplicate: ${orders[0].title}`)
|
|
165
|
+
assert(orders[0].fill === '2', `fill should not change on duplicate: ${orders[0].fill}`)
|
|
166
|
+
return true
|
|
167
|
+
},
|
|
168
|
+
{ onResult: (r: boolean) => r === true }
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
await async_test(
|
|
172
|
+
'V1: order_confirmation without sku_med falls back to pharmacy name',
|
|
173
|
+
async () => {
|
|
174
|
+
const orderNum = `ol-conf-fallback-${uid()}`
|
|
175
|
+
const res = await postV1(makeV1Confirmation({
|
|
176
|
+
patientID: healthieId1,
|
|
177
|
+
orderNumber: orderNum,
|
|
178
|
+
sku_med: undefined,
|
|
179
|
+
pharmacy: 'FallbackPharmacy',
|
|
180
|
+
}))
|
|
181
|
+
assert(res.status === 200, `Expected 200, got ${res.status}`)
|
|
182
|
+
|
|
183
|
+
const orders = await sdk.api.enduser_orders.getSome({
|
|
184
|
+
filter: { source: 'OpenLoop', externalId: orderNum }
|
|
185
|
+
})
|
|
186
|
+
assert(orders.length === 1, `Expected 1 order, got ${orders.length}`)
|
|
187
|
+
assert(orders[0].title === 'OpenLoop: FallbackPharmacy', `title mismatch: ${orders[0].title}`)
|
|
188
|
+
return true
|
|
189
|
+
},
|
|
190
|
+
{ onResult: (r: boolean) => r === true }
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
// ===== SECTION C: V1 order_shipped =====
|
|
194
|
+
log_header("V1 order_shipped")
|
|
195
|
+
|
|
196
|
+
const shipOrderNumber = `ol-ship-update-${uid()}`
|
|
197
|
+
await async_test(
|
|
198
|
+
'V1: order_shipped updates existing confirmed order',
|
|
199
|
+
async () => {
|
|
200
|
+
// First create a confirmed order
|
|
201
|
+
await postV1(makeV1Confirmation({
|
|
202
|
+
patientID: healthieId1,
|
|
203
|
+
orderNumber: shipOrderNumber,
|
|
204
|
+
}))
|
|
205
|
+
|
|
206
|
+
// Now ship it
|
|
207
|
+
const res = await postV1(makeV1Shipped({
|
|
208
|
+
patientID: healthieId1,
|
|
209
|
+
orderNumber: shipOrderNumber,
|
|
210
|
+
track_number: 'TRACK-ABC',
|
|
211
|
+
shipped_date: '2024-07-09',
|
|
212
|
+
}))
|
|
213
|
+
assert(res.status === 200, `Expected 200, got ${res.status}`)
|
|
214
|
+
|
|
215
|
+
const orders = await sdk.api.enduser_orders.getSome({
|
|
216
|
+
filter: { source: 'OpenLoop', externalId: shipOrderNumber }
|
|
217
|
+
})
|
|
218
|
+
assert(orders.length === 1, `Expected 1 order, got ${orders.length}`)
|
|
219
|
+
const order = orders[0]
|
|
220
|
+
assert(order.status === 'shipped', `status mismatch: ${order.status}`)
|
|
221
|
+
assert(order.tracking === 'TRACK-ABC', `tracking mismatch: ${order.tracking}`)
|
|
222
|
+
assert(order.shippedDate === '07-09-2024', `shippedDate mismatch (expected MM-DD-YYYY): ${order.shippedDate}`)
|
|
223
|
+
return true
|
|
224
|
+
},
|
|
225
|
+
{ onResult: (r: boolean) => r === true }
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
await async_test(
|
|
229
|
+
'V1: order_shipped updates title and sku when provided',
|
|
230
|
+
async () => {
|
|
231
|
+
// Ship again with sku_med and medicationSKU
|
|
232
|
+
const res = await postV1(makeV1Shipped({
|
|
233
|
+
patientID: healthieId1,
|
|
234
|
+
orderNumber: shipOrderNumber,
|
|
235
|
+
sku_med: 'Updated Title From Ship',
|
|
236
|
+
medicationSKU: 'NEW-SKU-123',
|
|
237
|
+
track_number: 'TRACK-DEF',
|
|
238
|
+
shipped_date: '2024-08-15',
|
|
239
|
+
}))
|
|
240
|
+
assert(res.status === 200, `Expected 200, got ${res.status}`)
|
|
241
|
+
|
|
242
|
+
const orders = await sdk.api.enduser_orders.getSome({
|
|
243
|
+
filter: { source: 'OpenLoop', externalId: shipOrderNumber }
|
|
244
|
+
})
|
|
245
|
+
assert(orders.length === 1, `Expected 1 order, got ${orders.length}`)
|
|
246
|
+
const order = orders[0]
|
|
247
|
+
assert(order.title === 'Updated Title From Ship', `title mismatch: ${order.title}`)
|
|
248
|
+
assert(order.sku === 'new-sku-123', `sku mismatch (should be lowercased): ${order.sku}`)
|
|
249
|
+
return true
|
|
250
|
+
},
|
|
251
|
+
{ onResult: (r: boolean) => r === true }
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
await async_test(
|
|
255
|
+
'V1: order_shipped creates new order if none exists',
|
|
256
|
+
async () => {
|
|
257
|
+
const newOrderNum = `ol-ship-new-${uid()}`
|
|
258
|
+
const res = await postV1(makeV1Shipped({
|
|
259
|
+
patientID: healthieId1,
|
|
260
|
+
orderNumber: newOrderNum,
|
|
261
|
+
sku_med: 'Direct Ship Med',
|
|
262
|
+
track_number: 'TRACK-NEW',
|
|
263
|
+
shipped_date: '2024-09-01',
|
|
264
|
+
fill: '3',
|
|
265
|
+
medicationSKU: 'DIRECT-SKU',
|
|
266
|
+
}))
|
|
267
|
+
assert(res.status === 200, `Expected 200, got ${res.status}`)
|
|
268
|
+
|
|
269
|
+
const orders = await sdk.api.enduser_orders.getSome({
|
|
270
|
+
filter: { source: 'OpenLoop', externalId: newOrderNum }
|
|
271
|
+
})
|
|
272
|
+
assert(orders.length === 1, `Expected 1 order, got ${orders.length}`)
|
|
273
|
+
const order = orders[0]
|
|
274
|
+
assert(order.status === 'shipped', `status mismatch: ${order.status}`)
|
|
275
|
+
assert(order.title === 'Direct Ship Med', `title mismatch: ${order.title}`)
|
|
276
|
+
assert(order.tracking === 'TRACK-NEW', `tracking mismatch: ${order.tracking}`)
|
|
277
|
+
assert(order.fill === '3', `fill mismatch: ${order.fill}`)
|
|
278
|
+
assert(order.sku === 'direct-sku', `sku mismatch: ${order.sku}`)
|
|
279
|
+
return true
|
|
280
|
+
},
|
|
281
|
+
{ onResult: (r: boolean) => r === true }
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
await async_test(
|
|
285
|
+
'V1: order_shipped can re-ship with updated tracking',
|
|
286
|
+
async () => {
|
|
287
|
+
const reshipOrderNum = `ol-reship-${uid()}`
|
|
288
|
+
// Create and ship
|
|
289
|
+
await postV1(makeV1Confirmation({ patientID: healthieId1, orderNumber: reshipOrderNum }))
|
|
290
|
+
await postV1(makeV1Shipped({
|
|
291
|
+
patientID: healthieId1,
|
|
292
|
+
orderNumber: reshipOrderNum,
|
|
293
|
+
track_number: 'TRACK-FIRST',
|
|
294
|
+
shipped_date: '2024-07-01',
|
|
295
|
+
}))
|
|
296
|
+
|
|
297
|
+
// Re-ship with new tracking
|
|
298
|
+
const res = await postV1(makeV1Shipped({
|
|
299
|
+
patientID: healthieId1,
|
|
300
|
+
orderNumber: reshipOrderNum,
|
|
301
|
+
track_number: 'TRACK-SECOND',
|
|
302
|
+
shipped_date: '2024-07-15',
|
|
303
|
+
}))
|
|
304
|
+
assert(res.status === 200, `Expected 200, got ${res.status}`)
|
|
305
|
+
|
|
306
|
+
const orders = await sdk.api.enduser_orders.getSome({
|
|
307
|
+
filter: { source: 'OpenLoop', externalId: reshipOrderNum }
|
|
308
|
+
})
|
|
309
|
+
assert(orders.length === 1, `Expected 1 order, got ${orders.length}`)
|
|
310
|
+
assert(orders[0].tracking === 'TRACK-SECOND', `tracking should be updated: ${orders[0].tracking}`)
|
|
311
|
+
assert(orders[0].shippedDate === '07-15-2024', `shippedDate should be updated: ${orders[0].shippedDate}`)
|
|
312
|
+
return true
|
|
313
|
+
},
|
|
314
|
+
{ onResult: (r: boolean) => r === true }
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
// ===== SECTION D: V1 enduserId Isolation =====
|
|
318
|
+
log_header("V1 enduserId Isolation")
|
|
319
|
+
|
|
320
|
+
await async_test(
|
|
321
|
+
'V1: same orderNumber for different endusers creates separate orders',
|
|
322
|
+
async () => {
|
|
323
|
+
const sharedOrderNum = `ol-shared-${uid()}`
|
|
324
|
+
|
|
325
|
+
const res1 = await postV1(makeV1Confirmation({
|
|
326
|
+
patientID: healthieId1,
|
|
327
|
+
orderNumber: sharedOrderNum,
|
|
328
|
+
}))
|
|
329
|
+
assert(res1.status === 200, `enduser1 confirm failed: ${res1.status}`)
|
|
330
|
+
|
|
331
|
+
const res2 = await postV1(makeV1Confirmation({
|
|
332
|
+
patientID: healthieId2,
|
|
333
|
+
orderNumber: sharedOrderNum,
|
|
334
|
+
}))
|
|
335
|
+
assert(res2.status === 200, `enduser2 confirm failed: ${res2.status}`)
|
|
336
|
+
|
|
337
|
+
const orders = await sdk.api.enduser_orders.getSome({
|
|
338
|
+
filter: { source: 'OpenLoop', externalId: sharedOrderNum }
|
|
339
|
+
})
|
|
340
|
+
assert(orders.length === 2, `Expected 2 separate orders, got ${orders.length}`)
|
|
341
|
+
|
|
342
|
+
const enduserIds = orders.map(o => o.enduserId).sort()
|
|
343
|
+
const expected = [enduser1.id, enduser2.id].sort()
|
|
344
|
+
assert(
|
|
345
|
+
enduserIds[0] === expected[0] && enduserIds[1] === expected[1],
|
|
346
|
+
`Orders should belong to different endusers: ${JSON.stringify(enduserIds)} vs ${JSON.stringify(expected)}`
|
|
347
|
+
)
|
|
348
|
+
return true
|
|
349
|
+
},
|
|
350
|
+
{ onResult: (r: boolean) => r === true }
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
// ===== SECTION E: V2 Validation =====
|
|
354
|
+
log_header("V2 Validation")
|
|
355
|
+
|
|
356
|
+
await async_test(
|
|
357
|
+
'V2: missing patientId returns 400',
|
|
358
|
+
async () => {
|
|
359
|
+
const res = await postV2({
|
|
360
|
+
id: 'test', eventType: 'prescription-created', medication: 'x',
|
|
361
|
+
medicationSku: 'x', fill: '1', prescriptionCreatedDate: '2024-01-01',
|
|
362
|
+
})
|
|
363
|
+
return res.status
|
|
364
|
+
},
|
|
365
|
+
{ onResult: (s: number) => s === 400 }
|
|
366
|
+
)
|
|
367
|
+
|
|
368
|
+
await async_test(
|
|
369
|
+
'V2: invalid eventType returns 400',
|
|
370
|
+
async () => {
|
|
371
|
+
const res = await postV2(makeV2Payload('invalid-event-type'))
|
|
372
|
+
return res.status
|
|
373
|
+
},
|
|
374
|
+
{ onResult: (s: number) => s === 400 }
|
|
375
|
+
)
|
|
376
|
+
|
|
377
|
+
await async_test(
|
|
378
|
+
'V2: unknown patient returns 404',
|
|
379
|
+
async () => {
|
|
380
|
+
const res = await postV2(makeV2Payload('prescription-created', { patientId: 'nonexistent-v2' }))
|
|
381
|
+
return res.status
|
|
382
|
+
},
|
|
383
|
+
{ onResult: (s: number) => s === 404 }
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
// ===== SECTION F: V2 Order Lifecycle =====
|
|
387
|
+
log_header("V2 Order Lifecycle")
|
|
388
|
+
|
|
389
|
+
const v2OrderId = `v2-lifecycle-${uid()}`
|
|
390
|
+
await async_test(
|
|
391
|
+
'V2: prescription-created creates order with correct fields',
|
|
392
|
+
async () => {
|
|
393
|
+
const res = await postV2(makeV2Payload('prescription-created', {
|
|
394
|
+
id: v2OrderId,
|
|
395
|
+
patientId: healthieId1,
|
|
396
|
+
medication: 'Lisinopril 10mg',
|
|
397
|
+
medicationSku: 'LIS-010',
|
|
398
|
+
fill: '2',
|
|
399
|
+
}))
|
|
400
|
+
assert(res.status === 200, `Expected 200, got ${res.status}`)
|
|
401
|
+
|
|
402
|
+
const orders = await sdk.api.enduser_orders.getSome({
|
|
403
|
+
filter: { source: 'OpenLoop', externalId: v2OrderId }
|
|
404
|
+
})
|
|
405
|
+
assert(orders.length === 1, `Expected 1 order, got ${orders.length}`)
|
|
406
|
+
const order = orders[0]
|
|
407
|
+
assert(order.enduserId === enduser1.id, 'enduserId mismatch')
|
|
408
|
+
assert(order.status === 'created', `status mismatch: ${order.status}`)
|
|
409
|
+
assert(order.title === 'Lisinopril 10mg', `title mismatch: ${order.title}`)
|
|
410
|
+
assert(order.fill === '2', `fill mismatch: ${order.fill}`)
|
|
411
|
+
assert(order.sku === 'lis-010', `sku mismatch (should be lowercased): ${order.sku}`)
|
|
412
|
+
return true
|
|
413
|
+
},
|
|
414
|
+
{ onResult: (r: boolean) => r === true }
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
await async_test(
|
|
418
|
+
'V2: prescription-shipped updates with carrier and tracking',
|
|
419
|
+
async () => {
|
|
420
|
+
const res = await postV2(makeV2Payload('prescription-shipped', {
|
|
421
|
+
id: v2OrderId,
|
|
422
|
+
patientId: healthieId1,
|
|
423
|
+
carrier: 'USPS',
|
|
424
|
+
carrierTrackingId: 'V2-TRACK-001',
|
|
425
|
+
prescriptionCarrierDate: '2024-02-20',
|
|
426
|
+
pharmacy: 'CVS',
|
|
427
|
+
}))
|
|
428
|
+
assert(res.status === 200, `Expected 200, got ${res.status}`)
|
|
429
|
+
|
|
430
|
+
const orders = await sdk.api.enduser_orders.getSome({
|
|
431
|
+
filter: { source: 'OpenLoop', externalId: v2OrderId }
|
|
432
|
+
})
|
|
433
|
+
assert(orders.length === 1, `Expected 1 order, got ${orders.length}`)
|
|
434
|
+
const order = orders[0]
|
|
435
|
+
assert(order.status === 'shipped', `status mismatch: ${order.status}`)
|
|
436
|
+
assert(order.carrier === 'USPS', `carrier mismatch: ${order.carrier}`)
|
|
437
|
+
assert(order.tracking === 'V2-TRACK-001', `tracking mismatch: ${order.tracking}`)
|
|
438
|
+
assert(order.shippedDate === '2024-02-20', `shippedDate mismatch: ${order.shippedDate}`)
|
|
439
|
+
assert(order.pharmacy === 'CVS', `pharmacy mismatch: ${order.pharmacy}`)
|
|
440
|
+
return true
|
|
441
|
+
},
|
|
442
|
+
{ onResult: (r: boolean) => r === true }
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
const v2CancelId = `v2-cancel-${uid()}`
|
|
446
|
+
await async_test(
|
|
447
|
+
'V2: prescription-cancelled sets cancelledDate and reason',
|
|
448
|
+
async () => {
|
|
449
|
+
// Create first
|
|
450
|
+
await postV2(makeV2Payload('prescription-created', {
|
|
451
|
+
id: v2CancelId,
|
|
452
|
+
patientId: healthieId1,
|
|
453
|
+
}))
|
|
454
|
+
|
|
455
|
+
// Cancel
|
|
456
|
+
const res = await postV2(makeV2Payload('prescription-cancelled', {
|
|
457
|
+
id: v2CancelId,
|
|
458
|
+
patientId: healthieId1,
|
|
459
|
+
prescriptionCancelledDate: '2024-03-01',
|
|
460
|
+
prescriptionCancellationReason: 'Patient request',
|
|
461
|
+
}))
|
|
462
|
+
assert(res.status === 200, `Expected 200, got ${res.status}`)
|
|
463
|
+
|
|
464
|
+
const orders = await sdk.api.enduser_orders.getSome({
|
|
465
|
+
filter: { source: 'OpenLoop', externalId: v2CancelId }
|
|
466
|
+
})
|
|
467
|
+
assert(orders.length === 1, `Expected 1 order, got ${orders.length}`)
|
|
468
|
+
const order = orders[0]
|
|
469
|
+
assert(order.status === 'cancelled', `status mismatch: ${order.status}`)
|
|
470
|
+
assert(order.cancelledDate === '2024-03-01', `cancelledDate mismatch: ${order.cancelledDate}`)
|
|
471
|
+
assert(order.cancellationReason === 'Patient request', `cancellationReason mismatch: ${order.cancellationReason}`)
|
|
472
|
+
return true
|
|
473
|
+
},
|
|
474
|
+
{ onResult: (r: boolean) => r === true }
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
const v2RefundId = `v2-refund-${uid()}`
|
|
478
|
+
await async_test(
|
|
479
|
+
'V2: prescription-refunded sets status',
|
|
480
|
+
async () => {
|
|
481
|
+
await postV2(makeV2Payload('prescription-created', {
|
|
482
|
+
id: v2RefundId,
|
|
483
|
+
patientId: healthieId1,
|
|
484
|
+
}))
|
|
485
|
+
|
|
486
|
+
const res = await postV2(makeV2Payload('prescription-refunded', {
|
|
487
|
+
id: v2RefundId,
|
|
488
|
+
patientId: healthieId1,
|
|
489
|
+
prescriptionRefundedDate: '2024-04-01',
|
|
490
|
+
}))
|
|
491
|
+
assert(res.status === 200, `Expected 200, got ${res.status}`)
|
|
492
|
+
|
|
493
|
+
const orders = await sdk.api.enduser_orders.getSome({
|
|
494
|
+
filter: { source: 'OpenLoop', externalId: v2RefundId }
|
|
495
|
+
})
|
|
496
|
+
assert(orders.length === 1, `Expected 1 order, got ${orders.length}`)
|
|
497
|
+
assert(orders[0].status === 'refunded', `status mismatch: ${orders[0].status}`)
|
|
498
|
+
return true
|
|
499
|
+
},
|
|
500
|
+
{ onResult: (r: boolean) => r === true }
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
// ===== SECTION G: V2 Idempotency & Isolation =====
|
|
504
|
+
log_header("V2 Idempotency & Isolation")
|
|
505
|
+
|
|
506
|
+
await async_test(
|
|
507
|
+
'V2: idempotency - same id and patientId does not create duplicate',
|
|
508
|
+
async () => {
|
|
509
|
+
const idempotentId = `v2-idemp-${uid()}`
|
|
510
|
+
await postV2(makeV2Payload('prescription-created', {
|
|
511
|
+
id: idempotentId,
|
|
512
|
+
patientId: healthieId1,
|
|
513
|
+
}))
|
|
514
|
+
await postV2(makeV2Payload('prescription-created', {
|
|
515
|
+
id: idempotentId,
|
|
516
|
+
patientId: healthieId1,
|
|
517
|
+
}))
|
|
518
|
+
|
|
519
|
+
const orders = await sdk.api.enduser_orders.getSome({
|
|
520
|
+
filter: { source: 'OpenLoop', externalId: idempotentId }
|
|
521
|
+
})
|
|
522
|
+
assert(orders.length === 1, `Expected 1 order after duplicate, got ${orders.length}`)
|
|
523
|
+
return true
|
|
524
|
+
},
|
|
525
|
+
{ onResult: (r: boolean) => r === true }
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
await async_test(
|
|
529
|
+
'V2: same id for different patients creates separate orders',
|
|
530
|
+
async () => {
|
|
531
|
+
const sharedV2Id = `v2-shared-${uid()}`
|
|
532
|
+
const res1 = await postV2(makeV2Payload('prescription-created', {
|
|
533
|
+
id: sharedV2Id,
|
|
534
|
+
patientId: healthieId1,
|
|
535
|
+
}))
|
|
536
|
+
assert(res1.status === 200, `enduser1 create failed: ${res1.status}`)
|
|
537
|
+
|
|
538
|
+
const res2 = await postV2(makeV2Payload('prescription-created', {
|
|
539
|
+
id: sharedV2Id,
|
|
540
|
+
patientId: healthieId2,
|
|
541
|
+
}))
|
|
542
|
+
assert(res2.status === 200, `enduser2 create failed: ${res2.status}`)
|
|
543
|
+
|
|
544
|
+
const orders = await sdk.api.enduser_orders.getSome({
|
|
545
|
+
filter: { source: 'OpenLoop', externalId: sharedV2Id }
|
|
546
|
+
})
|
|
547
|
+
assert(orders.length === 2, `Expected 2 separate orders, got ${orders.length}`)
|
|
548
|
+
|
|
549
|
+
const enduserIds = orders.map(o => o.enduserId).sort()
|
|
550
|
+
const expected = [enduser1.id, enduser2.id].sort()
|
|
551
|
+
assert(
|
|
552
|
+
enduserIds[0] === expected[0] && enduserIds[1] === expected[1],
|
|
553
|
+
`Orders should belong to different endusers`
|
|
554
|
+
)
|
|
555
|
+
return true
|
|
556
|
+
},
|
|
557
|
+
{ onResult: (r: boolean) => r === true }
|
|
558
|
+
)
|
|
559
|
+
|
|
560
|
+
// ===== SECTION H: V2 Full Lifecycle =====
|
|
561
|
+
log_header("V2 Full Lifecycle")
|
|
562
|
+
|
|
563
|
+
await async_test(
|
|
564
|
+
'V2: sequential status progression through full lifecycle',
|
|
565
|
+
async () => {
|
|
566
|
+
const lifecycleId = `v2-full-${uid()}`
|
|
567
|
+
const base = { id: lifecycleId, patientId: healthieId1 }
|
|
568
|
+
|
|
569
|
+
const steps: { eventType: string, expectedStatus: string, extras?: Record<string, any> }[] = [
|
|
570
|
+
{ eventType: 'prescription-created', expectedStatus: 'created' },
|
|
571
|
+
{ eventType: 'prescription-invoiced', expectedStatus: 'invoiced' },
|
|
572
|
+
{ eventType: 'prescription-paid', expectedStatus: 'paid' },
|
|
573
|
+
{ eventType: 'prescription-ordered', expectedStatus: 'ordered' },
|
|
574
|
+
{ eventType: 'prescription-shipped', expectedStatus: 'shipped', extras: { carrier: 'FedEx', carrierTrackingId: 'FX-999' } },
|
|
575
|
+
]
|
|
576
|
+
|
|
577
|
+
for (const step of steps) {
|
|
578
|
+
const res = await postV2(makeV2Payload(step.eventType, { ...base, ...(step.extras || {}) }))
|
|
579
|
+
assert(res.status === 200, `${step.eventType} failed: ${res.status}`)
|
|
580
|
+
|
|
581
|
+
const orders = await sdk.api.enduser_orders.getSome({
|
|
582
|
+
filter: { source: 'OpenLoop', externalId: lifecycleId }
|
|
583
|
+
})
|
|
584
|
+
assert(orders.length === 1, `Expected 1 order at ${step.eventType}, got ${orders.length}`)
|
|
585
|
+
assert(
|
|
586
|
+
orders[0].status === step.expectedStatus,
|
|
587
|
+
`After ${step.eventType}: expected status '${step.expectedStatus}', got '${orders[0].status}'`
|
|
588
|
+
)
|
|
589
|
+
}
|
|
590
|
+
return true
|
|
591
|
+
},
|
|
592
|
+
{ onResult: (r: boolean) => r === true }
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
// ===== SECTION I: V2 Undefined Field Handling =====
|
|
596
|
+
log_header("V2 Undefined Field Handling")
|
|
597
|
+
|
|
598
|
+
await async_test(
|
|
599
|
+
'V2: optional fields not written when absent',
|
|
600
|
+
async () => {
|
|
601
|
+
const minimalId = `v2-minimal-${uid()}`
|
|
602
|
+
const res = await postV2({
|
|
603
|
+
id: minimalId,
|
|
604
|
+
client: 'test',
|
|
605
|
+
eventId: `evt-${uid()}`,
|
|
606
|
+
eventType: 'prescription-created',
|
|
607
|
+
chartId: 'chart-1',
|
|
608
|
+
patientId: healthieId1,
|
|
609
|
+
providerId: 'prov-1',
|
|
610
|
+
medication: 'Minimal Med',
|
|
611
|
+
medicationSku: 'min-sku',
|
|
612
|
+
fill: '1',
|
|
613
|
+
prescriptionCreatedDate: '2024-01-01',
|
|
614
|
+
// Intentionally omitting: pharmacy, carrier, carrierTrackingId,
|
|
615
|
+
// prescriptionCancelledDate, prescriptionCancellationReason, etc.
|
|
616
|
+
})
|
|
617
|
+
assert(res.status === 200, `Expected 200, got ${res.status}`)
|
|
618
|
+
|
|
619
|
+
const orders = await sdk.api.enduser_orders.getSome({
|
|
620
|
+
filter: { source: 'OpenLoop', externalId: minimalId }
|
|
621
|
+
})
|
|
622
|
+
assert(orders.length === 1, `Expected 1 order, got ${orders.length}`)
|
|
623
|
+
const order = orders[0]
|
|
624
|
+
assert(order.carrier === undefined, `carrier should be undefined, got: ${order.carrier}`)
|
|
625
|
+
assert(order.tracking === undefined, `tracking should be undefined, got: ${order.tracking}`)
|
|
626
|
+
assert(order.pharmacy === undefined, `pharmacy should be undefined, got: ${order.pharmacy}`)
|
|
627
|
+
assert(order.cancelledDate === undefined, `cancelledDate should be undefined, got: ${order.cancelledDate}`)
|
|
628
|
+
assert(order.cancellationReason === undefined, `cancellationReason should be undefined, got: ${order.cancellationReason}`)
|
|
629
|
+
return true
|
|
630
|
+
},
|
|
631
|
+
{ onResult: (r: boolean) => r === true }
|
|
632
|
+
)
|
|
633
|
+
|
|
634
|
+
console.log("All OpenLoop webhook tests passed!")
|
|
635
|
+
|
|
636
|
+
} finally {
|
|
637
|
+
await sdk.api.endusers.deleteOne(enduser1.id).catch(console.error)
|
|
638
|
+
await sdk.api.endusers.deleteOne(enduser2.id).catch(console.error)
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// Allow running this test file independently
|
|
643
|
+
if (require.main === module) {
|
|
644
|
+
console.log(`Using API URL: ${host}`)
|
|
645
|
+
const sdk = new Session({ host })
|
|
646
|
+
const sdkNonAdmin = new Session({ host })
|
|
647
|
+
|
|
648
|
+
const runTests = async () => {
|
|
649
|
+
await setup_tests(sdk, sdkNonAdmin)
|
|
650
|
+
await openloop_webhooks_tests({ sdk, sdkNonAdmin })
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
runTests()
|
|
654
|
+
.then(() => {
|
|
655
|
+
console.log("OpenLoop webhooks test suite completed successfully")
|
|
656
|
+
process.exit(0)
|
|
657
|
+
})
|
|
658
|
+
.catch((error) => {
|
|
659
|
+
console.error("OpenLoop webhooks test suite failed:", error)
|
|
660
|
+
process.exit(1)
|
|
661
|
+
})
|
|
662
|
+
}
|
package/src/tests/tests.ts
CHANGED
|
@@ -86,7 +86,9 @@ import { database_cascade_delete_tests } from "./api_tests/database_cascade_dele
|
|
|
86
86
|
import { ai_conversations_tests } from "./api_tests/ai_conversations.test";
|
|
87
87
|
import { load_team_chat_tests } from "./api_tests/load_team_chat.test";
|
|
88
88
|
import { form_started_trigger_tests } from "./api_tests/form_started_trigger.test";
|
|
89
|
+
import { medication_added_trigger_tests } from "./api_tests/medication_added_trigger.test";
|
|
89
90
|
import { elation_user_id_tests } from "./api_tests/elation_user_id.test";
|
|
91
|
+
import { openloop_webhooks_tests } from "./api_tests/openloop_webhooks.test";
|
|
90
92
|
|
|
91
93
|
const UniquenessViolationMessage = 'Uniqueness Violation'
|
|
92
94
|
|
|
@@ -5010,6 +5012,7 @@ const trigger_events_api_tests = async () => {
|
|
|
5010
5012
|
const automation_trigger_tests = async () => {
|
|
5011
5013
|
log_header("Automation Trigger Tests")
|
|
5012
5014
|
|
|
5015
|
+
await medication_added_trigger_tests({ sdk, sdkNonAdmin })
|
|
5013
5016
|
await order_status_equals_tests()
|
|
5014
5017
|
await appointment_cancelled_tests()
|
|
5015
5018
|
await set_fields_tests()
|
|
@@ -5026,7 +5029,7 @@ const automation_trigger_tests = async () => {
|
|
|
5026
5029
|
await appointment_created_tests()
|
|
5027
5030
|
await tag_added_tests()
|
|
5028
5031
|
await order_created_tests()
|
|
5029
|
-
await formSubmittedTriggerTests()
|
|
5032
|
+
await formSubmittedTriggerTests()
|
|
5030
5033
|
}
|
|
5031
5034
|
|
|
5032
5035
|
const form_response_tests = async () => {
|
|
@@ -14097,6 +14100,7 @@ const ip_address_form_tests = async () => {
|
|
|
14097
14100
|
await replace_enduser_template_values_tests()
|
|
14098
14101
|
await mfa_tests()
|
|
14099
14102
|
await setup_tests(sdk, sdkNonAdmin)
|
|
14103
|
+
await openloop_webhooks_tests({ sdk, sdkNonAdmin })
|
|
14100
14104
|
await automation_trigger_tests()
|
|
14101
14105
|
await get_some_projection_tests({ sdk, sdkNonAdmin })
|
|
14102
14106
|
await elation_user_id_tests({ sdk, sdkNonAdmin })
|
package/test_generated.pdf
CHANGED
|
Binary file
|