From 4b8bdeba742d6ce49fb4d6a95264e9d57c4e704a Mon Sep 17 00:00:00 2001 From: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> Date: Tue, 17 Oct 2023 20:19:39 +0200 Subject: [PATCH] fix: scheduleEmailReminders cron job (#11929) Co-authored-by: CarinaWolli --- .../workflows/api/scheduleEmailReminders.ts | 478 ++++++++++-------- .../migration.sql | 5 + packages/prisma/schema.prisma | 2 + 3 files changed, 264 insertions(+), 221 deletions(-) create mode 100644 packages/prisma/migrations/20231016153526_add_workflow_reminder_indexes/migration.sql diff --git a/packages/features/ee/workflows/api/scheduleEmailReminders.ts b/packages/features/ee/workflows/api/scheduleEmailReminders.ts index b63b1ca47c..06aa08c447 100644 --- a/packages/features/ee/workflows/api/scheduleEmailReminders.ts +++ b/packages/features/ee/workflows/api/scheduleEmailReminders.ts @@ -99,26 +99,38 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { const sandboxMode = process.env.NEXT_PUBLIC_IS_E2E ? true : false; - //delete batch_ids with already past scheduled date from scheduled_sends - const remindersToDelete = await prisma.workflowReminder.findMany({ - where: { - method: WorkflowMethods.EMAIL, - cancelled: true, - scheduledDate: { - lte: dayjs().toISOString(), - }, - }, - }); + const pageSize = 90; + let pageNumber = 0; - for (const reminder of remindersToDelete) { - try { - await client.request({ - url: `/v3/user/scheduled_sends/${reminder.referenceId}`, - method: "DELETE", - }); - } catch (error) { - console.log(`Error deleting batch id from scheduled_sends: ${error}`); + //delete batch_ids with already past scheduled date from scheduled_sends + while (true) { + const remindersToDelete = await prisma.workflowReminder.findMany({ + where: { + method: WorkflowMethods.EMAIL, + cancelled: true, + scheduledDate: { + lte: dayjs().toISOString(), + }, + }, + skip: pageNumber * pageSize, + take: pageSize, + }); + + if (remindersToDelete.length === 0) { + break; } + + for (const reminder of remindersToDelete) { + try { + await client.request({ + url: `/v3/user/scheduled_sends/${reminder.referenceId}`, + method: "DELETE", + }); + } catch (error) { + console.log(`Error deleting batch id from scheduled_sends: ${error}`); + } + } + pageNumber++; } await prisma.workflowReminder.deleteMany({ @@ -131,225 +143,249 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { }); //cancel reminders for cancelled/rescheduled bookings that are scheduled within the next hour - const remindersToCancel = await prisma.workflowReminder.findMany({ - where: { - cancelled: true, - scheduled: true, //if it is false then they are already cancelled - scheduledDate: { - lte: dayjs().add(1, "hour").toISOString(), + + pageNumber = 0; + while (true) { + const remindersToCancel = await prisma.workflowReminder.findMany({ + where: { + cancelled: true, + scheduled: true, //if it is false then they are already cancelled + scheduledDate: { + lte: dayjs().add(1, "hour").toISOString(), + }, }, - }, - }); + skip: pageNumber * pageSize, + take: pageSize, + }); - for (const reminder of remindersToCancel) { - try { - await client.request({ - url: "/v3/user/scheduled_sends", - method: "POST", - body: { - batch_id: reminder.referenceId, - status: "cancel", - }, - }); - - await 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) - }, - }); - } catch (error) { - console.log(`Error cancelling scheduled Emails: ${error}`); + if (remindersToCancel.length === 0) { + break; } - } - //find all unscheduled Email reminders - const unscheduledReminders = await prisma.workflowReminder.findMany({ - where: { - method: WorkflowMethods.EMAIL, - scheduled: false, - scheduledDate: { - lte: dayjs().add(72, "hour").toISOString(), - }, - OR: [{ cancelled: false }, { cancelled: null }], - }, - include: { - workflowStep: true, - booking: { - include: { - eventType: true, - user: true, - attendees: true, - }, - }, - }, - }); - - if (!unscheduledReminders.length) { - res.status(200).json({ message: "No Emails to schedule" }); - return; - } - - for (const reminder of unscheduledReminders) { - if (!reminder.workflowStep || !reminder.booking) { - continue; - } - try { - let sendTo; - - switch (reminder.workflowStep.action) { - case WorkflowActions.EMAIL_HOST: - sendTo = reminder.booking.user?.email; - break; - case WorkflowActions.EMAIL_ATTENDEE: - sendTo = reminder.booking.attendees[0].email; - break; - case WorkflowActions.EMAIL_ADDRESS: - sendTo = reminder.workflowStep.sendTo; - } - - const name = - reminder.workflowStep.action === WorkflowActions.EMAIL_ATTENDEE - ? reminder.booking.attendees[0].name - : reminder.booking.user?.name; - - const attendeeName = - reminder.workflowStep.action === WorkflowActions.EMAIL_ATTENDEE - ? reminder.booking.user?.name - : reminder.booking.attendees[0].name; - - const timeZone = - reminder.workflowStep.action === WorkflowActions.EMAIL_ATTENDEE - ? reminder.booking.attendees[0].timeZone - : reminder.booking.user?.timeZone; - - const locale = - reminder.workflowStep.action === WorkflowActions.EMAIL_ATTENDEE || - reminder.workflowStep.action === WorkflowActions.SMS_ATTENDEE - ? reminder.booking.attendees[0].locale - : reminder.booking.user?.locale; - - let emailContent = { - emailSubject: reminder.workflowStep.emailSubject || "", - emailBody: `${reminder.workflowStep.reminderBody || ""}`, - }; - - let emailBodyEmpty = false; - - 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 || "", - variables, - emailLocale, - getTimeFormatStringFromUserTimeFormat(reminder.booking.user?.timeFormat), - !!reminder.booking.user?.hideBranding - ).html; - - emailBodyEmpty = - customTemplate( - reminder.workflowStep.reminderBody || "", - variables, - emailLocale, - getTimeFormatStringFromUserTimeFormat(reminder.booking.user?.timeFormat) - ).text.length === 0; - } 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 - ); - } - - if (emailContent.emailSubject.length > 0 && !emailBodyEmpty && sendTo) { - const batchIdResponse = await client.request({ - url: "/v3/mail/batch", + for (const reminder of remindersToCancel) { + try { + await client.request({ + url: "/v3/user/scheduled_sends", method: "POST", + body: { + batch_id: reminder.referenceId, + status: "cancel", + }, }); - const batchId = batchIdResponse[1].batch_id; - - if (reminder.workflowStep.action !== WorkflowActions.EMAIL_ADDRESS) { - await 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, + scheduled: false, // to know which reminder already got cancelled (to avoid error from cancelling the same reminders again) }, }); + } catch (error) { + console.log(`Error cancelling scheduled Emails: ${error}`); } - } catch (error) { - console.log(`Error scheduling Email with error ${error}`); } + pageNumber++; + } + + pageNumber = 0; + + while (true) { + //find all unscheduled Email reminders + 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, + include: { + workflowStep: true, + booking: { + include: { + eventType: true, + user: true, + attendees: true, + }, + }, + }, + }); + + if (!unscheduledReminders.length && pageNumber === 0) { + res.status(200).json({ message: "No Emails to schedule" }); + return; + } + + if (unscheduledReminders.length === 0) { + break; + } + + for (const reminder of unscheduledReminders) { + if (!reminder.workflowStep || !reminder.booking) { + continue; + } + try { + let sendTo; + + switch (reminder.workflowStep.action) { + case WorkflowActions.EMAIL_HOST: + sendTo = reminder.booking.user?.email; + break; + case WorkflowActions.EMAIL_ATTENDEE: + sendTo = reminder.booking.attendees[0].email; + break; + case WorkflowActions.EMAIL_ADDRESS: + sendTo = reminder.workflowStep.sendTo; + } + + const name = + reminder.workflowStep.action === WorkflowActions.EMAIL_ATTENDEE + ? reminder.booking.attendees[0].name + : reminder.booking.user?.name; + + const attendeeName = + reminder.workflowStep.action === WorkflowActions.EMAIL_ATTENDEE + ? reminder.booking.user?.name + : reminder.booking.attendees[0].name; + + const timeZone = + reminder.workflowStep.action === WorkflowActions.EMAIL_ATTENDEE + ? reminder.booking.attendees[0].timeZone + : reminder.booking.user?.timeZone; + + const locale = + reminder.workflowStep.action === WorkflowActions.EMAIL_ATTENDEE || + reminder.workflowStep.action === WorkflowActions.SMS_ATTENDEE + ? reminder.booking.attendees[0].locale + : reminder.booking.user?.locale; + + let emailContent = { + emailSubject: reminder.workflowStep.emailSubject || "", + emailBody: `${ + reminder.workflowStep.reminderBody || "" + }`, + }; + + let emailBodyEmpty = false; + + 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 || "", + variables, + emailLocale, + getTimeFormatStringFromUserTimeFormat(reminder.booking.user?.timeFormat), + !!reminder.booking.user?.hideBranding + ).html; + + emailBodyEmpty = + customTemplate( + reminder.workflowStep.reminderBody || "", + variables, + emailLocale, + getTimeFormatStringFromUserTimeFormat(reminder.booking.user?.timeFormat) + ).text.length === 0; + } 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 + ); + } + + if (emailContent.emailSubject.length > 0 && !emailBodyEmpty && sendTo) { + const batchIdResponse = await client.request({ + url: "/v3/mail/batch", + method: "POST", + }); + + const batchId = batchIdResponse[1].batch_id; + + if (reminder.workflowStep.action !== WorkflowActions.EMAIL_ADDRESS) { + await 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}`); + } + } + pageNumber++; } res.status(200).json({ message: "Emails scheduled" }); } diff --git a/packages/prisma/migrations/20231016153526_add_workflow_reminder_indexes/migration.sql b/packages/prisma/migrations/20231016153526_add_workflow_reminder_indexes/migration.sql new file mode 100644 index 0000000000..22f0396996 --- /dev/null +++ b/packages/prisma/migrations/20231016153526_add_workflow_reminder_indexes/migration.sql @@ -0,0 +1,5 @@ +-- CreateIndex +CREATE INDEX "WorkflowReminder_method_scheduled_scheduledDate_idx" ON "WorkflowReminder"("method", "scheduled", "scheduledDate"); + +-- CreateIndex +CREATE INDEX "WorkflowReminder_cancelled_scheduledDate_idx" ON "WorkflowReminder"("cancelled", "scheduledDate"); diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index c74a8556c3..75373b7394 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -820,6 +820,8 @@ model WorkflowReminder { @@index([bookingUid]) @@index([workflowStepId]) @@index([seatReferenceId]) + @@index([method, scheduled, scheduledDate]) + @@index([cancelled, scheduledDate]) } model WebhookScheduledTriggers {