diff --git a/apps/web/components/eventtype/EventAdvancedTab.tsx b/apps/web/components/eventtype/EventAdvancedTab.tsx index f033636b18..b4b1ed366a 100644 --- a/apps/web/components/eventtype/EventAdvancedTab.tsx +++ b/apps/web/components/eventtype/EventAdvancedTab.tsx @@ -30,6 +30,7 @@ import { Label, SettingsToggle, showToast, + Switch, TextField, Tooltip, } from "@calcom/ui"; @@ -44,10 +45,12 @@ export const EventAdvancedTab = ({ eventType, team }: Pick(); const { t } = useLocale(); - const [showEventNameTip, setShowEventNameTip] = useState(false); const [hashedLinkVisible, setHashedLinkVisible] = useState(!!formMethods.getValues("hashedLink")); const [redirectUrlVisible, setRedirectUrlVisible] = useState(!!formMethods.getValues("successRedirectUrl")); + const [useEventTypeDestinationCalendarEmail, setUseEventTypeDestinationCalendarEmail] = useState( + formMethods.getValues("useEventTypeDestinationCalendarEmail") + ); const [hashedUrl, setHashedUrl] = useState(eventType.hashedLink?.link); const bookingFields: Prisma.JsonObject = {}; @@ -116,6 +119,8 @@ export const EventAdvancedTab = ({ eventType, team }: Pick setShowEventNameTip(false); + const displayDestinationCalendarSelector = + !!connectedCalendarsQuery.data?.connectedCalendars.length && !team; return (
@@ -125,7 +130,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick - {!!connectedCalendarsQuery.data?.connectedCalendars.length && !team && ( + {displayDestinationCalendarSelector && (
@@ -171,6 +176,18 @@ export const EventAdvancedTab = ({ eventType, team }: Pick
+ {displayDestinationCalendarSelector && ( +
+ { + setUseEventTypeDestinationCalendarEmail(val); + formMethods.setValue("useEventTypeDestinationCalendarEmail", val); + }} + /> +
+ )}
diff --git a/apps/web/modules/bookings/views/bookings-single-view.getServerSideProps.tsx b/apps/web/modules/bookings/views/bookings-single-view.getServerSideProps.tsx index 4c476f1bab..9369ef6004 100644 --- a/apps/web/modules/bookings/views/bookings-single-view.getServerSideProps.tsx +++ b/apps/web/modules/bookings/views/bookings-single-view.getServerSideProps.tsx @@ -82,6 +82,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { cancellationReason: true, responses: true, rejectionReason: true, + userPrimaryEmail: true, user: { select: { id: true, diff --git a/apps/web/modules/bookings/views/bookings-single-view.tsx b/apps/web/modules/bookings/views/bookings-single-view.tsx index eb3ecfa145..dc492623d5 100644 --- a/apps/web/modules/bookings/views/bookings-single-view.tsx +++ b/apps/web/modules/bookings/views/bookings-single-view.tsx @@ -203,7 +203,7 @@ export default function Success(props: PageProps) { duration: calculatedDuration, organizer: { name: users[0].name || "Nameless", - email: users[0].email || "Email-less", + email: bookingInfo?.userPrimaryEmail || users[0].email || "Email-less", timeZone: users[0].timeZone, }, confirmed: !needsConfirmation, @@ -450,7 +450,9 @@ export default function Success(props: PageProps) { {t("Host")}
-

{bookingInfo.user.email}

+

+ {bookingInfo?.userPrimaryEmail ?? bookingInfo.user.email} +

)} {bookingInfo?.attendees.map((attendee) => ( diff --git a/apps/web/modules/event-types/views/event-types-single-view.tsx b/apps/web/modules/event-types/views/event-types-single-view.tsx index 6c01bc7b13..9945df548c 100644 --- a/apps/web/modules/event-types/views/event-types-single-view.tsx +++ b/apps/web/modules/event-types/views/event-types-single-view.tsx @@ -148,6 +148,7 @@ export type FormValues = { multipleDurationEnabled: boolean; users: EventTypeSetup["users"]; assignAllTeamMembers: boolean; + useEventTypeDestinationCalendarEmail: boolean; }; export type CustomInputParsed = typeof customInputSchema._output; @@ -298,6 +299,7 @@ const EventTypePage = (props: EventTypeSetupProps) => { hosts: eventType.hosts, successRedirectUrl: eventType.successRedirectUrl || "", users: eventType.users, + useEventTypeDestinationCalendarEmail: eventType.useEventTypeDestinationCalendarEmail, children: eventType.children.map((ch) => ({ ...ch, created: true, diff --git a/apps/web/pages/api/cron/bookingReminder.ts b/apps/web/pages/api/cron/bookingReminder.ts index 571b4850dd..9547bb1ece 100644 --- a/apps/web/pages/api/cron/bookingReminder.ts +++ b/apps/web/pages/api/cron/bookingReminder.ts @@ -119,7 +119,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse) endTime: booking.endTime.toISOString(), organizer: { id: user.id, - email: user.email, + email: booking?.userPrimaryEmail ?? user.email, name, timeZone: user.timeZone, language: { translate: tOrganizer, locale: user.locale ?? "en" }, diff --git a/apps/web/pages/api/recorded-daily-video.ts b/apps/web/pages/api/recorded-daily-video.ts index 2f61467561..9c63ee7cc5 100644 --- a/apps/web/pages/api/recorded-daily-video.ts +++ b/apps/web/pages/api/recorded-daily-video.ts @@ -230,7 +230,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { startTime: booking.startTime.toISOString(), endTime: booking.endTime.toISOString(), organizer: { - email: booking.user?.email || "Email-less", + email: booking?.userPrimaryEmail || booking.user?.email || "Email-less", name: booking.user?.name || "Nameless", timeZone: booking.user?.timeZone || "Europe/London", language: { translate: t, locale: booking?.user?.locale ?? "en" }, diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index cee336d2b8..e1e4e7e701 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -807,6 +807,7 @@ "additional_input_description": "Require scheduler to input additional inputs prior the booking is confirmed", "label": "Label", "placeholder": "Placeholder", + "display_add_to_calendar_organizer":"Display 'Add to calendar' email as the organizer", "type": "Type", "edit": "Edit", "add_input": "Add an Input", diff --git a/apps/web/test/lib/handleChildrenEventTypes.test.ts b/apps/web/test/lib/handleChildrenEventTypes.test.ts index 767f50076b..53ad966a61 100644 --- a/apps/web/test/lib/handleChildrenEventTypes.test.ts +++ b/apps/web/test/lib/handleChildrenEventTypes.test.ts @@ -108,6 +108,7 @@ describe("handleChildrenEventTypes", () => { timeZone, requiresBookerEmailVerification, lockTimeZoneToggleOnBookingPage, + useEventTypeDestinationCalendarEmail, ...evType } = mockFindFirstEventType({ id: 123, @@ -156,6 +157,7 @@ describe("handleChildrenEventTypes", () => { scheduleId, requiresBookerEmailVerification, lockTimeZoneToggleOnBookingPage, + useEventTypeDestinationCalendarEmail, ...evType } = mockFindFirstEventType({ metadata: { managedEventConfig: {} }, @@ -250,6 +252,7 @@ describe("handleChildrenEventTypes", () => { timeZone, requiresBookerEmailVerification, lockTimeZoneToggleOnBookingPage, + useEventTypeDestinationCalendarEmail, ...evType } = mockFindFirstEventType({ id: 123, @@ -298,6 +301,7 @@ describe("handleChildrenEventTypes", () => { userId, requiresBookerEmailVerification, lockTimeZoneToggleOnBookingPage, + useEventTypeDestinationCalendarEmail, ...evType } = mockFindFirstEventType({ metadata: { managedEventConfig: {} }, @@ -350,6 +354,7 @@ describe("handleChildrenEventTypes", () => { // eslint-disable-next-line @typescript-eslint/no-unused-vars requiresBookerEmailVerification, lockTimeZoneToggleOnBookingPage, + useEventTypeDestinationCalendarEmail, ...evType } = mockFindFirstEventType({ metadata: { managedEventConfig: {} }, diff --git a/apps/web/test/utils/bookingScenario/expects.ts b/apps/web/test/utils/bookingScenario/expects.ts index 83849cd7b1..d6f30f1718 100644 --- a/apps/web/test/utils/bookingScenario/expects.ts +++ b/apps/web/test/utils/bookingScenario/expects.ts @@ -307,16 +307,18 @@ export function expectWebhookToHaveBeenCalledWith( export function expectWorkflowToBeTriggered({ emails, organizer, + destinationEmail, }: { emails: Fixtures["emails"]; organizer: { email: string; name: string; timeZone: string }; + destinationEmail?: string; }) { const subjectPattern = /^Reminder: /i; expect(emails.get()).toEqual( expect.arrayContaining([ expect.objectContaining({ subject: expect.stringMatching(subjectPattern), - to: organizer.email, + to: destinationEmail ?? organizer.email, }), ]) ); @@ -370,6 +372,7 @@ export function expectSuccessfulBookingCreationEmails({ recurrence, bookingTimeRange, booking, + destinationEmail, }: { emails: Fixtures["emails"]; organizer: { email: string; name: string; timeZone: string }; @@ -381,6 +384,7 @@ export function expectSuccessfulBookingCreationEmails({ eventDomain?: string; bookingTimeRange?: { start: Date; end: Date }; booking: { uid: string; urlOrigin?: string }; + destinationEmail?: string; }) { const bookingUrlOrigin = booking.urlOrigin || WEBSITE_URL; expect(emails).toHaveEmail( @@ -409,7 +413,7 @@ export function expectSuccessfulBookingCreationEmails({ }, } : null), - to: `${organizer.email}`, + to: `${destinationEmail ?? organizer.email}`, ics: { filename: "event.ics", iCalUID: `${iCalUID}`, @@ -417,7 +421,7 @@ export function expectSuccessfulBookingCreationEmails({ method: "REQUEST", }, }, - `${organizer.email}` + `${destinationEmail ?? organizer.email}` ); expect(emails).toHaveEmail( diff --git a/packages/features/bookings/lib/handleCancelBooking.ts b/packages/features/bookings/lib/handleCancelBooking.ts index 9cdd8b9064..3378411e8c 100644 --- a/packages/features/bookings/lib/handleCancelBooking.ts +++ b/packages/features/bookings/lib/handleCancelBooking.ts @@ -245,7 +245,7 @@ async function handler(req: CustomRequest) { startTime: bookingToDelete?.startTime ? dayjs(bookingToDelete.startTime).format() : "", endTime: bookingToDelete?.endTime ? dayjs(bookingToDelete.endTime).format() : "", organizer: { - email: organizer.email, + email: bookingToDelete?.userPrimaryEmail ?? organizer.email, name: organizer.name ?? "Nameless", timeZone: organizer.timeZone, timeFormat: getTimeFormatStringFromUserTimeFormat(organizer.timeFormat), @@ -524,7 +524,7 @@ async function handler(req: CustomRequest) { startTime: bookingToDelete.startTime.toISOString(), endTime: bookingToDelete.endTime.toISOString(), organizer: { - email: bookingToDelete.user?.email ?? "dev@calendso.com", + email: bookingToDelete?.userPrimaryEmail ?? bookingToDelete.user?.email ?? "dev@calendso.com", name: bookingToDelete.user?.name ?? "no user", timeZone: bookingToDelete.user?.timeZone ?? "", timeFormat: getTimeFormatStringFromUserTimeFormat(organizer.timeFormat), diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index 0b1a10473e..6fb80a1167 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -342,6 +342,7 @@ export const getEventTypesFromDB = async (eventTypeId: number) => { durationLimits: true, assignAllTeamMembers: true, parentId: true, + useEventTypeDestinationCalendarEmail: true, owner: { select: { hideBranding: true, @@ -741,6 +742,7 @@ async function createBooking({ const newBookingData: Prisma.BookingCreateInput = { uid, + userPrimaryEmail: evt.organizer.email, responses: responses === null || evt.seatsPerTimeSlot ? Prisma.JsonNull : responses, title: evt.title, startTime: dayjs.utc(evt.startTime).toDate(), @@ -1496,6 +1498,7 @@ async function handler( if (isTeamEventType && eventType.schedulingType === "COLLECTIVE" && user.destinationCalendar) { teamDestinationCalendars.push(user.destinationCalendar); } + return { id: user.id, email: user.email ?? "", @@ -1548,6 +1551,12 @@ async function handler( ? await getBookerBaseUrl(eventType.team.parentId) : await getBookerBaseUrl(organizerOrganizationId ?? null); + const destinationCalendar = eventType.destinationCalendar + ? [eventType.destinationCalendar] + : organizerUser.destinationCalendar + ? [organizerUser.destinationCalendar] + : null; + let evt: CalendarEvent = { bookerUrl, type: eventType.slug, @@ -1560,7 +1569,10 @@ async function handler( organizer: { id: organizerUser.id, name: organizerUser.name || "Nameless", - email: organizerUser.email || "Email-less", + email: + eventType.useEventTypeDestinationCalendarEmail && destinationCalendar?.[0]?.primaryEmail + ? destinationCalendar[0].primaryEmail + : organizerUser.email || "Email-less", username: organizerUser.username || undefined, timeZone: organizerUser.timeZone, language: { translate: tOrganizer, locale: organizerUser.locale ?? "en" }, @@ -1571,11 +1583,7 @@ async function handler( attendees: attendeesList, location: bookingLocation, // Will be processed by the EventManager later. conferenceCredentialId, - destinationCalendar: eventType.destinationCalendar - ? [eventType.destinationCalendar] - : organizerUser.destinationCalendar - ? [organizerUser.destinationCalendar] - : null, + destinationCalendar, hideCalendarNotes: eventType.hideCalendarNotes, requiresConfirmation: !isConfirmedByDefault, eventTypeId: eventType.id, diff --git a/packages/features/bookings/lib/handleNewBooking/test/fresh-booking.test.ts b/packages/features/bookings/lib/handleNewBooking/test/fresh-booking.test.ts index 530e82f9d0..d911e62f9b 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/fresh-booking.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/fresh-booking.test.ts @@ -82,6 +82,9 @@ describe("handleNewBooking", () => { name: "Booker", }); + const organizerOtherEmail = "organizer2@example.com"; + const organizerDestinationCalendarEmailOnEventType = "organizerEventTypeEmail@example.com"; + const organizer = getOrganizer({ name: "Organizer", email: "organizer@example.com", @@ -92,6 +95,7 @@ describe("handleNewBooking", () => { destinationCalendar: { integration: "google_calendar", externalId: "organizer@google-calendar.com", + primaryEmail: organizerOtherEmail, }, }); @@ -122,6 +126,7 @@ describe("handleNewBooking", () => { id: 1, slotInterval: 30, length: 30, + useEventTypeDestinationCalendarEmail: true, users: [ { id: 101, @@ -130,6 +135,7 @@ describe("handleNewBooking", () => { destinationCalendar: { integration: "google_calendar", externalId: "event-type-1@google-calendar.com", + primaryEmail: organizerDestinationCalendarEmailOnEventType, }, }, ], @@ -208,7 +214,11 @@ describe("handleNewBooking", () => { iCalUID: createdBooking.iCalUID, }); - expectWorkflowToBeTriggered({ organizer, emails }); + expectWorkflowToBeTriggered({ + organizer, + emails, + destinationEmail: organizerDestinationCalendarEmailOnEventType, + }); expectSuccessfulCalendarEventCreationInCalendar(calendarMock, { calendarId: "event-type-1@google-calendar.com", videoCallUrl: "http://mock-dailyvideo.example.com/meeting-1", @@ -225,6 +235,7 @@ describe("handleNewBooking", () => { organizer, emails, iCalUID, + destinationEmail: organizerDestinationCalendarEmailOnEventType, }); expectBookingCreatedWebhookToHaveBeenFired({ diff --git a/packages/lib/defaultEvents.ts b/packages/lib/defaultEvents.ts index 99fb0c617f..863c29df56 100644 --- a/packages/lib/defaultEvents.ts +++ b/packages/lib/defaultEvents.ts @@ -101,6 +101,7 @@ const commons = { metadata: EventTypeMetaDataSchema.parse({}), bookingFields: [], assignAllTeamMembers: false, + useEventTypeDestinationCalendarEmail: false, }; const dynamicEvent = { diff --git a/packages/lib/getEventTypeById.ts b/packages/lib/getEventTypeById.ts index 09979adf3e..2741b3ac33 100644 --- a/packages/lib/getEventTypeById.ts +++ b/packages/lib/getEventTypeById.ts @@ -112,6 +112,7 @@ export default async function getEventTypeById({ successRedirectUrl: true, currency: true, bookingFields: true, + useEventTypeDestinationCalendarEmail: true, owner: { select: { id: true, diff --git a/packages/lib/payment/getBooking.ts b/packages/lib/payment/getBooking.ts index af0ea785ec..62ecb36580 100644 --- a/packages/lib/payment/getBooking.ts +++ b/packages/lib/payment/getBooking.ts @@ -96,7 +96,7 @@ export async function getBooking(bookingId: number) { bookingFields: booking.eventType?.bookingFields || null, }), organizer: { - email: user.email, + email: booking?.userPrimaryEmail ?? user.email, // eslint-disable-next-line @typescript-eslint/no-non-null-assertion name: user.name!, timeZone: user.timeZone, diff --git a/packages/lib/test/builder.ts b/packages/lib/test/builder.ts index bc1b23431b..dd6df48196 100644 --- a/packages/lib/test/builder.ts +++ b/packages/lib/test/builder.ts @@ -38,6 +38,7 @@ export const buildBooking = (booking?: Partial): Booking => { uid, userId: null, eventTypeId: null, + userPrimaryEmail: null, title: faker.lorem.sentence(), description: faker.lorem.paragraph(), customInputs: null, @@ -82,6 +83,7 @@ export const buildEventType = (eventType?: Partial): EventType => { userId: null, teamId: null, requiresBookerEmailVerification: false, + useEventTypeDestinationCalendarEmail: false, eventName: faker.lorem.words(), timeZone: null, periodType: "UNLIMITED", diff --git a/packages/prisma/migrations/20231220152005_add_email_in_destination_calendar/migration.sql b/packages/prisma/migrations/20231220152005_add_email_in_destination_calendar/migration.sql new file mode 100644 index 0000000000..4f1bf5f30e --- /dev/null +++ b/packages/prisma/migrations/20231220152005_add_email_in_destination_calendar/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "DestinationCalendar" ADD COLUMN "primaryEmail" TEXT; diff --git a/packages/prisma/migrations/20240205185412_add_email_field_in_booking/migration.sql b/packages/prisma/migrations/20240205185412_add_email_field_in_booking/migration.sql new file mode 100644 index 0000000000..dbb6a57e57 --- /dev/null +++ b/packages/prisma/migrations/20240205185412_add_email_field_in_booking/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Booking" ADD COLUMN "userPrimaryEmail" TEXT; diff --git a/packages/prisma/migrations/20240208075646_use_event_type_destination_calendar/migration.sql b/packages/prisma/migrations/20240208075646_use_event_type_destination_calendar/migration.sql new file mode 100644 index 0000000000..df8c3b5e39 --- /dev/null +++ b/packages/prisma/migrations/20240208075646_use_event_type_destination_calendar/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "EventType" ADD COLUMN "useEventTypeDestinationCalendarEmail" BOOLEAN NOT NULL DEFAULT false; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 72b15b2643..b1921a1922 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -70,62 +70,63 @@ model EventType { profileId Int? profile Profile? @relation(fields: [profileId], references: [id], onDelete: Cascade) - team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade) - teamId Int? - hashedLink HashedLink? - bookings Booking[] - availability Availability[] - webhooks Webhook[] - destinationCalendar DestinationCalendar? - eventName String? - customInputs EventTypeCustomInput[] - parentId Int? - parent EventType? @relation("managed_eventtype", fields: [parentId], references: [id], onDelete: Cascade) - children EventType[] @relation("managed_eventtype") + team Team? @relation(fields: [teamId], references: [id], onDelete: Cascade) + teamId Int? + hashedLink HashedLink? + bookings Booking[] + availability Availability[] + webhooks Webhook[] + destinationCalendar DestinationCalendar? + eventName String? + customInputs EventTypeCustomInput[] + parentId Int? + parent EventType? @relation("managed_eventtype", fields: [parentId], references: [id], onDelete: Cascade) + children EventType[] @relation("managed_eventtype") /// @zod.custom(imports.eventTypeBookingFields) - bookingFields Json? - timeZone String? - periodType PeriodType @default(UNLIMITED) + bookingFields Json? + timeZone String? + periodType PeriodType @default(UNLIMITED) /// @zod.custom(imports.coerceToDate) - periodStartDate DateTime? + periodStartDate DateTime? /// @zod.custom(imports.coerceToDate) - periodEndDate DateTime? - periodDays Int? - periodCountCalendarDays Boolean? - lockTimeZoneToggleOnBookingPage Boolean @default(false) - requiresConfirmation Boolean @default(false) - requiresBookerEmailVerification Boolean @default(false) + periodEndDate DateTime? + periodDays Int? + periodCountCalendarDays Boolean? + lockTimeZoneToggleOnBookingPage Boolean @default(false) + requiresConfirmation Boolean @default(false) + requiresBookerEmailVerification Boolean @default(false) /// @zod.custom(imports.recurringEventType) - recurringEvent Json? - disableGuests Boolean @default(false) - hideCalendarNotes Boolean @default(false) + recurringEvent Json? + disableGuests Boolean @default(false) + hideCalendarNotes Boolean @default(false) /// @zod.min(0) - minimumBookingNotice Int @default(120) - beforeEventBuffer Int @default(0) - afterEventBuffer Int @default(0) - seatsPerTimeSlot Int? - onlyShowFirstAvailableSlot Boolean @default(false) - seatsShowAttendees Boolean? @default(false) - seatsShowAvailabilityCount Boolean? @default(true) - schedulingType SchedulingType? - schedule Schedule? @relation(fields: [scheduleId], references: [id]) - scheduleId Int? + minimumBookingNotice Int @default(120) + beforeEventBuffer Int @default(0) + afterEventBuffer Int @default(0) + seatsPerTimeSlot Int? + onlyShowFirstAvailableSlot Boolean @default(false) + seatsShowAttendees Boolean? @default(false) + seatsShowAvailabilityCount Boolean? @default(true) + schedulingType SchedulingType? + schedule Schedule? @relation(fields: [scheduleId], references: [id]) + scheduleId Int? // price is deprecated. It has now moved to metadata.apps.stripe.price. Plan to drop this column. - price Int @default(0) + price Int @default(0) // currency is deprecated. It has now moved to metadata.apps.stripe.currency. Plan to drop this column. - currency String @default("usd") - slotInterval Int? + currency String @default("usd") + slotInterval Int? /// @zod.custom(imports.EventTypeMetaDataSchema) - metadata Json? + metadata Json? /// @zod.custom(imports.successRedirectUrl) - successRedirectUrl String? - workflows WorkflowsOnEventTypes[] + successRedirectUrl String? + workflows WorkflowsOnEventTypes[] /// @zod.custom(imports.intervalLimitsType) - bookingLimits Json? + bookingLimits Json? /// @zod.custom(imports.intervalLimitsType) - durationLimits Json? - isInstantEvent Boolean @default(false) - assignAllTeamMembers Boolean @default(false) + durationLimits Json? + isInstantEvent Boolean @default(false) + assignAllTeamMembers Boolean @default(false) + useEventTypeDestinationCalendarEmail Boolean @default(false) @@unique([userId, slug]) @@unique([teamId, slug]) @@ -173,6 +174,8 @@ model DestinationCalendar { id Int @id @default(autoincrement()) integration String externalId String + /// @zod.email() + primaryEmail String? user User? @relation(fields: [userId], references: [id], onDelete: Cascade) userId Int? @unique booking Booking[] @@ -459,6 +462,9 @@ model Booking { uid String @unique user User? @relation(fields: [userId], references: [id], onDelete: Cascade) userId Int? + // User's email at the time of booking + /// @zod.email() + userPrimaryEmail String? references BookingReference[] eventType EventType? @relation(fields: [eventTypeId], references: [id]) eventTypeId Int? diff --git a/packages/prisma/selects/booking.ts b/packages/prisma/selects/booking.ts index 7054f7dbf0..9615a17c55 100644 --- a/packages/prisma/selects/booking.ts +++ b/packages/prisma/selects/booking.ts @@ -3,6 +3,7 @@ import { Prisma } from "@prisma/client"; export const bookingMinimalSelect = Prisma.validator()({ id: true, title: true, + userPrimaryEmail: true, description: true, customInputs: true, startTime: true, diff --git a/packages/trpc/server/routers/loggedInViewer/connectedCalendars.handler.ts b/packages/trpc/server/routers/loggedInViewer/connectedCalendars.handler.ts index 8c195f1546..19ea64689a 100644 --- a/packages/trpc/server/routers/loggedInViewer/connectedCalendars.handler.ts +++ b/packages/trpc/server/routers/loggedInViewer/connectedCalendars.handler.ts @@ -39,6 +39,7 @@ export const connectedCalendarsHandler = async ({ ctx, input }: ConnectedCalenda user.selectedCalendars, user.destinationCalendar?.externalId ); + let toggledCalendarDetails: | { externalId: string; @@ -59,7 +60,12 @@ export const connectedCalendarsHandler = async ({ ctx, input }: ConnectedCalenda There are connected calendars, but no destination calendar So create a default destination calendar with the first primary connected calendar */ - const { integration = "", externalId = "", credentialId } = connectedCalendars[0].primary ?? {}; + const { + integration = "", + externalId = "", + credentialId, + email: primaryEmail, + } = connectedCalendars[0].primary ?? {}; // Select the first calendar matching the primary by default since that will also be the destination calendar if (onboarding && externalId) { const calendarIndex = (connectedCalendars[0].calendars || []).findIndex( @@ -79,6 +85,7 @@ export const connectedCalendarsHandler = async ({ ctx, input }: ConnectedCalenda integration, externalId, credentialId, + primaryEmail, }, }); } else { @@ -94,7 +101,7 @@ export const connectedCalendarsHandler = async ({ ctx, input }: ConnectedCalenda if (!destinationCal) { // If destinationCalendar is out of date, update it with the first primary connected calendar - const { integration = "", externalId = "" } = connectedCalendars[0].primary ?? {}; + const { integration = "", externalId = "", email: primaryEmail } = connectedCalendars[0].primary ?? {}; // Select the first calendar matching the primary by default since that will also be the destination calendar if (onboarding && externalId) { const calendarIndex = (connectedCalendars[0].calendars || []).findIndex( @@ -113,6 +120,7 @@ export const connectedCalendarsHandler = async ({ ctx, input }: ConnectedCalenda data: { integration, externalId, + primaryEmail, }, }); } else if (onboarding && !destinationCal.isSelected) { diff --git a/packages/trpc/server/routers/loggedInViewer/deleteCredential.handler.ts b/packages/trpc/server/routers/loggedInViewer/deleteCredential.handler.ts index e396d5e0d4..139b77fbc1 100644 --- a/packages/trpc/server/routers/loggedInViewer/deleteCredential.handler.ts +++ b/packages/trpc/server/routers/loggedInViewer/deleteCredential.handler.ts @@ -272,7 +272,7 @@ export const deleteCredentialHandler = async ({ ctx, input }: DeleteCredentialOp startTime: booking.startTime.toISOString(), endTime: booking.endTime.toISOString(), organizer: { - email: booking?.user?.email as string, + email: booking?.userPrimaryEmail ?? (booking?.user?.email as string), name: booking?.user?.name ?? "Nameless", timeZone: booking?.user?.timeZone as string, language: { translate: tOrganizer, locale: booking?.user?.locale ?? "en" }, diff --git a/packages/trpc/server/routers/loggedInViewer/setDestinationCalendar.handler.ts b/packages/trpc/server/routers/loggedInViewer/setDestinationCalendar.handler.ts index 71c6abd66e..e555327ddc 100644 --- a/packages/trpc/server/routers/loggedInViewer/setDestinationCalendar.handler.ts +++ b/packages/trpc/server/routers/loggedInViewer/setDestinationCalendar.handler.ts @@ -30,6 +30,8 @@ export const setDestinationCalendarHandler = async ({ ctx, input }: SetDestinati throw new TRPCError({ code: "BAD_REQUEST", message: `Could not find calendar ${input.externalId}` }); } + const primaryEmail = allCals.find((cal) => cal.primary && cal.credentialId === credentialId)?.email ?? null; + let where; if (eventTypeId) { @@ -56,12 +58,14 @@ export const setDestinationCalendarHandler = async ({ ctx, input }: SetDestinati integration, externalId, credentialId, + primaryEmail, }, create: { ...where, integration, externalId, credentialId, + primaryEmail, }, }); }; diff --git a/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts b/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts index e53cc02553..dbf0f449a8 100644 --- a/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/confirm.handler.ts @@ -49,6 +49,7 @@ export const confirmHandler = async ({ ctx, input }: ConfirmOptions) => { eventTypeId: true, responses: true, metadata: true, + userPrimaryEmail: true, eventType: { select: { id: true, @@ -168,7 +169,7 @@ export const confirmHandler = async ({ ctx, input }: ConfirmOptions) => { startTime: booking.startTime.toISOString(), endTime: booking.endTime.toISOString(), organizer: { - email: user.email, + email: booking.userPrimaryEmail ?? user.email, name: user.name || "Unnamed", username: user.username || undefined, timeZone: user.timeZone, diff --git a/packages/trpc/server/routers/viewer/bookings/editLocation.handler.ts b/packages/trpc/server/routers/viewer/bookings/editLocation.handler.ts index c780171712..636f49a35f 100644 --- a/packages/trpc/server/routers/viewer/bookings/editLocation.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/editLocation.handler.ts @@ -74,7 +74,7 @@ export const editLocationHandler = async ({ ctx, input }: EditLocationOptions) = startTime: booking.startTime ? dayjs(booking.startTime).format() : "", endTime: booking.endTime ? dayjs(booking.endTime).format() : "", organizer: { - email: organizer.email, + email: booking?.userPrimaryEmail ?? organizer.email, name: organizer.name ?? "Nameless", timeZone: organizer.timeZone, language: { translate: tOrganizer, locale: organizer.locale ?? "en" }, diff --git a/packages/trpc/server/routers/viewer/bookings/requestReschedule.handler.ts b/packages/trpc/server/routers/viewer/bookings/requestReschedule.handler.ts index 9b1c35fadb..a0bf49a47d 100644 --- a/packages/trpc/server/routers/viewer/bookings/requestReschedule.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/requestReschedule.handler.ts @@ -53,6 +53,7 @@ export const requestRescheduleHandler = async ({ ctx, input }: RequestReschedule startTime: true, endTime: true, eventTypeId: true, + userPrimaryEmail: true, eventType: { include: { team: { @@ -180,6 +181,10 @@ export const requestRescheduleHandler = async ({ ctx, input }: RequestReschedule const userTranslation = await getTranslation(user.locale ?? "en", "common"); const [userAsPeopleType] = usersToPeopleType([user], userTranslation); + const organizer = { + ...userAsPeopleType, + email: bookingToReschedule?.userPrimaryEmail ?? userAsPeopleType.email, + }; const builder = new CalendarEventBuilder(); const eventType = bookingToReschedule.eventType; @@ -196,7 +201,7 @@ export const requestRescheduleHandler = async ({ ctx, input }: RequestReschedule bookingToReschedule.attendees as unknown as PersonAttendeeCommonFields[], tAttendees ), - organizer: userAsPeopleType, + organizer, iCalUID: bookingToReschedule.iCalUID, }); @@ -257,7 +262,7 @@ export const requestRescheduleHandler = async ({ ctx, input }: RequestReschedule }), startTime: bookingToReschedule?.startTime ? dayjs(bookingToReschedule.startTime).format() : "", endTime: bookingToReschedule?.endTime ? dayjs(bookingToReschedule.endTime).format() : "", - organizer: userAsPeopleType, + organizer, attendees: usersToPeopleType( // username field doesn't exists on attendee but could be in the future bookingToReschedule.attendees as unknown as PersonAttendeeCommonFields[], diff --git a/packages/trpc/server/routers/viewer/payments/chargeCard.handler.ts b/packages/trpc/server/routers/viewer/payments/chargeCard.handler.ts index 36889b362c..bdc9cd3b63 100644 --- a/packages/trpc/server/routers/viewer/payments/chargeCard.handler.ts +++ b/packages/trpc/server/routers/viewer/payments/chargeCard.handler.ts @@ -67,7 +67,7 @@ export const chargeCardHandler = async ({ ctx, input }: ChargeCardHandlerOptions startTime: dayjs(booking.startTime).format(), endTime: dayjs(booking.endTime).format(), organizer: { - email: booking.user?.email || "", + email: booking?.userPrimaryEmail ?? booking.user?.email ?? "", name: booking.user?.name || "Nameless", timeZone: booking.user?.timeZone || "", language: { translate: tOrganizer, locale: booking.user?.locale ?? "en" }, diff --git a/packages/trpc/server/routers/viewer/workflows/activateEventType.handler.ts b/packages/trpc/server/routers/viewer/workflows/activateEventType.handler.ts index 1068ab3fb4..b4c5569108 100644 --- a/packages/trpc/server/routers/viewer/workflows/activateEventType.handler.ts +++ b/packages/trpc/server/routers/viewer/workflows/activateEventType.handler.ts @@ -169,7 +169,7 @@ export const activateEventTypeHandler = async ({ ctx, input }: ActivateEventType organizer: booking.user ? { name: booking.user.name || "", - email: booking.user.email, + email: booking?.userPrimaryEmail ?? booking.user.email, timeZone: booking.user.timeZone, timeFormat: getTimeFormatStringFromUserTimeFormat(booking.user.timeFormat), language: { locale: booking.user.locale || defaultLocale }, diff --git a/packages/trpc/server/routers/viewer/workflows/update.handler.ts b/packages/trpc/server/routers/viewer/workflows/update.handler.ts index 19296d8cc7..f729f89bf0 100644 --- a/packages/trpc/server/routers/viewer/workflows/update.handler.ts +++ b/packages/trpc/server/routers/viewer/workflows/update.handler.ts @@ -288,7 +288,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => { ? { language: { locale: booking.user.locale || defaultLocale }, name: booking.user.name || "", - email: booking.user.email, + email: booking?.userPrimaryEmail ?? booking.user.email, timeZone: booking.user.timeZone, timeFormat: getTimeFormatStringFromUserTimeFormat(booking.user.timeFormat), } @@ -525,7 +525,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => { ? { language: { locale: booking.user.locale || defaultLocale }, name: booking.user.name || "", - email: booking.user.email, + email: booking?.userPrimaryEmail ?? booking.user.email, timeZone: booking.user.timeZone, timeFormat: getTimeFormatStringFromUserTimeFormat(booking.user.timeFormat), } @@ -669,7 +669,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => { organizer: booking.user ? { name: booking.user.name || "", - email: booking.user.email, + email: booking?.userPrimaryEmail ?? booking.user.email, timeZone: booking.user.timeZone, timeFormat: getTimeFormatStringFromUserTimeFormat(booking.user.timeFormat), language: { locale: booking.user.locale || defaultLocale },