Files
calendar/packages/features/bookings/lib/handleNewBooking/test/workflow-notifications.test.ts
T
c4104af8a2 feat: org-wide workflows (#15083)
* add org to create workflow button

* add save button for testing in dev mode

* add select all to multi select dropdown

* fix select all

* pass isOrg to WorkflowDetailsPage

* add checkbox to apply to all including future

* fix count text on select

* WIP schema

* shows teams in dropdown

* add team option to UI

* [WIP] refactor for update handler

* filter out "all" from activeOn

* fix type error

* create more helper functions

* create reusable function for scheduling all booking reminders

* finish update workflows for orgs (without testing)

* fix isAuthorized check for orgs

* fix getting newActiveOn

* move all helper functions to util file

* more code clean up

* fix deleting reminders for org workflows

* fix adding and editing steps

* update form data when workflow is saved

* code clean up

* fixing creating/deleting reminders when user is member of several teams

* fix setting activeOn for teams in useffect

* add on delete cascade

* make multiSelectSchenbox searchable

* set activeOn correctly when checkbox true

* WIP

* fix type errors in MultiSelectCheckboxes

* implemented scheduling org-wide workfow notifications (not yet tested)

* fix type errors

* type error

* add missing changes from merge conflict

* remote not needed include statement

* fix type errors

* code clean up + some todo comments

* support org workflows for cancelled workflows

* delete reminders from removed members

* remove reminders if isActiveOnAll is turned off

* fix unti test and type error

* code clean up

* create basis for testing to book org team event

* create org workflow with active team

* fix getting active org workflows on team + test setup

* fix creating workflow step for tests

* fix first org test

* add test for user event type with org workflow active

* use deleteAllWorkflowReminders everywhere

* add test for deleteRemindersFromRemovedActiveOn

* fix type errors

* make all tests pass

* fix type error

* fix getSchedule test

* code clean up

* add missing import

* fix type error

* fix tests

* code clean up

* fix imports

* update reminders when trigger was changed

* check for teamId before userId in reminderScheduler

* move getOrgIdFromMemberOrTeamId to different folder

* code clean up

* fix tests

* test setup for scheduleBookingReminders

* fix typo

* add tests for scheduleBookingReminders

* fix prisma default import

* fix workflowStep type

* add scheduleBookingReminders test for sms

* return dummy sid for scheduleSMS testMode

* clean up + fixes

* add lost changes from merge

* get teamId and userId from incoming evt object

* removing not needed select

* add org support for scheduleMandatory email reminder

* add other teams to dropdown

* move getAllWorkflows to seperate file and call it in parent function

* include org wide workflows in createNewSeat

* some fixes + code clean up

* add new team to select text count when including future teams is checked

* fix upsert and remove sms reminder field

* correctly update activeOn if 'including future ...' is enabled

* list active Org workflows in event workflows settings

* fix sms reminder field in all handlers

* add helper function to check if step was edited

* fix active on badge on workflow

* fix type error

* fix double reminders

* add teamId: null for userWorkflow query

* fix activeOnAll with managed event types

* add missing teamId in getAllWorkflows

* add a dafaut to prisma param

* fix managed event types on select all user workflows

* code clean up

* better variable name

* small fixes in update handler

* fix test name to match function name

* add info badge

* fix workflow count in event type settings

* fix getting bookings from children manged event types

* delete reminders when user is not part of any time no more

* implement feedback

* fix disbale workflow in event type settings

* fix remove member

* create new function getAllWorkflowsFromEventType

* add some removed code

* use promise.allSettled when deleting workflow reminders

* create new function deleteWorkflowRemindersOfRemovedMember.ts

* fix userId param

* delete org worklfows when team is disbanded

* don't trigger active on all workflow if not part of any team

* fix active on count badge

* add test for deleteWorkflowRemidnersOfRemovedMember

* trigger workflow also if not member of any subteam

* fix failing test

* remove unused code

* use testId for go back button

* fixes for managed event types & activateEventTypeHandler

* code clean up

* don't activate workflow on locked managed event type

* fix type error

* type error

* more type fixes

* feedback

---------

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2024-07-08 17:44:11 +05:30

778 lines
23 KiB
TypeScript

import {
createBookingScenario,
getGoogleCalendarCredential,
TestData,
getOrganizer,
getBooker,
getScenarioData,
mockSuccessfulVideoMeetingCreation,
mockCalendarToHaveNoBusySlots,
BookingLocations,
getDate,
Timezones,
createOrganization,
} from "@calcom/web/test/utils/bookingScenario/bookingScenario";
import { createMockNextJsRequest } from "@calcom/web/test/utils/bookingScenario/createMockNextJsRequest";
import {
expectWorkflowToBeTriggered,
expectSMSWorkflowToBeTriggered,
expectSMSWorkflowToBeNotTriggered,
} from "@calcom/web/test/utils/bookingScenario/expects";
import { getMockRequestDataForBooking } from "@calcom/web/test/utils/bookingScenario/getMockRequestDataForBooking";
import { setupAndTeardown } from "@calcom/web/test/utils/bookingScenario/setupAndTeardown";
import { describe, beforeEach } from "vitest";
import { resetTestSMS } from "@calcom/lib/testSMS";
import { SMSLockState, SchedulingType } from "@calcom/prisma/enums";
import { test } from "@calcom/web/test/fixtures/fixtures";
// Local test runs sometime gets too slow
const timeout = process.env.CI ? 5000 : 20000;
describe("handleNewBooking", () => {
setupAndTeardown();
beforeEach(() => {
resetTestSMS();
});
describe("User Workflows", () => {
test(
"should send workflow email and sms when booking is created",
async ({ emails, sms }) => {
const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default;
const booker = getBooker({
email: "booker@example.com",
name: "Booker",
});
const organizerOtherEmail = "organizer2@example.com";
const organizerDestinationCalendarEmailOnEventType = "organizerEventTypeEmail@example.com";
const organizer = getOrganizer({
name: "Organizer",
email: "organizer@example.com",
id: 101,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
destinationCalendar: {
integration: "google_calendar",
externalId: "organizer@google-calendar.com",
primaryEmail: organizerOtherEmail,
},
});
await createBookingScenario(
getScenarioData({
workflows: [
{
userId: organizer.id,
trigger: "NEW_EVENT",
action: "EMAIL_HOST",
template: "REMINDER",
activeOn: [1],
},
{
userId: organizer.id,
trigger: "NEW_EVENT",
action: "SMS_ATTENDEE",
template: "REMINDER",
activeOn: [1],
},
],
eventTypes: [
{
id: 1,
slotInterval: 30,
length: 30,
useEventTypeDestinationCalendarEmail: true,
users: [
{
id: 101,
},
],
destinationCalendar: {
integration: "google_calendar",
externalId: "event-type-1@google-calendar.com",
primaryEmail: organizerDestinationCalendarEmailOnEventType,
},
},
],
organizer,
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
})
);
mockSuccessfulVideoMeetingCreation({
metadataLookupKey: "dailyvideo",
videoMeetingData: {
id: "MOCK_ID",
password: "MOCK_PASS",
url: `http://mock-dailyvideo.example.com/meeting-1`,
},
});
mockCalendarToHaveNoBusySlots("googlecalendar", {
create: {
id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID",
},
});
const mockBookingData = getMockRequestDataForBooking({
data: {
user: organizer.username,
eventTypeId: 1,
responses: {
email: booker.email,
name: booker.name,
location: { optionValue: "", value: BookingLocations.CalVideo },
smsReminderNumber: "000",
},
},
});
const { req } = createMockNextJsRequest({
method: "POST",
body: mockBookingData,
});
await handleNewBooking(req);
expectSMSWorkflowToBeTriggered({
sms,
toNumber: "000",
});
expectWorkflowToBeTriggered({
emailsToReceive: [organizerDestinationCalendarEmailOnEventType],
emails,
});
},
timeout
);
test(
"should not send workflow sms when booking is created if the organizer is locked for sms sending",
async ({ sms }) => {
const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default;
const booker = getBooker({
email: "booker@example.com",
name: "Booker",
});
const organizerOtherEmail = "organizer2@example.com";
const organizerDestinationCalendarEmailOnEventType = "organizerEventTypeEmail@example.com";
const organizer = getOrganizer({
name: "Organizer",
email: "organizer@example.com",
id: 101,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
destinationCalendar: {
integration: "google_calendar",
externalId: "organizer@google-calendar.com",
primaryEmail: organizerOtherEmail,
},
smsLockState: SMSLockState.LOCKED,
});
await createBookingScenario(
getScenarioData({
workflows: [
{
userId: organizer.id,
trigger: "NEW_EVENT",
action: "SMS_ATTENDEE",
template: "REMINDER",
activeOn: [1],
},
],
eventTypes: [
{
id: 1,
slotInterval: 30,
length: 30,
useEventTypeDestinationCalendarEmail: true,
users: [
{
id: 101,
},
],
destinationCalendar: {
integration: "google_calendar",
externalId: "event-type-1@google-calendar.com",
primaryEmail: organizerDestinationCalendarEmailOnEventType,
},
},
],
organizer,
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
})
);
mockCalendarToHaveNoBusySlots("googlecalendar", {
create: {
id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID",
},
});
const mockBookingData = getMockRequestDataForBooking({
data: {
user: organizer.username,
eventTypeId: 1,
responses: {
email: booker.email,
name: booker.name,
location: { optionValue: "", value: BookingLocations.CalVideo },
smsReminderNumber: "000",
},
},
});
const { req } = createMockNextJsRequest({
method: "POST",
body: mockBookingData,
});
await handleNewBooking(req);
expectSMSWorkflowToBeNotTriggered({
sms,
toNumber: "000",
});
},
timeout
);
});
describe("Team Workflows", () => {
test(
"should send workflow email and sms when booking is created",
async ({ emails, sms }) => {
const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default;
const booker = getBooker({
email: "booker@example.com",
name: "Booker",
});
const organizerDestinationCalendarEmailOnEventType = "organizerEventTypeEmail@example.com";
const organizer = getOrganizer({
name: "Organizer",
email: "organizer@example.com",
id: 101,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
destinationCalendar: {
integration: "google_calendar",
externalId: "organizer@google-calendar.com",
primaryEmail: organizerDestinationCalendarEmailOnEventType,
},
teams: [
{
membership: {
accepted: true,
},
team: {
id: 1,
name: "Team 1",
slug: "team-1",
},
},
],
});
const otherTeamMembers = [
{
name: "Other Team Member 1",
username: "other-team-member-1",
defaultScheduleId: null,
email: "other-team-member-1@example.com",
timeZone: Timezones["+0:00"],
id: 102,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
destinationCalendar: {
integration: TestData.apps["google-calendar"].type,
externalId: "other-team-member-1@google-calendar.com",
},
},
];
await createBookingScenario(
getScenarioData({
workflows: [
{
teamId: 1,
trigger: "NEW_EVENT",
action: "EMAIL_HOST",
template: "REMINDER",
activeOn: [1],
},
{
teamId: 1,
trigger: "NEW_EVENT",
action: "SMS_ATTENDEE",
template: "REMINDER",
activeOn: [1],
},
],
eventTypes: [
{
id: 1,
slotInterval: 15,
schedulingType: SchedulingType.COLLECTIVE,
length: 15,
users: [
{
id: 101,
},
{
id: 102,
},
],
teamId: 1,
destinationCalendar: {
integration: TestData.apps["google-calendar"].type,
externalId: "event-type-1@google-calendar.com",
},
},
],
organizer,
usersApartFromOrganizer: otherTeamMembers,
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
})
);
mockSuccessfulVideoMeetingCreation({
metadataLookupKey: "dailyvideo",
videoMeetingData: {
id: "MOCK_ID",
password: "MOCK_PASS",
url: `http://mock-dailyvideo.example.com/meeting-1`,
},
});
mockCalendarToHaveNoBusySlots("googlecalendar", {
create: {
id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID",
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
},
});
const mockBookingData = getMockRequestDataForBooking({
data: {
start: `${getDate({ dateIncrement: 1 }).dateString}T09:00:00.000Z`,
end: `${getDate({ dateIncrement: 1 }).dateString}T09:15:00.000Z`,
user: organizer.username,
eventTypeId: 1,
responses: {
email: booker.email,
name: booker.name,
location: { optionValue: "", value: BookingLocations.CalVideo },
smsReminderNumber: "000",
},
},
});
const { req } = createMockNextJsRequest({
method: "POST",
body: mockBookingData,
});
await handleNewBooking(req);
expectSMSWorkflowToBeTriggered({
sms,
toNumber: "000",
});
expectWorkflowToBeTriggered({
// emailsToReceive: [organizer.email].concat(otherTeamMembers.map(member => member.email)),
emailsToReceive: [organizer.email],
emails,
});
},
timeout
);
test(
"should not send workflow sms when booking is created if the team is locked for sms sending",
async ({ emails, sms }) => {
const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default;
const booker = getBooker({
email: "booker@example.com",
name: "Booker",
});
const organizerDestinationCalendarEmailOnEventType = "organizerEventTypeEmail@example.com";
const organizer = getOrganizer({
name: "Organizer",
email: "organizer@example.com",
id: 101,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
destinationCalendar: {
integration: "google_calendar",
externalId: "organizer@google-calendar.com",
primaryEmail: organizerDestinationCalendarEmailOnEventType,
},
teams: [
{
membership: {
accepted: true,
},
team: {
id: 1,
name: "Team 1",
slug: "team-1",
smsLockState: SMSLockState.LOCKED,
},
},
],
});
const otherTeamMembers = [
{
name: "Other Team Member 1",
username: "other-team-member-1",
defaultScheduleId: null,
email: "other-team-member-1@example.com",
timeZone: Timezones["+0:00"],
id: 102,
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
destinationCalendar: {
integration: TestData.apps["google-calendar"].type,
externalId: "other-team-member-1@google-calendar.com",
},
},
];
await createBookingScenario(
getScenarioData({
workflows: [
{
teamId: 1,
trigger: "NEW_EVENT",
action: "SMS_ATTENDEE",
template: "REMINDER",
activeOn: [1],
},
],
eventTypes: [
{
id: 1,
slotInterval: 15,
schedulingType: SchedulingType.COLLECTIVE,
length: 15,
users: [
{
id: 101,
},
{
id: 102,
},
],
teamId: 1,
destinationCalendar: {
integration: TestData.apps["google-calendar"].type,
externalId: "event-type-1@google-calendar.com",
},
},
],
organizer,
usersApartFromOrganizer: otherTeamMembers,
apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]],
})
);
mockSuccessfulVideoMeetingCreation({
metadataLookupKey: "dailyvideo",
videoMeetingData: {
id: "MOCK_ID",
password: "MOCK_PASS",
url: `http://mock-dailyvideo.example.com/meeting-1`,
},
});
mockCalendarToHaveNoBusySlots("googlecalendar", {
create: {
id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID",
iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID",
},
});
const mockBookingData = getMockRequestDataForBooking({
data: {
start: `${getDate({ dateIncrement: 1 }).dateString}T09:00:00.000Z`,
end: `${getDate({ dateIncrement: 1 }).dateString}T09:15:00.000Z`,
user: organizer.username,
eventTypeId: 1,
responses: {
email: booker.email,
name: booker.name,
location: { optionValue: "", value: BookingLocations.CalVideo },
smsReminderNumber: "000",
},
},
});
const { req } = createMockNextJsRequest({
method: "POST",
body: mockBookingData,
});
await handleNewBooking(req);
expectSMSWorkflowToBeNotTriggered({
sms,
toNumber: "000",
});
},
timeout
);
});
describe("Org Workflows", () => {
test("should trigger workflow when a new team event is booked and this team is active on org workflow", async ({
emails,
}) => {
const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default;
const org = await createOrganization({
name: "Test Org",
slug: "testorg",
});
const booker = getBooker({
email: "booker@example.com",
name: "Booker",
});
const organizer = getOrganizer({
name: "Organizer",
email: "organizer@example.com",
id: 101,
defaultScheduleId: null,
organizationId: org.id,
teams: [
{
membership: {
accepted: true,
},
team: {
id: 2,
name: "Team 1",
slug: "team-1",
parentId: org.id,
},
},
{
membership: {
accepted: true,
},
team: {
id: 1,
name: "Test Org",
slug: "testorg",
},
},
],
schedules: [TestData.schedules.IstMorningShift],
});
await createBookingScenario(
getScenarioData(
{
workflows: [
{
teamId: 1,
trigger: "NEW_EVENT",
action: "EMAIL_ATTENDEE",
template: "REMINDER",
activeOnTeams: [2],
},
],
eventTypes: [
{
id: 1,
slotInterval: 15,
schedulingType: SchedulingType.COLLECTIVE,
length: 15,
users: [
{
id: 101,
},
],
teamId: 2,
},
],
organizer: {
...organizer,
username: "organizer",
},
apps: [TestData.apps["daily-video"]],
},
{ id: org.id }
)
);
mockSuccessfulVideoMeetingCreation({
metadataLookupKey: "dailyvideo",
videoMeetingData: {
id: "MOCK_ID",
password: "MOCK_PASS",
url: `http://mock-dailyvideo.example.com/meeting-1`,
},
});
const mockBookingData = getMockRequestDataForBooking({
data: {
// Try booking the first available free timeslot in both the users' schedules
start: `${getDate({ dateIncrement: 1 }).dateString}T11:30:00.000Z`,
end: `${getDate({ dateIncrement: 1 }).dateString}T11:45:00.000Z`,
eventTypeId: 1,
responses: {
email: booker.email,
name: booker.name,
location: { optionValue: "", value: BookingLocations.CalVideo },
},
},
});
const { req } = createMockNextJsRequest({
method: "POST",
body: mockBookingData,
});
await handleNewBooking(req);
expectWorkflowToBeTriggered({
emailsToReceive: ["booker@example.com"],
emails,
});
});
test("should trigger workflow when a new user event is booked and the user is part of an org team that is active on a org workflow", async ({
sms,
}) => {
const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default;
const org = await createOrganization({
name: "Test Org",
slug: "testorg",
});
const booker = getBooker({
email: "booker@example.com",
name: "Booker",
});
const organizer = getOrganizer({
name: "Organizer",
email: "organizer@example.com",
id: 101,
defaultScheduleId: null,
organizationId: org.id,
teams: [
{
membership: {
accepted: true,
},
team: {
id: 2,
name: "Team 1",
slug: "team-1",
parentId: org.id,
},
},
{
membership: {
accepted: true,
},
team: {
id: 1,
name: "Test Org",
slug: "testorg",
},
},
],
schedules: [TestData.schedules.IstMorningShift],
});
await createBookingScenario(
getScenarioData(
{
workflows: [
{
teamId: 1,
trigger: "NEW_EVENT",
action: "SMS_ATTENDEE",
template: "REMINDER",
activeOnTeams: [2],
},
],
eventTypes: [
{
id: 1,
slotInterval: 15,
length: 15,
users: [
{
id: 101,
},
],
},
],
organizer: {
...organizer,
username: "organizer",
},
apps: [TestData.apps["daily-video"]],
},
{ id: org.id }
)
);
mockSuccessfulVideoMeetingCreation({
metadataLookupKey: "dailyvideo",
videoMeetingData: {
id: "MOCK_ID",
password: "MOCK_PASS",
url: `http://mock-dailyvideo.example.com/meeting-1`,
},
});
const mockBookingData = getMockRequestDataForBooking({
data: {
// Try booking the first available free timeslot in both the users' schedules
start: `${getDate({ dateIncrement: 1 }).dateString}T11:30:00.000Z`,
end: `${getDate({ dateIncrement: 1 }).dateString}T11:45:00.000Z`,
eventTypeId: 1,
responses: {
email: booker.email,
name: booker.name,
location: { optionValue: "", value: BookingLocations.CalVideo },
smsReminderNumber: "000",
},
},
});
const { req } = createMockNextJsRequest({
method: "POST",
body: mockBookingData,
});
await handleNewBooking(req);
expectSMSWorkflowToBeTriggered({
sms,
toNumber: "000",
});
});
});
});