fix: unhandled promise rejection in scheduleWorkflowReminder (#12301)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
This commit is contained in:
parent
b2f4eaee63
commit
a2f859b55a
|
@ -6,12 +6,19 @@ import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
|
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
|
||||||
|
import logger from "@calcom/lib/logger";
|
||||||
import { defaultHandler } from "@calcom/lib/server";
|
import { defaultHandler } from "@calcom/lib/server";
|
||||||
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
|
import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { WorkflowActions, WorkflowMethods, WorkflowTemplates } from "@calcom/prisma/enums";
|
import { WorkflowActions, WorkflowMethods, WorkflowTemplates } from "@calcom/prisma/enums";
|
||||||
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
|
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||||
|
|
||||||
|
import type { PartialWorkflowReminder } from "../lib/getWorkflowReminders";
|
||||||
|
import {
|
||||||
|
getAllRemindersToCancel,
|
||||||
|
getAllRemindersToDelete,
|
||||||
|
getAllUnscheduledReminders,
|
||||||
|
} from "../lib/getWorkflowReminders";
|
||||||
import { getiCalEventAsString } from "../lib/getiCalEventAsString";
|
import { getiCalEventAsString } from "../lib/getiCalEventAsString";
|
||||||
import type { VariablesType } from "../lib/reminders/templates/customTemplate";
|
import type { VariablesType } from "../lib/reminders/templates/customTemplate";
|
||||||
import customTemplate from "../lib/reminders/templates/customTemplate";
|
import customTemplate from "../lib/reminders/templates/customTemplate";
|
||||||
|
@ -38,45 +45,22 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
const sandboxMode = process.env.NEXT_PUBLIC_IS_E2E ? true : false;
|
const sandboxMode = process.env.NEXT_PUBLIC_IS_E2E ? true : false;
|
||||||
|
|
||||||
// delete batch_ids with already past scheduled date from scheduled_sends
|
// delete batch_ids with already past scheduled date from scheduled_sends
|
||||||
const pageSize = 90;
|
const remindersToDelete: { referenceId: string | null }[] = await getAllRemindersToDelete();
|
||||||
let pageNumber = 0;
|
|
||||||
const deletePromises: Promise<any>[] = [];
|
const deletePromises: Promise<any>[] = [];
|
||||||
|
|
||||||
while (true) {
|
for (const reminder of remindersToDelete) {
|
||||||
const remindersToDelete = await prisma.workflowReminder.findMany({
|
const deletePromise = client.request({
|
||||||
where: {
|
url: `/v3/user/scheduled_sends/${reminder.referenceId}`,
|
||||||
method: WorkflowMethods.EMAIL,
|
method: "DELETE",
|
||||||
cancelled: true,
|
|
||||||
scheduledDate: {
|
|
||||||
lte: dayjs().toISOString(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
skip: pageNumber * pageSize,
|
|
||||||
take: pageSize,
|
|
||||||
select: {
|
|
||||||
referenceId: true,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
deletePromises.push(deletePromise);
|
||||||
if (remindersToDelete.length === 0) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const reminder of remindersToDelete) {
|
|
||||||
const deletePromise = client.request({
|
|
||||||
url: `/v3/user/scheduled_sends/${reminder.referenceId}`,
|
|
||||||
method: "DELETE",
|
|
||||||
});
|
|
||||||
|
|
||||||
deletePromises.push(deletePromise);
|
|
||||||
}
|
|
||||||
pageNumber++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Promise.allSettled(deletePromises).then((results) => {
|
Promise.allSettled(deletePromises).then((results) => {
|
||||||
results.forEach((result) => {
|
results.forEach((result) => {
|
||||||
if (result.status === "rejected") {
|
if (result.status === "rejected") {
|
||||||
console.log(`Error deleting batch id from scheduled_sends: ${result.reason}`);
|
logger.error(`Error deleting batch id from scheduled_sends: ${result.reason}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -92,312 +76,221 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
});
|
});
|
||||||
|
|
||||||
//cancel reminders for cancelled/rescheduled bookings that are scheduled within the next hour
|
//cancel reminders for cancelled/rescheduled bookings that are scheduled within the next hour
|
||||||
pageNumber = 0;
|
const remindersToCancel: { referenceId: string | null; id: number }[] = await getAllRemindersToCancel();
|
||||||
|
|
||||||
const allPromisesCancelReminders: Promise<any>[] = [];
|
const cancelUpdatePromises: Promise<any>[] = [];
|
||||||
|
|
||||||
while (true) {
|
for (const reminder of remindersToCancel) {
|
||||||
const remindersToCancel = await prisma.workflowReminder.findMany({
|
const cancelPromise = client.request({
|
||||||
where: {
|
url: "/v3/user/scheduled_sends",
|
||||||
cancelled: true,
|
method: "POST",
|
||||||
scheduled: true, //if it is false then they are already cancelled
|
body: {
|
||||||
scheduledDate: {
|
batch_id: reminder.referenceId,
|
||||||
lte: dayjs().add(1, "hour").toISOString(),
|
status: "cancel",
|
||||||
},
|
|
||||||
},
|
|
||||||
skip: pageNumber * pageSize,
|
|
||||||
take: pageSize,
|
|
||||||
select: {
|
|
||||||
referenceId: true,
|
|
||||||
id: true,
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (remindersToCancel.length === 0) {
|
const updatePromise = prisma.workflowReminder.update({
|
||||||
break;
|
where: {
|
||||||
}
|
id: reminder.id,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
scheduled: false, // to know which reminder already got cancelled (to avoid error from cancelling the same reminders again)
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
for (const reminder of remindersToCancel) {
|
cancelUpdatePromises.push(cancelPromise, updatePromise);
|
||||||
const cancelPromise = client.request({
|
|
||||||
url: "/v3/user/scheduled_sends",
|
|
||||||
method: "POST",
|
|
||||||
body: {
|
|
||||||
batch_id: reminder.referenceId,
|
|
||||||
status: "cancel",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const updatePromise = prisma.workflowReminder.update({
|
|
||||||
where: {
|
|
||||||
id: reminder.id,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
scheduled: false, // to know which reminder already got cancelled (to avoid error from cancelling the same reminders again)
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
allPromisesCancelReminders.push(cancelPromise, updatePromise);
|
|
||||||
}
|
|
||||||
pageNumber++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Promise.allSettled(allPromisesCancelReminders).then((results) => {
|
Promise.allSettled(cancelUpdatePromises).then((results) => {
|
||||||
results.forEach((result) => {
|
results.forEach((result) => {
|
||||||
if (result.status === "rejected") {
|
if (result.status === "rejected") {
|
||||||
console.log(`Error cancelling scheduled_sends: ${result.reason}`);
|
logger.error(`Error cancelling scheduled_sends: ${result.reason}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// schedule all unscheduled reminders within the next 72 hours
|
// schedule all unscheduled reminders within the next 72 hours
|
||||||
pageNumber = 0;
|
|
||||||
const sendEmailPromises: Promise<any>[] = [];
|
const sendEmailPromises: Promise<any>[] = [];
|
||||||
|
|
||||||
while (true) {
|
const unscheduledReminders: PartialWorkflowReminder[] = await getAllUnscheduledReminders();
|
||||||
const unscheduledReminders = await prisma.workflowReminder.findMany({
|
|
||||||
where: {
|
|
||||||
method: WorkflowMethods.EMAIL,
|
|
||||||
scheduled: false,
|
|
||||||
scheduledDate: {
|
|
||||||
lte: dayjs().add(72, "hour").toISOString(),
|
|
||||||
},
|
|
||||||
OR: [{ cancelled: false }, { cancelled: null }],
|
|
||||||
},
|
|
||||||
skip: pageNumber * pageSize,
|
|
||||||
take: pageSize,
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
scheduledDate: true,
|
|
||||||
workflowStep: {
|
|
||||||
select: {
|
|
||||||
action: true,
|
|
||||||
sendTo: true,
|
|
||||||
reminderBody: true,
|
|
||||||
emailSubject: true,
|
|
||||||
template: true,
|
|
||||||
sender: true,
|
|
||||||
includeCalendarEvent: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
booking: {
|
|
||||||
select: {
|
|
||||||
startTime: true,
|
|
||||||
endTime: true,
|
|
||||||
location: true,
|
|
||||||
description: true,
|
|
||||||
user: {
|
|
||||||
select: {
|
|
||||||
email: true,
|
|
||||||
name: true,
|
|
||||||
timeZone: true,
|
|
||||||
locale: true,
|
|
||||||
username: true,
|
|
||||||
timeFormat: true,
|
|
||||||
hideBranding: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
metadata: true,
|
|
||||||
uid: true,
|
|
||||||
customInputs: true,
|
|
||||||
responses: true,
|
|
||||||
attendees: true,
|
|
||||||
eventType: {
|
|
||||||
select: {
|
|
||||||
bookingFields: true,
|
|
||||||
title: true,
|
|
||||||
slug: true,
|
|
||||||
recurringEvent: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!unscheduledReminders.length && pageNumber === 0) {
|
if (!unscheduledReminders.length) {
|
||||||
res.status(200).json({ message: "No Emails to schedule" });
|
res.status(200).json({ message: "No Emails to schedule" });
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const reminder of unscheduledReminders) {
|
||||||
|
if (!reminder.workflowStep || !reminder.booking) {
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
let sendTo;
|
||||||
|
|
||||||
if (unscheduledReminders.length === 0) {
|
switch (reminder.workflowStep.action) {
|
||||||
break;
|
case WorkflowActions.EMAIL_HOST:
|
||||||
}
|
sendTo = reminder.booking.user?.email;
|
||||||
|
break;
|
||||||
for (const reminder of unscheduledReminders) {
|
case WorkflowActions.EMAIL_ATTENDEE:
|
||||||
if (!reminder.workflowStep || !reminder.booking) {
|
sendTo = reminder.booking.attendees[0].email;
|
||||||
continue;
|
break;
|
||||||
|
case WorkflowActions.EMAIL_ADDRESS:
|
||||||
|
sendTo = reminder.workflowStep.sendTo;
|
||||||
}
|
}
|
||||||
try {
|
|
||||||
let sendTo;
|
|
||||||
|
|
||||||
switch (reminder.workflowStep.action) {
|
const name =
|
||||||
case WorkflowActions.EMAIL_HOST:
|
reminder.workflowStep.action === WorkflowActions.EMAIL_ATTENDEE
|
||||||
sendTo = reminder.booking.user?.email;
|
? reminder.booking.attendees[0].name
|
||||||
break;
|
: reminder.booking.user?.name;
|
||||||
case WorkflowActions.EMAIL_ATTENDEE:
|
|
||||||
sendTo = reminder.booking.attendees[0].email;
|
|
||||||
break;
|
|
||||||
case WorkflowActions.EMAIL_ADDRESS:
|
|
||||||
sendTo = reminder.workflowStep.sendTo;
|
|
||||||
}
|
|
||||||
|
|
||||||
const name =
|
const attendeeName =
|
||||||
reminder.workflowStep.action === WorkflowActions.EMAIL_ATTENDEE
|
reminder.workflowStep.action === WorkflowActions.EMAIL_ATTENDEE
|
||||||
? reminder.booking.attendees[0].name
|
? reminder.booking.user?.name
|
||||||
: reminder.booking.user?.name;
|
: reminder.booking.attendees[0].name;
|
||||||
|
|
||||||
const attendeeName =
|
const timeZone =
|
||||||
reminder.workflowStep.action === WorkflowActions.EMAIL_ATTENDEE
|
reminder.workflowStep.action === WorkflowActions.EMAIL_ATTENDEE
|
||||||
? reminder.booking.user?.name
|
? reminder.booking.attendees[0].timeZone
|
||||||
: reminder.booking.attendees[0].name;
|
: reminder.booking.user?.timeZone;
|
||||||
|
|
||||||
const timeZone =
|
const locale =
|
||||||
reminder.workflowStep.action === WorkflowActions.EMAIL_ATTENDEE
|
reminder.workflowStep.action === WorkflowActions.EMAIL_ATTENDEE ||
|
||||||
? reminder.booking.attendees[0].timeZone
|
reminder.workflowStep.action === WorkflowActions.SMS_ATTENDEE
|
||||||
: reminder.booking.user?.timeZone;
|
? reminder.booking.attendees[0].locale
|
||||||
|
: reminder.booking.user?.locale;
|
||||||
|
|
||||||
const locale =
|
let emailContent = {
|
||||||
reminder.workflowStep.action === WorkflowActions.EMAIL_ATTENDEE ||
|
emailSubject: reminder.workflowStep.emailSubject || "",
|
||||||
reminder.workflowStep.action === WorkflowActions.SMS_ATTENDEE
|
emailBody: `<body style="white-space: pre-wrap;">${reminder.workflowStep.reminderBody || ""}</body>`,
|
||||||
? reminder.booking.attendees[0].locale
|
};
|
||||||
: reminder.booking.user?.locale;
|
|
||||||
|
|
||||||
let emailContent = {
|
let emailBodyEmpty = false;
|
||||||
emailSubject: reminder.workflowStep.emailSubject || "",
|
|
||||||
emailBody: `<body style="white-space: pre-wrap;">${
|
if (reminder.workflowStep.reminderBody) {
|
||||||
reminder.workflowStep.reminderBody || ""
|
const { responses } = getCalEventResponses({
|
||||||
}</body>`,
|
bookingFields: reminder.booking.eventType?.bookingFields ?? null,
|
||||||
|
booking: reminder.booking,
|
||||||
|
});
|
||||||
|
|
||||||
|
const variables: VariablesType = {
|
||||||
|
eventName: reminder.booking.eventType?.title || "",
|
||||||
|
organizerName: reminder.booking.user?.name || "",
|
||||||
|
attendeeName: reminder.booking.attendees[0].name,
|
||||||
|
attendeeEmail: reminder.booking.attendees[0].email,
|
||||||
|
eventDate: dayjs(reminder.booking.startTime).tz(timeZone),
|
||||||
|
eventEndTime: dayjs(reminder.booking?.endTime).tz(timeZone),
|
||||||
|
timeZone: timeZone,
|
||||||
|
location: reminder.booking.location || "",
|
||||||
|
additionalNotes: reminder.booking.description,
|
||||||
|
responses: responses,
|
||||||
|
meetingUrl: bookingMetadataSchema.parse(reminder.booking.metadata || {})?.videoCallUrl,
|
||||||
|
cancelLink: `/booking/${reminder.booking.uid}?cancel=true`,
|
||||||
|
rescheduleLink: `/${reminder.booking.user?.username}/${reminder.booking.eventType?.slug}?rescheduleUid=${reminder.booking.uid}`,
|
||||||
};
|
};
|
||||||
|
const emailLocale = locale || "en";
|
||||||
|
const emailSubject = customTemplate(
|
||||||
|
reminder.workflowStep.emailSubject || "",
|
||||||
|
variables,
|
||||||
|
emailLocale,
|
||||||
|
getTimeFormatStringFromUserTimeFormat(reminder.booking.user?.timeFormat),
|
||||||
|
!!reminder.booking.user?.hideBranding
|
||||||
|
).text;
|
||||||
|
emailContent.emailSubject = emailSubject;
|
||||||
|
emailContent.emailBody = customTemplate(
|
||||||
|
reminder.workflowStep.reminderBody || "",
|
||||||
|
variables,
|
||||||
|
emailLocale,
|
||||||
|
getTimeFormatStringFromUserTimeFormat(reminder.booking.user?.timeFormat),
|
||||||
|
!!reminder.booking.user?.hideBranding
|
||||||
|
).html;
|
||||||
|
|
||||||
let emailBodyEmpty = false;
|
emailBodyEmpty =
|
||||||
|
customTemplate(
|
||||||
if (reminder.workflowStep.reminderBody) {
|
|
||||||
const { responses } = getCalEventResponses({
|
|
||||||
bookingFields: reminder.booking.eventType?.bookingFields ?? null,
|
|
||||||
booking: reminder.booking,
|
|
||||||
});
|
|
||||||
|
|
||||||
const variables: VariablesType = {
|
|
||||||
eventName: reminder.booking.eventType?.title || "",
|
|
||||||
organizerName: reminder.booking.user?.name || "",
|
|
||||||
attendeeName: reminder.booking.attendees[0].name,
|
|
||||||
attendeeEmail: reminder.booking.attendees[0].email,
|
|
||||||
eventDate: dayjs(reminder.booking.startTime).tz(timeZone),
|
|
||||||
eventEndTime: dayjs(reminder.booking?.endTime).tz(timeZone),
|
|
||||||
timeZone: timeZone,
|
|
||||||
location: reminder.booking.location || "",
|
|
||||||
additionalNotes: reminder.booking.description,
|
|
||||||
responses: responses,
|
|
||||||
meetingUrl: bookingMetadataSchema.parse(reminder.booking.metadata || {})?.videoCallUrl,
|
|
||||||
cancelLink: `/booking/${reminder.booking.uid}?cancel=true`,
|
|
||||||
rescheduleLink: `/${reminder.booking.user?.username}/${reminder.booking.eventType?.slug}?rescheduleUid=${reminder.booking.uid}`,
|
|
||||||
};
|
|
||||||
const emailLocale = locale || "en";
|
|
||||||
const emailSubject = customTemplate(
|
|
||||||
reminder.workflowStep.emailSubject || "",
|
|
||||||
variables,
|
|
||||||
emailLocale,
|
|
||||||
getTimeFormatStringFromUserTimeFormat(reminder.booking.user?.timeFormat),
|
|
||||||
!!reminder.booking.user?.hideBranding
|
|
||||||
).text;
|
|
||||||
emailContent.emailSubject = emailSubject;
|
|
||||||
emailContent.emailBody = customTemplate(
|
|
||||||
reminder.workflowStep.reminderBody || "",
|
reminder.workflowStep.reminderBody || "",
|
||||||
variables,
|
variables,
|
||||||
emailLocale,
|
emailLocale,
|
||||||
getTimeFormatStringFromUserTimeFormat(reminder.booking.user?.timeFormat),
|
getTimeFormatStringFromUserTimeFormat(reminder.booking.user?.timeFormat)
|
||||||
!!reminder.booking.user?.hideBranding
|
).text.length === 0;
|
||||||
).html;
|
} else if (reminder.workflowStep.template === WorkflowTemplates.REMINDER) {
|
||||||
|
emailContent = emailReminderTemplate(
|
||||||
|
false,
|
||||||
|
reminder.workflowStep.action,
|
||||||
|
getTimeFormatStringFromUserTimeFormat(reminder.booking.user?.timeFormat),
|
||||||
|
reminder.booking.startTime.toISOString() || "",
|
||||||
|
reminder.booking.endTime.toISOString() || "",
|
||||||
|
reminder.booking.eventType?.title || "",
|
||||||
|
timeZone || "",
|
||||||
|
attendeeName || "",
|
||||||
|
name || "",
|
||||||
|
!!reminder.booking.user?.hideBranding
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
emailBodyEmpty =
|
if (emailContent.emailSubject.length > 0 && !emailBodyEmpty && sendTo) {
|
||||||
customTemplate(
|
const batchIdResponse = await client.request({
|
||||||
reminder.workflowStep.reminderBody || "",
|
url: "/v3/mail/batch",
|
||||||
variables,
|
method: "POST",
|
||||||
emailLocale,
|
});
|
||||||
getTimeFormatStringFromUserTimeFormat(reminder.booking.user?.timeFormat)
|
|
||||||
).text.length === 0;
|
const batchId = batchIdResponse[1].batch_id;
|
||||||
} else if (reminder.workflowStep.template === WorkflowTemplates.REMINDER) {
|
|
||||||
emailContent = emailReminderTemplate(
|
if (reminder.workflowStep.action !== WorkflowActions.EMAIL_ADDRESS) {
|
||||||
false,
|
sendEmailPromises.push(
|
||||||
reminder.workflowStep.action,
|
sgMail.send({
|
||||||
getTimeFormatStringFromUserTimeFormat(reminder.booking.user?.timeFormat),
|
to: sendTo,
|
||||||
reminder.booking.startTime.toISOString() || "",
|
from: {
|
||||||
reminder.booking.endTime.toISOString() || "",
|
email: senderEmail,
|
||||||
reminder.booking.eventType?.title || "",
|
name: reminder.workflowStep.sender || "Cal.com",
|
||||||
timeZone || "",
|
},
|
||||||
attendeeName || "",
|
subject: emailContent.emailSubject,
|
||||||
name || "",
|
html: emailContent.emailBody,
|
||||||
!!reminder.booking.user?.hideBranding
|
batchId: batchId,
|
||||||
|
sendAt: dayjs(reminder.scheduledDate).unix(),
|
||||||
|
replyTo: reminder.booking.user?.email || senderEmail,
|
||||||
|
mailSettings: {
|
||||||
|
sandboxMode: {
|
||||||
|
enable: sandboxMode,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
attachments: reminder.workflowStep.includeCalendarEvent
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
content: Buffer.from(getiCalEventAsString(reminder.booking) || "").toString("base64"),
|
||||||
|
filename: "event.ics",
|
||||||
|
type: "text/calendar; method=REQUEST",
|
||||||
|
disposition: "attachment",
|
||||||
|
contentId: uuidv4(),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: undefined,
|
||||||
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (emailContent.emailSubject.length > 0 && !emailBodyEmpty && sendTo) {
|
await prisma.workflowReminder.update({
|
||||||
const batchIdResponse = await client.request({
|
where: {
|
||||||
url: "/v3/mail/batch",
|
id: reminder.id,
|
||||||
method: "POST",
|
},
|
||||||
});
|
data: {
|
||||||
|
scheduled: true,
|
||||||
const batchId = batchIdResponse[1].batch_id;
|
referenceId: batchId,
|
||||||
|
},
|
||||||
if (reminder.workflowStep.action !== WorkflowActions.EMAIL_ADDRESS) {
|
});
|
||||||
sendEmailPromises.push(
|
|
||||||
sgMail.send({
|
|
||||||
to: sendTo,
|
|
||||||
from: {
|
|
||||||
email: senderEmail,
|
|
||||||
name: reminder.workflowStep.sender || "Cal.com",
|
|
||||||
},
|
|
||||||
subject: emailContent.emailSubject,
|
|
||||||
html: emailContent.emailBody,
|
|
||||||
batchId: batchId,
|
|
||||||
sendAt: dayjs(reminder.scheduledDate).unix(),
|
|
||||||
replyTo: reminder.booking.user?.email || senderEmail,
|
|
||||||
mailSettings: {
|
|
||||||
sandboxMode: {
|
|
||||||
enable: sandboxMode,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
attachments: reminder.workflowStep.includeCalendarEvent
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
content: Buffer.from(getiCalEventAsString(reminder.booking) || "").toString("base64"),
|
|
||||||
filename: "event.ics",
|
|
||||||
type: "text/calendar; method=REQUEST",
|
|
||||||
disposition: "attachment",
|
|
||||||
contentId: uuidv4(),
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: undefined,
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
await prisma.workflowReminder.update({
|
|
||||||
where: {
|
|
||||||
id: reminder.id,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
scheduled: true,
|
|
||||||
referenceId: batchId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.log(`Error scheduling Email with error ${error}`);
|
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(`Error scheduling Email with error ${error}`);
|
||||||
}
|
}
|
||||||
pageNumber++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Promise.allSettled(sendEmailPromises).then((results) => {
|
Promise.allSettled(sendEmailPromises).then((results) => {
|
||||||
results.forEach((result) => {
|
results.forEach((result) => {
|
||||||
if (result.status === "rejected") {
|
if (result.status === "rejected") {
|
||||||
console.log("Email sending failed", result.reason);
|
logger.error("Email sending failed", result.reason);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
res.status(200).json({ message: "Emails scheduled" });
|
res.status(200).json({ message: `${unscheduledReminders.length} Emails scheduled` });
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defaultHandler({
|
export default defaultHandler({
|
||||||
|
|
|
@ -0,0 +1,164 @@
|
||||||
|
import dayjs from "@calcom/dayjs";
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
|
import type { EventType, Prisma, User, WorkflowReminder, WorkflowStep } from "@calcom/prisma/client";
|
||||||
|
import { WorkflowMethods } from "@calcom/prisma/enums";
|
||||||
|
|
||||||
|
type PartialWorkflowStep = Partial<WorkflowStep> | null;
|
||||||
|
|
||||||
|
type Booking = Prisma.BookingGetPayload<{
|
||||||
|
include: {
|
||||||
|
attendees: true;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
|
||||||
|
type PartialBooking =
|
||||||
|
| (Pick<
|
||||||
|
Booking,
|
||||||
|
| "startTime"
|
||||||
|
| "endTime"
|
||||||
|
| "location"
|
||||||
|
| "description"
|
||||||
|
| "metadata"
|
||||||
|
| "customInputs"
|
||||||
|
| "responses"
|
||||||
|
| "uid"
|
||||||
|
| "attendees"
|
||||||
|
> & { eventType: Partial<EventType> | null } & { user: Partial<User> | null })
|
||||||
|
| null;
|
||||||
|
|
||||||
|
export type PartialWorkflowReminder = Pick<WorkflowReminder, "id" | "scheduledDate"> & {
|
||||||
|
booking: PartialBooking | null;
|
||||||
|
} & { workflowStep: PartialWorkflowStep };
|
||||||
|
|
||||||
|
async function getWorkflowReminders<T extends Prisma.WorkflowReminderSelect>(
|
||||||
|
filter: Prisma.WorkflowReminderWhereInput,
|
||||||
|
select: T
|
||||||
|
): Promise<Array<Prisma.WorkflowReminderGetPayload<{ select: T }>>> {
|
||||||
|
const pageSize = 90;
|
||||||
|
let pageNumber = 0;
|
||||||
|
const filteredWorkflowReminders: Array<Prisma.WorkflowReminderGetPayload<{ select: T }>> = [];
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
const newFilteredWorkflowReminders = await prisma.workflowReminder.findMany({
|
||||||
|
where: filter,
|
||||||
|
select: select,
|
||||||
|
skip: pageNumber * pageSize,
|
||||||
|
take: pageSize,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (newFilteredWorkflowReminders.length === 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
filteredWorkflowReminders.push(
|
||||||
|
...(newFilteredWorkflowReminders as Array<Prisma.WorkflowReminderGetPayload<{ select: T }>>)
|
||||||
|
);
|
||||||
|
pageNumber++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return filteredWorkflowReminders;
|
||||||
|
}
|
||||||
|
|
||||||
|
type RemindersToDeleteType = { referenceId: string | null };
|
||||||
|
|
||||||
|
export async function getAllRemindersToDelete(): Promise<RemindersToDeleteType[]> {
|
||||||
|
const whereFilter: Prisma.WorkflowReminderWhereInput = {
|
||||||
|
method: WorkflowMethods.EMAIL,
|
||||||
|
cancelled: true,
|
||||||
|
scheduledDate: {
|
||||||
|
lte: dayjs().toISOString(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const select: Prisma.WorkflowReminderSelect = {
|
||||||
|
referenceId: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const remindersToDelete = await getWorkflowReminders(whereFilter, select);
|
||||||
|
|
||||||
|
return remindersToDelete;
|
||||||
|
}
|
||||||
|
|
||||||
|
type RemindersToCancelType = { referenceId: string | null; id: number };
|
||||||
|
|
||||||
|
export async function getAllRemindersToCancel(): Promise<RemindersToCancelType[]> {
|
||||||
|
const whereFilter: Prisma.WorkflowReminderWhereInput = {
|
||||||
|
cancelled: true,
|
||||||
|
scheduled: true, //if it is false then they are already cancelled
|
||||||
|
scheduledDate: {
|
||||||
|
lte: dayjs().add(1, "hour").toISOString(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const select: Prisma.WorkflowReminderSelect = {
|
||||||
|
referenceId: true,
|
||||||
|
id: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const remindersToCancel = await getWorkflowReminders(whereFilter, select);
|
||||||
|
|
||||||
|
return remindersToCancel;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getAllUnscheduledReminders(): Promise<PartialWorkflowReminder[]> {
|
||||||
|
const whereFilter: Prisma.WorkflowReminderWhereInput = {
|
||||||
|
method: WorkflowMethods.EMAIL,
|
||||||
|
scheduled: false,
|
||||||
|
scheduledDate: {
|
||||||
|
lte: dayjs().add(72, "hour").toISOString(),
|
||||||
|
},
|
||||||
|
OR: [{ cancelled: false }, { cancelled: null }],
|
||||||
|
};
|
||||||
|
|
||||||
|
const select: Prisma.WorkflowReminderSelect = {
|
||||||
|
id: true,
|
||||||
|
scheduledDate: true,
|
||||||
|
workflowStep: {
|
||||||
|
select: {
|
||||||
|
action: true,
|
||||||
|
sendTo: true,
|
||||||
|
reminderBody: true,
|
||||||
|
emailSubject: true,
|
||||||
|
template: true,
|
||||||
|
sender: true,
|
||||||
|
includeCalendarEvent: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
booking: {
|
||||||
|
select: {
|
||||||
|
startTime: true,
|
||||||
|
endTime: true,
|
||||||
|
location: true,
|
||||||
|
description: true,
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
email: true,
|
||||||
|
name: true,
|
||||||
|
timeZone: true,
|
||||||
|
locale: true,
|
||||||
|
username: true,
|
||||||
|
timeFormat: true,
|
||||||
|
hideBranding: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
metadata: true,
|
||||||
|
uid: true,
|
||||||
|
customInputs: true,
|
||||||
|
responses: true,
|
||||||
|
attendees: true,
|
||||||
|
eventType: {
|
||||||
|
select: {
|
||||||
|
bookingFields: true,
|
||||||
|
title: true,
|
||||||
|
slug: true,
|
||||||
|
recurringEvent: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const unscheduledReminders = (await getWorkflowReminders(whereFilter, select)) as PartialWorkflowReminder[];
|
||||||
|
|
||||||
|
return unscheduledReminders;
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user