@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.
- package/lib/cjs/enduser.d.ts +1 -1
- package/lib/cjs/enduser.d.ts.map +1 -1
- package/lib/cjs/enduser.js +1 -0
- package/lib/cjs/enduser.js.map +1 -1
- package/lib/cjs/tests/api_tests/calendar_event_create_and_bulk_triggers.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/calendar_event_create_and_bulk_triggers.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/calendar_event_create_and_bulk_triggers.test.js +505 -0
- package/lib/cjs/tests/api_tests/calendar_event_create_and_bulk_triggers.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/configurations_public_read.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/configurations_public_read.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/configurations_public_read.test.js +173 -0
- package/lib/cjs/tests/api_tests/configurations_public_read.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/form_started_trigger.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/form_started_trigger.test.js +87 -24
- package/lib/cjs/tests/api_tests/form_started_trigger.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/form_translations.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/form_translations.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/form_translations.test.js +259 -0
- package/lib/cjs/tests/api_tests/form_translations.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/group_event_attendee_status_triggers.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/group_event_attendee_status_triggers.test.js +340 -124
- package/lib/cjs/tests/api_tests/group_event_attendee_status_triggers.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/mdi_trigger_refill.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/mdi_trigger_refill.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/mdi_trigger_refill.test.js +277 -0
- package/lib/cjs/tests/api_tests/mdi_trigger_refill.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/purchase_made_trigger.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/purchase_made_trigger.test.js +262 -78
- package/lib/cjs/tests/api_tests/purchase_made_trigger.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/security/F-0006-develop-health-benefit-verification-rbac.test.d.ts +31 -0
- package/lib/cjs/tests/api_tests/security/F-0006-develop-health-benefit-verification-rbac.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/security/F-0006-develop-health-benefit-verification-rbac.test.js +230 -0
- package/lib/cjs/tests/api_tests/security/F-0006-develop-health-benefit-verification-rbac.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/security/public_endpoint_auth_field_lock.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/security/public_endpoint_auth_field_lock.test.js +206 -198
- package/lib/cjs/tests/api_tests/security/public_endpoint_auth_field_lock.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/stripe_webhook_triggers.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/stripe_webhook_triggers.test.js +343 -80
- package/lib/cjs/tests/api_tests/stripe_webhook_triggers.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/welle_partner_token.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/welle_partner_token.test.js +74 -35
- package/lib/cjs/tests/api_tests/welle_partner_token.test.js.map +1 -1
- package/lib/cjs/tests/stripe_webhook_helpers.d.ts +134 -0
- package/lib/cjs/tests/stripe_webhook_helpers.d.ts.map +1 -1
- package/lib/cjs/tests/stripe_webhook_helpers.js +126 -1
- package/lib/cjs/tests/stripe_webhook_helpers.js.map +1 -1
- package/lib/cjs/tests/tests.d.ts.map +1 -1
- package/lib/cjs/tests/tests.js +238 -212
- package/lib/cjs/tests/tests.js.map +1 -1
- package/lib/esm/enduser.d.ts +1 -1
- package/lib/esm/enduser.d.ts.map +1 -1
- package/lib/esm/enduser.js +1 -0
- package/lib/esm/enduser.js.map +1 -1
- package/lib/esm/tests/api_tests/calendar_event_create_and_bulk_triggers.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/calendar_event_create_and_bulk_triggers.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/calendar_event_create_and_bulk_triggers.test.js +501 -0
- package/lib/esm/tests/api_tests/calendar_event_create_and_bulk_triggers.test.js.map +1 -0
- package/lib/esm/tests/api_tests/configurations_public_read.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/configurations_public_read.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/configurations_public_read.test.js +169 -0
- package/lib/esm/tests/api_tests/configurations_public_read.test.js.map +1 -0
- package/lib/esm/tests/api_tests/form_started_trigger.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/form_started_trigger.test.js +87 -24
- package/lib/esm/tests/api_tests/form_started_trigger.test.js.map +1 -1
- package/lib/esm/tests/api_tests/form_translations.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/form_translations.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/form_translations.test.js +255 -0
- package/lib/esm/tests/api_tests/form_translations.test.js.map +1 -0
- package/lib/esm/tests/api_tests/group_event_attendee_status_triggers.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/group_event_attendee_status_triggers.test.js +340 -124
- package/lib/esm/tests/api_tests/group_event_attendee_status_triggers.test.js.map +1 -1
- package/lib/esm/tests/api_tests/mdi_trigger_refill.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/mdi_trigger_refill.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/mdi_trigger_refill.test.js +273 -0
- package/lib/esm/tests/api_tests/mdi_trigger_refill.test.js.map +1 -0
- package/lib/esm/tests/api_tests/purchase_made_trigger.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/purchase_made_trigger.test.js +262 -78
- package/lib/esm/tests/api_tests/purchase_made_trigger.test.js.map +1 -1
- package/lib/esm/tests/api_tests/security/F-0006-develop-health-benefit-verification-rbac.test.d.ts +31 -0
- package/lib/esm/tests/api_tests/security/F-0006-develop-health-benefit-verification-rbac.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/security/F-0006-develop-health-benefit-verification-rbac.test.js +226 -0
- package/lib/esm/tests/api_tests/security/F-0006-develop-health-benefit-verification-rbac.test.js.map +1 -0
- package/lib/esm/tests/api_tests/security/public_endpoint_auth_field_lock.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/security/public_endpoint_auth_field_lock.test.js +206 -198
- package/lib/esm/tests/api_tests/security/public_endpoint_auth_field_lock.test.js.map +1 -1
- package/lib/esm/tests/api_tests/stripe_webhook_triggers.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/stripe_webhook_triggers.test.js +344 -81
- package/lib/esm/tests/api_tests/stripe_webhook_triggers.test.js.map +1 -1
- package/lib/esm/tests/api_tests/welle_partner_token.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/welle_partner_token.test.js +74 -35
- package/lib/esm/tests/api_tests/welle_partner_token.test.js.map +1 -1
- package/lib/esm/tests/stripe_webhook_helpers.d.ts +134 -0
- package/lib/esm/tests/stripe_webhook_helpers.d.ts.map +1 -1
- package/lib/esm/tests/stripe_webhook_helpers.js +119 -0
- package/lib/esm/tests/stripe_webhook_helpers.js.map +1 -1
- package/lib/esm/tests/tests.d.ts.map +1 -1
- package/lib/esm/tests/tests.js +238 -212
- package/lib/esm/tests/tests.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +10 -10
- package/src/enduser.ts +4 -2
- package/src/tests/api_tests/calendar_event_create_and_bulk_triggers.test.ts +293 -0
- package/src/tests/api_tests/configurations_public_read.test.ts +116 -0
- package/src/tests/api_tests/form_started_trigger.test.ts +49 -1
- package/src/tests/api_tests/form_translations.test.ts +205 -0
- package/src/tests/api_tests/group_event_attendee_status_triggers.test.ts +135 -0
- package/src/tests/api_tests/mdi_trigger_refill.test.ts +184 -0
- package/src/tests/api_tests/purchase_made_trigger.test.ts +137 -0
- package/src/tests/api_tests/security/F-0006-develop-health-benefit-verification-rbac.test.ts +144 -0
- package/src/tests/api_tests/security/public_endpoint_auth_field_lock.test.ts +16 -6
- package/src/tests/api_tests/stripe_webhook_triggers.test.ts +221 -0
- package/src/tests/api_tests/welle_partner_token.test.ts +49 -6
- package/src/tests/stripe_webhook_helpers.ts +116 -0
- package/src/tests/tests.ts +27 -12
- 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
|
+
}
|