refactor: migrate workflows utilities from trpc to features layer (#25534)
* 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>
This commit is contained in:
1 parent
4e3ce77de4
commit
f98acb7301
26 files changed
+783
-714
No files matched your search
@@ -161,7 +161,7 @@ vi.mock("@calcom/features/ee/workflows/lib/getAllWorkflows", () => ({
|
||||
workflowSelect: {},
|
||||
}));
|
||||
|
||||
vi.mock("@calcom/trpc/server/routers/viewer/workflows/util", () => ({
|
||||
vi.mock("@calcom/features/ee/workflows/lib/getAllWorkflowsFromEventType", () => ({
|
||||
getAllWorkflowsFromEventType: vi.fn().mockResolvedValue([]),
|
||||
}));
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { sendAttendeeRequestEmailAndSMS, sendOrganizerRequestEmail } from "@calcom/emails/email-manager";
|
||||
import { getWebhookPayloadForBooking } from "@calcom/features/bookings/lib/getWebhookPayloadForBooking";
|
||||
import { CreditService } from "@calcom/features/ee/billing/credit-service";
|
||||
import { getAllWorkflowsFromEventType } from "@calcom/features/ee/workflows/lib/getAllWorkflowsFromEventType";
|
||||
import { WorkflowService } from "@calcom/features/ee/workflows/lib/service/WorkflowService";
|
||||
import type { Workflow } from "@calcom/features/ee/workflows/lib/types";
|
||||
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
|
||||
@@ -11,7 +12,6 @@ import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
import { WebhookTriggerEvents, WorkflowTriggerEvents } from "@calcom/prisma/enums";
|
||||
import type { EventTypeMetadata } from "@calcom/prisma/zod-utils";
|
||||
import { getAllWorkflowsFromEventType } from "@calcom/trpc/server/routers/viewer/workflows/util";
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
|
||||
const log = logger.getSubLogger({ prefix: ["[handleBookingRequested] book:user"] });
|
||||
|
||||
@@ -11,6 +11,7 @@ import { processNoShowFeeOnCancellation } from "@calcom/features/bookings/lib/pa
|
||||
import { processPaymentRefund } from "@calcom/features/bookings/lib/payment/processPaymentRefund";
|
||||
import { CreditService } from "@calcom/features/ee/billing/credit-service";
|
||||
import { getBookerBaseUrl } from "@calcom/features/ee/organizations/lib/getBookerUrlServer";
|
||||
import { getAllWorkflowsFromEventType } from "@calcom/features/ee/workflows/lib/getAllWorkflowsFromEventType";
|
||||
import { sendCancelledReminders } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler";
|
||||
import { WorkflowRepository } from "@calcom/features/ee/workflows/repositories/WorkflowRepository";
|
||||
import type { GetSubscriberOptions } from "@calcom/features/webhooks/lib/getWebhooks";
|
||||
@@ -38,7 +39,6 @@ import type { WebhookTriggerEvents } from "@calcom/prisma/enums";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
import { bookingMetadataSchema, bookingCancelInput } from "@calcom/prisma/zod-utils";
|
||||
import type { EventTypeMetadata } from "@calcom/prisma/zod-utils";
|
||||
import { getAllWorkflowsFromEventType } from "@calcom/trpc/server/routers/viewer/workflows/util";
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
|
||||
import type {
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
allowDisablingAttendeeConfirmationEmails,
|
||||
allowDisablingHostConfirmationEmails,
|
||||
} from "@calcom/features/ee/workflows/lib/allowDisablingStandardEmails";
|
||||
import { getAllWorkflowsFromEventType } from "@calcom/features/ee/workflows/lib/getAllWorkflowsFromEventType";
|
||||
import { WorkflowService } from "@calcom/features/ee/workflows/lib/service/WorkflowService";
|
||||
import type { Workflow } from "@calcom/features/ee/workflows/lib/types";
|
||||
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
|
||||
@@ -28,7 +29,6 @@ import type { SchedulingType } from "@calcom/prisma/enums";
|
||||
import { BookingStatus, WebhookTriggerEvents, WorkflowTriggerEvents } from "@calcom/prisma/enums";
|
||||
import type { PlatformClientParams } from "@calcom/prisma/zod-utils";
|
||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
import { getAllWorkflowsFromEventType } from "@calcom/trpc/server/routers/viewer/workflows/util";
|
||||
import type { AdditionalInformation, CalendarEvent } from "@calcom/types/Calendar";
|
||||
|
||||
import { getCalEventResponses } from "./getCalEventResponses";
|
||||
|
||||
@@ -41,6 +41,7 @@ import { getSpamCheckService } from "@calcom/features/di/watchlist/containers/Sp
|
||||
import { CreditService } from "@calcom/features/ee/billing/credit-service";
|
||||
import { getBookerBaseUrl } from "@calcom/features/ee/organizations/lib/getBookerUrlServer";
|
||||
import AssignmentReasonRecorder from "@calcom/features/ee/round-robin/assignmentReason/AssignmentReasonRecorder";
|
||||
import { getAllWorkflowsFromEventType } from "@calcom/features/ee/workflows/lib/getAllWorkflowsFromEventType";
|
||||
import { WorkflowService } from "@calcom/features/ee/workflows/lib/service/WorkflowService";
|
||||
import { WorkflowRepository } from "@calcom/features/ee/workflows/repositories/WorkflowRepository";
|
||||
import { getUsernameList } from "@calcom/features/eventtypes/lib/defaultEvents";
|
||||
@@ -87,7 +88,6 @@ import {
|
||||
CreationSource,
|
||||
} from "@calcom/prisma/enums";
|
||||
import { userMetadata as userMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
import { getAllWorkflowsFromEventType } from "@calcom/trpc/server/routers/viewer/workflows/util";
|
||||
import type {
|
||||
AdditionalInformation,
|
||||
AppsStatus,
|
||||
|
||||
@@ -4,10 +4,10 @@ import { CalendarEventBuilder } from "@calcom/features/CalendarEventBuilder";
|
||||
import type { BookingEmailSmsHandler } from "@calcom/features/bookings/lib/BookingEmailSmsHandler";
|
||||
import { getOriginalRescheduledBooking } from "@calcom/features/bookings/lib/handleNewBooking/originalRescheduledBookingUtils";
|
||||
import type { BookingRepository } from "@calcom/features/bookings/repositories/BookingRepository";
|
||||
import { getAllWorkflowsFromEventType } from "@calcom/features/ee/workflows/lib/getAllWorkflowsFromEventType";
|
||||
import type { EventNameObjectType } from "@calcom/features/eventtypes/lib/eventNaming";
|
||||
import type { ITaskerDependencies } from "@calcom/lib/tasker/types";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
import { getAllWorkflowsFromEventType } from "@calcom/trpc/server/routers/viewer/workflows/util";
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
import type { JsonObject } from "@calcom/types/Json";
|
||||
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { WorkflowRepository } from "@calcom/features/ee/workflows/repositories/WorkflowRepository";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import type { Prisma, WorkflowStep } from "@calcom/prisma/client";
|
||||
|
||||
async function getRemindersFromRemovedEventTypes(removedEventTypes: number[], workflowSteps: WorkflowStep[]) {
|
||||
const remindersToDeletePromise: Prisma.PrismaPromise<
|
||||
{
|
||||
id: number;
|
||||
referenceId: string | null;
|
||||
method: string;
|
||||
}[]
|
||||
>[] = [];
|
||||
removedEventTypes.forEach((eventTypeId) => {
|
||||
const remindersToDelete = prisma.workflowReminder.findMany({
|
||||
where: {
|
||||
booking: {
|
||||
eventTypeId,
|
||||
},
|
||||
workflowStepId: {
|
||||
in: workflowSteps.map((step) => {
|
||||
return step.id;
|
||||
}),
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
referenceId: true,
|
||||
method: true,
|
||||
},
|
||||
});
|
||||
|
||||
remindersToDeletePromise.push(remindersToDelete);
|
||||
});
|
||||
|
||||
const remindersToDelete = (await Promise.all(remindersToDeletePromise)).flat();
|
||||
return remindersToDelete;
|
||||
}
|
||||
|
||||
export async function deleteRemindersOfActiveOnIds({
|
||||
removedActiveOnIds,
|
||||
workflowSteps,
|
||||
isOrg,
|
||||
activeOnIds,
|
||||
}: {
|
||||
removedActiveOnIds: number[];
|
||||
workflowSteps: WorkflowStep[];
|
||||
isOrg: boolean;
|
||||
activeOnIds?: number[];
|
||||
}) {
|
||||
const remindersToDelete = !isOrg
|
||||
? await getRemindersFromRemovedEventTypes(removedActiveOnIds, workflowSteps)
|
||||
: await WorkflowRepository.getRemindersFromRemovedTeams(removedActiveOnIds, workflowSteps, activeOnIds);
|
||||
await WorkflowRepository.deleteAllWorkflowReminders(remindersToDelete);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import { getAllWorkflows } from "@calcom/ee/workflows/lib/getAllWorkflows";
|
||||
import type { Workflow as WorkflowType } from "@calcom/ee/workflows/lib/types";
|
||||
import getOrgIdFromMemberOrTeamId from "@calcom/lib/getOrgIdFromMemberOrTeamId";
|
||||
import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType";
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
import { WorkflowType as PrismaWorkflowType } from "@calcom/prisma/enums";
|
||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
export async function getAllWorkflowsFromEventType(
|
||||
eventType: {
|
||||
workflows?: {
|
||||
workflow: WorkflowType;
|
||||
}[];
|
||||
teamId?: number | null;
|
||||
parentId?: number | null;
|
||||
parent?: {
|
||||
id?: number | null;
|
||||
teamId: number | null;
|
||||
} | null;
|
||||
metadata?: Prisma.JsonValue;
|
||||
} | null,
|
||||
userId?: number | null
|
||||
) {
|
||||
if (!eventType) return [];
|
||||
|
||||
const eventTypeWorkflows = eventType?.workflows?.map((workflowRel) => workflowRel.workflow) ?? [];
|
||||
|
||||
const teamId = await getTeamIdFromEventType({
|
||||
eventType: {
|
||||
team: { id: eventType?.teamId ?? null },
|
||||
parentId: eventType?.parentId || eventType?.parent?.id || null,
|
||||
},
|
||||
});
|
||||
|
||||
const orgId = await getOrgIdFromMemberOrTeamId({ memberId: userId, teamId });
|
||||
|
||||
const isManagedEventType = !!eventType?.parent;
|
||||
|
||||
const eventTypeMetadata = EventTypeMetaDataSchema.parse(eventType?.metadata || {});
|
||||
|
||||
const workflowsLockedForUser = isManagedEventType
|
||||
? !eventTypeMetadata?.managedEventConfig?.unlockedFields?.workflows
|
||||
: false;
|
||||
|
||||
const allWorkflows = await getAllWorkflows({
|
||||
entityWorkflows: eventTypeWorkflows,
|
||||
userId,
|
||||
teamId,
|
||||
orgId,
|
||||
workflowsLockedForUser,
|
||||
type: PrismaWorkflowType.EVENT_TYPE,
|
||||
});
|
||||
|
||||
return allWorkflows;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import type { PermissionString } from "@calcom/features/pbac/domain/types/permission-registry";
|
||||
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
|
||||
import type { Workflow } from "@calcom/prisma/client";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
|
||||
export async function isAuthorized(
|
||||
workflow: Pick<Workflow, "id" | "teamId" | "userId"> | null,
|
||||
currentUserId: number,
|
||||
permission: PermissionString = "workflow.read"
|
||||
) {
|
||||
if (!workflow) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// For personal workflows (no teamId), check if user owns the workflow
|
||||
if (!workflow.teamId) {
|
||||
return workflow.userId === currentUserId;
|
||||
}
|
||||
|
||||
// For team workflows, use PBAC
|
||||
const permissionService = new PermissionCheckService();
|
||||
|
||||
// Determine fallback roles based on permission type
|
||||
const fallbackRoles =
|
||||
permission === "workflow.read"
|
||||
? [MembershipRole.ADMIN, MembershipRole.OWNER, MembershipRole.MEMBER]
|
||||
: [MembershipRole.ADMIN, MembershipRole.OWNER];
|
||||
|
||||
return await permissionService.checkPermission({
|
||||
userId: currentUserId,
|
||||
teamId: workflow.teamId,
|
||||
permission,
|
||||
fallbackRoles,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
import { scheduleAIPhoneCall } from "@calcom/ee/workflows/lib/reminders/aiPhoneCallManager";
|
||||
import { scheduleEmailReminder } from "@calcom/ee/workflows/lib/reminders/emailReminderManager";
|
||||
import { scheduleSMSReminder } from "@calcom/ee/workflows/lib/reminders/smsReminderManager";
|
||||
import { scheduleWhatsappReminder } from "@calcom/ee/workflows/lib/reminders/whatsappReminderManager";
|
||||
import { CreditService } from "@calcom/features/ee/billing/credit-service";
|
||||
import { getBookerBaseUrl } from "@calcom/features/ee/organizations/lib/getBookerUrlServer";
|
||||
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
|
||||
import type { WorkflowStep } from "@calcom/prisma/client";
|
||||
import type { TimeUnit } from "@calcom/prisma/enums";
|
||||
import { SchedulingType, WorkflowActions, WorkflowTriggerEvents } from "@calcom/prisma/enums";
|
||||
import type { CalEventResponses } from "@calcom/types/Calendar";
|
||||
|
||||
import type { getBookings } from "./scheduleWorkflowNotifications";
|
||||
import { verifyEmailSender } from "./verifyEmailSender";
|
||||
|
||||
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
|
||||
type Bookings = UnwrapPromise<ReturnType<typeof getBookings>>;
|
||||
|
||||
// some parts of scheduleWorkflowReminders (reminderSchedule.ts) is quite similar to this code
|
||||
// we should consider refactoring this to reuse similar code snippets
|
||||
export async function scheduleBookingReminders(
|
||||
bookings: Bookings,
|
||||
workflowSteps: Partial<WorkflowStep>[],
|
||||
time: number | null,
|
||||
timeUnit: TimeUnit | null,
|
||||
trigger: WorkflowTriggerEvents,
|
||||
userId: number,
|
||||
teamId: number | null,
|
||||
isOrg: boolean
|
||||
) {
|
||||
if (!bookings.length) return;
|
||||
if (trigger !== WorkflowTriggerEvents.BEFORE_EVENT && trigger !== WorkflowTriggerEvents.AFTER_EVENT) return;
|
||||
|
||||
const bookerUrl = await getBookerBaseUrl(isOrg ? teamId : null);
|
||||
|
||||
const creditService = new CreditService();
|
||||
|
||||
//create reminders for all bookings for each workflow step
|
||||
const promiseSteps = workflowSteps.map(async (step) => {
|
||||
const promiseScheduleReminders = bookings.map(async (booking) => {
|
||||
const defaultLocale = "en";
|
||||
const bookingInfo = {
|
||||
uid: booking.uid,
|
||||
bookerUrl,
|
||||
type: booking.eventType?.slug || "event",
|
||||
attendees: booking.attendees.map((attendee) => {
|
||||
return {
|
||||
name: attendee.name,
|
||||
email: attendee.email,
|
||||
timeZone: attendee.timeZone,
|
||||
language: { locale: attendee.locale || defaultLocale },
|
||||
};
|
||||
}),
|
||||
organizer: booking.user
|
||||
? {
|
||||
language: { locale: booking.user.locale || defaultLocale },
|
||||
name: booking.user.name || "",
|
||||
email: booking?.userPrimaryEmail ?? booking.user.email,
|
||||
timeZone: booking.user.timeZone,
|
||||
timeFormat: getTimeFormatStringFromUserTimeFormat(booking.user.timeFormat),
|
||||
}
|
||||
: { name: "", email: "", timeZone: "", language: { locale: "" } },
|
||||
startTime: booking.startTime?.toISOString(),
|
||||
endTime: booking.endTime?.toISOString(),
|
||||
title: booking.title,
|
||||
language: { locale: booking?.user?.locale || defaultLocale },
|
||||
hideOrganizerEmail: booking.eventType?.hideOrganizerEmail,
|
||||
eventType: {
|
||||
slug: booking.eventType?.slug || "",
|
||||
schedulingType: booking.eventType?.schedulingType,
|
||||
hosts: booking.eventType?.hosts,
|
||||
},
|
||||
metadata: booking.metadata,
|
||||
customReplyToEmail: booking.eventType?.customReplyToEmail,
|
||||
responses: booking.responses as CalEventResponses | null,
|
||||
};
|
||||
if (
|
||||
step.action === WorkflowActions.EMAIL_HOST ||
|
||||
step.action === WorkflowActions.EMAIL_ATTENDEE ||
|
||||
step.action === WorkflowActions.EMAIL_ADDRESS
|
||||
) {
|
||||
let sendTo: string[] = [];
|
||||
|
||||
switch (step.action) {
|
||||
case WorkflowActions.EMAIL_HOST: {
|
||||
sendTo = [bookingInfo.organizer?.email];
|
||||
const schedulingType = bookingInfo.eventType.schedulingType;
|
||||
const hosts = bookingInfo.eventType.hosts
|
||||
?.filter((host) => bookingInfo.attendees.some((attendee) => attendee.email === host.user.email))
|
||||
.map(({ user }) => user.destinationCalendar?.primaryEmail ?? user.email);
|
||||
if (
|
||||
hosts &&
|
||||
(schedulingType === SchedulingType.ROUND_ROBIN || schedulingType === SchedulingType.COLLECTIVE)
|
||||
) {
|
||||
sendTo = sendTo.concat(hosts);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case WorkflowActions.EMAIL_ATTENDEE:
|
||||
sendTo = bookingInfo.attendees.map((attendee) => attendee.email);
|
||||
break;
|
||||
case WorkflowActions.EMAIL_ADDRESS:
|
||||
await verifyEmailSender(step.sendTo || "", userId, teamId);
|
||||
sendTo = [step.sendTo || ""];
|
||||
break;
|
||||
}
|
||||
|
||||
await scheduleEmailReminder({
|
||||
evt: bookingInfo,
|
||||
triggerEvent: trigger,
|
||||
action: step.action,
|
||||
timeSpan: {
|
||||
time,
|
||||
timeUnit,
|
||||
},
|
||||
sendTo,
|
||||
emailSubject: step.emailSubject || "",
|
||||
emailBody: step.reminderBody || "",
|
||||
template: step.template,
|
||||
sender: step.sender,
|
||||
workflowStepId: step.id,
|
||||
verifiedAt: step?.verifiedAt ?? null,
|
||||
});
|
||||
} else if (step.action === WorkflowActions.SMS_NUMBER && step.sendTo) {
|
||||
await scheduleSMSReminder({
|
||||
evt: bookingInfo,
|
||||
reminderPhone: step.sendTo,
|
||||
triggerEvent: trigger,
|
||||
action: step.action,
|
||||
timeSpan: {
|
||||
time,
|
||||
timeUnit,
|
||||
},
|
||||
message: step.reminderBody || "",
|
||||
workflowStepId: step.id,
|
||||
template: step.template,
|
||||
sender: step.sender,
|
||||
userId: userId,
|
||||
teamId: teamId,
|
||||
verifiedAt: step?.verifiedAt ?? null,
|
||||
creditCheckFn: creditService.hasAvailableCredits.bind(creditService),
|
||||
});
|
||||
} else if (step.action === WorkflowActions.WHATSAPP_NUMBER && step.sendTo) {
|
||||
await scheduleWhatsappReminder({
|
||||
evt: bookingInfo,
|
||||
reminderPhone: step.sendTo,
|
||||
triggerEvent: trigger,
|
||||
action: step.action,
|
||||
timeSpan: {
|
||||
time,
|
||||
timeUnit,
|
||||
},
|
||||
message: step.reminderBody || "",
|
||||
workflowStepId: step.id || 0,
|
||||
template: step.template,
|
||||
userId: userId,
|
||||
teamId: teamId,
|
||||
verifiedAt: step?.verifiedAt ?? null,
|
||||
creditCheckFn: creditService.hasAvailableCredits.bind(creditService),
|
||||
});
|
||||
} else if (booking.smsReminderNumber) {
|
||||
if (step.action === WorkflowActions.SMS_ATTENDEE) {
|
||||
await scheduleSMSReminder({
|
||||
evt: bookingInfo,
|
||||
reminderPhone: booking.smsReminderNumber,
|
||||
triggerEvent: trigger,
|
||||
action: step.action,
|
||||
timeSpan: {
|
||||
time,
|
||||
timeUnit,
|
||||
},
|
||||
message: step.reminderBody || "",
|
||||
workflowStepId: step.id,
|
||||
template: step.template,
|
||||
sender: step.sender,
|
||||
userId: userId,
|
||||
teamId: teamId,
|
||||
verifiedAt: step?.verifiedAt ?? null,
|
||||
creditCheckFn: creditService.hasAvailableCredits.bind(creditService),
|
||||
});
|
||||
} else if (step.action === WorkflowActions.WHATSAPP_ATTENDEE) {
|
||||
await scheduleWhatsappReminder({
|
||||
evt: bookingInfo,
|
||||
reminderPhone: booking.smsReminderNumber,
|
||||
triggerEvent: trigger,
|
||||
action: step.action,
|
||||
timeSpan: {
|
||||
time,
|
||||
timeUnit,
|
||||
},
|
||||
message: step.reminderBody || "",
|
||||
workflowStepId: step.id,
|
||||
template: step.template,
|
||||
sender: step.sender,
|
||||
userId: userId,
|
||||
teamId: teamId,
|
||||
verifiedAt: step?.verifiedAt ?? null,
|
||||
creditCheckFn: creditService.hasAvailableCredits.bind(creditService),
|
||||
});
|
||||
}
|
||||
} else if (step.action === WorkflowActions.CAL_AI_PHONE_CALL) {
|
||||
await scheduleAIPhoneCall({
|
||||
evt: bookingInfo,
|
||||
triggerEvent: trigger,
|
||||
timeSpan: {
|
||||
time,
|
||||
timeUnit,
|
||||
},
|
||||
workflowStepId: step.id,
|
||||
userId,
|
||||
teamId,
|
||||
verifiedAt: step?.verifiedAt ?? null,
|
||||
submittedPhoneNumber: booking.smsReminderNumber,
|
||||
routedEventTypeId: null,
|
||||
});
|
||||
}
|
||||
});
|
||||
await Promise.all(promiseScheduleReminders);
|
||||
});
|
||||
return Promise.all(promiseSteps);
|
||||
}
|
||||
@@ -0,0 +1,193 @@
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import type { WorkflowStep } from "@calcom/prisma/client";
|
||||
import { BookingStatus, WorkflowTriggerEvents } from "@calcom/prisma/enums";
|
||||
import type { TimeUnit } from "@calcom/prisma/enums";
|
||||
|
||||
import { scheduleBookingReminders } from "./scheduleBookingReminders";
|
||||
|
||||
export const bookingSelect = {
|
||||
userPrimaryEmail: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
title: true,
|
||||
uid: true,
|
||||
metadata: true,
|
||||
smsReminderNumber: true,
|
||||
responses: true,
|
||||
attendees: {
|
||||
select: {
|
||||
name: true,
|
||||
email: true,
|
||||
timeZone: true,
|
||||
locale: true,
|
||||
},
|
||||
},
|
||||
eventType: {
|
||||
select: {
|
||||
slug: true,
|
||||
id: true,
|
||||
schedulingType: true,
|
||||
hideOrganizerEmail: true,
|
||||
customReplyToEmail: true,
|
||||
hosts: {
|
||||
select: {
|
||||
user: {
|
||||
select: {
|
||||
email: true,
|
||||
destinationCalendar: {
|
||||
select: {
|
||||
primaryEmail: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
user: {
|
||||
select: {
|
||||
name: true,
|
||||
timeZone: true,
|
||||
timeFormat: true,
|
||||
locale: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export async function scheduleWorkflowNotifications({
|
||||
activeOn,
|
||||
isOrg,
|
||||
workflowSteps,
|
||||
time,
|
||||
timeUnit,
|
||||
trigger,
|
||||
userId,
|
||||
teamId,
|
||||
alreadyScheduledActiveOnIds,
|
||||
}: {
|
||||
activeOn: number[];
|
||||
isOrg: boolean;
|
||||
workflowSteps: Partial<WorkflowStep>[];
|
||||
time: number | null;
|
||||
timeUnit: TimeUnit | null;
|
||||
trigger: WorkflowTriggerEvents;
|
||||
userId: number;
|
||||
teamId: number | null;
|
||||
alreadyScheduledActiveOnIds?: number[];
|
||||
}) {
|
||||
if (trigger !== WorkflowTriggerEvents.BEFORE_EVENT && trigger !== WorkflowTriggerEvents.AFTER_EVENT) return;
|
||||
|
||||
const bookingsToScheduleNotifications = await getBookings(activeOn, isOrg, alreadyScheduledActiveOnIds);
|
||||
|
||||
await scheduleBookingReminders(
|
||||
bookingsToScheduleNotifications,
|
||||
workflowSteps,
|
||||
time,
|
||||
timeUnit,
|
||||
trigger,
|
||||
userId,
|
||||
teamId,
|
||||
isOrg
|
||||
);
|
||||
}
|
||||
|
||||
export async function getBookings(
|
||||
activeOn: number[],
|
||||
isOrg: boolean,
|
||||
alreadyScheduledActiveOnIds: number[] = []
|
||||
) {
|
||||
if (activeOn.length === 0) return [];
|
||||
|
||||
if (isOrg) {
|
||||
const bookingsForReminders = await prisma.booking.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
// bookings from team event types + children managed event types
|
||||
eventType: {
|
||||
OR: [
|
||||
{
|
||||
teamId: {
|
||||
in: activeOn,
|
||||
},
|
||||
},
|
||||
{
|
||||
teamId: null,
|
||||
parent: {
|
||||
teamId: {
|
||||
in: activeOn,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
// user bookings
|
||||
user: {
|
||||
teams: {
|
||||
some: {
|
||||
teamId: {
|
||||
in: activeOn,
|
||||
},
|
||||
accepted: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
eventType: {
|
||||
teamId: null,
|
||||
parentId: null, // children managed event types are handled above with team event types
|
||||
},
|
||||
// if user is already part of an already scheduled activeOn connecting reminders are already scheduled
|
||||
NOT: {
|
||||
user: {
|
||||
teams: {
|
||||
some: {
|
||||
teamId: {
|
||||
in: alreadyScheduledActiveOnIds,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
status: BookingStatus.ACCEPTED,
|
||||
startTime: {
|
||||
gte: new Date(),
|
||||
},
|
||||
},
|
||||
select: bookingSelect,
|
||||
orderBy: {
|
||||
startTime: "asc",
|
||||
},
|
||||
});
|
||||
return bookingsForReminders;
|
||||
} else {
|
||||
const bookingsForReminders = await prisma.booking.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ eventTypeId: { in: activeOn } },
|
||||
{
|
||||
eventType: {
|
||||
parentId: {
|
||||
in: activeOn, // child event type can not disable workflows, so this should work
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
status: BookingStatus.ACCEPTED,
|
||||
startTime: {
|
||||
gte: new Date(),
|
||||
},
|
||||
},
|
||||
select: bookingSelect,
|
||||
orderBy: {
|
||||
startTime: "asc",
|
||||
},
|
||||
});
|
||||
return bookingsForReminders;
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import { v4 as uuidv4 } from "uuid";
|
||||
import { describe, expect, beforeAll, vi, beforeEach } from "vitest";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { scheduleBookingReminders } from "@calcom/features/ee/workflows/lib/scheduleBookingReminders";
|
||||
import { FeaturesRepository } from "@calcom/features/flags/features.repository";
|
||||
import tasker from "@calcom/features/tasker";
|
||||
import * as rateLimitModule from "@calcom/lib/checkRateLimitAndThrowError";
|
||||
@@ -28,17 +29,14 @@ import {
|
||||
WorkflowTriggerEvents,
|
||||
WorkflowActions,
|
||||
} from "@calcom/prisma/enums";
|
||||
import {
|
||||
deleteRemindersOfActiveOnIds,
|
||||
scheduleBookingReminders,
|
||||
bookingSelect,
|
||||
} from "@calcom/trpc/server/routers/viewer/workflows/util";
|
||||
import { test } from "@calcom/web/test/fixtures/fixtures";
|
||||
|
||||
import { deleteWorkfowRemindersOfRemovedMember } from "../../../teams/lib/deleteWorkflowRemindersOfRemovedMember";
|
||||
import { deleteRemindersOfActiveOnIds } from "../deleteRemindersOfActiveOnIds";
|
||||
import { scheduleAIPhoneCall } from "../reminders/aiPhoneCallManager";
|
||||
import { scheduleEmailReminder } from "../reminders/emailReminderManager";
|
||||
import * as emailProvider from "../reminders/providers/emailProvider";
|
||||
import { bookingSelect } from "../scheduleWorkflowNotifications";
|
||||
|
||||
const workflowSelect = {
|
||||
id: true,
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
import { ErrorCode } from "@calcom/lib/errorCodes";
|
||||
import { ErrorWithCode } from "@calcom/lib/errors";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
|
||||
export const verifyEmailSender = async (email: string, userId: number, teamId: number | null) => {
|
||||
const verifiedEmail = await prisma.verifiedEmail.findFirst({
|
||||
where: {
|
||||
email,
|
||||
OR: [{ userId }, { teamId }],
|
||||
},
|
||||
});
|
||||
|
||||
if (verifiedEmail) {
|
||||
if (teamId) {
|
||||
if (!verifiedEmail.teamId) {
|
||||
await prisma.verifiedEmail.update({
|
||||
where: {
|
||||
id: verifiedEmail.id,
|
||||
},
|
||||
data: {
|
||||
teamId,
|
||||
},
|
||||
});
|
||||
} else if (verifiedEmail.teamId !== teamId) {
|
||||
await prisma.verifiedEmail.create({
|
||||
data: {
|
||||
email,
|
||||
userId,
|
||||
teamId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const userEmail = await prisma.user.findFirst({
|
||||
where: {
|
||||
id: userId,
|
||||
email,
|
||||
},
|
||||
});
|
||||
|
||||
if (userEmail) {
|
||||
await prisma.verifiedEmail.create({
|
||||
data: {
|
||||
email,
|
||||
userId,
|
||||
teamId,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if it's a verified secondary email of the user
|
||||
const secondaryEmail = await prisma.secondaryEmail.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
email,
|
||||
emailVerified: {
|
||||
not: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (secondaryEmail) {
|
||||
await prisma.verifiedEmail.create({
|
||||
data: {
|
||||
email,
|
||||
userId,
|
||||
teamId,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (teamId) {
|
||||
const team = await prisma.team.findUnique({
|
||||
where: {
|
||||
id: teamId,
|
||||
},
|
||||
select: {
|
||||
members: {
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
secondaryEmails: {
|
||||
select: {
|
||||
email: true,
|
||||
emailVerified: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!team) {
|
||||
throw new ErrorWithCode(ErrorCode.NotFound, "Team not found");
|
||||
}
|
||||
|
||||
const isTeamMember = team.members.some((member) => member.userId === userId);
|
||||
|
||||
if (!isTeamMember) {
|
||||
throw new ErrorWithCode(ErrorCode.Forbidden, "You are not a member of this team");
|
||||
}
|
||||
|
||||
let foundTeamMember = team.members.find((member) => member.user.email === email);
|
||||
|
||||
// Only check secondary emails if no match was found with primary email
|
||||
if (!foundTeamMember) {
|
||||
foundTeamMember = team.members.find((member) =>
|
||||
member.user.secondaryEmails.some(
|
||||
(secondary) => secondary.email === email && !!secondary.emailVerified
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (foundTeamMember) {
|
||||
await prisma.verifiedEmail.create({
|
||||
data: {
|
||||
email,
|
||||
userId,
|
||||
teamId,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw new ErrorWithCode(ErrorCode.NotFound, "Email not verified");
|
||||
};
|
||||
@@ -857,4 +857,55 @@ export class WorkflowRepository {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
static bookingSelectForReminders = {
|
||||
userPrimaryEmail: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
title: true,
|
||||
uid: true,
|
||||
metadata: true,
|
||||
smsReminderNumber: true,
|
||||
responses: true,
|
||||
attendees: {
|
||||
select: {
|
||||
name: true,
|
||||
email: true,
|
||||
timeZone: true,
|
||||
locale: true,
|
||||
},
|
||||
},
|
||||
eventType: {
|
||||
select: {
|
||||
slug: true,
|
||||
id: true,
|
||||
schedulingType: true,
|
||||
hideOrganizerEmail: true,
|
||||
customReplyToEmail: true,
|
||||
hosts: {
|
||||
select: {
|
||||
user: {
|
||||
select: {
|
||||
email: true,
|
||||
destinationCalendar: {
|
||||
select: {
|
||||
primaryEmail: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
user: {
|
||||
select: {
|
||||
name: true,
|
||||
timeZone: true,
|
||||
timeFormat: true,
|
||||
locale: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { BookingRepository } from "@calcom/features/bookings/repositories/Bookin
|
||||
import { CreditService } from "@calcom/features/ee/billing/credit-service";
|
||||
import { getBookerBaseUrl } from "@calcom/features/ee/organizations/lib/getBookerUrlServer";
|
||||
import { workflowSelect } from "@calcom/features/ee/workflows/lib/getAllWorkflows";
|
||||
import { getAllWorkflowsFromEventType } from "@calcom/features/ee/workflows/lib/getAllWorkflowsFromEventType";
|
||||
import type { ExtendedCalendarEvent } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler";
|
||||
import { WorkflowService } from "@calcom/features/ee/workflows/lib/service/WorkflowService";
|
||||
import { WebhookService } from "@calcom/features/webhooks/lib/WebhookService";
|
||||
@@ -16,7 +17,6 @@ import { prisma } from "@calcom/prisma";
|
||||
import { WebhookTriggerEvents, WorkflowTriggerEvents } from "@calcom/prisma/enums";
|
||||
import { bookingMetadataSchema, type PlatformClientParams } from "@calcom/prisma/zod-utils";
|
||||
import type { TNoShowInputSchema } from "@calcom/trpc/server/routers/loggedInViewer/markNoShow.schema";
|
||||
import { getAllWorkflowsFromEventType } from "@calcom/trpc/server/routers/viewer/workflows/util";
|
||||
|
||||
import handleSendingAttendeeNoShowDataToApps from "./noShow/handleSendingAttendeeNoShowDataToApps";
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import prismaMock from "../../../../tests/libs/__mocks__/prismaMock";
|
||||
import { describe, expect, it, vi, beforeEach } from "vitest";
|
||||
|
||||
import { lockUser, LockReason } from "@calcom/features/ee/api-keys/lib/autoLock";
|
||||
import { scheduleWorkflowNotifications } from "@calcom/trpc/server/routers/viewer/workflows/util";
|
||||
import { scheduleWorkflowNotifications } from "@calcom/features/ee/workflows/lib/scheduleWorkflowNotifications";
|
||||
|
||||
import { scanWorkflowBody, iffyScanBody } from "./scanWorkflowBody";
|
||||
|
||||
@@ -15,7 +15,7 @@ vi.mock("@calcom/features/ee/api-keys/lib/autoLock", async (importActual) => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("@calcom/trpc/server/routers/viewer/workflows/util", () => ({
|
||||
vi.mock("@calcom/features/ee/workflows/lib/scheduleWorkflowNotifications", () => ({
|
||||
scheduleWorkflowNotifications: vi.fn(),
|
||||
}));
|
||||
|
||||
|
||||
@@ -2,12 +2,12 @@ import z from "zod";
|
||||
|
||||
import { getTemplateBodyForAction } from "@calcom/features/ee/workflows/lib/actionHelperFunctions";
|
||||
import compareReminderBodyToTemplate from "@calcom/features/ee/workflows/lib/compareReminderBodyToTemplate";
|
||||
import { scheduleWorkflowNotifications } from "@calcom/features/ee/workflows/lib/scheduleWorkflowNotifications";
|
||||
import { Task } from "@calcom/features/tasker/repository";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { scheduleWorkflowNotifications } from "@calcom/trpc/server/routers/viewer/workflows/util";
|
||||
|
||||
export const scanWorkflowBodySchema = z.object({
|
||||
userId: z.number(),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { isAuthorized } from "@calcom/features/ee/workflows/lib/isAuthorized";
|
||||
import type { Workflow } from "@calcom/prisma/client";
|
||||
import { isAuthorized } from "@calcom/trpc/server/routers/viewer/workflows/util";
|
||||
|
||||
export interface WorkflowPermissions {
|
||||
canView: boolean;
|
||||
|
||||
@@ -10,6 +10,7 @@ import { processPaymentRefund } from "@calcom/features/bookings/lib/payment/proc
|
||||
import { CreditService } from "@calcom/features/ee/billing/credit-service";
|
||||
import { getBookerBaseUrl } from "@calcom/features/ee/organizations/lib/getBookerUrlServer";
|
||||
import { workflowSelect } from "@calcom/features/ee/workflows/lib/getAllWorkflows";
|
||||
import { getAllWorkflowsFromEventType } from "@calcom/features/ee/workflows/lib/getAllWorkflowsFromEventType";
|
||||
import { WorkflowService } from "@calcom/features/ee/workflows/lib/service/WorkflowService";
|
||||
import type { GetSubscriberOptions } from "@calcom/features/webhooks/lib/getWebhooks";
|
||||
import type { EventPayloadType, EventTypeInfo } from "@calcom/features/webhooks/lib/sendPayload";
|
||||
@@ -31,7 +32,6 @@ import {
|
||||
UserPermissionRole,
|
||||
} from "@calcom/prisma/enums";
|
||||
import type { EventTypeMetadata } from "@calcom/prisma/zod-utils";
|
||||
import { getAllWorkflowsFromEventType } from "@calcom/trpc/server/routers/viewer/workflows/util";
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
@@ -41,7 +41,10 @@ import type { TConfirmInputSchema } from "./confirm.schema";
|
||||
|
||||
type ConfirmOptions = {
|
||||
ctx: {
|
||||
user: Pick<NonNullable<TrpcSessionUser>, "id" | "uuid" | "email" | "username" | "role" | "destinationCalendar">;
|
||||
user: Pick<
|
||||
NonNullable<TrpcSessionUser>,
|
||||
"id" | "uuid" | "email" | "username" | "role" | "destinationCalendar"
|
||||
>;
|
||||
traceContext: TraceContext;
|
||||
};
|
||||
input: TConfirmInputSchema;
|
||||
|
||||
@@ -3,17 +3,14 @@ import { prisma } from "@calcom/prisma/__mocks__/prisma";
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
import { createDefaultAIPhoneServiceProvider } from "@calcom/features/calAIPhone";
|
||||
import { isAuthorized } from "@calcom/features/ee/workflows/lib/isAuthorized";
|
||||
import { WorkflowRepository } from "@calcom/features/ee/workflows/repositories/WorkflowRepository";
|
||||
import { WorkflowActions } from "@calcom/prisma/enums";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import { deleteHandler } from "./delete.handler";
|
||||
import {
|
||||
isAuthorized,
|
||||
removeSmsReminderFieldForEventTypes,
|
||||
removeAIAgentCallPhoneNumberFieldForEventTypes,
|
||||
} from "./util";
|
||||
import { removeSmsReminderFieldForEventTypes, removeAIAgentCallPhoneNumberFieldForEventTypes } from "./util";
|
||||
|
||||
vi.mock("@calcom/prisma", () => ({
|
||||
prisma,
|
||||
@@ -29,8 +26,11 @@ vi.mock("@calcom/features/ee/workflows/repositories/WorkflowRepository", () => (
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("./util", () => ({
|
||||
vi.mock("@calcom/features/ee/workflows/lib/isAuthorized", () => ({
|
||||
isAuthorized: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("./util", () => ({
|
||||
removeSmsReminderFieldForEventTypes: vi.fn(),
|
||||
removeAIAgentCallPhoneNumberFieldForEventTypes: vi.fn(),
|
||||
}));
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createDefaultAIPhoneServiceProvider } from "@calcom/features/calAIPhone";
|
||||
import { isAuthorized } from "@calcom/features/ee/workflows/lib/isAuthorized";
|
||||
import { WorkflowRepository } from "@calcom/features/ee/workflows/repositories/WorkflowRepository";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
@@ -8,11 +9,7 @@ import type { TrpcSessionUser } from "@calcom/trpc/server/types";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import type { TDeleteInputSchema } from "./delete.schema";
|
||||
import {
|
||||
isAuthorized,
|
||||
removeSmsReminderFieldForEventTypes,
|
||||
removeAIAgentCallPhoneNumberFieldForEventTypes,
|
||||
} from "./util";
|
||||
import { removeSmsReminderFieldForEventTypes, removeAIAgentCallPhoneNumberFieldForEventTypes } from "./util";
|
||||
|
||||
type DeleteOptions = {
|
||||
ctx: {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isAuthorized } from "@calcom/features/ee/workflows/lib/isAuthorized";
|
||||
import { WorkflowRepository } from "@calcom/features/ee/workflows/repositories/WorkflowRepository";
|
||||
import { addPermissionsToWorkflow } from "@calcom/features/workflows/repositories/WorkflowPermissionsRepository";
|
||||
import type { TrpcSessionUser } from "@calcom/trpc/server/types";
|
||||
@@ -5,7 +6,6 @@ import type { TrpcSessionUser } from "@calcom/trpc/server/types";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import type { TGetInputSchema } from "./get.schema";
|
||||
import { isAuthorized } from "./util";
|
||||
|
||||
type GetOptions = {
|
||||
ctx: {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { eventTypeMetaDataSchemaWithTypedApps } from "@calcom/app-store/zod-utils";
|
||||
import { getAllWorkflowsFromEventType } from "@calcom/features/ee/workflows/lib/getAllWorkflowsFromEventType";
|
||||
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
|
||||
import { addPermissionsToWorkflows } from "@calcom/features/workflows/repositories/WorkflowPermissionsRepository";
|
||||
import { MembershipRole } from "@calcom/prisma/enums";
|
||||
@@ -7,7 +8,7 @@ import type { TrpcSessionUser } from "@calcom/trpc/server/types";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
|
||||
import type { TGetAllActiveWorkflowsInputSchema } from "./getAllActiveWorkflows.schema";
|
||||
import { getAllWorkflowsFromEventType, getEventTypeWorkflows } from "./util";
|
||||
import { getEventTypeWorkflows } from "./util";
|
||||
|
||||
type GetAllActiveWorkflowsOptions = {
|
||||
ctx: {
|
||||
|
||||
@@ -2,7 +2,11 @@ import { createDefaultAIPhoneServiceProvider } from "@calcom/features/calAIPhone
|
||||
import { PrismaAgentRepository } from "@calcom/features/calAIPhone/repositories/PrismaAgentRepository";
|
||||
import { TeamRepository } from "@calcom/features/ee/teams/repositories/TeamRepository";
|
||||
import { isEmailAction, isFormTrigger } from "@calcom/features/ee/workflows/lib/actionHelperFunctions";
|
||||
import { deleteRemindersOfActiveOnIds } from "@calcom/features/ee/workflows/lib/deleteRemindersOfActiveOnIds";
|
||||
import { isAuthorized } from "@calcom/features/ee/workflows/lib/isAuthorized";
|
||||
import { WorkflowReminderRepository } from "@calcom/features/ee/workflows/lib/repository/workflowReminder";
|
||||
import { scheduleWorkflowNotifications } from "@calcom/features/ee/workflows/lib/scheduleWorkflowNotifications";
|
||||
import { verifyEmailSender } from "@calcom/features/ee/workflows/lib/verifyEmailSender";
|
||||
import { WorkflowRepository } from "@calcom/features/ee/workflows/repositories/WorkflowRepository";
|
||||
import { EventTypeRepository } from "@calcom/features/eventtypes/repositories/eventTypeRepository";
|
||||
import tasker from "@calcom/features/tasker";
|
||||
@@ -23,12 +27,8 @@ import hasActiveTeamPlanHandler from "../teams/hasActiveTeamPlan.handler";
|
||||
import type { TUpdateInputSchema } from "./update.schema";
|
||||
import {
|
||||
getSender,
|
||||
isAuthorized,
|
||||
upsertSmsReminderFieldForEventTypes,
|
||||
deleteRemindersOfActiveOnIds,
|
||||
isAuthorizedToAddActiveOnIds,
|
||||
scheduleWorkflowNotifications,
|
||||
verifyEmailSender,
|
||||
removeSmsReminderFieldForEventTypes,
|
||||
isStepEdited,
|
||||
getEmailTemplateText,
|
||||
|
||||
@@ -2,10 +2,9 @@ import { prisma } from "@calcom/prisma/__mocks__/prisma";
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from "vitest";
|
||||
|
||||
import { isAuthorized } from "@calcom/features/ee/workflows/lib/isAuthorized";
|
||||
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
|
||||
|
||||
import { isAuthorized } from "./util";
|
||||
|
||||
vi.mock("@calcom/features/pbac/services/permission-check.service");
|
||||
|
||||
vi.mock("@calcom/prisma", () => ({
|
||||
|
||||
@@ -1,267 +1,32 @@
|
||||
import type { z } from "zod";
|
||||
|
||||
import { isSMSOrWhatsappAction } from "@calcom/ee/workflows/lib/actionHelperFunctions";
|
||||
import { getAllWorkflows } from "@calcom/ee/workflows/lib/getAllWorkflows";
|
||||
import { scheduleAIPhoneCall } from "@calcom/ee/workflows/lib/reminders/aiPhoneCallManager";
|
||||
import { scheduleEmailReminder } from "@calcom/ee/workflows/lib/reminders/emailReminderManager";
|
||||
import { scheduleSMSReminder } from "@calcom/ee/workflows/lib/reminders/smsReminderManager";
|
||||
import emailRatingTemplate from "@calcom/ee/workflows/lib/reminders/templates/emailRatingTemplate";
|
||||
import emailReminderTemplate from "@calcom/ee/workflows/lib/reminders/templates/emailReminderTemplate";
|
||||
import { scheduleWhatsappReminder } from "@calcom/ee/workflows/lib/reminders/whatsappReminderManager";
|
||||
import type { Workflow as WorkflowType } from "@calcom/ee/workflows/lib/types";
|
||||
import {
|
||||
getSmsReminderNumberField,
|
||||
getSmsReminderNumberSource,
|
||||
getAIAgentCallPhoneNumberField,
|
||||
getAIAgentCallPhoneNumberSource,
|
||||
} from "@calcom/features/bookings/lib/getBookingFields";
|
||||
import { CreditService } from "@calcom/features/ee/billing/credit-service";
|
||||
import { getBookerBaseUrl } from "@calcom/features/ee/organizations/lib/getBookerUrlServer";
|
||||
import { WorkflowRepository } from "@calcom/features/ee/workflows/repositories/WorkflowRepository";
|
||||
import { removeBookingField, upsertBookingField } from "@calcom/features/eventtypes/lib/bookingFieldsManager";
|
||||
import type { PermissionString } from "@calcom/features/pbac/domain/types/permission-registry";
|
||||
import { PermissionCheckService } from "@calcom/features/pbac/services/permission-check.service";
|
||||
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 getOrgIdFromMemberOrTeamId from "@calcom/lib/getOrgIdFromMemberOrTeamId";
|
||||
import { getTeamIdFromEventType } from "@calcom/lib/getTeamIdFromEventType";
|
||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { Workflow } from "@calcom/prisma/client";
|
||||
import type { Prisma, WorkflowStep } from "@calcom/prisma/client";
|
||||
import type { TimeUnit } from "@calcom/prisma/enums";
|
||||
import { WorkflowTemplates, WorkflowType as PrismaWorkflowType } from "@calcom/prisma/enums";
|
||||
import { SchedulingType } from "@calcom/prisma/enums";
|
||||
import { BookingStatus, MembershipRole, WorkflowActions, WorkflowTriggerEvents } from "@calcom/prisma/enums";
|
||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
import type { CalEventResponses } from "@calcom/types/Calendar";
|
||||
|
||||
import { TRPCError } from "@trpc/server";
|
||||
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 const bookingSelect = {
|
||||
userPrimaryEmail: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
title: true,
|
||||
uid: true,
|
||||
metadata: true,
|
||||
smsReminderNumber: true,
|
||||
responses: true,
|
||||
attendees: {
|
||||
select: {
|
||||
name: true,
|
||||
email: true,
|
||||
timeZone: true,
|
||||
locale: true,
|
||||
},
|
||||
},
|
||||
eventType: {
|
||||
select: {
|
||||
slug: true,
|
||||
id: true,
|
||||
schedulingType: true,
|
||||
hideOrganizerEmail: true,
|
||||
customReplyToEmail: true,
|
||||
hosts: {
|
||||
select: {
|
||||
user: {
|
||||
select: {
|
||||
email: true,
|
||||
destinationCalendar: {
|
||||
select: {
|
||||
primaryEmail: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
user: {
|
||||
select: {
|
||||
name: true,
|
||||
timeZone: true,
|
||||
timeFormat: true,
|
||||
locale: true,
|
||||
email: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const verifyEmailSender = async (email: string, userId: number, teamId: number | null) => {
|
||||
const verifiedEmail = await prisma.verifiedEmail.findFirst({
|
||||
where: {
|
||||
email,
|
||||
OR: [{ userId }, { teamId }],
|
||||
},
|
||||
});
|
||||
|
||||
if (verifiedEmail) {
|
||||
if (teamId) {
|
||||
if (!verifiedEmail.teamId) {
|
||||
await prisma.verifiedEmail.update({
|
||||
where: {
|
||||
id: verifiedEmail.id,
|
||||
},
|
||||
data: {
|
||||
teamId,
|
||||
},
|
||||
});
|
||||
} else if (verifiedEmail.teamId !== teamId) {
|
||||
await prisma.verifiedEmail.create({
|
||||
data: {
|
||||
email,
|
||||
userId,
|
||||
teamId,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const userEmail = await prisma.user.findFirst({
|
||||
where: {
|
||||
id: userId,
|
||||
email,
|
||||
},
|
||||
});
|
||||
|
||||
if (userEmail) {
|
||||
await prisma.verifiedEmail.create({
|
||||
data: {
|
||||
email,
|
||||
userId,
|
||||
teamId,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if it's a verified secondary email of the user
|
||||
const secondaryEmail = await prisma.secondaryEmail.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
email,
|
||||
emailVerified: {
|
||||
not: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (secondaryEmail) {
|
||||
await prisma.verifiedEmail.create({
|
||||
data: {
|
||||
email,
|
||||
userId,
|
||||
teamId,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (teamId) {
|
||||
const team = await prisma.team.findUnique({
|
||||
where: {
|
||||
id: teamId,
|
||||
},
|
||||
select: {
|
||||
members: {
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
secondaryEmails: {
|
||||
select: {
|
||||
email: true,
|
||||
emailVerified: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!team) {
|
||||
throw new TRPCError({ code: "NOT_FOUND", message: "Team not found" });
|
||||
}
|
||||
|
||||
const isTeamMember = team.members.some((member) => member.userId === userId);
|
||||
|
||||
if (!isTeamMember) {
|
||||
throw new TRPCError({ code: "FORBIDDEN", message: "You are not a member of this team" });
|
||||
}
|
||||
|
||||
let foundTeamMember = team.members.find((member) => member.user.email === email);
|
||||
|
||||
// Only check secondary emails if no match was found with primary email
|
||||
if (!foundTeamMember) {
|
||||
foundTeamMember = team.members.find((member) =>
|
||||
member.user.secondaryEmails.some(
|
||||
(secondary) => secondary.email === email && !!secondary.emailVerified
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if (foundTeamMember) {
|
||||
await prisma.verifiedEmail.create({
|
||||
data: {
|
||||
email,
|
||||
userId,
|
||||
teamId,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw new TRPCError({ code: "NOT_FOUND", message: "Email not verified" });
|
||||
};
|
||||
|
||||
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 isAuthorized(
|
||||
workflow: Pick<Workflow, "id" | "teamId" | "userId"> | null,
|
||||
currentUserId: number,
|
||||
permission: PermissionString = "workflow.read"
|
||||
) {
|
||||
if (!workflow) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// For personal workflows (no teamId), check if user owns the workflow
|
||||
if (!workflow.teamId) {
|
||||
return workflow.userId === currentUserId;
|
||||
}
|
||||
|
||||
// For team workflows, use PBAC
|
||||
const permissionService = new PermissionCheckService();
|
||||
|
||||
// Determine fallback roles based on permission type
|
||||
const fallbackRoles =
|
||||
permission === "workflow.read"
|
||||
? [MembershipRole.ADMIN, MembershipRole.OWNER, MembershipRole.MEMBER]
|
||||
: [MembershipRole.ADMIN, MembershipRole.OWNER];
|
||||
|
||||
return await permissionService.checkPermission({
|
||||
userId: currentUserId,
|
||||
teamId: workflow.teamId,
|
||||
permission,
|
||||
fallbackRoles,
|
||||
});
|
||||
}
|
||||
|
||||
export async function upsertSmsReminderFieldForEventTypes({
|
||||
activeOn,
|
||||
workflowId,
|
||||
@@ -535,397 +300,6 @@ export async function isAuthorizedToAddActiveOnIds({
|
||||
return true;
|
||||
}
|
||||
|
||||
export async function deleteRemindersOfActiveOnIds({
|
||||
removedActiveOnIds,
|
||||
workflowSteps,
|
||||
isOrg,
|
||||
activeOnIds,
|
||||
}: {
|
||||
removedActiveOnIds: number[];
|
||||
workflowSteps: WorkflowStep[];
|
||||
isOrg: boolean;
|
||||
activeOnIds?: number[];
|
||||
}) {
|
||||
const remindersToDelete = !isOrg
|
||||
? await getRemindersFromRemovedEventTypes(removedActiveOnIds, workflowSteps)
|
||||
: await WorkflowRepository.getRemindersFromRemovedTeams(removedActiveOnIds, workflowSteps, activeOnIds);
|
||||
await WorkflowRepository.deleteAllWorkflowReminders(remindersToDelete);
|
||||
}
|
||||
|
||||
async function getRemindersFromRemovedEventTypes(removedEventTypes: number[], workflowSteps: WorkflowStep[]) {
|
||||
const remindersToDeletePromise: Prisma.PrismaPromise<
|
||||
{
|
||||
id: number;
|
||||
referenceId: string | null;
|
||||
method: string;
|
||||
}[]
|
||||
>[] = [];
|
||||
removedEventTypes.forEach((eventTypeId) => {
|
||||
const remindersToDelete = prisma.workflowReminder.findMany({
|
||||
where: {
|
||||
booking: {
|
||||
eventTypeId,
|
||||
},
|
||||
workflowStepId: {
|
||||
in: workflowSteps.map((step) => {
|
||||
return step.id;
|
||||
}),
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
referenceId: true,
|
||||
method: true,
|
||||
},
|
||||
});
|
||||
|
||||
remindersToDeletePromise.push(remindersToDelete);
|
||||
});
|
||||
|
||||
const remindersToDelete = (await Promise.all(remindersToDeletePromise)).flat();
|
||||
return remindersToDelete;
|
||||
}
|
||||
|
||||
export async function scheduleWorkflowNotifications({
|
||||
activeOn,
|
||||
isOrg,
|
||||
workflowSteps,
|
||||
time,
|
||||
timeUnit,
|
||||
trigger,
|
||||
userId,
|
||||
teamId,
|
||||
alreadyScheduledActiveOnIds,
|
||||
}: {
|
||||
activeOn: number[];
|
||||
isOrg: boolean;
|
||||
workflowSteps: Partial<WorkflowStep>[];
|
||||
time: number | null;
|
||||
timeUnit: TimeUnit | null;
|
||||
trigger: WorkflowTriggerEvents;
|
||||
userId: number;
|
||||
teamId: number | null;
|
||||
alreadyScheduledActiveOnIds?: number[];
|
||||
}) {
|
||||
if (trigger !== WorkflowTriggerEvents.BEFORE_EVENT && trigger !== WorkflowTriggerEvents.AFTER_EVENT) return;
|
||||
|
||||
const bookingsToScheduleNotifications = await getBookings(activeOn, isOrg, alreadyScheduledActiveOnIds);
|
||||
|
||||
await scheduleBookingReminders(
|
||||
bookingsToScheduleNotifications,
|
||||
workflowSteps,
|
||||
time,
|
||||
timeUnit,
|
||||
trigger,
|
||||
userId,
|
||||
teamId,
|
||||
isOrg
|
||||
);
|
||||
}
|
||||
|
||||
async function getBookings(activeOn: number[], isOrg: boolean, alreadyScheduledActiveOnIds: number[] = []) {
|
||||
if (activeOn.length === 0) return [];
|
||||
|
||||
if (isOrg) {
|
||||
const bookingsForReminders = await prisma.booking.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{
|
||||
// bookings from team event types + children managed event types
|
||||
eventType: {
|
||||
OR: [
|
||||
{
|
||||
teamId: {
|
||||
in: activeOn,
|
||||
},
|
||||
},
|
||||
{
|
||||
teamId: null,
|
||||
parent: {
|
||||
teamId: {
|
||||
in: activeOn,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
// user bookings
|
||||
user: {
|
||||
teams: {
|
||||
some: {
|
||||
teamId: {
|
||||
in: activeOn,
|
||||
},
|
||||
accepted: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
eventType: {
|
||||
teamId: null,
|
||||
parentId: null, // children managed event types are handled above with team event types
|
||||
},
|
||||
// if user is already part of an already scheduled activeOn connecting reminders are already scheduled
|
||||
NOT: {
|
||||
user: {
|
||||
teams: {
|
||||
some: {
|
||||
teamId: {
|
||||
in: alreadyScheduledActiveOnIds,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
status: BookingStatus.ACCEPTED,
|
||||
startTime: {
|
||||
gte: new Date(),
|
||||
},
|
||||
},
|
||||
select: bookingSelect,
|
||||
orderBy: {
|
||||
startTime: "asc",
|
||||
},
|
||||
});
|
||||
return bookingsForReminders;
|
||||
} else {
|
||||
const bookingsForReminders = await prisma.booking.findMany({
|
||||
where: {
|
||||
OR: [
|
||||
{ eventTypeId: { in: activeOn } },
|
||||
{
|
||||
eventType: {
|
||||
parentId: {
|
||||
in: activeOn, // child event type can not disable workflows, so this should work
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
status: BookingStatus.ACCEPTED,
|
||||
startTime: {
|
||||
gte: new Date(),
|
||||
},
|
||||
},
|
||||
select: bookingSelect,
|
||||
orderBy: {
|
||||
startTime: "asc",
|
||||
},
|
||||
});
|
||||
return bookingsForReminders;
|
||||
}
|
||||
}
|
||||
|
||||
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
|
||||
|
||||
type Bookings = UnwrapPromise<ReturnType<typeof getBookings>>;
|
||||
|
||||
// some parts of scheduleWorkflowReminders (reminderSchedule.ts) is quite similar to this code
|
||||
// we should consider refactoring this to reuse similar code snippets
|
||||
export async function scheduleBookingReminders(
|
||||
bookings: Bookings,
|
||||
workflowSteps: Partial<WorkflowStep>[],
|
||||
time: number | null,
|
||||
timeUnit: TimeUnit | null,
|
||||
trigger: WorkflowTriggerEvents,
|
||||
userId: number,
|
||||
teamId: number | null,
|
||||
isOrg: boolean
|
||||
) {
|
||||
if (!bookings.length) return;
|
||||
if (trigger !== WorkflowTriggerEvents.BEFORE_EVENT && trigger !== WorkflowTriggerEvents.AFTER_EVENT) return;
|
||||
|
||||
const bookerUrl = await getBookerBaseUrl(isOrg ? teamId : null);
|
||||
|
||||
const creditService = new CreditService();
|
||||
|
||||
//create reminders for all bookings for each workflow step
|
||||
const promiseSteps = workflowSteps.map(async (step) => {
|
||||
const promiseScheduleReminders = bookings.map(async (booking) => {
|
||||
const defaultLocale = "en";
|
||||
const bookingInfo = {
|
||||
uid: booking.uid,
|
||||
bookerUrl,
|
||||
type: booking.eventType?.slug || "event",
|
||||
attendees: booking.attendees.map((attendee) => {
|
||||
return {
|
||||
name: attendee.name,
|
||||
email: attendee.email,
|
||||
timeZone: attendee.timeZone,
|
||||
language: { locale: attendee.locale || defaultLocale },
|
||||
};
|
||||
}),
|
||||
organizer: booking.user
|
||||
? {
|
||||
language: { locale: booking.user.locale || defaultLocale },
|
||||
name: booking.user.name || "",
|
||||
email: booking?.userPrimaryEmail ?? booking.user.email,
|
||||
timeZone: booking.user.timeZone,
|
||||
timeFormat: getTimeFormatStringFromUserTimeFormat(booking.user.timeFormat),
|
||||
}
|
||||
: { name: "", email: "", timeZone: "", language: { locale: "" } },
|
||||
startTime: booking.startTime?.toISOString(),
|
||||
endTime: booking.endTime?.toISOString(),
|
||||
title: booking.title,
|
||||
language: { locale: booking?.user?.locale || defaultLocale },
|
||||
hideOrganizerEmail: booking.eventType?.hideOrganizerEmail,
|
||||
eventType: {
|
||||
slug: booking.eventType?.slug || "",
|
||||
schedulingType: booking.eventType?.schedulingType,
|
||||
hosts: booking.eventType?.hosts,
|
||||
},
|
||||
metadata: booking.metadata,
|
||||
customReplyToEmail: booking.eventType?.customReplyToEmail,
|
||||
responses: booking.responses as CalEventResponses | null,
|
||||
};
|
||||
if (
|
||||
step.action === WorkflowActions.EMAIL_HOST ||
|
||||
step.action === WorkflowActions.EMAIL_ATTENDEE ||
|
||||
step.action === WorkflowActions.EMAIL_ADDRESS
|
||||
) {
|
||||
let sendTo: string[] = [];
|
||||
|
||||
switch (step.action) {
|
||||
case WorkflowActions.EMAIL_HOST: {
|
||||
sendTo = [bookingInfo.organizer?.email];
|
||||
const schedulingType = bookingInfo.eventType.schedulingType;
|
||||
const hosts = bookingInfo.eventType.hosts
|
||||
?.filter((host) => bookingInfo.attendees.some((attendee) => attendee.email === host.user.email))
|
||||
.map(({ user }) => user.destinationCalendar?.primaryEmail ?? user.email);
|
||||
if (
|
||||
hosts &&
|
||||
(schedulingType === SchedulingType.ROUND_ROBIN || schedulingType === SchedulingType.COLLECTIVE)
|
||||
) {
|
||||
sendTo = sendTo.concat(hosts);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case WorkflowActions.EMAIL_ATTENDEE:
|
||||
sendTo = bookingInfo.attendees.map((attendee) => attendee.email);
|
||||
break;
|
||||
case WorkflowActions.EMAIL_ADDRESS:
|
||||
await verifyEmailSender(step.sendTo || "", userId, teamId);
|
||||
sendTo = [step.sendTo || ""];
|
||||
break;
|
||||
}
|
||||
|
||||
await scheduleEmailReminder({
|
||||
evt: bookingInfo,
|
||||
triggerEvent: trigger,
|
||||
action: step.action,
|
||||
timeSpan: {
|
||||
time,
|
||||
timeUnit,
|
||||
},
|
||||
sendTo,
|
||||
emailSubject: step.emailSubject || "",
|
||||
emailBody: step.reminderBody || "",
|
||||
template: step.template,
|
||||
sender: step.sender,
|
||||
workflowStepId: step.id,
|
||||
verifiedAt: step?.verifiedAt ?? null,
|
||||
});
|
||||
} else if (step.action === WorkflowActions.SMS_NUMBER && step.sendTo) {
|
||||
await scheduleSMSReminder({
|
||||
evt: bookingInfo,
|
||||
reminderPhone: step.sendTo,
|
||||
triggerEvent: trigger,
|
||||
action: step.action,
|
||||
timeSpan: {
|
||||
time,
|
||||
timeUnit,
|
||||
},
|
||||
message: step.reminderBody || "",
|
||||
workflowStepId: step.id,
|
||||
template: step.template,
|
||||
sender: step.sender,
|
||||
userId: userId,
|
||||
teamId: teamId,
|
||||
verifiedAt: step?.verifiedAt ?? null,
|
||||
creditCheckFn: creditService.hasAvailableCredits.bind(creditService),
|
||||
});
|
||||
} else if (step.action === WorkflowActions.WHATSAPP_NUMBER && step.sendTo) {
|
||||
await scheduleWhatsappReminder({
|
||||
evt: bookingInfo,
|
||||
reminderPhone: step.sendTo,
|
||||
triggerEvent: trigger,
|
||||
action: step.action,
|
||||
timeSpan: {
|
||||
time,
|
||||
timeUnit,
|
||||
},
|
||||
message: step.reminderBody || "",
|
||||
workflowStepId: step.id || 0,
|
||||
template: step.template,
|
||||
userId: userId,
|
||||
teamId: teamId,
|
||||
verifiedAt: step?.verifiedAt ?? null,
|
||||
creditCheckFn: creditService.hasAvailableCredits.bind(creditService),
|
||||
});
|
||||
} else if (booking.smsReminderNumber) {
|
||||
if (step.action === WorkflowActions.SMS_ATTENDEE) {
|
||||
await scheduleSMSReminder({
|
||||
evt: bookingInfo,
|
||||
reminderPhone: booking.smsReminderNumber,
|
||||
triggerEvent: trigger,
|
||||
action: step.action,
|
||||
timeSpan: {
|
||||
time,
|
||||
timeUnit,
|
||||
},
|
||||
message: step.reminderBody || "",
|
||||
workflowStepId: step.id,
|
||||
template: step.template,
|
||||
sender: step.sender,
|
||||
userId: userId,
|
||||
teamId: teamId,
|
||||
verifiedAt: step?.verifiedAt ?? null,
|
||||
creditCheckFn: creditService.hasAvailableCredits.bind(creditService),
|
||||
});
|
||||
} else if (step.action === WorkflowActions.WHATSAPP_ATTENDEE) {
|
||||
await scheduleWhatsappReminder({
|
||||
evt: bookingInfo,
|
||||
reminderPhone: booking.smsReminderNumber,
|
||||
triggerEvent: trigger,
|
||||
action: step.action,
|
||||
timeSpan: {
|
||||
time,
|
||||
timeUnit,
|
||||
},
|
||||
message: step.reminderBody || "",
|
||||
workflowStepId: step.id,
|
||||
template: step.template,
|
||||
sender: step.sender,
|
||||
userId: userId,
|
||||
teamId: teamId,
|
||||
verifiedAt: step?.verifiedAt ?? null,
|
||||
creditCheckFn: creditService.hasAvailableCredits.bind(creditService),
|
||||
});
|
||||
}
|
||||
} else if (step.action === WorkflowActions.CAL_AI_PHONE_CALL) {
|
||||
await scheduleAIPhoneCall({
|
||||
evt: bookingInfo,
|
||||
triggerEvent: trigger,
|
||||
timeSpan: {
|
||||
time,
|
||||
timeUnit,
|
||||
},
|
||||
workflowStepId: step.id,
|
||||
userId,
|
||||
teamId,
|
||||
verifiedAt: step?.verifiedAt ?? null,
|
||||
submittedPhoneNumber: booking.smsReminderNumber,
|
||||
routedEventTypeId: null,
|
||||
});
|
||||
}
|
||||
});
|
||||
await Promise.all(promiseScheduleReminders);
|
||||
});
|
||||
return Promise.all(promiseSteps);
|
||||
}
|
||||
|
||||
export function isStepEdited(oldStep: WorkflowStep, newStep: WorkflowStep) {
|
||||
const oldStepKeys = Object.keys(oldStep);
|
||||
const newStepKeys = Object.keys(newStep);
|
||||
@@ -943,54 +317,6 @@ export function isStepEdited(oldStep: WorkflowStep, newStep: WorkflowStep) {
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function getAllWorkflowsFromEventType(
|
||||
eventType: {
|
||||
workflows?: {
|
||||
workflow: WorkflowType;
|
||||
}[];
|
||||
teamId?: number | null;
|
||||
parentId?: number | null;
|
||||
parent?: {
|
||||
id?: number | null;
|
||||
teamId: number | null;
|
||||
} | null;
|
||||
metadata?: Prisma.JsonValue;
|
||||
} | null,
|
||||
userId?: number | null
|
||||
) {
|
||||
if (!eventType) return [];
|
||||
|
||||
const eventTypeWorkflows = eventType?.workflows?.map((workflowRel) => workflowRel.workflow) ?? [];
|
||||
|
||||
const teamId = await getTeamIdFromEventType({
|
||||
eventType: {
|
||||
team: { id: eventType?.teamId ?? null },
|
||||
parentId: eventType?.parentId || eventType?.parent?.id || null,
|
||||
},
|
||||
});
|
||||
|
||||
const orgId = await getOrgIdFromMemberOrTeamId({ memberId: userId, teamId });
|
||||
|
||||
const isManagedEventType = !!eventType?.parent;
|
||||
|
||||
const eventTypeMetadata = EventTypeMetaDataSchema.parse(eventType?.metadata || {});
|
||||
|
||||
const workflowsLockedForUser = isManagedEventType
|
||||
? !eventTypeMetadata?.managedEventConfig?.unlockedFields?.workflows
|
||||
: false;
|
||||
|
||||
const allWorkflows = await getAllWorkflows({
|
||||
entityWorkflows: eventTypeWorkflows,
|
||||
userId,
|
||||
teamId,
|
||||
orgId,
|
||||
workflowsLockedForUser,
|
||||
type: PrismaWorkflowType.EVENT_TYPE,
|
||||
});
|
||||
|
||||
return allWorkflows;
|
||||
}
|
||||
|
||||
export const getEventTypeWorkflows = async (
|
||||
userId: number,
|
||||
eventTypeId: number
|
||||
|
||||
Reference in new issue
Block a user