Skip to content

Commit 2ebfcf4

Browse files
CarinaWolliCarinaWolli
andauthored
Fixes that workflow reminders of cancelled and rescheduled bookings are still sent (#7232)
* small UI fix * fix cancelling scheduled emails * improve comments * delete reminders for rescheduled bookings * add migration file * cancel rescheduled bookings immediately * remove immediate delete for request reschedule --------- Co-authored-by: CarinaWolli <[email protected]>
1 parent 235ec0e commit 2ebfcf4

File tree

10 files changed

+241
-203
lines changed

10 files changed

+241
-203
lines changed

packages/features/bookings/lib/handleCancelBooking.ts

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import {
22
BookingStatus,
33
MembershipRole,
4-
Prisma,
5-
PrismaPromise,
64
WebhookTriggerEvents,
75
WorkflowMethods,
86
WorkflowReminder,
@@ -483,29 +481,18 @@ async function handler(req: NextApiRequest & { userId?: number }) {
483481
cancelScheduledJobs(booking);
484482
});
485483

486-
//Workflows - delete all reminders for bookings
487-
const remindersToDelete: PrismaPromise<Prisma.BatchPayload>[] = [];
484+
//Workflows - cancel all reminders for cancelled bookings
488485
updatedBookings.forEach((booking) => {
489486
booking.workflowReminders.forEach((reminder) => {
490-
if (reminder.scheduled && reminder.referenceId) {
491-
if (reminder.method === WorkflowMethods.EMAIL) {
492-
deleteScheduledEmailReminder(reminder.referenceId);
493-
} else if (reminder.method === WorkflowMethods.SMS) {
494-
deleteScheduledSMSReminder(reminder.referenceId);
495-
}
487+
if (reminder.method === WorkflowMethods.EMAIL) {
488+
deleteScheduledEmailReminder(reminder.id, reminder.referenceId);
489+
} else if (reminder.method === WorkflowMethods.SMS) {
490+
deleteScheduledSMSReminder(reminder.id, reminder.referenceId);
496491
}
497-
const reminderToDelete = prisma.workflowReminder.deleteMany({
498-
where: {
499-
id: reminder.id,
500-
},
501-
});
502-
remindersToDelete.push(reminderToDelete);
503492
});
504493
});
505494

506-
const prismaPromises: Promise<unknown>[] = [attendeeDeletes, bookingReferenceDeletes].concat(
507-
remindersToDelete
508-
);
495+
const prismaPromises: Promise<unknown>[] = [attendeeDeletes, bookingReferenceDeletes];
509496

510497
await Promise.all(prismaPromises.concat(apiDeletes));
511498

packages/features/bookings/lib/handleNewBooking.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
1-
import type { App, Credential, EventTypeCustomInput, Prisma } from "@prisma/client";
2-
import { BookingStatus, SchedulingType, WebhookTriggerEvents } from "@prisma/client";
1+
import {
2+
App,
3+
BookingStatus,
4+
Credential,
5+
EventTypeCustomInput,
6+
Prisma,
7+
SchedulingType,
8+
WebhookTriggerEvents,
9+
WorkflowMethods,
10+
} from "@prisma/client";
311
import async from "async";
412
import { isValidPhoneNumber } from "libphonenumber-js";
513
import { cloneDeep } from "lodash";
@@ -28,7 +36,9 @@ import {
2836
sendScheduledEmails,
2937
sendScheduledSeatsEmails,
3038
} from "@calcom/emails";
39+
import { deleteScheduledEmailReminder } from "@calcom/features/ee/workflows/lib/reminders/emailReminderManager";
3140
import { scheduleWorkflowReminders } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler";
41+
import { deleteScheduledSMSReminder } from "@calcom/features/ee/workflows/lib/reminders/smsReminderManager";
3242
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
3343
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
3444
import { getVideoCallUrl } from "@calcom/lib/CalEventParser";
@@ -759,6 +769,7 @@ async function handler(req: NextApiRequest & { userId?: number | undefined }) {
759769
},
760770
},
761771
payment: true,
772+
workflowReminders: true,
762773
},
763774
});
764775
}
@@ -950,6 +961,19 @@ async function handler(req: NextApiRequest & { userId?: number | undefined }) {
950961
let videoCallUrl;
951962

952963
if (originalRescheduledBooking?.uid) {
964+
try {
965+
// cancel workflow reminders from previous rescheduled booking
966+
originalRescheduledBooking.workflowReminders.forEach((reminder) => {
967+
if (reminder.method === WorkflowMethods.EMAIL) {
968+
deleteScheduledEmailReminder(reminder.id, reminder.referenceId, true);
969+
} else if (reminder.method === WorkflowMethods.SMS) {
970+
deleteScheduledSMSReminder(reminder.id, reminder.referenceId);
971+
}
972+
});
973+
} catch (error) {
974+
log.error("Error while canceling scheduled workflow reminders", error);
975+
}
976+
953977
// Use EventManager to conditionally use all needed integrations.
954978
const updateManager = await eventManager.reschedule(
955979
evt,

packages/features/ee/workflows/api/scheduleEmailReminders.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
77
import dayjs from "@calcom/dayjs";
88
import { defaultHandler } from "@calcom/lib/server";
99
import prisma from "@calcom/prisma";
10+
import { Prisma, WorkflowReminder } from "@calcom/prisma/client";
1011
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
1112

1213
import customTemplate, { VariablesType } from "../lib/reminders/templates/customTemplate";
@@ -39,6 +40,42 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
3940
},
4041
});
4142

43+
//cancel reminders for cancelled/rescheduled bookings that are scheduled within the next hour
44+
const remindersToCancel = await prisma.workflowReminder.findMany({
45+
where: {
46+
cancelled: true,
47+
scheduledDate: {
48+
lte: dayjs().add(1, "hour").toISOString(),
49+
},
50+
},
51+
});
52+
53+
try {
54+
const workflowRemindersToDelete: Prisma.Prisma__WorkflowReminderClient<WorkflowReminder, never>[] = [];
55+
56+
for (const reminder of remindersToCancel) {
57+
await client.request({
58+
url: "/v3/user/scheduled_sends",
59+
method: "POST",
60+
body: {
61+
batch_id: reminder.referenceId,
62+
status: "cancel",
63+
},
64+
});
65+
66+
const workflowReminderToDelete = prisma.workflowReminder.delete({
67+
where: {
68+
id: reminder.id,
69+
},
70+
});
71+
72+
workflowRemindersToDelete.push(workflowReminderToDelete);
73+
}
74+
await Promise.all(workflowRemindersToDelete);
75+
} catch (error) {
76+
console.log(`Error cancelling scheduled Emails: ${error}`);
77+
}
78+
4279
//find all unscheduled Email reminders
4380
const unscheduledReminders = await prisma.workflowReminder.findMany({
4481
where: {

packages/features/ee/workflows/components/WorkflowStepContainer.tsx

Lines changed: 60 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -387,81 +387,75 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
387387
}}
388388
/>
389389
</div>
390-
{(isPhoneNumberNeeded || isSenderIdNeeded) && (
390+
{isPhoneNumberNeeded && (
391391
<div className="mt-2 rounded-md bg-gray-50 p-4 pt-0">
392-
{isPhoneNumberNeeded && (
392+
<Label className="pt-4">{t("custom_phone_number")}</Label>
393+
<div className="block sm:flex">
394+
<PhoneInput<FormValues>
395+
control={form.control}
396+
name={`steps.${step.stepNumber - 1}.sendTo`}
397+
placeholder={t("phone_number")}
398+
id={`steps.${step.stepNumber - 1}.sendTo`}
399+
className="min-w-fit sm:rounded-tl-md sm:rounded-bl-md sm:border-r-transparent"
400+
required
401+
onChange={() => {
402+
const isAlreadyVerified = !!verifiedNumbers
403+
?.concat([])
404+
.find((number) => number === form.getValues(`steps.${step.stepNumber - 1}.sendTo`));
405+
setNumberVerified(isAlreadyVerified);
406+
}}
407+
/>
408+
<Button
409+
color="secondary"
410+
disabled={numberVerified || false}
411+
className={classNames(
412+
"-ml-[3px] h-[40px] min-w-fit sm:block sm:rounded-tl-none sm:rounded-bl-none ",
413+
numberVerified ? "hidden" : "mt-3 sm:mt-0"
414+
)}
415+
onClick={() =>
416+
sendVerificationCodeMutation.mutate({
417+
phoneNumber: form.getValues(`steps.${step.stepNumber - 1}.sendTo`) || "",
418+
})
419+
}>
420+
{t("send_code")}
421+
</Button>
422+
</div>
423+
424+
{form.formState.errors.steps &&
425+
form.formState?.errors?.steps[step.stepNumber - 1]?.sendTo && (
426+
<p className="mt-1 text-xs text-red-500">
427+
{form.formState?.errors?.steps[step.stepNumber - 1]?.sendTo?.message || ""}
428+
</p>
429+
)}
430+
{numberVerified ? (
431+
<div className="mt-1">
432+
<Badge variant="green">{t("number_verified")}</Badge>
433+
</div>
434+
) : (
393435
<>
394-
<Label className="pt-4">{t("custom_phone_number")}</Label>
395-
<div className="block sm:flex">
396-
<PhoneInput<FormValues>
397-
control={form.control}
398-
name={`steps.${step.stepNumber - 1}.sendTo`}
399-
placeholder={t("phone_number")}
400-
id={`steps.${step.stepNumber - 1}.sendTo`}
401-
className="min-w-fit sm:rounded-tl-md sm:rounded-bl-md sm:border-r-transparent"
402-
required
403-
onChange={() => {
404-
const isAlreadyVerified = !!verifiedNumbers
405-
?.concat([])
406-
.find(
407-
(number) => number === form.getValues(`steps.${step.stepNumber - 1}.sendTo`)
408-
);
409-
setNumberVerified(isAlreadyVerified);
436+
<div className="mt-3 flex">
437+
<TextField
438+
className=" border-r-transparent"
439+
placeholder="Verification code"
440+
value={verificationCode}
441+
onChange={(e) => {
442+
setVerificationCode(e.target.value);
410443
}}
444+
required
411445
/>
412446
<Button
413447
color="secondary"
414-
disabled={numberVerified || false}
415-
className={classNames(
416-
"-ml-[3px] h-[40px] min-w-fit sm:block sm:rounded-tl-none sm:rounded-bl-none ",
417-
numberVerified ? "hidden" : "mt-3 sm:mt-0"
418-
)}
419-
onClick={() =>
420-
sendVerificationCodeMutation.mutate({
448+
className="-ml-[3px] rounded-tl-none rounded-bl-none "
449+
disabled={verifyPhoneNumberMutation.isLoading}
450+
onClick={() => {
451+
verifyPhoneNumberMutation.mutate({
421452
phoneNumber: form.getValues(`steps.${step.stepNumber - 1}.sendTo`) || "",
422-
})
423-
}>
424-
{t("send_code")}
453+
code: verificationCode,
454+
});
455+
}}>
456+
Verify
425457
</Button>
426458
</div>
427-
428-
{form.formState.errors.steps &&
429-
form.formState?.errors?.steps[step.stepNumber - 1]?.sendTo && (
430-
<p className="mt-1 text-xs text-red-500">
431-
{form.formState?.errors?.steps[step.stepNumber - 1]?.sendTo?.message || ""}
432-
</p>
433-
)}
434-
{numberVerified ? (
435-
<div className="mt-1">
436-
<Badge variant="green">{t("number_verified")}</Badge>
437-
</div>
438-
) : (
439-
<>
440-
<div className="mt-3 flex">
441-
<TextField
442-
className=" border-r-transparent"
443-
placeholder="Verification code"
444-
value={verificationCode}
445-
onChange={(e) => {
446-
setVerificationCode(e.target.value);
447-
}}
448-
required
449-
/>
450-
<Button
451-
color="secondary"
452-
className="-ml-[3px] rounded-tl-none rounded-bl-none "
453-
disabled={verifyPhoneNumberMutation.isLoading}
454-
onClick={() => {
455-
verifyPhoneNumberMutation.mutate({
456-
phoneNumber: form.getValues(`steps.${step.stepNumber - 1}.sendTo`) || "",
457-
code: verificationCode,
458-
});
459-
}}>
460-
Verify
461-
</Button>
462-
</div>
463-
</>
464-
)}
465459
</>
466460
)}
467461
</div>

packages/features/ee/workflows/lib/reminders/emailReminderManager.ts

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -194,20 +194,41 @@ export const scheduleEmailReminder = async (
194194
}
195195
};
196196

197-
export const deleteScheduledEmailReminder = async (referenceId: string) => {
197+
export const deleteScheduledEmailReminder = async (
198+
reminderId: number,
199+
referenceId: string | null,
200+
immediateDelete?: boolean
201+
) => {
198202
try {
199-
await client.request({
200-
url: "/v3/user/scheduled_sends",
201-
method: "POST",
202-
body: {
203-
batch_id: referenceId,
204-
status: "cancel",
205-
},
206-
});
203+
if (!referenceId) {
204+
await prisma.workflowReminder.delete({
205+
where: {
206+
id: reminderId,
207+
},
208+
});
209+
210+
return;
211+
}
207212

208-
await client.request({
209-
url: `/v3/user/scheduled_sends/${referenceId}`,
210-
method: "DELETE",
213+
if (immediateDelete) {
214+
await client.request({
215+
url: "/v3/user/scheduled_sends",
216+
method: "POST",
217+
body: {
218+
batch_id: referenceId,
219+
status: "cancel",
220+
},
221+
});
222+
return;
223+
}
224+
225+
await prisma.workflowReminder.update({
226+
where: {
227+
id: reminderId,
228+
},
229+
data: {
230+
cancelled: true,
231+
},
211232
});
212233
} catch (error) {
213234
console.log(`Error canceling reminder with error ${error}`);

packages/features/ee/workflows/lib/reminders/smsReminderManager.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,9 +174,16 @@ export const scheduleSMSReminder = async (
174174
}
175175
};
176176

177-
export const deleteScheduledSMSReminder = async (referenceId: string) => {
177+
export const deleteScheduledSMSReminder = async (reminderId: number, referenceId: string | null) => {
178178
try {
179-
await twilio.cancelSMS(referenceId);
179+
if (referenceId) {
180+
await twilio.cancelSMS(referenceId);
181+
}
182+
await prisma.workflowReminder.delete({
183+
where: {
184+
id: reminderId,
185+
},
186+
});
180187
} catch (error) {
181188
console.log(`Error canceling reminder with error ${error}`);
182189
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- AlterTable
2+
ALTER TABLE "WorkflowReminder" ADD COLUMN "cancelled" BOOLEAN;

packages/prisma/schema.prisma

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -641,6 +641,7 @@ model WorkflowReminder {
641641
scheduled Boolean
642642
workflowStepId Int
643643
workflowStep WorkflowStep @relation(fields: [workflowStepId], references: [id], onDelete: Cascade)
644+
cancelled Boolean?
644645
}
645646

646647
enum WorkflowTemplates {

0 commit comments

Comments
 (0)