fix: schedule unscheduled emails with tasker in cron job (#20748)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
This commit is contained in:
co-authored by
CarinaWolli
parent
dc361f7879
commit
ffb36509ff
@@ -52,10 +52,6 @@ const getDownloadLinkOfCalVideo = async (recordingId: string) => {
|
||||
};
|
||||
|
||||
export async function postHandler(request: NextRequest) {
|
||||
if (!process.env.SENDGRID_API_KEY || !process.env.SENDGRID_EMAIL) {
|
||||
return NextResponse.json({ message: "No SendGrid API key or email" }, { status: 405 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
|
||||
if (testRequestSchema.safeParse(body).success) {
|
||||
|
||||
@@ -21,7 +21,9 @@ import {
|
||||
getAllRemindersToCancel,
|
||||
getAllRemindersToDelete,
|
||||
getAllUnscheduledReminders,
|
||||
select,
|
||||
} from "../lib/getWorkflowReminders";
|
||||
import { sendOrScheduleWorkflowEmails } from "../lib/reminders/providers/emailProvider";
|
||||
import {
|
||||
cancelScheduledEmail,
|
||||
deleteScheduledSend,
|
||||
@@ -40,28 +42,27 @@ export async function handler(req: NextRequest) {
|
||||
return NextResponse.json({ message: "Not authenticated" }, { status: 401 });
|
||||
}
|
||||
|
||||
if (!process.env.SENDGRID_API_KEY || !process.env.SENDGRID_EMAIL) {
|
||||
return NextResponse.json({ message: "No SendGrid API key or email" }, { status: 405 });
|
||||
}
|
||||
const isSendgridEnabled = process.env.SENDGRID_API_KEY && process.env.SENDGRID_EMAIL;
|
||||
|
||||
// delete batch_ids with already past scheduled date from scheduled_sends
|
||||
const remindersToDelete: { referenceId: string | null }[] = await getAllRemindersToDelete();
|
||||
if (isSendgridEnabled) {
|
||||
// delete batch_ids with already past scheduled date from scheduled_sends
|
||||
const remindersToDelete: { referenceId: string | null }[] = await getAllRemindersToDelete();
|
||||
|
||||
const deletePromises: Promise<any>[] = [];
|
||||
const deletePromises: Promise<any>[] = [];
|
||||
|
||||
for (const reminder of remindersToDelete) {
|
||||
const deletePromise = deleteScheduledSend(reminder.referenceId);
|
||||
deletePromises.push(deletePromise);
|
||||
}
|
||||
for (const reminder of remindersToDelete) {
|
||||
const deletePromise = deleteScheduledSend(reminder.referenceId);
|
||||
deletePromises.push(deletePromise);
|
||||
}
|
||||
|
||||
Promise.allSettled(deletePromises).then((results) => {
|
||||
results.forEach((result) => {
|
||||
if (result.status === "rejected") {
|
||||
logger.error(`Error deleting batch id from scheduled_sends: ${result.reason}`);
|
||||
}
|
||||
Promise.allSettled(deletePromises).then((results) => {
|
||||
results.forEach((result) => {
|
||||
if (result.status === "rejected") {
|
||||
logger.error(`Error deleting batch id from scheduled_sends: ${result.reason}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
}
|
||||
//delete workflow reminders with past scheduled date
|
||||
await prisma.workflowReminder.deleteMany({
|
||||
where: {
|
||||
@@ -72,38 +73,53 @@ export async function handler(req: NextRequest) {
|
||||
},
|
||||
});
|
||||
|
||||
//cancel reminders for cancelled/rescheduled bookings that are scheduled within the next hour
|
||||
const remindersToCancel: { referenceId: string | null; id: number }[] = await getAllRemindersToCancel();
|
||||
if (isSendgridEnabled) {
|
||||
//cancel reminders for cancelled/rescheduled bookings that are scheduled within the next hour
|
||||
const remindersToCancel: { referenceId: string | null; id: number }[] = await getAllRemindersToCancel();
|
||||
|
||||
const cancelUpdatePromises: Promise<any>[] = [];
|
||||
const cancelUpdatePromises: Promise<any>[] = [];
|
||||
|
||||
for (const reminder of remindersToCancel) {
|
||||
const cancelPromise = cancelScheduledEmail(reminder.referenceId);
|
||||
for (const reminder of remindersToCancel) {
|
||||
const cancelPromise = cancelScheduledEmail(reminder.referenceId);
|
||||
|
||||
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)
|
||||
},
|
||||
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)
|
||||
},
|
||||
});
|
||||
|
||||
cancelUpdatePromises.push(cancelPromise, updatePromise);
|
||||
}
|
||||
|
||||
Promise.allSettled(cancelUpdatePromises).then((results) => {
|
||||
results.forEach((result) => {
|
||||
if (result.status === "rejected") {
|
||||
logger.error(`Error cancelling scheduled_sends: ${result.reason}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
cancelUpdatePromises.push(cancelPromise, updatePromise);
|
||||
}
|
||||
|
||||
Promise.allSettled(cancelUpdatePromises).then((results) => {
|
||||
results.forEach((result) => {
|
||||
if (result.status === "rejected") {
|
||||
logger.error(`Error cancelling scheduled_sends: ${result.reason}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// schedule all unscheduled reminders within the next 72 hours
|
||||
const sendEmailPromises: Promise<any>[] = [];
|
||||
|
||||
const unscheduledReminders: PartialWorkflowReminder[] = await getAllUnscheduledReminders();
|
||||
let unscheduledReminders: PartialWorkflowReminder[] = [];
|
||||
|
||||
if (isSendgridEnabled) {
|
||||
unscheduledReminders = await getAllUnscheduledReminders();
|
||||
} else {
|
||||
unscheduledReminders = (await prisma.workflowReminder.findMany({
|
||||
where: {
|
||||
method: WorkflowMethods.EMAIL,
|
||||
scheduled: false,
|
||||
OR: [{ cancelled: false }, { cancelled: null }],
|
||||
},
|
||||
select,
|
||||
})) as PartialWorkflowReminder[];
|
||||
}
|
||||
|
||||
if (!unscheduledReminders.length) {
|
||||
return NextResponse.json({ message: "No Emails to schedule" }, { status: 200 });
|
||||
@@ -113,6 +129,8 @@ export async function handler(req: NextRequest) {
|
||||
if (!reminder.booking) {
|
||||
continue;
|
||||
}
|
||||
const referenceUid = reminder.uuid ?? uuidv4();
|
||||
|
||||
if (!reminder.isMandatoryReminder && reminder.workflowStep) {
|
||||
try {
|
||||
let sendTo;
|
||||
@@ -290,7 +308,7 @@ export async function handler(req: NextRequest) {
|
||||
}
|
||||
|
||||
if (emailContent.emailSubject.length > 0 && !emailBodyEmpty && sendTo) {
|
||||
const batchId = await getBatchId();
|
||||
const batchId = isSendgridEnabled ? await getBatchId() : undefined;
|
||||
|
||||
const booking = reminder.booking;
|
||||
|
||||
@@ -325,35 +343,49 @@ export async function handler(req: NextRequest) {
|
||||
title: booking.title || booking.eventType?.title || "",
|
||||
};
|
||||
|
||||
sendEmailPromises.push(
|
||||
sendSendgridMail({
|
||||
to: sendTo,
|
||||
subject: emailContent.emailSubject,
|
||||
html: emailContent.emailBody,
|
||||
batchId: batchId,
|
||||
sendAt: dayjs(reminder.scheduledDate).unix(),
|
||||
...(!reminder.booking?.eventType?.hideOrganizerEmail && {
|
||||
replyTo:
|
||||
reminder.booking?.eventType?.customReplyToEmail ??
|
||||
reminder.booking?.userPrimaryEmail ??
|
||||
reminder.booking.user?.email,
|
||||
}),
|
||||
attachments: reminder.workflowStep.includeCalendarEvent
|
||||
? [
|
||||
{
|
||||
content: Buffer.from(generateIcsString({ event, status: "CONFIRMED" }) || "").toString(
|
||||
"base64"
|
||||
),
|
||||
filename: "event.ics",
|
||||
type: "text/calendar; method=REQUEST",
|
||||
disposition: "attachment",
|
||||
contentId: uuidv4(),
|
||||
},
|
||||
]
|
||||
: undefined,
|
||||
sender: reminder.workflowStep.sender,
|
||||
})
|
||||
);
|
||||
const mailData = {
|
||||
subject: emailContent.emailSubject,
|
||||
to: Array.isArray(sendTo) ? sendTo : [sendTo],
|
||||
html: emailContent.emailBody,
|
||||
attachments: reminder.workflowStep.includeCalendarEvent
|
||||
? [
|
||||
{
|
||||
content: Buffer.from(generateIcsString({ event, status: "CONFIRMED" }) || "").toString(
|
||||
"base64"
|
||||
),
|
||||
filename: "event.ics",
|
||||
type: "text/calendar; method=REQUEST",
|
||||
disposition: "attachment",
|
||||
contentId: uuidv4(),
|
||||
},
|
||||
]
|
||||
: undefined,
|
||||
sender: reminder.workflowStep.sender,
|
||||
...(!reminder.booking?.eventType?.hideOrganizerEmail && {
|
||||
replyTo:
|
||||
reminder.booking?.eventType?.customReplyToEmail ??
|
||||
reminder.booking?.userPrimaryEmail ??
|
||||
reminder.booking.user?.email,
|
||||
}),
|
||||
};
|
||||
|
||||
if (isSendgridEnabled) {
|
||||
sendEmailPromises.push(
|
||||
sendSendgridMail({
|
||||
...mailData,
|
||||
batchId,
|
||||
sendAt: dayjs(reminder.scheduledDate).unix(),
|
||||
})
|
||||
);
|
||||
} else {
|
||||
sendEmailPromises.push(
|
||||
sendOrScheduleWorkflowEmails({
|
||||
...mailData,
|
||||
referenceUid,
|
||||
sendAt: reminder.scheduledDate,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
await prisma.workflowReminder.update({
|
||||
where: {
|
||||
@@ -362,6 +394,7 @@ export async function handler(req: NextRequest) {
|
||||
data: {
|
||||
scheduled: true,
|
||||
referenceId: batchId,
|
||||
uuid: referenceUid,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -403,24 +436,37 @@ export async function handler(req: NextRequest) {
|
||||
isBrandingDisabled: brandingDisabled,
|
||||
});
|
||||
if (emailContent.emailSubject.length > 0 && !emailBodyEmpty && sendTo) {
|
||||
const batchId = await getBatchId();
|
||||
const batchId = isSendgridEnabled ? await getBatchId() : undefined;
|
||||
|
||||
sendEmailPromises.push(
|
||||
sendSendgridMail({
|
||||
to: sendTo,
|
||||
subject: emailContent.emailSubject,
|
||||
html: emailContent.emailBody,
|
||||
batchId: batchId,
|
||||
sendAt: dayjs(reminder.scheduledDate).unix(),
|
||||
...(!reminder.booking?.eventType?.hideOrganizerEmail && {
|
||||
replyTo:
|
||||
reminder.booking?.eventType?.customReplyToEmail ||
|
||||
reminder.booking?.userPrimaryEmail ||
|
||||
reminder.booking.user?.email,
|
||||
}),
|
||||
sender: reminder.workflowStep?.sender,
|
||||
})
|
||||
);
|
||||
const mailData = {
|
||||
subject: emailContent.emailSubject,
|
||||
to: [sendTo],
|
||||
html: emailContent.emailBody,
|
||||
sender: reminder.workflowStep?.sender,
|
||||
...(!reminder.booking?.eventType?.hideOrganizerEmail && {
|
||||
replyTo:
|
||||
reminder.booking?.eventType?.customReplyToEmail ||
|
||||
reminder.booking?.userPrimaryEmail ||
|
||||
reminder.booking.user?.email,
|
||||
}),
|
||||
};
|
||||
if (isSendgridEnabled) {
|
||||
sendEmailPromises.push(
|
||||
sendSendgridMail({
|
||||
...mailData,
|
||||
batchId,
|
||||
sendAt: dayjs(reminder.scheduledDate).unix(),
|
||||
})
|
||||
);
|
||||
} else {
|
||||
sendEmailPromises.push(
|
||||
sendOrScheduleWorkflowEmails({
|
||||
...mailData,
|
||||
sendAt: reminder.scheduledDate,
|
||||
referenceUid,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
await prisma.workflowReminder.update({
|
||||
where: {
|
||||
@@ -429,6 +475,7 @@ export async function handler(req: NextRequest) {
|
||||
data: {
|
||||
scheduled: true,
|
||||
referenceId: batchId,
|
||||
uuid: referenceUid,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ type PartialBooking =
|
||||
|
||||
export type PartialWorkflowReminder = Pick<
|
||||
WorkflowReminder,
|
||||
"id" | "isMandatoryReminder" | "scheduledDate"
|
||||
"id" | "isMandatoryReminder" | "scheduledDate" | "uuid"
|
||||
> & {
|
||||
booking: PartialBooking | null;
|
||||
} & { workflowStep: PartialWorkflowStep };
|
||||
@@ -123,6 +123,7 @@ export const select = Prisma.validator<Prisma.WorkflowReminderSelect>()({
|
||||
id: true,
|
||||
scheduledDate: true,
|
||||
isMandatoryReminder: true,
|
||||
uuid: true,
|
||||
workflowStep: {
|
||||
select: {
|
||||
action: true,
|
||||
|
||||
@@ -389,24 +389,17 @@ export const deleteScheduledEmailReminder = async (reminderId: number) => {
|
||||
}
|
||||
|
||||
const { uuid, referenceId } = workflowReminder;
|
||||
if (uuid) {
|
||||
const taskId = await tasker.cancelWithReference(uuid, "sendWorkflowEmails");
|
||||
if (taskId) {
|
||||
await prisma.workflowReminder.delete({
|
||||
where: {
|
||||
id: reminderId,
|
||||
},
|
||||
});
|
||||
|
||||
const task = await prisma.task.findFirst({
|
||||
where: {
|
||||
type: "sendWorkflowEmails",
|
||||
referenceUid: uuid,
|
||||
},
|
||||
});
|
||||
|
||||
if (task) {
|
||||
await tasker.cancel(task.id);
|
||||
|
||||
await prisma.workflowReminder.delete({
|
||||
where: {
|
||||
id: reminderId,
|
||||
},
|
||||
});
|
||||
|
||||
return;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Task } from "./repository";
|
||||
import type { TaskTypes } from "./tasker";
|
||||
import { type TaskerCreate, type Tasker } from "./tasker";
|
||||
import tasksMap, { tasksConfig } from "./tasks";
|
||||
|
||||
@@ -52,4 +53,9 @@ export class InternalTasker implements Tasker {
|
||||
const task = await Task.cancel(id);
|
||||
return task.id;
|
||||
}
|
||||
|
||||
async cancelWithReference(referenceUid: string, type: TaskTypes): Promise<string | null> {
|
||||
const task = await Task.cancelWithReference(referenceUid, type);
|
||||
return task?.id ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,6 +154,26 @@ export class Task {
|
||||
});
|
||||
}
|
||||
|
||||
static async cancelWithReference(referenceUid: string, type: TaskTypes) {
|
||||
const task = await db.task.findFirst({
|
||||
where: {
|
||||
referenceUid,
|
||||
type,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!task) return null;
|
||||
|
||||
return await db.task.delete({
|
||||
where: {
|
||||
id: task.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
static async cleanup() {
|
||||
// TODO: Uncomment this later
|
||||
// return db.task.deleteMany({
|
||||
|
||||
@@ -34,4 +34,5 @@ export interface Tasker {
|
||||
processQueue(): Promise<void>;
|
||||
cleanup(): Promise<void>;
|
||||
cancel(id: string): Promise<string>;
|
||||
cancelWithReference(referenceUid: string, type: TaskTypes): Promise<string | null>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user