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:
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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: {
|
||||
|
||||
@@ -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'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'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'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'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> {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
+2
-2
@@ -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;
|
||||
});
|
||||
|
||||
+8
-5
@@ -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
|
||||
|
||||
+8
-5
@@ -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
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Vendored
+2
-1
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user