From 36d8554fd58527edb281664d24b6051c30dc259b Mon Sep 17 00:00:00 2001 From: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> Date: Tue, 9 May 2023 19:08:14 +0200 Subject: [PATCH] disable standard confirmation emails if workflow exists (#8747) * add switches to advanced even type settings * only show switches when workflow is enabled * check in event type update handler if user is allowed to disable standard emails * don't send emails if disabled * make sure emails can't be disabled if no workflow exist * send workflow emails to all attendees * code clean up * add translations * always send to all attendees when scheduling reminders * change text --------- Co-authored-by: CarinaWolli --- .../components/eventtype/EventAdvancedTab.tsx | 52 ++++++++++++++++++- apps/web/public/static/locales/en/common.json | 8 ++- packages/emails/email-manager.ts | 51 ++++++++++-------- packages/emails/templates/_base-email.ts | 10 ++-- .../features/bookings/lib/handleNewBooking.ts | 28 +++++++++- .../ee/teams/pages/team-profile-view.tsx | 2 +- .../lib/allowDisablingStandardEmails.ts | 27 ++++++++++ .../lib/reminders/emailReminderManager.ts | 38 ++++++++++---- .../lib/reminders/reminderScheduler.ts | 13 +++-- packages/prisma/zod-utils.ts | 10 ++++ .../viewer/eventTypes/update.handler.ts | 39 ++++++++++++++ .../viewer/workflows/update.handler.ts | 18 +++---- 12 files changed, 241 insertions(+), 55 deletions(-) create mode 100644 packages/features/ee/workflows/lib/allowDisablingStandardEmails.ts diff --git a/apps/web/components/eventtype/EventAdvancedTab.tsx b/apps/web/components/eventtype/EventAdvancedTab.tsx index 0de520f5d5..caae6dc9d8 100644 --- a/apps/web/components/eventtype/EventAdvancedTab.tsx +++ b/apps/web/components/eventtype/EventAdvancedTab.tsx @@ -11,6 +11,10 @@ import { getEventName } from "@calcom/core/event"; import getLocationsOptionsForSelect from "@calcom/features/bookings/lib/getLocationOptionsForSelect"; import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector"; import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager"; +import { + allowDisablingAttendeeConfirmationEmails, + allowDisablingHostConfirmationEmails, +} from "@calcom/features/ee/workflows/lib/allowDisablingStandardEmails"; import { FormBuilder } from "@calcom/features/form-builder/FormBuilder"; import { classNames } from "@calcom/lib"; import { APP_NAME, CAL_URL } from "@calcom/lib/constants"; @@ -43,6 +47,8 @@ export const EventAdvancedTab = ({ eventType, team }: Pick workflowOnEventType.workflow); + eventType.bookingFields.forEach(({ name }) => { bookingFields[name] = name + " input"; }); @@ -338,7 +344,51 @@ export const EventAdvancedTab = ({ eventType, team }: Pick )} /> - + {allowDisablingAttendeeConfirmationEmails(workflows) && ( + <> +
+ ( + <> + { + formMethods.setValue("metadata.disableStandardEmails.confirmation.attendee", e); + onChange(e); + }} + /> + + )} + /> + + )} + {allowDisablingHostConfirmationEmails(workflows) && ( + <> +
+ ( + <> + { + formMethods.setValue("metadata.disableStandardEmails.confirmation.host", e); + onChange(e); + }} + /> + + )} + /> + + )} {showEventNameTip && ( BaseEmail) => { }); }; -export const sendScheduledEmails = async (calEvent: CalendarEvent, eventNameObject?: EventNameObjectType) => { +export const sendScheduledEmails = async ( + calEvent: CalendarEvent, + eventNameObject?: EventNameObjectType, + hostEmailDisabled?: boolean, + attendeeEmailDisabled?: boolean +) => { const emailsToSend: Promise[] = []; - emailsToSend.push(sendEmail(() => new OrganizerScheduledEmail({ calEvent }))); + if (!hostEmailDisabled) { + emailsToSend.push(sendEmail(() => new OrganizerScheduledEmail({ calEvent }))); - if (calEvent.team) { - for (const teamMember of calEvent.team.members) { - emailsToSend.push(sendEmail(() => new OrganizerScheduledEmail({ calEvent, teamMember }))); + if (calEvent.team) { + for (const teamMember of calEvent.team.members) { + emailsToSend.push(sendEmail(() => new OrganizerScheduledEmail({ calEvent, teamMember }))); + } } } - emailsToSend.push( - ...calEvent.attendees.map((attendee) => { - return sendEmail( - () => - new AttendeeScheduledEmail( - { - ...calEvent, - ...(eventNameObject && { - title: getEventName({ ...eventNameObject, t: attendee.language.translate }), - }), - }, - attendee - ) - ); - }) - ); + if (!attendeeEmailDisabled) { + emailsToSend.push( + ...calEvent.attendees.map((attendee) => { + return sendEmail( + () => + new AttendeeScheduledEmail( + { + ...calEvent, + ...(eventNameObject && { + title: getEventName({ ...eventNameObject, t: attendee.language.translate }), + }), + }, + attendee + ) + ); + }) + ); + } await Promise.all(emailsToSend); }; diff --git a/packages/emails/templates/_base-email.ts b/packages/emails/templates/_base-email.ts index 51509bd58d..53dadcbb24 100644 --- a/packages/emails/templates/_base-email.ts +++ b/packages/emails/templates/_base-email.ts @@ -52,9 +52,10 @@ export default class BaseEmail { ...(parseSubject.success && { subject: decodeHTML(parseSubject.data) }), }; - new Promise((resolve, reject) => - createTransport(this.getMailerOptions().transport) - .sendMail(payloadWithUnEscapedSubject, (_err, info) => { + new Promise((resolve, reject) => + createTransport(this.getMailerOptions().transport).sendMail( + payloadWithUnEscapedSubject, + (_err, info) => { if (_err) { const err = getErrorFromUnknown(_err); this.printNodeMailerError(err); @@ -62,7 +63,8 @@ export default class BaseEmail { } else { resolve(info); } - }) + } + ) ).catch((e) => console.error("sendEmail", e)); return new Promise((resolve) => resolve("send mail async")); } diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index 9a69df44c7..2a55ab7c3a 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -33,6 +33,10 @@ import { } from "@calcom/emails"; import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields"; import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses"; +import { + allowDisablingAttendeeConfirmationEmails, + allowDisablingHostConfirmationEmails, +} from "@calcom/features/ee/workflows/lib/allowDisablingStandardEmails"; import { deleteScheduledEmailReminder } from "@calcom/features/ee/workflows/lib/reminders/emailReminderManager"; import { scheduleWorkflowReminders } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler"; import { deleteScheduledSMSReminder } from "@calcom/features/ee/workflows/lib/reminders/smsReminderManager"; @@ -1966,6 +1970,26 @@ async function handler( videoCallUrl = metadata.hangoutLink || defaultLocationUrl || videoCallUrl; } if (noEmail !== true) { + let isHostConfirmationEmailsDisabled = false; + let isAttendeeConfirmationEmailDisabled = false; + + const workflows = eventType.workflows.map((workflow) => workflow.workflow); + + if (eventType.workflows) { + isHostConfirmationEmailsDisabled = + eventType.metadata?.disableStandardEmails?.confirmation?.host || false; + isAttendeeConfirmationEmailDisabled = + eventType.metadata?.disableStandardEmails?.confirmation?.attendee || false; + + if (isHostConfirmationEmailsDisabled) { + isHostConfirmationEmailsDisabled = allowDisablingHostConfirmationEmails(workflows); + } + + if (isAttendeeConfirmationEmailDisabled) { + isAttendeeConfirmationEmailDisabled = allowDisablingAttendeeConfirmationEmails(workflows); + } + } + await sendScheduledEmails( { ...evt, @@ -1973,7 +1997,9 @@ async function handler( additionalNotes, customInputs, }, - eventNameObject + eventNameObject, + isHostConfirmationEmailsDisabled, + isAttendeeConfirmationEmailDisabled ); } } diff --git a/packages/features/ee/teams/pages/team-profile-view.tsx b/packages/features/ee/teams/pages/team-profile-view.tsx index 8c047c7ddd..2314fe1001 100644 --- a/packages/features/ee/teams/pages/team-profile-view.tsx +++ b/packages/features/ee/teams/pages/team-profile-view.tsx @@ -54,7 +54,7 @@ const ProfileView = () => { const utils = trpc.useContext(); const session = useSession(); const [firstRender, setFirstRender] = useState(true); - + useLayoutEffect(() => { document.body.focus(); }, []); diff --git a/packages/features/ee/workflows/lib/allowDisablingStandardEmails.ts b/packages/features/ee/workflows/lib/allowDisablingStandardEmails.ts new file mode 100644 index 0000000000..5f9d8615d3 --- /dev/null +++ b/packages/features/ee/workflows/lib/allowDisablingStandardEmails.ts @@ -0,0 +1,27 @@ +import type { Workflow, WorkflowStep } from "@calcom/prisma/client"; +import { WorkflowTriggerEvents } from "@calcom/prisma/client"; +import { WorkflowActions } from "@calcom/prisma/enums"; + +export function allowDisablingHostConfirmationEmails( + workflows: (Workflow & { + steps: WorkflowStep[]; + })[] +) { + return !!workflows.find( + (workflow) => + workflow.trigger === WorkflowTriggerEvents.NEW_EVENT && + !!workflow.steps.find((step) => step.action === WorkflowActions.EMAIL_HOST) + ); +} + +export function allowDisablingAttendeeConfirmationEmails( + workflows: (Workflow & { + steps: WorkflowStep[]; + })[] +) { + return !!workflows.find( + (workflow) => + workflow.trigger === WorkflowTriggerEvents.NEW_EVENT && + !!workflow.steps.find((step) => step.action === WorkflowActions.EMAIL_ATTENDEE) + ); +} diff --git a/packages/features/ee/workflows/lib/reminders/emailReminderManager.ts b/packages/features/ee/workflows/lib/reminders/emailReminderManager.ts index 74d08f5938..90cff2a1a8 100644 --- a/packages/features/ee/workflows/lib/reminders/emailReminderManager.ts +++ b/packages/features/ee/workflows/lib/reminders/emailReminderManager.ts @@ -134,17 +134,33 @@ export const scheduleEmailReminder = async ( triggerEvent === WorkflowTriggerEvents.RESCHEDULE_EVENT ) { try { - await sgMail.send({ - to: sendTo, - from: { - email: senderEmail, - name: sender, - }, - subject: emailContent.emailSubject, - html: emailContent.emailBody, - batchId: batchIdResponse[1].batch_id, - replyTo: evt.organizer.email, - }); + if (Array.isArray(sendTo)) { + for (const email of sendTo) { + await sgMail.send({ + to: email, + from: { + email: senderEmail, + name: sender, + }, + subject: emailContent.emailSubject, + html: emailContent.emailBody, + batchId: batchIdResponse[1].batch_id, + replyTo: evt.organizer.email, + }); + } + } else { + await sgMail.send({ + to: sendTo, + from: { + email: senderEmail, + name: sender, + }, + subject: emailContent.emailSubject, + html: emailContent.emailBody, + batchId: batchIdResponse[1].batch_id, + replyTo: evt.organizer.email, + }); + } } catch (error) { console.log("Error sending Email"); } diff --git a/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts b/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts index 88c753a68f..09ce10cb90 100644 --- a/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts +++ b/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts @@ -77,16 +77,19 @@ export const scheduleWorkflowReminders = async (args: ScheduleWorkflowRemindersA step.action === WorkflowActions.EMAIL_ATTENDEE || step.action === WorkflowActions.EMAIL_HOST ) { - let sendTo = ""; + let sendTo: string[] = []; switch (step.action) { case WorkflowActions.EMAIL_HOST: - sendTo = evt.organizer?.email || ""; + sendTo = [evt.organizer?.email || ""]; break; case WorkflowActions.EMAIL_ATTENDEE: - sendTo = !!emailAttendeeSendToOverride - ? emailAttendeeSendToOverride - : evt.attendees?.[0]?.email || ""; + const attendees = !!emailAttendeeSendToOverride + ? [emailAttendeeSendToOverride] + : evt.attendees?.map((attendee) => attendee.email); + + sendTo = attendees; + break; } diff --git a/packages/prisma/zod-utils.ts b/packages/prisma/zod-utils.ts index bfd7809952..9d23c14db4 100644 --- a/packages/prisma/zod-utils.ts +++ b/packages/prisma/zod-utils.ts @@ -41,6 +41,16 @@ export const EventTypeMetaDataSchema = z apps: z.object(appDataSchemas).partial().optional(), additionalNotesRequired: z.boolean().optional(), disableSuccessPage: z.boolean().optional(), + disableStandardEmails: z + .object({ + confirmation: z + .object({ + host: z.boolean().optional(), + attendee: z.boolean().optional(), + }) + .optional(), + }) + .optional(), managedEventConfig: z .object({ unlockedFields: z.custom<{ [k in keyof Omit]: true }>().optional(), diff --git a/packages/trpc/server/routers/viewer/eventTypes/update.handler.ts b/packages/trpc/server/routers/viewer/eventTypes/update.handler.ts index 6efa74abf2..99fb7b545b 100644 --- a/packages/trpc/server/routers/viewer/eventTypes/update.handler.ts +++ b/packages/trpc/server/routers/viewer/eventTypes/update.handler.ts @@ -5,6 +5,7 @@ import type { NextApiResponse, GetServerSidePropsContext } from "next"; import { stripeDataSchema } from "@calcom/app-store/stripepayment/lib/server"; import updateChildrenEventTypes from "@calcom/features/ee/managed-event-types/lib/handleChildrenEventTypes"; import { validateIntervalLimitOrder } from "@calcom/lib"; +import { WorkflowActions, WorkflowTriggerEvents } from "@calcom/prisma/client"; import { SchedulingType } from "@calcom/prisma/enums"; import { TRPCError } from "@trpc/server"; @@ -141,6 +142,44 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => { }; } + if (input.metadata?.disableStandardEmails) { + //check if user is allowed to disabled standard emails + + const workflows = await ctx.prisma.workflow.findMany({ + where: { + activeOn: { + some: { + eventTypeId: input.id, + }, + }, + trigger: WorkflowTriggerEvents.NEW_EVENT, + }, + include: { + steps: true, + }, + }); + + if (input.metadata?.disableStandardEmails.confirmation?.host) { + if ( + !workflows.find( + (workflow) => !!workflow.steps.find((step) => step.action === WorkflowActions.EMAIL_HOST) + ) + ) { + input.metadata.disableStandardEmails.confirmation.host = false; + } + } + + if (input.metadata?.disableStandardEmails.confirmation?.attendee) { + if ( + !workflows.find( + (workflow) => !!workflow.steps.find((step) => step.action === WorkflowActions.EMAIL_ATTENDEE) + ) + ) { + input.metadata.disableStandardEmails.confirmation.attendee = false; + } + } + } + if (input?.price || input.metadata?.apps?.stripe?.price) { data.price = input.price || input.metadata?.apps?.stripe?.price; const paymentCredential = await ctx.prisma.credential.findFirst({ diff --git a/packages/trpc/server/routers/viewer/workflows/update.handler.ts b/packages/trpc/server/routers/viewer/workflows/update.handler.ts index 6c9879c58b..69263c9eb5 100644 --- a/packages/trpc/server/routers/viewer/workflows/update.handler.ts +++ b/packages/trpc/server/routers/viewer/workflows/update.handler.ts @@ -237,14 +237,14 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => { step.action === WorkflowActions.EMAIL_ATTENDEE /*|| step.action === WorkflowActions.EMAIL_ADDRESS*/ ) { - let sendTo = ""; + let sendTo: string[] = []; switch (step.action) { case WorkflowActions.EMAIL_HOST: - sendTo = bookingInfo.organizer?.email; + sendTo = [bookingInfo.organizer?.email]; break; case WorkflowActions.EMAIL_ATTENDEE: - sendTo = bookingInfo.attendees[0].email; + sendTo = bookingInfo.attendees.map((attendee) => attendee.email); break; /*case WorkflowActions.EMAIL_ADDRESS: sendTo = step.sendTo || "";*/ @@ -435,14 +435,14 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => { newStep.action === WorkflowActions.EMAIL_ATTENDEE /*|| newStep.action === WorkflowActions.EMAIL_ADDRESS*/ ) { - let sendTo = ""; + let sendTo: string[] = []; switch (newStep.action) { case WorkflowActions.EMAIL_HOST: - sendTo = bookingInfo.organizer?.email; + sendTo = [bookingInfo.organizer?.email]; break; case WorkflowActions.EMAIL_ATTENDEE: - sendTo = bookingInfo.attendees[0].email; + sendTo = bookingInfo.attendees.map((attendee) => attendee.email); break; /*case WorkflowActions.EMAIL_ADDRESS: sendTo = newStep.sendTo || "";*/ @@ -565,14 +565,14 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => { step.action === WorkflowActions.EMAIL_HOST /*|| step.action === WorkflowActions.EMAIL_ADDRESS*/ ) { - let sendTo = ""; + let sendTo: string[] = []; switch (step.action) { case WorkflowActions.EMAIL_HOST: - sendTo = bookingInfo.organizer?.email; + sendTo = [bookingInfo.organizer?.email]; break; case WorkflowActions.EMAIL_ATTENDEE: - sendTo = bookingInfo.attendees[0].email; + sendTo = bookingInfo.attendees.map((attendee) => attendee.email); break; /*case WorkflowActions.EMAIL_ADDRESS: sendTo = step.sendTo || "";*/