@tellescope/sdk 1.255.0 → 1.255.2
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/tests/api_tests/appointment_no_showed_trigger.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/appointment_no_showed_trigger.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/appointment_no_showed_trigger.test.js +236 -0
- package/lib/cjs/tests/api_tests/appointment_no_showed_trigger.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/calendar_events_bulk_update.test.d.ts.map +1 -1
- package/lib/cjs/tests/api_tests/calendar_events_bulk_update.test.js +108 -73
- package/lib/cjs/tests/api_tests/calendar_events_bulk_update.test.js.map +1 -1
- package/lib/cjs/tests/api_tests/group_event_attendee_status_triggers.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/group_event_attendee_status_triggers.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/group_event_attendee_status_triggers.test.js +587 -0
- package/lib/cjs/tests/api_tests/group_event_attendee_status_triggers.test.js.map +1 -0
- package/lib/cjs/tests/api_tests/healthie_force_sync.test.d.ts +6 -0
- package/lib/cjs/tests/api_tests/healthie_force_sync.test.d.ts.map +1 -0
- package/lib/cjs/tests/api_tests/healthie_force_sync.test.js +165 -0
- package/lib/cjs/tests/api_tests/healthie_force_sync.test.js.map +1 -0
- package/lib/cjs/tests/tests.d.ts.map +1 -1
- package/lib/cjs/tests/tests.js +55 -41
- package/lib/cjs/tests/tests.js.map +1 -1
- package/lib/esm/sdk.d.ts +2 -2
- package/lib/esm/session.d.ts +0 -1
- package/lib/esm/session.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/appointment_no_showed_trigger.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/appointment_no_showed_trigger.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/appointment_no_showed_trigger.test.js +232 -0
- package/lib/esm/tests/api_tests/appointment_no_showed_trigger.test.js.map +1 -0
- package/lib/esm/tests/api_tests/calendar_events_bulk_update.test.d.ts.map +1 -1
- package/lib/esm/tests/api_tests/calendar_events_bulk_update.test.js +109 -74
- package/lib/esm/tests/api_tests/calendar_events_bulk_update.test.js.map +1 -1
- package/lib/esm/tests/api_tests/group_event_attendee_status_triggers.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/group_event_attendee_status_triggers.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/group_event_attendee_status_triggers.test.js +583 -0
- package/lib/esm/tests/api_tests/group_event_attendee_status_triggers.test.js.map +1 -0
- package/lib/esm/tests/api_tests/healthie_force_sync.test.d.ts +6 -0
- package/lib/esm/tests/api_tests/healthie_force_sync.test.d.ts.map +1 -0
- package/lib/esm/tests/api_tests/healthie_force_sync.test.js +161 -0
- package/lib/esm/tests/api_tests/healthie_force_sync.test.js.map +1 -0
- package/lib/esm/tests/tests.d.ts.map +1 -1
- package/lib/esm/tests/tests.js +55 -41
- package/lib/esm/tests/tests.js.map +1 -1
- package/lib/tsconfig.tsbuildinfo +1 -1
- package/package.json +10 -10
- package/src/tests/api_tests/appointment_no_showed_trigger.test.ts +144 -0
- package/src/tests/api_tests/calendar_events_bulk_update.test.ts +22 -0
- package/src/tests/api_tests/group_event_attendee_status_triggers.test.ts +381 -0
- package/src/tests/tests.ts +7 -1
- package/test_generated.pdf +0 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { Session } from "../../sdk"
|
|
2
|
+
import { log_header, wait, async_test } from "@tellescope/testing"
|
|
3
|
+
import { Enduser } from "@tellescope/types-client"
|
|
4
|
+
import { setup_tests } from "../setup"
|
|
5
|
+
|
|
6
|
+
const host = process.env.API_URL || "http://localhost:8080"
|
|
7
|
+
|
|
8
|
+
// Covers the event-wide "Appointment No-Showed" trigger path (handle_no_showed_event), which had
|
|
9
|
+
// no dedicated test. Mirrors appointment_completed_trigger.test.ts.
|
|
10
|
+
export const appointment_no_showed_trigger_tests = async ({ sdk } : { sdk: Session, sdkNonAdmin: Session }) => {
|
|
11
|
+
log_header("Automation Trigger Tests (Appointment No-Showed)")
|
|
12
|
+
|
|
13
|
+
const t1 = await sdk.api.automation_triggers.createOne({
|
|
14
|
+
event: { type: 'Appointment No-Showed', info: {} },
|
|
15
|
+
action: { type: 'Add Tags', info: { tags: ['No-Show - Any'] } },
|
|
16
|
+
status: 'Active',
|
|
17
|
+
title: "No-Show - Any",
|
|
18
|
+
})
|
|
19
|
+
const t2 = await sdk.api.automation_triggers.createOne({
|
|
20
|
+
event: { type: 'Appointment No-Showed', info: { titles: ['Test Appointment'] } },
|
|
21
|
+
action: { type: 'Add Tags', info: { tags: ['No-Show - By Title'] } },
|
|
22
|
+
status: 'Active',
|
|
23
|
+
title: "No-Show - By Title",
|
|
24
|
+
})
|
|
25
|
+
const template = await sdk.api.calendar_event_templates.createOne({
|
|
26
|
+
title: 'No-Show Template',
|
|
27
|
+
durationInMinutes: 30,
|
|
28
|
+
})
|
|
29
|
+
const t3 = await sdk.api.automation_triggers.createOne({
|
|
30
|
+
event: { type: 'Appointment No-Showed', info: { templateIds: [template.id] } },
|
|
31
|
+
action: { type: 'Add Tags', info: { tags: ['No-Show - By Template'] } },
|
|
32
|
+
status: 'Active',
|
|
33
|
+
title: "No-Show - By Template",
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
// Separate endusers to avoid per-trigger throttling (1 min per trigger per enduser)
|
|
37
|
+
const enduser1 = await sdk.api.endusers.createOne({})
|
|
38
|
+
const enduser2 = await sdk.api.endusers.createOne({})
|
|
39
|
+
const enduser3 = await sdk.api.endusers.createOne({})
|
|
40
|
+
|
|
41
|
+
// Note: the "Appointment No-Showed" trigger fires only on UPDATE (handle_no_showed_event);
|
|
42
|
+
// there is no create-time no-show path, so every case creates a normal event then updates it.
|
|
43
|
+
|
|
44
|
+
// Test 1: Update to no-showed by title (should trigger Any + By Title)
|
|
45
|
+
const event1 = await sdk.api.calendar_events.createOne({
|
|
46
|
+
title: 'Test Appointment',
|
|
47
|
+
durationInMinutes: 30,
|
|
48
|
+
startTimeInMS: Date.now(),
|
|
49
|
+
attendees: [{ type: 'enduser', id: enduser1.id }],
|
|
50
|
+
})
|
|
51
|
+
await wait(undefined, 500)
|
|
52
|
+
await async_test(
|
|
53
|
+
"No initial triggers before no-show",
|
|
54
|
+
() => sdk.api.endusers.getOne(enduser1.id),
|
|
55
|
+
{ onResult: (e: Enduser) => !e.tags?.length }
|
|
56
|
+
)
|
|
57
|
+
await sdk.api.calendar_events.updateOne(event1.id, { noShowedAt: new Date() })
|
|
58
|
+
await wait(undefined, 500)
|
|
59
|
+
await async_test(
|
|
60
|
+
"Update to no-showed triggers automation (Any + By Title)",
|
|
61
|
+
() => sdk.api.endusers.getOne(enduser1.id),
|
|
62
|
+
{ onResult: (e: Enduser) =>
|
|
63
|
+
e.tags?.length === 2
|
|
64
|
+
&& e.tags.includes('No-Show - Any')
|
|
65
|
+
&& e.tags.includes('No-Show - By Title')
|
|
66
|
+
}
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
// Test 2: Template-based no-show (should trigger Any + By Template)
|
|
70
|
+
const event2 = await sdk.api.calendar_events.createOne({
|
|
71
|
+
title: 'Template Test',
|
|
72
|
+
durationInMinutes: 30,
|
|
73
|
+
startTimeInMS: Date.now(),
|
|
74
|
+
attendees: [{ type: 'enduser', id: enduser2.id }],
|
|
75
|
+
templateId: template.id,
|
|
76
|
+
})
|
|
77
|
+
await wait(undefined, 500)
|
|
78
|
+
await sdk.api.calendar_events.updateOne(event2.id, { noShowedAt: new Date() })
|
|
79
|
+
await wait(undefined, 500)
|
|
80
|
+
await async_test(
|
|
81
|
+
"Template-based no-showed appointment triggers automation (Any + By Template)",
|
|
82
|
+
() => sdk.api.endusers.getOne(enduser2.id),
|
|
83
|
+
{ onResult: (e: Enduser) =>
|
|
84
|
+
e.tags?.length === 2
|
|
85
|
+
&& e.tags.includes('No-Show - Any')
|
|
86
|
+
&& e.tags.includes('No-Show - By Template')
|
|
87
|
+
}
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
// Test 3: Non-matching title only fires the "Any" trigger
|
|
91
|
+
const event3 = await sdk.api.calendar_events.createOne({
|
|
92
|
+
title: 'Other Title',
|
|
93
|
+
durationInMinutes: 30,
|
|
94
|
+
startTimeInMS: Date.now(),
|
|
95
|
+
attendees: [{ type: 'enduser', id: enduser3.id }],
|
|
96
|
+
})
|
|
97
|
+
await wait(undefined, 500)
|
|
98
|
+
await sdk.api.calendar_events.updateOne(event3.id, { noShowedAt: new Date() })
|
|
99
|
+
await wait(undefined, 500)
|
|
100
|
+
await async_test(
|
|
101
|
+
"No-show with non-matching title fires only the Any trigger",
|
|
102
|
+
() => sdk.api.endusers.getOne(enduser3.id),
|
|
103
|
+
{ onResult: (e: Enduser) =>
|
|
104
|
+
e.tags?.length === 1
|
|
105
|
+
&& e.tags.includes('No-Show - Any')
|
|
106
|
+
}
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
// Cleanup
|
|
110
|
+
await Promise.all([
|
|
111
|
+
sdk.api.automation_triggers.deleteOne(t1.id),
|
|
112
|
+
sdk.api.automation_triggers.deleteOne(t2.id),
|
|
113
|
+
sdk.api.automation_triggers.deleteOne(t3.id),
|
|
114
|
+
sdk.api.calendar_event_templates.deleteOne(template.id),
|
|
115
|
+
sdk.api.endusers.deleteOne(enduser1.id),
|
|
116
|
+
sdk.api.endusers.deleteOne(enduser2.id),
|
|
117
|
+
sdk.api.endusers.deleteOne(enduser3.id),
|
|
118
|
+
sdk.api.calendar_events.deleteOne(event1.id),
|
|
119
|
+
sdk.api.calendar_events.deleteOne(event2.id),
|
|
120
|
+
sdk.api.calendar_events.deleteOne(event3.id),
|
|
121
|
+
])
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Allow running this test file independently
|
|
125
|
+
if (require.main === module) {
|
|
126
|
+
console.log(`🌐 Using API URL: ${host}`)
|
|
127
|
+
const sdk = new Session({ host })
|
|
128
|
+
const sdkNonAdmin = new Session({ host })
|
|
129
|
+
|
|
130
|
+
const runTests = async () => {
|
|
131
|
+
await setup_tests(sdk, sdkNonAdmin)
|
|
132
|
+
await appointment_no_showed_trigger_tests({ sdk, sdkNonAdmin })
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
runTests()
|
|
136
|
+
.then(() => {
|
|
137
|
+
console.log("✅ Appointment No-Showed trigger tests completed successfully")
|
|
138
|
+
process.exit(0)
|
|
139
|
+
})
|
|
140
|
+
.catch((error) => {
|
|
141
|
+
console.error("❌ Appointment No-Showed trigger tests failed:", error)
|
|
142
|
+
process.exit(1)
|
|
143
|
+
})
|
|
144
|
+
}
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
assert,
|
|
7
7
|
handleAnyError,
|
|
8
8
|
log_header,
|
|
9
|
+
wait,
|
|
9
10
|
} from "@tellescope/testing"
|
|
10
11
|
import { setup_tests } from "../setup"
|
|
11
12
|
|
|
@@ -17,6 +18,7 @@ export const calendar_events_bulk_update_tests = async ({ sdk }: { sdk: Session
|
|
|
17
18
|
// Track all created resources for cleanup
|
|
18
19
|
const createdEventIds: string[] = []
|
|
19
20
|
const createdEnduserIds: string[] = []
|
|
21
|
+
const createdTriggerIds: string[] = []
|
|
20
22
|
|
|
21
23
|
const createEvent = async (overrides: Record<string, any> = {}) => {
|
|
22
24
|
const event = await sdk.api.calendar_events.createOne({
|
|
@@ -305,6 +307,16 @@ export const calendar_events_bulk_update_tests = async ({ sdk }: { sdk: Session
|
|
|
305
307
|
// --- Cancel for attendee ---
|
|
306
308
|
log_header("Bulk Update - Recurring Cancel for Attendee")
|
|
307
309
|
|
|
310
|
+
// Cancelled trigger to verify cancel_for_attendee fires the Appointment Cancelled trigger
|
|
311
|
+
// (previously this path's mutation was tested, but the trigger firing was not)
|
|
312
|
+
const cancelledTrigger = await sdk.api.automation_triggers.createOne({
|
|
313
|
+
event: { type: 'Appointment Cancelled', info: {} },
|
|
314
|
+
action: { type: 'Add Tags', info: { tags: ['Bulk Cancel For Attendee'] } },
|
|
315
|
+
status: 'Active',
|
|
316
|
+
title: "Bulk Cancel For Attendee",
|
|
317
|
+
})
|
|
318
|
+
createdTriggerIds.push(cancelledTrigger.id)
|
|
319
|
+
|
|
308
320
|
await async_test(
|
|
309
321
|
"cancel_for_attendee adds enduser to cancelledGroupAttendees across series",
|
|
310
322
|
() => sdk.api.calendar_events.bulk_update({
|
|
@@ -323,6 +335,13 @@ export const calendar_events_bulk_update_tests = async ({ sdk }: { sdk: Session
|
|
|
323
335
|
}
|
|
324
336
|
)
|
|
325
337
|
|
|
338
|
+
await wait(undefined, 750) // allow Cancelled trigger to fire
|
|
339
|
+
await async_test(
|
|
340
|
+
"cancel_for_attendee fires Appointment Cancelled trigger for the attendee",
|
|
341
|
+
() => sdk.api.endusers.getOne(enduser.id),
|
|
342
|
+
{ onResult: (e: any) => !!e.tags?.includes('Bulk Cancel For Attendee') }
|
|
343
|
+
)
|
|
344
|
+
|
|
326
345
|
// --- Uncancel for attendee ---
|
|
327
346
|
log_header("Bulk Update - Recurring Uncancel for Attendee")
|
|
328
347
|
|
|
@@ -392,6 +411,9 @@ export const calendar_events_bulk_update_tests = async ({ sdk }: { sdk: Session
|
|
|
392
411
|
for (const id of createdEnduserIds) {
|
|
393
412
|
try { await sdk.api.endusers.deleteOne(id) } catch (_) {}
|
|
394
413
|
}
|
|
414
|
+
for (const id of createdTriggerIds) {
|
|
415
|
+
try { await sdk.api.automation_triggers.deleteOne(id) } catch (_) {}
|
|
416
|
+
}
|
|
395
417
|
}
|
|
396
418
|
}
|
|
397
419
|
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import { Session } from "../../sdk"
|
|
2
|
+
import { log_header, wait, async_test } from "@tellescope/testing"
|
|
3
|
+
import { Enduser } from "@tellescope/types-client"
|
|
4
|
+
import { AttendeeStatus } from "@tellescope/types-models"
|
|
5
|
+
import { setup_tests } from "../setup"
|
|
6
|
+
|
|
7
|
+
const host = process.env.API_URL || "http://localhost:8080"
|
|
8
|
+
|
|
9
|
+
// Tags applied by the four per-status triggers. Distinct so we can prove exactly which trigger
|
|
10
|
+
// fired against which attendee (no false positives / no false negatives).
|
|
11
|
+
const TAG = {
|
|
12
|
+
'Completed': 'Group Attendee: Completed',
|
|
13
|
+
'No-showed': 'Group Attendee: No-showed',
|
|
14
|
+
'Rescheduled': 'Group Attendee: Rescheduled',
|
|
15
|
+
'Cancelled': 'Group Attendee: Cancelled',
|
|
16
|
+
} as const
|
|
17
|
+
|
|
18
|
+
type TrackableStatus = keyof typeof TAG
|
|
19
|
+
const ALL_STATUSES: TrackableStatus[] = ['Completed', 'No-showed', 'Rescheduled', 'Cancelled']
|
|
20
|
+
|
|
21
|
+
export const group_event_attendee_status_triggers_tests = async ({ sdk } : { sdk: Session, sdkNonAdmin: Session }) => {
|
|
22
|
+
log_header("Automation Trigger Tests (Group Event Per-Attendee Status)")
|
|
23
|
+
|
|
24
|
+
const createdTriggerIds: string[] = []
|
|
25
|
+
const createdEnduserIds: string[] = []
|
|
26
|
+
const createdEventIds: string[] = []
|
|
27
|
+
|
|
28
|
+
const createEnduser = async () => {
|
|
29
|
+
const e = await sdk.api.endusers.createOne({})
|
|
30
|
+
createdEnduserIds.push(e.id)
|
|
31
|
+
return e
|
|
32
|
+
}
|
|
33
|
+
const createGroupEvent = async (attendeeIds: string[]) => {
|
|
34
|
+
const event = await sdk.api.calendar_events.createOne({
|
|
35
|
+
title: 'Group Status Test',
|
|
36
|
+
durationInMinutes: 30,
|
|
37
|
+
startTimeInMS: Date.now() + Math.floor(Math.random() * 100000),
|
|
38
|
+
attendees: attendeeIds.map(id => ({ type: 'enduser' as const, id })),
|
|
39
|
+
})
|
|
40
|
+
createdEventIds.push(event.id)
|
|
41
|
+
return event
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Replicates the webapp AttendeeStatusEditor save: writes attendeeStatuses, and mirrors statuses
|
|
45
|
+
// into cancelledGroupAttendees for Cancelled/No-showed/Rescheduled (to suppress reminders).
|
|
46
|
+
const setAttendeeStatuses = async (eventId: string, statuses: { id: string, status: AttendeeStatus['status'] }[]) => (
|
|
47
|
+
sdk.api.calendar_events.updateOne(eventId, {
|
|
48
|
+
attendeeStatuses: statuses.map(s => ({ id: s.id, at: new Date(), status: s.status })),
|
|
49
|
+
cancelledGroupAttendees: (
|
|
50
|
+
statuses
|
|
51
|
+
.filter(s => s.status === 'Cancelled' || s.status === 'No-showed' || s.status === 'Rescheduled')
|
|
52
|
+
.map(s => ({ id: s.id, at: new Date() }))
|
|
53
|
+
),
|
|
54
|
+
}, { replaceObjectFields: true })
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
const tagsOf = async (enduserId: string) => (await sdk.api.endusers.getOne(enduserId)).tags || []
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
// One "Add Tags" trigger per status, matching any appointment (info: {})
|
|
61
|
+
for (const status of ALL_STATUSES) {
|
|
62
|
+
const t = await sdk.api.automation_triggers.createOne({
|
|
63
|
+
event: {
|
|
64
|
+
type: status === 'No-showed' ? 'Appointment No-Showed'
|
|
65
|
+
: status === 'Completed' ? 'Appointment Completed'
|
|
66
|
+
: status === 'Rescheduled' ? 'Appointment Rescheduled'
|
|
67
|
+
: 'Appointment Cancelled',
|
|
68
|
+
info: {},
|
|
69
|
+
},
|
|
70
|
+
action: { type: 'Add Tags', info: { tags: [TAG[status]] } },
|
|
71
|
+
status: 'Active',
|
|
72
|
+
title: `Group Attendee ${status}`,
|
|
73
|
+
})
|
|
74
|
+
createdTriggerIds.push(t.id)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ============================================================
|
|
78
|
+
// For each status: target gets exactly its tag; bystanders get nothing.
|
|
79
|
+
// ============================================================
|
|
80
|
+
for (const status of ALL_STATUSES) {
|
|
81
|
+
log_header(`Per-attendee '${status}' in a group event`)
|
|
82
|
+
|
|
83
|
+
const target = await createEnduser()
|
|
84
|
+
const bystander1 = await createEnduser()
|
|
85
|
+
const bystander2 = await createEnduser()
|
|
86
|
+
const event = await createGroupEvent([target.id, bystander1.id, bystander2.id])
|
|
87
|
+
|
|
88
|
+
await setAttendeeStatuses(event.id, [{ id: target.id, status }])
|
|
89
|
+
await wait(undefined, 750) // allow trigger to fire
|
|
90
|
+
|
|
91
|
+
// 1 + 2. True positive / no false negative: target has the correct tag
|
|
92
|
+
await async_test(
|
|
93
|
+
`'${status}' fires its trigger for the target attendee`,
|
|
94
|
+
() => tagsOf(target.id),
|
|
95
|
+
{ onResult: (tags: string[]) => tags.includes(TAG[status]) }
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
// 3. No false positive (wrong trigger): target has none of the other status tags
|
|
99
|
+
await async_test(
|
|
100
|
+
`'${status}' does NOT fire any other status trigger for the target`,
|
|
101
|
+
() => tagsOf(target.id),
|
|
102
|
+
{ onResult: (tags: string[]) => ALL_STATUSES.every(s => s === status || !tags.includes(TAG[s])) }
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
// 4. No false positive (collateral): bystanders received zero tags
|
|
106
|
+
await async_test(
|
|
107
|
+
`'${status}' does NOT fire for bystander attendee #1`,
|
|
108
|
+
() => sdk.api.endusers.getOne(bystander1.id),
|
|
109
|
+
{ onResult: (e: Enduser) => !e.tags?.length }
|
|
110
|
+
)
|
|
111
|
+
await async_test(
|
|
112
|
+
`'${status}' does NOT fire for bystander attendee #2`,
|
|
113
|
+
() => sdk.api.endusers.getOne(bystander2.id),
|
|
114
|
+
{ onResult: (e: Enduser) => !e.tags?.length }
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ============================================================
|
|
119
|
+
// Multiple distinct statuses set in one group event — no cross-talk.
|
|
120
|
+
// ============================================================
|
|
121
|
+
log_header("Multiple distinct per-attendee statuses in one group event")
|
|
122
|
+
|
|
123
|
+
const aCompleted = await createEnduser()
|
|
124
|
+
const bNoShow = await createEnduser()
|
|
125
|
+
const cCancelled = await createEnduser()
|
|
126
|
+
const dUnset = await createEnduser()
|
|
127
|
+
const mixedEvent = await createGroupEvent([aCompleted.id, bNoShow.id, cCancelled.id, dUnset.id])
|
|
128
|
+
|
|
129
|
+
await setAttendeeStatuses(mixedEvent.id, [
|
|
130
|
+
{ id: aCompleted.id, status: 'Completed' },
|
|
131
|
+
{ id: bNoShow.id, status: 'No-showed' },
|
|
132
|
+
{ id: cCancelled.id, status: 'Cancelled' },
|
|
133
|
+
// dUnset intentionally left without a status
|
|
134
|
+
])
|
|
135
|
+
await wait(undefined, 1000) // allow all triggers to fire
|
|
136
|
+
|
|
137
|
+
await async_test(
|
|
138
|
+
"mixed: Completed attendee gets only the Completed tag",
|
|
139
|
+
() => tagsOf(aCompleted.id),
|
|
140
|
+
{ onResult: (tags: string[]) => tags.includes(TAG['Completed']) && ALL_STATUSES.every(s => s === 'Completed' || !tags.includes(TAG[s])) }
|
|
141
|
+
)
|
|
142
|
+
await async_test(
|
|
143
|
+
"mixed: No-showed attendee gets only the No-showed tag",
|
|
144
|
+
() => tagsOf(bNoShow.id),
|
|
145
|
+
{ onResult: (tags: string[]) => tags.includes(TAG['No-showed']) && ALL_STATUSES.every(s => s === 'No-showed' || !tags.includes(TAG[s])) }
|
|
146
|
+
)
|
|
147
|
+
await async_test(
|
|
148
|
+
"mixed: Cancelled attendee gets only the Cancelled tag",
|
|
149
|
+
() => tagsOf(cCancelled.id),
|
|
150
|
+
{ onResult: (tags: string[]) => tags.includes(TAG['Cancelled']) && ALL_STATUSES.every(s => s === 'Cancelled' || !tags.includes(TAG[s])) }
|
|
151
|
+
)
|
|
152
|
+
await async_test(
|
|
153
|
+
"mixed: unset attendee receives no tags",
|
|
154
|
+
() => sdk.api.endusers.getOne(dUnset.id),
|
|
155
|
+
{ onResult: (e: Enduser) => !e.tags?.length }
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
// ============================================================
|
|
159
|
+
// Status transitions (A -> B): changing an attendee's status fires the NEW status's trigger.
|
|
160
|
+
// Exercises both directions of the cancelledGroupAttendees mirroring:
|
|
161
|
+
// - mirrored -> unmirrored (No-showed -> Completed)
|
|
162
|
+
// - unmirrored -> mirrored (Completed -> No-showed)
|
|
163
|
+
// The prior status's tag remains (it fired at its own transition); no spurious Cancelled fires
|
|
164
|
+
// from the cancelledGroupAttendees add/remove, and bystanders never fire.
|
|
165
|
+
// ============================================================
|
|
166
|
+
log_header("Per-attendee status transitions fire the new status only")
|
|
167
|
+
|
|
168
|
+
// mirrored -> unmirrored
|
|
169
|
+
const transA = await createEnduser()
|
|
170
|
+
const transABystander = await createEnduser()
|
|
171
|
+
const transAEvent = await createGroupEvent([transA.id, transABystander.id])
|
|
172
|
+
|
|
173
|
+
await setAttendeeStatuses(transAEvent.id, [{ id: transA.id, status: 'No-showed' }])
|
|
174
|
+
await wait(undefined, 750)
|
|
175
|
+
await async_test(
|
|
176
|
+
"transition step 1: No-showed fires No-showed",
|
|
177
|
+
() => tagsOf(transA.id),
|
|
178
|
+
{ onResult: (tags: string[]) => tags.includes(TAG['No-showed']) }
|
|
179
|
+
)
|
|
180
|
+
await setAttendeeStatuses(transAEvent.id, [{ id: transA.id, status: 'Completed' }])
|
|
181
|
+
await wait(undefined, 750)
|
|
182
|
+
await async_test(
|
|
183
|
+
"transition No-showed -> Completed fires Completed (No-showed tag retained, no Cancelled)",
|
|
184
|
+
() => tagsOf(transA.id),
|
|
185
|
+
{ onResult: (tags: string[]) =>
|
|
186
|
+
tags.includes(TAG['Completed'])
|
|
187
|
+
&& tags.includes(TAG['No-showed'])
|
|
188
|
+
&& !tags.includes(TAG['Cancelled'])
|
|
189
|
+
&& !tags.includes(TAG['Rescheduled'])
|
|
190
|
+
}
|
|
191
|
+
)
|
|
192
|
+
await async_test(
|
|
193
|
+
"transition (mirrored->unmirrored): bystander never fires",
|
|
194
|
+
() => sdk.api.endusers.getOne(transABystander.id),
|
|
195
|
+
{ onResult: (e: Enduser) => !e.tags?.length }
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
// unmirrored -> mirrored
|
|
199
|
+
const transB = await createEnduser()
|
|
200
|
+
const transBBystander = await createEnduser()
|
|
201
|
+
const transBEvent = await createGroupEvent([transB.id, transBBystander.id])
|
|
202
|
+
|
|
203
|
+
await setAttendeeStatuses(transBEvent.id, [{ id: transB.id, status: 'Completed' }])
|
|
204
|
+
await wait(undefined, 750)
|
|
205
|
+
await setAttendeeStatuses(transBEvent.id, [{ id: transB.id, status: 'No-showed' }])
|
|
206
|
+
await wait(undefined, 750)
|
|
207
|
+
await async_test(
|
|
208
|
+
"transition Completed -> No-showed fires No-showed (Completed tag retained, no Cancelled)",
|
|
209
|
+
() => tagsOf(transB.id),
|
|
210
|
+
{ onResult: (tags: string[]) =>
|
|
211
|
+
tags.includes(TAG['No-showed'])
|
|
212
|
+
&& tags.includes(TAG['Completed'])
|
|
213
|
+
&& !tags.includes(TAG['Cancelled'])
|
|
214
|
+
&& !tags.includes(TAG['Rescheduled'])
|
|
215
|
+
}
|
|
216
|
+
)
|
|
217
|
+
await async_test(
|
|
218
|
+
"transition (unmirrored->mirrored): bystander never fires",
|
|
219
|
+
() => sdk.api.endusers.getOne(transBBystander.id),
|
|
220
|
+
{ onResult: (e: Enduser) => !e.tags?.length }
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
// ============================================================
|
|
224
|
+
// 'by' filter on the per-attendee Cancelled path. The AttendeeStatusEditor update does not set
|
|
225
|
+
// statusChangeSource, so handle_cancelled_event's by-filter (which requires statusChangeSource
|
|
226
|
+
// to match 'user'/'enduser') excludes by-filtered triggers. Only the unfiltered (by: undefined)
|
|
227
|
+
// Cancelled trigger fires for a per-attendee cancellation. This pins current behavior; see the
|
|
228
|
+
// note returned with this change about whether by:'user' SHOULD fire here.
|
|
229
|
+
// ============================================================
|
|
230
|
+
log_header("Per-attendee Cancelled respects 'by' filter")
|
|
231
|
+
|
|
232
|
+
const byUserTrigger = await sdk.api.automation_triggers.createOne({
|
|
233
|
+
event: { type: 'Appointment Cancelled', info: { by: 'user' } },
|
|
234
|
+
action: { type: 'Add Tags', info: { tags: ['Group Attendee Cancelled by:user'] } },
|
|
235
|
+
status: 'Active',
|
|
236
|
+
title: "Group Attendee Cancelled by user",
|
|
237
|
+
})
|
|
238
|
+
createdTriggerIds.push(byUserTrigger.id)
|
|
239
|
+
const byEnduserTrigger = await sdk.api.automation_triggers.createOne({
|
|
240
|
+
event: { type: 'Appointment Cancelled', info: { by: 'enduser' } },
|
|
241
|
+
action: { type: 'Add Tags', info: { tags: ['Group Attendee Cancelled by:enduser'] } },
|
|
242
|
+
status: 'Active',
|
|
243
|
+
title: "Group Attendee Cancelled by enduser",
|
|
244
|
+
})
|
|
245
|
+
createdTriggerIds.push(byEnduserTrigger.id)
|
|
246
|
+
|
|
247
|
+
const byTarget = await createEnduser()
|
|
248
|
+
const byBystander = await createEnduser()
|
|
249
|
+
const byEvent = await createGroupEvent([byTarget.id, byBystander.id])
|
|
250
|
+
|
|
251
|
+
await setAttendeeStatuses(byEvent.id, [{ id: byTarget.id, status: 'Cancelled' }])
|
|
252
|
+
await wait(undefined, 750)
|
|
253
|
+
|
|
254
|
+
await async_test(
|
|
255
|
+
"per-attendee cancel fires the unfiltered (by: undefined) Cancelled trigger",
|
|
256
|
+
() => tagsOf(byTarget.id),
|
|
257
|
+
{ onResult: (tags: string[]) => tags.includes(TAG['Cancelled']) }
|
|
258
|
+
)
|
|
259
|
+
await async_test(
|
|
260
|
+
"per-attendee cancel does NOT fire by:'enduser' Cancelled trigger",
|
|
261
|
+
() => tagsOf(byTarget.id),
|
|
262
|
+
{ onResult: (tags: string[]) => !tags.includes('Group Attendee Cancelled by:enduser') }
|
|
263
|
+
)
|
|
264
|
+
await async_test(
|
|
265
|
+
"per-attendee cancel does NOT fire by:'user' Cancelled trigger (statusChangeSource unset)",
|
|
266
|
+
() => tagsOf(byTarget.id),
|
|
267
|
+
{ onResult: (tags: string[]) => !tags.includes('Group Attendee Cancelled by:user') }
|
|
268
|
+
)
|
|
269
|
+
await async_test(
|
|
270
|
+
"'by' filter case: bystander never fires",
|
|
271
|
+
() => sdk.api.endusers.getOne(byBystander.id),
|
|
272
|
+
{ onResult: (e: Enduser) => !e.tags?.length }
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
// ============================================================
|
|
276
|
+
// bulk_update cancel_for_attendee fires Cancelled exactly once (via the new handler;
|
|
277
|
+
// the legacy cancelledGroupAttendees branch is suppressed by the attendeeStatuses filter).
|
|
278
|
+
// ============================================================
|
|
279
|
+
log_header("bulk_update cancel_for_attendee fires Cancelled for attendee")
|
|
280
|
+
|
|
281
|
+
const bulkTarget = await createEnduser()
|
|
282
|
+
const bulkBystander = await createEnduser()
|
|
283
|
+
const now = Date.now()
|
|
284
|
+
const DAY = 24 * 60 * 60 * 1000
|
|
285
|
+
const rootEvent = await sdk.api.calendar_events.createOne({
|
|
286
|
+
title: 'Group Status Test',
|
|
287
|
+
durationInMinutes: 30,
|
|
288
|
+
startTimeInMS: now,
|
|
289
|
+
attendees: [{ type: 'enduser', id: bulkTarget.id }, { type: 'enduser', id: bulkBystander.id }],
|
|
290
|
+
})
|
|
291
|
+
createdEventIds.push(rootEvent.id)
|
|
292
|
+
const childEvent = await sdk.api.calendar_events.createOne({
|
|
293
|
+
title: 'Group Status Test',
|
|
294
|
+
durationInMinutes: 30,
|
|
295
|
+
startTimeInMS: now + DAY,
|
|
296
|
+
copiedFrom: rootEvent.id,
|
|
297
|
+
attendees: [{ type: 'enduser', id: bulkTarget.id }, { type: 'enduser', id: bulkBystander.id }],
|
|
298
|
+
})
|
|
299
|
+
createdEventIds.push(childEvent.id)
|
|
300
|
+
|
|
301
|
+
await sdk.api.calendar_events.bulk_update({
|
|
302
|
+
recurringEventId: rootEvent.id,
|
|
303
|
+
action: 'cancel_for_attendee',
|
|
304
|
+
scope: 'all',
|
|
305
|
+
enduserId: bulkTarget.id,
|
|
306
|
+
})
|
|
307
|
+
await wait(undefined, 750)
|
|
308
|
+
|
|
309
|
+
await async_test(
|
|
310
|
+
"cancel_for_attendee fires Cancelled trigger for the target",
|
|
311
|
+
() => tagsOf(bulkTarget.id),
|
|
312
|
+
{ onResult: (tags: string[]) => tags.includes(TAG['Cancelled']) }
|
|
313
|
+
)
|
|
314
|
+
await async_test(
|
|
315
|
+
"cancel_for_attendee does NOT fire for the bystander",
|
|
316
|
+
() => sdk.api.endusers.getOne(bulkBystander.id),
|
|
317
|
+
{ onResult: (e: Enduser) => !e.tags?.length }
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
// ============================================================
|
|
321
|
+
// 'Confirmed' and clearing a status fire nothing.
|
|
322
|
+
// ============================================================
|
|
323
|
+
log_header("Confirmed / cleared statuses fire no triggers")
|
|
324
|
+
|
|
325
|
+
const confirmEnduser = await createEnduser()
|
|
326
|
+
const clearEnduser = await createEnduser()
|
|
327
|
+
const noopEvent = await createGroupEvent([confirmEnduser.id, clearEnduser.id])
|
|
328
|
+
|
|
329
|
+
// set clearEnduser to No-showed, then clear it; set confirmEnduser to Confirmed
|
|
330
|
+
await setAttendeeStatuses(noopEvent.id, [{ id: clearEnduser.id, status: 'No-showed' }])
|
|
331
|
+
await wait(undefined, 750)
|
|
332
|
+
await setAttendeeStatuses(noopEvent.id, [{ id: confirmEnduser.id, status: 'Confirmed' }]) // also clears clearEnduser
|
|
333
|
+
await wait(undefined, 750)
|
|
334
|
+
|
|
335
|
+
await async_test(
|
|
336
|
+
"'Confirmed' status fires no trigger",
|
|
337
|
+
() => sdk.api.endusers.getOne(confirmEnduser.id),
|
|
338
|
+
{ onResult: (e: Enduser) => !e.tags?.length }
|
|
339
|
+
)
|
|
340
|
+
// clearEnduser previously got the No-showed tag (that firing is expected); clearing must not
|
|
341
|
+
// add anything new. Assert it still has exactly the one No-showed tag and nothing else.
|
|
342
|
+
await async_test(
|
|
343
|
+
"clearing a status fires no additional trigger",
|
|
344
|
+
() => tagsOf(clearEnduser.id),
|
|
345
|
+
{ onResult: (tags: string[]) => tags.length === 1 && tags.includes(TAG['No-showed']) }
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
} finally {
|
|
349
|
+
for (const id of createdEventIds) {
|
|
350
|
+
try { await sdk.api.calendar_events.deleteOne(id) } catch (_) {}
|
|
351
|
+
}
|
|
352
|
+
for (const id of createdEnduserIds) {
|
|
353
|
+
try { await sdk.api.endusers.deleteOne(id) } catch (_) {}
|
|
354
|
+
}
|
|
355
|
+
for (const id of createdTriggerIds) {
|
|
356
|
+
try { await sdk.api.automation_triggers.deleteOne(id) } catch (_) {}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Allow running this test file independently
|
|
362
|
+
if (require.main === module) {
|
|
363
|
+
console.log(`🌐 Using API URL: ${host}`)
|
|
364
|
+
const sdk = new Session({ host })
|
|
365
|
+
const sdkNonAdmin = new Session({ host })
|
|
366
|
+
|
|
367
|
+
const runTests = async () => {
|
|
368
|
+
await setup_tests(sdk, sdkNonAdmin)
|
|
369
|
+
await group_event_attendee_status_triggers_tests({ sdk, sdkNonAdmin })
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
runTests()
|
|
373
|
+
.then(() => {
|
|
374
|
+
console.log("✅ Group event per-attendee status trigger tests completed successfully")
|
|
375
|
+
process.exit(0)
|
|
376
|
+
})
|
|
377
|
+
.catch((error) => {
|
|
378
|
+
console.error("❌ Group event per-attendee status trigger tests failed:", error)
|
|
379
|
+
process.exit(1)
|
|
380
|
+
})
|
|
381
|
+
}
|
package/src/tests/tests.ts
CHANGED
|
@@ -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"
|
|
@@ -5711,6 +5713,8 @@ const trigger_events_api_tests = async () => {
|
|
|
5711
5713
|
const automation_trigger_tests = async () => {
|
|
5712
5714
|
log_header("Automation Trigger Tests")
|
|
5713
5715
|
|
|
5716
|
+
await appointment_no_showed_trigger_tests({ sdk, sdkNonAdmin })
|
|
5717
|
+
await group_event_attendee_status_triggers_tests({ sdk, sdkNonAdmin })
|
|
5714
5718
|
await assign_care_team_tests()
|
|
5715
5719
|
await push_forms_to_portal_group_completion_tests({ sdk, sdkNonAdmin })
|
|
5716
5720
|
await order_status_equals_tests()
|
|
@@ -11423,6 +11427,7 @@ const lockout_tests = async () => {
|
|
|
11423
11427
|
() => sdk.api.users.updateOne(nonAdminId, { lockedOutUntil: -1 }),
|
|
11424
11428
|
passOnAnyResult
|
|
11425
11429
|
)
|
|
11430
|
+
await wait(undefined, 1500) // ensure new token's iat second clears the stale deauthenticated-<id> cache marker (1s tolerance)
|
|
11426
11431
|
await async_test(
|
|
11427
11432
|
"non-admin can re authenciate when locked to 0",
|
|
11428
11433
|
() => sdkNonAdmin.authenticate(nonAdminEmail, nonAdminPassword),
|
|
@@ -11455,6 +11460,7 @@ const lockout_tests = async () => {
|
|
|
11455
11460
|
() => sdk.api.users.updateOne(nonAdminId, { lockedOutUntil: -1 }),
|
|
11456
11461
|
passOnAnyResult
|
|
11457
11462
|
)
|
|
11463
|
+
await wait(undefined, 1500) // ensure new token's iat second clears the stale deauthenticated-<id> cache marker (1s tolerance)
|
|
11458
11464
|
await async_test(
|
|
11459
11465
|
"non-admin can re authenciate when locked to future date",
|
|
11460
11466
|
() => sdkNonAdmin.authenticate(nonAdminEmail, nonAdminPassword),
|
|
@@ -15028,6 +15034,7 @@ const ip_address_form_tests = async () => {
|
|
|
15028
15034
|
await replace_form_field_template_values_tests()
|
|
15029
15035
|
await mfa_tests()
|
|
15030
15036
|
await setup_tests(sdk, sdkNonAdmin)
|
|
15037
|
+
await automation_trigger_tests()
|
|
15031
15038
|
await resource_access_tags_tests({ sdk, sdkNonAdmin })
|
|
15032
15039
|
await beluga_manual_sync_tests({ sdk, sdkNonAdmin })
|
|
15033
15040
|
await beluga_pharmacy_mappings_tests({ sdk, sdkNonAdmin })
|
|
@@ -15038,7 +15045,6 @@ const ip_address_form_tests = async () => {
|
|
|
15038
15045
|
await load_inbox_redaction_tests({ sdk, sdkNonAdmin })
|
|
15039
15046
|
await webhook_timeout_tests({ sdk, sdkNonAdmin })
|
|
15040
15047
|
await user_portal_settings_tests({ sdk, sdkNonAdmin })
|
|
15041
|
-
await automation_trigger_tests()
|
|
15042
15048
|
await invite_user_enumeration_tests({ sdk, sdkNonAdmin })
|
|
15043
15049
|
await handle_incoming_communication_cross_tenant_tests({ sdk, sdkNonAdmin })
|
|
15044
15050
|
await calendar_event_webhook_template_tests({ sdk, sdkNonAdmin })
|
package/test_generated.pdf
CHANGED
|
Binary file
|