* update to consider sendToAll * added tests for team routingForms selectAllMembers cases * chore * chore * chore * updated test case in case of toggle off * tweak --------- Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com> Co-authored-by: unknown <adhabal2002@gmail.com>
219 lines
6.8 KiB
TypeScript
219 lines
6.8 KiB
TypeScript
import { Prisma } from "@prisma/client";
|
|
import { z } from "zod";
|
|
|
|
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 { prisma } from "@calcom/prisma";
|
|
import type { App_RoutingForms_Form } from "@calcom/prisma/client";
|
|
import { RoutingFormSettings } from "@calcom/prisma/zod-utils";
|
|
import { TRPCError } from "@calcom/trpc/server";
|
|
import type { ZResponseInputSchema } from "@calcom/trpc/server/routers/viewer/routing-forms/response.schema";
|
|
|
|
import isRouter from "../lib/isRouter";
|
|
import { onFormSubmission } from "../trpc/utils";
|
|
import type { FormResponse, SerializableForm } from "../types/types";
|
|
|
|
export type Form = SerializableForm<
|
|
App_RoutingForms_Form & {
|
|
user: {
|
|
id: number;
|
|
email: string;
|
|
};
|
|
team: {
|
|
parentId: number | null;
|
|
} | null;
|
|
}
|
|
>;
|
|
|
|
const moduleLogger = logger.getSubLogger({ prefix: ["routing-forms/lib/handleResponse"] });
|
|
|
|
export const handleResponse = async ({
|
|
response,
|
|
form,
|
|
// Unused but probably should be used
|
|
// formFillerId,
|
|
chosenRouteId,
|
|
isPreview,
|
|
}: {
|
|
response: z.infer<typeof ZResponseInputSchema>["response"];
|
|
form: Form;
|
|
formFillerId: string;
|
|
chosenRouteId: string | null;
|
|
isPreview: boolean;
|
|
}) => {
|
|
try {
|
|
if (!form.fields) {
|
|
// There is no point in submitting a form that doesn't have fields defined
|
|
throw new TRPCError({
|
|
code: "BAD_REQUEST",
|
|
});
|
|
}
|
|
|
|
const formTeamId = form.teamId;
|
|
const formOrgId = form.team?.parentId ?? null;
|
|
const serializableFormWithFields = {
|
|
...form,
|
|
fields: form.fields,
|
|
};
|
|
|
|
const missingFields = serializableFormWithFields.fields
|
|
.filter((field) => !(field.required ? response[field.id]?.value : true))
|
|
.map((f) => f.label);
|
|
|
|
if (missingFields.length) {
|
|
throw new TRPCError({
|
|
code: "BAD_REQUEST",
|
|
message: `Missing required fields ${missingFields.join(", ")}`,
|
|
});
|
|
}
|
|
const invalidFields = serializableFormWithFields.fields
|
|
.filter((field) => {
|
|
const fieldValue = response[field.id]?.value;
|
|
// The field isn't required at this point. Validate only if it's set
|
|
if (!fieldValue) {
|
|
return false;
|
|
}
|
|
let schema;
|
|
if (field.type === "email") {
|
|
schema = emailSchema;
|
|
} else if (field.type === "phone") {
|
|
schema = z.any();
|
|
} else {
|
|
schema = z.any();
|
|
}
|
|
return !schema.safeParse(fieldValue).success;
|
|
})
|
|
.map((f) => ({ label: f.label, type: f.type, value: response[f.id]?.value }));
|
|
|
|
if (invalidFields.length) {
|
|
throw new TRPCError({
|
|
code: "BAD_REQUEST",
|
|
message: `Invalid value for fields ${invalidFields
|
|
.map((f) => `'${f.label}' with value '${f.value}' should be valid ${f.type}`)
|
|
.join(", ")}`,
|
|
});
|
|
}
|
|
|
|
const settings = RoutingFormSettings.parse(form.settings);
|
|
let userWithEmails: string[] = [];
|
|
if (form.teamId && (settings?.sendToAll || settings?.sendUpdatesTo?.length)) {
|
|
const whereClause: Prisma.MembershipWhereInput = { teamId: form.teamId };
|
|
if (!settings?.sendToAll) {
|
|
whereClause.userId = { in: settings.sendUpdatesTo };
|
|
}
|
|
const userEmails = await prisma.membership.findMany({
|
|
where: whereClause,
|
|
select: {
|
|
user: {
|
|
select: {
|
|
email: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
userWithEmails = userEmails.map((userEmail) => userEmail.user.email);
|
|
}
|
|
|
|
const chosenRoute = serializableFormWithFields.routes?.find((route) => route.id === chosenRouteId);
|
|
let teamMemberIdsMatchingAttributeLogic: number[] | null = null;
|
|
let timeTaken: Record<string, number | null> = {};
|
|
if (chosenRoute) {
|
|
if (isRouter(chosenRoute)) {
|
|
throw new TRPCError({
|
|
code: "BAD_REQUEST",
|
|
message: "Chosen route is a router",
|
|
});
|
|
}
|
|
|
|
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 })
|
|
);
|
|
|
|
teamMemberIdsMatchingAttributeLogic =
|
|
teamMembersMatchingAttributeLogicWithResult?.teamMembersMatchingAttributeLogic
|
|
? teamMembersMatchingAttributeLogicWithResult.teamMembersMatchingAttributeLogic.map(
|
|
(member) => member.userId
|
|
)
|
|
: null;
|
|
|
|
timeTaken = teamMembersMatchingAttributeLogicWithResult?.timeTaken ?? {};
|
|
} else {
|
|
// It currently happens for a Router route. Such a route id isn't present in the form.routes
|
|
}
|
|
|
|
let dbFormResponse;
|
|
if (!isPreview) {
|
|
dbFormResponse = await prisma.app_RoutingForms_FormResponse.create({
|
|
data: {
|
|
// TODO: Why do we not save formFillerId available in the input?
|
|
// formFillerId,
|
|
formId: form.id,
|
|
response: response,
|
|
chosenRouteId,
|
|
},
|
|
});
|
|
|
|
await onFormSubmission(
|
|
{ ...serializableFormWithFields, userWithEmails },
|
|
dbFormResponse.response as FormResponse,
|
|
dbFormResponse.id,
|
|
chosenRoute ? ("action" in chosenRoute ? chosenRoute.action : undefined) : undefined
|
|
);
|
|
} else {
|
|
moduleLogger.debug("Dry run mode - Form response not stored and also webhooks and emails not sent");
|
|
// Create a mock response for dry run
|
|
dbFormResponse = {
|
|
id: 0,
|
|
formId: form.id,
|
|
response,
|
|
chosenRouteId,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
};
|
|
}
|
|
|
|
return {
|
|
isPreview: !!isPreview,
|
|
formResponse: dbFormResponse,
|
|
teamMembersMatchingAttributeLogic: teamMemberIdsMatchingAttributeLogic,
|
|
attributeRoutingConfig: chosenRoute
|
|
? "attributeRoutingConfig" in chosenRoute
|
|
? chosenRoute.attributeRoutingConfig
|
|
: null
|
|
: null,
|
|
timeTaken,
|
|
};
|
|
} catch (e) {
|
|
if (e instanceof Prisma.PrismaClientKnownRequestError) {
|
|
if (e.code === "P2002") {
|
|
throw new TRPCError({
|
|
code: "CONFLICT",
|
|
});
|
|
}
|
|
}
|
|
throw e;
|
|
}
|
|
};
|