import type { Workflow, Prisma } from "@prisma/client"; import { WorkflowTemplates, WorkflowActions, WorkflowTriggerEvents, BookingStatus, WorkflowMethods, TimeUnit, MembershipRole, } from "@prisma/client"; import { z } from "zod"; import emailReminderTemplate from "@calcom/ee/workflows/lib/reminders/templates/emailReminderTemplate"; import { SMS_REMINDER_NUMBER_FIELD, getSmsReminderNumberField, getSmsReminderNumberSource, } from "@calcom/features/bookings/lib/getBookingFields"; import type { WorkflowType } from "@calcom/features/ee/workflows/components/WorkflowListPage"; import { isSMSAction } from "@calcom/features/ee/workflows/lib/actionHelperFunctions"; import { WORKFLOW_TEMPLATES, WORKFLOW_TRIGGER_EVENTS, WORKFLOW_ACTIONS, TIME_UNIT, } from "@calcom/features/ee/workflows/lib/constants"; import { getWorkflowActionOptions } from "@calcom/features/ee/workflows/lib/getOptions"; import { deleteScheduledEmailReminder, scheduleEmailReminder, } from "@calcom/features/ee/workflows/lib/reminders/emailReminderManager"; import { deleteScheduledSMSReminder, scheduleSMSReminder, } from "@calcom/features/ee/workflows/lib/reminders/smsReminderManager"; import { verifyPhoneNumber, sendVerificationCode, } from "@calcom/features/ee/workflows/lib/reminders/verifyPhoneNumber"; import { upsertBookingField, removeBookingField } from "@calcom/features/eventtypes/lib/bookingFieldsManager"; import { IS_SELF_HOSTED, SENDER_ID, CAL_URL } from "@calcom/lib/constants"; import { SENDER_NAME } from "@calcom/lib/constants"; import hasKeyInMetadata from "@calcom/lib/hasKeyInMetadata"; // import { getErrorFromUnknown } from "@calcom/lib/errors"; import { getTranslation } from "@calcom/lib/server/i18n"; import type PrismaType from "@calcom/prisma"; import type { WorkflowStep } from "@calcom/prisma/client"; import { TRPCError } from "@trpc/server"; import { router, authedProcedure } from "../../trpc"; import { viewerTeamsRouter } from "./teams"; function getSender( step: Pick & { senderName: string | null | undefined } ) { return isSMSAction(step.action) ? step.sender || SENDER_ID : step.senderName || SENDER_NAME; } async function isAuthorized( workflow: Pick | null, prisma: typeof PrismaType, currentUserId: number, readOnly?: boolean ) { if (!workflow) { return false; } if (!readOnly) { const userWorkflow = await prisma.workflow.findFirst({ where: { id: workflow.id, OR: [ { userId: currentUserId }, { team: { members: { some: { userId: currentUserId, accepted: true, }, }, }, }, ], }, }); if (userWorkflow) return true; } const userWorkflow = await prisma.workflow.findFirst({ where: { id: workflow.id, OR: [ { userId: currentUserId }, { team: { members: { some: { userId: currentUserId, accepted: true, NOT: { role: MembershipRole.MEMBER, }, }, }, }, }, ], }, }); if (userWorkflow) return true; return false; } export const workflowsRouter = router({ list: authedProcedure .input( z .object({ teamId: z.number().optional(), userId: z.number().optional(), }) .optional() ) .query(async ({ ctx, input }) => { if (input && input.teamId) { const workflows: WorkflowType[] = await ctx.prisma.workflow.findMany({ where: { team: { id: input.teamId, members: { some: { userId: ctx.user.id, accepted: true, }, }, }, }, include: { team: { select: { id: true, slug: true, name: true, members: true, }, }, activeOn: { select: { eventType: { select: { id: true, title: true, }, }, }, }, steps: true, }, orderBy: { id: "asc", }, }); const workflowsWithReadOnly = workflows.map((workflow) => { const readOnly = !!workflow.team?.members?.find( (member) => member.userId === ctx.user.id && member.role === MembershipRole.MEMBER ); return { ...workflow, readOnly }; }); return { workflows: workflowsWithReadOnly }; } if (input && input.userId) { const workflows: WorkflowType[] = await ctx.prisma.workflow.findMany({ where: { userId: ctx.user.id, }, include: { activeOn: { select: { eventType: { select: { id: true, title: true, }, }, }, }, steps: true, team: { select: { id: true, slug: true, name: true, members: true, }, }, }, orderBy: { id: "asc", }, }); return { workflows }; } const workflows = await ctx.prisma.workflow.findMany({ where: { OR: [ { userId: ctx.user.id }, { team: { members: { some: { userId: ctx.user.id, accepted: true, }, }, }, }, ], }, include: { activeOn: { select: { eventType: { select: { id: true, title: true, }, }, }, }, steps: true, team: { select: { id: true, slug: true, name: true, members: true, }, }, }, orderBy: { id: "asc", }, }); const workflowsWithReadOnly: WorkflowType[] = workflows.map((workflow) => { const readOnly = !!workflow.team?.members?.find( (member) => member.userId === ctx.user.id && member.role === MembershipRole.MEMBER ); return { readOnly, ...workflow }; }); return { workflows: workflowsWithReadOnly }; }), get: authedProcedure .input( z.object({ id: z.number(), }) ) .query(async ({ ctx, input }) => { const workflow = await ctx.prisma.workflow.findFirst({ where: { id: input.id, }, select: { id: true, name: true, userId: true, teamId: true, team: { select: { id: true, slug: true, members: true, }, }, time: true, timeUnit: true, activeOn: { select: { eventType: true, }, }, trigger: true, steps: { orderBy: { stepNumber: "asc", }, }, }, }); const isUserAuthorized = await isAuthorized(workflow, ctx.prisma, ctx.user.id); if (!isUserAuthorized) { throw new TRPCError({ code: "UNAUTHORIZED", }); } return workflow; }), create: authedProcedure .input( z.object({ teamId: z.number().optional(), }) ) .mutation(async ({ ctx, input }) => { const { teamId } = input; const userId = ctx.user.id; if (teamId) { const team = await ctx.prisma.team.findFirst({ where: { id: teamId, members: { some: { userId: ctx.user.id, accepted: true, NOT: { role: MembershipRole.MEMBER, }, }, }, }, }); if (!team) { throw new TRPCError({ code: "UNAUTHORIZED", }); } } try { const workflow: Workflow = await ctx.prisma.workflow.create({ data: { name: "", trigger: WorkflowTriggerEvents.BEFORE_EVENT, time: 24, timeUnit: TimeUnit.HOUR, userId, teamId, }, }); await ctx.prisma.workflowStep.create({ data: { stepNumber: 1, action: WorkflowActions.EMAIL_ATTENDEE, template: WorkflowTemplates.REMINDER, reminderBody: emailReminderTemplate(true, WorkflowActions.EMAIL_ATTENDEE).emailBody, emailSubject: emailReminderTemplate(true, WorkflowActions.EMAIL_ATTENDEE).emailSubject, workflowId: workflow.id, sender: SENDER_NAME, numberVerificationPending: false, }, }); return { workflow }; } catch (e) { throw e; } }), delete: authedProcedure .input( z.object({ id: z.number(), }) ) .mutation(async ({ ctx, input }) => { const { id } = input; const workflowToDelete = await ctx.prisma.workflow.findFirst({ where: { id, }, include: { activeOn: true, }, }); const isUserAuthorized = await isAuthorized(workflowToDelete, ctx.prisma, ctx.user.id, true); if (!isUserAuthorized || !workflowToDelete) { throw new TRPCError({ code: "UNAUTHORIZED" }); } const scheduledReminders = await ctx.prisma.workflowReminder.findMany({ where: { workflowStep: { workflowId: id, }, scheduled: true, NOT: { referenceId: null, }, }, }); //cancel workflow reminders of deleted workflow scheduledReminders.forEach((reminder) => { if (reminder.method === WorkflowMethods.EMAIL) { deleteScheduledEmailReminder(reminder.id, reminder.referenceId); } else if (reminder.method === WorkflowMethods.SMS) { deleteScheduledSMSReminder(reminder.id, reminder.referenceId); } }); for (const activeOn of workflowToDelete.activeOn) { await removeSmsReminderFieldForBooking({ workflowId: id, eventTypeId: activeOn.eventTypeId }); } await ctx.prisma.workflow.deleteMany({ where: { id, }, }); return { id, }; }), update: authedProcedure .input( z.object({ id: z.number(), name: z.string(), activeOn: z.number().array(), steps: z .object({ id: z.number(), stepNumber: z.number(), action: z.enum(WORKFLOW_ACTIONS), workflowId: z.number(), sendTo: z.string().optional().nullable(), reminderBody: z.string().optional().nullable(), emailSubject: z.string().optional().nullable(), template: z.enum(WORKFLOW_TEMPLATES), numberRequired: z.boolean().nullable(), sender: z.string().optional().nullable(), senderName: z.string().optional().nullable(), }) .array(), trigger: z.enum(WORKFLOW_TRIGGER_EVENTS), time: z.number().nullable(), timeUnit: z.enum(TIME_UNIT).nullable(), }) ) .mutation(async ({ ctx, input }) => { const { user } = ctx; const { id, name, activeOn, steps, trigger, time, timeUnit } = input; const userWorkflow = await ctx.prisma.workflow.findUnique({ where: { id, }, select: { id: true, userId: true, teamId: true, user: { select: { teams: true, }, }, steps: true, activeOn: true, }, }); const isUserAuthorized = await isAuthorized(userWorkflow, ctx.prisma, ctx.user.id, true); if (!isUserAuthorized || !userWorkflow) { throw new TRPCError({ code: "UNAUTHORIZED" }); } if (steps.find((step) => step.workflowId != id)) { throw new TRPCError({ code: "UNAUTHORIZED" }); } const oldActiveOnEventTypes = await ctx.prisma.workflowsOnEventTypes.findMany({ where: { workflowId: id, }, select: { eventTypeId: true, }, }); const newActiveEventTypes = activeOn.filter((eventType) => { if ( !oldActiveOnEventTypes || !oldActiveOnEventTypes .map((oldEventType) => { return oldEventType.eventTypeId; }) .includes(eventType) ) { return eventType; } }); //check if new event types belong to user or team for (const newEventTypeId of newActiveEventTypes) { const newEventType = await ctx.prisma.eventType.findFirst({ where: { id: newEventTypeId, }, include: { users: true, team: { include: { members: true, }, }, }, }); if (newEventType) { if (userWorkflow.teamId && userWorkflow.teamId !== newEventType.teamId) { throw new TRPCError({ code: "UNAUTHORIZED" }); } if ( !userWorkflow.teamId && userWorkflow.userId && newEventType.userId !== userWorkflow.userId && !newEventType?.users.find((eventTypeUser) => eventTypeUser.id === userWorkflow.userId) ) { throw new TRPCError({ code: "UNAUTHORIZED" }); } } } //remove all scheduled Email and SMS reminders for eventTypes that are not active any more const removedEventTypes = oldActiveOnEventTypes .map((eventType) => { return eventType.eventTypeId; }) .filter((eventType) => { if (!activeOn.includes(eventType)) { return eventType; } }); const remindersToDeletePromise: Prisma.PrismaPromise< { id: number; referenceId: string | null; method: string; scheduled: boolean; }[] >[] = []; removedEventTypes.forEach((eventTypeId) => { const reminderToDelete = ctx.prisma.workflowReminder.findMany({ where: { booking: { eventTypeId: eventTypeId, userId: ctx.user.id, }, workflowStepId: { in: userWorkflow.steps.map((step) => { return step.id; }), }, }, select: { id: true, referenceId: true, method: true, scheduled: true, }, }); remindersToDeletePromise.push(reminderToDelete); }); const remindersToDelete = await Promise.all(remindersToDeletePromise); //cancel workflow reminders for all bookings from event types that got disabled remindersToDelete.flat().forEach((reminder) => { if (reminder.method === WorkflowMethods.EMAIL) { deleteScheduledEmailReminder(reminder.id, reminder.referenceId); } else if (reminder.method === WorkflowMethods.SMS) { deleteScheduledSMSReminder(reminder.id, reminder.referenceId); } }); //update active on & reminders for new eventTypes await ctx.prisma.workflowsOnEventTypes.deleteMany({ where: { workflowId: id, }, }); let newEventTypes: number[] = []; if (activeOn.length) { if (trigger === WorkflowTriggerEvents.BEFORE_EVENT || trigger === WorkflowTriggerEvents.AFTER_EVENT) { newEventTypes = newActiveEventTypes; } if (newEventTypes.length > 0) { //create reminders for all bookings with newEventTypes const bookingsForReminders = await ctx.prisma.booking.findMany({ where: { eventTypeId: { in: newEventTypes }, status: BookingStatus.ACCEPTED, startTime: { gte: new Date(), }, }, include: { attendees: true, eventType: true, user: true, }, }); steps.forEach(async (step) => { if (step.action !== WorkflowActions.SMS_ATTENDEE) { //as we do not have attendees phone number (user is notified about that when setting this action) bookingsForReminders.forEach(async (booking) => { const bookingInfo = { uid: booking.uid, attendees: booking.attendees.map((attendee) => { return { name: attendee.name, email: attendee.email, timeZone: attendee.timeZone, language: { locale: attendee.locale || "" }, }; }), organizer: booking.user ? { language: { locale: booking.user.locale || "" }, name: booking.user.name || "", email: booking.user.email, timeZone: booking.user.timeZone, } : { name: "", email: "", timeZone: "", language: { locale: "" } }, startTime: booking.startTime.toISOString(), endTime: booking.endTime.toISOString(), title: booking.title, language: { locale: booking?.user?.locale || "" }, eventType: { slug: booking.eventType?.slug, }, }; if ( step.action === WorkflowActions.EMAIL_HOST || step.action === WorkflowActions.EMAIL_ATTENDEE /*|| step.action === WorkflowActions.EMAIL_ADDRESS*/ ) { let sendTo = ""; switch (step.action) { case WorkflowActions.EMAIL_HOST: sendTo = bookingInfo.organizer?.email; break; case WorkflowActions.EMAIL_ATTENDEE: sendTo = bookingInfo.attendees[0].email; break; /*case WorkflowActions.EMAIL_ADDRESS: sendTo = step.sendTo || "";*/ } await scheduleEmailReminder( bookingInfo, trigger, step.action, { time, timeUnit, }, sendTo, step.emailSubject || "", step.reminderBody || "", step.id, step.template, step.senderName || SENDER_NAME ); } else if (step.action === WorkflowActions.SMS_NUMBER) { await scheduleSMSReminder( bookingInfo, step.sendTo || "", trigger, step.action, { time, timeUnit, }, step.reminderBody || "", step.id, step.template, step.sender || SENDER_ID, user.id, userWorkflow.teamId ); } }); } }); } //create all workflow - eventtypes relationships activeOn.forEach(async (eventTypeId) => { await ctx.prisma.workflowsOnEventTypes.createMany({ data: { workflowId: id, eventTypeId, }, }); }); } userWorkflow.steps.map(async (oldStep) => { const newStep = steps.filter((s) => s.id === oldStep.id)[0]; const remindersFromStep = await ctx.prisma.workflowReminder.findMany({ where: { workflowStepId: oldStep.id, }, include: { booking: true, }, }); //step was deleted if (!newStep) { // cancel all workflow reminders from deleted steps if (remindersFromStep.length > 0) { remindersFromStep.forEach((reminder) => { if (reminder.method === WorkflowMethods.EMAIL) { deleteScheduledEmailReminder(reminder.id, reminder.referenceId); } else if (reminder.method === WorkflowMethods.SMS) { deleteScheduledSMSReminder(reminder.id, reminder.referenceId); } }); } await ctx.prisma.workflowStep.delete({ where: { id: oldStep.id, }, }); //step was edited } else if (JSON.stringify(oldStep) !== JSON.stringify(newStep)) { if ( !userWorkflow.teamId && !userWorkflow.user?.teams.length && !isSMSAction(oldStep.action) && isSMSAction(newStep.action) ) { throw new TRPCError({ code: "UNAUTHORIZED" }); } await ctx.prisma.workflowStep.update({ where: { id: oldStep.id, }, data: { action: newStep.action, sendTo: newStep.action === WorkflowActions.SMS_NUMBER /*|| newStep.action === WorkflowActions.EMAIL_ADDRESS*/ ? newStep.sendTo : null, stepNumber: newStep.stepNumber, workflowId: newStep.workflowId, reminderBody: newStep.reminderBody, emailSubject: newStep.emailSubject, template: newStep.template, numberRequired: newStep.numberRequired, sender: getSender({ action: newStep.action, sender: newStep.sender || null, senderName: newStep.senderName, }), numberVerificationPending: false, }, }); //cancel all reminders of step and create new ones (not for newEventTypes) const remindersToUpdate = remindersFromStep.filter((reminder) => { if (reminder.booking?.eventTypeId && !newEventTypes.includes(reminder.booking?.eventTypeId)) { return reminder; } }); //cancel all workflow reminders from steps that were edited remindersToUpdate.forEach(async (reminder) => { if (reminder.method === WorkflowMethods.EMAIL) { deleteScheduledEmailReminder(reminder.id, reminder.referenceId); } else if (reminder.method === WorkflowMethods.SMS) { deleteScheduledSMSReminder(reminder.id, reminder.referenceId); } }); const eventTypesToUpdateReminders = activeOn.filter((eventTypeId) => { if (!newEventTypes.includes(eventTypeId)) { return eventTypeId; } }); if ( eventTypesToUpdateReminders && (trigger === WorkflowTriggerEvents.BEFORE_EVENT || trigger === WorkflowTriggerEvents.AFTER_EVENT) ) { const bookingsOfEventTypes = await ctx.prisma.booking.findMany({ where: { eventTypeId: { in: eventTypesToUpdateReminders, }, status: BookingStatus.ACCEPTED, startTime: { gte: new Date(), }, }, include: { attendees: true, eventType: true, user: true, }, }); bookingsOfEventTypes.forEach(async (booking) => { const bookingInfo = { uid: booking.uid, attendees: booking.attendees.map((attendee) => { return { name: attendee.name, email: attendee.email, timeZone: attendee.timeZone, language: { locale: attendee.locale || "" }, }; }), organizer: booking.user ? { language: { locale: booking.user.locale || "" }, name: booking.user.name || "", email: booking.user.email, timeZone: booking.user.timeZone, } : { name: "", email: "", timeZone: "", language: { locale: "" } }, startTime: booking.startTime.toISOString(), endTime: booking.endTime.toISOString(), title: booking.title, language: { locale: booking?.user?.locale || "" }, eventType: { slug: booking.eventType?.slug, }, }; if ( newStep.action === WorkflowActions.EMAIL_HOST || newStep.action === WorkflowActions.EMAIL_ATTENDEE /*|| newStep.action === WorkflowActions.EMAIL_ADDRESS*/ ) { let sendTo = ""; switch (newStep.action) { case WorkflowActions.EMAIL_HOST: sendTo = bookingInfo.organizer?.email; break; case WorkflowActions.EMAIL_ATTENDEE: sendTo = bookingInfo.attendees[0].email; break; /*case WorkflowActions.EMAIL_ADDRESS: sendTo = newStep.sendTo || "";*/ } await scheduleEmailReminder( bookingInfo, trigger, newStep.action, { time, timeUnit, }, sendTo, newStep.emailSubject || "", newStep.reminderBody || "", newStep.id, newStep.template, newStep.senderName || SENDER_NAME ); } else if (newStep.action === WorkflowActions.SMS_NUMBER) { await scheduleSMSReminder( bookingInfo, newStep.sendTo || "", trigger, newStep.action, { time, timeUnit, }, newStep.reminderBody || "", newStep.id || 0, newStep.template, newStep.sender || SENDER_ID, user.id, userWorkflow.teamId ); } }); } } }); //added steps const addedSteps = steps.map((s) => { if (s.id <= 0) { if (!userWorkflow.user?.teams.length && isSMSAction(s.action)) { throw new TRPCError({ code: "UNAUTHORIZED" }); } const { id: _stepId, ...stepToAdd } = s; return stepToAdd; } }); if (addedSteps) { const eventTypesToCreateReminders = activeOn.map((activeEventType) => { if (activeEventType && !newEventTypes.includes(activeEventType)) { return activeEventType; } }); addedSteps.forEach(async (step) => { if (step) { const { senderName, ...newStep } = step; newStep.sender = getSender({ action: newStep.action, sender: newStep.sender || null, senderName: senderName, }); const createdStep = await ctx.prisma.workflowStep.create({ data: { ...newStep, numberVerificationPending: false }, }); if ( (trigger === WorkflowTriggerEvents.BEFORE_EVENT || trigger === WorkflowTriggerEvents.AFTER_EVENT) && eventTypesToCreateReminders && step.action !== WorkflowActions.SMS_ATTENDEE ) { const bookingsForReminders = await ctx.prisma.booking.findMany({ where: { eventTypeId: { in: eventTypesToCreateReminders as number[] }, status: BookingStatus.ACCEPTED, startTime: { gte: new Date(), }, }, include: { attendees: true, eventType: true, user: true, }, }); bookingsForReminders.forEach(async (booking) => { const bookingInfo = { uid: booking.uid, attendees: booking.attendees.map((attendee) => { return { name: attendee.name, email: attendee.email, timeZone: attendee.timeZone, language: { locale: attendee.locale || "" }, }; }), organizer: booking.user ? { name: booking.user.name || "", email: booking.user.email, timeZone: booking.user.timeZone, language: { locale: booking.user.locale || "" }, } : { name: "", email: "", timeZone: "", language: { locale: "" } }, startTime: booking.startTime.toISOString(), endTime: booking.endTime.toISOString(), title: booking.title, language: { locale: booking?.user?.locale || "" }, eventType: { slug: booking.eventType?.slug, }, }; if ( step.action === WorkflowActions.EMAIL_ATTENDEE || step.action === WorkflowActions.EMAIL_HOST /*|| step.action === WorkflowActions.EMAIL_ADDRESS*/ ) { let sendTo = ""; switch (step.action) { case WorkflowActions.EMAIL_HOST: sendTo = bookingInfo.organizer?.email; break; case WorkflowActions.EMAIL_ATTENDEE: sendTo = bookingInfo.attendees[0].email; break; /*case WorkflowActions.EMAIL_ADDRESS: sendTo = step.sendTo || "";*/ } await scheduleEmailReminder( bookingInfo, trigger, step.action, { time, timeUnit, }, sendTo, step.emailSubject || "", step.reminderBody || "", createdStep.id, step.template, step.senderName || SENDER_NAME ); } else if (step.action === WorkflowActions.SMS_NUMBER && step.sendTo) { await scheduleSMSReminder( bookingInfo, step.sendTo, trigger, step.action, { time, timeUnit, }, step.reminderBody || "", createdStep.id, step.template, step.sender || SENDER_ID, user.id, userWorkflow.teamId ); } }); } } }); } //update trigger, name, time, timeUnit await ctx.prisma.workflow.update({ where: { id, }, data: { name, trigger, time, timeUnit, }, }); const workflow = await ctx.prisma.workflow.findFirst({ where: { id, }, include: { activeOn: { select: { eventType: true, }, }, team: { select: { id: true, slug: true, members: true, }, }, steps: { orderBy: { stepNumber: "asc", }, }, }, }); // Remove or add booking field for sms reminder number const smsReminderNumberNeeded = activeOn.length && steps.some((step) => step.action === WorkflowActions.SMS_ATTENDEE); for (const removedEventType of removedEventTypes) { await removeSmsReminderFieldForBooking({ workflowId: id, eventTypeId: removedEventType, }); } for (const eventTypeId of activeOn) { if (smsReminderNumberNeeded) { await upsertSmsReminderFieldForBooking({ workflowId: id, isSmsReminderNumberRequired: steps.some( (s) => s.action === WorkflowActions.SMS_ATTENDEE && s.numberRequired ), eventTypeId, }); } else { await removeSmsReminderFieldForBooking({ workflowId: id, eventTypeId }); } } return { workflow, }; }), /* testAction: authedRateLimitedProcedure({ intervalInMs: 10000, limit: 3 }) .input( z.object({ step: z.object({ id: z.number(), stepNumber: z.number(), action: z.enum(WORKFLOW_ACTIONS), workflowId: z.number(), sendTo: z.string().optional().nullable(), reminderBody: z.string().optional().nullable(), emailSubject: z.string().optional().nullable(), template: z.enum(WORKFLOW_TEMPLATES), numberRequired: z.boolean().nullable(), sender: z.string().optional().nullable(), }), emailSubject: z.string(), reminderBody: z.string(), }) ) .mutation(async ({ ctx, input }) => { const { user } = ctx; const { step, emailSubject, reminderBody } = input; const { action, template, sendTo, sender } = step; const senderID = sender || SENDER_ID; if (action === WorkflowActions.SMS_NUMBER) { if (!sendTo) throw new TRPCError({ code: "BAD_REQUEST", message: "Missing sendTo" }); const verifiedNumbers = await ctx.prisma.verifiedNumber.findFirst({ where: { userId: ctx.user.id, phoneNumber: sendTo, }, }); if (!verifiedNumbers) throw new TRPCError({ code: "UNAUTHORIZED", message: "Phone number is not verified" }); } try { const userWorkflow = await ctx.prisma.workflow.findUnique({ where: { id: step.workflowId, }, select: { userId: true, steps: true, }, }); if (!userWorkflow || userWorkflow.userId !== user.id) { throw new TRPCError({ code: "UNAUTHORIZED" }); } if (isSMSAction(step.action) /*|| step.action === WorkflowActions.EMAIL_ADDRESS*/ /*) { const hasTeamPlan = (await ctx.prisma.membership.count({ where: { userId: user.id } })) > 0; if (!hasTeamPlan) { throw new TRPCError({ code: "UNAUTHORIZED", message: "Team plan needed" }); } } const booking = await ctx.prisma.booking.findFirst({ orderBy: { createdAt: "desc", }, where: { userId: ctx.user.id, }, include: { attendees: true, user: true, }, }); let evt: BookingInfo; if (booking) { evt = { uid: booking?.uid, attendees: booking?.attendees.map((attendee) => { return { name: attendee.name, email: attendee.email, timeZone: attendee.timeZone }; }) || [], organizer: { language: { locale: booking?.user?.locale || "", }, name: booking?.user?.name || "", email: booking?.user?.email || "", timeZone: booking?.user?.timeZone || "", }, startTime: booking?.startTime.toISOString() || "", endTime: booking?.endTime.toISOString() || "", title: booking?.title || "", location: booking?.location || null, additionalNotes: booking?.description || null, customInputs: booking?.customInputs, }; } else { //if no booking exists create an example booking evt = { attendees: [{ name: "John Doe", email: "john.doe@example.com", timeZone: "Europe/London" }], organizer: { language: { locale: ctx.user.locale, }, name: ctx.user.name || "", email: ctx.user.email, timeZone: ctx.user.timeZone, }, startTime: dayjs().add(10, "hour").toISOString(), endTime: dayjs().add(11, "hour").toISOString(), title: "Example Booking", location: "Office", additionalNotes: "These are additional notes", }; } if ( action === WorkflowActions.EMAIL_ATTENDEE || action === WorkflowActions.EMAIL_HOST /*|| action === WorkflowActions.EMAIL_ADDRESS*/ /*) { scheduleEmailReminder( evt, WorkflowTriggerEvents.NEW_EVENT, action, { time: null, timeUnit: null }, ctx.user.email, emailSubject, reminderBody, 0, template ); return { message: "Notification sent" }; } else if (action === WorkflowActions.SMS_NUMBER && sendTo) { scheduleSMSReminder( evt, sendTo, WorkflowTriggerEvents.NEW_EVENT, action, { time: null, timeUnit: null }, reminderBody, 0, template, senderID, ctx.user.id ); return { message: "Notification sent" }; } return { ok: false, status: 500, message: "Notification could not be sent", }; } catch (_err) { const error = getErrorFromUnknown(_err); return { ok: false, status: 500, message: error.message, }; } }), */ activateEventType: authedProcedure .input( z.object({ eventTypeId: z.number(), workflowId: z.number(), }) ) .mutation(async ({ ctx, input }) => { const { eventTypeId, workflowId } = input; // Check that vent type belong to the user or team const userEventType = await ctx.prisma.eventType.findFirst({ where: { id: eventTypeId, OR: [ { userId: ctx.user.id }, { team: { members: { some: { userId: ctx.user.id, accepted: true, NOT: { role: MembershipRole.MEMBER, }, }, }, }, }, ], }, }); if (!userEventType) throw new TRPCError({ code: "UNAUTHORIZED", message: "Not authorized to edit this event type" }); // Check that the workflow belongs to the user or team const eventTypeWorkflow = await ctx.prisma.workflow.findFirst({ where: { id: workflowId, OR: [ { userId: ctx.user.id, }, { teamId: userEventType.teamId, }, ], }, include: { steps: true, }, }); if (!eventTypeWorkflow) throw new TRPCError({ code: "UNAUTHORIZED", message: "Not authorized to enable/disable this workflow", }); //check if event type is already active const isActive = await ctx.prisma.workflowsOnEventTypes.findFirst({ where: { workflowId, eventTypeId, }, }); if (isActive) { await ctx.prisma.workflowsOnEventTypes.deleteMany({ where: { workflowId, eventTypeId, }, }); await removeSmsReminderFieldForBooking({ workflowId, eventTypeId, }); } else { await ctx.prisma.workflowsOnEventTypes.create({ data: { workflowId, eventTypeId, }, }); if ( eventTypeWorkflow.steps.some((step) => { return step.action === WorkflowActions.SMS_ATTENDEE; }) ) { const isSmsReminderNumberRequired = eventTypeWorkflow.steps.some((step) => { return step.action === WorkflowActions.SMS_ATTENDEE && step.numberRequired; }); await upsertSmsReminderFieldForBooking({ workflowId, isSmsReminderNumberRequired, eventTypeId, }); } } }), sendVerificationCode: authedProcedure .input( z.object({ phoneNumber: z.string(), }) ) .mutation(async ({ input }) => { const { phoneNumber } = input; return sendVerificationCode(phoneNumber); }), verifyPhoneNumber: authedProcedure .input( z.object({ phoneNumber: z.string(), code: z.string(), teamId: z.number().optional(), }) ) .mutation(async ({ ctx, input }) => { const { phoneNumber, code, teamId } = input; const { user } = ctx; const verifyStatus = await verifyPhoneNumber(phoneNumber, code, user.id, teamId); return verifyStatus; }), getVerifiedNumbers: authedProcedure .input( z.object({ teamId: z.number().optional(), }) ) .query(async ({ ctx, input }) => { const { user } = ctx; const verifiedNumbers = await ctx.prisma.verifiedNumber.findMany({ where: { OR: [{ userId: user.id }, { teamId: input.teamId }], }, }); return verifiedNumbers; }), getWorkflowActionOptions: authedProcedure.query(async ({ ctx }) => { const { user } = ctx; const isCurrentUsernamePremium = user && user.metadata && hasKeyInMetadata(user, "isPremium"); let isTeamsPlan = false; if (!isCurrentUsernamePremium) { const { hasTeamPlan } = await viewerTeamsRouter.createCaller(ctx).hasTeamPlan(); isTeamsPlan = !!hasTeamPlan; } const t = await getTranslation(ctx.user.locale, "common"); return getWorkflowActionOptions(t, IS_SELF_HOSTED || isCurrentUsernamePremium || isTeamsPlan); }), getByViewer: authedProcedure.query(async ({ ctx }) => { const { prisma } = ctx; const user = await prisma.user.findUnique({ where: { id: ctx.user.id, }, select: { id: true, username: true, avatar: true, name: true, startTime: true, endTime: true, bufferTime: true, workflows: { select: { id: true, name: true, }, }, teams: { where: { accepted: true, }, select: { role: true, team: { select: { id: true, name: true, slug: true, members: { select: { userId: true, }, }, workflows: { select: { id: true, name: true, }, }, }, }, }, }, }, }); if (!user) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" }); } const userWorkflows = user.workflows; type WorkflowGroup = { teamId?: number | null; profile: { slug: (typeof user)["username"]; name: (typeof user)["name"]; image?: string; }; metadata?: { readOnly: boolean; }; workflows: typeof userWorkflows; }; let workflowGroups: WorkflowGroup[] = []; workflowGroups.push({ teamId: null, profile: { slug: user.username, name: user.name, image: user.avatar || undefined, }, workflows: userWorkflows, metadata: { readOnly: false, }, }); workflowGroups = ([] as WorkflowGroup[]).concat( workflowGroups, user.teams.map((membership) => ({ teamId: membership.team.id, profile: { name: membership.team.name, slug: "team/" + membership.team.slug, image: `${CAL_URL}/team/${membership.team.slug}/avatar.png`, }, metadata: { readOnly: membership.role === MembershipRole.MEMBER, }, workflows: membership.team.workflows, })) ); return { workflowGroups: workflowGroups.filter((groupBy) => !!groupBy.workflows?.length), profiles: workflowGroups.map((group) => ({ teamId: group.teamId, ...group.profile, ...group.metadata, })), }; }), }); async function upsertSmsReminderFieldForBooking({ workflowId, eventTypeId, isSmsReminderNumberRequired, }: { workflowId: number; isSmsReminderNumberRequired: boolean; eventTypeId: number; }) { await upsertBookingField( getSmsReminderNumberField(), getSmsReminderNumberSource({ workflowId, isSmsReminderNumberRequired, }), eventTypeId ); } async function removeSmsReminderFieldForBooking({ workflowId, eventTypeId, }: { workflowId: number; eventTypeId: number; }) { await removeBookingField( { name: SMS_REMINDER_NUMBER_FIELD, }, { id: "" + workflowId, type: "workflow", }, eventTypeId ); }