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:
co-authored by
Hariom Balhara
parent
30367cd5f3
commit
48016e0f3e
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user