fix: Disable all emails per event type w/o a workflow [CAL-4066] (#15902)

* Create confirmation dialog for attendees

* Add disabling host emails option

* Fix DOM issues

* Allow updating disabling emails on backend

* Pass metadata to sendEmails functions

* Clean up

* Type fixes

* Fix test

* Allow for enterprise only

* Type fix

* Test fix

* Check for parentId

* Disable emails for sendScheduledEmails and sendScheduledSeatsEmails

* Remove unused variable

* Only update disable all emails if event type belongs to an org

* Disable scheduling mandatory reminder workflow if attendee emails are disabled

* Refactor disable email UI

* Update copy

* Type fixes

* Fix missing params
This commit is contained in:
Joe Au-Yeung
2024-08-14 13:17:52 +02:00
committed by GitHub
parent c41e5ddc2c
commit 95e2ad3007
31 changed files with 526 additions and 212 deletions
@@ -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<EventTypeSetupProps,
);
const [hashedUrl, setHashedUrl] = useState(eventType.hashedLink?.link);
const bookingFields: Prisma.JsonObject = {};
const workflows = eventType.workflows.map((workflowOnEventType) => workflowOnEventType.workflow);
const selectedThemeIsDark =
user?.theme === "dark" ||
@@ -590,6 +590,34 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
)}
/>
)}
{team?.parentId && (
<>
<Controller
name="metadata.disableStandardEmails.all.attendee"
render={({ field: { value, onChange } }) => {
return (
<>
<DisableAllEmailsSetting
checked={value}
onCheckedChange={onChange}
recipient="attendees"
t={t}
/>
</>
);
}}
/>
<Controller
name="metadata.disableStandardEmails.all.host"
defaultValue={!!formMethods.getValues("seatsPerTimeSlot")}
render={({ field: { value, onChange } }) => (
<>
<DisableAllEmailsSetting checked={value} onCheckedChange={onChange} recipient="hosts" t={t} />
</>
)}
/>
</>
)}
{showEventNameTip && (
<CustomEventTypeModal
close={closeEventNameTip}
@@ -0,0 +1,82 @@
import type { TFunction } from "next-i18next";
import { Trans } from "next-i18next";
import { useState } from "react";
import {
SettingsToggle,
Dialog,
DialogContent,
DialogFooter,
InputField,
DialogClose,
Button,
} from "@calcom/ui";
interface DisableEmailsSettingProps {
checked: boolean;
onCheckedChange: (e: boolean) => 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 (
<div>
<Dialog open={dialogOpen} onOpenChange={(e) => setDialogOpen(e)}>
<DialogContent title={title} Icon="circle-alert">
<p className="text-default text-sm">
<Trans i18nKey="disable_attendees_emails_description">
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.
<br />
<br />
It is your responsibility to ensure that your {{ recipient }} are aware of any bookings and
changes to their bookings.
</Trans>
</p>
<p className="text-default mb-1 mt-2 text-sm">{t("type_confirm_to_continue")}</p>
<InputField
value={confirmText}
onChange={(e) => {
setConfirmText(e.target.value);
}}
/>
<DialogFooter>
<DialogClose />
<Button
disabled={confirmText !== "confirm"}
onClick={(e) => {
onCheckedChange(true);
setDialogOpen(false);
}}>
{t("disable_email")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<SettingsToggle
labelClassName="text-sm"
toggleSwitchAtTheEnd={true}
switchContainerClassName="border-subtle rounded-lg border py-6 px-4 sm:px-6"
title={title}
description={t("disable_all_emails_description")}
checked={!!checked}
onCheckedChange={() => {
checked ? onCheckedChange(!checked) : setDialogOpen(true);
}}
/>
</div>
);
};
+3 -1
View File
@@ -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: {
+23 -18
View File
@@ -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</1>.",
"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</1>.",
"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 <strong>{{teamName}}</strong> 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 <strong>{{teamName}}</strong> 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&apos;t worry, all previous links will still work and redirect appropriately.<br /><br />Please note: All of your personal event types will be moved into the <strong>{teamName}</strong> organization, which may also include potential personal link. <br /><br />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 <a href={`https://${prevLink}`}>{prevLinkWithoutProtocol}</a> to <a href={`https://${newLink}`}>{newLinkWithoutProtocol}</a> but don&apos;t worry, all previous links still work and redirect appropriately. <br /><br />Please note: All of your personal event types have been moved into the <strong>{teamName}</strong> organisation, which may also include potential personal link.<br /><br />Please log in and make sure you have no private events on your new organisational account.<br /><br />For personal events we recommend creating a new account with a personal email address.<br /><br />Enjoy your new clean link: <a href={`https://${newLink}`}>{newLinkWithoutProtocol}</a>",
"email|existing_user_added_link_will_change": "On accepting the invite, your link will change to your organization domain but don&apos;t worry, all previous links will still work and redirect appropriately.<br /><br />Please note: All of your personal event types will be moved into the <strong>{teamName}</strong> organization, which may also include potential personal link. <br /><br />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 <a href={`https://${prevLink}`}>{prevLinkWithoutProtocol}</a> to <a href={`https://${newLink}`}>{newLinkWithoutProtocol}</a> but don&apos;t worry, all previous links still work and redirect appropriately. <br /><br />Please note: All of your personal event types have been moved into the <strong>{teamName}</strong> organisation, which may also include potential personal link.<br /><br />Please log in and make sure you have no private events on your new organisational account.<br /><br />For personal events we recommend creating a new account with a personal email address.<br /><br />Enjoy your new clean link: <a href={`https://${newLink}`}>{newLinkWithoutProtocol}</a>",
"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",
@@ -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<void> {
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<boolean> {
+13 -3
View File
@@ -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);
@@ -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);
+177 -85
View File
@@ -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<typeof EventTypeMetaDataSchema>;
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<unknown>[] = [];
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<unknown>[] = [];
@@ -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<unknown>[] = [];
@@ -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<unknown>[] = [];
@@ -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<unknown>[] = [];
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<unknown>[] = [
sendEmail(() => new AttendeeRescheduledEmail(clonedCalEvent, attendee)),
sendEmail(() => new OrganizerRescheduledEmail({ calEvent: calendarEvent })),
];
const emailsToSend: Promise<unknown>[] = [];
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<unknown>[] = [];
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<unknown>[] = [];
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<unknown>[] = [];
@@ -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<unknown>[] = [];
@@ -276,57 +338,67 @@ export const sendDeclinedEmails = async (calEvent: CalendarEvent) => {
export const sendCancelledEmails = async (
calEvent: CalendarEvent,
eventNameObject: Pick<EventNameObjectType, "eventName">
eventNameObject: Pick<EventNameObjectType, "eventName">,
eventTypeMetadata?: EventTypeMetadata
) => {
const calendarEvent = formatCalEvent(calEvent);
const emailsToSend: Promise<unknown>[] = [];
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<unknown>[] = [];
@@ -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<unknown>[] = [];
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<unknown>[] = [];
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<unknown>[] = [];
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));
};
@@ -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,
@@ -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);
}
@@ -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,
@@ -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({
@@ -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
@@ -138,7 +138,8 @@ const createNewSeat = async (
newSeat,
!!eventType.seatsShowAttendees,
isHostConfirmationEmailsDisabled,
isAttendeeConfirmationEmailDisabled
isAttendeeConfirmationEmailDisabled,
eventType.metadata
);
}
const credentials = await refreshCredentials(allCredentials);
@@ -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;
});
@@ -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
@@ -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);
@@ -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
);
}
});
+2 -2
View File
@@ -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);
}
};
@@ -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
+1 -1
View File
@@ -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);
}
+1 -1
View File
@@ -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({
+8
View File
@@ -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<typeof EventTypeMetaDataSchema>;
export const eventTypeBookingFields = formBuilderFieldsSchema;
export const BookingFieldTypeEnum = eventTypeBookingFields.element.shape.type.Enum;
export type BookingFieldType = FormBuilderFieldType;
@@ -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 };
@@ -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: {
@@ -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");
}
@@ -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,
@@ -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;
}
}
@@ -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) {
@@ -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) {
+2 -1
View File
@@ -49,7 +49,8 @@ export interface IAbstractPaymentService {
startTime: { toISOString: () => string };
uid: string;
},
paymentData: Payment
paymentData: Payment,
eventTypeMetadata?: EventTypeMetadata
): Promise<void>;
deletePayment(paymentId: Payment["id"]): Promise<boolean>;
isSetupAlready(): boolean;