Files
cal.diy/packages/features/ee/round-robin/roundRobinManualReassignment.ts
T
e04a394e1c fix: (booking-audit) Remove IS_PRODUCTION gate and add feature flag check in producer (#26524)
* fix: remove IS_PRODUCTION gate from BookingAuditProducer

Remove the IS_PRODUCTION check that was preventing booking audits from
being queued in production. Audits are still properly gated by:

1. Organization check: Audits are skipped for non-organization bookings
   (organizationId === null)
2. Feature flag: The BookingAuditTaskConsumer checks if the 'booking-audit'
   feature is enabled for the organization via featuresRepository.checkIfTeamHasFeature()

The IS_PRODUCTION gate was intentionally added to prevent logs from being
created in production while the action data versioning was being actively
reviewed and finalized. Without proper versioning handling, the Booking
History UI could crash when encountering unversioned data. Now that the
versioning system is in place, this gate can be safely removed.

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: revert formatting, keep only IS_PRODUCTION removal

Reverts the unintended formatting changes from the previous commit.
Only removes the IS_PRODUCTION gate without changing indentation.

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* feat: add booking-audit feature flag check in producer to avoid unnecessary task creation

- Query booking-audit and booking-email-sms-tasker flags in parallel before fireBookingEvents
- Pass isBookingAuditEnabled through BookingEventHandler to producer's queueTask method
- Add conditional check in queueTask with debug log when skipping audit
- Reuse pre-queried isBookingEmailSmsTaskerEnabled flag instead of querying again

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* feat: make isBookingAuditEnabled required and pass it through all booking audit flows

- Make isBookingAuditEnabled a required property in BookingAuditProducerService interface
- Update all BookingEventHandler methods to require isBookingAuditEnabled
- Add feature flag check in all flows that call booking audit:
  - handleSeats (seat booking/rescheduling)
  - RecurringBookingService (bulk bookings)
  - handleCancelBooking (booking cancellation)
  - handleConfirmation (booking acceptance)
  - roundRobinReassignment (automatic reassignment)
  - roundRobinManualReassignment (manual reassignment)
  - trpc handlers: addGuests, confirm, editLocation, requestReschedule
- Skip queueing audit tasks when feature is disabled with debug logging

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: add featuresRepository dependency to RecurringBookingService DI module

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* refactor: make isBookingAuditEnabled optional for non-main flows

Keep feature flag check only in main flows (handleSeats, RegularBookingService, RecurringBookingService) which are frequently triggered. For other flows (handleCancelBooking, handleConfirmation, roundRobinReassignment, etc.), rely on the existing consumer-level check.

Changes:
- Revert feature flag check from non-main flows
- Make isBookingAuditEnabled optional in interface for non-main flow methods
- Keep isBookingAuditEnabled required for main flow methods (queueCreatedAudit, queueRescheduledAudit, queueSeatBookedAudit, queueSeatRescheduledAudit, queueBulkCreatedAudit, queueBulkRescheduledAudit)
- Update BookingEventHandlerService to use required params for main flows and optional for non-main flows

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* refactor: remove isBookingAuditEnabled from non-main flow methods

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* feat: make isBookingAuditEnabled required in all BookingEventHandlerService methods

- Add isBookingAuditEnabled as required parameter in all BookingAuditProducerService interface methods
- Update BookingAuditTaskerProducerService to use simplified check (!params.isBookingAuditEnabled)
- Update BookingEventHandlerService to require isBookingAuditEnabled in all methods
- Update all callers to query booking-audit feature flag and pass isBookingAuditEnabled:
  - handleCancelBooking
  - handleConfirmation
  - roundRobinReassignment
  - roundRobinManualReassignment
  - addGuests.handler
  - confirm.handler
  - editLocation.handler
  - requestReschedule.handler
- Inject featuresRepository in API V2's booking-location.service.ts

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* test: update roundRobinReassignment tests to include isBookingAuditEnabled

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* test: update roundRobinManualReassignment tests to include isBookingAuditEnabled

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: inject featuresRepository in API V2 RecurringBookingService

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: import PrismaWorkerModule for PrismaFeaturesRepository dependency

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: use booking's organization for feature flag check in addGuests handler

Use booking.user?.profiles?.[0]?.organizationId instead of user.organizationId
to check the booking-audit feature flag. This ensures the feature flag is
checked against the booking's organization rather than the actor's organization,
which is consistent with other handlers in this PR.

Addresses Cubic AI review feedback (confidence 9/10).

Co-Authored-By: unknown <>

* Add comment

* fix: use user.organizationId for feature flag check in addGuests handler

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: use booking's organizationId for feature flag check in addGuests handler

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: revert to user.organizationId for feature flag check in addGuests handler

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: add isBookingAuditEnabled to onNoShowUpdated calls after main merge

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

* fix: add isBookingAuditEnabled to onNoShowUpdated calls and update tests

Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com>

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-09 09:25:33 -03:00

677 lines
22 KiB
TypeScript

import { enrichUserWithDelegationCredentialsIncludeServiceAccountKey } from "@calcom/app-store/delegationCredential";
import { eventTypeAppMetadataOptionalSchema } from "@calcom/app-store/zod-utils";
import dayjs from "@calcom/dayjs";
import {
sendReassignedEmailsAndSMS,
sendReassignedScheduledEmailsAndSMS,
sendReassignedUpdatedEmailsAndSMS,
} from "@calcom/emails/email-manager";
import { makeUserActor } from "@calcom/features/booking-audit/lib/makeActor";
import type { ValidActionSource } from "@calcom/features/booking-audit/lib/types/actionSource";
import { getBookingEventHandlerService } from "@calcom/features/bookings/di/BookingEventHandlerService.container";
import { getFeaturesRepository } from "@calcom/features/di/containers/FeaturesRepository";
import EventManager from "@calcom/features/bookings/lib/EventManager";
import { getAllCredentialsIncludeServiceAccountKey } from "@calcom/features/bookings/lib/getAllCredentialsForUsersOnEvent/getAllCredentials";
import { getBookingResponsesPartialSchema } from "@calcom/features/bookings/lib/getBookingResponsesSchema";
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
import { getEventTypesFromDB } from "@calcom/features/bookings/lib/handleNewBooking/getEventTypesFromDB";
import { CreditService } from "@calcom/features/ee/billing/credit-service";
import { getBookerBaseUrl } from "@calcom/features/ee/organizations/lib/getBookerUrlServer";
import AssignmentReasonRecorder, {
RRReassignmentType,
} from "@calcom/features/ee/round-robin/assignmentReason/AssignmentReasonRecorder";
import { BookingLocationService } from "@calcom/features/ee/round-robin/lib/bookingLocationService";
import {
deleteScheduledEmailReminder,
scheduleEmailReminder,
} from "@calcom/features/ee/workflows/lib/reminders/emailReminderManager";
import { scheduleWorkflowReminders } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler";
import { getEventName } from "@calcom/features/eventtypes/lib/eventNaming";
import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser";
import { SENDER_NAME } from "@calcom/lib/constants";
import { IdempotencyKeyService } from "@calcom/lib/idempotencyKey/idempotencyKeyService";
import { isPrismaObjOrUndefined } from "@calcom/lib/isPrismaObj";
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 { WorkflowActions, WorkflowMethods, WorkflowTriggerEvents } from "@calcom/prisma/enums";
import type { EventTypeMetadata, PlatformClientParams } from "@calcom/prisma/zod-utils";
import type { CalendarEvent } from "@calcom/types/Calendar";
import { cloneDeep } from "lodash";
import { handleRescheduleEventManager } from "./handleRescheduleEventManager";
import type { BookingSelectResult } from "./utils/bookingSelect";
import { bookingSelect } from "./utils/bookingSelect";
import { getDestinationCalendar } from "./utils/getDestinationCalendar";
import { getTeamMembers } from "./utils/getTeamMembers";
enum ErrorCode {
InvalidRoundRobinHost = "invalid_round_robin_host",
UserIsFixed = "user_is_round_robin_fixed",
}
export const roundRobinManualReassignment = async ({
bookingId,
newUserId,
orgId,
reassignReason,
reassignedById,
emailsEnabled = true,
platformClientParams,
actionSource,
reassignedByUuid,
}: {
bookingId: number;
newUserId: number;
orgId: number | null;
reassignReason?: string;
reassignedById: number;
emailsEnabled?: boolean;
platformClientParams?: PlatformClientParams;
actionSource: ValidActionSource;
reassignedByUuid: string;
}) => {
const roundRobinReassignLogger = logger.getSubLogger({
prefix: ["roundRobinManualReassign", `${bookingId}`],
});
roundRobinReassignLogger.info(`User ${reassignedById} initiating manual reassignment to user ${newUserId}`);
let booking = await prisma.booking.findUnique({
where: { id: bookingId },
select: bookingSelect,
});
if (!booking || !booking.user) {
roundRobinReassignLogger.error(`Booking ${bookingId} not found or has no associated user`);
throw new Error("Booking not found or has no associated user");
}
const eventTypeId = booking.eventTypeId;
if (!eventTypeId) {
roundRobinReassignLogger.error(`Booking ${bookingId} does not have an event type id`);
throw new Error("Event type not found");
}
const eventType = await getEventTypesFromDB(eventTypeId);
if (!eventType) {
roundRobinReassignLogger.error(`Event type ${eventTypeId} not found`);
throw new Error("Event type not found");
}
if (eventType.hostGroups && eventType.hostGroups.length > 1) {
roundRobinReassignLogger.error(
`Event type ${eventTypeId} has more than one round robin group, reassignment is not allowed`
);
throw new Error("Reassignment not allowed with more than one round robin group");
}
const eventTypeHosts = eventType.hosts.length
? eventType.hosts
: eventType.users.map((user) => ({
user,
isFixed: false,
priority: 2,
weight: 100,
schedule: null,
createdAt: new Date(0), // use earliest possible date as fallback
groupId: null,
location: null,
}));
const fixedHost = eventTypeHosts.find((host) => host.isFixed);
let currentRRHostUserUuid = null;
let attendeeUpdatedId = null;
const currentRRHostAttendee = booking.attendees.find((attendee) => {
const matchingEventTypeHost = eventTypeHosts.find(
(host) => !host.isFixed && host.user.email === attendee.email
);
if (matchingEventTypeHost) {
currentRRHostUserUuid = matchingEventTypeHost.user.uuid;
return true;
}
return false;
});
const newUserHost = eventTypeHosts.find((host) => host.user.id === newUserId);
if (!newUserHost) {
throw new Error(ErrorCode.InvalidRoundRobinHost);
}
if (newUserHost.isFixed) {
throw new Error(ErrorCode.UserIsFixed);
}
const originalOrganizer = booking.user;
const hasOrganizerChanged = !fixedHost && booking.userId !== newUserId;
const newUser = newUserHost.user;
const newUserT = await getTranslation(newUser.locale || "en", "common");
const originalOrganizerT = await getTranslation(originalOrganizer.locale || "en", "common");
const roundRobinHosts = eventType.hosts.filter((host) => !host.isFixed);
const attendeeEmailsSet = new Set(booking.attendees.map((attendee) => attendee.email));
// Find the current round robin host assigned
const previousRRHost = (() => {
for (const host of roundRobinHosts) {
if (host.user.id === booking.userId) {
return host.user;
}
if (attendeeEmailsSet.has(host.user.email)) {
return host.user;
}
}
})();
const previousRRHostT = await getTranslation(previousRRHost?.locale || "en", "common");
let bookingLocation = booking.location;
let conferenceCredentialId: number | null = null;
const organizer = hasOrganizerChanged ? newUser : (booking.user ?? newUser);
const teamMembers = await getTeamMembers({
eventTypeHosts,
attendees: booking.attendees,
organizer,
previousHost: previousRRHost || null,
reassignedHost: newUser,
});
if (hasOrganizerChanged) {
const bookingResponses = booking.responses;
const responseSchema = getBookingResponsesPartialSchema({
bookingFields: eventType.bookingFields,
view: "reschedule",
});
const responseSafeParse = await responseSchema.safeParseAsync(bookingResponses);
const responses = responseSafeParse.success ? responseSafeParse.data : undefined;
// Determine the location for the new host
const isManagedEventType = !!eventType.parentId;
const isTeamEventType = !!eventType.teamId;
const locationResult = BookingLocationService.getLocationForHost({
hostMetadata: newUser.metadata,
eventTypeLocations: eventType.locations,
isManagedEventType,
isTeamEventType,
});
bookingLocation = locationResult.bookingLocation;
if (locationResult.requiresActualLink) {
conferenceCredentialId = locationResult.conferenceCredentialId;
}
const newBookingTitle = getEventName({
attendeeName: responses?.name || "Nameless",
eventType: eventType.title,
eventName: eventType.eventName,
// we send on behalf of team if >1 round robin attendee | collective
teamName: teamMembers.length > 1 ? eventType.team?.name : null,
host: newUser.name || "Nameless",
location: bookingLocation || "integrations:daily",
bookingFields: { ...responses },
eventDuration: dayjs(booking.endTime).diff(booking.startTime, "minutes"),
t: newUserT,
});
booking = await prisma.booking.update({
where: { id: bookingId },
data: {
userId: newUserId,
title: newBookingTitle,
userPrimaryEmail: newUser.email,
reassignReason,
reassignById: reassignedById,
idempotencyKey: IdempotencyKeyService.generate({
startTime: booking.startTime,
endTime: booking.endTime,
userId: newUser.id,
reassignedById,
}),
},
select: bookingSelect,
});
await AssignmentReasonRecorder.roundRobinReassignment({
bookingId,
reassignReason,
reassignById: reassignedById,
reassignmentType: RRReassignmentType.MANUAL,
});
} else if (currentRRHostAttendee) {
// Update the round-robin host attendee
await prisma.attendee.update({
where: { id: currentRRHostAttendee.id },
data: {
name: newUser.name || "",
email: newUser.email,
timeZone: newUser.timeZone,
locale: newUser.locale,
},
});
attendeeUpdatedId = currentRRHostAttendee.id;
}
const bookingEventHandlerService = getBookingEventHandlerService();
const featuresRepository = getFeaturesRepository();
const isBookingAuditEnabled = orgId
? await featuresRepository.checkIfTeamHasFeature(orgId, "booking-audit")
: false;
await bookingEventHandlerService.onReassignment({
bookingUid: booking.uid,
actor: makeUserActor(reassignedByUuid),
organizationId: orgId,
source: actionSource,
auditData: {
...(hasOrganizerChanged
? { organizerUuid: { old: originalOrganizer.uuid, new: newUser.uuid } }
: null),
...(attendeeUpdatedId
? {
hostAttendeeUpdated: {
id: attendeeUpdatedId,
withUserUuid: {
old: currentRRHostUserUuid,
new: newUser.uuid,
},
},
}
: null),
reassignmentReason: reassignReason ?? null,
reassignmentType: "manual",
},
isBookingAuditEnabled,
});
// When organizer hasn't changed, still extract conferenceCredentialId from event type locations
if (!hasOrganizerChanged && bookingLocation) {
const { conferenceCredentialId: extractedCredentialId } =
BookingLocationService.getLocationDetailsFromType({
locationType: bookingLocation,
eventTypeLocations: eventType.locations || [],
});
if (extractedCredentialId) {
conferenceCredentialId = extractedCredentialId;
}
}
const destinationCalendar = await getDestinationCalendar({
eventType,
booking,
newUserId,
hasOrganizerChanged,
});
const organizerT = await getTranslation(organizer?.locale || "en", "common");
const attendeePromises = [];
for (const attendee of booking.attendees) {
if (
attendee.email === newUser.email ||
attendee.email === previousRRHost?.email ||
teamMembers.some((member) => member.email === attendee.email)
) {
continue;
}
attendeePromises.push(
getTranslation(attendee.locale ?? "en", "common").then((tAttendee) => ({
email: attendee.email,
name: attendee.name,
timeZone: attendee.timeZone,
language: { translate: tAttendee, locale: attendee.locale ?? "en" },
phoneNumber: attendee.phoneNumber || undefined,
}))
);
}
const attendeeList = await Promise.all(attendeePromises);
const evt: CalendarEvent = {
type: eventType.slug,
title: booking.title,
description: eventType.description,
startTime: dayjs(booking.startTime).utc().format(),
endTime: dayjs(booking.endTime).utc().format(),
organizer: {
email: organizer.email,
name: organizer.name || "",
timeZone: organizer.timeZone,
language: { translate: organizerT, locale: organizer.locale || "en" },
},
attendees: attendeeList,
uid: booking.uid,
iCalUID: booking.iCalUID,
destinationCalendar,
team: {
members: teamMembers,
name: eventType.team?.name || "",
id: eventType.team?.id || 0,
},
schedulingType: eventType.schedulingType,
hideOrganizerEmail: eventType.hideOrganizerEmail,
customInputs: isPrismaObjOrUndefined(booking.customInputs),
...getCalEventResponses({
bookingFields: eventType.bookingFields ?? null,
booking,
}),
customReplyToEmail: eventType?.customReplyToEmail,
location: bookingLocation,
...(platformClientParams ? platformClientParams : {}),
conferenceCredentialId: conferenceCredentialId ?? undefined,
organizationId: orgId,
seatsPerTimeSlot: eventType.seatsPerTimeSlot,
};
if (hasOrganizerChanged) {
// location might changed and will be new created in eventManager.create (organizer default location)
evt.videoCallData = undefined;
// To prevent "The requested identifier already exists" error while updating event, we need to remove iCalUID
evt.iCalUID = undefined;
}
const credentials = await prisma.credential.findMany({
where: { userId: newUser.id },
include: { user: { select: { email: true } } },
});
const newUserWithCredentials = await enrichUserWithDelegationCredentialsIncludeServiceAccountKey({
user: { ...newUser, credentials },
});
const previousHostDestinationCalendar = hasOrganizerChanged
? await prisma.destinationCalendar.findFirst({
where: { userId: originalOrganizer.id },
})
: null;
const apps = eventTypeAppMetadataOptionalSchema.parse(eventType?.metadata?.apps);
// remove the event and meeting using the old host's credentials
if (hasOrganizerChanged && originalOrganizer.id !== newUser.id) {
const previousHostCredentials = await getAllCredentialsIncludeServiceAccountKey(
originalOrganizer,
eventType
);
const originalHostEventManager = new EventManager(
{ ...originalOrganizer, credentials: previousHostCredentials },
apps
);
const deletionEvent: CalendarEvent = {
...evt,
organizer: {
id: originalOrganizer.id,
name: originalOrganizer.name || "",
email: originalOrganizer.email,
username: originalOrganizer.username || undefined,
timeZone: originalOrganizer.timeZone,
language: { translate: originalOrganizerT, locale: originalOrganizer.locale ?? "en" },
timeFormat: getTimeFormatStringFromUserTimeFormat(originalOrganizer.timeFormat),
},
destinationCalendar: previousHostDestinationCalendar ? [previousHostDestinationCalendar] : [],
title: booking.title,
};
await originalHostEventManager.deleteEventsAndMeetings({
event: deletionEvent,
bookingReferences: booking.references,
});
}
const { evtWithAdditionalInfo } = await handleRescheduleEventManager({
evt,
rescheduleUid: booking.uid,
newBookingId: undefined,
changedOrganizer: hasOrganizerChanged,
previousHostDestinationCalendar: previousHostDestinationCalendar ? [previousHostDestinationCalendar] : [],
initParams: {
user: newUserWithCredentials,
eventType,
},
bookingId,
bookingLocation,
bookingICalUID: booking.iCalUID,
bookingMetadata: booking.metadata,
});
const { cancellationReason: _cancellationReason, ...evtWithoutCancellationReason } = evtWithAdditionalInfo;
// Send emails
if (emailsEnabled) {
await sendReassignedScheduledEmailsAndSMS({
calEvent: evtWithoutCancellationReason,
members: [
{
...newUser,
name: newUser.name || "",
username: newUser.username || "",
timeFormat: getTimeFormatStringFromUserTimeFormat(newUser.timeFormat),
language: { translate: newUserT, locale: newUser.locale || "en" },
},
],
reassigned: {
name: newUser.name,
email: newUser.email,
reason: reassignReason,
byUser: originalOrganizer.name || undefined,
},
});
}
// Send cancellation email to previous RR host
const cancelledEvt = cloneDeep(evtWithAdditionalInfo);
cancelledEvt.organizer = {
email: originalOrganizer.email,
name: originalOrganizer.name || "",
timeZone: originalOrganizer.timeZone,
language: { translate: originalOrganizerT, locale: originalOrganizer.locale || "en" },
};
if (previousRRHost && emailsEnabled) {
await sendReassignedEmailsAndSMS({
calEvent: cancelledEvt,
members: [
{
...previousRRHost,
name: previousRRHost.name || "",
username: previousRRHost.username || "",
timeFormat: getTimeFormatStringFromUserTimeFormat(previousRRHost.timeFormat),
language: { translate: previousRRHostT, locale: previousRRHost.locale || "en" },
},
],
reassignedTo: { name: newUser.name, email: newUser.email },
eventTypeMetadata: eventType?.metadata as EventTypeMetadata,
});
}
if (hasOrganizerChanged) {
if (emailsEnabled && dayjs(evt.startTime).isAfter(dayjs())) {
// send email with event updates to attendees
await sendReassignedUpdatedEmailsAndSMS({
calEvent: evtWithoutCancellationReason,
eventTypeMetadata: eventType?.metadata as EventTypeMetadata,
showAttendees: !!eventType.seatsShowAttendees,
});
}
// Handle changing workflows with organizer
await handleWorkflowsUpdate({
booking,
newUser,
evt: evtWithAdditionalInfo,
eventType,
orgId,
});
}
return booking;
};
export async function handleWorkflowsUpdate({
booking,
newUser,
evt,
eventType,
orgId,
}: {
booking: BookingSelectResult;
newUser: {
id: number;
email: string;
locale?: string | null;
};
evt: CalendarEvent;
eventType: Awaited<ReturnType<typeof getEventTypesFromDB>>;
orgId: number | null;
}) {
const workflowReminders = await prisma.workflowReminder.findMany({
where: {
bookingUid: booking.uid,
method: WorkflowMethods.EMAIL,
scheduled: true,
OR: [{ cancelled: false }, { cancelled: null }],
workflowStep: {
action: WorkflowActions.EMAIL_HOST,
workflow: {
trigger: {
in: [
WorkflowTriggerEvents.BEFORE_EVENT,
WorkflowTriggerEvents.NEW_EVENT,
WorkflowTriggerEvents.AFTER_EVENT,
],
},
},
},
},
select: {
id: true,
referenceId: true,
workflowStep: {
select: {
id: true,
template: true,
workflow: {
select: {
userId: true,
teamId: true,
trigger: true,
time: true,
timeUnit: true,
},
},
emailSubject: true,
reminderBody: true,
sender: true,
includeCalendarEvent: true,
verifiedAt: true,
},
},
},
});
const workflowEventMetadata = { videoCallUrl: getVideoCallUrlFromCalEvent(evt) };
const bookerUrl = await getBookerBaseUrl(orgId);
for (const workflowReminder of workflowReminders) {
const workflowStep = workflowReminder.workflowStep;
if (workflowStep) {
const workflow = workflowStep.workflow;
await scheduleEmailReminder({
evt: {
...evt,
metadata: workflowEventMetadata,
eventType,
bookerUrl,
},
action: WorkflowActions.EMAIL_HOST,
triggerEvent: workflow.trigger,
timeSpan: {
time: workflow.time,
timeUnit: workflow.timeUnit,
},
sendTo: [newUser.email],
template: workflowStep.template,
emailSubject: workflowStep.emailSubject || undefined,
emailBody: workflowStep.reminderBody || undefined,
sender: workflowStep.sender || SENDER_NAME,
hideBranding: true,
includeCalendarEvent: workflowStep.includeCalendarEvent,
workflowStepId: workflowStep.id,
verifiedAt: workflowStep.verifiedAt,
});
}
await deleteScheduledEmailReminder(workflowReminder.id);
}
// Send new event workflows to new organizer
const newEventWorkflows = await prisma.workflow.findMany({
where: {
trigger: WorkflowTriggerEvents.NEW_EVENT,
OR: [
{
isActiveOnAll: true,
teamId: eventType?.teamId,
},
{
activeOn: {
some: {
eventTypeId: eventType.id,
},
},
},
...(eventType?.teamId
? [
{
activeOnTeams: {
some: {
teamId: eventType.teamId,
},
},
},
]
: []),
...(eventType?.team?.parentId
? [
{
isActiveOnAll: true,
teamId: eventType.team.parentId,
},
]
: []),
],
},
include: {
steps: {
where: {
action: WorkflowActions.EMAIL_HOST,
},
},
},
});
const creditService = new CreditService();
await scheduleWorkflowReminders({
workflows: newEventWorkflows,
smsReminderNumber: null,
calendarEvent: {
...evt,
metadata: workflowEventMetadata,
eventType: { slug: eventType.slug },
bookerUrl,
},
hideBranding: !!eventType?.owner?.hideBranding,
creditCheckFn: creditService.hasAvailableCredits.bind(creditService),
});
}
export default roundRobinManualReassignment;