/** * This route is used only by "Test Preview" button and Virtual Queues * Also, it is applicable only for sub-teams. Regular teams and user Routing Forms don't hit this endpoint. * Live mode uses findTeamMembersMatchingAttributeLogicOfRoute fn directly */ import type { App_RoutingForms_Form } from "@prisma/client"; import type { ServerResponse } from "http"; import type { NextApiResponse } from "next"; import { enrichFormWithMigrationData } from "@calcom/app-store/routing-forms/enrichFormWithMigrationData"; import { getUrlSearchParamsToForwardForTestPreview } from "@calcom/app-store/routing-forms/pages/routing-link/getUrlSearchParamsToForward"; import { entityPrismaWhereClause } from "@calcom/lib/entityPermissionUtils"; import { fromEntriesWithDuplicateKeys } from "@calcom/lib/fromEntriesWithDuplicateKeys"; import { findTeamMembersMatchingAttributeLogic } from "@calcom/lib/raqb/findTeamMembersMatchingAttributeLogic"; import { getOrderedListOfLuckyUsers } from "@calcom/lib/server/getLuckyUser"; import { EventTypeRepository } from "@calcom/lib/server/repository/eventType"; import { UserRepository } from "@calcom/lib/server/repository/user"; import type { PrismaClient } from "@calcom/prisma"; import { getAbsoluteEventTypeRedirectUrl } from "@calcom/routing-forms/getEventTypeRedirectUrl"; import { getSerializableForm } from "@calcom/routing-forms/lib/getSerializableForm"; import { getServerTimingHeader } from "@calcom/routing-forms/lib/getServerTimingHeader"; import isRouter from "@calcom/routing-forms/lib/isRouter"; import { RouteActionType } from "@calcom/routing-forms/zod"; import { TRPCError } from "@calcom/trpc/server"; import type { TrpcSessionUser } from "@calcom/trpc/server/trpc"; import type { TFindTeamMembersMatchingAttributeLogicOfRouteInputSchema } from "./findTeamMembersMatchingAttributeLogicOfRoute.schema"; interface FindTeamMembersMatchingAttributeLogicOfRouteHandlerOptions { ctx: { prisma: PrismaClient; user: NonNullable; res: ServerResponse | NextApiResponse | undefined; }; input: TFindTeamMembersMatchingAttributeLogicOfRouteInputSchema; } async function getEnrichedSerializableForm< TForm extends App_RoutingForms_Form & { user: { id: number; username: string | null; movedToProfileId: number | null; }; team: { parent: { slug: string | null; } | null; metadata: unknown; } | null; } >(form: TForm) { const formWithUserInfoProfile = { ...form, user: await UserRepository.enrichUserWithItsProfile({ user: form.user }), }; const serializableForm = await getSerializableForm({ form: enrichFormWithMigrationData(formWithUserInfoProfile), }); return serializableForm; } export const findTeamMembersMatchingAttributeLogicOfRouteHandler = async ({ ctx, input, }: FindTeamMembersMatchingAttributeLogicOfRouteHandlerOptions) => { const { prisma, user } = ctx; const { getTeamMemberEmailForResponseOrContactUsingUrlQuery } = await import( "@calcom/lib/server/getTeamMemberEmailFromCrm" ); const { formId, response, route, isPreview, _enablePerf, _concurrency } = input; const form = await prisma.app_RoutingForms_Form.findFirst({ where: { id: formId, ...entityPrismaWhereClause({ userId: user.id }), }, include: { team: { select: { parentId: true, parent: { select: { slug: true, }, }, metadata: true, }, }, user: { select: { id: true, username: true, movedToProfileId: true, }, }, }, }); if (!form) { throw new TRPCError({ code: "NOT_FOUND", message: "Form not found", }); } if (!form.teamId) { throw new TRPCError({ code: "BAD_REQUEST", message: "This form is not associated with a team", }); } const formOrgId = form.team?.parentId; if (!formOrgId) { throw new TRPCError({ code: "NOT_FOUND", message: "This form is not associated with an organization", }); } const beforeEnrichedForm = performance.now(); const serializableForm = await getEnrichedSerializableForm(form); const afterEnrichedForm = performance.now(); const timeTakenToEnrichForm = afterEnrichedForm - beforeEnrichedForm; if (!serializableForm.fields) { throw new TRPCError({ code: "NOT_FOUND", message: "Form fields not found", }); } if (!route) { throw new TRPCError({ code: "NOT_FOUND", message: "Route not found", }); } if (isRouter(route)) { throw new TRPCError({ code: "BAD_REQUEST", message: "This route is a global router which is not supported", }); } if (route.action.type !== RouteActionType.EventTypeRedirectUrl) { return { troubleshooter: null, result: null, contactOwnerEmail: null, checkedFallback: false, mainWarnings: [], fallbackWarnings: [], eventTypeRedirectUrl: null, isUsingAttributeWeights: false, }; } const eventTypeId = route.action.eventTypeId; // e.g. /team/abc/team-event-type const eventTypeRedirectPath = route.action.value; if (!eventTypeId) { // If it ever happens, should automatically be fixed by saving the form again from route-builder. // Legacy route actions do not have eventTypeId. throw new TRPCError({ code: "BAD_REQUEST", message: "The route action is missing eventTypeId.", }); } const eventType = await EventTypeRepository.findByIdIncludeHostsAndTeam({ id: eventTypeId }); if (!eventType) { throw new TRPCError({ code: "NOT_FOUND", message: "Event type not found", }); } const { teamMembersMatchingAttributeLogic: matchingTeamMembersWithResult, timeTaken: teamMembersMatchingAttributeLogicTimeTaken, troubleshooter, checkedFallback, mainAttributeLogicBuildingWarnings: mainWarnings, fallbackAttributeLogicBuildingWarnings: fallbackWarnings, } = await findTeamMembersMatchingAttributeLogic( { dynamicFieldValueOperands: { response, fields: serializableForm.fields || [], }, attributesQueryValue: route.attributesQueryValue ?? null, fallbackAttributesQueryValue: route.fallbackAttributesQueryValue ?? null, teamId: form.teamId, orgId: formOrgId, isPreview: !!isPreview, }, { enablePerf: _enablePerf, // Reuse same flag for enabling troubleshooter. We would normally use them together enableTroubleshooter: _enablePerf, concurrency: _concurrency, } ); const urlSearchParamsToForward = getUrlSearchParamsToForwardForTestPreview({ formResponse: response, fields: serializableForm.fields, attributeRoutingConfig: route.attributeRoutingConfig ?? null, teamMembersMatchingAttributeLogic: matchingTeamMembersWithResult ? matchingTeamMembersWithResult.map((member) => member.userId) : [], }); const eventTypeRedirectUrl = getAbsoluteEventTypeRedirectUrl({ eventTypeRedirectUrl: eventTypeRedirectPath, form: serializableForm, allURLSearchParams: urlSearchParamsToForward, }); const timeBeforeCrm = performance.now(); const { email: contactOwnerEmail } = await getTeamMemberEmailForResponseOrContactUsingUrlQuery({ query: fromEntriesWithDuplicateKeys(urlSearchParamsToForward.entries()), eventData: eventType, chosenRoute: route, }); const timeAfterCrm = performance.now(); const timeTaken: Record = { ...teamMembersMatchingAttributeLogicTimeTaken, crm: timeAfterCrm - timeBeforeCrm, enrichForm: timeTakenToEnrichForm, }; if (!matchingTeamMembersWithResult) { return { contactOwnerEmail, troubleshooter, checkedFallback, mainWarnings, fallbackWarnings, eventTypeRedirectUrl, isUsingAttributeWeights: false, result: null, }; } const matchingTeamMembersIds = matchingTeamMembersWithResult.map((member) => member.userId); const matchingTeamMembers = await UserRepository.findByIds({ ids: matchingTeamMembersIds }); const matchingHosts = eventType.hosts.filter((host) => matchingTeamMembersIds.includes(host.user.id)); if (matchingTeamMembers.length !== matchingHosts.length) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", message: "Looks like not all matching team members are assigned to the event", }); } const timeBeforeGetOrderedLuckyUsers = performance.now(); const { users: orderedLuckyUsers, perUserData, isUsingAttributeWeights, } = matchingTeamMembers.length ? await getOrderedListOfLuckyUsers({ // Assuming all are available availableUsers: [ { ...matchingHosts[0].user, weight: matchingHosts[0].weight, priority: matchingHosts[0].priority, }, ...matchingHosts.slice(1).map((host) => ({ ...host.user, weight: host.weight, priority: host.priority, })), ], eventType, allRRHosts: matchingHosts, routingFormResponse: { response, form, chosenRouteId: route.id, }, }) : { users: [], perUserData: null, isUsingAttributeWeights: false }; const timeAfterGetOrderedLuckyUsers = performance.now(); timeTaken.getOrderedLuckyUsers = timeAfterGetOrderedLuckyUsers - timeBeforeGetOrderedLuckyUsers; console.log("_enablePerf, _concurrency", _enablePerf, _concurrency); if (_enablePerf) { const serverTimingHeader = getServerTimingHeader(timeTaken); ctx.res?.setHeader("Server-Timing", serverTimingHeader); console.log("Server-Timing", serverTimingHeader); } return { troubleshooter, contactOwnerEmail, checkedFallback, mainWarnings, fallbackWarnings, result: { users: orderedLuckyUsers.map((user) => ({ id: user.id, name: user.name, email: user.email, })), perUserData, }, isUsingAttributeWeights, eventTypeRedirectUrl, }; }; export default findTeamMembersMatchingAttributeLogicOfRouteHandler;