@tellescope/sdk 1.254.1 → 1.255.1

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 (69) hide show
  1. package/lib/cjs/sdk.d.ts +4 -0
  2. package/lib/cjs/sdk.d.ts.map +1 -1
  3. package/lib/cjs/sdk.js +5 -0
  4. package/lib/cjs/sdk.js.map +1 -1
  5. package/lib/cjs/tests/api_tests/appointment_no_showed_trigger.test.d.ts +6 -0
  6. package/lib/cjs/tests/api_tests/appointment_no_showed_trigger.test.d.ts.map +1 -0
  7. package/lib/cjs/tests/api_tests/appointment_no_showed_trigger.test.js +236 -0
  8. package/lib/cjs/tests/api_tests/appointment_no_showed_trigger.test.js.map +1 -0
  9. package/lib/cjs/tests/api_tests/calendar_events_bulk_update.test.d.ts.map +1 -1
  10. package/lib/cjs/tests/api_tests/calendar_events_bulk_update.test.js +108 -73
  11. package/lib/cjs/tests/api_tests/calendar_events_bulk_update.test.js.map +1 -1
  12. package/lib/cjs/tests/api_tests/group_event_attendee_status_triggers.test.d.ts +6 -0
  13. package/lib/cjs/tests/api_tests/group_event_attendee_status_triggers.test.d.ts.map +1 -0
  14. package/lib/cjs/tests/api_tests/group_event_attendee_status_triggers.test.js +587 -0
  15. package/lib/cjs/tests/api_tests/group_event_attendee_status_triggers.test.js.map +1 -0
  16. package/lib/cjs/tests/api_tests/healthie_force_sync.test.d.ts +6 -0
  17. package/lib/cjs/tests/api_tests/healthie_force_sync.test.d.ts.map +1 -0
  18. package/lib/cjs/tests/api_tests/healthie_force_sync.test.js +165 -0
  19. package/lib/cjs/tests/api_tests/healthie_force_sync.test.js.map +1 -0
  20. package/lib/cjs/tests/api_tests/order_status_equals_frequency.test.d.ts +6 -0
  21. package/lib/cjs/tests/api_tests/order_status_equals_frequency.test.d.ts.map +1 -0
  22. package/lib/cjs/tests/api_tests/order_status_equals_frequency.test.js +254 -0
  23. package/lib/cjs/tests/api_tests/order_status_equals_frequency.test.js.map +1 -0
  24. package/lib/cjs/tests/api_tests/organization_api_keys.test.d.ts +6 -0
  25. package/lib/cjs/tests/api_tests/organization_api_keys.test.d.ts.map +1 -0
  26. package/lib/cjs/tests/api_tests/organization_api_keys.test.js +226 -0
  27. package/lib/cjs/tests/api_tests/organization_api_keys.test.js.map +1 -0
  28. package/lib/cjs/tests/tests.d.ts.map +1 -1
  29. package/lib/cjs/tests/tests.js +560 -305
  30. package/lib/cjs/tests/tests.js.map +1 -1
  31. package/lib/esm/sdk.d.ts +6 -2
  32. package/lib/esm/sdk.d.ts.map +1 -1
  33. package/lib/esm/sdk.js +5 -0
  34. package/lib/esm/sdk.js.map +1 -1
  35. package/lib/esm/tests/api_tests/appointment_no_showed_trigger.test.d.ts +6 -0
  36. package/lib/esm/tests/api_tests/appointment_no_showed_trigger.test.d.ts.map +1 -0
  37. package/lib/esm/tests/api_tests/appointment_no_showed_trigger.test.js +232 -0
  38. package/lib/esm/tests/api_tests/appointment_no_showed_trigger.test.js.map +1 -0
  39. package/lib/esm/tests/api_tests/calendar_events_bulk_update.test.d.ts.map +1 -1
  40. package/lib/esm/tests/api_tests/calendar_events_bulk_update.test.js +109 -74
  41. package/lib/esm/tests/api_tests/calendar_events_bulk_update.test.js.map +1 -1
  42. package/lib/esm/tests/api_tests/group_event_attendee_status_triggers.test.d.ts +6 -0
  43. package/lib/esm/tests/api_tests/group_event_attendee_status_triggers.test.d.ts.map +1 -0
  44. package/lib/esm/tests/api_tests/group_event_attendee_status_triggers.test.js +583 -0
  45. package/lib/esm/tests/api_tests/group_event_attendee_status_triggers.test.js.map +1 -0
  46. package/lib/esm/tests/api_tests/healthie_force_sync.test.d.ts +6 -0
  47. package/lib/esm/tests/api_tests/healthie_force_sync.test.d.ts.map +1 -0
  48. package/lib/esm/tests/api_tests/healthie_force_sync.test.js +161 -0
  49. package/lib/esm/tests/api_tests/healthie_force_sync.test.js.map +1 -0
  50. package/lib/esm/tests/api_tests/order_status_equals_frequency.test.d.ts +6 -0
  51. package/lib/esm/tests/api_tests/order_status_equals_frequency.test.d.ts.map +1 -0
  52. package/lib/esm/tests/api_tests/order_status_equals_frequency.test.js +250 -0
  53. package/lib/esm/tests/api_tests/order_status_equals_frequency.test.js.map +1 -0
  54. package/lib/esm/tests/api_tests/organization_api_keys.test.d.ts +6 -0
  55. package/lib/esm/tests/api_tests/organization_api_keys.test.d.ts.map +1 -0
  56. package/lib/esm/tests/api_tests/organization_api_keys.test.js +222 -0
  57. package/lib/esm/tests/api_tests/organization_api_keys.test.js.map +1 -0
  58. package/lib/esm/tests/tests.d.ts.map +1 -1
  59. package/lib/esm/tests/tests.js +560 -305
  60. package/lib/esm/tests/tests.js.map +1 -1
  61. package/lib/tsconfig.tsbuildinfo +1 -1
  62. package/package.json +10 -10
  63. package/src/sdk.ts +11 -0
  64. package/src/tests/api_tests/appointment_no_showed_trigger.test.ts +144 -0
  65. package/src/tests/api_tests/calendar_events_bulk_update.test.ts +22 -0
  66. package/src/tests/api_tests/group_event_attendee_status_triggers.test.ts +381 -0
  67. package/src/tests/api_tests/organization_api_keys.test.ts +207 -0
  68. package/src/tests/tests.ts +196 -3
  69. package/test_generated.pdf +0 -0
@@ -0,0 +1,207 @@
1
+ require('source-map-support').install();
2
+
3
+ import { Session } from "../../sdk"
4
+ import {
5
+ assert,
6
+ async_test,
7
+ log_header,
8
+ wait,
9
+ } from "@tellescope/testing"
10
+ import { setup_tests } from "../setup"
11
+
12
+ const host = process.env.API_URL || 'http://localhost:8080' as const
13
+
14
+ // fields that the admin org-wide list endpoint is allowed to return
15
+ const ALLOWED_FIELDS = ['id', 'creator', 'businessId', 'updatedAt'].sort()
16
+
17
+ export const organization_api_keys_tests = async (
18
+ { sdk, sdkNonAdmin }: { sdk: Session, sdkNonAdmin: Session }
19
+ ) => {
20
+ log_header("Organization API Keys (admin org-wide management)")
21
+
22
+ // We never mutate sdkNonAdmin's roles here (throwaway-user rule). Instead we create a
23
+ // dedicated throwaway user in the SAME org that DOES have api_keys access (via a custom
24
+ // role), so it can create its own key (the offboarded-employee scenario) and so the
25
+ // admin-gating negative tests below exercise the adminOnly gate (401) rather than the
26
+ // access-permission gate (403) — a non-admin without api_keys access is rejected with
27
+ // "Inaccessible" before adminOnly is ever reached.
28
+ const adminKeyIds: string[] = []
29
+ let otherKeyId: string | undefined
30
+ let otherUserId: string | undefined
31
+ let apiKeysRoleId: string | undefined
32
+
33
+ try {
34
+ // A custom role that grants api_keys access (merged over PROVIDER_PERMISSIONS server-side)
35
+ const apiKeysRole = await sdk.api.role_based_access_permissions.createOne({
36
+ role: 'api-keys-test-role',
37
+ permissions: { api_keys: { create: 'All', read: 'All', update: 'All', delete: 'All' } },
38
+ })
39
+ apiKeysRoleId = apiKeysRole.id
40
+
41
+ // A throwaway non-admin user in the same org, holding that role
42
+ const otherUser = (
43
+ await sdk.api.users.getOne({ email: 'api.keys.other.user@tellescope.com' }).catch(() => null)
44
+ ) || (
45
+ await sdk.api.users.createOne({ email: 'api.keys.other.user@tellescope.com', notificationEmailsDisabled: true, verifiedEmail: true })
46
+ )
47
+ otherUserId = otherUser.id
48
+ await sdk.api.users.updateOne(otherUser.id, { roles: [apiKeysRole.role] }, { replaceObjectFields: true })
49
+ await wait(undefined, 2000) // wait for the role change to propagate before authenticating
50
+
51
+ const otherSdk = new Session({
52
+ host,
53
+ authToken: (await sdk.api.users.generate_auth_token({ id: otherUser.id })).authToken,
54
+ })
55
+
56
+ // A key created by the admin...
57
+ const adminKey = await sdk.api.api_keys.createOne({})
58
+ adminKeyIds.push(adminKey.id)
59
+
60
+ // ...and a key created by ANOTHER user in the org (the offboarded-employee scenario)
61
+ const otherKey = await otherSdk.api.api_keys.createOne({})
62
+ otherKeyId = otherKey.id
63
+
64
+ // =============================================
65
+ // CORE: admin sees keys created by other users
66
+ // =============================================
67
+ const { apiKeys } = await sdk.api.api_keys.get_organization_api_keys()
68
+
69
+ assert(
70
+ apiKeys.some(k => k.id === otherKeyId),
71
+ `admin list did not include another user's key ${otherKeyId}`,
72
+ "admin get_organization_api_keys returns keys created by other users in the org"
73
+ )
74
+ assert(
75
+ apiKeys.some(k => k.id === adminKey.id),
76
+ "admin list did not include the admin's own key",
77
+ "admin get_organization_api_keys returns the admin's own keys"
78
+ )
79
+ assert(
80
+ !!apiKeys.find(k => k.id === otherKeyId && k.creator === otherUser.id),
81
+ "creator on the other user's key did not match",
82
+ "returned key reports the correct creator"
83
+ )
84
+
85
+ // =============================================
86
+ // SECRET REDACTION (server-side, allowlist)
87
+ // =============================================
88
+ assert(
89
+ apiKeys.every(k => (k as any).key === undefined),
90
+ "a returned record exposed a plaintext `key`",
91
+ "no returned record exposes a plaintext key (legacy field)"
92
+ )
93
+ assert(
94
+ apiKeys.every(k => (k as any).hashedKey === undefined),
95
+ "a returned record exposed `hashedKey`",
96
+ "no returned record exposes hashedKey"
97
+ )
98
+ // Strict allowlist: every returned field must be in the allowlist (no extra/sensitive field
99
+ // ever leaks). We can't require all allowlisted fields to be PRESENT — never-updated keys
100
+ // have no `updatedAt`, and res.json() drops undefined values — so this is a subset check.
101
+ const offending = apiKeys
102
+ .map(k => Object.keys(k).filter(key => !ALLOWED_FIELDS.includes(key)))
103
+ .find(extra => extra.length > 0)
104
+ assert(
105
+ offending === undefined,
106
+ `a returned record exposed non-allowlisted fields: ${JSON.stringify(offending)}`,
107
+ "returned records contain only allowlisted fields (subset of id, creator, businessId, updatedAt)"
108
+ )
109
+
110
+ // =============================================
111
+ // CROSS-ORG ISOLATION (no leakage across businesses)
112
+ // =============================================
113
+ assert(
114
+ apiKeys.every(k => k.businessId === sdk.userInfo.businessId),
115
+ "a returned key belonged to a different business",
116
+ "all returned keys are scoped to the admin's businessId"
117
+ )
118
+
119
+ // =============================================
120
+ // ADMIN GATING (negative tests)
121
+ // =============================================
122
+ await async_test(
123
+ "non-admin get_organization_api_keys is rejected (401)",
124
+ () => otherSdk.api.api_keys.get_organization_api_keys(),
125
+ { shouldError: true, onError: (e: any) => e.statusCode === 401 || /admin access only/i.test(e.message || '') }
126
+ )
127
+ await async_test(
128
+ "non-admin delete_organization_api_key is rejected (401)",
129
+ () => otherSdk.api.api_keys.delete_organization_api_key({ id: adminKey.id }),
130
+ { shouldError: true, onError: (e: any) => e.statusCode === 401 || /admin access only/i.test(e.message || '') }
131
+ )
132
+
133
+ // confirm the gating did not delete the key
134
+ const afterGating = await sdk.api.api_keys.get_organization_api_keys()
135
+ assert(
136
+ afterGating.apiKeys.some(k => k.id === adminKey.id),
137
+ "rejected non-admin delete still removed the key",
138
+ "rejected non-admin delete left the key intact"
139
+ )
140
+
141
+ // =============================================
142
+ // ADMIN DELETE another user's key
143
+ // =============================================
144
+ await async_test(
145
+ "admin delete_organization_api_key deletes another user's key",
146
+ () => sdk.api.api_keys.delete_organization_api_key({ id: otherKeyId! }),
147
+ { shouldError: false, onResult: () => true }
148
+ )
149
+
150
+ const afterDelete = await sdk.api.api_keys.get_organization_api_keys()
151
+ assert(
152
+ !afterDelete.apiKeys.some(k => k.id === otherKeyId),
153
+ "deleted key still present in org-wide list",
154
+ "deleted key no longer appears in the org-wide list"
155
+ )
156
+ otherKeyId = undefined // successfully deleted; nothing to clean up
157
+
158
+ // deleting a key in a different org (fabricated id) must be a no-op for our org
159
+ await async_test(
160
+ "admin delete of a non-existent/cross-org key id is a safe no-op",
161
+ () => sdk.api.api_keys.delete_organization_api_key({ id: '000000000000000000000000' }),
162
+ { shouldError: false, onResult: () => true }
163
+ )
164
+ const afterNoop = await sdk.api.api_keys.get_organization_api_keys()
165
+ assert(
166
+ afterNoop.apiKeys.some(k => k.id === adminKey.id),
167
+ "no-op delete removed an unrelated key",
168
+ "no-op cross-org delete left our org's keys intact"
169
+ )
170
+ } finally {
171
+ // cleanup: remove any keys we created (creator-only default delete)
172
+ for (const id of adminKeyIds) {
173
+ await sdk.api.api_keys.deleteOne(id).catch(() => {})
174
+ }
175
+ // the admin can clean up the throwaway user's key org-wide; fall through is harmless
176
+ if (otherKeyId) {
177
+ await sdk.api.api_keys.delete_organization_api_key({ id: otherKeyId }).catch(() => {})
178
+ }
179
+ if (otherUserId) {
180
+ await sdk.api.users.deleteOne(otherUserId).catch(() => {})
181
+ }
182
+ if (apiKeysRoleId) {
183
+ await sdk.api.role_based_access_permissions.deleteOne(apiKeysRoleId).catch(() => {})
184
+ }
185
+ }
186
+ }
187
+
188
+ // Allow running this test file independently
189
+ if (require.main === module) {
190
+ const sdk = new Session({ host })
191
+ const sdkNonAdmin = new Session({ host })
192
+
193
+ const runTests = async () => {
194
+ await setup_tests(sdk, sdkNonAdmin)
195
+ await organization_api_keys_tests({ sdk, sdkNonAdmin })
196
+ }
197
+
198
+ runTests()
199
+ .then(() => {
200
+ console.log("✅ Organization API keys test suite completed successfully")
201
+ process.exit(0)
202
+ })
203
+ .catch((error) => {
204
+ console.error("❌ Organization API keys test suite failed:", error)
205
+ process.exit(1)
206
+ })
207
+ }
@@ -48,6 +48,8 @@ import { concurrent_build_threads_tests } from "./api_tests/concurrent_build_thr
48
48
  import { appointment_completed_trigger_tests } from "./api_tests/appointment_completed_trigger.test"
49
49
  import { purchase_made_trigger_tests } from "./api_tests/purchase_made_trigger.test"
50
50
  import { appointment_rescheduled_trigger_tests } from "./api_tests/appointment_rescheduled_trigger.test"
51
+ import { appointment_no_showed_trigger_tests } from "./api_tests/appointment_no_showed_trigger.test"
52
+ import { group_event_attendee_status_triggers_tests } from "./api_tests/group_event_attendee_status_triggers.test"
51
53
  import { journey_error_branching_tests } from "./api_tests/journey_error_branching.test"
52
54
  import { afteraction_day_of_month_delay_tests } from "./api_tests/afteraction_day_of_month_delay.test"
53
55
  import { push_forms_to_portal_group_completion_tests } from "./api_tests/push_forms_to_portal_group_completion.test"
@@ -80,6 +82,7 @@ import { enduser_login_tests } from "./api_tests/enduser_login.test";
80
82
  import { enduser_login_rate_limits_tests } from "./api_tests/enduser_login_rate_limits.test";
81
83
  import { eom_procedure_codes_tests } from "./api_tests/eom_procedure_codes.test";
82
84
  import { cross_org_api_key_tests } from "./api_tests/cross_org_api_key.test";
85
+ import { organization_api_keys_tests } from "./api_tests/organization_api_keys.test";
83
86
  import { custom_dashboards_tests } from "./api_tests/custom_dashboards.test";
84
87
  import { message_assignment_trigger_tests } from "./api_tests/message_assignment_trigger.test";
85
88
  import { outbound_chat_sent_trigger_tests } from "./api_tests/outbound_chat_sent_trigger.test";
@@ -3493,6 +3496,39 @@ const order_status_equals_tests = async () => {
3493
3496
  title: "Protocol Condition"
3494
3497
  })
3495
3498
 
3499
+ // Frequency filter triggers (exact-match array filter, mirrors fills/skus). All use a dedicated
3500
+ // source/status so their assertions stay isolated from the t1-t9 incremental tag-count checks above.
3501
+ const tf1 = await sdk.api.automation_triggers.createOne({
3502
+ event: { type: 'Order Status Equals', info: { source: 'FreqSource', status: "FreqStatus" } }, // no frequencies -> fires for all
3503
+ action: { type: 'Add Tags', info: { tags: ['Freq No Filter'] }},
3504
+ status: 'Active',
3505
+ title: "Frequency - no filter (backward compat)"
3506
+ })
3507
+ const tf2 = await sdk.api.automation_triggers.createOne({
3508
+ event: { type: 'Order Status Equals', info: { source: 'FreqSource', status: "FreqStatus", frequencies: ['4', '12'] } },
3509
+ action: { type: 'Add Tags', info: { tags: ['Freq Match'] }},
3510
+ status: 'Active',
3511
+ title: "Frequency - multi-value match"
3512
+ })
3513
+ const tf3 = await sdk.api.automation_triggers.createOne({
3514
+ event: { type: 'Order Status Equals', info: { source: 'FreqSource', status: "FreqStatus", frequencies: [] } }, // empty array -> fires for all
3515
+ action: { type: 'Add Tags', info: { tags: ['Freq Empty All'] }},
3516
+ status: 'Active',
3517
+ title: "Frequency - empty array means all"
3518
+ })
3519
+ const tf4 = await sdk.api.automation_triggers.createOne({
3520
+ event: { type: 'Order Status Equals', info: { source: 'FreqSource', status: "FreqShipped", frequencies: ['4'] } },
3521
+ action: { type: 'Add Tags', info: { tags: ['Freq Status Combo'] }},
3522
+ status: 'Active',
3523
+ title: "Frequency + status combination (AND semantics)"
3524
+ })
3525
+ const tf5 = await sdk.api.automation_triggers.createOne({
3526
+ event: { type: 'Order Status Equals', info: { source: 'FreqSource', status: "FreqStatus", frequencies: ['12'] } },
3527
+ action: { type: 'Add Tags', info: { tags: ['Freq Match 12'] }},
3528
+ status: 'Active',
3529
+ title: "Frequency - second listed value"
3530
+ })
3531
+
3496
3532
  const e = await sdk.api.endusers.createOne({})
3497
3533
 
3498
3534
  await sdk.api.enduser_orders.createOne({ status: 'Nooo', source: 'Source', title: 'nomatch', externalId: '1', enduserId: e.id })
@@ -3705,6 +3741,85 @@ const order_status_equals_tests = async () => {
3705
3741
  ) }
3706
3742
  )
3707
3743
 
3744
+ // --- Frequency filter coverage (fresh enduser keeps tag-count assertions isolated) ---
3745
+ const ef = await sdk.api.endusers.createOne({})
3746
+
3747
+ // Order with NO frequency: no-filter trigger (tf1) and empty-array trigger (tf3) both fire;
3748
+ // populated-filter trigger (tf2) does NOT (missing frequency must never match a populated filter).
3749
+ await sdk.api.enduser_orders.createOne({ status: 'FreqStatus', source: 'FreqSource', externalId: 'f1', enduserId: ef.id, title: "Freq No Frequency" })
3750
+ await wait(undefined, 500) // allow triggers to happen
3751
+ await async_test(
3752
+ "Frequency: no-filter + empty-array fire for order with no frequency (backward compat)",
3753
+ () => sdk.api.endusers.getOne(ef.id),
3754
+ { onResult: e => !!(
3755
+ e.tags?.length === 2
3756
+ && e.tags?.includes('Freq No Filter')
3757
+ && e.tags?.includes('Freq Empty All')
3758
+ && !e.tags?.includes('Freq Match')
3759
+ ) }
3760
+ )
3761
+
3762
+ // Order with a frequency NOT in ['4','12'] -> populated-filter trigger still does not fire.
3763
+ await sdk.api.enduser_orders.createOne({ status: 'FreqStatus', source: 'FreqSource', externalId: 'f2', enduserId: ef.id, title: "Freq 8", frequency: '8' })
3764
+ await wait(undefined, 500) // allow triggers to happen
3765
+ await async_test(
3766
+ "Frequency: no match (order frequency '8' not in ['4','12'])",
3767
+ () => sdk.api.endusers.getOne(ef.id),
3768
+ { onResult: e => !!(
3769
+ e.tags?.length === 2
3770
+ && !e.tags?.includes('Freq Match')
3771
+ ) }
3772
+ )
3773
+
3774
+ // Order with frequency '4' -> ['4','12'] trigger fires.
3775
+ await sdk.api.enduser_orders.createOne({ status: 'FreqStatus', source: 'FreqSource', externalId: 'f3', enduserId: ef.id, title: "Freq 4", frequency: '4' })
3776
+ await wait(undefined, 500) // allow triggers to happen
3777
+ await async_test(
3778
+ "Frequency: match (order frequency '4' is in ['4','12'])",
3779
+ () => sdk.api.endusers.getOne(ef.id),
3780
+ { onResult: e => !!(
3781
+ e.tags?.length === 3
3782
+ && e.tags?.includes('Freq Match')
3783
+ && !e.tags?.includes('Freq Match 12')
3784
+ ) }
3785
+ )
3786
+
3787
+ // Order with frequency '12' -> the second listed value also matches (tf5 frequencies: ['12']).
3788
+ await sdk.api.enduser_orders.createOne({ status: 'FreqStatus', source: 'FreqSource', externalId: 'f4', enduserId: ef.id, title: "Freq 12", frequency: '12' })
3789
+ await wait(undefined, 500) // allow triggers to happen
3790
+ await async_test(
3791
+ "Frequency: second listed value '12' also matches",
3792
+ () => sdk.api.endusers.getOne(ef.id),
3793
+ { onResult: e => !!(
3794
+ e.tags?.length === 4
3795
+ && e.tags?.includes('Freq Match 12')
3796
+ ) }
3797
+ )
3798
+
3799
+ // AND semantics: frequency + status must BOTH match. Right frequency, wrong status -> no fire.
3800
+ await sdk.api.enduser_orders.createOne({ status: 'FreqShipped', source: 'FreqSource', externalId: 'f5', enduserId: ef.id, title: "Freq Combo Wrong Freq", frequency: '8' })
3801
+ await wait(undefined, 500) // allow triggers to happen
3802
+ await async_test(
3803
+ "Frequency + status combo: status matches but frequency does not -> no fire",
3804
+ () => sdk.api.endusers.getOne(ef.id),
3805
+ { onResult: e => !!(
3806
+ e.tags?.length === 4
3807
+ && !e.tags?.includes('Freq Status Combo')
3808
+ ) }
3809
+ )
3810
+
3811
+ // AND semantics: right status AND right frequency -> fires.
3812
+ await sdk.api.enduser_orders.createOne({ status: 'FreqShipped', source: 'FreqSource', externalId: 'f6', enduserId: ef.id, title: "Freq Combo Match", frequency: '4' })
3813
+ await wait(undefined, 500) // allow triggers to happen
3814
+ await async_test(
3815
+ "Frequency + status combo: both match -> fires (AND semantics)",
3816
+ () => sdk.api.endusers.getOne(ef.id),
3817
+ { onResult: e => !!(
3818
+ e.tags?.length === 5
3819
+ && e.tags?.includes('Freq Status Combo')
3820
+ ) }
3821
+ )
3822
+
3708
3823
  await Promise.all([
3709
3824
  sdk.api.automation_triggers.deleteOne(t1.id),
3710
3825
  sdk.api.automation_triggers.deleteOne(t2.id),
@@ -3715,7 +3830,13 @@ const order_status_equals_tests = async () => {
3715
3830
  sdk.api.automation_triggers.deleteOne(t7.id),
3716
3831
  sdk.api.automation_triggers.deleteOne(t8.id),
3717
3832
  sdk.api.automation_triggers.deleteOne(t9.id),
3833
+ sdk.api.automation_triggers.deleteOne(tf1.id),
3834
+ sdk.api.automation_triggers.deleteOne(tf2.id),
3835
+ sdk.api.automation_triggers.deleteOne(tf3.id),
3836
+ sdk.api.automation_triggers.deleteOne(tf4.id),
3837
+ sdk.api.automation_triggers.deleteOne(tf5.id),
3718
3838
  sdk.api.endusers.deleteOne(e.id),
3839
+ sdk.api.endusers.deleteOne(ef.id),
3719
3840
  ])
3720
3841
  }
3721
3842
 
@@ -5592,6 +5713,8 @@ const trigger_events_api_tests = async () => {
5592
5713
  const automation_trigger_tests = async () => {
5593
5714
  log_header("Automation Trigger Tests")
5594
5715
 
5716
+ await appointment_no_showed_trigger_tests({ sdk, sdkNonAdmin })
5717
+ await group_event_attendee_status_triggers_tests({ sdk, sdkNonAdmin })
5595
5718
  await assign_care_team_tests()
5596
5719
  await push_forms_to_portal_group_completion_tests({ sdk, sdkNonAdmin })
5597
5720
  await order_status_equals_tests()
@@ -6717,11 +6840,25 @@ export const self_serve_appointment_booking_tests = async () => {
6717
6840
  confirmationEmailDisabled: true,
6718
6841
  confirmationSMSDisabled: true,
6719
6842
  })
6720
- const event30minGroup = await sdk.api.calendar_event_templates.createOne({
6843
+ const event30minGroup = await sdk.api.calendar_event_templates.createOne({
6721
6844
  title: 'test group', durationInMinutes: 30,
6722
6845
  confirmationEmailDisabled: true,
6723
6846
  confirmationSMSDisabled: true,
6724
- enduserAttendeeLimit: 2,
6847
+ enduserAttendeeLimit: 2,
6848
+ })
6849
+ const event30minTwilio = await sdk.api.calendar_event_templates.createOne({
6850
+ title: 'test twilio', durationInMinutes: 30,
6851
+ confirmationEmailDisabled: true,
6852
+ confirmationSMSDisabled: true,
6853
+ enableVideoCall: true,
6854
+ videoIntegration: 'Twilio',
6855
+ })
6856
+ const event30minZoom = await sdk.api.calendar_event_templates.createOne({
6857
+ title: 'test zoom', durationInMinutes: 30,
6858
+ confirmationEmailDisabled: true,
6859
+ confirmationSMSDisabled: true,
6860
+ enableVideoCall: true,
6861
+ videoIntegration: 'Zoom',
6725
6862
  })
6726
6863
 
6727
6864
  // ensure it doesn't match current day, to avoid errors on testing
@@ -7281,17 +7418,70 @@ export const self_serve_appointment_booking_tests = async () => {
7281
7418
  { onResult: event => !!event.rescheduledAt }
7282
7419
  )
7283
7420
 
7421
+ // videoIntegration passthrough from template to booked event (Twilio fix + Zoom regression guard)
7422
+ // set up fresh, dedicated availability so this doesn't interfere with the slot-accounting tests above
7423
+ await sdk.api.users.updateOne(sdk.userInfo.id, {
7424
+ weeklyAvailabilities: [
7425
+ {
7426
+ dayOfWeekStartingSundayIndexedByZero,
7427
+ startTimeInMinutes: 60 * 9, // 9am,
7428
+ endTimeInMinutes: 60 * 11, // 11am,
7429
+ },
7430
+ ],
7431
+ timezone: 'America/New_York',
7432
+ }, {
7433
+ replaceObjectFields: true,
7434
+ })
7435
+
7436
+ const twilioSlots = await enduserSDK.api.calendar_events.get_appointment_availability({
7437
+ calendarEventTemplateId: event30minTwilio.id,
7438
+ from: new Date(Date.now() - 10000),
7439
+ to: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000),
7440
+ userIds: [sdk.userInfo.id],
7441
+ })
7442
+ const bookedTwilioAppointment = (await enduserSDK.api.calendar_events.book_appointment({
7443
+ calendarEventTemplateId: event30minTwilio.id,
7444
+ startTime: new Date(twilioSlots.availabilityBlocks[0].startTimeInMS),
7445
+ userId: sdk.userInfo.id,
7446
+ })).createdEvent
7447
+ assert(
7448
+ bookedTwilioAppointment.videoIntegration === 'Twilio',
7449
+ 'Twilio videoIntegration not propagated to booked event',
7450
+ 'Twilio videoIntegration propagated to booked event'
7451
+ )
7452
+
7453
+ const zoomSlots = await enduserSDK.api.calendar_events.get_appointment_availability({
7454
+ calendarEventTemplateId: event30minZoom.id,
7455
+ from: new Date(Date.now() - 10000),
7456
+ to: new Date(Date.now() + 3 * 24 * 60 * 60 * 1000),
7457
+ userIds: [sdk.userInfo.id],
7458
+ })
7459
+ const bookedZoomAppointment = (await enduserSDK.api.calendar_events.book_appointment({
7460
+ calendarEventTemplateId: event30minZoom.id,
7461
+ startTime: new Date(zoomSlots.availabilityBlocks[0].startTimeInMS),
7462
+ userId: sdk.userInfo.id,
7463
+ })).createdEvent
7464
+ assert(
7465
+ bookedZoomAppointment.videoIntegration === 'Zoom',
7466
+ 'Zoom videoIntegration not propagated to booked event',
7467
+ 'Zoom videoIntegration propagated to booked event'
7468
+ )
7469
+
7284
7470
  await Promise.all([
7285
7471
  sdk.api.endusers.deleteOne(e1.id),
7286
7472
  sdk.api.endusers.deleteOne(e2.id),
7287
7473
  sdk.api.endusers.deleteOne(e3.id),
7288
7474
  sdk.api.calendar_event_templates.deleteOne(event30min.id),
7289
7475
  sdk.api.calendar_event_templates.deleteOne(event30minGroup.id),
7476
+ sdk.api.calendar_event_templates.deleteOne(event30minTwilio.id),
7477
+ sdk.api.calendar_event_templates.deleteOne(event30minZoom.id),
7290
7478
  sdk.api.calendar_event_templates.deleteOne(event15min.id),
7291
7479
  sdk.api.calendar_event_templates.deleteOne(templatePreserveHosts.id),
7292
7480
  sdk.api.calendar_event_templates.deleteOne(templateReplaceHosts.id),
7293
7481
  sdk.api.calendar_events.deleteOne(bookedAppointment.id),
7294
7482
  sdk.api.calendar_events.deleteOne(bookedMultiAppointment.id),
7483
+ sdk.api.calendar_events.deleteOne(bookedTwilioAppointment.id),
7484
+ sdk.api.calendar_events.deleteOne(bookedZoomAppointment.id),
7295
7485
  sdk.api.calendar_events.deleteOne(groupEvent.id),
7296
7486
  sdk.api.calendar_events.deleteOne(originalMultiHostAppt.id),
7297
7487
  sdk.api.calendar_events.deleteOne(rescheduledPreserveHosts.id),
@@ -11237,6 +11427,7 @@ const lockout_tests = async () => {
11237
11427
  () => sdk.api.users.updateOne(nonAdminId, { lockedOutUntil: -1 }),
11238
11428
  passOnAnyResult
11239
11429
  )
11430
+ await wait(undefined, 1500) // ensure new token's iat second clears the stale deauthenticated-<id> cache marker (1s tolerance)
11240
11431
  await async_test(
11241
11432
  "non-admin can re authenciate when locked to 0",
11242
11433
  () => sdkNonAdmin.authenticate(nonAdminEmail, nonAdminPassword),
@@ -11269,6 +11460,7 @@ const lockout_tests = async () => {
11269
11460
  () => sdk.api.users.updateOne(nonAdminId, { lockedOutUntil: -1 }),
11270
11461
  passOnAnyResult
11271
11462
  )
11463
+ await wait(undefined, 1500) // ensure new token's iat second clears the stale deauthenticated-<id> cache marker (1s tolerance)
11272
11464
  await async_test(
11273
11465
  "non-admin can re authenciate when locked to future date",
11274
11466
  () => sdkNonAdmin.authenticate(nonAdminEmail, nonAdminPassword),
@@ -14842,6 +15034,7 @@ const ip_address_form_tests = async () => {
14842
15034
  await replace_form_field_template_values_tests()
14843
15035
  await mfa_tests()
14844
15036
  await setup_tests(sdk, sdkNonAdmin)
15037
+ await automation_trigger_tests()
14845
15038
  await resource_access_tags_tests({ sdk, sdkNonAdmin })
14846
15039
  await beluga_manual_sync_tests({ sdk, sdkNonAdmin })
14847
15040
  await beluga_pharmacy_mappings_tests({ sdk, sdkNonAdmin })
@@ -14852,7 +15045,6 @@ const ip_address_form_tests = async () => {
14852
15045
  await load_inbox_redaction_tests({ sdk, sdkNonAdmin })
14853
15046
  await webhook_timeout_tests({ sdk, sdkNonAdmin })
14854
15047
  await user_portal_settings_tests({ sdk, sdkNonAdmin })
14855
- await automation_trigger_tests()
14856
15048
  await invite_user_enumeration_tests({ sdk, sdkNonAdmin })
14857
15049
  await handle_incoming_communication_cross_tenant_tests({ sdk, sdkNonAdmin })
14858
15050
  await calendar_event_webhook_template_tests({ sdk, sdkNonAdmin })
@@ -14870,6 +15062,7 @@ const ip_address_form_tests = async () => {
14870
15062
  await enduser_cross_access_isolation_tests({ sdk, sdkNonAdmin })
14871
15063
  await eom_procedure_codes_tests({ sdk, sdkNonAdmin })
14872
15064
  await cross_org_api_key_tests({ sdk, sdkNonAdmin })
15065
+ await organization_api_keys_tests({ sdk, sdkNonAdmin })
14873
15066
  await organization_settings_duplicates_tests({ sdk, sdkNonAdmin })
14874
15067
  await enduser_session_invalidation_tests({ sdk, sdkNonAdmin })
14875
15068
  await chats_analytics_tests({ sdk, sdkNonAdmin })
Binary file