@tellescope/sdk 1.256.6 → 1.256.7

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 (115) 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_status_triggers.test.d.ts.map +1 -1
  21. package/lib/cjs/tests/api_tests/group_event_attendee_status_triggers.test.js +340 -124
  22. package/lib/cjs/tests/api_tests/group_event_attendee_status_triggers.test.js.map +1 -1
  23. package/lib/cjs/tests/api_tests/mdi_trigger_refill.test.d.ts +6 -0
  24. package/lib/cjs/tests/api_tests/mdi_trigger_refill.test.d.ts.map +1 -0
  25. package/lib/cjs/tests/api_tests/mdi_trigger_refill.test.js +277 -0
  26. package/lib/cjs/tests/api_tests/mdi_trigger_refill.test.js.map +1 -0
  27. package/lib/cjs/tests/api_tests/purchase_made_trigger.test.d.ts.map +1 -1
  28. package/lib/cjs/tests/api_tests/purchase_made_trigger.test.js +262 -78
  29. package/lib/cjs/tests/api_tests/purchase_made_trigger.test.js.map +1 -1
  30. package/lib/cjs/tests/api_tests/security/F-0006-develop-health-benefit-verification-rbac.test.d.ts +31 -0
  31. package/lib/cjs/tests/api_tests/security/F-0006-develop-health-benefit-verification-rbac.test.d.ts.map +1 -0
  32. package/lib/cjs/tests/api_tests/security/F-0006-develop-health-benefit-verification-rbac.test.js +230 -0
  33. package/lib/cjs/tests/api_tests/security/F-0006-develop-health-benefit-verification-rbac.test.js.map +1 -0
  34. package/lib/cjs/tests/api_tests/security/public_endpoint_auth_field_lock.test.d.ts.map +1 -1
  35. package/lib/cjs/tests/api_tests/security/public_endpoint_auth_field_lock.test.js +206 -198
  36. package/lib/cjs/tests/api_tests/security/public_endpoint_auth_field_lock.test.js.map +1 -1
  37. package/lib/cjs/tests/api_tests/stripe_webhook_triggers.test.d.ts.map +1 -1
  38. package/lib/cjs/tests/api_tests/stripe_webhook_triggers.test.js +343 -80
  39. package/lib/cjs/tests/api_tests/stripe_webhook_triggers.test.js.map +1 -1
  40. package/lib/cjs/tests/api_tests/welle_partner_token.test.d.ts.map +1 -1
  41. package/lib/cjs/tests/api_tests/welle_partner_token.test.js +74 -35
  42. package/lib/cjs/tests/api_tests/welle_partner_token.test.js.map +1 -1
  43. package/lib/cjs/tests/stripe_webhook_helpers.d.ts +134 -0
  44. package/lib/cjs/tests/stripe_webhook_helpers.d.ts.map +1 -1
  45. package/lib/cjs/tests/stripe_webhook_helpers.js +126 -1
  46. package/lib/cjs/tests/stripe_webhook_helpers.js.map +1 -1
  47. package/lib/cjs/tests/tests.d.ts.map +1 -1
  48. package/lib/cjs/tests/tests.js +238 -212
  49. package/lib/cjs/tests/tests.js.map +1 -1
  50. package/lib/esm/enduser.d.ts +1 -1
  51. package/lib/esm/enduser.d.ts.map +1 -1
  52. package/lib/esm/enduser.js +1 -0
  53. package/lib/esm/enduser.js.map +1 -1
  54. package/lib/esm/tests/api_tests/calendar_event_create_and_bulk_triggers.test.d.ts +6 -0
  55. package/lib/esm/tests/api_tests/calendar_event_create_and_bulk_triggers.test.d.ts.map +1 -0
  56. package/lib/esm/tests/api_tests/calendar_event_create_and_bulk_triggers.test.js +501 -0
  57. package/lib/esm/tests/api_tests/calendar_event_create_and_bulk_triggers.test.js.map +1 -0
  58. package/lib/esm/tests/api_tests/configurations_public_read.test.d.ts +6 -0
  59. package/lib/esm/tests/api_tests/configurations_public_read.test.d.ts.map +1 -0
  60. package/lib/esm/tests/api_tests/configurations_public_read.test.js +169 -0
  61. package/lib/esm/tests/api_tests/configurations_public_read.test.js.map +1 -0
  62. package/lib/esm/tests/api_tests/form_started_trigger.test.d.ts.map +1 -1
  63. package/lib/esm/tests/api_tests/form_started_trigger.test.js +87 -24
  64. package/lib/esm/tests/api_tests/form_started_trigger.test.js.map +1 -1
  65. package/lib/esm/tests/api_tests/form_translations.test.d.ts +6 -0
  66. package/lib/esm/tests/api_tests/form_translations.test.d.ts.map +1 -0
  67. package/lib/esm/tests/api_tests/form_translations.test.js +255 -0
  68. package/lib/esm/tests/api_tests/form_translations.test.js.map +1 -0
  69. package/lib/esm/tests/api_tests/group_event_attendee_status_triggers.test.d.ts.map +1 -1
  70. package/lib/esm/tests/api_tests/group_event_attendee_status_triggers.test.js +340 -124
  71. package/lib/esm/tests/api_tests/group_event_attendee_status_triggers.test.js.map +1 -1
  72. package/lib/esm/tests/api_tests/mdi_trigger_refill.test.d.ts +6 -0
  73. package/lib/esm/tests/api_tests/mdi_trigger_refill.test.d.ts.map +1 -0
  74. package/lib/esm/tests/api_tests/mdi_trigger_refill.test.js +273 -0
  75. package/lib/esm/tests/api_tests/mdi_trigger_refill.test.js.map +1 -0
  76. package/lib/esm/tests/api_tests/purchase_made_trigger.test.d.ts.map +1 -1
  77. package/lib/esm/tests/api_tests/purchase_made_trigger.test.js +262 -78
  78. package/lib/esm/tests/api_tests/purchase_made_trigger.test.js.map +1 -1
  79. package/lib/esm/tests/api_tests/security/F-0006-develop-health-benefit-verification-rbac.test.d.ts +31 -0
  80. package/lib/esm/tests/api_tests/security/F-0006-develop-health-benefit-verification-rbac.test.d.ts.map +1 -0
  81. package/lib/esm/tests/api_tests/security/F-0006-develop-health-benefit-verification-rbac.test.js +226 -0
  82. package/lib/esm/tests/api_tests/security/F-0006-develop-health-benefit-verification-rbac.test.js.map +1 -0
  83. package/lib/esm/tests/api_tests/security/public_endpoint_auth_field_lock.test.d.ts.map +1 -1
  84. package/lib/esm/tests/api_tests/security/public_endpoint_auth_field_lock.test.js +206 -198
  85. package/lib/esm/tests/api_tests/security/public_endpoint_auth_field_lock.test.js.map +1 -1
  86. package/lib/esm/tests/api_tests/stripe_webhook_triggers.test.d.ts.map +1 -1
  87. package/lib/esm/tests/api_tests/stripe_webhook_triggers.test.js +344 -81
  88. package/lib/esm/tests/api_tests/stripe_webhook_triggers.test.js.map +1 -1
  89. package/lib/esm/tests/api_tests/welle_partner_token.test.d.ts.map +1 -1
  90. package/lib/esm/tests/api_tests/welle_partner_token.test.js +74 -35
  91. package/lib/esm/tests/api_tests/welle_partner_token.test.js.map +1 -1
  92. package/lib/esm/tests/stripe_webhook_helpers.d.ts +134 -0
  93. package/lib/esm/tests/stripe_webhook_helpers.d.ts.map +1 -1
  94. package/lib/esm/tests/stripe_webhook_helpers.js +119 -0
  95. package/lib/esm/tests/stripe_webhook_helpers.js.map +1 -1
  96. package/lib/esm/tests/tests.d.ts.map +1 -1
  97. package/lib/esm/tests/tests.js +238 -212
  98. package/lib/esm/tests/tests.js.map +1 -1
  99. package/lib/tsconfig.tsbuildinfo +1 -1
  100. package/package.json +10 -10
  101. package/src/enduser.ts +4 -2
  102. package/src/tests/api_tests/calendar_event_create_and_bulk_triggers.test.ts +293 -0
  103. package/src/tests/api_tests/configurations_public_read.test.ts +116 -0
  104. package/src/tests/api_tests/form_started_trigger.test.ts +49 -1
  105. package/src/tests/api_tests/form_translations.test.ts +205 -0
  106. package/src/tests/api_tests/group_event_attendee_status_triggers.test.ts +135 -0
  107. package/src/tests/api_tests/mdi_trigger_refill.test.ts +184 -0
  108. package/src/tests/api_tests/purchase_made_trigger.test.ts +137 -0
  109. package/src/tests/api_tests/security/F-0006-develop-health-benefit-verification-rbac.test.ts +144 -0
  110. package/src/tests/api_tests/security/public_endpoint_auth_field_lock.test.ts +16 -6
  111. package/src/tests/api_tests/stripe_webhook_triggers.test.ts +221 -0
  112. package/src/tests/api_tests/welle_partner_token.test.ts +49 -6
  113. package/src/tests/stripe_webhook_helpers.ts +116 -0
  114. package/src/tests/tests.ts +27 -12
  115. package/test_generated.pdf +0 -0
@@ -0,0 +1,205 @@
1
+ require('source-map-support').install();
2
+
3
+ import { Session, EnduserSession } from "../../sdk"
4
+ import {
5
+ assert,
6
+ async_test,
7
+ handleAnyError,
8
+ log_header,
9
+ } from "@tellescope/testing"
10
+ import {
11
+ decodeTranslationMapFromStorage,
12
+ encodeTranslationMapForStorage,
13
+ objects_equivalent,
14
+ } from "@tellescope/utilities"
15
+ import { setup_tests } from "../setup"
16
+
17
+ const host = process.env.API_URL || 'http://localhost:8080' as const
18
+
19
+ // Modular test for AI form translations (display-language maps for v2 forms).
20
+ //
21
+ // Covers:
22
+ // 1. Storage round-trip: encodeTranslationMapForStorage survives the configurations.value escapeHTML
23
+ // validator (sanitize_html strips tags and entity-encodes & < >) for maps containing HTML, & < >,
24
+ // quotes, {{placeholders}}, and non-ASCII text.
25
+ // 2. Form.translationConfigurations (language -> configurationId) persistence.
26
+ // 3. public_form_details language param: returns the map only for a publicRead-flagged configuration
27
+ // on a form that allows a public URL.
28
+ // 4. submit_form_response viewedInLanguage: persisted as metadata while stored answers stay English.
29
+ //
30
+ // NOTE: this test deliberately does NOT invoke the AI translation endpoint (ai_conversations.send_message) —
31
+ // that path consumes AI credits. Map generation is exercised via manual map fixtures.
32
+ export const form_translations_tests = async ({ sdk } : { sdk: Session, sdkNonAdmin: Session }) => {
33
+ log_header("Form Translations Tests")
34
+
35
+ const ENGLISH_CHOICES = ['Salt & Pepper', '<b>Bold</b> choice', `It's "quoted"`, 'Résumé 中文']
36
+
37
+ // keys AND values include the characters sanitize_html would otherwise mangle
38
+ const TRANSLATION_MAP: Record<string, string> = {
39
+ 'What is your favorite seasoning?': '¿Cuál es tu condimento favorito?',
40
+ 'Salt & Pepper': 'Sal & Pimienta',
41
+ '<b>Bold</b> choice': '<b>Negrita</b> elección',
42
+ [`It's "quoted"`]: `Está "citado"`,
43
+ 'Résumé 中文': 'Currículum 中文',
44
+ 'Hello {{enduser.fname}} <br/> welcome': 'Hola {{enduser.fname}} <br/> bienvenido',
45
+ }
46
+
47
+ const createdConfigurationIds: string[] = []
48
+ let formId: string | undefined
49
+ let fieldId: string | undefined
50
+ let enduserId: string | undefined
51
+
52
+ try {
53
+ // --- 1. escapeHTML-safe storage round-trip (run first: it guards the whole storage design) ---
54
+ const encoded = encodeTranslationMapForStorage(TRANSLATION_MAP)
55
+ const configuration = await sdk.api.configurations.createOne({
56
+ type: 'form-translations:test:es',
57
+ value: encoded,
58
+ publicRead: true,
59
+ })
60
+ createdConfigurationIds.push(configuration.id)
61
+
62
+ const storedConfiguration = await sdk.api.configurations.getOne(configuration.id)
63
+ const decoded = decodeTranslationMapFromStorage(storedConfiguration.value)
64
+ assert(
65
+ objects_equivalent(decoded, TRANSLATION_MAP),
66
+ `translation map corrupted by storage: ${JSON.stringify(decoded)}`,
67
+ 'translation map with & < > quotes HTML and non-ASCII survives storage round-trip',
68
+ )
69
+
70
+ // an unflagged configuration, to verify the publicRead gate on public_form_details
71
+ const unflaggedConfiguration = await sdk.api.configurations.createOne({
72
+ type: 'form-translations:test:fr-unflagged',
73
+ value: encodeTranslationMapForStorage({ Hello: 'Bonjour' }),
74
+ })
75
+ createdConfigurationIds.push(unflaggedConfiguration.id)
76
+
77
+ // --- 2. form mapping persistence ---
78
+ const form = await sdk.api.forms.createOne({
79
+ title: 'Form Translations Test Form',
80
+ version: 'v2',
81
+ allowPublicURL: true,
82
+ })
83
+ formId = form.id
84
+
85
+ const field = await sdk.api.form_fields.createOne({
86
+ formId: form.id,
87
+ title: 'What is your favorite seasoning?',
88
+ type: 'multiple_choice',
89
+ options: { choices: ENGLISH_CHOICES, radio: true },
90
+ previousFields: [{ type: 'root', info: {} }],
91
+ })
92
+ fieldId = field.id
93
+
94
+ await sdk.api.forms.updateOne(form.id, {
95
+ translationConfigurations: [
96
+ { language: 'es', configurationId: configuration.id },
97
+ { language: 'fr', configurationId: unflaggedConfiguration.id },
98
+ ],
99
+ }, { replaceObjectFields: true })
100
+
101
+ const updatedForm = await sdk.api.forms.getOne(form.id)
102
+ assert(
103
+ updatedForm.translationConfigurations?.length === 2
104
+ && updatedForm.translationConfigurations.some(t => t.language === 'es' && t.configurationId === configuration.id),
105
+ `translationConfigurations not persisted: ${JSON.stringify(updatedForm.translationConfigurations)}`,
106
+ 'form translationConfigurations mapping persists',
107
+ )
108
+
109
+ // --- 3. public_form_details language param (unauthenticated) ---
110
+ const publicSDK = new EnduserSession({ host, businessId: sdk.userInfo.businessId })
111
+
112
+ await async_test(
113
+ 'public_form_details returns translations for a mapped language with a flagged configuration',
114
+ () => publicSDK.api.forms.public_form_details({ formId: form.id, language: 'es' }),
115
+ { onResult: r => objects_equivalent(r.translations, TRANSLATION_MAP) },
116
+ )
117
+ await async_test(
118
+ 'public_form_details returns no translations without a language param',
119
+ () => publicSDK.api.forms.public_form_details({ formId: form.id }),
120
+ { onResult: r => !r.translations || Object.keys(r.translations).length === 0 },
121
+ )
122
+ await async_test(
123
+ 'public_form_details returns no translations for an unmapped language',
124
+ () => publicSDK.api.forms.public_form_details({ formId: form.id, language: 'de' }),
125
+ { onResult: r => !r.translations || Object.keys(r.translations).length === 0 },
126
+ )
127
+ await async_test(
128
+ 'public_form_details returns no translations for an UNFLAGGED configuration (publicRead gate)',
129
+ () => publicSDK.api.forms.public_form_details({ formId: form.id, language: 'fr' }),
130
+ { onResult: r => !r.translations || Object.keys(r.translations).length === 0 },
131
+ )
132
+
133
+ // a form without allowPublicURL still rejects public_form_details entirely
134
+ await sdk.api.forms.updateOne(form.id, { allowPublicURL: false })
135
+ await async_test(
136
+ 'public_form_details still rejects forms without allowPublicURL',
137
+ () => publicSDK.api.forms.public_form_details({ formId: form.id, language: 'es' }),
138
+ handleAnyError,
139
+ )
140
+ await sdk.api.forms.updateOne(form.id, { allowPublicURL: true })
141
+
142
+ // --- 4. viewedInLanguage persists; stored answers remain English ---
143
+ const enduser = await sdk.api.endusers.createOne({
144
+ email: `form_translations_${Date.now()}@test.tellescope.com`,
145
+ })
146
+ enduserId = enduser.id
147
+
148
+ const { accessCode } = await sdk.api.form_responses.prepare_form_response({
149
+ formId: form.id, enduserId: enduser.id,
150
+ })
151
+
152
+ const { formResponse } = await sdk.api.form_responses.submit_form_response({
153
+ accessCode,
154
+ viewedInLanguage: 'es',
155
+ responses: [{
156
+ fieldId: field.id,
157
+ fieldTitle: field.title, // English snapshot, as the client submits it
158
+ answer: { type: 'multiple_choice', value: ['Salt & Pepper'] }, // English source value, never the translation
159
+ }],
160
+ })
161
+
162
+ const storedResponse = await sdk.api.form_responses.getOne(formResponse.id)
163
+ assert(
164
+ storedResponse.viewedInLanguage === 'es',
165
+ `viewedInLanguage not persisted: ${storedResponse.viewedInLanguage}`,
166
+ 'viewedInLanguage persists on the submitted form response',
167
+ )
168
+ const storedAnswer = storedResponse.responses?.find(r => r.fieldId === field.id)
169
+ assert(
170
+ Array.isArray(storedAnswer?.answer?.value)
171
+ && (storedAnswer!.answer.value as string[])[0] === 'Salt & Pepper'
172
+ && storedAnswer!.fieldTitle === 'What is your favorite seasoning?',
173
+ `stored response not English: ${JSON.stringify(storedAnswer)}`,
174
+ 'stored answer value and fieldTitle remain the English source strings',
175
+ )
176
+ } finally {
177
+ if (enduserId) await sdk.api.endusers.deleteOne(enduserId).catch(() => {})
178
+ if (fieldId) await sdk.api.form_fields.deleteOne(fieldId).catch(() => {})
179
+ if (formId) await sdk.api.forms.deleteOne(formId).catch(() => {})
180
+ for (const id of createdConfigurationIds) {
181
+ await sdk.api.configurations.deleteOne(id).catch(() => {})
182
+ }
183
+ }
184
+ }
185
+
186
+ // Allow running this test file independently
187
+ if (require.main === module) {
188
+ const sdk = new Session({ host })
189
+ const sdkNonAdmin = new Session({ host })
190
+
191
+ const runTests = async () => {
192
+ await setup_tests(sdk, sdkNonAdmin)
193
+ await form_translations_tests({ sdk, sdkNonAdmin })
194
+ }
195
+
196
+ runTests()
197
+ .then(() => {
198
+ console.log("✅ Form translations test suite completed successfully")
199
+ process.exit(0)
200
+ })
201
+ .catch((error) => {
202
+ console.error("❌ Form translations test suite failed:", error)
203
+ process.exit(1)
204
+ })
205
+ }
@@ -54,6 +54,11 @@ export const group_event_attendee_status_triggers_tests = async ({ sdk } : { sdk
54
54
  }, { replaceObjectFields: true })
55
55
  )
56
56
 
57
+ // Replicates the webapp "Mark Completed" action on the whole event (Calendar.tsx)
58
+ const markEventCompleted = async (eventId: string) => (
59
+ sdk.api.calendar_events.updateOne(eventId, { completedAt: new Date(), completedBy: sdk.userInfo.id })
60
+ )
61
+
57
62
  const tagsOf = async (enduserId: string) => (await sdk.api.endusers.getOne(enduserId)).tags || []
58
63
 
59
64
  try {
@@ -345,6 +350,136 @@ export const group_event_attendee_status_triggers_tests = async ({ sdk } : { sdk
345
350
  { onResult: (tags: string[]) => tags.length === 1 && tags.includes(TAG['No-showed']) }
346
351
  )
347
352
 
353
+ // ============================================================
354
+ // Marking the WHOLE event Complete must not fan "Appointment Completed" out to attendees who
355
+ // already have an explicit per-attendee status — they were resolved individually (and already
356
+ // fired their own trigger via handle_attendee_status_change_triggers). Attendees with no status,
357
+ // or 'Confirmed', are still active participants and must still fire.
358
+ //
359
+ // Each case uses fresh endusers and a fresh event: triggers are throttled for 60s per
360
+ // trigger-per-enduser, and the completion handler early-returns once completedAt is already set.
361
+ //
362
+ // Not covered here: per-attendee 'Completed' followed by event-wide completion. That duplicate
363
+ // fire is already swallowed by the 60s throttle (which is applied before the action, and Add Tags
364
+ // is idempotent), so the assertion would pass with or without the filter.
365
+ // ============================================================
366
+ log_header("Event-wide completion respects per-attendee statuses")
367
+
368
+ // status -> tag that must NOT appear on the target after the event is completed
369
+ for (const status of ['No-showed', 'Cancelled', 'Rescheduled'] as const) {
370
+ const target = await createEnduser()
371
+ const active = await createEnduser()
372
+ const event = await createGroupEvent([target.id, active.id])
373
+
374
+ await setAttendeeStatuses(event.id, [{ id: target.id, status }])
375
+ await wait(undefined, 750)
376
+
377
+ // pre-condition: the per-attendee status fired its own trigger
378
+ await async_test(
379
+ `event-wide completion setup: '${status}' attendee has the '${status}' tag`,
380
+ () => tagsOf(target.id),
381
+ { onResult: (tags: string[]) => tags.includes(TAG[status]) }
382
+ )
383
+
384
+ await markEventCompleted(event.id)
385
+ await wait(undefined, 1000)
386
+
387
+ await async_test(
388
+ `event-wide completion does NOT fire Completed for the '${status}' attendee`,
389
+ () => tagsOf(target.id),
390
+ { onResult: (tags: string[]) => !tags.includes(TAG['Completed']) }
391
+ )
392
+ await async_test(
393
+ `event-wide completion leaves the '${status}' attendee with only its own tag`,
394
+ () => tagsOf(target.id),
395
+ { onResult: (tags: string[]) => tags.length === 1 && tags.includes(TAG[status]) }
396
+ )
397
+ // guard against over-filtering: the un-statused attendee must still be completed
398
+ await async_test(
399
+ `event-wide completion still fires Completed for the active attendee (alongside '${status}')`,
400
+ () => tagsOf(active.id),
401
+ { onResult: (tags: string[]) => tags.includes(TAG['Completed']) }
402
+ )
403
+ }
404
+
405
+ // cancelledGroupAttendees with no attendeeStatuses entry — the shape written by the patient
406
+ // portal when an enduser cancels themselves out of a group event.
407
+ log_header("Event-wide completion respects legacy cancelledGroupAttendees")
408
+
409
+ const portalCancelled = await createEnduser()
410
+ const portalActive = await createEnduser()
411
+ const portalEvent = await createGroupEvent([portalCancelled.id, portalActive.id])
412
+
413
+ await sdk.api.calendar_events.updateOne(portalEvent.id, {
414
+ cancelledGroupAttendees: [{ id: portalCancelled.id, at: new Date() }],
415
+ }, { replaceObjectFields: true })
416
+ await wait(undefined, 750)
417
+
418
+ // pre-condition: the legacy branch fires Cancelled for them
419
+ await async_test(
420
+ "legacy cancelledGroupAttendees setup: attendee has the Cancelled tag",
421
+ () => tagsOf(portalCancelled.id),
422
+ { onResult: (tags: string[]) => tags.includes(TAG['Cancelled']) }
423
+ )
424
+
425
+ await markEventCompleted(portalEvent.id)
426
+ await wait(undefined, 1000)
427
+
428
+ await async_test(
429
+ "event-wide completion does NOT fire Completed for a cancelledGroupAttendees attendee",
430
+ () => tagsOf(portalCancelled.id),
431
+ { onResult: (tags: string[]) => !tags.includes(TAG['Completed']) }
432
+ )
433
+ await async_test(
434
+ "event-wide completion still fires Completed for the active attendee (legacy cancel case)",
435
+ () => tagsOf(portalActive.id),
436
+ { onResult: (tags: string[]) => tags.includes(TAG['Completed']) }
437
+ )
438
+
439
+ // ============================================================
440
+ // Over-filtering guards: 'Confirmed' and no-status attendees must still complete.
441
+ // ============================================================
442
+ log_header("Event-wide completion still fires for Confirmed / un-statused attendees")
443
+
444
+ const confirmed = await createEnduser()
445
+ const confirmedBystander = await createEnduser()
446
+ const confirmedEvent = await createGroupEvent([confirmed.id, confirmedBystander.id])
447
+
448
+ await setAttendeeStatuses(confirmedEvent.id, [{ id: confirmed.id, status: 'Confirmed' }])
449
+ await wait(undefined, 750)
450
+ await markEventCompleted(confirmedEvent.id)
451
+ await wait(undefined, 1000)
452
+
453
+ await async_test(
454
+ "event-wide completion fires Completed for a 'Confirmed' attendee",
455
+ () => tagsOf(confirmed.id),
456
+ { onResult: (tags: string[]) => tags.includes(TAG['Completed']) }
457
+ )
458
+ await async_test(
459
+ "event-wide completion fires Completed for the bystander alongside 'Confirmed'",
460
+ () => tagsOf(confirmedBystander.id),
461
+ { onResult: (tags: string[]) => tags.includes(TAG['Completed']) }
462
+ )
463
+
464
+ // baseline: a group event with no per-attendee statuses completes for everyone
465
+ const plain1 = await createEnduser()
466
+ const plain2 = await createEnduser()
467
+ const plain3 = await createEnduser()
468
+ const plainEvent = await createGroupEvent([plain1.id, plain2.id, plain3.id])
469
+
470
+ await markEventCompleted(plainEvent.id)
471
+ await wait(undefined, 1000)
472
+
473
+ const plainAttendees = [plain1, plain2, plain3]
474
+ for (let i = 0; i < plainAttendees.length; i++) {
475
+ const enduserId = plainAttendees[i].id
476
+ await async_test(
477
+ `event-wide completion fires Completed for un-statused attendee #${i + 1}`,
478
+ () => tagsOf(enduserId),
479
+ { onResult: (tags: string[]) => tags.includes(TAG['Completed']) }
480
+ )
481
+ }
482
+
348
483
  } finally {
349
484
  for (const id of createdEventIds) {
350
485
  try { await sdk.api.calendar_events.deleteOne(id) } catch (_) {}
@@ -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
+ }