@tellescope/sdk 1.250.1 → 1.251.0

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 (96) hide show
  1. package/lib/cjs/sdk.d.ts +9 -0
  2. package/lib/cjs/sdk.d.ts.map +1 -1
  3. package/lib/cjs/sdk.js +3 -0
  4. package/lib/cjs/sdk.js.map +1 -1
  5. package/lib/cjs/tests/api_tests/account_switcher.test.d.ts.map +1 -1
  6. package/lib/cjs/tests/api_tests/account_switcher.test.js +1700 -306
  7. package/lib/cjs/tests/api_tests/account_switcher.test.js.map +1 -1
  8. package/lib/cjs/tests/api_tests/enduser_cross_access_isolation.test.d.ts.map +1 -1
  9. package/lib/cjs/tests/api_tests/enduser_cross_access_isolation.test.js +28 -15
  10. package/lib/cjs/tests/api_tests/enduser_cross_access_isolation.test.js.map +1 -1
  11. package/lib/cjs/tests/api_tests/enduser_login.test.d.ts +6 -0
  12. package/lib/cjs/tests/api_tests/enduser_login.test.d.ts.map +1 -0
  13. package/lib/cjs/tests/api_tests/enduser_login.test.js +315 -0
  14. package/lib/cjs/tests/api_tests/enduser_login.test.js.map +1 -0
  15. package/lib/cjs/tests/api_tests/medication_added_trigger.test.d.ts.map +1 -1
  16. package/lib/cjs/tests/api_tests/medication_added_trigger.test.js +556 -105
  17. package/lib/cjs/tests/api_tests/medication_added_trigger.test.js.map +1 -1
  18. package/lib/cjs/tests/api_tests/outbound_chat_sent_trigger.test.d.ts +7 -0
  19. package/lib/cjs/tests/api_tests/outbound_chat_sent_trigger.test.d.ts.map +1 -0
  20. package/lib/cjs/tests/api_tests/outbound_chat_sent_trigger.test.js +436 -0
  21. package/lib/cjs/tests/api_tests/outbound_chat_sent_trigger.test.js.map +1 -0
  22. package/lib/cjs/tests/api_tests/push_forms_to_portal_group_completion.test.d.ts +6 -0
  23. package/lib/cjs/tests/api_tests/push_forms_to_portal_group_completion.test.d.ts.map +1 -0
  24. package/lib/cjs/tests/api_tests/push_forms_to_portal_group_completion.test.js +370 -0
  25. package/lib/cjs/tests/api_tests/push_forms_to_portal_group_completion.test.js.map +1 -0
  26. package/lib/cjs/tests/api_tests/set_fields_order_templates.test.d.ts +6 -0
  27. package/lib/cjs/tests/api_tests/set_fields_order_templates.test.d.ts.map +1 -0
  28. package/lib/cjs/tests/api_tests/set_fields_order_templates.test.js +373 -0
  29. package/lib/cjs/tests/api_tests/set_fields_order_templates.test.js.map +1 -0
  30. package/lib/cjs/tests/setup.d.ts.map +1 -1
  31. package/lib/cjs/tests/setup.js +47 -32
  32. package/lib/cjs/tests/setup.js.map +1 -1
  33. package/lib/cjs/tests/tests.d.ts.map +1 -1
  34. package/lib/cjs/tests/tests.js +190 -161
  35. package/lib/cjs/tests/tests.js.map +1 -1
  36. package/lib/cjs/tests/unit_tests/conditional_logic_medication.test.d.ts +3 -0
  37. package/lib/cjs/tests/unit_tests/conditional_logic_medication.test.d.ts.map +1 -0
  38. package/lib/cjs/tests/unit_tests/conditional_logic_medication.test.js +114 -0
  39. package/lib/cjs/tests/unit_tests/conditional_logic_medication.test.js.map +1 -0
  40. package/lib/esm/sdk.d.ts +9 -0
  41. package/lib/esm/sdk.d.ts.map +1 -1
  42. package/lib/esm/sdk.js +3 -0
  43. package/lib/esm/sdk.js.map +1 -1
  44. package/lib/esm/tests/api_tests/account_switcher.test.d.ts.map +1 -1
  45. package/lib/esm/tests/api_tests/account_switcher.test.js +1702 -305
  46. package/lib/esm/tests/api_tests/account_switcher.test.js.map +1 -1
  47. package/lib/esm/tests/api_tests/enduser_cross_access_isolation.test.d.ts.map +1 -1
  48. package/lib/esm/tests/api_tests/enduser_cross_access_isolation.test.js +28 -15
  49. package/lib/esm/tests/api_tests/enduser_cross_access_isolation.test.js.map +1 -1
  50. package/lib/esm/tests/api_tests/enduser_login.test.d.ts +6 -0
  51. package/lib/esm/tests/api_tests/enduser_login.test.d.ts.map +1 -0
  52. package/lib/esm/tests/api_tests/enduser_login.test.js +308 -0
  53. package/lib/esm/tests/api_tests/enduser_login.test.js.map +1 -0
  54. package/lib/esm/tests/api_tests/enduser_login_phi_disclosure.test.d.ts +6 -0
  55. package/lib/esm/tests/api_tests/enduser_login_phi_disclosure.test.d.ts.map +1 -0
  56. package/lib/esm/tests/api_tests/enduser_login_phi_disclosure.test.js +268 -0
  57. package/lib/esm/tests/api_tests/enduser_login_phi_disclosure.test.js.map +1 -0
  58. package/lib/esm/tests/api_tests/medication_added_trigger.test.d.ts.map +1 -1
  59. package/lib/esm/tests/api_tests/medication_added_trigger.test.js +556 -105
  60. package/lib/esm/tests/api_tests/medication_added_trigger.test.js.map +1 -1
  61. package/lib/esm/tests/api_tests/outbound_chat_sent_trigger.test.d.ts +7 -0
  62. package/lib/esm/tests/api_tests/outbound_chat_sent_trigger.test.d.ts.map +1 -0
  63. package/lib/esm/tests/api_tests/outbound_chat_sent_trigger.test.js +432 -0
  64. package/lib/esm/tests/api_tests/outbound_chat_sent_trigger.test.js.map +1 -0
  65. package/lib/esm/tests/api_tests/push_forms_to_portal_group_completion.test.d.ts +6 -0
  66. package/lib/esm/tests/api_tests/push_forms_to_portal_group_completion.test.d.ts.map +1 -0
  67. package/lib/esm/tests/api_tests/push_forms_to_portal_group_completion.test.js +366 -0
  68. package/lib/esm/tests/api_tests/push_forms_to_portal_group_completion.test.js.map +1 -0
  69. package/lib/esm/tests/api_tests/set_fields_order_templates.test.d.ts +6 -0
  70. package/lib/esm/tests/api_tests/set_fields_order_templates.test.d.ts.map +1 -0
  71. package/lib/esm/tests/api_tests/set_fields_order_templates.test.js +369 -0
  72. package/lib/esm/tests/api_tests/set_fields_order_templates.test.js.map +1 -0
  73. package/lib/esm/tests/setup.d.ts.map +1 -1
  74. package/lib/esm/tests/setup.js +47 -32
  75. package/lib/esm/tests/setup.js.map +1 -1
  76. package/lib/esm/tests/tests.d.ts.map +1 -1
  77. package/lib/esm/tests/tests.js +190 -161
  78. package/lib/esm/tests/tests.js.map +1 -1
  79. package/lib/esm/tests/unit_tests/conditional_logic_medication.test.d.ts +3 -0
  80. package/lib/esm/tests/unit_tests/conditional_logic_medication.test.d.ts.map +1 -0
  81. package/lib/esm/tests/unit_tests/conditional_logic_medication.test.js +111 -0
  82. package/lib/esm/tests/unit_tests/conditional_logic_medication.test.js.map +1 -0
  83. package/lib/tsconfig.tsbuildinfo +1 -1
  84. package/package.json +10 -10
  85. package/src/sdk.ts +12 -0
  86. package/src/tests/api_tests/account_switcher.test.ts +1283 -0
  87. package/src/tests/api_tests/enduser_cross_access_isolation.test.ts +26 -0
  88. package/src/tests/api_tests/enduser_login.test.ts +215 -0
  89. package/src/tests/api_tests/medication_added_trigger.test.ts +345 -4
  90. package/src/tests/api_tests/outbound_chat_sent_trigger.test.ts +339 -0
  91. package/src/tests/api_tests/push_forms_to_portal_group_completion.test.ts +198 -0
  92. package/src/tests/api_tests/set_fields_order_templates.test.ts +258 -0
  93. package/src/tests/setup.ts +8 -1
  94. package/src/tests/tests.ts +23 -6
  95. package/src/tests/unit_tests/conditional_logic_medication.test.ts +133 -0
  96. package/test_generated.pdf +0 -0
@@ -0,0 +1,339 @@
1
+ require('source-map-support').install();
2
+
3
+ import { Session, EnduserSession } from "../../sdk"
4
+ import {
5
+ log_header,
6
+ wait,
7
+ } from "@tellescope/testing"
8
+ import { setup_tests } from "../setup"
9
+
10
+ /**
11
+ * Tests for the "Outbound Chat Sent" automation trigger event.
12
+ *
13
+ * Covered cases (one shared trigger, one shared TAG):
14
+ * 1. Single-enduser room, inbound (enduser session) → no trigger, no recentOutboundChatAt
15
+ * 2. Single-enduser room, outbound (user session) → trigger fires, recentOutboundChatAt set + recent
16
+ * 3. Multi-enduser room, inbound (one enduser's session) → neither enduser tagged or stamped
17
+ * 4. Multi-enduser room, outbound (user session) → BOTH endusers tagged + stamped
18
+ * 5. User session, senderId = enduser.id (backfill case) → treated as inbound: no trigger, no stamp
19
+ *
20
+ * Chats are NOT explicitly deleted — they cascade-delete from chat_rooms.
21
+ */
22
+
23
+ const isRecent = (value: Date | string | undefined, sinceMs: number): boolean => {
24
+ if (!value) return false
25
+ const t = new Date(value).valueOf()
26
+ return t >= sinceMs - 1000 && t <= Date.now() + 1000
27
+ }
28
+
29
+ export const outbound_chat_sent_trigger_tests = async ({ sdk }: { sdk: Session }) => {
30
+ log_header("Outbound Chat Sent Trigger Tests")
31
+
32
+ const host = process.env.API_URL || 'http://localhost:8080'
33
+ const TAG = `outbound-chat-sent-${Date.now()}`
34
+
35
+ let endSolo: { id: string } | undefined
36
+ let endA: { id: string } | undefined
37
+ let endB: { id: string } | undefined
38
+ let endBackfill: { id: string } | undefined
39
+ let endAutoreply: { id: string } | undefined
40
+
41
+ let trigger: { id: string } | undefined
42
+ let roomSolo: { id: string } | undefined
43
+ let roomGroup: { id: string } | undefined
44
+ let roomBackfill: { id: string } | undefined
45
+ let roomAutoreply: { id: string } | undefined
46
+
47
+ let settingsModified = false
48
+ let originalOutOfOfficeHours: any[] = []
49
+ let originalAutoReplyEnabled = false
50
+
51
+ try {
52
+ // ── Endusers ──────────────────────────────────────────────────────
53
+ endSolo = await sdk.api.endusers.createOne({ fname: 'Solo', lname: 'Patient' })
54
+ endA = await sdk.api.endusers.createOne({ fname: 'GroupA', lname: 'Patient' })
55
+ endB = await sdk.api.endusers.createOne({ fname: 'GroupB', lname: 'Patient' })
56
+ endBackfill = await sdk.api.endusers.createOne({ fname: 'Backfill', lname: 'Patient' })
57
+ console.log(`Created endusers: solo=${endSolo.id}, A=${endA.id}, B=${endB.id}, backfill=${endBackfill.id}`)
58
+
59
+ // ── Enduser sessions for inbound sends ────────────────────────────
60
+ const { authToken: tokenSolo } = await sdk.api.endusers.generate_auth_token({ id: endSolo.id })
61
+ const endSoloSDK = new EnduserSession({ host, authToken: tokenSolo, businessId: sdk.userInfo.businessId })
62
+
63
+ const { authToken: tokenA } = await sdk.api.endusers.generate_auth_token({ id: endA.id })
64
+ const endASDK = new EnduserSession({ host, authToken: tokenA, businessId: sdk.userInfo.businessId })
65
+
66
+ // ── Trigger ───────────────────────────────────────────────────────
67
+ trigger = await sdk.api.automation_triggers.createOne({
68
+ title: `Outbound Chat Sent Test ${Date.now()}`,
69
+ status: 'Active',
70
+ event: { type: 'Outbound Chat Sent', info: {} },
71
+ action: { type: 'Add Tags', info: { tags: [TAG] } },
72
+ })
73
+ console.log(`Created trigger: ${trigger.id}`)
74
+
75
+ // ── Rooms ─────────────────────────────────────────────────────────
76
+ roomSolo = await sdk.api.chat_rooms.createOne({
77
+ userIds: [sdk.userInfo.id],
78
+ enduserIds: [endSolo.id],
79
+ })
80
+ roomGroup = await sdk.api.chat_rooms.createOne({
81
+ userIds: [sdk.userInfo.id],
82
+ enduserIds: [endA.id, endB.id],
83
+ })
84
+ roomBackfill = await sdk.api.chat_rooms.createOne({
85
+ userIds: [sdk.userInfo.id],
86
+ enduserIds: [endBackfill.id],
87
+ })
88
+ console.log(`Created rooms: solo=${roomSolo.id}, group=${roomGroup.id}, backfill=${roomBackfill.id}`)
89
+
90
+ // ═════════════════════════════════════════════════════════════════
91
+ // Case 1: Single-enduser inbound (enduser session)
92
+ // ═════════════════════════════════════════════════════════════════
93
+ await endSoloSDK.api.chats.createOne({
94
+ roomId: roomSolo.id,
95
+ message: 'solo inbound from enduser',
96
+ })
97
+ await wait(undefined, 2000)
98
+
99
+ const soloAfterInbound = await sdk.api.endusers.getOne(endSolo.id)
100
+ const c1_notTagged = !soloAfterInbound.tags?.includes(TAG)
101
+ const c1_noStamp = !soloAfterInbound.recentOutboundChatAt
102
+ console.log(c1_notTagged
103
+ ? `✅ [1] Solo inbound did NOT fire trigger`
104
+ : `❌ [1] Solo inbound fired trigger. tags=${JSON.stringify(soloAfterInbound.tags)}`)
105
+ console.log(c1_noStamp
106
+ ? `✅ [1] Solo inbound did NOT set recentOutboundChatAt`
107
+ : `❌ [1] Solo inbound set recentOutboundChatAt=${soloAfterInbound.recentOutboundChatAt}`)
108
+
109
+ // ═════════════════════════════════════════════════════════════════
110
+ // Case 2: Single-enduser outbound (user session)
111
+ // ═════════════════════════════════════════════════════════════════
112
+ const c2_sendStart = Date.now()
113
+ await sdk.api.chats.createOne({
114
+ roomId: roomSolo.id,
115
+ message: 'solo outbound from user',
116
+ senderId: sdk.userInfo.id,
117
+ })
118
+ await wait(undefined, 2000)
119
+
120
+ const soloAfterOutbound = await sdk.api.endusers.getOne(endSolo.id)
121
+ const c2_tagged = !!soloAfterOutbound.tags?.includes(TAG)
122
+ const c2_stamped = isRecent(soloAfterOutbound.recentOutboundChatAt, c2_sendStart)
123
+ console.log(c2_tagged
124
+ ? `✅ [2] Solo outbound fired trigger`
125
+ : `❌ [2] Solo outbound did NOT fire trigger. tags=${JSON.stringify(soloAfterOutbound.tags)}`)
126
+ console.log(c2_stamped
127
+ ? `✅ [2] Solo outbound set recentOutboundChatAt=${soloAfterOutbound.recentOutboundChatAt}`
128
+ : `❌ [2] Solo outbound did not set a recent recentOutboundChatAt. got=${soloAfterOutbound.recentOutboundChatAt}`)
129
+
130
+ // ═════════════════════════════════════════════════════════════════
131
+ // Case 3: Multi-enduser inbound (endA sends in [A, B] room)
132
+ // ═════════════════════════════════════════════════════════════════
133
+ await endASDK.api.chats.createOne({
134
+ roomId: roomGroup.id,
135
+ message: 'group inbound from endA',
136
+ })
137
+ await wait(undefined, 2000)
138
+
139
+ const aAfterInbound = await sdk.api.endusers.getOne(endA.id)
140
+ const bAfterInbound = await sdk.api.endusers.getOne(endB.id)
141
+ const c3_aNotTagged = !aAfterInbound.tags?.includes(TAG)
142
+ const c3_bNotTagged = !bAfterInbound.tags?.includes(TAG)
143
+ const c3_neitherTagged = c3_aNotTagged && c3_bNotTagged
144
+ const c3_neitherStamped = !aAfterInbound.recentOutboundChatAt && !bAfterInbound.recentOutboundChatAt
145
+ console.log(c3_neitherTagged
146
+ ? `✅ [3] Group inbound did NOT fire trigger on either enduser`
147
+ : `❌ [3] Group inbound fired trigger. A tags=${JSON.stringify(aAfterInbound.tags)} B tags=${JSON.stringify(bAfterInbound.tags)}`)
148
+ console.log(c3_neitherStamped
149
+ ? `✅ [3] Group inbound did NOT set recentOutboundChatAt on either enduser`
150
+ : `❌ [3] Group inbound stamped recentOutboundChatAt. A=${aAfterInbound.recentOutboundChatAt} B=${bAfterInbound.recentOutboundChatAt}`)
151
+
152
+ // ═════════════════════════════════════════════════════════════════
153
+ // Case 4: Multi-enduser outbound (user sends in [A, B] room)
154
+ // ═════════════════════════════════════════════════════════════════
155
+ const c4_sendStart = Date.now()
156
+ await sdk.api.chats.createOne({
157
+ roomId: roomGroup.id,
158
+ message: 'group outbound from user',
159
+ senderId: sdk.userInfo.id,
160
+ })
161
+ await wait(undefined, 2000)
162
+
163
+ const aAfterOutbound = await sdk.api.endusers.getOne(endA.id)
164
+ const bAfterOutbound = await sdk.api.endusers.getOne(endB.id)
165
+ const c4_bothTagged = !!aAfterOutbound.tags?.includes(TAG) && !!bAfterOutbound.tags?.includes(TAG)
166
+ const c4_bothStamped = isRecent(aAfterOutbound.recentOutboundChatAt, c4_sendStart)
167
+ && isRecent(bAfterOutbound.recentOutboundChatAt, c4_sendStart)
168
+ console.log(c4_bothTagged
169
+ ? `✅ [4] Group outbound fired trigger on BOTH endusers`
170
+ : `❌ [4] Group outbound did not tag both. A tags=${JSON.stringify(aAfterOutbound.tags)} B tags=${JSON.stringify(bAfterOutbound.tags)}`)
171
+ console.log(c4_bothStamped
172
+ ? `✅ [4] Group outbound set recentOutboundChatAt on BOTH endusers`
173
+ : `❌ [4] Group outbound did not stamp both. A=${aAfterOutbound.recentOutboundChatAt} B=${bAfterOutbound.recentOutboundChatAt}`)
174
+
175
+ // ═════════════════════════════════════════════════════════════════
176
+ // Case 5: User session, senderId = enduser.id (backfill)
177
+ // ═════════════════════════════════════════════════════════════════
178
+ await sdk.api.chats.createOne({
179
+ roomId: roomBackfill.id,
180
+ message: 'backfilled inbound (user session, enduser senderId)',
181
+ senderId: endBackfill.id,
182
+ })
183
+ await wait(undefined, 2000)
184
+
185
+ const backfillAfter = await sdk.api.endusers.getOne(endBackfill.id)
186
+ const c5_notTagged = !backfillAfter.tags?.includes(TAG)
187
+ const c5_noStamp = !backfillAfter.recentOutboundChatAt
188
+ console.log(c5_notTagged
189
+ ? `✅ [5] Backfill (user-session + enduser senderId) did NOT fire trigger`
190
+ : `❌ [5] Backfill fired trigger. tags=${JSON.stringify(backfillAfter.tags)}`)
191
+ console.log(c5_noStamp
192
+ ? `✅ [5] Backfill did NOT set recentOutboundChatAt`
193
+ : `❌ [5] Backfill set recentOutboundChatAt=${backfillAfter.recentOutboundChatAt}`)
194
+
195
+ // ═════════════════════════════════════════════════════════════════
196
+ // Case 6: Autoreply — enduser inbound triggers an org autoreply.
197
+ // The autoreply chat (isAutoreply: true) MUST NOT fire the trigger
198
+ // or stamp recentOutboundChatAt.
199
+ //
200
+ // Setup requires enabling org autoreply with an OOO window covering
201
+ // "now" (is_out_of_office returns false on empty config).
202
+ // ═════════════════════════════════════════════════════════════════
203
+ const originalOrg = await sdk.api.organizations.getOne(sdk.userInfo.businessId)
204
+ originalOutOfOfficeHours = originalOrg.outOfOfficeHours ?? []
205
+ originalAutoReplyEnabled = !!originalOrg.settings?.endusers?.autoReplyEnabled
206
+
207
+ settingsModified = true
208
+ await sdk.api.organizations.updateOne(sdk.userInfo.businessId, {
209
+ outOfOfficeHours: [{
210
+ from: new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString(),
211
+ to: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
212
+ autoreplyText: 'OOO autoreply (test)',
213
+ }] as any,
214
+ settings: { endusers: { autoReplyEnabled: true } } as any,
215
+ }, { replaceObjectFields: true })
216
+
217
+ endAutoreply = await sdk.api.endusers.createOne({ fname: 'Autoreply', lname: 'Patient' })
218
+ const { authToken: tokenAR } = await sdk.api.endusers.generate_auth_token({ id: endAutoreply.id })
219
+ const endAutoreplySDK = new EnduserSession({ host, authToken: tokenAR, businessId: sdk.userInfo.businessId })
220
+
221
+ roomAutoreply = await sdk.api.chat_rooms.createOne({
222
+ userIds: [sdk.userInfo.id],
223
+ enduserIds: [endAutoreply.id],
224
+ })
225
+
226
+ await endAutoreplySDK.api.chats.createOne({
227
+ roomId: roomAutoreply.id,
228
+ message: 'trigger an autoreply',
229
+ })
230
+
231
+ // Autoreply path involves an extra org/user lookup + insert before
232
+ // handle_chat_create re-runs against the autoreply chat.
233
+ await wait(undefined, 4000)
234
+
235
+ const arAfter = await sdk.api.endusers.getOne(endAutoreply.id)
236
+ const roomChats = await sdk.api.chats.getSome({ filter: { roomId: roomAutoreply.id } })
237
+ const sanity_autoreplySent = roomChats.some(c => (c as any).isAutoreply === true)
238
+
239
+ const c6_notTagged = !arAfter.tags?.includes(TAG)
240
+ const c6_noStamp = !arAfter.recentOutboundChatAt
241
+
242
+ console.log(sanity_autoreplySent
243
+ ? `✅ [6] Autoreply chat was sent (sanity check)`
244
+ : `❌ [6] No autoreply chat found in room — test setup invalid. Cannot validate trigger skip.`)
245
+ console.log(c6_notTagged
246
+ ? `✅ [6] Autoreply did NOT fire trigger`
247
+ : `❌ [6] Autoreply fired trigger. tags=${JSON.stringify(arAfter.tags)}`)
248
+ console.log(c6_noStamp
249
+ ? `✅ [6] Autoreply did NOT set recentOutboundChatAt`
250
+ : `❌ [6] Autoreply set recentOutboundChatAt=${arAfter.recentOutboundChatAt}`)
251
+
252
+ // ── Summary ───────────────────────────────────────────────────────
253
+ console.log(`\n=== Outbound Chat Sent Trigger Test Results ===`)
254
+ console.log(`[1] Solo inbound — no trigger: ${c1_notTagged ? '✅' : '❌'}`)
255
+ console.log(`[1] Solo inbound — no stamp: ${c1_noStamp ? '✅' : '❌'}`)
256
+ console.log(`[2] Solo outbound — trigger: ${c2_tagged ? '✅' : '❌'}`)
257
+ console.log(`[2] Solo outbound — stamp: ${c2_stamped ? '✅' : '❌'}`)
258
+ console.log(`[3] Group inbound — no trigger (both): ${c3_neitherTagged ? '✅' : '❌'}`)
259
+ console.log(`[3] Group inbound — no stamp (both): ${c3_neitherStamped ? '✅' : '❌'}`)
260
+ console.log(`[4] Group outbound — trigger (both): ${c4_bothTagged ? '✅' : '❌'}`)
261
+ console.log(`[4] Group outbound — stamp (both): ${c4_bothStamped ? '✅' : '❌'}`)
262
+ console.log(`[5] Backfill — no trigger: ${c5_notTagged ? '✅' : '❌'}`)
263
+ console.log(`[5] Backfill — no stamp: ${c5_noStamp ? '✅' : '❌'}`)
264
+ console.log(`[6] Autoreply — sanity sent: ${sanity_autoreplySent ? '✅' : '❌'}`)
265
+ console.log(`[6] Autoreply — no trigger: ${c6_notTagged ? '✅' : '❌'}`)
266
+ console.log(`[6] Autoreply — no stamp: ${c6_noStamp ? '✅' : '❌'}`)
267
+
268
+ const allPassed =
269
+ c1_notTagged && c1_noStamp
270
+ && c2_tagged && c2_stamped
271
+ && c3_neitherTagged && c3_neitherStamped
272
+ && c4_bothTagged && c4_bothStamped
273
+ && c5_notTagged && c5_noStamp
274
+ && sanity_autoreplySent && c6_notTagged && c6_noStamp
275
+
276
+ if (!allPassed) {
277
+ throw new Error('Outbound Chat Sent trigger tests failed')
278
+ }
279
+
280
+ return { success: true }
281
+ } finally {
282
+ try {
283
+ // Restore org settings FIRST (sequentially, before deletes), so a
284
+ // failure here doesn't leave autoReplyEnabled=true polluting other tests.
285
+ if (settingsModified) {
286
+ try {
287
+ await sdk.api.organizations.updateOne(sdk.userInfo.businessId, {
288
+ outOfOfficeHours: originalOutOfOfficeHours as any,
289
+ settings: { endusers: { autoReplyEnabled: originalAutoReplyEnabled } } as any,
290
+ }, { replaceObjectFields: true })
291
+ console.log(`Restored org settings (autoReplyEnabled=${originalAutoReplyEnabled}, outOfOfficeHours.length=${originalOutOfOfficeHours.length})`)
292
+ } catch (err) {
293
+ console.error(`❌ Failed to restore org settings — manual cleanup may be required: ${err}`)
294
+ }
295
+ }
296
+
297
+ const cleanups: Promise<any>[] = []
298
+
299
+ if (trigger?.id) cleanups.push(sdk.api.automation_triggers.deleteOne(trigger.id).catch(() => {}))
300
+ if (roomSolo?.id) cleanups.push(sdk.api.chat_rooms.deleteOne(roomSolo.id).catch(() => {}))
301
+ if (roomGroup?.id) cleanups.push(sdk.api.chat_rooms.deleteOne(roomGroup.id).catch(() => {}))
302
+ if (roomBackfill?.id) cleanups.push(sdk.api.chat_rooms.deleteOne(roomBackfill.id).catch(() => {}))
303
+ if (roomAutoreply?.id) cleanups.push(sdk.api.chat_rooms.deleteOne(roomAutoreply.id).catch(() => {}))
304
+ if (endSolo?.id) cleanups.push(sdk.api.endusers.deleteOne(endSolo.id).catch(() => {}))
305
+ if (endA?.id) cleanups.push(sdk.api.endusers.deleteOne(endA.id).catch(() => {}))
306
+ if (endB?.id) cleanups.push(sdk.api.endusers.deleteOne(endB.id).catch(() => {}))
307
+ if (endBackfill?.id) cleanups.push(sdk.api.endusers.deleteOne(endBackfill.id).catch(() => {}))
308
+ if (endAutoreply?.id) cleanups.push(sdk.api.endusers.deleteOne(endAutoreply.id).catch(() => {}))
309
+
310
+ await Promise.all(cleanups)
311
+ console.log(`Outbound Chat Sent trigger test cleanup completed`)
312
+ } catch (error) {
313
+ console.error(`Cleanup error: ${error}`)
314
+ }
315
+ }
316
+ }
317
+
318
+ // Allow running this test file independently
319
+ if (require.main === module) {
320
+ const host = process.env.API_URL || 'http://localhost:8080'
321
+ console.log(`🌐 Using API URL: ${host}`)
322
+ const sdk = new Session({ host })
323
+ const sdkNonAdmin = new Session({ host })
324
+
325
+ const runTests = async () => {
326
+ await setup_tests(sdk, sdkNonAdmin)
327
+ await outbound_chat_sent_trigger_tests({ sdk })
328
+ }
329
+
330
+ runTests()
331
+ .then(() => {
332
+ console.log("✅ Outbound Chat Sent trigger tests completed successfully")
333
+ process.exit(0)
334
+ })
335
+ .catch((error) => {
336
+ console.error("❌ Outbound Chat Sent trigger tests failed:", error)
337
+ process.exit(1)
338
+ })
339
+ }
@@ -0,0 +1,198 @@
1
+ require('source-map-support').install();
2
+
3
+ import { Session } from "../../sdk"
4
+ import { log_header, wait, async_test } from "@tellescope/testing"
5
+ import { Enduser } from "@tellescope/types-client"
6
+ import { setup_tests } from "../setup"
7
+
8
+ const host = process.env.API_URL || "http://localhost:8080"
9
+
10
+ const pollFor = async <T>(
11
+ fetchFn: () => Promise<T | undefined>,
12
+ evaluateFn: (result: T | undefined) => result is T,
13
+ description: string,
14
+ intervalMs = 500,
15
+ maxIterations = 30,
16
+ ): Promise<T> => {
17
+ let lastResult: T | undefined
18
+ for (let i = 0; i < maxIterations; i++) {
19
+ await wait(undefined, intervalMs)
20
+ lastResult = await fetchFn()
21
+ if (evaluateFn(lastResult)) return lastResult
22
+ }
23
+ throw new Error(`Polling timeout: ${description} - waited ${maxIterations * intervalMs}ms`)
24
+ }
25
+
26
+ export const push_forms_to_portal_group_completion_tests = async ({ sdk, sdkNonAdmin } : { sdk: Session, sdkNonAdmin: Session }) => {
27
+ log_header("Push Forms To Portal - Form Group Completed Trigger Tests")
28
+
29
+ const createdEnduserIds: string[] = []
30
+ const createdJourneyIds: string[] = []
31
+ const createdFormIds: string[] = []
32
+ const createdFormGroupIds: string[] = []
33
+ const createdTriggerIds: string[] = []
34
+
35
+ try {
36
+ // 1. Create two forms, each with a single text field
37
+ const formA = await sdk.api.forms.createOne({ title: 'Push To Portal Form A' })
38
+ createdFormIds.push(formA.id)
39
+ const fieldA = await sdk.api.form_fields.createOne({
40
+ formId: formA.id,
41
+ type: 'string',
42
+ title: 'FieldA',
43
+ previousFields: [{ type: 'root', info: {} }],
44
+ })
45
+
46
+ const formB = await sdk.api.forms.createOne({ title: 'Push To Portal Form B' })
47
+ createdFormIds.push(formB.id)
48
+ const fieldB = await sdk.api.form_fields.createOne({
49
+ formId: formB.id,
50
+ type: 'string',
51
+ title: 'FieldB',
52
+ previousFields: [{ type: 'root', info: {} }],
53
+ })
54
+
55
+ // 2. Create a form group containing both forms
56
+ const formGroup = await sdk.api.form_groups.createOne({
57
+ title: 'Push To Portal Test Group',
58
+ formIds: [formA.id, formB.id],
59
+ })
60
+ createdFormGroupIds.push(formGroup.id)
61
+
62
+ // 3. Configure trigger with event.info.groupId = the real formGroupId
63
+ const trigger = await sdk.api.automation_triggers.createOne({
64
+ event: { type: 'Form Group Completed', info: { groupId: formGroup.id } },
65
+ action: { type: 'Add Tags', info: { tags: ['form-group-completed-push'] } },
66
+ status: 'Active',
67
+ title: 'Form Group Completed - Push to Portal',
68
+ })
69
+ createdTriggerIds.push(trigger.id)
70
+
71
+ // 4. Create journey with a pushFormsToPortal step referencing the form group
72
+ const journey = await sdk.api.journeys.createOne({
73
+ title: 'Push To Portal Trigger Journey',
74
+ })
75
+ createdJourneyIds.push(journey.id)
76
+
77
+ const pushStep = await sdk.api.automation_steps.createOne({
78
+ journeyId: journey.id,
79
+ action: { type: 'pushFormsToPortal', info: { formGroupIds: [formGroup.id] } },
80
+ events: [{ type: 'onJourneyStart', info: {} }],
81
+ })
82
+
83
+ // 5. Create enduser and add to journey
84
+ const enduser = await sdk.api.endusers.createOne({ fname: 'PushPortal', lname: 'Tester' })
85
+ createdEnduserIds.push(enduser.id)
86
+
87
+ await sdk.api.endusers.add_to_journey({
88
+ enduserIds: [enduser.id],
89
+ journeyId: journey.id,
90
+ })
91
+
92
+ // 6. Poll for the worker to create the push-to-portal form_responses
93
+ const pushedResponses = await pollFor(
94
+ async () => {
95
+ const responses = await sdk.api.form_responses.getSome({
96
+ filter: { enduserId: enduser.id },
97
+ })
98
+ const pushed = responses.filter(r => !!r.pushedToPortalAt)
99
+ return pushed.length >= 2 ? pushed : undefined
100
+ },
101
+ (result): result is any[] => Array.isArray(result) && result.length >= 2,
102
+ 'pushed-to-portal form_responses to be created by worker',
103
+ 500,
104
+ 40,
105
+ )
106
+
107
+ // 7. Assert worker behavior: groupId === automationStepId and pushedToPortalAt is set
108
+ for (const fr of pushedResponses) {
109
+ if (!fr.pushedToPortalAt) {
110
+ throw new Error(`Expected pushedToPortalAt to be set on form_response ${fr.id}`)
111
+ }
112
+ if (fr.groupId !== pushStep.id) {
113
+ throw new Error(`Expected form_response.groupId (${fr.groupId}) to equal automation step id (${pushStep.id})`)
114
+ }
115
+ if (fr.automationStepId !== pushStep.id) {
116
+ throw new Error(`Expected form_response.automationStepId (${fr.automationStepId}) to equal automation step id (${pushStep.id})`)
117
+ }
118
+ }
119
+
120
+ await async_test(
121
+ "Worker writes groupId === automationStepId and pushedToPortalAt set",
122
+ async () => true,
123
+ { onResult: r => r === true },
124
+ )
125
+
126
+ // 8. Submit every form_response on behalf of the enduser
127
+ // Identify which form_response corresponds to formA / formB via formId
128
+ for (const fr of pushedResponses) {
129
+ const isFormA = fr.formId === formA.id
130
+ const targetFieldId = isFormA ? fieldA.id : fieldB.id
131
+ const targetFieldTitle = isFormA ? 'FieldA' : 'FieldB'
132
+ await sdk.api.form_responses.submit_form_response({
133
+ accessCode: fr.accessCode as string,
134
+ responses: [{
135
+ fieldId: targetFieldId,
136
+ fieldTitle: targetFieldTitle,
137
+ answer: { type: 'string', value: 'pushed-portal-answer' },
138
+ }],
139
+ })
140
+ }
141
+
142
+ // 9. Poll for the trigger's side-effect (tag on enduser)
143
+ await pollFor(
144
+ async () => {
145
+ const e = await sdk.api.endusers.getOne(enduser.id)
146
+ return e.tags?.includes('form-group-completed-push') ? e : undefined
147
+ },
148
+ (result): result is Enduser => !!result,
149
+ 'Form Group Completed trigger to apply tag after push-to-portal submissions',
150
+ 500,
151
+ 30,
152
+ )
153
+
154
+ await async_test(
155
+ "Form Group Completed trigger fires for push-to-portal completion",
156
+ () => sdk.api.endusers.getOne(enduser.id),
157
+ { onResult: (e: Enduser) => !!e.tags?.includes('form-group-completed-push') },
158
+ )
159
+
160
+ } finally {
161
+ for (const id of createdTriggerIds) {
162
+ try { await sdk.api.automation_triggers.deleteOne(id) } catch (e) { /* ignore */ }
163
+ }
164
+ for (const id of createdEnduserIds) {
165
+ try { await sdk.api.endusers.deleteOne(id) } catch (e) { /* ignore */ }
166
+ }
167
+ for (const id of createdJourneyIds) {
168
+ try { await sdk.api.journeys.deleteOne(id) } catch (e) { /* ignore */ }
169
+ }
170
+ for (const id of createdFormGroupIds) {
171
+ try { await sdk.api.form_groups.deleteOne(id) } catch (e) { /* ignore */ }
172
+ }
173
+ for (const id of createdFormIds) {
174
+ try { await sdk.api.forms.deleteOne(id) } catch (e) { /* ignore */ }
175
+ }
176
+ }
177
+ }
178
+
179
+ if (require.main === module) {
180
+ console.log(`🌐 Using API URL: ${host}`)
181
+ const sdk = new Session({ host })
182
+ const sdkNonAdmin = new Session({ host })
183
+
184
+ const runTests = async () => {
185
+ await setup_tests(sdk, sdkNonAdmin)
186
+ await push_forms_to_portal_group_completion_tests({ sdk, sdkNonAdmin })
187
+ }
188
+
189
+ runTests()
190
+ .then(() => {
191
+ console.log("✅ Push forms to portal group completion test suite completed successfully")
192
+ process.exit(0)
193
+ })
194
+ .catch((error) => {
195
+ console.error("❌ Push forms to portal group completion test suite failed:", error)
196
+ process.exit(1)
197
+ })
198
+ }