chore: Move CRM owner calls handleResponse (#19316)

* Handle normal ownership routing

* Skip contact owner check if true

* Fix typos

* Call getting contact owner in promise.all in handleResponse

* Pass crm contact owner data to params

* Add crm contact params

* Only get CRM owner calls if params don't exist

* Handle if properties don't exist

* Fix typo

* Pass CRM specific variables to getServerSideProps

* Remove logs

* Type fixes

* Use `wrapGetServerSIdePropsWIthSentry` in router getServerSideProps

* Add sentry monitoring

* Type fixes

* Type fix

* Type fixes

* Update imports for sentryWrapper

---------

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
This commit is contained in:
Joe Au-Yeung
2025-03-28 10:42:28 -04:00
committed by GitHub
co-authored by Hariom Balhara
parent 30367cd5f3
commit 48016e0f3e
7 changed files with 211 additions and 48 deletions
@@ -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;
@@ -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");
@@ -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<typeof ZResponseInputSchema>["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;
}
@@ -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<typeof _handleResponse>) => {
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<string, number | null> = {};
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
@@ -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;
@@ -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<string, string | string[]>;
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,
+15 -2
View File
@@ -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<GetServerSidePropsContext, "query" | "req">) => {
export const getRoutedUrl = (context: Pick<GetServerSidePropsContext, "query" | "req">) => {
return monitorCallbackAsync(_getRoutedUrl, context);
};
const _getRoutedUrl = async (context: Pick<GetServerSidePropsContext, "query" | "req">) => {
const queryParsed = querySchema.safeParse(context.query);
const isEmbed = hasEmbedPath(context.req.url || "");
const pageProps = {
@@ -118,6 +123,9 @@ export const getRoutedUrl = async (context: Pick<GetServerSidePropsContext, "que
let teamMembersMatchingAttributeLogic = null;
let formResponseId = null;
let attributeRoutingConfig = null;
let crmContactOwnerEmail: string | null = null;
let crmContactOwnerRecordType: string | null = null;
let crmAppSlug: string | null = null;
try {
const result = await handleResponse({
form: serializableForm,
@@ -133,6 +141,9 @@ export const getRoutedUrl = async (context: Pick<GetServerSidePropsContext, "que
...timeTaken,
...result.timeTaken,
};
crmContactOwnerEmail = result.crmContactOwnerEmail;
crmContactOwnerRecordType = result.crmContactOwnerRecordType;
crmAppSlug = result.crmAppSlug;
} catch (e) {
if (e instanceof TRPCError) {
return {
@@ -163,7 +174,6 @@ export const getRoutedUrl = async (context: Pick<GetServerSidePropsContext, "que
response,
serializableForm.fields
);
return {
redirect: {
destination: getAbsoluteEventTypeRedirectUrlWithEmbedSupport({
@@ -181,6 +191,9 @@ export const getRoutedUrl = async (context: Pick<GetServerSidePropsContext, "que
attributeRoutingConfig: attributeRoutingConfig ?? null,
teamId: form?.teamId,
orgId: form.team?.parentId,
crmContactOwnerEmail,
crmContactOwnerRecordType,
crmAppSlug,
}),
isEmbed: pageProps.isEmbed,
}),