diff --git a/apps/web/lib/team/[slug]/[type]/getServerSideProps.tsx b/apps/web/lib/team/[slug]/[type]/getServerSideProps.tsx index d6fb2413cc..52f2e3ea71 100644 --- a/apps/web/lib/team/[slug]/[type]/getServerSideProps.tsx +++ b/apps/web/lib/team/[slug]/[type]/getServerSideProps.tsx @@ -63,17 +63,36 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => const fromRedirectOfNonOrgLink = context.query.orgRedirection === "true"; const isUnpublished = team.parent ? !team.parent.slug : !team.slug; - const { getTeamMemberEmailForResponseOrContactUsingUrlQuery } = await import( - "@calcom/lib/server/getTeamMemberEmailFromCrm" - ); - const { - email: teamMemberEmail, - recordType: crmOwnerRecordType, - crmAppSlug, - } = await getTeamMemberEmailForResponseOrContactUsingUrlQuery({ - query, - eventData, - }); + const crmContactOwnerEmail = query["cal.crmContactOwnerEmail"]; + const crmContactOwnerRecordType = query["cal.crmContactOwnerRecordType"]; + const crmAppSlugParam = query["cal.crmAppSlug"]; + + // Handle string[] type from query params + let teamMemberEmail = Array.isArray(crmContactOwnerEmail) ? crmContactOwnerEmail[0] : crmContactOwnerEmail; + + let crmOwnerRecordType = Array.isArray(crmContactOwnerRecordType) + ? crmContactOwnerRecordType[0] + : crmContactOwnerRecordType; + + let crmAppSlug = Array.isArray(crmAppSlugParam) ? crmAppSlugParam[0] : crmAppSlugParam; + + if (!teamMemberEmail || !crmOwnerRecordType || !crmAppSlug) { + const { getTeamMemberEmailForResponseOrContactUsingUrlQuery } = await import( + "@calcom/lib/server/getTeamMemberEmailFromCrm" + ); + const { + email, + recordType, + crmAppSlug: crmAppSlugQuery, + } = await getTeamMemberEmailForResponseOrContactUsingUrlQuery({ + query, + eventData, + }); + + teamMemberEmail = email ?? undefined; + crmOwnerRecordType = recordType ?? undefined; + crmAppSlug = crmAppSlugQuery ?? undefined; + } const organizationSettings = getOrganizationSEOSettings(team); const allowSEOIndexing = organizationSettings?.allowSEOIndexing ?? false; diff --git a/apps/web/server/lib/router/getServerSideProps.ts b/apps/web/server/lib/router/getServerSideProps.ts index fbc5d3de81..47990b46f9 100644 --- a/apps/web/server/lib/router/getServerSideProps.ts +++ b/apps/web/server/lib/router/getServerSideProps.ts @@ -1,7 +1,11 @@ +import { wrapGetServerSidePropsWithSentry } from "@sentry/nextjs"; import type { GetServerSidePropsContext } from "next"; import { getRoutedUrl } from "@calcom/lib/server/getRoutedUrl"; -export const getServerSideProps = async function getServerSideProps(context: GetServerSidePropsContext) { - return getRoutedUrl(context); -}; +export const getServerSideProps = wrapGetServerSidePropsWithSentry(async function getServerSideProps( + context: GetServerSidePropsContext +) { + return await getRoutedUrl(context); +}, +"/router"); diff --git a/packages/app-store/routing-forms/lib/crmRouting/routerGetCrmContactOwnerEmail.ts b/packages/app-store/routing-forms/lib/crmRouting/routerGetCrmContactOwnerEmail.ts new file mode 100644 index 0000000000..86fccf7c18 --- /dev/null +++ b/packages/app-store/routing-forms/lib/crmRouting/routerGetCrmContactOwnerEmail.ts @@ -0,0 +1,78 @@ +import type { z } from "zod"; + +import { getCRMContactOwnerForRRLeadSkip } from "@calcom/app-store/_utils/CRMRoundRobinSkip"; +import { EventTypeRepository } from "@calcom/lib/server/repository/eventType"; +import { SchedulingType } from "@calcom/prisma/enums"; +import type { ZResponseInputSchema } from "@calcom/trpc/server/routers/viewer/routing-forms/response.schema"; + +import type { LocalRoute } from "../../types/types"; +import { enabledAppSlugs } from "../enabledApps"; + +export default async function routerGetCrmContactOwnerEmail({ + attributeRoutingConfig, + response, + action, +}: { + attributeRoutingConfig: LocalRoute["attributeRoutingConfig"]; + response: z.infer["response"]; + action: LocalRoute["action"]; +}) { + // Check if route is skipping CRM contact check + if (attributeRoutingConfig?.skipContactOwner) return null; + + // Check if email is present + let prospectEmail: string | null = null; + for (const field of Object.keys(response)) { + const fieldResponse = response[field]; + if (fieldResponse.identifier === "email") { + prospectEmail = fieldResponse.value as string; + break; + } + } + if (!prospectEmail) return null; + + // Determine if the action is an event type redirect + if (action.type !== "eventTypeRedirectUrl" || !action.eventTypeId) return null; + + const eventType = await EventTypeRepository.findByIdIncludeHostsAndTeam({ id: action.eventTypeId }); + if (!eventType || eventType.schedulingType !== SchedulingType.ROUND_ROBIN) return null; + + const eventTypeMetadata = eventType.metadata; + if (!eventTypeMetadata) return null; + + let contactOwner: { email: string | null; recordType: string | null; crmAppSlug: string | null } = { + email: null, + recordType: null, + crmAppSlug: null, + }; + // Determine if there is a CRM option enabled in the chosen route + for (const appSlug of enabledAppSlugs) { + const routingOptions = attributeRoutingConfig?.[appSlug as keyof typeof attributeRoutingConfig]; + + if (!routingOptions) continue; + // See if any options are true + if (Object.values(routingOptions).some((option) => option === true)) { + const appBookingFormHandler = (await import("@calcom/app-store/routing-forms/appBookingFormHandler")) + .default; + const appHandler = appBookingFormHandler[appSlug as keyof typeof appBookingFormHandler]; + + const ownerQuery = await appHandler(prospectEmail, attributeRoutingConfig, action.eventTypeId); + + if (ownerQuery?.email) { + contactOwner = { ...ownerQuery, crmAppSlug: appSlug }; + break; + } + } + } + + if (!contactOwner) { + const ownerQuery = await getCRMContactOwnerForRRLeadSkip(prospectEmail, eventTypeMetadata); + if (ownerQuery?.email) contactOwner = ownerQuery; + } + if (!contactOwner) return null; + + // Check that the contact owner is a part of the event type + if (!eventType.hosts.some((host) => host.user.email === contactOwner.email)) return null; + + return contactOwner; +} diff --git a/packages/app-store/routing-forms/lib/handleResponse.ts b/packages/app-store/routing-forms/lib/handleResponse.ts index 91f7bd9d61..8f0ab7784f 100644 --- a/packages/app-store/routing-forms/lib/handleResponse.ts +++ b/packages/app-store/routing-forms/lib/handleResponse.ts @@ -5,6 +5,7 @@ import { emailSchema } from "@calcom/lib/emailSchema"; import logger from "@calcom/lib/logger"; import { findTeamMembersMatchingAttributeLogic } from "@calcom/lib/raqb/findTeamMembersMatchingAttributeLogic"; import { safeStringify } from "@calcom/lib/safeStringify"; +import monitorCallbackAsync from "@calcom/lib/sentryWrapper"; import { prisma } from "@calcom/prisma"; import type { App_RoutingForms_Form } from "@calcom/prisma/client"; import { RoutingFormSettings } from "@calcom/prisma/zod-utils"; @@ -15,6 +16,7 @@ import { TRPCError } from "@trpc/server"; import isRouter from "../lib/isRouter"; import { onFormSubmission } from "../trpc/utils"; import type { FormResponse, SerializableForm } from "../types/types"; +import routerGetCrmContactOwnerEmail from "./crmRouting/routerGetCrmContactOwnerEmail"; export type Form = SerializableForm< App_RoutingForms_Form & { @@ -30,7 +32,11 @@ export type Form = SerializableForm< const moduleLogger = logger.getSubLogger({ prefix: ["routing-forms/lib/handleResponse"] }); -export const handleResponse = async ({ +export const handleResponse = (...args: Parameters) => { + return monitorCallbackAsync(_handleResponse, ...args); +}; + +const _handleResponse = async ({ response, form, // Unused but probably should be used @@ -119,6 +125,9 @@ export const handleResponse = async ({ const chosenRoute = serializableFormWithFields.routes?.find((route) => route.id === chosenRouteId); let teamMemberIdsMatchingAttributeLogic: number[] | null = null; + let crmContactOwnerEmail: string | null = null; + let crmContactOwnerRecordType: string | null = null; + let crmAppSlug: string | null = null; let timeTaken: Record = {}; if (chosenRoute) { if (isRouter(chosenRoute)) { @@ -128,38 +137,55 @@ export const handleResponse = async ({ }); } - const teamMembersMatchingAttributeLogicWithResult = - formTeamId && formOrgId - ? await findTeamMembersMatchingAttributeLogic( - { - dynamicFieldValueOperands: { - response, - fields: form.fields || [], - }, - attributesQueryValue: chosenRoute.attributesQueryValue ?? null, - fallbackAttributesQueryValue: chosenRoute.fallbackAttributesQueryValue, - teamId: formTeamId, - orgId: formOrgId, - }, - { - enablePerf: true, - } - ) - : null; + const getRoutedMembers = async () => + await Promise.all([ + (async () => { + const contactOwnerQuery = await routerGetCrmContactOwnerEmail({ + attributeRoutingConfig: chosenRoute.attributeRoutingConfig, + response, + action: chosenRoute.action, + }); + crmContactOwnerEmail = contactOwnerQuery?.email ?? null; + crmContactOwnerRecordType = contactOwnerQuery?.recordType ?? null; + crmAppSlug = contactOwnerQuery?.crmAppSlug ?? null; + })(), + (async () => { + const teamMembersMatchingAttributeLogicWithResult = + formTeamId && formOrgId + ? await findTeamMembersMatchingAttributeLogic( + { + dynamicFieldValueOperands: { + response, + fields: form.fields || [], + }, + attributesQueryValue: chosenRoute.attributesQueryValue ?? null, + fallbackAttributesQueryValue: chosenRoute.fallbackAttributesQueryValue, + teamId: formTeamId, + orgId: formOrgId, + }, + { + enablePerf: true, + } + ) + : null; - moduleLogger.debug( - "teamMembersMatchingAttributeLogic", - safeStringify({ teamMembersMatchingAttributeLogicWithResult }) - ); + moduleLogger.debug( + "teamMembersMatchingAttributeLogic", + safeStringify({ teamMembersMatchingAttributeLogicWithResult }) + ); - teamMemberIdsMatchingAttributeLogic = - teamMembersMatchingAttributeLogicWithResult?.teamMembersMatchingAttributeLogic - ? teamMembersMatchingAttributeLogicWithResult.teamMembersMatchingAttributeLogic.map( - (member) => member.userId - ) - : null; + teamMemberIdsMatchingAttributeLogic = + teamMembersMatchingAttributeLogicWithResult?.teamMembersMatchingAttributeLogic + ? teamMembersMatchingAttributeLogicWithResult.teamMembersMatchingAttributeLogic.map( + (member) => member.userId + ) + : null; - timeTaken = teamMembersMatchingAttributeLogicWithResult?.timeTaken ?? {}; + timeTaken = teamMembersMatchingAttributeLogicWithResult?.timeTaken ?? {}; + })(), + ]); + + await monitorCallbackAsync(getRoutedMembers); } else { // It currently happens for a Router route. Such a route id isn't present in the form.routes } @@ -176,7 +202,8 @@ export const handleResponse = async ({ }, }); - await onFormSubmission( + await monitorCallbackAsync( + onFormSubmission, { ...serializableFormWithFields, userWithEmails }, dbFormResponse.response as FormResponse, dbFormResponse.id, @@ -199,6 +226,9 @@ export const handleResponse = async ({ isPreview: !!isPreview, formResponse: dbFormResponse, teamMembersMatchingAttributeLogic: teamMemberIdsMatchingAttributeLogic, + crmContactOwnerEmail, + crmContactOwnerRecordType, + crmAppSlug, attributeRoutingConfig: chosenRoute ? "attributeRoutingConfig" in chosenRoute ? chosenRoute.attributeRoutingConfig diff --git a/packages/app-store/routing-forms/pages/routing-link/[...appPages].tsx b/packages/app-store/routing-forms/pages/routing-link/[...appPages].tsx index 7ea9002152..fa9573fa5c 100644 --- a/packages/app-store/routing-forms/pages/routing-link/[...appPages].tsx +++ b/packages/app-store/routing-forms/pages/routing-link/[...appPages].tsx @@ -98,7 +98,14 @@ function RoutingForm({ form, profile, ...restProps }: Props) { const responseMutation = trpc.viewer.routingForms.public.response.useMutation({ onSuccess: async (data) => { - const { teamMembersMatchingAttributeLogic, formResponse, attributeRoutingConfig } = data; + const { + teamMembersMatchingAttributeLogic, + formResponse, + attributeRoutingConfig, + crmContactOwnerEmail, + crmContactOwnerRecordType, + crmAppSlug, + } = data; const chosenRouteWithFormResponse = chosenRouteWithFormResponseRef.current; if (!chosenRouteWithFormResponse) { return; @@ -114,6 +121,9 @@ function RoutingForm({ form, profile, ...restProps }: Props) { searchParams: new URLSearchParams(window.location.search), teamMembersMatchingAttributeLogic, attributeRoutingConfig: attributeRoutingConfig ?? null, + crmContactOwnerEmail, + crmContactOwnerRecordType, + crmAppSlug, }); const chosenRoute = chosenRouteWithFormResponse.route; const decidedAction = chosenRoute.action; diff --git a/packages/app-store/routing-forms/pages/routing-link/getUrlSearchParamsToForward.ts b/packages/app-store/routing-forms/pages/routing-link/getUrlSearchParamsToForward.ts index 0a8eb8332f..29ae84668b 100644 --- a/packages/app-store/routing-forms/pages/routing-link/getUrlSearchParamsToForward.ts +++ b/packages/app-store/routing-forms/pages/routing-link/getUrlSearchParamsToForward.ts @@ -26,6 +26,9 @@ type GetUrlSearchParamsToForwardOptions = { reroutingFormResponses?: FormResponseValueOnly; teamId?: number | null; orgId?: number | null; + crmContactOwnerEmail?: string | null; + crmContactOwnerRecordType?: string | null; + crmAppSlug?: string | null; }; export function getUrlSearchParamsToForward({ @@ -38,6 +41,9 @@ export function getUrlSearchParamsToForward({ reroutingFormResponses, teamId, orgId, + crmContactOwnerEmail, + crmContactOwnerRecordType, + crmAppSlug, }: GetUrlSearchParamsToForwardOptions) { type Params = Record; const paramsFromResponse: Params = {}; @@ -107,6 +113,9 @@ export function getUrlSearchParamsToForward({ const allQueryParams: Params = { ...(teamId && { ["cal.teamId"]: `${teamId}` }), ...(orgId && { ["cal.orgId"]: `${orgId}` }), + ...(crmContactOwnerEmail && { ["cal.crmContactOwnerEmail"]: `${crmContactOwnerEmail}` }), + ...(crmContactOwnerRecordType && { ["cal.crmContactOwnerRecordType"]: `${crmContactOwnerRecordType}` }), + ...(crmAppSlug && { ["cal.crmAppSlug"]: `${crmAppSlug}` }), ...paramsFromCurrentUrl, // In case of conflict b/w paramsFromResponse and paramsFromCurrentUrl, paramsFromResponse should win as the booker probably improved upon the prefilled value. ...paramsFromResponse, diff --git a/packages/lib/server/getRoutedUrl.ts b/packages/lib/server/getRoutedUrl.ts index 9fcd8071a6..3b41822a93 100644 --- a/packages/lib/server/getRoutedUrl.ts +++ b/packages/lib/server/getRoutedUrl.ts @@ -17,6 +17,7 @@ import type { FormResponse } from "@calcom/app-store/routing-forms/types/types"; import { orgDomainConfig } from "@calcom/features/ee/organizations/lib/orgDomains"; import { isAuthorizedToViewFormOnOrgDomain } from "@calcom/features/routing-forms/lib/isAuthorizedToViewForm"; import logger from "@calcom/lib/logger"; +import monitorCallbackAsync from "@calcom/lib/sentryWrapper"; import { RoutingFormRepository } from "@calcom/lib/server/repository/routingForm"; import { TRPCError } from "@trpc/server"; @@ -33,7 +34,11 @@ function hasEmbedPath(pathWithQuery: string) { return onlyPath.endsWith("/embed") || onlyPath.endsWith("/embed/"); } -export const getRoutedUrl = async (context: Pick) => { +export const getRoutedUrl = (context: Pick) => { + return monitorCallbackAsync(_getRoutedUrl, context); +}; + +const _getRoutedUrl = async (context: Pick) => { const queryParsed = querySchema.safeParse(context.query); const isEmbed = hasEmbedPath(context.req.url || ""); const pageProps = { @@ -118,6 +123,9 @@ export const getRoutedUrl = async (context: Pick