From c6cb3423fa0023412ac69488d35b15580c22c5d9 Mon Sep 17 00:00:00 2001 From: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com> Date: Wed, 23 Apr 2025 19:14:44 +0530 Subject: [PATCH] feat: custom reply To Email (#20771) Co-authored-by: Joe Au-Yeung --- apps/web/lib/daily-webhook/getBooking.ts | 1 + .../web/lib/daily-webhook/getCalendarEvent.ts | 1 + apps/web/public/static/locales/en/common.json | 3 + .../test/lib/handleChildrenEventTypes.test.ts | 6 ++ .../templates/attendee-add-guests-email.ts | 4 +- .../attendee-awaiting-payment-email.ts | 4 +- .../templates/attendee-cancelled-email.ts | 3 +- .../attendee-cancelled-seat-email.ts | 4 +- ...ee-daily-video-download-recording-email.ts | 3 +- ...e-daily-video-download-transcript-email.ts | 3 +- .../templates/attendee-declined-email.ts | 3 +- .../attendee-location-change-email.ts | 4 +- .../templates/attendee-request-email.ts | 3 +- .../templates/attendee-rescheduled-email.ts | 3 +- .../templates/attendee-scheduled-email.ts | 3 +- .../templates/attendee-updated-email.ts | 3 +- .../templates/no-show-fee-charged-email.ts | 4 +- .../templates/organizer-rescheduled-email.ts | 3 +- packages/features/CalendarEventBuilder.ts | 2 + .../bookings/lib/getBookingToDelete.ts | 1 + .../bookings/lib/handleCancelBooking.ts | 1 + .../features/bookings/lib/handleNewBooking.ts | 1 + .../handleNewBooking/getEventTypesFromDB.ts | 1 + .../roundRobinManualReassignment.ts | 1 + .../ee/round-robin/roundRobinReassignment.ts | 1 + .../workflows/api/scheduleEmailReminders.ts | 10 ++- .../ee/workflows/lib/getWorkflowReminders.ts | 1 + .../lib/reminders/emailReminderManager.ts | 2 +- .../lib/reminders/smsReminderManager.ts | 1 + .../tabs/advanced/EventAdvancedTab.tsx | 52 ++++++++++++ packages/features/eventtypes/lib/types.ts | 1 + packages/lib/buildCalEventFromBooking.ts | 2 + packages/lib/builders/CalendarEvent/class.ts | 1 + packages/lib/defaultEvents.ts | 1 + packages/lib/getReplyToEmail.test.ts | 37 ++++++++ packages/lib/getReplyToEmail.ts | 8 ++ packages/lib/payment/getBooking.ts | 2 + packages/lib/server/eventTypeSelect.ts | 1 + packages/lib/server/repository/eventType.ts | 1 + .../lib/server/repository/workflow.test.ts | 84 +++++++++++++++++++ packages/lib/server/repository/workflow.ts | 48 ++++++++--- packages/lib/test/builder.ts | 1 + .../event-types/hooks/useEventTypeForm.ts | 3 + .../wrappers/EventAdvancedWebWrapper.tsx | 4 + .../migration.sql | 2 + packages/prisma/schema.prisma | 2 + .../loggedInViewer/connectAndJoin.handler.ts | 2 + .../viewer/bookings/addGuests.handler.ts | 1 + .../viewer/bookings/confirm.handler.ts | 2 + .../routers/viewer/bookings/get.handler.ts | 1 + .../bookings/requestReschedule.handler.ts | 1 + .../viewer/eventTypes/duplicate.handler.ts | 2 + .../trpc/server/routers/viewer/payments.tsx | 1 + .../workflows/activateEventType.handler.ts | 2 + .../server/routers/viewer/workflows/util.ts | 2 + packages/types/Calendar.d.ts | 1 + 56 files changed, 315 insertions(+), 30 deletions(-) create mode 100644 packages/lib/getReplyToEmail.test.ts create mode 100644 packages/lib/getReplyToEmail.ts create mode 100644 packages/lib/server/repository/workflow.test.ts create mode 100644 packages/prisma/migrations/20250418155253_custom_reply_to_email/migration.sql diff --git a/apps/web/lib/daily-webhook/getBooking.ts b/apps/web/lib/daily-webhook/getBooking.ts index f2c7c06b4c..5fcc997ab1 100644 --- a/apps/web/lib/daily-webhook/getBooking.ts +++ b/apps/web/lib/daily-webhook/getBooking.ts @@ -22,6 +22,7 @@ export const getBooking = async (bookingId: number) => { teamId: true, parentId: true, canSendCalVideoTranscriptionEmails: true, + customReplyToEmail: true, }, }, user: { diff --git a/apps/web/lib/daily-webhook/getCalendarEvent.ts b/apps/web/lib/daily-webhook/getCalendarEvent.ts index e3f76b0e78..f28682a179 100644 --- a/apps/web/lib/daily-webhook/getCalendarEvent.ts +++ b/apps/web/lib/daily-webhook/getCalendarEvent.ts @@ -34,6 +34,7 @@ export const getCalendarEvent = async (booking: getBookingResponse) => { }, attendees: attendeesList, uid: booking.uid, + customReplyToEmail: booking.eventType?.customReplyToEmail, }; return Promise.resolve(evt); diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 11bc98d2dd..09338c06a2 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -3141,6 +3141,9 @@ "meeting_not_joined_question": "didn't join the meeting?", "reschedule_cta_short": "Reschedule here.", "you_and_conjunction": "You &", + "select_verified_email": "Select a verified email", + "custom_reply_to_email_title": "Custom 'Reply-To' email", + "custom_reply_to_email_description": "Use a different email address as the replyTo for confirmation emails instead of the organizer's email", "email_survey_triggered_by_workflow": "This survey was triggered by a Workflow in Cal.", "sms_workflow_consent": "By entering your phone number you consent to receive SMS messages for this event. SMS rates may apply.", "sms_opt_out_message": "Text STOP to opt-out of SMS messages", diff --git a/apps/web/test/lib/handleChildrenEventTypes.test.ts b/apps/web/test/lib/handleChildrenEventTypes.test.ts index 3edfde66d7..5efc20fc69 100644 --- a/apps/web/test/lib/handleChildrenEventTypes.test.ts +++ b/apps/web/test/lib/handleChildrenEventTypes.test.ts @@ -142,6 +142,7 @@ describe("handleChildrenEventTypes", () => { durationLimits: undefined, recurringEvent: undefined, eventTypeColor: undefined, + customReplyToEmail: undefined, userId: 4, rrSegmentQueryValue: undefined, assignRRMembersUsingSegment: false, @@ -192,6 +193,7 @@ describe("handleChildrenEventTypes", () => { ...rest, assignRRMembersUsingSegment: undefined, useEventLevelSelectedCalendars: undefined, + customReplyToEmail: undefined, rrSegmentQueryValue: undefined, locations: [], scheduleId: null, @@ -299,6 +301,7 @@ describe("handleChildrenEventTypes", () => { durationLimits: undefined, recurringEvent: undefined, eventTypeColor: undefined, + customReplyToEmail: undefined, instantMeetingScheduleId: undefined, lockTimeZoneToggleOnBookingPage: false, requiresBookerEmailVerification: false, @@ -354,6 +357,7 @@ describe("handleChildrenEventTypes", () => { assignRRMembersUsingSegment: undefined, useEventLevelSelectedCalendars: undefined, rrSegmentQueryValue: undefined, + customReplyToEmail: undefined, locations: [], hashedLink: { deleteMany: {}, @@ -444,6 +448,7 @@ describe("handleChildrenEventTypes", () => { durationLimits: undefined, recurringEvent: undefined, eventTypeColor: undefined, + customReplyToEmail: undefined, instantMeetingScheduleId: undefined, locations: [], lockTimeZoneToggleOnBookingPage: false, @@ -470,6 +475,7 @@ describe("handleChildrenEventTypes", () => { locations: [], assignRRMembersUsingSegment: undefined, useEventLevelSelectedCalendars: undefined, + customReplyToEmail: undefined, rrSegmentQueryValue: undefined, lockTimeZoneToggleOnBookingPage: false, requiresBookerEmailVerification: false, diff --git a/packages/emails/templates/attendee-add-guests-email.ts b/packages/emails/templates/attendee-add-guests-email.ts index 4281743630..cb1b9dddf2 100644 --- a/packages/emails/templates/attendee-add-guests-email.ts +++ b/packages/emails/templates/attendee-add-guests-email.ts @@ -1,3 +1,5 @@ +import { getReplyToEmail } from "@calcom/lib/getReplyToEmail"; + import { renderEmail } from "../"; import generateIcsFile, { GenerateIcsRole } from "../lib/generateIcsFile"; import AttendeeScheduledEmail from "./attendee-scheduled-email"; @@ -12,7 +14,7 @@ export default class AttendeeAddGuestsEmail extends AttendeeScheduledEmail { }), to: `${this.attendee.name} <${this.attendee.email}>`, from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`, - replyTo: this.calEvent.organizer.email, + replyTo: getReplyToEmail(this.calEvent), subject: `${this.t("guests_added_event_type_subject", { eventType: this.calEvent.type, name: this.calEvent.team?.name || this.calEvent.organizer.name, diff --git a/packages/emails/templates/attendee-awaiting-payment-email.ts b/packages/emails/templates/attendee-awaiting-payment-email.ts index 24eef1262c..2470530d40 100644 --- a/packages/emails/templates/attendee-awaiting-payment-email.ts +++ b/packages/emails/templates/attendee-awaiting-payment-email.ts @@ -1,3 +1,5 @@ +import { getReplyToEmail } from "@calcom/lib/getReplyToEmail"; + import { renderEmail } from "../"; import AttendeeScheduledEmail from "./attendee-scheduled-email"; @@ -6,7 +8,7 @@ export default class AttendeeAwaitingPaymentEmail extends AttendeeScheduledEmail return { to: `${this.attendee.name} <${this.attendee.email}>`, from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`, - replyTo: this.calEvent.organizer.email, + replyTo: getReplyToEmail(this.calEvent), subject: `${this.attendee.language.translate("complete_your_booking_subject", { title: this.calEvent.title, date: this.getFormattedDate(), diff --git a/packages/emails/templates/attendee-cancelled-email.ts b/packages/emails/templates/attendee-cancelled-email.ts index b2243db7db..42bcaed2b2 100644 --- a/packages/emails/templates/attendee-cancelled-email.ts +++ b/packages/emails/templates/attendee-cancelled-email.ts @@ -1,3 +1,4 @@ +import { getReplyToEmail } from "@calcom/lib/getReplyToEmail"; import type { CalendarEvent, Person } from "@calcom/types/Calendar"; import { renderEmail } from "../"; @@ -14,7 +15,7 @@ export default class AttendeeCancelledEmail extends AttendeeScheduledEmail { }), to: `${this.attendee.name} <${this.attendee.email}>`, from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`, - replyTo: this.calEvent.organizer.email, + replyTo: getReplyToEmail(this.calEvent), subject: `${this.t("event_cancelled_subject", { title: this.calEvent.title, date: this.getFormattedDate(), diff --git a/packages/emails/templates/attendee-cancelled-seat-email.ts b/packages/emails/templates/attendee-cancelled-seat-email.ts index 610274732e..6d0984f637 100644 --- a/packages/emails/templates/attendee-cancelled-seat-email.ts +++ b/packages/emails/templates/attendee-cancelled-seat-email.ts @@ -1,3 +1,5 @@ +import { getReplyToEmail } from "@calcom/lib/getReplyToEmail"; + import { renderEmail } from "../"; import AttendeeScheduledEmail from "./attendee-scheduled-email"; @@ -6,7 +8,7 @@ export default class AttendeeCancelledSeatEmail extends AttendeeScheduledEmail { return { to: `${this.attendee.name} <${this.attendee.email}>`, from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`, - replyTo: this.calEvent.organizer.email, + replyTo: getReplyToEmail(this.calEvent), subject: `${this.t("event_no_longer_attending_subject", { title: this.calEvent.title, date: this.getFormattedDate(), diff --git a/packages/emails/templates/attendee-daily-video-download-recording-email.ts b/packages/emails/templates/attendee-daily-video-download-recording-email.ts index ddfc108d8b..af76e449ac 100644 --- a/packages/emails/templates/attendee-daily-video-download-recording-email.ts +++ b/packages/emails/templates/attendee-daily-video-download-recording-email.ts @@ -1,6 +1,7 @@ // TODO: We should find a way to keep App specific email templates within the App itself import type { TFunction } from "i18next"; +import { getReplyToEmail } from "@calcom/lib/getReplyToEmail"; import { TimeFormat } from "@calcom/lib/timeFormat"; import type { CalendarEvent, Person } from "@calcom/types/Calendar"; @@ -29,7 +30,7 @@ export default class AttendeeDailyVideoDownloadRecordingEmail extends BaseEmail ...this.calEvent.attendees .filter(({ email }) => email !== this.attendee.email) .map(({ email }) => email), - this.calEvent.organizer.email, + getReplyToEmail(this.calEvent), ], subject: `${this.t("download_recording_subject", { title: this.calEvent.title, diff --git a/packages/emails/templates/attendee-daily-video-download-transcript-email.ts b/packages/emails/templates/attendee-daily-video-download-transcript-email.ts index 4fb52a1b25..e2ffed8c43 100644 --- a/packages/emails/templates/attendee-daily-video-download-transcript-email.ts +++ b/packages/emails/templates/attendee-daily-video-download-transcript-email.ts @@ -1,5 +1,6 @@ import type { TFunction } from "i18next"; +import { getReplyToEmail } from "@calcom/lib/getReplyToEmail"; import { TimeFormat } from "@calcom/lib/timeFormat"; import type { CalendarEvent, Person } from "@calcom/types/Calendar"; @@ -40,7 +41,7 @@ export default class AttendeeDailyVideoDownloadTranscriptEmail extends BaseEmail ...this.calEvent.attendees .filter(({ email }) => email !== this.attendee.email) .map(({ email }) => email), - this.calEvent.organizer.email, + getReplyToEmail(this.calEvent), ], subject: `${this.t("download_transcript_email_subject", { title: this.calEvent.title, diff --git a/packages/emails/templates/attendee-declined-email.ts b/packages/emails/templates/attendee-declined-email.ts index 90f1700c6d..53a9e7e3de 100644 --- a/packages/emails/templates/attendee-declined-email.ts +++ b/packages/emails/templates/attendee-declined-email.ts @@ -1,3 +1,4 @@ +import { getReplyToEmail } from "@calcom/lib/getReplyToEmail"; import type { CalendarEvent, Person } from "@calcom/types/Calendar"; import { renderEmail } from "../"; @@ -8,7 +9,7 @@ export default class AttendeeDeclinedEmail extends AttendeeScheduledEmail { return { to: `${this.attendee.name} <${this.attendee.email}>`, from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`, - replyTo: this.calEvent.organizer.email, + replyTo: getReplyToEmail(this.calEvent), subject: `${this.t("event_declined_subject", { title: this.calEvent.title, date: this.getFormattedDate(), diff --git a/packages/emails/templates/attendee-location-change-email.ts b/packages/emails/templates/attendee-location-change-email.ts index 996410738f..fb6fa70739 100644 --- a/packages/emails/templates/attendee-location-change-email.ts +++ b/packages/emails/templates/attendee-location-change-email.ts @@ -1,3 +1,5 @@ +import { getReplyToEmail } from "@calcom/lib/getReplyToEmail"; + import { renderEmail } from "../"; import generateIcsFile, { GenerateIcsRole } from "../lib/generateIcsFile"; import AttendeeScheduledEmail from "./attendee-scheduled-email"; @@ -12,7 +14,7 @@ export default class AttendeeLocationChangeEmail extends AttendeeScheduledEmail }), to: `${this.attendee.name} <${this.attendee.email}>`, from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`, - replyTo: this.calEvent.organizer.email, + replyTo: getReplyToEmail(this.calEvent), subject: `${this.t("location_changed_event_type_subject", { eventType: this.calEvent.type, name: this.calEvent.team?.name || this.calEvent.organizer.name, diff --git a/packages/emails/templates/attendee-request-email.ts b/packages/emails/templates/attendee-request-email.ts index 70df950a3e..ba7781a4e1 100644 --- a/packages/emails/templates/attendee-request-email.ts +++ b/packages/emails/templates/attendee-request-email.ts @@ -1,4 +1,5 @@ import { EMAIL_FROM_NAME } from "@calcom/lib/constants"; +import { getReplyToEmail } from "@calcom/lib/getReplyToEmail"; import type { CalendarEvent, Person } from "@calcom/types/Calendar"; import { renderEmail } from "../"; @@ -15,7 +16,7 @@ export default class AttendeeRequestEmail extends AttendeeScheduledEmail { ...this.calEvent.attendees .filter(({ email }) => email !== this.attendee.email) .map(({ email }) => email), - this.calEvent.organizer.email, + getReplyToEmail(this.calEvent), ], subject: `${this.calEvent.attendees[0].language.translate("booking_submitted_subject", { title: this.calEvent.title, diff --git a/packages/emails/templates/attendee-rescheduled-email.ts b/packages/emails/templates/attendee-rescheduled-email.ts index c6c2d84a18..43f16a4a17 100644 --- a/packages/emails/templates/attendee-rescheduled-email.ts +++ b/packages/emails/templates/attendee-rescheduled-email.ts @@ -1,3 +1,4 @@ +import { getReplyToEmail } from "@calcom/lib/getReplyToEmail"; import type { CalendarEvent, Person } from "@calcom/types/Calendar"; import { renderEmail } from "../"; @@ -18,7 +19,7 @@ export default class AttendeeRescheduledEmail extends AttendeeScheduledEmail { ...this.calEvent.attendees .filter(({ email }) => email !== this.attendee.email) .map(({ email }) => email), - this.calEvent.organizer.email, + getReplyToEmail(this.calEvent), ], subject: `${this.attendee.language.translate("event_type_has_been_rescheduled_on_time_date", { title: this.calEvent.title, diff --git a/packages/emails/templates/attendee-scheduled-email.ts b/packages/emails/templates/attendee-scheduled-email.ts index 3f52fef04f..67cdbae14f 100644 --- a/packages/emails/templates/attendee-scheduled-email.ts +++ b/packages/emails/templates/attendee-scheduled-email.ts @@ -2,6 +2,7 @@ import type { TFunction } from "i18next"; import { default as cloneDeep } from "lodash/cloneDeep"; import { getRichDescription } from "@calcom/lib/CalEventParser"; +import { getReplyToEmail } from "@calcom/lib/getReplyToEmail"; import { TimeFormat } from "@calcom/lib/timeFormat"; import type { CalendarEvent, Person } from "@calcom/types/Calendar"; @@ -43,7 +44,7 @@ export default class AttendeeScheduledEmail extends BaseEmail { ...this.calEvent.attendees .filter(({ email }) => email !== this.attendee.email) .map(({ email }) => email), - this.calEvent.organizer.email, + getReplyToEmail(this.calEvent), ], subject: `${this.calEvent.title}`, html: await this.getHtml(clonedCalEvent, this.attendee), diff --git a/packages/emails/templates/attendee-updated-email.ts b/packages/emails/templates/attendee-updated-email.ts index 42e98121a3..6e5133b5af 100644 --- a/packages/emails/templates/attendee-updated-email.ts +++ b/packages/emails/templates/attendee-updated-email.ts @@ -1,3 +1,4 @@ +import { getReplyToEmail } from "@calcom/lib/getReplyToEmail"; import type { CalendarEvent, Person } from "@calcom/types/Calendar"; import { renderEmail } from "../"; @@ -18,7 +19,7 @@ export default class AttendeeUpdatedEmail extends AttendeeScheduledEmail { ...this.calEvent.attendees .filter(({ email }) => email !== this.attendee.email) .map(({ email }) => email), - this.calEvent.organizer.email, + getReplyToEmail(this.calEvent), ], subject: `${this.attendee.language.translate("event_type_has_been_updated", { title: this.calEvent.title, diff --git a/packages/emails/templates/no-show-fee-charged-email.ts b/packages/emails/templates/no-show-fee-charged-email.ts index 3ede12d719..f75c5eade9 100644 --- a/packages/emails/templates/no-show-fee-charged-email.ts +++ b/packages/emails/templates/no-show-fee-charged-email.ts @@ -1,3 +1,5 @@ +import { getReplyToEmail } from "@calcom/lib/getReplyToEmail"; + import { renderEmail } from "../"; import AttendeeScheduledEmail from "./attendee-scheduled-email"; @@ -7,7 +9,7 @@ export default class NoShowFeeChargedEmail extends AttendeeScheduledEmail { return { to: `${this.attendee.name} <${this.attendee.email}>`, from: `${this.calEvent.organizer.name} <${this.getMailerOptions().from}>`, - replyTo: this.calEvent.organizer.email, + replyTo: getReplyToEmail(this.calEvent), subject: `${this.attendee.language.translate("no_show_fee_charged_email_subject", { title: this.calEvent.title, date: this.getFormattedDate(), diff --git a/packages/emails/templates/organizer-rescheduled-email.ts b/packages/emails/templates/organizer-rescheduled-email.ts index 0a7678a7ef..9b96031123 100644 --- a/packages/emails/templates/organizer-rescheduled-email.ts +++ b/packages/emails/templates/organizer-rescheduled-email.ts @@ -1,4 +1,5 @@ import { EMAIL_FROM_NAME } from "@calcom/lib/constants"; +import { getReplyToEmail } from "@calcom/lib/getReplyToEmail"; import type { CalendarEvent, Person } from "@calcom/types/Calendar"; import { renderEmail } from "../"; @@ -17,7 +18,7 @@ export default class OrganizerRescheduledEmail extends OrganizerScheduledEmail { }), from: `${EMAIL_FROM_NAME} <${this.getMailerOptions().from}>`, to: toAddresses.join(","), - replyTo: [...this.calEvent.attendees.map(({ email }) => email)], + replyTo: [...this.calEvent.attendees.map(({ email }) => email), getReplyToEmail(this.calEvent)], subject: `${this.calEvent.organizer.language.translate("event_type_has_been_rescheduled_on_time_date", { title: this.calEvent.title, date: this.getFormattedDate(), diff --git a/packages/features/CalendarEventBuilder.ts b/packages/features/CalendarEventBuilder.ts index bef812ac33..447664775f 100644 --- a/packages/features/CalendarEventBuilder.ts +++ b/packages/features/CalendarEventBuilder.ts @@ -51,6 +51,7 @@ export class CalendarEventBuilder { seatsPerTimeSlot?: number | null; seatsShowAttendees?: boolean | null; seatsShowAvailabilityCount?: boolean | null; + customReplyToEmail?: string | null; }) { this.event = { ...this.event, @@ -64,6 +65,7 @@ export class CalendarEventBuilder { // if seats are not enabled we should default true seatsShowAttendees: eventType.seatsPerTimeSlot ? eventType.seatsShowAttendees : true, seatsShowAvailabilityCount: eventType.seatsPerTimeSlot ? eventType.seatsShowAvailabilityCount : true, + customReplyToEmail: eventType.customReplyToEmail, }; return this; } diff --git a/packages/features/bookings/lib/getBookingToDelete.ts b/packages/features/bookings/lib/getBookingToDelete.ts index 3b419f4716..ea5d1c1422 100644 --- a/packages/features/bookings/lib/getBookingToDelete.ts +++ b/packages/features/bookings/lib/getBookingToDelete.ts @@ -74,6 +74,7 @@ export async function getBookingToDelete(id: number | undefined, uid: string | u seatsShowAttendees: true, metadata: true, schedulingType: true, + customReplyToEmail: true, hosts: { select: { user: true, diff --git a/packages/features/bookings/lib/handleCancelBooking.ts b/packages/features/bookings/lib/handleCancelBooking.ts index 1171aa6d83..69fb31c7c1 100644 --- a/packages/features/bookings/lib/handleCancelBooking.ts +++ b/packages/features/bookings/lib/handleCancelBooking.ts @@ -266,6 +266,7 @@ async function handler(input: CancelBookingInput) { platformRescheduleUrl, platformCancelUrl, platformBookingUrl, + customReplyToEmail: bookingToDelete.eventType?.customReplyToEmail, }; const dataForWebhooks = { evt, webhooks, eventTypeInfo }; diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index 44b6825c42..819b0ba679 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -1024,6 +1024,7 @@ async function handler( // if seats are not enabled we should default true seatsShowAttendees: eventType.seatsPerTimeSlot ? eventType.seatsShowAttendees : true, seatsShowAvailabilityCount: eventType.seatsPerTimeSlot ? eventType.seatsShowAvailabilityCount : true, + customReplyToEmail: eventType.customReplyToEmail, }) .withOrganizer({ id: organizerUser.id, diff --git a/packages/features/bookings/lib/handleNewBooking/getEventTypesFromDB.ts b/packages/features/bookings/lib/handleNewBooking/getEventTypesFromDB.ts index 96526f16de..642a4e1600 100644 --- a/packages/features/bookings/lib/handleNewBooking/getEventTypesFromDB.ts +++ b/packages/features/bookings/lib/handleNewBooking/getEventTypesFromDB.ts @@ -80,6 +80,7 @@ export const getEventTypesFromDB = async (eventTypeId: number) => { assignAllTeamMembers: true, isRRWeightsEnabled: true, beforeEventBuffer: true, + customReplyToEmail: true, afterEventBuffer: true, parentId: true, parent: { diff --git a/packages/features/ee/round-robin/roundRobinManualReassignment.ts b/packages/features/ee/round-robin/roundRobinManualReassignment.ts index 24e01384e0..381837e21d 100644 --- a/packages/features/ee/round-robin/roundRobinManualReassignment.ts +++ b/packages/features/ee/round-robin/roundRobinManualReassignment.ts @@ -271,6 +271,7 @@ export const roundRobinManualReassignment = async ({ bookingFields: eventType.bookingFields ?? null, booking, }), + customReplyToEmail: eventType?.customReplyToEmail, location: bookingLocation, ...(platformClientParams ? platformClientParams : {}), }; diff --git a/packages/features/ee/round-robin/roundRobinReassignment.ts b/packages/features/ee/round-robin/roundRobinReassignment.ts index d72b8a297f..c581baa22d 100644 --- a/packages/features/ee/round-robin/roundRobinReassignment.ts +++ b/packages/features/ee/round-robin/roundRobinReassignment.ts @@ -318,6 +318,7 @@ export const roundRobinReassignment = async ({ bookingFields: eventType?.bookingFields ?? null, booking, }), + customReplyToEmail: eventType?.customReplyToEmail, location: bookingLocation, ...(platformClientParams ? platformClientParams : {}), }; diff --git a/packages/features/ee/workflows/api/scheduleEmailReminders.ts b/packages/features/ee/workflows/api/scheduleEmailReminders.ts index 6f68ae009b..16b38241a0 100644 --- a/packages/features/ee/workflows/api/scheduleEmailReminders.ts +++ b/packages/features/ee/workflows/api/scheduleEmailReminders.ts @@ -332,7 +332,10 @@ export async function handler(req: NextRequest) { html: emailContent.emailBody, batchId: batchId, sendAt: dayjs(reminder.scheduledDate).unix(), - replyTo: reminder.booking?.userPrimaryEmail ?? reminder.booking.user?.email, + replyTo: + reminder.booking?.eventType?.customReplyToEmail ?? + reminder.booking?.userPrimaryEmail ?? + reminder.booking.user?.email, attachments: reminder.workflowStep.includeCalendarEvent ? [ { @@ -407,7 +410,10 @@ export async function handler(req: NextRequest) { html: emailContent.emailBody, batchId: batchId, sendAt: dayjs(reminder.scheduledDate).unix(), - replyTo: reminder.booking?.userPrimaryEmail ?? reminder.booking.user?.email, + replyTo: + reminder.booking?.eventType?.customReplyToEmail || + reminder.booking?.userPrimaryEmail || + reminder.booking.user?.email, sender: reminder.workflowStep?.sender, }) ); diff --git a/packages/features/ee/workflows/lib/getWorkflowReminders.ts b/packages/features/ee/workflows/lib/getWorkflowReminders.ts index e41ca972bc..ded5b597d8 100644 --- a/packages/features/ee/workflows/lib/getWorkflowReminders.ts +++ b/packages/features/ee/workflows/lib/getWorkflowReminders.ts @@ -190,6 +190,7 @@ export const select = Prisma.validator()({ hideBranding: true, }, }, + customReplyToEmail: true, }, }, }, diff --git a/packages/features/ee/workflows/lib/reminders/emailReminderManager.ts b/packages/features/ee/workflows/lib/reminders/emailReminderManager.ts index dd96f9715d..3ee9609b7b 100644 --- a/packages/features/ee/workflows/lib/reminders/emailReminderManager.ts +++ b/packages/features/ee/workflows/lib/reminders/emailReminderManager.ts @@ -251,7 +251,7 @@ export const scheduleEmailReminder = async (args: scheduleEmailReminderArgs) => return { subject: emailContent.emailSubject, html: emailContent.emailBody, - replyTo: evt.organizer.email, + replyTo: evt?.eventType?.customReplyToEmail || evt.organizer.email, attachments, sender, }; diff --git a/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts b/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts index e94dbe4734..5598894fcf 100644 --- a/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts +++ b/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts @@ -53,6 +53,7 @@ export type BookingInfo = { eventType?: { slug: string; recurringEvent?: RecurringEvent | null; + customReplyToEmail?: string | null; }; startTime: string; endTime: string; diff --git a/packages/features/eventtypes/components/tabs/advanced/EventAdvancedTab.tsx b/packages/features/eventtypes/components/tabs/advanced/EventAdvancedTab.tsx index 7385b2c336..0663bd525c 100644 --- a/packages/features/eventtypes/components/tabs/advanced/EventAdvancedTab.tsx +++ b/packages/features/eventtypes/components/tabs/advanced/EventAdvancedTab.tsx @@ -99,6 +99,7 @@ export type EventAdvancedTabCustomClassNames = { warningText?: string; }; roundRobinReschedule?: SettingsToggleClassNames; + customReplyToEmail?: SettingsToggleClassNames; emailNotifications?: EmailNotificationToggleCustomClassNames; }; @@ -120,6 +121,7 @@ export type EventAdvancedTabProps = EventAdvancedBaseProps & { error: unknown; }; showBookerLayoutSelector: boolean; + verifiedEmails?: string[]; }; type CalendarSettingsProps = { @@ -399,6 +401,7 @@ export const EventAdvancedTab = ({ showToast, showBookerLayoutSelector, customClassNames, + verifiedEmails, }: EventAdvancedTabProps) => { const isPlatform = useIsPlatform(); const platformContext = useAtomsContext(); @@ -488,6 +491,7 @@ export const EventAdvancedTab = ({ const lockTimeZoneToggleOnBookingPageLocked = shouldLockDisableProps("lockTimeZoneToggleOnBookingPage"); const multiplePrivateLinksLocked = shouldLockDisableProps("multiplePrivateLinks"); const reschedulingPastBookingsLocked = shouldLockDisableProps("allowReschedulingPastBookings"); + const customReplyToEmailLocked = shouldLockDisableProps("customReplyToEmail"); const disableCancellingLocked = shouldLockDisableProps("disableCancelling"); const disableReschedulingLocked = shouldLockDisableProps("disableRescheduling"); @@ -506,6 +510,8 @@ export const EventAdvancedTab = ({ const [isEventTypeColorChecked, setIsEventTypeColorChecked] = useState(!!eventType.eventTypeColor); + const customReplyToEmail = formMethods.watch("customReplyToEmail"); + const [eventTypeColorState, setEventTypeColorState] = useState( eventType.eventTypeColor || { lightEventTypeColor: DEFAULT_LIGHT_BRAND_COLOR, @@ -1000,6 +1006,52 @@ export const EventAdvancedTab = ({ /> )} /> + {!isPlatform && ( + <> + ( + <> + { + onChange( + e + ? customReplyToEmail || eventType.customReplyToEmail || verifiedEmails?.[0] || null + : null + ); + }}> +
+ onChange(option?.value || null)} + options={verifiedEmails?.map((email) => ({ label: email, value: email })) || []} + /> +
+
+ + )} + /> + + )} ( diff --git a/packages/features/eventtypes/lib/types.ts b/packages/features/eventtypes/lib/types.ts index cc51637e4a..5f98411b6b 100644 --- a/packages/features/eventtypes/lib/types.ts +++ b/packages/features/eventtypes/lib/types.ts @@ -90,6 +90,7 @@ export type FormValues = { hideCalendarNotes: boolean; multiplePrivateLinks: string[] | undefined; eventTypeColor: z.infer; + customReplyToEmail: string | null; locations: EventLocation[]; aiPhoneCallConfig: PhoneCallConfig; customInputs: CustomInputParsed[]; diff --git a/packages/lib/buildCalEventFromBooking.ts b/packages/lib/buildCalEventFromBooking.ts index ff93104d8a..87f4154868 100644 --- a/packages/lib/buildCalEventFromBooking.ts +++ b/packages/lib/buildCalEventFromBooking.ts @@ -36,6 +36,7 @@ type EventType = { recurringEvent: Prisma.JsonValue | null; seatsPerTimeSlot: number | null; seatsShowAttendees: boolean | null; + customReplyToEmail: string | null; }; type Booking = { @@ -104,5 +105,6 @@ export const buildCalEventFromBooking = async ({ : [], seatsPerTimeSlot: booking.eventType?.seatsPerTimeSlot, seatsShowAttendees: booking.eventType?.seatsShowAttendees, + customReplyToEmail: booking.eventType?.customReplyToEmail, }; }; diff --git a/packages/lib/builders/CalendarEvent/class.ts b/packages/lib/builders/CalendarEvent/class.ts index 412d39e8ff..a4bd03c949 100644 --- a/packages/lib/builders/CalendarEvent/class.ts +++ b/packages/lib/builders/CalendarEvent/class.ts @@ -34,6 +34,7 @@ class CalendarEventClass implements CalendarEvent { additionalNotes?: string | null | undefined; recurrence?: string; iCalUID?: string | null; + customReplyToEmail?: string | null; constructor(initProps?: CalendarEvent) { // If more parameters are given we update this diff --git a/packages/lib/defaultEvents.ts b/packages/lib/defaultEvents.ts index d2ad16017a..6e41bee897 100644 --- a/packages/lib/defaultEvents.ts +++ b/packages/lib/defaultEvents.ts @@ -128,6 +128,7 @@ const commons = { maxLeadThreshold: null, useEventLevelSelectedCalendars: false, rrResetInterval: null, + customReplyToEmail: null, }; export const dynamicEvent = { diff --git a/packages/lib/getReplyToEmail.test.ts b/packages/lib/getReplyToEmail.test.ts new file mode 100644 index 0000000000..baf6672857 --- /dev/null +++ b/packages/lib/getReplyToEmail.test.ts @@ -0,0 +1,37 @@ +import type { TFunction } from "i18next"; +import { describe, expect, it } from "vitest"; + +import { getReplyToEmail } from "./getReplyToEmail"; + +describe("getReplyToEmail", () => { + it("returns the customReplyToEmail if it is set", () => { + const calEvent = { + customReplyToEmail: "customReplyToEmail@example.com", + organizer: { + name: "Organizer", + email: "organizer@example.com", + timeZone: "UTC", + language: { + locale: "en", + translate: ((key: string) => key) as TFunction, + }, + }, + }; + expect(getReplyToEmail(calEvent)).toBe("customReplyToEmail@example.com"); + }); + it("returns the organizer's email if customReplyToEmail is not set", () => { + const calEvent = { + customReplyToEmail: null, + organizer: { + name: "Organizer", + email: "organizer@example.com", + timeZone: "UTC", + language: { + locale: "en", + translate: ((key: string) => key) as TFunction, + }, + }, + }; + expect(getReplyToEmail(calEvent)).toBe("organizer@example.com"); + }); +}); diff --git a/packages/lib/getReplyToEmail.ts b/packages/lib/getReplyToEmail.ts new file mode 100644 index 0000000000..67b430e302 --- /dev/null +++ b/packages/lib/getReplyToEmail.ts @@ -0,0 +1,8 @@ +import type { CalendarEvent } from "@calcom/types/Calendar"; + +/** + * Returns the reply-to email address to use, with fallback to organizer's email + */ +export const getReplyToEmail = (calEvent: Pick) => { + return calEvent.customReplyToEmail || calEvent.organizer.email; +}; diff --git a/packages/lib/payment/getBooking.ts b/packages/lib/payment/getBooking.ts index 267bbd7c6e..92fad9676f 100644 --- a/packages/lib/payment/getBooking.ts +++ b/packages/lib/payment/getBooking.ts @@ -42,6 +42,7 @@ export async function getBooking(bookingId: number) { price: true, requiresConfirmation: true, metadata: true, + customReplyToEmail: true, title: true, teamId: true, parentId: true, @@ -170,6 +171,7 @@ export async function getBooking(bookingId: number) { uid: booking.uid, destinationCalendar: selectedDestinationCalendar ? [selectedDestinationCalendar] : [], recurringEvent: parseRecurringEvent(eventType?.recurringEvent), + customReplyToEmail: booking.eventType?.customReplyToEmail, }; return { diff --git a/packages/lib/server/eventTypeSelect.ts b/packages/lib/server/eventTypeSelect.ts index 15786101ba..23dc40c4ef 100644 --- a/packages/lib/server/eventTypeSelect.ts +++ b/packages/lib/server/eventTypeSelect.ts @@ -67,4 +67,5 @@ export const eventTypeSelect = Prisma.validator()({ assignRRMembersUsingSegment: true, maxLeadThreshold: true, useEventLevelSelectedCalendars: true, + customReplyToEmail: true, }); diff --git a/packages/lib/server/repository/eventType.ts b/packages/lib/server/repository/eventType.ts index 3259ddd261..e47914fb9f 100644 --- a/packages/lib/server/repository/eventType.ts +++ b/packages/lib/server/repository/eventType.ts @@ -526,6 +526,7 @@ export class EventTypeRepository { currency: true, bookingFields: true, useEventTypeDestinationCalendarEmail: true, + customReplyToEmail: true, owner: { select: { id: true, diff --git a/packages/lib/server/repository/workflow.test.ts b/packages/lib/server/repository/workflow.test.ts new file mode 100644 index 0000000000..1d7ae2af1a --- /dev/null +++ b/packages/lib/server/repository/workflow.test.ts @@ -0,0 +1,84 @@ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import prismaMock from "../../../../tests/libs/__mocks__/prismaMock"; + +import { describe, it, expect } from "vitest"; + +import { WorkflowRepository } from "./workflow"; + +describe("WorkflowRepository", () => { + describe("getVerifiedEmails", () => { + it("should return the email of the user", async () => { + prismaMock.secondaryEmail.findMany.mockResolvedValue([]); + prismaMock.verifiedEmail.findMany.mockResolvedValue([]); + + const emails = await WorkflowRepository.getVerifiedEmails({ + userEmail: "user@example.com", + userId: 1, + }); + expect(emails).toEqual(["user@example.com"]); + }); + + it("should return the email of the user and verified secondary emails", async () => { + prismaMock.verifiedEmail.findMany.mockResolvedValue([]); + prismaMock.secondaryEmail.findMany.mockResolvedValue([ + { + id: 101, + email: "secondary@example.com", + emailVerified: new Date(), + }, + ]); + + const emails = await WorkflowRepository.getVerifiedEmails({ + userEmail: "user@example.com", + userId: 1, + }); + expect(emails).toEqual(["user@example.com", "secondary@example.com"]); + }); + + it("should return the email of the user and verified secondary emails from the team", async () => { + prismaMock.verifiedEmail.findMany.mockResolvedValue([]); + prismaMock.secondaryEmail.findMany.mockResolvedValue([ + { + id: 101, + email: "secondary@example.com", + emailVerified: new Date(), + }, + ]); + + prismaMock.user.findMany.mockResolvedValue([ + { + id: 1, + email: "organizer@example.com", + }, + { + id: 2, + email: "teammember1@example.com", + secondaryEmails: [], + }, + { + id: 3, + email: "teammember2@example.com", + secondaryEmails: [ + { + email: "secondary2@example.com", + emailVerified: new Date(), + }, + ], + }, + ]); + + const emails = await WorkflowRepository.getVerifiedEmails({ + userEmail: "user@example.com", + userId: 1, + teamId: 1, + }); + expect(emails).toEqual([ + "user@example.com", + "secondary@example.com", + "teammember1@example.com", + "teammember2@example.com", + "secondary2@example.com", + ]); + }); + }); +}); diff --git a/packages/lib/server/repository/workflow.ts b/packages/lib/server/repository/workflow.ts index 4f1191a0fd..0dea44ab8a 100644 --- a/packages/lib/server/repository/workflow.ts +++ b/packages/lib/server/repository/workflow.ts @@ -139,36 +139,58 @@ export class WorkflowRepository { let verifiedEmails: string[] = [userEmail]; + const secondaryEmails = await prisma.secondaryEmail.findMany({ + where: { + userId, + emailVerified: { + not: null, + }, + }, + }); + verifiedEmails = verifiedEmails.concat(secondaryEmails.map((secondaryEmail) => secondaryEmail.email)); if (teamId) { - const team = await prisma.team.findFirst({ + const teamMembers = await prisma.user.findMany({ where: { - id: teamId, + teams: { + some: { + teamId, + }, + }, }, select: { - members: { - include: { - user: { - select: { - id: true, - email: true, - }, + id: true, + email: true, + secondaryEmails: { + where: { + emailVerified: { + not: null, }, }, + select: { + email: true, + }, }, }, }); - - if (!team) { + if (!teamMembers.length) { throw new Error("Team not found"); } - const isTeamMember = team.members.some((member) => member.userId === userId); + const isTeamMember = teamMembers.some((member) => member.id === userId); if (!isTeamMember) { throw new Error("You are not a member of this team"); } - verifiedEmails = verifiedEmails.concat(team.members.map((member) => member.user.email)); + teamMembers.forEach((member) => { + if (member.id === userId) { + return; + } + verifiedEmails.push(member.email); + member.secondaryEmails.forEach((secondaryEmail) => { + verifiedEmails.push(secondaryEmail.email); + }); + }); } const emails = ( diff --git a/packages/lib/test/builder.ts b/packages/lib/test/builder.ts index 4a5546abc6..6de5be6615 100644 --- a/packages/lib/test/builder.ts +++ b/packages/lib/test/builder.ts @@ -150,6 +150,7 @@ export const buildEventType = (eventType?: Partial): EventType => { autoTranslateDescriptionEnabled: false, useEventLevelSelectedCalendars: false, allowReschedulingPastBookings: false, + customReplyToEmail: null, ...eventType, }; }; diff --git a/packages/platform/atoms/event-types/hooks/useEventTypeForm.ts b/packages/platform/atoms/event-types/hooks/useEventTypeForm.ts index ede162fe88..402ac35e7d 100644 --- a/packages/platform/atoms/event-types/hooks/useEventTypeForm.ts +++ b/packages/platform/atoms/event-types/hooks/useEventTypeForm.ts @@ -127,6 +127,7 @@ export const useEventTypeForm = ({ isRRWeightsEnabled: eventType.isRRWeightsEnabled, maxLeadThreshold: eventType.maxLeadThreshold, useEventLevelSelectedCalendars: eventType.useEventLevelSelectedCalendars, + customReplyToEmail: eventType.customReplyToEmail || null, }; }, [eventType, periodDates]); @@ -280,6 +281,7 @@ export const useEventTypeForm = ({ durationLimits, recurringEvent, eventTypeColor, + customReplyToEmail, locations, metadata, customInputs, @@ -357,6 +359,7 @@ export const useEventTypeForm = ({ onlyShowFirstAvailableSlot, durationLimits, eventTypeColor, + customReplyToEmail, seatsPerTimeSlot, seatsShowAttendees, seatsShowAvailabilityCount, diff --git a/packages/platform/atoms/event-types/wrappers/EventAdvancedWebWrapper.tsx b/packages/platform/atoms/event-types/wrappers/EventAdvancedWebWrapper.tsx index 699d3c7e18..de2dfb1f29 100644 --- a/packages/platform/atoms/event-types/wrappers/EventAdvancedWebWrapper.tsx +++ b/packages/platform/atoms/event-types/wrappers/EventAdvancedWebWrapper.tsx @@ -4,6 +4,9 @@ import { trpc } from "@calcom/trpc/react"; const EventAdvancedWebWrapper = ({ ...props }: EventAdvancedBaseProps) => { const connectedCalendarsQuery = trpc.viewer.calendars.connectedCalendars.useQuery(); + const { data: verifiedEmails } = trpc.viewer.workflows.getVerifiedEmails.useQuery({ + teamId: props.team?.id, + }); return ( { error: connectedCalendarsQuery.error, }} showBookerLayoutSelector={true} + verifiedEmails={verifiedEmails} /> ); }; diff --git a/packages/prisma/migrations/20250418155253_custom_reply_to_email/migration.sql b/packages/prisma/migrations/20250418155253_custom_reply_to_email/migration.sql new file mode 100644 index 0000000000..21dcf7e14f --- /dev/null +++ b/packages/prisma/migrations/20250418155253_custom_reply_to_email/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "EventType" ADD COLUMN "customReplyToEmail" TEXT; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index ce0c50878e..858f419681 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -165,6 +165,8 @@ model EventType { maxLeadThreshold Int? selectedCalendars SelectedCalendar[] allowReschedulingPastBookings Boolean @default(false) + /// @zod.custom(imports.emailSchema) + customReplyToEmail String? /// @zod.custom(imports.eventTypeColor) eventTypeColor Json? diff --git a/packages/trpc/server/routers/loggedInViewer/connectAndJoin.handler.ts b/packages/trpc/server/routers/loggedInViewer/connectAndJoin.handler.ts index 3c09078612..53aff15ad8 100644 --- a/packages/trpc/server/routers/loggedInViewer/connectAndJoin.handler.ts +++ b/packages/trpc/server/routers/loggedInViewer/connectAndJoin.handler.ts @@ -128,6 +128,7 @@ export const Handler = async ({ ctx, input }: Options) => { metadata: true, customInputs: true, parentId: true, + customReplyToEmail: true, team: { select: { id: true, @@ -208,6 +209,7 @@ export const Handler = async ({ ctx, input }: Options) => { requiresConfirmation: false, eventTypeId: eventType?.id, videoCallData, + customReplyToEmail: eventType?.customReplyToEmail, team: !!updatedBooking.eventType?.team ? { name: updatedBooking.eventType.team.name, diff --git a/packages/trpc/server/routers/viewer/bookings/addGuests.handler.ts b/packages/trpc/server/routers/viewer/bookings/addGuests.handler.ts index bb06ff6458..6a902425b2 100644 --- a/packages/trpc/server/routers/viewer/bookings/addGuests.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/addGuests.handler.ts @@ -144,6 +144,7 @@ export const addGuestsHandler = async ({ ctx, input }: AddGuestsOptions) => { : [], seatsPerTimeSlot: booking.eventType?.seatsPerTimeSlot, seatsShowAttendees: booking.eventType?.seatsShowAttendees, + customReplyToEmail: booking.eventType?.customReplyToEmail, }; if (videoCallReference) { diff --git a/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts b/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts index d93f7070d1..b3ee8293bd 100644 --- a/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts @@ -81,6 +81,7 @@ export const confirmHandler = async ({ ctx, input }: ConfirmOptions) => { price: true, bookingFields: true, disableGuests: true, + customReplyToEmail: true, metadata: true, locations: true, team: { @@ -226,6 +227,7 @@ export const confirmHandler = async ({ ctx, input }: ConfirmOptions) => { : [], requiresConfirmation: booking?.eventType?.requiresConfirmation ?? false, eventTypeId: booking.eventType?.id, + customReplyToEmail: booking.eventType?.customReplyToEmail, team: !!booking.eventType?.team ? { name: booking.eventType.team.name, diff --git a/packages/trpc/server/routers/viewer/bookings/get.handler.ts b/packages/trpc/server/routers/viewer/bookings/get.handler.ts index 55082026d0..6ca4177baf 100644 --- a/packages/trpc/server/routers/viewer/bookings/get.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/get.handler.ts @@ -101,6 +101,7 @@ export async function getBookings({ seatsShowAttendees: true, seatsShowAvailabilityCount: true, eventTypeColor: true, + customReplyToEmail: true, allowReschedulingPastBookings: true, disableCancelling: true, disableRescheduling: true, diff --git a/packages/trpc/server/routers/viewer/bookings/requestReschedule.handler.ts b/packages/trpc/server/routers/viewer/bookings/requestReschedule.handler.ts index 2c52b5721d..f7b7ed05df 100644 --- a/packages/trpc/server/routers/viewer/bookings/requestReschedule.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/requestReschedule.handler.ts @@ -202,6 +202,7 @@ export const requestRescheduleHandler = async ({ ctx, input }: RequestReschedule ), organizer, iCalUID: bookingToReschedule.iCalUID, + customReplyToEmail: bookingToReschedule.eventType?.customReplyToEmail, team: !!bookingToReschedule.eventType?.team ? { name: bookingToReschedule.eventType.team.name, diff --git a/packages/trpc/server/routers/viewer/eventTypes/duplicate.handler.ts b/packages/trpc/server/routers/viewer/eventTypes/duplicate.handler.ts index 6645292e8b..a06c256037 100644 --- a/packages/trpc/server/routers/viewer/eventTypes/duplicate.handler.ts +++ b/packages/trpc/server/routers/viewer/eventTypes/duplicate.handler.ts @@ -76,6 +76,7 @@ export const duplicateHandler = async ({ ctx, input }: DuplicateOptions) => { bookingLimits, durationLimits, eventTypeColor, + customReplyToEmail, metadata, workflows, hashedLink, @@ -115,6 +116,7 @@ export const duplicateHandler = async ({ ctx, input }: DuplicateOptions) => { bookingLimits: bookingLimits ?? undefined, durationLimits: durationLimits ?? undefined, eventTypeColor: eventTypeColor ?? undefined, + customReplyToEmail: customReplyToEmail ?? undefined, metadata: metadata === null ? Prisma.DbNull : metadata, bookingFields: eventType.bookingFields === null ? Prisma.DbNull : eventType.bookingFields, rrSegmentQueryValue: diff --git a/packages/trpc/server/routers/viewer/payments.tsx b/packages/trpc/server/routers/viewer/payments.tsx index eacb4535f0..1af0a40009 100644 --- a/packages/trpc/server/routers/viewer/payments.tsx +++ b/packages/trpc/server/routers/viewer/payments.tsx @@ -91,6 +91,7 @@ export const paymentsRouter = router({ currency: payment.currency, paymentOption: payment.paymentOption, }, + customReplyToEmail: booking.eventType?.customReplyToEmail, }; const paymentCredential = await prisma.credential.findFirst({ diff --git a/packages/trpc/server/routers/viewer/workflows/activateEventType.handler.ts b/packages/trpc/server/routers/viewer/workflows/activateEventType.handler.ts index e1511918b4..fc433c7483 100644 --- a/packages/trpc/server/routers/viewer/workflows/activateEventType.handler.ts +++ b/packages/trpc/server/routers/viewer/workflows/activateEventType.handler.ts @@ -205,6 +205,7 @@ export const activateEventTypeHandler = async ({ ctx, input }: ActivateEventType select: { schedulingType: true, slug: true, + customReplyToEmail: true, hosts: { select: { user: { @@ -259,6 +260,7 @@ export const activateEventTypeHandler = async ({ ctx, input }: ActivateEventType hosts: booking.eventType?.hosts, }, metadata: booking.metadata, + customReplyToEmail: booking.eventType?.customReplyToEmail, }; for (const step of eventTypeWorkflow.steps) { if ( diff --git a/packages/trpc/server/routers/viewer/workflows/util.ts b/packages/trpc/server/routers/viewer/workflows/util.ts index c00add228d..b0a3438f80 100644 --- a/packages/trpc/server/routers/viewer/workflows/util.ts +++ b/packages/trpc/server/routers/viewer/workflows/util.ts @@ -57,6 +57,7 @@ export const bookingSelect = { slug: true, id: true, schedulingType: true, + customReplyToEmail: true, hosts: { select: { user: { @@ -648,6 +649,7 @@ export async function scheduleBookingReminders( hosts: booking.eventType?.hosts, }, metadata: booking.metadata, + customReplyToEmail: booking.eventType?.customReplyToEmail, }; if ( step.action === WorkflowActions.EMAIL_HOST || diff --git a/packages/types/Calendar.d.ts b/packages/types/Calendar.d.ts index 726050bf5e..9853dd0c20 100644 --- a/packages/types/Calendar.d.ts +++ b/packages/types/Calendar.d.ts @@ -218,6 +218,7 @@ export interface CalendarEvent { oneTimePassword?: string | null; delegationCredentialId?: string | null; domainWideDelegationCredentialId?: string | null; + customReplyToEmail?: string | null; } export interface EntryPoint {