@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.
Files changed (28) hide show
  1. package/lib/cjs/tests/api_tests/medication_added_trigger.test.d.ts +6 -0
  2. package/lib/cjs/tests/api_tests/medication_added_trigger.test.d.ts.map +1 -0
  3. package/lib/cjs/tests/api_tests/medication_added_trigger.test.js +452 -0
  4. package/lib/cjs/tests/api_tests/medication_added_trigger.test.js.map +1 -0
  5. package/lib/cjs/tests/api_tests/openloop_webhooks.test.d.ts +6 -0
  6. package/lib/cjs/tests/api_tests/openloop_webhooks.test.d.ts.map +1 -0
  7. package/lib/cjs/tests/api_tests/openloop_webhooks.test.js +833 -0
  8. package/lib/cjs/tests/api_tests/openloop_webhooks.test.js.map +1 -0
  9. package/lib/cjs/tests/tests.d.ts.map +1 -1
  10. package/lib/cjs/tests/tests.js +142 -134
  11. package/lib/cjs/tests/tests.js.map +1 -1
  12. package/lib/esm/tests/api_tests/medication_added_trigger.test.d.ts +6 -0
  13. package/lib/esm/tests/api_tests/medication_added_trigger.test.d.ts.map +1 -0
  14. package/lib/esm/tests/api_tests/medication_added_trigger.test.js +448 -0
  15. package/lib/esm/tests/api_tests/medication_added_trigger.test.js.map +1 -0
  16. package/lib/esm/tests/api_tests/openloop_webhooks.test.d.ts +6 -0
  17. package/lib/esm/tests/api_tests/openloop_webhooks.test.d.ts.map +1 -0
  18. package/lib/esm/tests/api_tests/openloop_webhooks.test.js +829 -0
  19. package/lib/esm/tests/api_tests/openloop_webhooks.test.js.map +1 -0
  20. package/lib/esm/tests/tests.d.ts.map +1 -1
  21. package/lib/esm/tests/tests.js +142 -134
  22. package/lib/esm/tests/tests.js.map +1 -1
  23. package/lib/tsconfig.tsbuildinfo +1 -1
  24. package/package.json +10 -10
  25. package/src/tests/api_tests/medication_added_trigger.test.ts +306 -0
  26. package/src/tests/api_tests/openloop_webhooks.test.ts +662 -0
  27. package/src/tests/tests.ts +5 -1
  28. 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
+ }
@@ -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 })
Binary file