416 lines
9.4 KiB
TypeScript
416 lines
9.4 KiB
TypeScript
import type { z } from "zod";
|
|
|
|
import { isSMSOrWhatsappAction } from "@calcom/ee/workflows/lib/actionHelperFunctions";
|
|
import emailRatingTemplate from "@calcom/ee/workflows/lib/reminders/templates/emailRatingTemplate";
|
|
import emailReminderTemplate from "@calcom/ee/workflows/lib/reminders/templates/emailReminderTemplate";
|
|
import {
|
|
getSmsReminderNumberField,
|
|
getSmsReminderNumberSource,
|
|
getAIAgentCallPhoneNumberField,
|
|
getAIAgentCallPhoneNumberSource,
|
|
} from "@calcom/features/bookings/lib/getBookingFields";
|
|
import { removeBookingField, upsertBookingField } from "@calcom/features/eventtypes/lib/bookingFieldsManager";
|
|
import { SMS_REMINDER_NUMBER_FIELD, CAL_AI_AGENT_PHONE_NUMBER_FIELD } from "@calcom/lib/bookings/SystemField";
|
|
import { SENDER_ID, SENDER_NAME } from "@calcom/lib/constants";
|
|
import { getTranslation } from "@calcom/i18n/server";
|
|
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
|
|
import prisma from "@calcom/prisma";
|
|
import type { WorkflowStep } from "@calcom/prisma/client";
|
|
import { WorkflowTemplates } from "@calcom/prisma/enums";
|
|
import { WorkflowActions } from "@calcom/prisma/enums";
|
|
|
|
import type { ZWorkflows } from "./getAllActiveWorkflows.schema";
|
|
|
|
export function getSender(
|
|
step: Pick<WorkflowStep, "action" | "sender"> & { senderName: string | null | undefined }
|
|
) {
|
|
return isSMSOrWhatsappAction(step.action) ? step.sender || SENDER_ID : step.senderName || SENDER_NAME;
|
|
}
|
|
|
|
export async function upsertSmsReminderFieldForEventTypes({
|
|
activeOn,
|
|
workflowId,
|
|
isSmsReminderNumberRequired,
|
|
isOrg,
|
|
}: {
|
|
activeOn: number[];
|
|
workflowId: number;
|
|
isSmsReminderNumberRequired: boolean;
|
|
isOrg: boolean;
|
|
}) {
|
|
let allEventTypeIds = activeOn;
|
|
|
|
if (isOrg) {
|
|
allEventTypeIds = await getAllUserAndTeamEventTypes(activeOn);
|
|
}
|
|
|
|
for (const eventTypeId of allEventTypeIds) {
|
|
await upsertBookingField(
|
|
getSmsReminderNumberField(),
|
|
getSmsReminderNumberSource({
|
|
workflowId,
|
|
isSmsReminderNumberRequired,
|
|
}),
|
|
eventTypeId
|
|
);
|
|
}
|
|
}
|
|
|
|
export async function removeSmsReminderFieldForEventTypes({
|
|
activeOnToRemove,
|
|
workflowId,
|
|
isOrg,
|
|
activeOn,
|
|
}: {
|
|
activeOnToRemove: number[];
|
|
workflowId: number;
|
|
isOrg: boolean;
|
|
activeOn?: number[];
|
|
}) {
|
|
let allEventTypeIds = activeOnToRemove;
|
|
|
|
if (isOrg) {
|
|
allEventTypeIds = await getAllUserAndTeamEventTypes(activeOnToRemove, activeOn);
|
|
}
|
|
for (const eventTypeId of allEventTypeIds) {
|
|
await removeSmsReminderFieldForEventType({
|
|
workflowId,
|
|
eventTypeId,
|
|
});
|
|
}
|
|
}
|
|
|
|
export async function removeSmsReminderFieldForEventType({
|
|
workflowId,
|
|
eventTypeId,
|
|
}: {
|
|
workflowId: number;
|
|
eventTypeId: number;
|
|
}) {
|
|
await removeBookingField(
|
|
{
|
|
name: SMS_REMINDER_NUMBER_FIELD,
|
|
},
|
|
{
|
|
id: `${workflowId}`,
|
|
type: "workflow",
|
|
},
|
|
eventTypeId
|
|
);
|
|
}
|
|
|
|
export async function upsertAIAgentCallPhoneNumberFieldForEventTypes({
|
|
activeOn,
|
|
workflowId,
|
|
isAIAgentCallPhoneNumberRequired,
|
|
isOrg,
|
|
}: {
|
|
activeOn: number[];
|
|
workflowId: number;
|
|
isAIAgentCallPhoneNumberRequired?: boolean;
|
|
isOrg: boolean;
|
|
}) {
|
|
let allEventTypeIds = activeOn;
|
|
|
|
if (isOrg) {
|
|
allEventTypeIds = await getAllUserAndTeamEventTypes(activeOn);
|
|
}
|
|
|
|
for (const eventTypeId of allEventTypeIds) {
|
|
await upsertBookingField(
|
|
getAIAgentCallPhoneNumberField(),
|
|
getAIAgentCallPhoneNumberSource({
|
|
workflowId,
|
|
isAIAgentCallPhoneNumberRequired: isAIAgentCallPhoneNumberRequired ?? false,
|
|
}),
|
|
eventTypeId
|
|
);
|
|
}
|
|
}
|
|
|
|
export async function removeAIAgentCallPhoneNumberFieldForEventTypes({
|
|
activeOnToRemove,
|
|
workflowId,
|
|
isOrg,
|
|
activeOn,
|
|
}: {
|
|
activeOnToRemove: number[];
|
|
workflowId: number;
|
|
isOrg: boolean;
|
|
activeOn?: number[];
|
|
}) {
|
|
let allEventTypeIds = activeOnToRemove;
|
|
|
|
if (isOrg) {
|
|
allEventTypeIds = await getAllUserAndTeamEventTypes(activeOnToRemove, activeOn);
|
|
}
|
|
for (const eventTypeId of allEventTypeIds) {
|
|
await removeAIAgentCallPhoneNumberFieldForEventType({
|
|
workflowId,
|
|
eventTypeId,
|
|
});
|
|
}
|
|
}
|
|
|
|
export async function removeAIAgentCallPhoneNumberFieldForEventType({
|
|
workflowId,
|
|
eventTypeId,
|
|
}: {
|
|
workflowId: number;
|
|
eventTypeId: number;
|
|
}) {
|
|
await removeBookingField(
|
|
{
|
|
name: CAL_AI_AGENT_PHONE_NUMBER_FIELD,
|
|
},
|
|
{
|
|
id: `${workflowId}`,
|
|
type: "workflow",
|
|
},
|
|
eventTypeId
|
|
);
|
|
}
|
|
|
|
async function getAllUserAndTeamEventTypes(teamIds: number[], notMemberOfTeamId: number[] = []) {
|
|
const teamMembersWithEventTypes = await prisma.membership.findMany({
|
|
where: {
|
|
teamId: {
|
|
in: teamIds,
|
|
},
|
|
user: {
|
|
teams: {
|
|
none: {
|
|
team: {
|
|
id: {
|
|
in: notMemberOfTeamId ?? [],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
select: {
|
|
user: {
|
|
select: {
|
|
eventTypes: {
|
|
select: {
|
|
id: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|
|
|
|
const teamEventTypes = await prisma.eventType.findMany({
|
|
where: {
|
|
teamId: {
|
|
in: teamIds,
|
|
},
|
|
},
|
|
});
|
|
const userEventTypes = teamMembersWithEventTypes?.flatMap((membership) =>
|
|
membership.user.eventTypes.map((et) => et.id)
|
|
);
|
|
|
|
return teamEventTypes.map((et) => et.id).concat(userEventTypes);
|
|
}
|
|
|
|
export async function isAuthorizedToAddActiveOnIds({
|
|
newEventTypeIds,
|
|
newRoutingFormIds,
|
|
newTeamIds,
|
|
teamId,
|
|
userId,
|
|
}: {
|
|
newEventTypeIds: number[];
|
|
newRoutingFormIds: string[];
|
|
newTeamIds: number[];
|
|
teamId?: number | null;
|
|
userId?: number | null;
|
|
}) {
|
|
for (const id of newTeamIds) {
|
|
const newTeam = await prisma.team.findUnique({
|
|
where: {
|
|
id,
|
|
},
|
|
select: {
|
|
parent: true,
|
|
},
|
|
});
|
|
if (newTeam?.parent?.id !== teamId) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Check authorization for event type IDs
|
|
for (const id of newEventTypeIds) {
|
|
const newEventType = await prisma.eventType.findUnique({
|
|
where: {
|
|
id,
|
|
},
|
|
include: {
|
|
users: {
|
|
select: {
|
|
id: true,
|
|
},
|
|
},
|
|
children: true,
|
|
},
|
|
});
|
|
|
|
if (newEventType) {
|
|
if (teamId && teamId !== newEventType.teamId) {
|
|
return false;
|
|
}
|
|
if (
|
|
!teamId &&
|
|
userId &&
|
|
newEventType.userId !== userId &&
|
|
!newEventType?.users.find((eventTypeUser) => eventTypeUser.id === userId)
|
|
) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check authorization for routing form IDs
|
|
for (const id of newRoutingFormIds) {
|
|
// For routing forms, check if user has access to the form
|
|
const routingForm = await prisma.app_RoutingForms_Form.findUnique({
|
|
where: {
|
|
id: String(id),
|
|
},
|
|
select: {
|
|
userId: true,
|
|
teamId: true,
|
|
},
|
|
});
|
|
|
|
if (!routingForm) return false;
|
|
|
|
if (teamId && teamId !== routingForm.teamId) {
|
|
return false;
|
|
}
|
|
|
|
if (!teamId && userId && routingForm.userId !== userId) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
export function isStepEdited(oldStep: WorkflowStep, newStep: WorkflowStep) {
|
|
const oldStepKeys = Object.keys(oldStep);
|
|
const newStepKeys = Object.keys(newStep);
|
|
|
|
if (oldStepKeys.length !== newStepKeys.length) {
|
|
return true;
|
|
}
|
|
|
|
for (const key of oldStepKeys) {
|
|
if (oldStep[key as keyof WorkflowStep] !== newStep[key as keyof WorkflowStep]) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
export const getEventTypeWorkflows = async (
|
|
userId: number,
|
|
eventTypeId: number
|
|
): Promise<z.infer<typeof ZWorkflows>> => {
|
|
const workflows = await prisma.workflow.findMany({
|
|
where: {
|
|
OR: [
|
|
{
|
|
userId: userId,
|
|
},
|
|
{
|
|
team: {
|
|
members: {
|
|
some: {
|
|
userId: userId,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
],
|
|
activeOn: {
|
|
some: {
|
|
eventTypeId: eventTypeId,
|
|
},
|
|
},
|
|
},
|
|
select: {
|
|
name: true,
|
|
id: true,
|
|
trigger: true,
|
|
time: true,
|
|
timeUnit: true,
|
|
userId: true,
|
|
teamId: true,
|
|
team: {
|
|
select: {
|
|
id: true,
|
|
slug: true,
|
|
name: true,
|
|
members: true,
|
|
},
|
|
},
|
|
activeOn: {
|
|
select: {
|
|
eventType: {
|
|
select: {
|
|
id: true,
|
|
title: true,
|
|
parentId: true,
|
|
_count: {
|
|
select: {
|
|
children: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
steps: true,
|
|
},
|
|
});
|
|
|
|
return workflows.map((workflow) => ({ workflow }));
|
|
};
|
|
|
|
export async function getEmailTemplateText(
|
|
template: WorkflowTemplates,
|
|
params: { locale: string; action: WorkflowActions; timeFormat: number | null }
|
|
) {
|
|
const { locale, action } = params;
|
|
|
|
const timeFormat = getTimeFormatStringFromUserTimeFormat(params.timeFormat);
|
|
|
|
let { emailBody, emailSubject } = emailReminderTemplate({
|
|
isEditingMode: true,
|
|
locale,
|
|
t: await getTranslation(locale ?? "en", "common"),
|
|
action,
|
|
timeFormat,
|
|
});
|
|
|
|
if (template === WorkflowTemplates.RATING) {
|
|
const ratingTemplate = emailRatingTemplate({
|
|
isEditingMode: true,
|
|
locale,
|
|
action,
|
|
t: await getTranslation(locale ?? "en", "common"),
|
|
timeFormat,
|
|
});
|
|
|
|
emailBody = ratingTemplate.emailBody;
|
|
emailSubject = ratingTemplate.emailSubject;
|
|
}
|
|
|
|
return { emailBody, emailSubject };
|
|
}
|