Skip to content

Commit

Permalink
fix: Set teamMemberEmail server side for CRM RR Skip (#16367)
Browse files Browse the repository at this point in the history
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
Co-authored-by: zomars <zomars@me.com>
  • Loading branch information
3 people committed Sep 4, 2024
1 parent 45c3724 commit af7347e
Show file tree
Hide file tree
Showing 17 changed files with 140 additions and 114 deletions.
38 changes: 34 additions & 4 deletions apps/web/lib/team/[slug]/[type]/getServerSideProps.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -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);
Expand Down Expand Up @@ -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<string | null> {
// 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;
}
2 changes: 2 additions & 0 deletions apps/web/pages/team/[slug]/[type].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export default function Type({
eventData,
isInstantMeeting,
orgBannerUrl,
teamMemberEmail,
}: PageProps) {
const searchParams = useSearchParams();

Expand Down Expand Up @@ -67,6 +68,7 @@ export default function Type({
eventData.length
)}
orgBannerUrl={orgBannerUrl}
teamMemberEmail={teamMemberEmail}
/>
</main>
);
Expand Down
15 changes: 3 additions & 12 deletions apps/web/test/lib/getSchedule.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`],
Expand All @@ -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(
[
Expand Down Expand Up @@ -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(
[
Expand All @@ -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`],
Expand Down
61 changes: 61 additions & 0 deletions packages/app-store/_utils/CRMRoundRobinSkip.ts
Original file line number Diff line number Diff line change
@@ -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<string | undefined> {
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<typeof EventTypeAppMetadataSchema>) {
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);
}
6 changes: 3 additions & 3 deletions packages/core/crmManager/crmManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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;
}
}
3 changes: 2 additions & 1 deletion packages/core/getUserAvailability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -839,13 +839,14 @@ const _getOutOfOfficeDays = async ({

type GetUserAvailabilityQuery = Parameters<typeof getUserAvailability>[0];
type GetUserAvailabilityInitialData = NonNullable<Parameters<typeof getUserAvailability>[1]>;
export type GetAvailabilityUser = NonNullable<GetUserAvailabilityInitialData["user"]>;

const _getUsersAvailability = async ({
users,
query,
initialData,
}: {
users: (NonNullable<GetUserAvailabilityInitialData["user"]> & {
users: (GetAvailabilityUser & {
currentBookings?: GetUserAvailabilityInitialData["currentBookings"];
})[];
query: Omit<GetUserAvailabilityQuery, "userId" | "username">;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export interface IUseBookings {
hashedLink?: string | null;
bookingForm: UseBookingFormReturnType["bookingForm"];
metadata: Record<string, string>;
teamMemberEmail?: string;
teamMemberEmail?: string | null;
}

const getBookingSuccessfulEventPayload = (booking: {
Expand Down Expand Up @@ -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,
Expand Down
11 changes: 10 additions & 1 deletion packages/features/bookings/Booker/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type StoreInitializeType = {
durationConfig?: number[] | null;
org?: string | null;
isInstantMeeting?: boolean;
teamMemberEmail?: string | null;
};

type SeatedEventData = {
Expand Down Expand Up @@ -148,6 +149,8 @@ export type BookerStore = {

org?: string | null;
setOrg: (org: string | null | undefined) => void;

teamMemberEmail?: string | null;
};

/**
Expand Down Expand Up @@ -253,6 +256,7 @@ export const useBookerStore = create<BookerStore>((set, get) => ({
durationConfig,
org,
isInstantMeeting,
teamMemberEmail,
}: StoreInitializeType) => {
const selectedDateInStore = get().selectedDate;

Expand All @@ -265,7 +269,8 @@ export const useBookerStore = create<BookerStore>((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({
Expand All @@ -284,6 +289,7 @@ export const useBookerStore = create<BookerStore>((set, get) => ({
selectedDate:
selectedDateInStore ||
(["week_view", "column_view"].includes(layout) ? dayjs().format("YYYY-MM-DD") : null),
teamMemberEmail,
});

if (durationConfig?.includes(Number(getQueryParam("duration")))) {
Expand Down Expand Up @@ -372,6 +378,7 @@ export const useInitializeBookerStore = ({
durationConfig,
org,
isInstantMeeting,
teamMemberEmail,
}: StoreInitializeType) => {
const initializeStore = useBookerStore((state) => state.initialize);
useEffect(() => {
Expand All @@ -389,6 +396,7 @@ export const useInitializeBookerStore = ({
verifiedEmail,
durationConfig,
isInstantMeeting,
teamMemberEmail,
});
}, [
initializeStore,
Expand All @@ -405,5 +413,6 @@ export const useInitializeBookerStore = ({
verifiedEmail,
durationConfig,
isInstantMeeting,
teamMemberEmail,
]);
};
1 change: 1 addition & 0 deletions packages/features/bookings/Booker/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ export interface BookerProps {
*/
hashedLink?: string | null;
isInstantMeeting?: boolean;
teamMemberEmail?: string | null;
}

export type WrappedBookerPropsMain = {
Expand Down
6 changes: 3 additions & 3 deletions packages/features/bookings/Booker/utils/event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export const useScheduleForEvent = ({
dayCount,
selectedDate,
orgSlug,
bookerEmail,
teamMemberEmail,
}: {
prefetchNextMonth?: boolean;
username?: string | null;
Expand All @@ -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();
Expand Down Expand Up @@ -107,7 +107,7 @@ export const useScheduleForEvent = ({
duration: durationFromStore ?? duration,
isTeamEvent: pathname?.indexOf("/team/") !== -1 || isTeam,
orgSlug,
bookerEmail,
teamMemberEmail,
});

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export type BookingOptions = {
bookingUid?: string;
seatReferenceUid?: string;
hashedLink?: string | null;
teamMemberEmail?: string;
teamMemberEmail?: string | null;
orgSlug?: string;
};

Expand Down
6 changes: 3 additions & 3 deletions packages/features/schedules/lib/use-schedule/useSchedule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export type UseScheduleWithCacheArgs = {
rescheduleUid?: string | null;
isTeamEvent?: boolean;
orgSlug?: string;
bookerEmail?: string;
teamMemberEmail?: string | null;
};

export const useSchedule = ({
Expand All @@ -33,7 +33,7 @@ export const useSchedule = ({
rescheduleUid,
isTeamEvent,
orgSlug,
bookerEmail,
teamMemberEmail,
}: UseScheduleWithCacheArgs) => {
const [startTime, endTime] = useTimesForSchedule({
month,
Expand All @@ -60,7 +60,7 @@ export const useSchedule = ({
duration: duration ? `${duration}` : undefined,
rescheduleUid,
orgSlug,
bookerEmail,
teamMemberEmail,
},
{
trpc: {
Expand Down
Loading

0 comments on commit af7347e

Please sign in to comment.