From af7347ec7f3faa42db9d1f9dbd22ad9f5613018d Mon Sep 17 00:00:00 2001 From: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com> Date: Thu, 29 Aug 2024 17:16:48 -0400 Subject: [PATCH] fix: Set `teamMemberEmail` server side for CRM RR Skip (#16367) Co-authored-by: Keith Williams Co-authored-by: zomars --- .../team/[slug]/[type]/getServerSideProps.tsx | 38 +++++++- apps/web/pages/team/[slug]/[type].tsx | 2 + apps/web/test/lib/getSchedule.test.ts | 15 +--- .../app-store/_utils/CRMRoundRobinSkip.ts | 61 +++++++++++++ packages/core/crmManager/crmManager.ts | 6 +- packages/core/getUserAvailability.ts | 3 +- .../Booker/components/hooks/useBookings.ts | 3 +- packages/features/bookings/Booker/store.ts | 11 ++- packages/features/bookings/Booker/types.ts | 1 + .../features/bookings/Booker/utils/event.ts | 6 +- .../booking-to-mutation-input-mapper.tsx | 2 +- .../schedules/lib/use-schedule/useSchedule.ts | 6 +- .../atoms/booker/BookerWebWrapper.tsx | 6 +- .../atoms/hooks/useHandleBookEvent.ts | 3 +- packages/prisma/zod-utils.ts | 3 +- .../trpc/server/routers/viewer/slots/types.ts | 2 +- .../trpc/server/routers/viewer/slots/util.ts | 86 +++---------------- 17 files changed, 140 insertions(+), 114 deletions(-) create mode 100644 packages/app-store/_utils/CRMRoundRobinSkip.ts diff --git a/apps/web/lib/team/[slug]/[type]/getServerSideProps.tsx b/apps/web/lib/team/[slug]/[type]/getServerSideProps.tsx index 03a7c876965d6c..5777abc73b9f85 100644 --- a/apps/web/lib/team/[slug]/[type]/getServerSideProps.tsx +++ b/apps/web/lib/team/[slug]/[type]/getServerSideProps.tsx @@ -1,14 +1,16 @@ import type { GetServerSidePropsContext } from "next"; import { z } from "zod"; +import { getCRMContactOwnerForRRLeadSkip } from "@calcom/app-store/_utils/CRMRoundRobinSkip"; import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; -import { getBookingForReschedule } from "@calcom/features/bookings/lib/get-booking"; import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking"; -import { getSlugOrRequestedSlug } from "@calcom/features/ee/organizations/lib/orgDomains"; -import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains"; +import { getBookingForReschedule } from "@calcom/features/bookings/lib/get-booking"; +import { getSlugOrRequestedSlug, orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains"; import slugify from "@calcom/lib/slugify"; import prisma from "@calcom/prisma"; import { RedirectType } from "@calcom/prisma/client"; +import { SchedulingType } from "@calcom/prisma/enums"; +import type { RouterOutputs } from "@calcom/trpc"; import { getTemporaryOrgRedirect } from "@lib/getTemporaryOrgRedirect"; @@ -23,7 +25,12 @@ const paramsSchema = z.object({ export const getServerSideProps = async (context: GetServerSidePropsContext) => { const session = await getServerSession(context); const { slug: teamSlug, type: meetingSlug } = paramsSchema.parse(context.params); - const { rescheduleUid, duration: queryDuration, isInstantMeeting: queryIsInstantMeeting } = context.query; + const { + rescheduleUid, + duration: queryDuration, + isInstantMeeting: queryIsInstantMeeting, + email, + } = context.query; const { ssrInit } = await import("@server/lib/ssr"); const ssr = await ssrInit(context); const { currentOrgDomain, isValidOrgDomain } = orgDomainConfig(context.req, context.params?.orgSlug); @@ -97,6 +104,29 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => isInstantMeeting: eventData.isInstantEvent && queryIsInstantMeeting ? true : false, themeBasis: null, orgBannerUrl: eventData?.team?.parent?.bannerUrl ?? "", + teamMemberEmail: await getTeamMemberEmail(eventData, email as string), }, }; }; + +type EventData = RouterOutputs["viewer"]["public"]["event"]; + +async function getTeamMemberEmail(eventData: EventData, email?: string): Promise { + // Pre-requisites + if (!eventData || !email || eventData.schedulingType !== SchedulingType.ROUND_ROBIN) return null; + const crmContactOwnerEmail = await getCRMContactOwnerForRRLeadSkip(email, eventData.id); + if (!crmContactOwnerEmail) return null; + // Determine if the contactOwner is a part of the event type + const contactOwnerQuery = await prisma.user.findFirst({ + where: { + email: crmContactOwnerEmail, + hosts: { + some: { + eventTypeId: eventData.id, + }, + }, + }, + }); + if (!contactOwnerQuery) return null; + return crmContactOwnerEmail; +} diff --git a/apps/web/pages/team/[slug]/[type].tsx b/apps/web/pages/team/[slug]/[type].tsx index 3f6eda1ca304fa..0482582ccc6d77 100644 --- a/apps/web/pages/team/[slug]/[type].tsx +++ b/apps/web/pages/team/[slug]/[type].tsx @@ -35,6 +35,7 @@ export default function Type({ eventData, isInstantMeeting, orgBannerUrl, + teamMemberEmail, }: PageProps) { const searchParams = useSearchParams(); @@ -67,6 +68,7 @@ export default function Type({ eventData.length )} orgBannerUrl={orgBannerUrl} + teamMemberEmail={teamMemberEmail} /> ); diff --git a/apps/web/test/lib/getSchedule.test.ts b/apps/web/test/lib/getSchedule.test.ts index 27ee4f20b794a2..4d0a49c5763583 100644 --- a/apps/web/test/lib/getSchedule.test.ts +++ b/apps/web/test/lib/getSchedule.test.ts @@ -175,12 +175,10 @@ describe("getSchedule", () => { endTime: `${plus2DateString}T18:29:59.999Z`, timeZone: Timezones["+5:30"], isTeamEvent: true, - bookerEmail: "test@test.com", + teamMemberEmail: "example@example.com", }, }); - expect(scheduleWithLeadSkip.teamMember).toBe("example@example.com"); - // only slots where example@example.com is available expect(scheduleWithLeadSkip).toHaveTimeSlots( [`11:30:00.000Z`, `12:30:00.000Z`, `13:30:00.000Z`, `14:30:00.000Z`, `15:30:00.000Z`], @@ -197,12 +195,9 @@ describe("getSchedule", () => { endTime: `${plus2DateString}T18:29:59.999Z`, timeZone: Timezones["+5:30"], isTeamEvent: true, - bookerEmail: "testtest@test.com", }, }); - expect(scheduleWithoutLeadSkip.teamMember).toBe(undefined); - // slots where either one of the rr hosts is available expect(scheduleWithoutLeadSkip).toHaveTimeSlots( [ @@ -325,12 +320,10 @@ describe("getSchedule", () => { endTime: `${plus2DateString}T18:29:59.999Z`, timeZone: Timezones["+5:30"], isTeamEvent: true, - bookerEmail: "test@test.com", + teamMemberEmail: "example@example.com", }, }); - expect(scheduleFixedHostLead.teamMember).toBe("example@example.com"); - // show normal slots, example@example + one RR host needs to be available expect(scheduleFixedHostLead).toHaveTimeSlots( [ @@ -355,12 +348,10 @@ describe("getSchedule", () => { endTime: `${plus2DateString}T18:29:59.999Z`, timeZone: Timezones["+5:30"], isTeamEvent: true, - bookerEmail: "test1@test.com", + teamMemberEmail: "example1@example.com", }, }); - expect(scheduleRRHostLead.teamMember).toBe("example1@example.com"); - // slots where example@example (fixed host) + example1@example.com are available together expect(scheduleRRHostLead).toHaveTimeSlots( [`07:30:00.000Z`, `08:30:00.000Z`, `09:30:00.000Z`, `10:30:00.000Z`, `11:30:00.000Z`], diff --git a/packages/app-store/_utils/CRMRoundRobinSkip.ts b/packages/app-store/_utils/CRMRoundRobinSkip.ts new file mode 100644 index 00000000000000..902f6dcba4cac2 --- /dev/null +++ b/packages/app-store/_utils/CRMRoundRobinSkip.ts @@ -0,0 +1,61 @@ +import type { z } from "zod"; + +import CrmManager from "@calcom/core/crmManager/crmManager"; +import { prisma } from "@calcom/prisma"; +import type { EventTypeAppMetadataSchema } from "@calcom/prisma/zod-utils"; +import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; + +export async function getCRMContactOwnerForRRLeadSkip( + bookerEmail: string, + eventTypeId: number +): Promise { + const eventTypeMetadataQuery = await prisma.eventType.findUnique({ + where: { + id: eventTypeId, + }, + select: { metadata: true }, + }); + + const eventTypeMetadata = EventTypeMetaDataSchema.safeParse(eventTypeMetadataQuery?.metadata); + + if (!eventTypeMetadata.success || !eventTypeMetadata.data?.apps) return; + + const crm = await getCRMManagerWithRRLeadSkip(eventTypeMetadata.data.apps); + + if (!crm) return; + + const contact = await crm.getContacts(bookerEmail, true); + if (!contact?.length) return; + return contact[0].ownerEmail; +} + +async function getCRMManagerWithRRLeadSkip(apps: z.infer) { + let crmRoundRobinLeadSkip; + for (const appKey in apps) { + const app = apps[appKey as keyof typeof apps]; + if ( + app.enabled && + typeof app.appCategories === "object" && + app.appCategories.some((category: string) => category === "crm") && + app.roundRobinLeadSkip + ) { + crmRoundRobinLeadSkip = app; + break; + } + } + if (!crmRoundRobinLeadSkip) return; + const crmCredential = await prisma.credential.findUnique({ + where: { + id: crmRoundRobinLeadSkip.credentialId, + }, + include: { + user: { + select: { + email: true, + }, + }, + }, + }); + if (!crmCredential) return; + return new CrmManager(crmCredential); +} diff --git a/packages/core/crmManager/crmManager.ts b/packages/core/crmManager/crmManager.ts index e97366ca49f9f6..23864fedb86891 100644 --- a/packages/core/crmManager/crmManager.ts +++ b/packages/core/crmManager/crmManager.ts @@ -39,7 +39,7 @@ export default class CrmManager { const contactsToCreate = event.attendees.filter( (attendee) => !contacts.some((contact) => contact.email === attendee.email) ); - const createdContacts = await this.createContacts(contactsToCreate); + const createdContacts = await this.createContacts(contactsToCreate, event.organizer?.email); contacts = contacts.concat(createdContacts); return await crmService?.createEvent(event, contacts); } @@ -60,9 +60,9 @@ export default class CrmManager { return contacts; } - public async createContacts(contactsToCreate: ContactCreateInput[]) { + public async createContacts(contactsToCreate: ContactCreateInput[], organizerEmail?: string) { const crmService = await this.getCrmService(this.credential); - const createdContacts = (await crmService?.createContacts(contactsToCreate)) || []; + const createdContacts = (await crmService?.createContacts(contactsToCreate, organizerEmail)) || []; return createdContacts; } } diff --git a/packages/core/getUserAvailability.ts b/packages/core/getUserAvailability.ts index 61f466c2ee4186..e79a1d4c2d19ed 100644 --- a/packages/core/getUserAvailability.ts +++ b/packages/core/getUserAvailability.ts @@ -839,13 +839,14 @@ const _getOutOfOfficeDays = async ({ type GetUserAvailabilityQuery = Parameters[0]; type GetUserAvailabilityInitialData = NonNullable[1]>; +export type GetAvailabilityUser = NonNullable; const _getUsersAvailability = async ({ users, query, initialData, }: { - users: (NonNullable & { + users: (GetAvailabilityUser & { currentBookings?: GetUserAvailabilityInitialData["currentBookings"]; })[]; query: Omit; diff --git a/packages/features/bookings/Booker/components/hooks/useBookings.ts b/packages/features/bookings/Booker/components/hooks/useBookings.ts index e988410b4ce494..49e9d9aabe9704 100644 --- a/packages/features/bookings/Booker/components/hooks/useBookings.ts +++ b/packages/features/bookings/Booker/components/hooks/useBookings.ts @@ -47,7 +47,7 @@ export interface IUseBookings { hashedLink?: string | null; bookingForm: UseBookingFormReturnType["bookingForm"]; metadata: Record; - teamMemberEmail?: string; + teamMemberEmail?: string | null; } const getBookingSuccessfulEventPayload = (booking: { @@ -310,7 +310,6 @@ export const useBookings = ({ event, hashedLink, bookingForm, metadata, teamMemb bookingForm, hashedLink, metadata, - teamMemberEmail, handleInstantBooking: createInstantBookingMutation.mutate, handleRecBooking: createRecurringBookingMutation.mutate, handleBooking: createBookingMutation.mutate, diff --git a/packages/features/bookings/Booker/store.ts b/packages/features/bookings/Booker/store.ts index 27db6dede53adc..f54dbfcc3ee1dd 100644 --- a/packages/features/bookings/Booker/store.ts +++ b/packages/features/bookings/Booker/store.ts @@ -32,6 +32,7 @@ type StoreInitializeType = { durationConfig?: number[] | null; org?: string | null; isInstantMeeting?: boolean; + teamMemberEmail?: string | null; }; type SeatedEventData = { @@ -148,6 +149,8 @@ export type BookerStore = { org?: string | null; setOrg: (org: string | null | undefined) => void; + + teamMemberEmail?: string | null; }; /** @@ -253,6 +256,7 @@ export const useBookerStore = create((set, get) => ({ durationConfig, org, isInstantMeeting, + teamMemberEmail, }: StoreInitializeType) => { const selectedDateInStore = get().selectedDate; @@ -265,7 +269,8 @@ export const useBookerStore = create((set, get) => ({ get().bookingUid === bookingUid && get().bookingData?.responses.email === bookingData?.responses.email && get().layout === layout && - get().rescheduledBy === rescheduledBy + get().rescheduledBy === rescheduledBy && + get().teamMemberEmail ) return; set({ @@ -284,6 +289,7 @@ export const useBookerStore = create((set, get) => ({ selectedDate: selectedDateInStore || (["week_view", "column_view"].includes(layout) ? dayjs().format("YYYY-MM-DD") : null), + teamMemberEmail, }); if (durationConfig?.includes(Number(getQueryParam("duration")))) { @@ -372,6 +378,7 @@ export const useInitializeBookerStore = ({ durationConfig, org, isInstantMeeting, + teamMemberEmail, }: StoreInitializeType) => { const initializeStore = useBookerStore((state) => state.initialize); useEffect(() => { @@ -389,6 +396,7 @@ export const useInitializeBookerStore = ({ verifiedEmail, durationConfig, isInstantMeeting, + teamMemberEmail, }); }, [ initializeStore, @@ -405,5 +413,6 @@ export const useInitializeBookerStore = ({ verifiedEmail, durationConfig, isInstantMeeting, + teamMemberEmail, ]); }; diff --git a/packages/features/bookings/Booker/types.ts b/packages/features/bookings/Booker/types.ts index 71460e61e55e4a..42fd05a0fef8ca 100644 --- a/packages/features/bookings/Booker/types.ts +++ b/packages/features/bookings/Booker/types.ts @@ -85,6 +85,7 @@ export interface BookerProps { */ hashedLink?: string | null; isInstantMeeting?: boolean; + teamMemberEmail?: string | null; } export type WrappedBookerPropsMain = { diff --git a/packages/features/bookings/Booker/utils/event.ts b/packages/features/bookings/Booker/utils/event.ts index ae4cf7c09e65ab..16b483e157a057 100644 --- a/packages/features/bookings/Booker/utils/event.ts +++ b/packages/features/bookings/Booker/utils/event.ts @@ -65,7 +65,7 @@ export const useScheduleForEvent = ({ dayCount, selectedDate, orgSlug, - bookerEmail, + teamMemberEmail, }: { prefetchNextMonth?: boolean; username?: string | null; @@ -77,7 +77,7 @@ export const useScheduleForEvent = ({ dayCount?: number | null; selectedDate?: string | null; orgSlug?: string; - bookerEmail?: string; + teamMemberEmail?: string | null; } = {}) => { const { timezone } = useTimePreferences(); const event = useEvent(); @@ -107,7 +107,7 @@ export const useScheduleForEvent = ({ duration: durationFromStore ?? duration, isTeamEvent: pathname?.indexOf("/team/") !== -1 || isTeam, orgSlug, - bookerEmail, + teamMemberEmail, }); return { diff --git a/packages/features/bookings/lib/book-event-form/booking-to-mutation-input-mapper.tsx b/packages/features/bookings/lib/book-event-form/booking-to-mutation-input-mapper.tsx index 2b0a859e4baf46..aedb4f46be012e 100644 --- a/packages/features/bookings/lib/book-event-form/booking-to-mutation-input-mapper.tsx +++ b/packages/features/bookings/lib/book-event-form/booking-to-mutation-input-mapper.tsx @@ -20,7 +20,7 @@ export type BookingOptions = { bookingUid?: string; seatReferenceUid?: string; hashedLink?: string | null; - teamMemberEmail?: string; + teamMemberEmail?: string | null; orgSlug?: string; }; diff --git a/packages/features/schedules/lib/use-schedule/useSchedule.ts b/packages/features/schedules/lib/use-schedule/useSchedule.ts index 48ee3d8eb91ef9..cbe6dc245e50d2 100644 --- a/packages/features/schedules/lib/use-schedule/useSchedule.ts +++ b/packages/features/schedules/lib/use-schedule/useSchedule.ts @@ -16,7 +16,7 @@ export type UseScheduleWithCacheArgs = { rescheduleUid?: string | null; isTeamEvent?: boolean; orgSlug?: string; - bookerEmail?: string; + teamMemberEmail?: string | null; }; export const useSchedule = ({ @@ -33,7 +33,7 @@ export const useSchedule = ({ rescheduleUid, isTeamEvent, orgSlug, - bookerEmail, + teamMemberEmail, }: UseScheduleWithCacheArgs) => { const [startTime, endTime] = useTimesForSchedule({ month, @@ -60,7 +60,7 @@ export const useSchedule = ({ duration: duration ? `${duration}` : undefined, rescheduleUid, orgSlug, - bookerEmail, + teamMemberEmail, }, { trpc: { diff --git a/packages/platform/atoms/booker/BookerWebWrapper.tsx b/packages/platform/atoms/booker/BookerWebWrapper.tsx index 2b20f5574e57ac..d89e42787fe6b6 100644 --- a/packages/platform/atoms/booker/BookerWebWrapper.tsx +++ b/packages/platform/atoms/booker/BookerWebWrapper.tsx @@ -21,7 +21,6 @@ import { useBookerStore, useInitializeBookerStore } from "@calcom/features/booki import { useEvent, useScheduleForEvent } from "@calcom/features/bookings/Booker/utils/event"; import { useBrandColors } from "@calcom/features/bookings/Booker/utils/use-brand-colors"; import { DEFAULT_LIGHT_BRAND_COLOR, DEFAULT_DARK_BRAND_COLOR } from "@calcom/lib/constants"; -import { useDebounce } from "@calcom/lib/hooks/useDebounce"; import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery"; import { BookerLayouts } from "@calcom/prisma/zod-utils"; @@ -122,7 +121,6 @@ export const BookerWebWrapper = (props: BookerWebWrapperAtomProps) => { * Prioritize dateSchedule load * Component will render but use data already fetched from here, and no duplicate requests will be made * */ - const debouncedFormEmail = useDebounce(bookerForm.formEmail, 600); const schedule = useScheduleForEvent({ prefetchNextMonth, username: props.username, @@ -132,14 +130,14 @@ export const BookerWebWrapper = (props: BookerWebWrapperAtomProps) => { month: props.month, duration: props.duration, selectedDate, - bookerEmail: debouncedFormEmail, + teamMemberEmail: props.teamMemberEmail, }); const bookings = useBookings({ event, hashedLink: props.hashedLink, bookingForm: bookerForm.bookingForm, metadata: metadata ?? {}, - teamMemberEmail: schedule.data?.teamMember, + teamMemberEmail: props.teamMemberEmail, }); const verifyCode = useVerifyCode({ diff --git a/packages/platform/atoms/hooks/useHandleBookEvent.ts b/packages/platform/atoms/hooks/useHandleBookEvent.ts index 48256cb09ab2f1..99292226d7161c 100644 --- a/packages/platform/atoms/hooks/useHandleBookEvent.ts +++ b/packages/platform/atoms/hooks/useHandleBookEvent.ts @@ -21,7 +21,6 @@ type UseHandleBookingProps = { }; metadata: Record; hashedLink?: string | null; - teamMemberEmail?: string; handleBooking: (input: UseCreateBookingInput) => void; handleInstantBooking: (input: BookingCreateBody) => void; handleRecBooking: (input: BookingCreateBody[]) => void; @@ -33,7 +32,6 @@ export const useHandleBookEvent = ({ event, metadata, hashedLink, - teamMemberEmail, handleBooking, handleInstantBooking, handleRecBooking, @@ -52,6 +50,7 @@ export const useHandleBookEvent = ({ const seatedEventData = useBookerStore((state) => state.seatedEventData); const isInstantMeeting = useBookerStore((state) => state.isInstantMeeting); const orgSlug = useBookerStore((state) => state.org); + const teamMemberEmail = useBookerStore((state) => state.teamMemberEmail); const handleBookEvent = () => { const values = bookingForm.getValues(); diff --git a/packages/prisma/zod-utils.ts b/packages/prisma/zod-utils.ts index a0ba5a160859b1..6e8995dad6b486 100644 --- a/packages/prisma/zod-utils.ts +++ b/packages/prisma/zod-utils.ts @@ -246,7 +246,7 @@ export const bookingCreateBodySchema = z.object({ hashedLink: z.string().nullish(), seatReferenceUid: z.string().optional(), orgSlug: z.string().optional(), - teamMemberEmail: z.string().optional(), + teamMemberEmail: z.string().nullish(), }); export const requiredCustomInputSchema = z.union([ @@ -293,7 +293,6 @@ export const extendedBookingCreateBody = bookingCreateBodySchema.merge( .optional(), luckyUsers: z.array(z.number()).optional(), customInputs: z.undefined().optional(), - teamMemberEmail: z.string().optional(), }) ); diff --git a/packages/trpc/server/routers/viewer/slots/types.ts b/packages/trpc/server/routers/viewer/slots/types.ts index 3c0f206ab77721..851f2135818936 100644 --- a/packages/trpc/server/routers/viewer/slots/types.ts +++ b/packages/trpc/server/routers/viewer/slots/types.ts @@ -24,7 +24,7 @@ export const getScheduleSchema = z // whether to do team event or user event isTeamEvent: z.boolean().optional().default(false), orgSlug: z.string().optional(), - bookerEmail: z.string().optional(), + teamMemberEmail: z.string().nullable().optional(), }) .transform((val) => { // Need this so we can pass a single username in the query string form public API diff --git a/packages/trpc/server/routers/viewer/slots/util.ts b/packages/trpc/server/routers/viewer/slots/util.ts index c90db2e86d6914..12dd41c21f3c45 100644 --- a/packages/trpc/server/routers/viewer/slots/util.ts +++ b/packages/trpc/server/routers/viewer/slots/util.ts @@ -1,12 +1,10 @@ // eslint-disable-next-line no-restricted-imports import { countBy } from "lodash"; import { v4 as uuid } from "uuid"; -import type z from "zod"; -import CrmManager from "@calcom/core/crmManager/crmManager"; import { getAggregatedAvailability } from "@calcom/core/getAggregatedAvailability"; import { getBusyTimesForLimitChecks } from "@calcom/core/getBusyTimes"; -import type { CurrentSeats, IFromUser, IToUser } from "@calcom/core/getUserAvailability"; +import type { CurrentSeats, IFromUser, IToUser, GetAvailabilityUser } from "@calcom/core/getUserAvailability"; import { getUsersAvailability } from "@calcom/core/getUserAvailability"; import type { Dayjs } from "@calcom/dayjs"; import dayjs from "@calcom/dayjs"; @@ -32,7 +30,6 @@ import { SchedulingType } from "@calcom/prisma/enums"; import { BookingStatus } from "@calcom/prisma/enums"; import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/credential"; import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils"; -import type { EventTypeAppMetadataSchema } from "@calcom/prisma/zod-utils"; import type { EventBusyDate } from "@calcom/types/Calendar"; import { TRPCError } from "@trpc/server"; @@ -322,57 +319,6 @@ export interface IGetAvailableSlots { emoji?: string | undefined; }[] >; - teamMember?: string | undefined; -} - -async function getCRMContactOwnerForRRLeadSkip( - bookerEmail: string, - apps?: z.infer -) { - if (!apps) return; - const crm = await getCRMManagerWithRRLeadSkip(apps); - - if (!crm) return; - - const contact = await crm.getContacts(bookerEmail, true); - if (contact?.length) { - return contact[0].ownerEmail; - } -} - -async function getCRMManagerWithRRLeadSkip(apps: z.infer) { - let crmRoundRobinLeadSkip; - for (const appKey in apps) { - const app = apps[appKey as keyof typeof apps]; - if ( - app.enabled && - typeof app.appCategories === "object" && - app.appCategories.some((category: string) => category === "crm") && - app.roundRobinLeadSkip - ) { - crmRoundRobinLeadSkip = app; - break; - } - } - - if (crmRoundRobinLeadSkip) { - const crmCredential = await prisma.credential.findUnique({ - where: { - id: crmRoundRobinLeadSkip.credentialId, - }, - include: { - user: { - select: { - email: true, - }, - }, - }, - }); - if (crmCredential) { - return new CrmManager(crmCredential); - } - } - return; } export async function getAvailableSlots({ input, ctx }: GetScheduleOptions): Promise { @@ -435,8 +381,6 @@ export async function getAvailableSlots({ input, ctx }: GetScheduleOptions): Pro } let currentSeats: CurrentSeats | undefined; - let teamMember: string | undefined; - let hosts = eventType.hosts?.length && eventType.schedulingType ? eventType.hosts @@ -466,25 +410,18 @@ export async function getAvailableSlots({ input, ctx }: GetScheduleOptions): Pro hosts = hosts.filter((host) => host.user.id === originalRescheduledBooking?.userId || 0); } - let usersWithCredentials = hosts.map(({ isFixed, user }) => ({ isFixed, ...user })); - - if (eventType.schedulingType === SchedulingType.ROUND_ROBIN && input.bookerEmail) { - const crmContactOwner = await getCRMContactOwnerForRRLeadSkip( - input.bookerEmail, - eventType?.metadata?.apps - ); - const contactOwnerHost = hosts.find((host) => host.user.email === crmContactOwner); + const teamMemberHost = hosts.find((host) => host.user.email === input?.teamMemberEmail); - if (contactOwnerHost) { - teamMember = contactOwnerHost.user.email; - const contactOwnerIsRRHost = !contactOwnerHost.isFixed; + // If the requested team member is a fixed host proceed as normal else get availability like the requested member is a fixed host + const usersWithCredentials = + !input.teamMemberEmail || !teamMemberHost || teamMemberHost.isFixed + ? hosts.map(({ isFixed, user }) => ({ isFixed, ...user })) + : hosts.reduce((usersArray, host) => { + if (host.isFixed || host.user.email === input.teamMemberEmail) + usersArray.push({ ...host.user, isFixed: host.isFixed }); - usersWithCredentials = usersWithCredentials.filter( - (user) => user.email !== contactOwnerHost.user.email && (!contactOwnerIsRRHost || user.isFixed) - ); - usersWithCredentials.push({ ...contactOwnerHost.user, isFixed: true }); - } - } + return usersArray; + }, [] as (GetAvailabilityUser & { isFixed: boolean })[]); const durationToUse = input.duration || 0; @@ -894,7 +831,6 @@ export async function getAvailableSlots({ input, ctx }: GetScheduleOptions): Pro return { slots: withinBoundsSlotsMappedToDate, - teamMember, }; }