* refactor: migrate workflows utilities from trpc to features layer Move workflow utility functions from packages/trpc/server/routers/viewer/workflows/util.ts to packages/features/ee/workflows/lib/workflowUtils.ts to break circular dependencies. Functions migrated: - isAuthorized - getAllWorkflowsFromEventType - scheduleWorkflowNotifications - scheduleBookingReminders Changes: - Created new workflowUtils.ts in features layer with migrated functions - Added getBookingsForWorkflowReminders to WorkflowRepository (no prisma outside repositories) - Updated all imports in features layer to use new location - Updated trpc util.ts to re-export from features for backward compatibility - Fixed pre-existing lint warning (any -> unknown) in handleMarkNoShow.ts This is part of the effort to remove circular dependencies between packages/features and packages/trpc. Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * refactor: remove re-exports from trpc util.ts, update imports to use features layer Per user request, removed the backward compatibility re-exports from packages/trpc/server/routers/viewer/workflows/util.ts and updated all imports in the trpc package to import directly from the features layer. Files updated to import from @calcom/features/ee/workflows/lib/workflowUtils: - confirm.handler.ts (getAllWorkflowsFromEventType) - delete.handler.ts (isAuthorized) - update.handler.ts (isAuthorized, scheduleWorkflowNotifications) - getAllActiveWorkflows.handler.ts (getAllWorkflowsFromEventType) - get.handler.ts (isAuthorized) - util.test.ts (isAuthorized) - delete.handler.test.ts (isAuthorized) Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: update test imports to use features layer for workflow utilities Updated test files to import scheduleBookingReminders and scheduleWorkflowNotifications from @calcom/features/ee/workflows/lib/workflowUtils instead of @calcom/trpc/server/routers/viewer/workflows/util. Files updated: - packages/features/ee/workflows/lib/test/workflows.test.ts - packages/features/tasker/tasks/scanWorkflowBody.test.ts Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * refactor: split workflowUtils.ts into individual files Split the monolithic workflowUtils.ts into separate files for each function: - isAuthorized.ts - Authorization check for workflow access - getAllWorkflowsFromEventType.ts - Get workflows for an event type - scheduleWorkflowNotifications.ts - Schedule workflow notifications - scheduleBookingReminders.ts - Schedule booking reminders The workflowUtils.ts now re-exports from these individual files for backward compatibility. Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix import * fix more * wip * wip * remove workflowUtils * wip * refactor deleteRemindersOfActiveOnIds * fix --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
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/lib/server/i18n";
|
|
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 };
|
|
}
|