@tellescope/sdk 1.256.6 → 1.256.8

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 (128) hide show
  1. package/lib/cjs/enduser.d.ts +1 -1
  2. package/lib/cjs/enduser.d.ts.map +1 -1
  3. package/lib/cjs/enduser.js +1 -0
  4. package/lib/cjs/enduser.js.map +1 -1
  5. package/lib/cjs/tests/api_tests/calendar_event_create_and_bulk_triggers.test.d.ts +6 -0
  6. package/lib/cjs/tests/api_tests/calendar_event_create_and_bulk_triggers.test.d.ts.map +1 -0
  7. package/lib/cjs/tests/api_tests/calendar_event_create_and_bulk_triggers.test.js +505 -0
  8. package/lib/cjs/tests/api_tests/calendar_event_create_and_bulk_triggers.test.js.map +1 -0
  9. package/lib/cjs/tests/api_tests/configurations_public_read.test.d.ts +6 -0
  10. package/lib/cjs/tests/api_tests/configurations_public_read.test.d.ts.map +1 -0
  11. package/lib/cjs/tests/api_tests/configurations_public_read.test.js +173 -0
  12. package/lib/cjs/tests/api_tests/configurations_public_read.test.js.map +1 -0
  13. package/lib/cjs/tests/api_tests/form_started_trigger.test.d.ts.map +1 -1
  14. package/lib/cjs/tests/api_tests/form_started_trigger.test.js +87 -24
  15. package/lib/cjs/tests/api_tests/form_started_trigger.test.js.map +1 -1
  16. package/lib/cjs/tests/api_tests/form_translations.test.d.ts +6 -0
  17. package/lib/cjs/tests/api_tests/form_translations.test.d.ts.map +1 -0
  18. package/lib/cjs/tests/api_tests/form_translations.test.js +259 -0
  19. package/lib/cjs/tests/api_tests/form_translations.test.js.map +1 -0
  20. package/lib/cjs/tests/api_tests/group_event_attendee_self_update.test.d.ts +6 -0
  21. package/lib/cjs/tests/api_tests/group_event_attendee_self_update.test.d.ts.map +1 -0
  22. package/lib/cjs/tests/api_tests/group_event_attendee_self_update.test.js +293 -0
  23. package/lib/cjs/tests/api_tests/group_event_attendee_self_update.test.js.map +1 -0
  24. package/lib/cjs/tests/api_tests/group_event_attendee_status_triggers.test.d.ts.map +1 -1
  25. package/lib/cjs/tests/api_tests/group_event_attendee_status_triggers.test.js +340 -124
  26. package/lib/cjs/tests/api_tests/group_event_attendee_status_triggers.test.js.map +1 -1
  27. package/lib/cjs/tests/api_tests/mdi_trigger_refill.test.d.ts +6 -0
  28. package/lib/cjs/tests/api_tests/mdi_trigger_refill.test.d.ts.map +1 -0
  29. package/lib/cjs/tests/api_tests/mdi_trigger_refill.test.js +277 -0
  30. package/lib/cjs/tests/api_tests/mdi_trigger_refill.test.js.map +1 -0
  31. package/lib/cjs/tests/api_tests/purchase_made_trigger.test.d.ts.map +1 -1
  32. package/lib/cjs/tests/api_tests/purchase_made_trigger.test.js +262 -78
  33. package/lib/cjs/tests/api_tests/purchase_made_trigger.test.js.map +1 -1
  34. package/lib/cjs/tests/api_tests/security/F-0006-develop-health-benefit-verification-rbac.test.d.ts +31 -0
  35. package/lib/cjs/tests/api_tests/security/F-0006-develop-health-benefit-verification-rbac.test.d.ts.map +1 -0
  36. package/lib/cjs/tests/api_tests/security/F-0006-develop-health-benefit-verification-rbac.test.js +315 -0
  37. package/lib/cjs/tests/api_tests/security/F-0006-develop-health-benefit-verification-rbac.test.js.map +1 -0
  38. package/lib/cjs/tests/api_tests/security/public_endpoint_auth_field_lock.test.d.ts.map +1 -1
  39. package/lib/cjs/tests/api_tests/security/public_endpoint_auth_field_lock.test.js +206 -198
  40. package/lib/cjs/tests/api_tests/security/public_endpoint_auth_field_lock.test.js.map +1 -1
  41. package/lib/cjs/tests/api_tests/stripe_webhook_triggers.test.d.ts.map +1 -1
  42. package/lib/cjs/tests/api_tests/stripe_webhook_triggers.test.js +343 -80
  43. package/lib/cjs/tests/api_tests/stripe_webhook_triggers.test.js.map +1 -1
  44. package/lib/cjs/tests/api_tests/welle_partner_token.test.d.ts.map +1 -1
  45. package/lib/cjs/tests/api_tests/welle_partner_token.test.js +74 -35
  46. package/lib/cjs/tests/api_tests/welle_partner_token.test.js.map +1 -1
  47. package/lib/cjs/tests/stripe_webhook_helpers.d.ts +134 -0
  48. package/lib/cjs/tests/stripe_webhook_helpers.d.ts.map +1 -1
  49. package/lib/cjs/tests/stripe_webhook_helpers.js +126 -1
  50. package/lib/cjs/tests/stripe_webhook_helpers.js.map +1 -1
  51. package/lib/cjs/tests/tests.d.ts.map +1 -1
  52. package/lib/cjs/tests/tests.js +242 -212
  53. package/lib/cjs/tests/tests.js.map +1 -1
  54. package/lib/esm/enduser.d.ts +2 -1
  55. package/lib/esm/enduser.d.ts.map +1 -1
  56. package/lib/esm/enduser.js +1 -0
  57. package/lib/esm/enduser.js.map +1 -1
  58. package/lib/esm/sdk.d.ts +3 -2
  59. package/lib/esm/sdk.d.ts.map +1 -1
  60. package/lib/esm/session.d.ts +1 -0
  61. package/lib/esm/session.d.ts.map +1 -1
  62. package/lib/esm/tests/api_tests/calendar_event_create_and_bulk_triggers.test.d.ts +6 -0
  63. package/lib/esm/tests/api_tests/calendar_event_create_and_bulk_triggers.test.d.ts.map +1 -0
  64. package/lib/esm/tests/api_tests/calendar_event_create_and_bulk_triggers.test.js +501 -0
  65. package/lib/esm/tests/api_tests/calendar_event_create_and_bulk_triggers.test.js.map +1 -0
  66. package/lib/esm/tests/api_tests/configurations_public_read.test.d.ts +6 -0
  67. package/lib/esm/tests/api_tests/configurations_public_read.test.d.ts.map +1 -0
  68. package/lib/esm/tests/api_tests/configurations_public_read.test.js +169 -0
  69. package/lib/esm/tests/api_tests/configurations_public_read.test.js.map +1 -0
  70. package/lib/esm/tests/api_tests/form_started_trigger.test.d.ts.map +1 -1
  71. package/lib/esm/tests/api_tests/form_started_trigger.test.js +87 -24
  72. package/lib/esm/tests/api_tests/form_started_trigger.test.js.map +1 -1
  73. package/lib/esm/tests/api_tests/form_translations.test.d.ts +6 -0
  74. package/lib/esm/tests/api_tests/form_translations.test.d.ts.map +1 -0
  75. package/lib/esm/tests/api_tests/form_translations.test.js +255 -0
  76. package/lib/esm/tests/api_tests/form_translations.test.js.map +1 -0
  77. package/lib/esm/tests/api_tests/group_event_attendee_self_update.test.d.ts +6 -0
  78. package/lib/esm/tests/api_tests/group_event_attendee_self_update.test.d.ts.map +1 -0
  79. package/lib/esm/tests/api_tests/group_event_attendee_self_update.test.js +289 -0
  80. package/lib/esm/tests/api_tests/group_event_attendee_self_update.test.js.map +1 -0
  81. package/lib/esm/tests/api_tests/group_event_attendee_status_triggers.test.d.ts.map +1 -1
  82. package/lib/esm/tests/api_tests/group_event_attendee_status_triggers.test.js +340 -124
  83. package/lib/esm/tests/api_tests/group_event_attendee_status_triggers.test.js.map +1 -1
  84. package/lib/esm/tests/api_tests/mdi_trigger_refill.test.d.ts +6 -0
  85. package/lib/esm/tests/api_tests/mdi_trigger_refill.test.d.ts.map +1 -0
  86. package/lib/esm/tests/api_tests/mdi_trigger_refill.test.js +273 -0
  87. package/lib/esm/tests/api_tests/mdi_trigger_refill.test.js.map +1 -0
  88. package/lib/esm/tests/api_tests/purchase_made_trigger.test.d.ts.map +1 -1
  89. package/lib/esm/tests/api_tests/purchase_made_trigger.test.js +262 -78
  90. package/lib/esm/tests/api_tests/purchase_made_trigger.test.js.map +1 -1
  91. package/lib/esm/tests/api_tests/security/F-0006-develop-health-benefit-verification-rbac.test.d.ts +31 -0
  92. package/lib/esm/tests/api_tests/security/F-0006-develop-health-benefit-verification-rbac.test.d.ts.map +1 -0
  93. package/lib/esm/tests/api_tests/security/F-0006-develop-health-benefit-verification-rbac.test.js +311 -0
  94. package/lib/esm/tests/api_tests/security/F-0006-develop-health-benefit-verification-rbac.test.js.map +1 -0
  95. package/lib/esm/tests/api_tests/security/public_endpoint_auth_field_lock.test.d.ts.map +1 -1
  96. package/lib/esm/tests/api_tests/security/public_endpoint_auth_field_lock.test.js +206 -198
  97. package/lib/esm/tests/api_tests/security/public_endpoint_auth_field_lock.test.js.map +1 -1
  98. package/lib/esm/tests/api_tests/stripe_webhook_triggers.test.d.ts.map +1 -1
  99. package/lib/esm/tests/api_tests/stripe_webhook_triggers.test.js +344 -81
  100. package/lib/esm/tests/api_tests/stripe_webhook_triggers.test.js.map +1 -1
  101. package/lib/esm/tests/api_tests/welle_partner_token.test.d.ts.map +1 -1
  102. package/lib/esm/tests/api_tests/welle_partner_token.test.js +74 -35
  103. package/lib/esm/tests/api_tests/welle_partner_token.test.js.map +1 -1
  104. package/lib/esm/tests/stripe_webhook_helpers.d.ts +134 -0
  105. package/lib/esm/tests/stripe_webhook_helpers.d.ts.map +1 -1
  106. package/lib/esm/tests/stripe_webhook_helpers.js +119 -0
  107. package/lib/esm/tests/stripe_webhook_helpers.js.map +1 -1
  108. package/lib/esm/tests/tests.d.ts.map +1 -1
  109. package/lib/esm/tests/tests.js +242 -212
  110. package/lib/esm/tests/tests.js.map +1 -1
  111. package/lib/tsconfig.tsbuildinfo +1 -1
  112. package/package.json +10 -10
  113. package/src/enduser.ts +4 -2
  114. package/src/tests/api_tests/calendar_event_create_and_bulk_triggers.test.ts +293 -0
  115. package/src/tests/api_tests/configurations_public_read.test.ts +116 -0
  116. package/src/tests/api_tests/form_started_trigger.test.ts +49 -1
  117. package/src/tests/api_tests/form_translations.test.ts +205 -0
  118. package/src/tests/api_tests/group_event_attendee_self_update.test.ts +161 -0
  119. package/src/tests/api_tests/group_event_attendee_status_triggers.test.ts +135 -0
  120. package/src/tests/api_tests/mdi_trigger_refill.test.ts +184 -0
  121. package/src/tests/api_tests/purchase_made_trigger.test.ts +137 -0
  122. package/src/tests/api_tests/security/F-0006-develop-health-benefit-verification-rbac.test.ts +204 -0
  123. package/src/tests/api_tests/security/public_endpoint_auth_field_lock.test.ts +16 -6
  124. package/src/tests/api_tests/stripe_webhook_triggers.test.ts +221 -0
  125. package/src/tests/api_tests/welle_partner_token.test.ts +49 -6
  126. package/src/tests/stripe_webhook_helpers.ts +116 -0
  127. package/src/tests/tests.ts +29 -12
  128. package/test_generated.pdf +0 -0
@@ -0,0 +1,184 @@
1
+ require('source-map-support').install();
2
+
3
+ import { Session } from "../../sdk"
4
+ import {
5
+ async_test,
6
+ log_header,
7
+ passOnAnyResult,
8
+ } from "@tellescope/testing"
9
+ import { MD_INTEGRATIONS_TITLE } from "@tellescope/constants"
10
+ import { setup_tests } from "../setup"
11
+
12
+ const host = process.env.API_URL || 'http://localhost:8080' as const
13
+
14
+ // MDI Trigger Refill journey action tests.
15
+ //
16
+ // Validator-acceptance plus handler-guard coverage: verifies that automation steps configured
17
+ // with the new mdiTriggerRefill action pass validation (and invalid shapes are rejected), and
18
+ // exercises the process endpoint's guard branches that return BEFORE any live MDI call (missing
19
+ // integration, missing patient reference) — no sandbox credentials needed. The happy path
20
+ // (fetch cases -> find scheduled orders -> submit) requires an MDI sandbox with a
21
+ // Direct-to-Pharmacy refill sequence and is covered by manual QA.
22
+ export const mdi_trigger_refill_tests = async ({ sdk, sdkNonAdmin }: { sdk: Session, sdkNonAdmin: Session }) => {
23
+ log_header("MDI Trigger Refill Journey Action Tests")
24
+
25
+ let journeyId: string | undefined
26
+ const stepIds: string[] = []
27
+ const automatedActionIds: string[] = []
28
+ let enduserId: string | undefined
29
+ let createdMDIIntegrationId: string | undefined
30
+
31
+ try {
32
+ const journey = await sdk.api.journeys.createOne({ title: 'MDI Trigger Refill Test Journey' })
33
+ journeyId = journey.id
34
+
35
+ // Full config accepted
36
+ await async_test(
37
+ "mdiTriggerRefill step accepted (onlySubmitWhenDue)",
38
+ () => sdk.api.automation_steps.createOne({
39
+ journeyId: journey.id,
40
+ events: [{ type: 'onJourneyStart', info: {} }],
41
+ action: {
42
+ type: 'mdiTriggerRefill',
43
+ info: { onlySubmitWhenDue: true },
44
+ },
45
+ }).then(s => { stepIds.push(s.id); return s }),
46
+ passOnAnyResult,
47
+ )
48
+
49
+ // Empty info accepted (all fields optional)
50
+ await async_test(
51
+ "mdiTriggerRefill step accepted (empty info)",
52
+ () => sdk.api.automation_steps.createOne({
53
+ journeyId: journey.id,
54
+ events: [{ type: 'onJourneyStart', info: {} }],
55
+ action: {
56
+ type: 'mdiTriggerRefill',
57
+ info: {},
58
+ },
59
+ }).then(s => { stepIds.push(s.id); return s }),
60
+ passOnAnyResult,
61
+ )
62
+
63
+ // Wrong type for onlySubmitWhenDue rejected
64
+ await async_test(
65
+ "mdiTriggerRefill step rejected (non-boolean onlySubmitWhenDue)",
66
+ () => sdk.api.automation_steps.createOne({
67
+ journeyId: journey.id,
68
+ events: [{ type: 'onJourneyStart', info: {} }],
69
+ action: {
70
+ type: 'mdiTriggerRefill',
71
+ info: { onlySubmitWhenDue: 'yes' },
72
+ } as any,
73
+ }),
74
+ { shouldError: true, onError: () => true },
75
+ )
76
+
77
+ // Unexpected info fields are stripped by objectValidator, not rejected — assert they never
78
+ // reach the stored config (the step itself is accepted)
79
+ await async_test(
80
+ "mdiTriggerRefill step strips unexpected info field",
81
+ () => sdk.api.automation_steps.createOne({
82
+ journeyId: journey.id,
83
+ events: [{ type: 'onJourneyStart', info: {} }],
84
+ action: {
85
+ type: 'mdiTriggerRefill',
86
+ info: { onlySubmitWhenDue: false, formId: 'not-allowed' },
87
+ } as any,
88
+ }).then(s => { stepIds.push(s.id); return s }),
89
+ {
90
+ onResult: s => (
91
+ (s.action.info as any).onlySubmitWhenDue === false
92
+ && (s.action.info as any).formId === undefined
93
+ ),
94
+ },
95
+ )
96
+
97
+ // ── Handler guards (process endpoint) ────────────────────────────────────────
98
+ const enduser = await sdk.api.endusers.createOne({ fname: 'MDI', lname: 'RefillPatient' })
99
+ enduserId = enduser.id
100
+
101
+ // processAfter is set in the future so no background worker picks these up before the explicit call
102
+ const createAutomatedAction = () => sdk.api.automated_actions.createOne({
103
+ journeyId: journey!.id,
104
+ automationStepId: stepIds[0],
105
+ enduserId: enduser.id,
106
+ event: { type: 'onJourneyStart', info: {} },
107
+ action: { type: 'mdiTriggerRefill', info: {} },
108
+ status: 'active',
109
+ processAfter: new Date().getTime() + 60 * 60 * 1000,
110
+ } as any).then(a => { automatedActionIds.push(a.id); return a })
111
+
112
+ let mdiIntegrationExists = false
113
+ try {
114
+ const existing = await sdk.api.integrations.load_redacted({})
115
+ mdiIntegrationExists = !!existing.integrations.find((i: any) => i.title === MD_INTEGRATIONS_TITLE)
116
+ } catch { /* load not permitted — assume absent */ }
117
+
118
+ if (!mdiIntegrationExists) {
119
+ const noIntegrationAction = await createAutomatedAction()
120
+ await async_test(
121
+ "mdiTriggerRefill fails cleanly without an MDI integration",
122
+ () => sdk.api.automated_actions.process({ automatedActionId: noIntegrationAction.id }),
123
+ { onResult: (r: any) => r?.success === false && /MD Integrations integration not found/i.test(r?.error || '') },
124
+ )
125
+
126
+ try {
127
+ const created = await sdk.api.integrations.createOne({
128
+ title: MD_INTEGRATIONS_TITLE,
129
+ authentication: {
130
+ type: 'oauth2',
131
+ info: { access_token: 'test-access-token', refresh_token: 'test-refresh-token', scope: '', token_type: 'Bearer', expiry_date: new Date().getTime() },
132
+ },
133
+ } as any)
134
+ createdMDIIntegrationId = created.id
135
+ mdiIntegrationExists = true
136
+ } catch (e) {
137
+ console.log("Could not create an MDI integration; skipping patient-reference guard test:", e)
138
+ }
139
+ } else {
140
+ console.log("⏭️ Skipping mdiTriggerRefill missing-integration guard test (an MDI integration already exists)")
141
+ }
142
+
143
+ if (mdiIntegrationExists) {
144
+ // enduser has no MD Integrations reference — the guard returns before any outbound MDI call
145
+ const noReferenceAction = await createAutomatedAction()
146
+ await async_test(
147
+ "mdiTriggerRefill fails cleanly for an enduser with no MDI patient reference",
148
+ () => sdk.api.automated_actions.process({ automatedActionId: noReferenceAction.id }),
149
+ { onResult: (r: any) => r?.success === false && /no MD Integrations patient reference/i.test(r?.error || '') },
150
+ )
151
+ }
152
+ } finally {
153
+ for (const id of automatedActionIds) {
154
+ await sdk.api.automated_actions.deleteOne(id).catch(console.error)
155
+ }
156
+ if (enduserId) await sdk.api.endusers.deleteOne(enduserId).catch(console.error)
157
+ if (createdMDIIntegrationId) await sdk.api.integrations.deleteOne(createdMDIIntegrationId).catch(console.error)
158
+ for (const id of stepIds) {
159
+ await sdk.api.automation_steps.deleteOne(id).catch(console.error)
160
+ }
161
+ if (journeyId) await sdk.api.journeys.deleteOne(journeyId).catch(console.error)
162
+ }
163
+ }
164
+
165
+ if (require.main === module) {
166
+ console.log(`Using API URL: ${host}`)
167
+ const sdk = new Session({ host })
168
+ const sdkNonAdmin = new Session({ host })
169
+
170
+ const runTests = async () => {
171
+ await setup_tests(sdk, sdkNonAdmin)
172
+ await mdi_trigger_refill_tests({ sdk, sdkNonAdmin })
173
+ }
174
+
175
+ runTests()
176
+ .then(() => {
177
+ console.log("✅ MDI trigger refill test suite completed successfully")
178
+ process.exit(0)
179
+ })
180
+ .catch((error) => {
181
+ console.error("❌ MDI trigger refill test suite failed:", error)
182
+ process.exit(1)
183
+ })
184
+ }
@@ -111,6 +111,37 @@ export const purchase_made_trigger_tests = async ({ sdk, sdkNonAdmin } : { sdk:
111
111
  const enduser13 = await sdk.api.endusers.createOne({})
112
112
  const enduser14 = await sdk.api.endusers.createOne({})
113
113
 
114
+ // couponCodes filter triggers
115
+ const t10 = await sdk.api.automation_triggers.createOne({
116
+ event: { type: 'Purchase Made', info: { couponCodes: ['SUMMER20'] } },
117
+ action: { type: 'Add Tags', info: { tags: ['Purchase-Coupon-Summer20'] }},
118
+ status: 'Active',
119
+ title: "Purchase - Coupon SUMMER20"
120
+ })
121
+
122
+ // Empty couponCodes array should behave like no filter
123
+ const t11 = await sdk.api.automation_triggers.createOne({
124
+ event: { type: 'Purchase Made', info: { couponCodes: [] } },
125
+ action: { type: 'Add Tags', info: { tags: ['Purchase-Coupon-Empty'] }},
126
+ status: 'Active',
127
+ title: "Purchase - Empty couponCodes Array"
128
+ })
129
+
130
+ // Combined filter: title AND coupon must both match
131
+ const t12 = await sdk.api.automation_triggers.createOne({
132
+ event: { type: 'Purchase Made', info: { titles: ['Premium Course Package'], couponCodes: ['SUMMER20'] } },
133
+ action: { type: 'Add Tags', info: { tags: ['Purchase-Title-And-Coupon'] }},
134
+ status: 'Active',
135
+ title: "Purchase - Title and Coupon"
136
+ })
137
+
138
+ const enduser15 = await sdk.api.endusers.createOne({})
139
+ const enduser16 = await sdk.api.endusers.createOne({})
140
+ const enduser17 = await sdk.api.endusers.createOne({})
141
+ const enduser18 = await sdk.api.endusers.createOne({})
142
+ const enduser19 = await sdk.api.endusers.createOne({})
143
+ const enduser20 = await sdk.api.endusers.createOne({})
144
+
114
145
  // Test 1: Purchase without specific filters (should trigger t1 for any purchase)
115
146
  const purchase1 = await sdk.api.purchases.createOne({
116
147
  enduserId: enduser1.id,
@@ -321,6 +352,103 @@ export const purchase_made_trigger_tests = async ({ sdk, sdkNonAdmin } : { sdk:
321
352
  { onResult: (e: Enduser) => !!e.tags?.includes('Purchase-Any') && !e.tags?.includes('Purchase-No-Title-Test') }
322
353
  )
323
354
 
355
+ // Test 15: exact coupon code match fires the coupon-scoped trigger
356
+ const purchase15 = await sdk.api.purchases.createOne({
357
+ enduserId: enduser15.id,
358
+ title: 'Coupon Test Purchase',
359
+ cost: { amount: 9999, currency: 'USD' },
360
+ processor: 'Stripe',
361
+ couponCodes: ['SUMMER20'],
362
+ })
363
+ await wait(undefined, 500)
364
+ await async_test(
365
+ "Purchase Made - Coupon Code Match (exact)",
366
+ () => sdk.api.endusers.getOne(enduser15.id),
367
+ { onResult: (e: Enduser) => !!e.tags?.includes('Purchase-Coupon-Summer20') && !!e.tags?.includes('Purchase-Coupon-Empty') }
368
+ )
369
+
370
+ // Test 16: coupon match is case-sensitive — Stripe returns the canonical merchant-defined
371
+ // code, so a differently-cased value must NOT fire the coupon-scoped trigger
372
+ const purchase16 = await sdk.api.purchases.createOne({
373
+ enduserId: enduser16.id,
374
+ title: 'Coupon Test Purchase',
375
+ cost: { amount: 9999, currency: 'USD' },
376
+ processor: 'Stripe',
377
+ couponCodes: ['summer20'],
378
+ })
379
+ await wait(undefined, 500)
380
+ await async_test(
381
+ "Purchase Made - Coupon Code Case Mismatch (case sensitive, 'summer20' vs 'SUMMER20')",
382
+ () => sdk.api.endusers.getOne(enduser16.id),
383
+ { onResult: (e: Enduser) => !!e.tags?.includes('Purchase-Any') && !e.tags?.includes('Purchase-Coupon-Summer20') }
384
+ )
385
+
386
+ // Test 17: a different coupon code does NOT fire the coupon-scoped trigger
387
+ const purchase17 = await sdk.api.purchases.createOne({
388
+ enduserId: enduser17.id,
389
+ title: 'Coupon Test Purchase',
390
+ cost: { amount: 9999, currency: 'USD' },
391
+ processor: 'Stripe',
392
+ couponCodes: ['WINTER10'],
393
+ })
394
+ await wait(undefined, 500)
395
+ await async_test(
396
+ "Purchase Made - Non-matching Coupon Code",
397
+ () => sdk.api.endusers.getOne(enduser17.id),
398
+ { onResult: (e: Enduser) => !!e.tags?.includes('Purchase-Any') && !e.tags?.includes('Purchase-Coupon-Summer20') }
399
+ )
400
+
401
+ // Test 18: a purchase with no couponCodes never matches a coupon-scoped trigger, but
402
+ // unscoped and empty-array coupon triggers still fire (backward compat)
403
+ const purchase18 = await sdk.api.purchases.createOne({
404
+ enduserId: enduser18.id,
405
+ title: 'Coupon Test Purchase',
406
+ cost: { amount: 9999, currency: 'USD' },
407
+ processor: 'Stripe',
408
+ })
409
+ await wait(undefined, 500)
410
+ await async_test(
411
+ "Purchase Made - No Coupon Data (backward compat)",
412
+ () => sdk.api.endusers.getOne(enduser18.id),
413
+ { onResult: (e: Enduser) => !!e.tags?.includes('Purchase-Any') && !!e.tags?.includes('Purchase-Coupon-Empty') && !e.tags?.includes('Purchase-Coupon-Summer20') }
414
+ )
415
+
416
+ // Test 19: combined titles + couponCodes filter fires when both match
417
+ const purchase19 = await sdk.api.purchases.createOne({
418
+ enduserId: enduser19.id,
419
+ title: 'Premium Course Package',
420
+ cost: { amount: 9999, currency: 'USD' },
421
+ processor: 'Stripe',
422
+ couponCodes: ['SUMMER20'],
423
+ })
424
+ await wait(undefined, 500)
425
+ await async_test(
426
+ "Purchase Made - Title AND Coupon Match",
427
+ () => sdk.api.endusers.getOne(enduser19.id),
428
+ { onResult: (e: Enduser) => !!e.tags?.includes('Purchase-Title-And-Coupon') }
429
+ )
430
+
431
+ // Test 20: combined filter does NOT fire when the title matches but the coupon does not
432
+ const purchase20 = await sdk.api.purchases.createOne({
433
+ enduserId: enduser20.id,
434
+ title: 'Premium Course Package',
435
+ cost: { amount: 9999, currency: 'USD' },
436
+ processor: 'Stripe',
437
+ })
438
+ await wait(undefined, 500)
439
+ await async_test(
440
+ "Purchase Made - Title Matches But Coupon Missing (AND semantics)",
441
+ () => sdk.api.endusers.getOne(enduser20.id),
442
+ { onResult: (e: Enduser) => !!e.tags?.includes('Purchase-Exact-Title') && !e.tags?.includes('Purchase-Title-And-Coupon') }
443
+ )
444
+
445
+ // Validator round-trip: couponCodes persists on the trigger's event.info
446
+ await async_test(
447
+ "Purchase Made - couponCodes round-trips on trigger info",
448
+ () => sdk.api.automation_triggers.getOne(t10.id),
449
+ { onResult: t => JSON.stringify((t.event as any)?.info?.couponCodes) === JSON.stringify(['SUMMER20']) }
450
+ )
451
+
324
452
  try {
325
453
  // Clean up test data - triggers first
326
454
  await sdk.api.automation_triggers.deleteOne(t1.id)
@@ -332,6 +460,9 @@ export const purchase_made_trigger_tests = async ({ sdk, sdkNonAdmin } : { sdk:
332
460
  await sdk.api.automation_triggers.deleteOne(t7.id)
333
461
  await sdk.api.automation_triggers.deleteOne(t8.id)
334
462
  await sdk.api.automation_triggers.deleteOne(t9.id)
463
+ await sdk.api.automation_triggers.deleteOne(t10.id)
464
+ await sdk.api.automation_triggers.deleteOne(t11.id)
465
+ await sdk.api.automation_triggers.deleteOne(t12.id)
335
466
 
336
467
  // Clean up products
337
468
  await sdk.api.products.deleteOne(product1.id)
@@ -353,6 +484,12 @@ export const purchase_made_trigger_tests = async ({ sdk, sdkNonAdmin } : { sdk:
353
484
  await sdk.api.endusers.deleteOne(enduser12.id)
354
485
  await sdk.api.endusers.deleteOne(enduser13.id)
355
486
  await sdk.api.endusers.deleteOne(enduser14.id)
487
+ await sdk.api.endusers.deleteOne(enduser15.id)
488
+ await sdk.api.endusers.deleteOne(enduser16.id)
489
+ await sdk.api.endusers.deleteOne(enduser17.id)
490
+ await sdk.api.endusers.deleteOne(enduser18.id)
491
+ await sdk.api.endusers.deleteOne(enduser19.id)
492
+ await sdk.api.endusers.deleteOne(enduser20.id)
356
493
  } finally {}
357
494
  }
358
495
 
@@ -0,0 +1,204 @@
1
+ require('source-map-support').install();
2
+
3
+ import { ObjectId } from 'bson'
4
+ import { Session } from "../../../sdk"
5
+ import {
6
+ async_test,
7
+ log_header,
8
+ wait,
9
+ } from "@tellescope/testing"
10
+ import { setup_tests } from "../../setup"
11
+ import { PROVIDER_PERMISSIONS } from "@tellescope/constants"
12
+
13
+ const host = process.env.API_URL || 'http://localhost:8080' as const
14
+ const [nonAdminEmail, nonAdminPassword] = [process.env.NON_ADMIN_EMAIL, process.env.NON_ADMIN_PASSWORD]
15
+
16
+ /**
17
+ * Regression test for F-0006 (security-audit/findings/F-0006-zoom-and-benefit-verification-bypass-rbac.md),
18
+ * reopened 2026-08-17 for the `develop_health_run_benefit_verification` sub-item.
19
+ *
20
+ * `enduser_eligibility_results.develop_health_run_benefit_verification` is registered with
21
+ * `noAccessPermissions: true` (packages/private/api/api/v1/api.ts, ~line 24606). Before the fix,
22
+ * it had no in-handler check on `session.access?.endusers?.read` /
23
+ * `session.access?.enduser_eligibility_results?.create`, so it loaded the caller-supplied
24
+ * enduser's PHI via `DB.endusers.findById` and forwarded it to the Develop Health integration
25
+ * regardless of the caller's role. It also let a zero-access caller fingerprint an enduser's
26
+ * profile completeness via the distinct validation error messages returned for missing fields.
27
+ *
28
+ * This test:
29
+ * 1. Creates a role with explicit NO_ACCESS for `endusers` and `enduser_eligibility_results`.
30
+ * 2. Assigns the role to the non-admin user and re-authenticates.
31
+ * 3. Calls the endpoint with a fake enduserId — asserts a 403-equivalent access error, not the
32
+ * validation-error / integration-not-found path that would run pre-fix.
33
+ *
34
+ * Pre-fix: the handler proceeds past `DB.endusers.findById` and either 400s with a
35
+ * `validate_enduser_for_develop_health` message (enduser not found — since the id is fake) or,
36
+ * for a real target id, would load PHI and forward it to Develop Health. Either way, the access
37
+ * check never fires. Test fails (no access-denial signal).
38
+ *
39
+ * Post-fix: the handler throws 403 before touching `DB.endusers` at all. Test passes.
40
+ */
41
+ export const develop_health_benefit_verification_rbac_tests = async ({ sdk, sdkNonAdmin } : { sdk: Session, sdkNonAdmin: Session }) => {
42
+ log_header("F-0006: develop_health benefit verification RBAC regression")
43
+
44
+ const roleName = `f0006-develop-health-no-access-${Date.now()}`
45
+ let rbapId: string | undefined
46
+ const originalRoles = sdkNonAdmin.userInfo.roles
47
+
48
+ try {
49
+ // 1. Create a role that explicitly denies endusers access AND enduser_eligibility_results
50
+ // access. This is the realistic configuration the bypass affects: a tenant operator
51
+ // deliberately restricts a role from reading enduser PHI (e.g. for PHI-restricted
52
+ // integration/API-key roles).
53
+ const rbap = await sdk.api.role_based_access_permissions.createOne({
54
+ role: roleName,
55
+ permissions: {
56
+ ...PROVIDER_PERMISSIONS,
57
+ endusers: { create: null, read: null, update: null, delete: null },
58
+ enduser_eligibility_results: { create: null, read: null, update: null, delete: null },
59
+ },
60
+ })
61
+ rbapId = rbap.id
62
+
63
+ // 2. Assign the role to the non-admin user and re-authenticate so the new
64
+ // session reflects the role's denied permissions.
65
+ await sdk.api.users.updateOne(
66
+ sdkNonAdmin.userInfo.id,
67
+ { roles: [roleName] },
68
+ { replaceObjectFields: true },
69
+ )
70
+ await wait(undefined, 1500)
71
+ await sdkNonAdmin.authenticate(nonAdminEmail!, nonAdminPassword!)
72
+
73
+ // 3. develop_health_run_benefit_verification must throw 403 BEFORE any enduser lookup.
74
+ // Pre-fix: proceeds to DB.endusers.findById and either 400s on validation (fake id) or
75
+ // succeeds and forwards PHI to Develop Health (real id) — never a 403.
76
+ await async_test(
77
+ "F-0006: enduser_eligibility_results.develop_health_run_benefit_verification must throw 403 when role denies endusers/enduser_eligibility_results",
78
+ () => sdkNonAdmin.api.enduser_eligibility_results.develop_health_run_benefit_verification({
79
+ enduserId: new ObjectId().toHexString(), // fake id — access check should fire first
80
+ providerUserId: new ObjectId().toHexString(), // fake id — access check should fire first
81
+ insuranceType: 'primary',
82
+ diagnoses: [],
83
+ // drugs uses a plain (non-empty-ok) list validator, so a real-shaped entry is required
84
+ // to pass request validation and actually reach the handler's access check.
85
+ drugs: [{ name: 'Test Drug', dosage: '10mg', quantity: 1 }],
86
+ drug_history: { currently_taking_drugs: [], previously_taken_drugs: [] },
87
+ } as any),
88
+ {
89
+ shouldError: true,
90
+ onError: (e: any) => {
91
+ // Match the gate's exact message. The SDK error surfaces { message, info } (no top-level
92
+ // status/code), so we key on the message rather than the HTTP status. This is deliberately
93
+ // specific: a lapsed subscription ("Your subscription has expired") or an expired token
94
+ // ("Unauthenticated") also errors BEFORE the handler runs, but neither contains this string,
95
+ // so the test can't false-pass without the gate executing. Also reject the pre-fix
96
+ // downstream messages, which would indicate the access check never fired.
97
+ const msg = (e?.message ?? '').toLowerCase()
98
+ if (msg.includes('integration not found') || msg.includes('enduser is required')
99
+ || msg.includes('organization') || msg.includes('provider')) {
100
+ return false // reached past the access gate — regression
101
+ }
102
+ return msg.includes('access to this resource')
103
+ },
104
+ },
105
+ )
106
+
107
+ // 4. POSITIVE case: a provider role WITH enduser access must PASS the gate and reach the
108
+ // integration stage (this test org has no Develop Health integration, so it 400s with
109
+ // "Integration not found"). This guards against the gate over-blocking legitimate users —
110
+ // the failure mode a future tightening (e.g. gating on 'All') would introduce.
111
+ const providerRoleName = `f0006-develop-health-provider-${Date.now()}`
112
+ let providerRbapId: string | undefined
113
+ try {
114
+ const providerRbap = await sdk.api.role_based_access_permissions.createOne({
115
+ role: providerRoleName,
116
+ permissions: { ...PROVIDER_PERMISSIONS }, // endusers read/create non-null, eligibility create non-null
117
+ })
118
+ providerRbapId = providerRbap.id
119
+
120
+ await sdk.api.users.updateOne(sdkNonAdmin.userInfo.id, { roles: [providerRoleName] }, { replaceObjectFields: true })
121
+ await wait(undefined, 1500)
122
+ await sdkNonAdmin.authenticate(nonAdminEmail!, nonAdminPassword!)
123
+
124
+ await async_test(
125
+ "F-0006: a provider role with enduser access passes the gate (reaches 'Integration not found', not 403)",
126
+ () => sdkNonAdmin.api.enduser_eligibility_results.develop_health_run_benefit_verification({
127
+ enduserId: new ObjectId().toHexString(),
128
+ providerUserId: new ObjectId().toHexString(),
129
+ insuranceType: 'primary',
130
+ diagnoses: [],
131
+ drugs: [{ name: 'Test Drug', dosage: '10mg', quantity: 1 }],
132
+ drug_history: { currently_taking_drugs: [], previously_taken_drugs: [] },
133
+ } as any),
134
+ {
135
+ shouldError: true,
136
+ onError: (e: any) => {
137
+ const msg = (e?.message ?? '').toLowerCase()
138
+ // Must NOT be the access-denial message (that would mean the gate wrongly blocked a
139
+ // provider). Reaching "Integration not found" (or org/provider validation) proves the
140
+ // gate passed and the read-scoped enduser load did not over-restrict a legitimate role.
141
+ if (msg.includes('access to this resource')) return false
142
+ return msg.includes('integration not found') || msg.includes('organization') || msg.includes('provider')
143
+ },
144
+ },
145
+ )
146
+ } finally {
147
+ if (providerRbapId) {
148
+ try {
149
+ await sdk.api.users.updateOne(sdkNonAdmin.userInfo.id, { roles: originalRoles ?? [] }, { replaceObjectFields: true })
150
+ } catch {}
151
+ try { await sdk.api.role_based_access_permissions.deleteOne(providerRbapId) } catch {}
152
+ await wait(undefined, 1500)
153
+ try { await sdkNonAdmin.authenticate(nonAdminEmail!, nonAdminPassword!) } catch {}
154
+ }
155
+ }
156
+
157
+ // NOTE — the create>read bypass (endusers.create broader than endusers.read) that the 2026-08-19
158
+ // read-scoping closes is NOT live-testable here: this org has no Develop Health integration, so
159
+ // the handler throws "Integration not found" BEFORE reaching the (now read-scoped) enduser load,
160
+ // and the enduser load is what the create>read fix governs. Exercising it would need either a real
161
+ // Develop Health integration or reordering the handler purely for the test. It is covered by code
162
+ // review + F-0166 (systemic follow-up). If a Develop Health sandbox integration is ever available
163
+ // in the test org, add: role { endusers: create 'All', read 'Assigned' } → call with a real
164
+ // enduser NOT assigned to the caller → expect the enduser-not-found path, not a successful forward.
165
+ } finally {
166
+ // Cleanup: restore original roles, delete the test role.
167
+ try {
168
+ await sdk.api.users.updateOne(
169
+ sdkNonAdmin.userInfo.id,
170
+ { roles: originalRoles ?? [] },
171
+ { replaceObjectFields: true },
172
+ )
173
+ } catch {}
174
+ if (rbapId) {
175
+ try { await sdk.api.role_based_access_permissions.deleteOne(rbapId) } catch {}
176
+ }
177
+ // Re-authenticate the non-admin to drop the no-access role from their JWT
178
+ // before subsequent tests run.
179
+ await wait(undefined, 1500)
180
+ try { await sdkNonAdmin.authenticate(nonAdminEmail!, nonAdminPassword!) } catch {}
181
+ }
182
+ }
183
+
184
+ // Allow running this test file independently
185
+ if (require.main === module) {
186
+ console.log(`🌐 Using API URL: ${host}`)
187
+ const sdk = new Session({ host })
188
+ const sdkNonAdmin = new Session({ host })
189
+
190
+ const runTests = async () => {
191
+ await setup_tests(sdk, sdkNonAdmin)
192
+ await develop_health_benefit_verification_rbac_tests({ sdk, sdkNonAdmin })
193
+ }
194
+
195
+ runTests()
196
+ .then(() => {
197
+ console.log("✅ F-0006 develop_health_run_benefit_verification RBAC test suite completed successfully")
198
+ process.exit(0)
199
+ })
200
+ .catch((error) => {
201
+ console.error("❌ F-0006 develop_health_run_benefit_verification RBAC test suite failed:", error)
202
+ process.exit(1)
203
+ })
204
+ }
@@ -327,12 +327,22 @@ export const public_endpoint_auth_field_lock_tests = async (
327
327
 
328
328
  // Throwaway host user with all-7-day availability, so G9 has slots to find without depending on
329
329
  // (or mutating) the shared test user's calendar configuration.
330
- const hostUser = await sdk.api.users.createOne({
331
- email: emailFor('lock-host'),
332
- verifiedEmail: true,
333
- fname: 'Lock',
334
- lname: `Host${RUN}`,
335
- })
330
+ // Stable email + get-or-create (rather than a per-run address): the cleanup below is not in a
331
+ // finally, and reset_db does not clear the users collection, so an interrupted run strands this
332
+ // user permanently. Reusing one address means at most one can ever exist — otherwise every
333
+ // interrupted run adds another provider to org-wide availability queries, which are capped at 25
334
+ // users sorted newest-first and would eventually evict the real test users.
335
+ const hostEmail = 'lock-host@tellescope.com'
336
+ const hostUser = (
337
+ await sdk.api.users.getOne({ email: hostEmail }).catch(() => null)
338
+ ) || (
339
+ await sdk.api.users.createOne({
340
+ email: hostEmail,
341
+ verifiedEmail: true,
342
+ fname: 'Lock',
343
+ lname: 'Host',
344
+ })
345
+ )
336
346
  createdUserIds.push(hostUser.id)
337
347
  await sdk.api.users.updateOne(hostUser.id, {
338
348
  weeklyAvailabilities: [0, 1, 2, 3, 4, 5, 6].map(dayOfWeekStartingSundayIndexedByZero => ({