@tellescope/sdk 1.255.4 → 1.255.6

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.
@@ -0,0 +1,286 @@
1
+ require('source-map-support').install();
2
+
3
+ import axios from "axios"
4
+ import { Session } from "../../sdk"
5
+ import {
6
+ assert,
7
+ async_test,
8
+ log_header,
9
+ wait,
10
+ } from "@tellescope/testing"
11
+ import { evaluate_conditional_logic_for_enduser_fields } from "@tellescope/utilities"
12
+ import { setup_tests } from "../setup"
13
+ import { BUILT_INS_FOR_SET_FIELDS, HEALTHIE_TITLE } from "@tellescope/constants"
14
+
15
+ const host = process.env.API_URL || 'http://localhost:8080' as const
16
+
17
+ const TAG = 'clinic-two'
18
+ const FORMSORT_TEST_KEY = '9d4f9dff00f60df2690a16da2cb848f289b447614ad9bef850e54af09a1fbf7a'
19
+
20
+ export const healthie_multi_integration_tests = async ({ sdk, sdkNonAdmin } : { sdk: Session, sdkNonAdmin: Session }) => {
21
+ log_header("Multiple Healthie Integrations Tests")
22
+
23
+ const businessId = sdk.userInfo.businessId
24
+
25
+ let primaryIntegrationId = ''
26
+ let taggedIntegrationId = ''
27
+ let enduserId = ''
28
+ let formsortEnduserId = ''
29
+ let formId = ''
30
+
31
+ try {
32
+ // ── Set Fields support ────────────────────────────────────────────────────────────────────
33
+ if (!BUILT_INS_FOR_SET_FIELDS.includes('healthieIntegrationId')) {
34
+ throw new Error("healthieIntegrationId missing from BUILT_INS_FOR_SET_FIELDS")
35
+ }
36
+ console.log("✅ healthieIntegrationId exposed in BUILT_INS_FOR_SET_FIELDS")
37
+
38
+ // ── Conditional logic evaluation (pure — resolves via the generic built-in field fallback) ──
39
+ const conditionPlaceholders = {
40
+ businessId: '', creator: '', hashedPassword: '', lastActive: '', lastLogout: '', updatedAt: new Date(),
41
+ }
42
+ const taggedForConditions = { ...conditionPlaceholders, healthieIntegrationId: TAG } as any
43
+ const untaggedForConditions = { ...conditionPlaceholders } as any
44
+
45
+ assert(
46
+ evaluate_conditional_logic_for_enduser_fields(taggedForConditions, { $and: [{ condition: { healthieIntegrationId: TAG } }] }),
47
+ 'Conditional logic error', 'healthieIntegrationId equality matches a tagged patient',
48
+ )
49
+ assert(
50
+ !evaluate_conditional_logic_for_enduser_fields(taggedForConditions, { $and: [{ condition: { healthieIntegrationId: 'other-clinic' } }] }),
51
+ 'Conditional logic error', 'healthieIntegrationId equality rejects a different id',
52
+ )
53
+ assert(
54
+ !evaluate_conditional_logic_for_enduser_fields(untaggedForConditions, { $and: [{ condition: { healthieIntegrationId: TAG } }] }),
55
+ 'Conditional logic error', 'healthieIntegrationId equality rejects an unset (primary) patient',
56
+ )
57
+ assert(
58
+ evaluate_conditional_logic_for_enduser_fields(taggedForConditions, { $and: [{ condition: { healthieIntegrationId: { $isSet: true } } }] }),
59
+ 'Conditional logic error', 'healthieIntegrationId $isSet matches a tagged patient',
60
+ )
61
+ assert(
62
+ evaluate_conditional_logic_for_enduser_fields(untaggedForConditions, { $and: [{ condition: { healthieIntegrationId: { $isNotSet: true } } }] }),
63
+ 'Conditional logic error', 'healthieIntegrationId $isNotSet matches an unset (primary) patient',
64
+ )
65
+ assert(
66
+ !evaluate_conditional_logic_for_enduser_fields(taggedForConditions, { $and: [{ condition: { healthieIntegrationId: { $ne: TAG } } }] }),
67
+ 'Conditional logic error', 'healthieIntegrationId $ne rejects the matching tag',
68
+ )
69
+ assert(
70
+ evaluate_conditional_logic_for_enduser_fields(untaggedForConditions, { $and: [{ condition: { healthieIntegrationId: { $ne: TAG } } }] }),
71
+ 'Conditional logic error', 'healthieIntegrationId $ne matches an unset (primary) patient',
72
+ )
73
+ console.log("✅ healthieIntegrationId evaluates in conditional logic (equality, $isSet/$isNotSet, $ne)")
74
+
75
+ // ── Create primary (untagged, sandbox-style key) + additional (tagged, production-style key)
76
+ // via the standard REST create endpoint — the real creation path for additional connections ──
77
+ const primary = await sdk.api.integrations.createOne({
78
+ title: HEALTHIE_TITLE,
79
+ disableEnduserAutoSync: true, // fake keys — avoid auto-sync attempts during the test
80
+ disableTicketAutoSync: true,
81
+ authentication: {
82
+ type: 'apiKey',
83
+ info: { access_token: 'gh_sbox_fake_primary_key', refresh_token: 'unused', scope: '', token_type: 'Bearer', expiry_date: 0 },
84
+ },
85
+ })
86
+ primaryIntegrationId = primary.id
87
+
88
+ const tagged = await sdk.api.integrations.createOne({
89
+ title: HEALTHIE_TITLE,
90
+ tenantId: TAG,
91
+ disableEnduserAutoSync: true,
92
+ disableTicketAutoSync: true,
93
+ authentication: {
94
+ type: 'apiKey',
95
+ info: { access_token: 'fake_production_key', refresh_token: 'unused', scope: '', token_type: 'Bearer', expiry_date: 0 },
96
+ },
97
+ })
98
+ taggedIntegrationId = tagged.id
99
+
100
+ if (tagged.tenantId !== TAG) {
101
+ throw new Error(`tenantId not persisted on create (got "${tagged.tenantId}")`)
102
+ }
103
+ console.log("✅ tenantId persists via standard integrations create")
104
+
105
+ // ── Per-connection settings: update_settings works against a tagged integration ──
106
+ await async_test(
107
+ "update_settings updates a tagged integration",
108
+ () => sdk.api.integrations.update_settings({ id: taggedIntegrationId, updates: { pushAddedTags: true } }),
109
+ { onResult: r => r.integration?.pushAddedTags === true },
110
+ )
111
+ await async_test(
112
+ "tagged integration settings persist independently",
113
+ () => sdk.api.integrations.getOne(taggedIntegrationId),
114
+ { onResult: i => i.pushAddedTags === true && i.tenantId === TAG },
115
+ )
116
+ await async_test(
117
+ "primary integration unaffected by tagged settings update",
118
+ () => sdk.api.integrations.getOne(primaryIntegrationId),
119
+ { onResult: i => !i.pushAddedTags },
120
+ )
121
+
122
+ // ── Invalid API key surfaces as an actionable 400 (fake key → Healthie rejects the request) ──
123
+ await async_test(
124
+ "proxy_read webhooks with an invalid API key returns a clear error",
125
+ () => sdk.api.integrations.proxy_read({
126
+ integration: HEALTHIE_TITLE, type: 'webhooks', healthieIntegrationId: TAG,
127
+ }),
128
+ { shouldError: true, onError: (e: any) => (e?.message || '').includes('API Key is Invalid') },
129
+ )
130
+
131
+ // ── Organization.healthieIntegrationIds pushed by create side effect ──
132
+ await wait(undefined, 2000) // side effects are async
133
+ await async_test(
134
+ "organization.healthieIntegrationIds includes tag after create",
135
+ () => sdk.api.organizations.getOne(businessId),
136
+ { onResult: o => !!(o as any).healthieIntegrationIds?.includes(TAG) },
137
+ )
138
+
139
+ // ── Inbound webhook resolution (help page proves the integration lookup without network) ──
140
+ await async_test(
141
+ "webhook help page resolves primary (sandbox) without query param",
142
+ () => axios.get(`${host}/v1/webhooks/healthie/${businessId}`),
143
+ { onResult: r => typeof r.data === 'string' && r.data.startsWith('Sandbox') },
144
+ )
145
+ await async_test(
146
+ "webhook help page resolves tagged (production) integration via healthieIntegrationId param",
147
+ () => axios.get(`${host}/v1/webhooks/healthie/${businessId}?healthieIntegrationId=${TAG}`),
148
+ { onResult: r => typeof r.data === 'string' && r.data.startsWith('Production') },
149
+ )
150
+ await async_test(
151
+ "webhook help page rejects unknown healthieIntegrationId",
152
+ () => axios.get(`${host}/v1/webhooks/healthie/${businessId}?healthieIntegrationId=not-a-real-id`),
153
+ { shouldError: true, onError: (e: any) => e?.response?.status === 400 },
154
+ )
155
+
156
+ // ── Enduser field + one-org-per-patient lock (relationship constraint) ──
157
+ const enduser = await sdk.api.endusers.createOne({
158
+ email: 'multi-healthie-test@tellescope.com',
159
+ healthieIntegrationId: TAG,
160
+ })
161
+ enduserId = enduser.id
162
+ if (enduser.healthieIntegrationId !== TAG) {
163
+ throw new Error("healthieIntegrationId not persisted on enduser create")
164
+ }
165
+
166
+ await async_test(
167
+ "healthieIntegrationId freely changeable while unlinked",
168
+ () => sdk.api.endusers.updateOne(enduserId, { healthieIntegrationId: '' }),
169
+ { onResult: e => !e.healthieIntegrationId },
170
+ )
171
+ await async_test(
172
+ "healthieIntegrationId re-set while unlinked",
173
+ () => sdk.api.endusers.updateOne(enduserId, { healthieIntegrationId: TAG }),
174
+ { onResult: e => e.healthieIntegrationId === TAG },
175
+ )
176
+
177
+ // link the patient to Healthie (reference with a Healthie patient id)
178
+ await sdk.api.endusers.updateOne(enduserId, { references: [{ type: HEALTHIE_TITLE, id: '12345' }] } as any, { replaceObjectFields: true })
179
+
180
+ // no lock: linked patients stay editable so customers can self-serve fix or migrate them
181
+ await async_test(
182
+ "healthieIntegrationId changeable while linked (self-serve fix/migrate)",
183
+ () => sdk.api.endusers.updateOne(enduserId, { healthieIntegrationId: 'somewhere-else' }),
184
+ { onResult: e => e.healthieIntegrationId === 'somewhere-else' },
185
+ )
186
+ await async_test(
187
+ "healthieIntegrationId revertible while linked",
188
+ () => sdk.api.endusers.updateOne(enduserId, { healthieIntegrationId: TAG }),
189
+ { onResult: e => e.healthieIntegrationId === TAG },
190
+ )
191
+
192
+ await sdk.api.endusers.updateOne(enduserId, { references: [] } as any, { replaceObjectFields: true })
193
+ await async_test(
194
+ "healthieIntegrationId clearable",
195
+ () => sdk.api.endusers.updateOne(enduserId, { healthieIntegrationId: '' }),
196
+ { onResult: e => !e.healthieIntegrationId },
197
+ )
198
+
199
+ // ── Formsort mapping ──
200
+ const form = await sdk.api.forms.createOne({ title: "Multi Healthie FormSort" })
201
+ formId = form.id
202
+
203
+ const formsortEmail = 'multi-healthie-formsort@tellescope.com'
204
+ const postToFormsort = (answers: { key: string, value: any }[], responder_uuid: string) => {
205
+ const url = new URL(`${host}/v1/webhooks/formsort/${FORMSORT_TEST_KEY}`)
206
+ url.searchParams.set('formId', form.id)
207
+ return axios.post(url.toString(), { answers, responder_uuid, finalized: true })
208
+ }
209
+
210
+ await postToFormsort([
211
+ { key: 'email', value: formsortEmail },
212
+ { key: 'healthieIntegrationId', value: TAG },
213
+ ], "multi-healthie-1")
214
+
215
+ await async_test(
216
+ "formsort sets healthieIntegrationId on unlinked enduser",
217
+ () => sdk.api.endusers.getSome({ filter: { email: formsortEmail } }),
218
+ { onResult: es => {
219
+ const e = es.find(e => e.email === formsortEmail)
220
+ formsortEnduserId = e?.id || ''
221
+ return e?.healthieIntegrationId === TAG
222
+ }},
223
+ )
224
+
225
+ // linked endusers are re-routable via formsort too (self-serve fix/migrate)
226
+ await sdk.api.endusers.updateOne(formsortEnduserId, { references: [{ type: HEALTHIE_TITLE, id: '6789' }] } as any, { replaceObjectFields: true })
227
+ await postToFormsort([
228
+ { key: 'email', value: formsortEmail },
229
+ { key: 'healthieIntegrationId', value: 'somewhere-else' },
230
+ ], "multi-healthie-2")
231
+ await wait(undefined, 1000)
232
+
233
+ await async_test(
234
+ "formsort updates healthieIntegrationId on a linked enduser",
235
+ () => sdk.api.endusers.getOne(formsortEnduserId),
236
+ { onResult: e => e.healthieIntegrationId === 'somewhere-else' },
237
+ )
238
+
239
+ // ── Removing the primary must not affect the tagged integration; org list recomputes on delete ──
240
+ await sdk.api.integrations.deleteOne(primaryIntegrationId)
241
+ primaryIntegrationId = ''
242
+ await async_test(
243
+ "tagged integration survives primary deletion",
244
+ () => sdk.api.integrations.getOne(taggedIntegrationId),
245
+ { onResult: i => i.tenantId === TAG },
246
+ )
247
+
248
+ await sdk.api.integrations.deleteOne(taggedIntegrationId)
249
+ taggedIntegrationId = ''
250
+ await wait(undefined, 2000) // delete side effect is async
251
+ await async_test(
252
+ "organization.healthieIntegrationIds recomputed (emptied) after delete",
253
+ () => sdk.api.organizations.getOne(businessId),
254
+ { onResult: o => !(o as any).healthieIntegrationIds?.includes(TAG) },
255
+ )
256
+ } finally {
257
+ for (const id of [primaryIntegrationId, taggedIntegrationId]) {
258
+ if (!id) continue
259
+ await sdk.api.integrations.deleteOne(id).catch(console.error)
260
+ }
261
+ if (enduserId) await sdk.api.endusers.deleteOne(enduserId).catch(console.error)
262
+ if (formsortEnduserId) await sdk.api.endusers.deleteOne(formsortEnduserId).catch(console.error)
263
+ if (formId) await sdk.api.forms.deleteOne(formId).catch(console.error)
264
+ }
265
+ }
266
+
267
+ if (require.main === module) {
268
+ console.log(`Using API URL: ${host}`)
269
+ const sdk = new Session({ host })
270
+ const sdkNonAdmin = new Session({ host })
271
+
272
+ const runTests = async () => {
273
+ await setup_tests(sdk, sdkNonAdmin)
274
+ await healthie_multi_integration_tests({ sdk, sdkNonAdmin })
275
+ }
276
+
277
+ runTests()
278
+ .then(() => {
279
+ console.log("✅ Multiple Healthie integrations test suite completed successfully")
280
+ process.exit(0)
281
+ })
282
+ .catch((error) => {
283
+ console.error("❌ Multiple Healthie integrations test suite failed:", error)
284
+ process.exit(1)
285
+ })
286
+ }
@@ -39,6 +39,7 @@ import { enduser_observations_acknowledge_tests } from "./api_tests/enduser_obse
39
39
  import { translations_tests } from "./api_tests/translations.test"
40
40
  import { user_portal_settings_tests } from "./api_tests/user_portal_settings.test"
41
41
  import { integrations_redacted_tests } from "./api_tests/integrations_redacted.test"
42
+ import { healthie_multi_integration_tests } from "./api_tests/healthie_multi_integration.test"
42
43
  import { get_some_projection_tests } from "./api_tests/get_some_projection.test"
43
44
  import { mdb_sort_tests } from "./api_tests/mdb_sort.test"
44
45
  import { create_user_notifications_trigger_tests } from "./api_tests/create_user_notifications_trigger.test"
@@ -15073,6 +15074,7 @@ const ip_address_form_tests = async () => {
15073
15074
  await openloop_webhooks_tests({ sdk, sdkNonAdmin })
15074
15075
  await mdi_webhooks_tests({ sdk, sdkNonAdmin })
15075
15076
  await integrations_redacted_tests({ sdk, sdkNonAdmin })
15077
+ await healthie_multi_integration_tests({ sdk, sdkNonAdmin })
15076
15078
  await mdb_sort_tests({ sdk, sdkNonAdmin })
15077
15079
  await search_tests()
15078
15080
  await time_tracks_tests({ sdk, sdkNonAdmin })
Binary file