diff --git a/apps/web/components/eventtype/EventAdvancedTab.tsx b/apps/web/components/eventtype/EventAdvancedTab.tsx index 39245a06f4..04c4095d03 100644 --- a/apps/web/components/eventtype/EventAdvancedTab.tsx +++ b/apps/web/components/eventtype/EventAdvancedTab.tsx @@ -41,6 +41,7 @@ import { } from "@calcom/ui"; import RequiresConfirmationController from "./RequiresConfirmationController"; +import { DisableAllEmailsSetting } from "./settings/DisableAllEmailsSetting"; const CustomEventTypeModal = dynamic(() => import("@components/eventtype/CustomEventTypeModal")); @@ -57,7 +58,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick workflowOnEventType.workflow); const selectedThemeIsDark = user?.theme === "dark" || @@ -590,6 +590,34 @@ export const EventAdvancedTab = ({ eventType, team }: Pick )} + {team?.parentId && ( + <> + { + return ( + <> + + + ); + }} + /> + ( + <> + + + )} + /> + + )} {showEventNameTip && ( void; + recipient: "attendees" | "hosts"; + t: TFunction; +} + +export const DisableAllEmailsSetting = ({ + checked, + onCheckedChange, + recipient, + t, +}: DisableEmailsSettingProps) => { + const [dialogOpen, setDialogOpen] = useState(false); + const [confirmText, setConfirmText] = useState(""); + + const title = + recipient === "attendees" ? t("disable_all_emails_to_attendees") : t("disable_all_emails_to_hosts"); + + return ( +
+ setDialogOpen(e)}> + +

+ + This will disable all emails to {{ recipient }}. This includes booking confirmations, requests, + reschedules and reschedule requests, cancellation emails, and any other emails related to + booking updates. +
+
+ It is your responsibility to ensure that your {{ recipient }} are aware of any bookings and + changes to their bookings. +
+

+

{t("type_confirm_to_continue")}

+ { + setConfirmText(e.target.value); + }} + /> + + + + +
+
+ { + checked ? onCheckedChange(!checked) : setDialogOpen(true); + }} + /> +
+ ); +}; diff --git a/apps/web/pages/api/cron/bookingReminder.ts b/apps/web/pages/api/cron/bookingReminder.ts index 9547bb1ece..149f372c88 100644 --- a/apps/web/pages/api/cron/bookingReminder.ts +++ b/apps/web/pages/api/cron/bookingReminder.ts @@ -7,6 +7,7 @@ import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib"; import { getTranslation } from "@calcom/lib/server/i18n"; import prisma, { bookingMinimalSelect } from "@calcom/prisma"; import { BookingStatus, ReminderType } from "@calcom/prisma/enums"; +import type { EventTypeMetadata } from "@calcom/prisma/zod-utils"; import type { CalendarEvent } from "@calcom/types/Calendar"; export default async function handler(req: NextApiRequest, res: NextApiResponse) { @@ -61,6 +62,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) select: { recurringEvent: true, bookingFields: true, + metadata: true, }, }, responses: true, @@ -130,7 +132,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) destinationCalendar: selectedDestinationCalendar ? [selectedDestinationCalendar] : [], }; - await sendOrganizerRequestReminderEmail(evt); + await sendOrganizerRequestReminderEmail(evt, booking?.eventType?.metadata as EventTypeMetadata); await prisma.reminderMail.create({ data: { diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 7128212cd9..6c3a579e35 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -22,7 +22,7 @@ "verify_email_banner_body": "Verify your email address to guarantee the best email and calendar deliverability", "verify_email_email_header": "Verify your email address", "verify_email_email_button": "Verify email", - "cal_ai_assistant":"Cal AI Assistant", + "cal_ai_assistant": "Cal AI Assistant", "verify_email_change_description": "You have recently requested to change the email address you use to log into your {{appName}} account. Please click the button below to confirm your new email address.", "verify_email_change_success_toast": "Updated your email to {{email}}", "verify_email_change_failure_toast": "Failed to update email.", @@ -87,7 +87,7 @@ "missing_card_fields": "Missing card fields", "pay_now": "Pay now", "general_prompt": "General Prompt", - "begin_message":"Begin Message", + "begin_message": "Begin Message", "codebase_has_to_stay_opensource": "The codebase has to stay open source, whether it was modified or not", "cannot_repackage_codebase": "You can not repackage or sell the codebase", "acquire_license": "Acquire a commercial license to remove these terms by emailing", @@ -118,9 +118,9 @@ "event_still_awaiting_approval": "An event is still waiting for your approval", "booking_submitted_subject": "Booking Submitted: {{title}} at {{date}}", "download_recording_subject": "Download Recording: {{title}} at {{date}}", - "download_transcript_email_subject":"Download Transcript: {{title}} at {{date}}", + "download_transcript_email_subject": "Download Transcript: {{title}} at {{date}}", "download_your_recording": "Download your recording", - "download_your_transcripts":"Download your Transcripts", + "download_your_transcripts": "Download your Transcripts", "your_meeting_has_been_booked": "Your meeting has been booked", "event_type_has_been_rescheduled_on_time_date": "Your {{title}} has been rescheduled to {{date}}.", "event_has_been_rescheduled": "Updated - Your event has been rescheduled", @@ -171,7 +171,7 @@ "seat_options_doesnt_support_confirmation": "Seats option doesn't support confirmation requirement", "multilocation_doesnt_support_seats": "Multiple Locations doesn't support seats option", "no_show_fee_doesnt_support_seats": "No show fee doesn't support seats option", - "seats_option_doesnt_support_multi_location" : "Seats option doesn't support Multiple Locations", + "seats_option_doesnt_support_multi_location": "Seats option doesn't support Multiple Locations", "team_upgrade_seats_details": "Of the {{memberCount}} members in your team, {{unpaidCount}} seat(s) are unpaid. At ${{seatPrice}}/month per seat the estimated total cost of your membership is ${{totalCost}}/month.", "team_upgrade_banner_description": "You haven't finished your team setup. Your team \"{{teamName}}\" needs to be upgraded.", "upgrade_banner_action": "Upgrade here", @@ -222,7 +222,7 @@ "2fa_confirm_current_password": "Confirm your current password to get started.", "2fa_scan_image_or_use_code": "Scan the image below with the authenticator app on your phone or manually enter the text code instead.", "text": "Text", - "your_phone_number":"Your Phone Number", + "your_phone_number": "Your Phone Number", "multiline_text": "Multiline Text", "number": "Number", "checkbox": "Checkbox", @@ -1309,7 +1309,7 @@ "upgrade": "Upgrade", "upgrade_to_access_recordings_title": "Upgrade to access recordings", "upgrade_to_access_recordings_description": "Recordings are only available as part of our teams plan. Upgrade to start recording your calls", - "upgrade_to_cal_ai_phone_number_description":"Upgrade to Enterprise to generate an AI Agent phone number that can call guests to schedule calls", + "upgrade_to_cal_ai_phone_number_description": "Upgrade to Enterprise to generate an AI Agent phone number that can call guests to schedule calls", "recordings_are_part_of_the_teams_plan": "Recordings are part of the teams plan", "team_feature_teams": "This is a Team feature. Upgrade to Team to see your team's availability.", "team_feature_workflows": "This is a Team feature. Upgrade to Team to automate your event notifications and reminders with Workflows.", @@ -1465,7 +1465,7 @@ "download_transcript": "Download Transcript", "recording_from_your_recent_call": "A recording from your recent call on {{appName}} is ready for download", "transcript_from_previous_call": "Transcript from your recent call on {{appName}} is ready to download. Links are valid only for 1 Hour", - "link_valid_for_12_hrs":"Note: The download link is valid only for 12 hours. You can generate new download link by following instructions <1>here.", + "link_valid_for_12_hrs": "Note: The download link is valid only for 12 hours. You can generate new download link by following instructions <1>here.", "create_your_first_form": "Create your first form", "create_your_first_form_description": "With Routing Forms you can ask qualifying questions and route to the correct person or event type.", "create_your_first_webhook": "Create your first Webhook", @@ -2310,7 +2310,7 @@ "dont_want_to_wait": "Don't want to wait?", "meeting_started": "Meeting Started", "pay_and_book": "Pay to book", - "cal_ai_event_tab_description":"Let AI Agents book you", + "cal_ai_event_tab_description": "Let AI Agents book you", "booking_not_found_error": "Could not find booking", "booking_seats_full_error": "Booking seats are full", "missing_payment_credential_error": "Missing payment credentials", @@ -2370,7 +2370,7 @@ "account_unlinked_success": "Account unlinked successfully", "account_unlinked_error": "There was an error unlinking the account", "travel_schedule": "Travel Schedule", - "travel_schedule_description": "Plan your trip ahead to keep your existing schedule in a different timezone and prevent being booked at midnight.", + "travel_schedule_description": "Plan your trip ahead to keep your existing schedule in a different timezone and prevent being booked at midnight.", "schedule_timezone_change": "Schedule timezone change", "date": "Date", "overlaps_with_existing_schedule": "This overlaps with an existing schedule. Please select a different date.", @@ -2399,11 +2399,11 @@ "email_team_invite|content|added_to_subteam": "{{invitedBy}} has added you to the team {{teamName}} in their organization {{parentTeamName}}. {{appName}} is the event-juggling scheduler that enables you and your team to schedule meetings without the email tennis.", "email_team_invite|content|invited_to_subteam": "{{invitedBy}} has invited you to join the team {{teamName}} in their organization {{parentTeamName}}. {{appName}} is the event-juggling scheduler that enables you and your team to schedule meetings without the email tennis.", "email_team_invite|content|invited_to_regular_team": "{{invitedBy}} has invited you to join their team {{teamName}} on {{appName}}. {{appName}} is the event-juggling scheduler that enables you and your team to schedule meetings without the email tennis.", - "email|existing_user_added_link_will_change":"On accepting the invite, your link will change to your organization domain but don't worry, all previous links will still work and redirect appropriately.

Please note: All of your personal event types will be moved into the {teamName} organization, which may also include potential personal link.

For personal events we recommend creating a new account with a personal email address.", - "email|existing_user_added_link_changed":"Your link has been changed from {prevLinkWithoutProtocol} to {newLinkWithoutProtocol} but don't worry, all previous links still work and redirect appropriately.

Please note: All of your personal event types have been moved into the {teamName} organisation, which may also include potential personal link.

Please log in and make sure you have no private events on your new organisational account.

For personal events we recommend creating a new account with a personal email address.

Enjoy your new clean link: {newLinkWithoutProtocol}", + "email|existing_user_added_link_will_change": "On accepting the invite, your link will change to your organization domain but don't worry, all previous links will still work and redirect appropriately.

Please note: All of your personal event types will be moved into the {teamName} organization, which may also include potential personal link.

For personal events we recommend creating a new account with a personal email address.", + "email|existing_user_added_link_changed": "Your link has been changed from {prevLinkWithoutProtocol} to {newLinkWithoutProtocol} but don't worry, all previous links still work and redirect appropriately.

Please note: All of your personal event types have been moved into the {teamName} organisation, which may also include potential personal link.

Please log in and make sure you have no private events on your new organisational account.

For personal events we recommend creating a new account with a personal email address.

Enjoy your new clean link: {newLinkWithoutProtocol}", "email_organization_created|subject": "Your organization has been created", - "your_current_plan":"Your current plan", - "organization_price_per_user_month":"$37 per user per month (30 seats minimum)", + "your_current_plan": "Your current plan", + "organization_price_per_user_month": "$37 per user per month (30 seats minimum)", "privacy_organization_description": "Manage privacy settings for your organization", "privacy": "Privacy", "team_will_be_under_org": "New teams will be under your organization", @@ -2472,10 +2472,10 @@ "review": "Review", "reviewed": "Reviewed", "unreviewed": "Unreviewed", - "rating_url_info":"The URL for Rating Feedback Form", - "no_show_url_info":"The URL for No Show Feedback", - "no_support_needed":"No Support Needed?", - "hide_support":"Hide Support", + "rating_url_info": "The URL for Rating Feedback Form", + "no_show_url_info": "The URL for No Show Feedback", + "no_support_needed": "No Support Needed?", + "hide_support": "Hide Support", "event_ratings": "Average Ratings", "event_no_show": "Host No Show", "recent_ratings": "Recent ratings", @@ -2517,6 +2517,11 @@ "event_expired": "This event is expired", "skip_contact_creation": "Skip creating contacts if they do not exist in {{appName}} ", "skip_writing_to_calendar_note": "If your ICS link is read-only (e.g., Proton Calendar), check the box above to avoid errors. You'll also need to manually update your calendar for changes.", + "disable_all_emails_to_attendees": "Disable standard emails to attendees related to this event type", + "disable_all_emails_description": "Disables standard email communication related to this event type, including booking confirmations, reminders, and cancellations.", + "disable_all_emails_to_hosts": "Disable standard emails to hosts related to this event type", + "type_confirm_to_continue": "Type confirm to continue", + "disable_email": "Disable email", "grant_admin_api": "Grant Admin API Access", "revoke_admin_api": "Revoke Admin API Access", "apple_connect_atom_label": "Connect Apple Calendar", diff --git a/packages/app-store/stripepayment/lib/PaymentService.ts b/packages/app-store/stripepayment/lib/PaymentService.ts index 5218ac8180..89c78ddb85 100644 --- a/packages/app-store/stripepayment/lib/PaymentService.ts +++ b/packages/app-store/stripepayment/lib/PaymentService.ts @@ -9,6 +9,7 @@ import { getErrorFromUnknown } from "@calcom/lib/errors"; import logger from "@calcom/lib/logger"; import { safeStringify } from "@calcom/lib/safeStringify"; import prisma from "@calcom/prisma"; +import type { EventTypeMetadata } from "@calcom/prisma/zod-utils"; import type { CalendarEvent } from "@calcom/types/Calendar"; import type { IAbstractPaymentService } from "@calcom/types/PaymentService"; @@ -336,22 +337,26 @@ export class PaymentService implements IAbstractPaymentService { startTime: { toISOString: () => string }; uid: string; }, - paymentData: Payment + paymentData: Payment, + eventTypeMetadata?: EventTypeMetadata ): Promise { - await sendAwaitingPaymentEmail({ - ...event, - paymentInfo: { - link: createPaymentLink({ - paymentUid: paymentData.uid, - name: booking.user?.name, - email: booking.user?.email, - date: booking.startTime.toISOString(), - }), - paymentOption: paymentData.paymentOption || "ON_BOOKING", - amount: paymentData.amount, - currency: paymentData.currency, + await sendAwaitingPaymentEmail( + { + ...event, + paymentInfo: { + link: createPaymentLink({ + paymentUid: paymentData.uid, + name: booking.user?.name, + email: booking.user?.email, + date: booking.startTime.toISOString(), + }), + paymentOption: paymentData.paymentOption || "ON_BOOKING", + amount: paymentData.amount, + currency: paymentData.currency, + }, }, - }); + eventTypeMetadata + ); } async deletePayment(paymentId: Payment["id"]): Promise { diff --git a/packages/app-store/vital/lib/reschedule.ts b/packages/app-store/vital/lib/reschedule.ts index c800597dcb..4d34043d05 100644 --- a/packages/app-store/vital/lib/reschedule.ts +++ b/packages/app-store/vital/lib/reschedule.ts @@ -10,6 +10,7 @@ import logger from "@calcom/lib/logger"; import { getTranslation } from "@calcom/lib/server/i18n"; import prisma from "@calcom/prisma"; import { BookingStatus } from "@calcom/prisma/enums"; +import type { EventTypeMetadata } from "@calcom/prisma/zod-utils"; import type { Person } from "@calcom/types/Calendar"; import { getCalendar } from "../../_utils/getCalendar"; @@ -41,6 +42,11 @@ const Reschedule = async (bookingUid: string, cancellationReason: string) => { destinationCalendar: true, }, }, + eventType: { + select: { + metadata: true, + }, + }, }, where: { uid: bookingUid, @@ -141,9 +147,13 @@ const Reschedule = async (bookingUid: string, cancellationReason: string) => { // Send emails try { - await sendRequestRescheduleEmail(builder.calendarEvent, { - rescheduleLink: builder.rescheduleLink, - }); + await sendRequestRescheduleEmail( + builder.calendarEvent, + { + rescheduleLink: builder.rescheduleLink, + }, + bookingToReschedule?.eventType?.metadata as EventTypeMetadata + ); } catch (error) { if (error instanceof Error) { logger.error(error.message); diff --git a/packages/app-store/wipemycalother/lib/reschedule.ts b/packages/app-store/wipemycalother/lib/reschedule.ts index ff8d2afe28..91f3da7140 100644 --- a/packages/app-store/wipemycalother/lib/reschedule.ts +++ b/packages/app-store/wipemycalother/lib/reschedule.ts @@ -10,6 +10,7 @@ import logger from "@calcom/lib/logger"; import { getTranslation } from "@calcom/lib/server/i18n"; import prisma from "@calcom/prisma"; import { BookingStatus } from "@calcom/prisma/enums"; +import type { EventTypeMetadata } from "@calcom/prisma/zod-utils"; import type { Person } from "@calcom/types/Calendar"; import { getCalendar } from "../../_utils/getCalendar"; @@ -41,6 +42,11 @@ const Reschedule = async (bookingUid: string, cancellationReason: string) => { destinationCalendar: true, }, }, + eventType: { + select: { + metadata: true, + }, + }, }, where: { uid: bookingUid, @@ -141,9 +147,13 @@ const Reschedule = async (bookingUid: string, cancellationReason: string) => { // Send emails try { - await sendRequestRescheduleEmail(builder.calendarEvent, { - rescheduleLink: builder.rescheduleLink, - }); + await sendRequestRescheduleEmail( + builder.calendarEvent, + { + rescheduleLink: builder.rescheduleLink, + }, + bookingToReschedule?.eventType?.metadata as EventTypeMetadata + ); } catch (error) { if (error instanceof Error) { logger.error(error.message); diff --git a/packages/emails/email-manager.ts b/packages/emails/email-manager.ts index 484464efb4..525954819c 100644 --- a/packages/emails/email-manager.ts +++ b/packages/emails/email-manager.ts @@ -1,6 +1,7 @@ // eslint-disable-next-line no-restricted-imports import { cloneDeep } from "lodash"; import type { TFunction } from "next-i18next"; +import type { z } from "zod"; import type { EventNameObjectType } from "@calcom/core/event"; import { getEventName } from "@calcom/core/event"; @@ -8,6 +9,7 @@ import type BaseEmail from "@calcom/emails/templates/_base-email"; import { formatCalEvent } from "@calcom/lib/formatCalendarEvent"; import logger from "@calcom/lib/logger"; import { safeStringify } from "@calcom/lib/safeStringify"; +import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; import type { CalendarEvent, Person } from "@calcom/types/Calendar"; import type { MonthlyDigestEmailData } from "./src/templates/MonthlyDigestEmail"; @@ -61,6 +63,8 @@ import SlugReplacementEmail from "./templates/slug-replacement-email"; import type { TeamInvite } from "./templates/team-invite-email"; import TeamInviteEmail from "./templates/team-invite-email"; +type EventTypeMetadata = z.infer; + const sendEmail = (prepare: () => BaseEmail) => { return new Promise((resolve, reject) => { try { @@ -72,16 +76,25 @@ const sendEmail = (prepare: () => BaseEmail) => { }); }; +const eventTypeDisableAttendeeEmail = (metadata?: EventTypeMetadata) => { + return !!metadata?.disableStandardEmails?.all?.attendee; +}; + +const eventTypeDisableHostEmail = (metadata?: EventTypeMetadata) => { + return !!metadata?.disableStandardEmails?.all?.host; +}; + export const sendScheduledEmails = async ( calEvent: CalendarEvent, eventNameObject?: EventNameObjectType, hostEmailDisabled?: boolean, - attendeeEmailDisabled?: boolean + attendeeEmailDisabled?: boolean, + eventTypeMetadata?: EventTypeMetadata ) => { const formattedCalEvent = formatCalEvent(calEvent); const emailsToSend: Promise[] = []; - if (!hostEmailDisabled) { + if (!hostEmailDisabled && !eventTypeDisableHostEmail(eventTypeMetadata)) { emailsToSend.push(sendEmail(() => new OrganizerScheduledEmail({ calEvent: formattedCalEvent }))); if (formattedCalEvent.team) { @@ -93,7 +106,7 @@ export const sendScheduledEmails = async ( } } - if (!attendeeEmailDisabled) { + if (!attendeeEmailDisabled && !eventTypeDisableAttendeeEmail(eventTypeMetadata)) { emailsToSend.push( ...formattedCalEvent.attendees.map((attendee) => { return sendEmail( @@ -117,7 +130,12 @@ export const sendScheduledEmails = async ( }; // for rescheduled round robin booking that assigned new members -export const sendRoundRobinScheduledEmails = async (calEvent: CalendarEvent, members: Person[]) => { +export const sendRoundRobinScheduledEmails = async ( + calEvent: CalendarEvent, + members: Person[], + eventTypeMetadata?: EventTypeMetadata +) => { + if (eventTypeDisableHostEmail(eventTypeMetadata)) return; const formattedCalEvent = formatCalEvent(calEvent); const emailsToSend: Promise[] = []; @@ -130,7 +148,12 @@ export const sendRoundRobinScheduledEmails = async (calEvent: CalendarEvent, mem await Promise.all(emailsToSend); }; -export const sendRoundRobinRescheduledEmails = async (calEvent: CalendarEvent, members: Person[]) => { +export const sendRoundRobinRescheduledEmails = async ( + calEvent: CalendarEvent, + members: Person[], + eventTypeMetadata?: EventTypeMetadata +) => { + if (eventTypeDisableHostEmail(eventTypeMetadata)) return; const calendarEvent = formatCalEvent(calEvent); const emailsToSend: Promise[] = []; @@ -143,7 +166,12 @@ export const sendRoundRobinRescheduledEmails = async (calEvent: CalendarEvent, m await Promise.all(emailsToSend); }; -export const sendRoundRobinCancelledEmails = async (calEvent: CalendarEvent, members: Person[]) => { +export const sendRoundRobinCancelledEmails = async ( + calEvent: CalendarEvent, + members: Person[], + eventTypeMetadata?: EventTypeMetadata +) => { + if (eventTypeDisableHostEmail(eventTypeMetadata)) return; const calendarEvent = formatCalEvent(calEvent); const emailsToSend: Promise[] = []; @@ -154,37 +182,50 @@ export const sendRoundRobinCancelledEmails = async (calEvent: CalendarEvent, mem await Promise.all(emailsToSend); }; -export const sendRescheduledEmails = async (calEvent: CalendarEvent) => { +export const sendRescheduledEmails = async ( + calEvent: CalendarEvent, + eventTypeMetadata?: EventTypeMetadata +) => { const calendarEvent = formatCalEvent(calEvent); const emailsToSend: Promise[] = []; - emailsToSend.push(sendEmail(() => new OrganizerRescheduledEmail({ calEvent: calendarEvent }))); + if (!eventTypeDisableHostEmail(eventTypeMetadata)) { + emailsToSend.push(sendEmail(() => new OrganizerRescheduledEmail({ calEvent: calendarEvent }))); - if (calendarEvent.team) { - for (const teamMember of calendarEvent.team.members) { - emailsToSend.push( - sendEmail(() => new OrganizerRescheduledEmail({ calEvent: calendarEvent, teamMember })) - ); + if (calendarEvent.team) { + for (const teamMember of calendarEvent.team.members) { + emailsToSend.push( + sendEmail(() => new OrganizerRescheduledEmail({ calEvent: calendarEvent, teamMember })) + ); + } } } - emailsToSend.push( - ...calendarEvent.attendees.map((attendee) => { - return sendEmail(() => new AttendeeRescheduledEmail(calendarEvent, attendee)); - }) - ); + if (!eventTypeDisableAttendeeEmail(eventTypeMetadata)) { + emailsToSend.push( + ...calendarEvent.attendees.map((attendee) => { + return sendEmail(() => new AttendeeRescheduledEmail(calendarEvent, attendee)); + }) + ); + } await Promise.all(emailsToSend); }; -export const sendRescheduledSeatEmail = async (calEvent: CalendarEvent, attendee: Person) => { +export const sendRescheduledSeatEmail = async ( + calEvent: CalendarEvent, + attendee: Person, + eventTypeMetadata?: EventTypeMetadata +) => { const calendarEvent = formatCalEvent(calEvent); const clonedCalEvent = cloneDeep(calendarEvent); - const emailsToSend: Promise[] = [ - sendEmail(() => new AttendeeRescheduledEmail(clonedCalEvent, attendee)), - sendEmail(() => new OrganizerRescheduledEmail({ calEvent: calendarEvent })), - ]; + const emailsToSend: Promise[] = []; + + if (!eventTypeDisableHostEmail(eventTypeMetadata)) + emailsToSend.push(sendEmail(() => new OrganizerRescheduledEmail({ calEvent: calendarEvent }))); + if (!eventTypeDisableAttendeeEmail(eventTypeMetadata)) + emailsToSend.push(sendEmail(() => new AttendeeRescheduledEmail(clonedCalEvent, attendee))); await Promise.all(emailsToSend); }; @@ -195,13 +236,14 @@ export const sendScheduledSeatsEmails = async ( newSeat: boolean, showAttendees: boolean, hostEmailDisabled?: boolean, - attendeeEmailDisabled?: boolean + attendeeEmailDisabled?: boolean, + eventTypeMetadata?: EventTypeMetadata ) => { const calendarEvent = formatCalEvent(calEvent); const emailsToSend: Promise[] = []; - if (!hostEmailDisabled) { + if (!hostEmailDisabled && !eventTypeDisableHostEmail(eventTypeMetadata)) { emailsToSend.push(sendEmail(() => new OrganizerScheduledEmail({ calEvent: calendarEvent, newSeat }))); if (calendarEvent.team) { @@ -213,7 +255,7 @@ export const sendScheduledSeatsEmails = async ( } } - if (!attendeeEmailDisabled) { + if (!attendeeEmailDisabled && !eventTypeDisableAttendeeEmail(eventTypeMetadata)) { emailsToSend.push( sendEmail( () => @@ -231,16 +273,30 @@ export const sendScheduledSeatsEmails = async ( await Promise.all(emailsToSend); }; -export const sendCancelledSeatEmails = async (calEvent: CalendarEvent, cancelledAttendee: Person) => { +export const sendCancelledSeatEmails = async ( + calEvent: CalendarEvent, + cancelledAttendee: Person, + eventTypeMetadata?: EventTypeMetadata +) => { const formattedCalEvent = formatCalEvent(calEvent); const clonedCalEvent = cloneDeep(formattedCalEvent); - await Promise.all([ - sendEmail(() => new AttendeeCancelledSeatEmail(clonedCalEvent, cancelledAttendee)), - sendEmail(() => new OrganizerAttendeeCancelledSeatEmail({ calEvent: formattedCalEvent })), - ]); + const emailsToSend: Promise[] = []; + + if (!eventTypeDisableAttendeeEmail(eventTypeMetadata)) + emailsToSend.push(sendEmail(() => new AttendeeCancelledSeatEmail(clonedCalEvent, cancelledAttendee))); + if (!eventTypeDisableHostEmail(eventTypeMetadata)) + emailsToSend.push( + sendEmail(() => new OrganizerAttendeeCancelledSeatEmail({ calEvent: formattedCalEvent })) + ); + + await Promise.all(emailsToSend); }; -export const sendOrganizerRequestEmail = async (calEvent: CalendarEvent) => { +export const sendOrganizerRequestEmail = async ( + calEvent: CalendarEvent, + eventTypeMetadata?: EventTypeMetadata +) => { + if (eventTypeDisableHostEmail(eventTypeMetadata)) return; const calendarEvent = formatCalEvent(calEvent); const emailsToSend: Promise[] = []; @@ -256,12 +312,18 @@ export const sendOrganizerRequestEmail = async (calEvent: CalendarEvent) => { await Promise.all(emailsToSend); }; -export const sendAttendeeRequestEmail = async (calEvent: CalendarEvent, attendee: Person) => { +export const sendAttendeeRequestEmail = async ( + calEvent: CalendarEvent, + attendee: Person, + eventTypeMetadata?: EventTypeMetadata +) => { + if (eventTypeDisableAttendeeEmail(eventTypeMetadata)) return; const calendarEvent = formatCalEvent(calEvent); await sendEmail(() => new AttendeeRequestEmail(calendarEvent, attendee)); }; -export const sendDeclinedEmails = async (calEvent: CalendarEvent) => { +export const sendDeclinedEmails = async (calEvent: CalendarEvent, eventTypeMetadata?: EventTypeMetadata) => { + if (eventTypeDisableAttendeeEmail(eventTypeMetadata)) return; const calendarEvent = formatCalEvent(calEvent); const emailsToSend: Promise[] = []; @@ -276,57 +338,67 @@ export const sendDeclinedEmails = async (calEvent: CalendarEvent) => { export const sendCancelledEmails = async ( calEvent: CalendarEvent, - eventNameObject: Pick + eventNameObject: Pick, + eventTypeMetadata?: EventTypeMetadata ) => { const calendarEvent = formatCalEvent(calEvent); const emailsToSend: Promise[] = []; - - emailsToSend.push(sendEmail(() => new OrganizerCancelledEmail({ calEvent: calendarEvent }))); const calEventLength = calendarEvent.length; + const eventDuration = calEventLength as number; + if (typeof calEventLength !== "number") { logger.error( "`calEventLength` is not a number", safeStringify({ calEventLength, calEventTitle: calEvent.title, bookingId: calEvent.bookingId }) ); } - const eventDuration = calEventLength as number; - if (calendarEvent.team?.members) { - for (const teamMember of calendarEvent.team.members) { - emailsToSend.push( - sendEmail(() => new OrganizerCancelledEmail({ calEvent: calendarEvent, teamMember })) - ); + if (!eventTypeDisableHostEmail(eventTypeMetadata)) { + emailsToSend.push(sendEmail(() => new OrganizerCancelledEmail({ calEvent: calendarEvent }))); + + if (calendarEvent.team?.members) { + for (const teamMember of calendarEvent.team.members) { + emailsToSend.push( + sendEmail(() => new OrganizerCancelledEmail({ calEvent: calendarEvent, teamMember })) + ); + } } } - emailsToSend.push( - ...calendarEvent.attendees.map((attendee) => { - return sendEmail( - () => - new AttendeeCancelledEmail( - { - ...calendarEvent, - title: getEventName({ - ...eventNameObject, - t: attendee.language.translate, - attendeeName: attendee.name, - host: calendarEvent.organizer.name, - eventType: calendarEvent.type, - eventDuration, - ...(calendarEvent.responses && { bookingFields: calendarEvent.responses }), - ...(calendarEvent.location && { location: calendarEvent.location }), - }), - }, - attendee - ) - ); - }) - ); + if (!eventTypeDisableAttendeeEmail(eventTypeMetadata)) { + emailsToSend.push( + ...calendarEvent.attendees.map((attendee) => { + return sendEmail( + () => + new AttendeeCancelledEmail( + { + ...calendarEvent, + title: getEventName({ + ...eventNameObject, + t: attendee.language.translate, + attendeeName: attendee.name, + host: calendarEvent.organizer.name, + eventType: calendarEvent.type, + eventDuration, + ...(calendarEvent.responses && { bookingFields: calendarEvent.responses }), + ...(calendarEvent.location && { location: calendarEvent.location }), + }), + }, + attendee + ) + ); + }) + ); + } await Promise.all(emailsToSend); }; -export const sendOrganizerRequestReminderEmail = async (calEvent: CalendarEvent) => { +export const sendOrganizerRequestReminderEmail = async ( + calEvent: CalendarEvent, + eventTypeMetadata?: EventTypeMetadata +) => { + if (eventTypeDisableHostEmail(eventTypeMetadata)) return; const calendarEvent = formatCalEvent(calEvent); const emailsToSend: Promise[] = []; @@ -342,7 +414,11 @@ export const sendOrganizerRequestReminderEmail = async (calEvent: CalendarEvent) } }; -export const sendAwaitingPaymentEmail = async (calEvent: CalendarEvent) => { +export const sendAwaitingPaymentEmail = async ( + calEvent: CalendarEvent, + eventTypeMetadata?: EventTypeMetadata +) => { + if (eventTypeDisableAttendeeEmail(eventTypeMetadata)) return; const emailsToSend: Promise[] = []; emailsToSend.push( @@ -398,38 +474,49 @@ export const sendChangeOfEmailVerificationLink = async (verificationInput: Chang export const sendRequestRescheduleEmail = async ( calEvent: CalendarEvent, - metadata: { rescheduleLink: string } + metadata: { rescheduleLink: string }, + eventTypeMetadata?: EventTypeMetadata ) => { const emailsToSend: Promise[] = []; const calendarEvent = formatCalEvent(calEvent); - emailsToSend.push(sendEmail(() => new OrganizerRequestedToRescheduleEmail(calendarEvent, metadata))); - - emailsToSend.push(sendEmail(() => new AttendeeWasRequestedToRescheduleEmail(calendarEvent, metadata))); + if (!eventTypeDisableHostEmail(eventTypeMetadata)) { + emailsToSend.push(sendEmail(() => new OrganizerRequestedToRescheduleEmail(calendarEvent, metadata))); + } + if (!eventTypeDisableAttendeeEmail(eventTypeMetadata)) { + emailsToSend.push(sendEmail(() => new AttendeeWasRequestedToRescheduleEmail(calendarEvent, metadata))); + } await Promise.all(emailsToSend); }; -export const sendLocationChangeEmails = async (calEvent: CalendarEvent) => { +export const sendLocationChangeEmails = async ( + calEvent: CalendarEvent, + eventTypeMetadata?: EventTypeMetadata +) => { const calendarEvent = formatCalEvent(calEvent); const emailsToSend: Promise[] = []; - emailsToSend.push(sendEmail(() => new OrganizerLocationChangeEmail({ calEvent: calendarEvent }))); + if (!eventTypeDisableHostEmail(eventTypeMetadata)) { + emailsToSend.push(sendEmail(() => new OrganizerLocationChangeEmail({ calEvent: calendarEvent }))); - if (calendarEvent.team?.members) { - for (const teamMember of calendarEvent.team.members) { - emailsToSend.push( - sendEmail(() => new OrganizerLocationChangeEmail({ calEvent: calendarEvent, teamMember })) - ); + if (calendarEvent.team?.members) { + for (const teamMember of calendarEvent.team.members) { + emailsToSend.push( + sendEmail(() => new OrganizerLocationChangeEmail({ calEvent: calendarEvent, teamMember })) + ); + } } } - emailsToSend.push( - ...calendarEvent.attendees.map((attendee) => { - return sendEmail(() => new AttendeeLocationChangeEmail(calendarEvent, attendee)); - }) - ); + if (!eventTypeDisableAttendeeEmail(eventTypeMetadata)) { + emailsToSend.push( + ...calendarEvent.attendees.map((attendee) => { + return sendEmail(() => new AttendeeLocationChangeEmail(calendarEvent, attendee)); + }) + ); + } await Promise.all(emailsToSend); }; @@ -476,7 +563,12 @@ export const sendSlugReplacementEmail = async ({ await sendEmail(() => new SlugReplacementEmail(email, name, teamName, slug, t)); }; -export const sendNoShowFeeChargedEmail = async (attendee: Person, evt: CalendarEvent) => { +export const sendNoShowFeeChargedEmail = async ( + attendee: Person, + evt: CalendarEvent, + eventTypeMetadata?: EventTypeMetadata +) => { + if (eventTypeDisableAttendeeEmail(eventTypeMetadata)) return; await sendEmail(() => new NoShowFeeChargedEmail(evt, attendee)); }; diff --git a/packages/features/bookings/lib/handleBookingRequested.ts b/packages/features/bookings/lib/handleBookingRequested.ts index c6869c64fd..9252315690 100644 --- a/packages/features/bookings/lib/handleBookingRequested.ts +++ b/packages/features/bookings/lib/handleBookingRequested.ts @@ -1,3 +1,5 @@ +import type { Prisma } from "@prisma/client"; + import { sendAttendeeRequestEmail, sendOrganizerRequestEmail } from "@calcom/emails"; import { getWebhookPayloadForBooking } from "@calcom/features/bookings/lib/getWebhookPayloadForBooking"; import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks"; @@ -6,6 +8,7 @@ import getOrgIdFromMemberOrTeamId from "@calcom/lib/getOrgIdFromMemberOrTeamId"; import logger from "@calcom/lib/logger"; import { safeStringify } from "@calcom/lib/safeStringify"; import { WebhookTriggerEvents } from "@calcom/prisma/enums"; +import type { EventTypeMetadata } from "@calcom/prisma/zod-utils"; import type { CalendarEvent } from "@calcom/types/Calendar"; const log = logger.getSubLogger({ prefix: ["[handleBookingRequested] book:user"] }); @@ -28,6 +31,7 @@ export async function handleBookingRequested(args: { requiresConfirmation: boolean; title: string; teamId?: number | null; + metadata: Prisma.JsonValue; } | null; eventTypeId: number | null; userId: number | null; @@ -37,8 +41,12 @@ export async function handleBookingRequested(args: { const { evt, booking } = args; log.debug("Emails: Sending booking requested emails"); - await sendOrganizerRequestEmail({ ...evt }); - await sendAttendeeRequestEmail({ ...evt }, evt.attendees[0]); + await sendOrganizerRequestEmail({ ...evt }, booking?.eventType?.metadata as EventTypeMetadata); + await sendAttendeeRequestEmail( + { ...evt }, + evt.attendees[0], + booking?.eventType?.metadata as EventTypeMetadata + ); const orgId = await getOrgIdFromMemberOrTeamId({ memberId: booking.userId, diff --git a/packages/features/bookings/lib/handleCancelBooking.ts b/packages/features/bookings/lib/handleCancelBooking.ts index 48d84cf2f7..6a20fb5201 100644 --- a/packages/features/bookings/lib/handleCancelBooking.ts +++ b/packages/features/bookings/lib/handleCancelBooking.ts @@ -26,6 +26,7 @@ import type { WebhookTriggerEvents } from "@calcom/prisma/enums"; import { BookingStatus } from "@calcom/prisma/enums"; import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential"; import { EventTypeMetaDataSchema, schemaBookingCancelParams } from "@calcom/prisma/zod-utils"; +import type { EventTypeMetadata } from "@calcom/prisma/zod-utils"; import { deleteAllWorkflowReminders, getAllWorkflowsFromEventType, @@ -320,7 +321,11 @@ async function handler(req: CustomRequest) { const dataForWebhooks = { evt, webhooks, eventTypeInfo }; // If it's just an attendee of a booking then just remove them from that booking - const result = await cancelAttendeeSeat(req, dataForWebhooks); + const result = await cancelAttendeeSeat( + req, + dataForWebhooks, + bookingToDelete?.eventType?.metadata as EventTypeMetadata + ); if (result) return { success: true, @@ -503,7 +508,11 @@ async function handler(req: CustomRequest) { try { // TODO: if emails fail try to requeue them if (!platformClientId || (platformClientId && arePlatformEmailsEnabled)) - await sendCancelledEmails(evt, { eventName: bookingToDelete?.eventType?.eventName }); + await sendCancelledEmails( + evt, + { eventName: bookingToDelete?.eventType?.eventName }, + bookingToDelete?.eventType?.metadata as EventTypeMetadata + ); } catch (error) { console.error("Error deleting event", error); } diff --git a/packages/features/bookings/lib/handleConfirmation.ts b/packages/features/bookings/lib/handleConfirmation.ts index 0f964dd50f..3bc204e386 100644 --- a/packages/features/bookings/lib/handleConfirmation.ts +++ b/packages/features/bookings/lib/handleConfirmation.ts @@ -111,7 +111,8 @@ export async function handleConfirmation(args: { { ...evt, additionalInformation: metadata }, undefined, isHostConfirmationEmailsDisabled, - isAttendeeConfirmationEmailDisabled + isAttendeeConfirmationEmailDisabled, + eventTypeMetadata ); } catch (error) { log.error(error); @@ -255,13 +256,15 @@ export async function handleConfirmation(args: { evtOfBooking.uid = updatedBookings[index].uid; const isFirstBooking = index === 0; - await scheduleMandatoryReminder( - evtOfBooking, - workflows, - false, - !!updatedBookings[index].eventType?.owner?.hideBranding, - evt.attendeeSeatId - ); + if (!eventTypeMetadata?.disableStandardEmails?.all?.attendee) { + await scheduleMandatoryReminder( + evtOfBooking, + workflows, + false, + !!updatedBookings[index].eventType?.owner?.hideBranding, + evt.attendeeSeatId + ); + } await scheduleWorkflowReminders({ workflows, diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index adb8fa91d5..ccf4a7912b 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -1357,17 +1357,20 @@ async function handler( originalBookingMemberEmails.find((orignalMember) => orignalMember.email === member.email) ); - sendRoundRobinRescheduledEmails(copyEventAdditionalInfo, rescheduledMembers); - sendRoundRobinScheduledEmails(copyEventAdditionalInfo, newBookedMembers); - sendRoundRobinCancelledEmails(copyEventAdditionalInfo, cancelledMembers); + sendRoundRobinRescheduledEmails(copyEventAdditionalInfo, rescheduledMembers, eventType.metadata); + sendRoundRobinScheduledEmails(copyEventAdditionalInfo, newBookedMembers, eventType.metadata); + sendRoundRobinCancelledEmails(copyEventAdditionalInfo, cancelledMembers, eventType.metadata); } else { // send normal rescheduled emails (non round robin event, where organizers stay the same) - await sendRescheduledEmails({ - ...copyEvent, - additionalInformation: metadata, - additionalNotes, // Resets back to the additionalNote input and not the override value - cancellationReason: `$RCH$${rescheduleReason ? rescheduleReason : ""}`, // Removable code prefix to differentiate cancellation from rescheduling for email - }); + await sendRescheduledEmails( + { + ...copyEvent, + additionalInformation: metadata, + additionalNotes, // Resets back to the additionalNote input and not the override value + cancellationReason: `$RCH$${rescheduleReason ? rescheduleReason : ""}`, // Removable code prefix to differentiate cancellation from rescheduling for email + }, + eventType?.metadata + ); } } // If it's not a reschedule, doesn't require confirmation and there's no price, @@ -1504,7 +1507,8 @@ async function handler( }, eventNameObject, isHostConfirmationEmailsDisabled, - isAttendeeConfirmationEmailDisabled + isAttendeeConfirmationEmailDisabled, + eventType.metadata ); } } @@ -1533,8 +1537,8 @@ async function handler( calEvent: getPiiFreeCalendarEvent(evt), }) ); - await sendOrganizerRequestEmail({ ...evt, additionalNotes }); - await sendAttendeeRequestEmail({ ...evt, additionalNotes }, attendeesList[0]); + await sendOrganizerRequestEmail({ ...evt, additionalNotes }, eventType.metadata); + await sendAttendeeRequestEmail({ ...evt, additionalNotes }, attendeesList[0], eventType.metadata); } if (booking.location?.startsWith("http")) { @@ -1745,13 +1749,15 @@ async function handler( const evtWithMetadata = { ...evt, metadata, eventType: { slug: eventType.slug } }; - await scheduleMandatoryReminder( - evtWithMetadata, - workflows, - !isConfirmedByDefault, - !!eventType.owner?.hideBranding, - evt.attendeeSeatId - ); + if (!eventType.metadata?.disableStandardEmails?.all?.attendee) { + await scheduleMandatoryReminder( + evtWithMetadata, + workflows, + !isConfirmedByDefault, + !!eventType.owner?.hideBranding, + evt.attendeeSeatId + ); + } try { await scheduleWorkflowReminders({ diff --git a/packages/features/bookings/lib/handleSeats/cancel/cancelAttendeeSeat.ts b/packages/features/bookings/lib/handleSeats/cancel/cancelAttendeeSeat.ts index af732a9027..06624de91d 100644 --- a/packages/features/bookings/lib/handleSeats/cancel/cancelAttendeeSeat.ts +++ b/packages/features/bookings/lib/handleSeats/cancel/cancelAttendeeSeat.ts @@ -11,6 +11,7 @@ import prisma from "@calcom/prisma"; import { WebhookTriggerEvents } from "@calcom/prisma/enums"; import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential"; import { schemaBookingCancelParams } from "@calcom/prisma/zod-utils"; +import type { EventTypeMetadata } from "@calcom/prisma/zod-utils"; import { deleteAllWorkflowReminders } from "@calcom/trpc/server/routers/viewer/workflows/util"; import type { CalendarEvent } from "@calcom/types/Calendar"; @@ -28,7 +29,8 @@ async function cancelAttendeeSeat( }[]; evt: CalendarEvent; eventTypeInfo: EventTypeInfo; - } + }, + eventTypeMetadata: EventTypeMetadata ) { const { seatReferenceUid } = schemaBookingCancelParams.parse(req.body); const { webhooks, evt, eventTypeInfo } = dataForWebhooks; @@ -105,10 +107,14 @@ async function cancelAttendeeSeat( const tAttendees = await getTranslation(attendee.locale ?? "en", "common"); - await sendCancelledSeatEmails(evt, { - ...attendee, - language: { translate: tAttendees, locale: attendee.locale ?? "en" }, - }); + await sendCancelledSeatEmails( + evt, + { + ...attendee, + language: { translate: tAttendees, locale: attendee.locale ?? "en" }, + }, + eventTypeMetadata + ); } evt.attendees = attendee diff --git a/packages/features/bookings/lib/handleSeats/create/createNewSeat.ts b/packages/features/bookings/lib/handleSeats/create/createNewSeat.ts index 9a6fd8b17e..6bedcc2f2e 100644 --- a/packages/features/bookings/lib/handleSeats/create/createNewSeat.ts +++ b/packages/features/bookings/lib/handleSeats/create/createNewSeat.ts @@ -138,7 +138,8 @@ const createNewSeat = async ( newSeat, !!eventType.seatsShowAttendees, isHostConfirmationEmailsDisabled, - isAttendeeConfirmationEmailDisabled + isAttendeeConfirmationEmailDisabled, + eventType.metadata ); } const credentials = await refreshCredentials(allCredentials); diff --git a/packages/features/bookings/lib/handleSeats/reschedule/attendee/attendeeRescheduleSeatedBooking.ts b/packages/features/bookings/lib/handleSeats/reschedule/attendee/attendeeRescheduleSeatedBooking.ts index e340319666..c3a8369f79 100644 --- a/packages/features/bookings/lib/handleSeats/reschedule/attendee/attendeeRescheduleSeatedBooking.ts +++ b/packages/features/bookings/lib/handleSeats/reschedule/attendee/attendeeRescheduleSeatedBooking.ts @@ -18,7 +18,7 @@ const attendeeRescheduleSeatedBooking = async ( originalBookingEvt: CalendarEvent, eventManager: EventManager ) => { - const { tAttendees, bookingSeat, bookerEmail, evt } = rescheduleSeatedBookingObject; + const { tAttendees, bookingSeat, bookerEmail, evt, eventType } = rescheduleSeatedBookingObject; let { originalRescheduledBooking } = rescheduleSeatedBookingObject; seatAttendee["language"] = { translate: tAttendees, locale: bookingSeat?.attendee.locale ?? "en" }; @@ -91,7 +91,7 @@ const attendeeRescheduleSeatedBooking = async ( await eventManager.updateCalendarAttendees(copyEvent, newTimeSlotBooking); - await sendRescheduledSeatEmail(copyEvent, seatAttendee as Person); + await sendRescheduledSeatEmail(copyEvent, seatAttendee as Person, eventType.metadata); const filteredAttendees = originalRescheduledBooking?.attendees.filter((attendee) => { return attendee.email !== bookerEmail; }); diff --git a/packages/features/bookings/lib/handleSeats/reschedule/owner/combineTwoSeatedBookings.ts b/packages/features/bookings/lib/handleSeats/reschedule/owner/combineTwoSeatedBookings.ts index 7efe855e03..8b009e7ee0 100644 --- a/packages/features/bookings/lib/handleSeats/reschedule/owner/combineTwoSeatedBookings.ts +++ b/packages/features/bookings/lib/handleSeats/reschedule/owner/combineTwoSeatedBookings.ts @@ -136,11 +136,14 @@ const combineTwoSeatedBookings = async ( if (noEmail !== true && isConfirmedByDefault) { // TODO send reschedule emails to attendees of the old booking loggerWithEventDetails.debug("Emails: Sending reschedule emails - handleSeats"); - await sendRescheduledEmails({ - ...copyEvent, - additionalNotes, // Resets back to the additionalNote input and not the override value - cancellationReason: `$RCH$${rescheduleReason ? rescheduleReason : ""}`, // Removable code prefix to differentiate cancellation from rescheduling for email - }); + await sendRescheduledEmails( + { + ...copyEvent, + additionalNotes, // Resets back to the additionalNote input and not the override value + cancellationReason: `$RCH$${rescheduleReason ? rescheduleReason : ""}`, // Removable code prefix to differentiate cancellation from rescheduling for email + }, + eventType.metadata + ); } // Update the old booking with the cancelled status diff --git a/packages/features/bookings/lib/handleSeats/reschedule/owner/moveSeatedBookingToNewTimeSlot.ts b/packages/features/bookings/lib/handleSeats/reschedule/owner/moveSeatedBookingToNewTimeSlot.ts index 93dc6f4342..42222aa929 100644 --- a/packages/features/bookings/lib/handleSeats/reschedule/owner/moveSeatedBookingToNewTimeSlot.ts +++ b/packages/features/bookings/lib/handleSeats/reschedule/owner/moveSeatedBookingToNewTimeSlot.ts @@ -90,11 +90,14 @@ const moveSeatedBookingToNewTimeSlot = async ( if (noEmail !== true && isConfirmedByDefault) { const copyEvent = cloneDeep(evt); loggerWithEventDetails.debug("Emails: Sending reschedule emails - handleSeats"); - await sendRescheduledEmails({ - ...copyEvent, - additionalNotes, // Resets back to the additionalNote input and not the override value - cancellationReason: `$RCH$${rescheduleReason ? rescheduleReason : ""}`, // Removable code prefix to differentiate cancellation from rescheduling for email - }); + await sendRescheduledEmails( + { + ...copyEvent, + additionalNotes, // Resets back to the additionalNote input and not the override value + cancellationReason: `$RCH$${rescheduleReason ? rescheduleReason : ""}`, // Removable code prefix to differentiate cancellation from rescheduling for email + }, + eventType.metadata + ); } const foundBooking = await findBookingQuery(newBooking.id); diff --git a/packages/features/credentials/handleDeleteCredential.ts b/packages/features/credentials/handleDeleteCredential.ts index bf69811dfe..c5343fdf7c 100644 --- a/packages/features/credentials/handleDeleteCredential.ts +++ b/packages/features/credentials/handleDeleteCredential.ts @@ -13,7 +13,7 @@ import { getTranslation } from "@calcom/lib/server/i18n"; import { bookingMinimalSelect, prisma } from "@calcom/prisma"; import { AppCategories, BookingStatus } from "@calcom/prisma/enums"; import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential"; -import type { EventTypeAppMetadataSchema } from "@calcom/prisma/zod-utils"; +import type { EventTypeAppMetadataSchema, EventTypeMetadata } from "@calcom/prisma/zod-utils"; import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; import { userMetadata as userMetadataSchema } from "@calcom/prisma/zod-utils"; @@ -244,6 +244,7 @@ const handleDeleteCredential = async ({ seatsPerTimeSlot: true, seatsShowAttendees: true, eventName: true, + metadata: true, }, }, uid: true, @@ -335,7 +336,8 @@ const handleDeleteCredential = async ({ }, { eventName: booking?.eventType?.eventName, - } + }, + booking?.eventType?.metadata as EventTypeMetadata ); } }); diff --git a/packages/features/ee/payments/api/webhook.ts b/packages/features/ee/payments/api/webhook.ts index fa07ee1dc8..3992966b8d 100644 --- a/packages/features/ee/payments/api/webhook.ts +++ b/packages/features/ee/payments/api/webhook.ts @@ -107,8 +107,8 @@ const handleSetupSuccess = async (event: Stripe.Event) => { paid: true, }); } else { - await sendOrganizerRequestEmail({ ...evt }); - await sendAttendeeRequestEmail({ ...evt }, evt.attendees[0]); + await sendOrganizerRequestEmail({ ...evt }, eventType.metadata); + await sendAttendeeRequestEmail({ ...evt }, evt.attendees[0], eventType.metadata); } }; diff --git a/packages/features/ee/round-robin/roundRobinReassignment.ts b/packages/features/ee/round-robin/roundRobinReassignment.ts index c566d3e641..6f5c7ee67f 100644 --- a/packages/features/ee/round-robin/roundRobinReassignment.ts +++ b/packages/features/ee/round-robin/roundRobinReassignment.ts @@ -26,6 +26,7 @@ import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat"; import { prisma } from "@calcom/prisma"; import { WorkflowActions, WorkflowMethods, WorkflowTriggerEvents } from "@calcom/prisma/enums"; import { userMetadata as userMetadataSchema } from "@calcom/prisma/zod-utils"; +import type { EventTypeMetadata } from "@calcom/prisma/zod-utils"; import type { CalendarEvent } from "@calcom/types/Calendar"; const bookingSelect = { @@ -398,15 +399,19 @@ export const roundRobinReassignment = async ({ bookingId }: { bookingId: number }); } - await sendRoundRobinCancelledEmails(cancelledRRHostEvt, [ - { - ...previousRRHost, - name: previousRRHost.name || "", - username: previousRRHost.username || "", - timeFormat: getTimeFormatStringFromUserTimeFormat(previousRRHost.timeFormat), - language: { translate: previousRRHostT, locale: previousRRHost.locale || "en" }, - }, - ]); + await sendRoundRobinCancelledEmails( + cancelledRRHostEvt, + [ + { + ...previousRRHost, + name: previousRRHost.name || "", + username: previousRRHost.username || "", + timeFormat: getTimeFormatStringFromUserTimeFormat(previousRRHost.timeFormat), + language: { translate: previousRRHostT, locale: previousRRHost.locale || "en" }, + }, + ], + eventType?.metadata as EventTypeMetadata + ); } // Handle changing workflows with organizer diff --git a/packages/lib/payment/handlePayment.ts b/packages/lib/payment/handlePayment.ts index 88ae20af71..2288dff7aa 100644 --- a/packages/lib/payment/handlePayment.ts +++ b/packages/lib/payment/handlePayment.ts @@ -75,7 +75,7 @@ const handlePayment = async ( throw new Error("Payment data is null"); } try { - await paymentInstance.afterPayment(evt, booking, paymentData); + await paymentInstance.afterPayment(evt, booking, paymentData, selectedEventType?.metadata); } catch (e) { console.error(e); } diff --git a/packages/lib/payment/handlePaymentSuccess.ts b/packages/lib/payment/handlePaymentSuccess.ts index 1baa03ac00..1e5eaa8fcf 100644 --- a/packages/lib/payment/handlePaymentSuccess.ts +++ b/packages/lib/payment/handlePaymentSuccess.ts @@ -76,7 +76,7 @@ export async function handlePaymentSuccess(paymentId: number, bookingId: number) log.debug(`handling booking request for eventId ${eventType.id}`); } } else { - await sendScheduledEmails({ ...evt }); + await sendScheduledEmails({ ...evt }, undefined, undefined, undefined, eventType.metadata); } throw new HttpCode({ diff --git a/packages/prisma/zod-utils.ts b/packages/prisma/zod-utils.ts index 80b594e412..71c2d51c47 100644 --- a/packages/prisma/zod-utils.ts +++ b/packages/prisma/zod-utils.ts @@ -80,6 +80,12 @@ export const EventTypeMetaDataSchema = z disableSuccessPage: z.boolean().optional(), disableStandardEmails: z .object({ + all: z + .object({ + host: z.boolean().optional(), + attendee: z.boolean().optional(), + }) + .optional(), confirmation: z .object({ host: z.boolean().optional(), @@ -108,6 +114,8 @@ export const EventTypeMetaDataSchema = z }) .nullable(); +export type EventTypeMetadata = z.infer; + export const eventTypeBookingFields = formBuilderFieldsSchema; export const BookingFieldTypeEnum = eventTypeBookingFields.element.shape.type.Enum; export type BookingFieldType = FormBuilderFieldType; diff --git a/packages/trpc/server/routers/loggedInViewer/connectAndJoin.handler.ts b/packages/trpc/server/routers/loggedInViewer/connectAndJoin.handler.ts index 41b94ecca2..99dea2cc96 100644 --- a/packages/trpc/server/routers/loggedInViewer/connectAndJoin.handler.ts +++ b/packages/trpc/server/routers/loggedInViewer/connectAndJoin.handler.ts @@ -5,7 +5,7 @@ import { getTranslation } from "@calcom/lib/server/i18n"; import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat"; import { prisma } from "@calcom/prisma"; import { BookingStatus } from "@calcom/prisma/enums"; -import { bookingMetadataSchema } from "@calcom/prisma/zod-utils"; +import { bookingMetadataSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; import type { TrpcSessionUser } from "@calcom/trpc/server/trpc"; import type { CalendarEvent } from "@calcom/types/Calendar"; @@ -204,13 +204,16 @@ export const Handler = async ({ ctx, input }: Options) => { videoCallData, }; + const eventTypeMetadata = EventTypeMetaDataSchema.parse(updatedBooking?.eventType?.metadata); + await sendScheduledEmails( { ...evt, }, undefined, false, - false + false, + eventTypeMetadata ); return { isBookingAlreadyAcceptedBySomeoneElse, meetingUrl: locationVideoCallUrl }; diff --git a/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts b/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts index 44e43a9278..dc33184a12 100644 --- a/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts @@ -18,6 +18,7 @@ import { getUsersCredentials } from "@calcom/lib/server/getUsersCredentials"; import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat"; import { prisma } from "@calcom/prisma"; import { BookingStatus, MembershipRole, SchedulingType, WebhookTriggerEvents } from "@calcom/prisma/enums"; +import type { EventTypeMetadata } from "@calcom/prisma/zod-utils"; import type { CalendarEvent } from "@calcom/types/Calendar"; import type { IAbstractPaymentService, PaymentApp } from "@calcom/types/PaymentService"; @@ -370,7 +371,7 @@ export const confirmHandler = async ({ ctx, input }: ConfirmOptions) => { }); } - await sendDeclinedEmails(evt); + await sendDeclinedEmails(evt, booking.eventType?.metadata as EventTypeMetadata); const teamId = await getTeamIdFromEventType({ eventType: { diff --git a/packages/trpc/server/routers/viewer/bookings/editLocation.handler.ts b/packages/trpc/server/routers/viewer/bookings/editLocation.handler.ts index 636f49a35f..f8fcd4bd54 100644 --- a/packages/trpc/server/routers/viewer/bookings/editLocation.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/editLocation.handler.ts @@ -7,6 +7,7 @@ import { getTranslation } from "@calcom/lib/server"; import { getUsersCredentials } from "@calcom/lib/server/getUsersCredentials"; import { prisma } from "@calcom/prisma"; import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential"; +import type { EventTypeMetadata } from "@calcom/prisma/zod-utils"; import type { AdditionalInformation, CalendarEvent } from "@calcom/types/Calendar"; import type { CredentialPayload } from "@calcom/types/Credential"; @@ -131,7 +132,10 @@ export const editLocationHandler = async ({ ctx, input }: EditLocationOptions) = metadata.entryPoints = results[0].updatedEvent?.entryPoints; } try { - await sendLocationChangeEmails({ ...evt, additionalInformation: metadata }); + await sendLocationChangeEmails( + { ...evt, additionalInformation: metadata }, + booking?.eventType?.metadata as EventTypeMetadata + ); } catch (error) { console.log("Error sending LocationChangeEmails"); } diff --git a/packages/trpc/server/routers/viewer/bookings/requestReschedule.handler.ts b/packages/trpc/server/routers/viewer/bookings/requestReschedule.handler.ts index 54ce65d75c..392a34215f 100644 --- a/packages/trpc/server/routers/viewer/bookings/requestReschedule.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/requestReschedule.handler.ts @@ -22,6 +22,7 @@ import { getUsersCredentials } from "@calcom/lib/server/getUsersCredentials"; import { prisma } from "@calcom/prisma"; import type { WebhookTriggerEvents } from "@calcom/prisma/enums"; import { BookingStatus } from "@calcom/prisma/enums"; +import type { EventTypeMetadata } from "@calcom/prisma/zod-utils"; import type { CalendarEvent, Person } from "@calcom/types/Calendar"; import { TRPCError } from "@trpc/server"; @@ -240,9 +241,13 @@ export const requestRescheduleHandler = async ({ ctx, input }: RequestReschedule log.debug("builder.calendarEvent", safeStringify(builder.calendarEvent)); // Send emails - await sendRequestRescheduleEmail(builder.calendarEvent, { - rescheduleLink: builder.rescheduleLink, - }); + await sendRequestRescheduleEmail( + builder.calendarEvent, + { + rescheduleLink: builder.rescheduleLink, + }, + eventType?.metadata as EventTypeMetadata + ); const evt: CalendarEvent = { title: bookingToReschedule?.title, diff --git a/packages/trpc/server/routers/viewer/eventTypes/update.handler.ts b/packages/trpc/server/routers/viewer/eventTypes/update.handler.ts index d4a467dff0..7f8f8eee6b 100644 --- a/packages/trpc/server/routers/viewer/eventTypes/update.handler.ts +++ b/packages/trpc/server/routers/viewer/eventTypes/update.handler.ts @@ -3,12 +3,16 @@ import type { NextApiResponse, GetServerSidePropsContext } from "next"; import type { appDataSchemas } from "@calcom/app-store/apps.schemas.generated"; import updateChildrenEventTypes from "@calcom/features/ee/managed-event-types/lib/handleChildrenEventTypes"; +import { + allowDisablingAttendeeConfirmationEmails, + allowDisablingHostConfirmationEmails, +} from "@calcom/features/ee/workflows/lib/allowDisablingStandardEmails"; import { validateIntervalLimitOrder } from "@calcom/lib"; import logger from "@calcom/lib/logger"; import { getTranslation } from "@calcom/lib/server"; import { validateBookerLayouts } from "@calcom/lib/validateBookerLayouts"; import type { PrismaClient } from "@calcom/prisma"; -import { WorkflowActions, WorkflowTriggerEvents } from "@calcom/prisma/client"; +import { WorkflowTriggerEvents } from "@calcom/prisma/client"; import { SchedulingType } from "@calcom/prisma/enums"; import { TRPCError } from "@trpc/server"; @@ -255,9 +259,15 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => { }; } - if (input.metadata?.disableStandardEmails) { - //check if user is allowed to disabled standard emails + if (input.metadata?.disableStandardEmails?.all) { + if (!eventType?.team?.parentId) { + input.metadata.disableStandardEmails.all.host = false; + input.metadata.disableStandardEmails.all.attendee = false; + } + } + if (input.metadata?.disableStandardEmails?.confirmation) { + //check if user is allowed to disabled standard emails const workflows = await ctx.prisma.workflow.findMany({ where: { activeOn: { @@ -273,21 +283,13 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => { }); if (input.metadata?.disableStandardEmails.confirmation?.host) { - if ( - !workflows.find( - (workflow) => !!workflow.steps.find((step) => step.action === WorkflowActions.EMAIL_HOST) - ) - ) { + if (!allowDisablingHostConfirmationEmails(workflows)) { 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) - ) - ) { + if (!allowDisablingAttendeeConfirmationEmails(workflows)) { input.metadata.disableStandardEmails.confirmation.attendee = false; } } diff --git a/packages/trpc/server/routers/viewer/payments.tsx b/packages/trpc/server/routers/viewer/payments.tsx index 54c77e432e..cd39e0a056 100644 --- a/packages/trpc/server/routers/viewer/payments.tsx +++ b/packages/trpc/server/routers/viewer/payments.tsx @@ -8,6 +8,7 @@ import getOrgIdFromMemberOrTeamId from "@calcom/lib/getOrgIdFromMemberOrTeamId"; import { getTranslation } from "@calcom/lib/server/i18n"; import sendPayload from "@calcom/lib/server/webhooks/sendPayload"; import { WebhookTriggerEvents } from "@calcom/prisma/enums"; +import type { EventTypeMetadata } from "@calcom/prisma/zod-utils"; import type { CalendarEvent } from "@calcom/types/Calendar"; import type { PaymentApp } from "@calcom/types/PaymentService"; @@ -149,7 +150,11 @@ export const paymentsRouter = router({ }) ); - await sendNoShowFeeChargedEmail(attendeesListPromises[0], evt); + await sendNoShowFeeChargedEmail( + attendeesListPromises[0], + evt, + booking?.eventType?.metadata as EventTypeMetadata + ); return paymentData; } catch (err) { diff --git a/packages/trpc/server/routers/viewer/payments/chargeCard.handler.ts b/packages/trpc/server/routers/viewer/payments/chargeCard.handler.ts index bdc9cd3b63..ab4a0dd536 100644 --- a/packages/trpc/server/routers/viewer/payments/chargeCard.handler.ts +++ b/packages/trpc/server/routers/viewer/payments/chargeCard.handler.ts @@ -3,6 +3,7 @@ import dayjs from "@calcom/dayjs"; import { sendNoShowFeeChargedEmail } from "@calcom/emails"; import { getTranslation } from "@calcom/lib/server/i18n"; import type { PrismaClient } from "@calcom/prisma"; +import type { EventTypeMetadata } from "@calcom/prisma/zod-utils"; import type { CalendarEvent } from "@calcom/types/Calendar"; import type { IAbstractPaymentService, PaymentApp } from "@calcom/types/PaymentService"; @@ -112,7 +113,11 @@ export const chargeCardHandler = async ({ ctx, input }: ChargeCardHandlerOptions throw new TRPCError({ code: "NOT_FOUND", message: `Could not generate payment data` }); } - await sendNoShowFeeChargedEmail(attendeesListPromises[0], evt); + await sendNoShowFeeChargedEmail( + attendeesListPromises[0], + evt, + booking?.eventType?.metadata as EventTypeMetadata + ); return paymentData; } catch (err) { diff --git a/packages/types/PaymentService.d.ts b/packages/types/PaymentService.d.ts index e9ab38905e..da9d1869cb 100644 --- a/packages/types/PaymentService.d.ts +++ b/packages/types/PaymentService.d.ts @@ -49,7 +49,8 @@ export interface IAbstractPaymentService { startTime: { toISOString: () => string }; uid: string; }, - paymentData: Payment + paymentData: Payment, + eventTypeMetadata?: EventTypeMetadata ): Promise; deletePayment(paymentId: Payment["id"]): Promise; isSetupAlready(): boolean;