Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 6 additions & 19 deletions packages/features/bookings/lib/handleCancelBooking.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import {
BookingStatus,
MembershipRole,
Prisma,
PrismaPromise,
WebhookTriggerEvents,
WorkflowMethods,
WorkflowReminder,
Expand Down Expand Up @@ -483,29 +481,18 @@ async function handler(req: NextApiRequest & { userId?: number }) {
cancelScheduledJobs(booking);
});

//Workflows - delete all reminders for bookings
const remindersToDelete: PrismaPromise<Prisma.BatchPayload>[] = [];
//Workflows - cancel all reminders for cancelled bookings
updatedBookings.forEach((booking) => {
booking.workflowReminders.forEach((reminder) => {
if (reminder.scheduled && reminder.referenceId) {
if (reminder.method === WorkflowMethods.EMAIL) {
deleteScheduledEmailReminder(reminder.referenceId);
} else if (reminder.method === WorkflowMethods.SMS) {
deleteScheduledSMSReminder(reminder.referenceId);
}
if (reminder.method === WorkflowMethods.EMAIL) {
deleteScheduledEmailReminder(reminder.id, reminder.referenceId);
} else if (reminder.method === WorkflowMethods.SMS) {
deleteScheduledSMSReminder(reminder.id, reminder.referenceId);
}
const reminderToDelete = prisma.workflowReminder.deleteMany({
where: {
id: reminder.id,
},
});
remindersToDelete.push(reminderToDelete);
});
});
Comment on lines +484 to 493

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing await on reminder cancellation calls.

The calls to deleteScheduledEmailReminder and deleteScheduledSMSReminder (lines 488, 490) are not awaited, which means errors could be silently ignored and the deletion may not complete before proceeding. This could result in reminders not being cancelled if the process terminates early.

🔧 Proposed fix
  //Workflows - cancel all reminders for cancelled bookings
-  updatedBookings.forEach((booking) => {
+  await Promise.all(updatedBookings.map(async (booking) => {
+    const cancellations = booking.workflowReminders.map((reminder) => {
-    booking.workflowReminders.forEach((reminder) => {
       if (reminder.method === WorkflowMethods.EMAIL) {
-        deleteScheduledEmailReminder(reminder.id, reminder.referenceId);
+        return deleteScheduledEmailReminder(reminder.id, reminder.referenceId);
       } else if (reminder.method === WorkflowMethods.SMS) {
-        deleteScheduledSMSReminder(reminder.id, reminder.referenceId);
+        return deleteScheduledSMSReminder(reminder.id, reminder.referenceId);
       }
+      return Promise.resolve();
     });
-  });
+    return Promise.all(cancellations);
+  }));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
//Workflows - cancel all reminders for cancelled bookings
updatedBookings.forEach((booking) => {
booking.workflowReminders.forEach((reminder) => {
if (reminder.scheduled && reminder.referenceId) {
if (reminder.method === WorkflowMethods.EMAIL) {
deleteScheduledEmailReminder(reminder.referenceId);
} else if (reminder.method === WorkflowMethods.SMS) {
deleteScheduledSMSReminder(reminder.referenceId);
}
if (reminder.method === WorkflowMethods.EMAIL) {
deleteScheduledEmailReminder(reminder.id, reminder.referenceId);
} else if (reminder.method === WorkflowMethods.SMS) {
deleteScheduledSMSReminder(reminder.id, reminder.referenceId);
}
const reminderToDelete = prisma.workflowReminder.deleteMany({
where: {
id: reminder.id,
},
});
remindersToDelete.push(reminderToDelete);
});
});
//Workflows - cancel all reminders for cancelled bookings
await Promise.all(updatedBookings.map(async (booking) => {
const cancellations = booking.workflowReminders.map((reminder) => {
if (reminder.method === WorkflowMethods.EMAIL) {
return deleteScheduledEmailReminder(reminder.id, reminder.referenceId);
} else if (reminder.method === WorkflowMethods.SMS) {
return deleteScheduledSMSReminder(reminder.id, reminder.referenceId);
}
return Promise.resolve();
});
return Promise.all(cancellations);
}));
🤖 Prompt for AI Agents
In @packages/features/bookings/lib/handleCancelBooking.ts around lines 484 -
493, The reminder deletion loop is calling deleteScheduledEmailReminder and
deleteScheduledSMSReminder without awaiting them; update the handling in the
updatedBookings.forEach block so each call is awaited (or collect promises and
await Promise.all) to ensure deletions complete and errors surface before
proceeding—modify the loop that iterates updatedBookings and
booking.workflowReminders to either use async/await (e.g., make the outer
iterator async and await each
deleteScheduledEmailReminder/deleteScheduledSMSReminder) or push the returned
promises and await Promise.all on that array, and preserve references to the
existing functions deleteScheduledEmailReminder and deleteScheduledSMSReminder
and the booking.workflowReminders structure.


const prismaPromises: Promise<unknown>[] = [attendeeDeletes, bookingReferenceDeletes].concat(
remindersToDelete
);
const prismaPromises: Promise<unknown>[] = [attendeeDeletes, bookingReferenceDeletes];

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

Expand Down
28 changes: 26 additions & 2 deletions packages/features/bookings/lib/handleNewBooking.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
import type { App, Credential, EventTypeCustomInput, Prisma } from "@prisma/client";
import { BookingStatus, SchedulingType, WebhookTriggerEvents } from "@prisma/client";
import {
App,
BookingStatus,
Credential,
EventTypeCustomInput,
Prisma,
SchedulingType,
WebhookTriggerEvents,
WorkflowMethods,
} from "@prisma/client";
import async from "async";
import { isValidPhoneNumber } from "libphonenumber-js";
import { cloneDeep } from "lodash";
Expand Down Expand Up @@ -28,7 +36,9 @@ import {
sendScheduledEmails,
sendScheduledSeatsEmails,
} from "@calcom/emails";
import { deleteScheduledEmailReminder } from "@calcom/features/ee/workflows/lib/reminders/emailReminderManager";
import { scheduleWorkflowReminders } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler";
import { deleteScheduledSMSReminder } from "@calcom/features/ee/workflows/lib/reminders/smsReminderManager";
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
import { getVideoCallUrl } from "@calcom/lib/CalEventParser";
Expand Down Expand Up @@ -759,6 +769,7 @@ async function handler(req: NextApiRequest & { userId?: number | undefined }) {
},
},
payment: true,
workflowReminders: true,
},
});
}
Expand Down Expand Up @@ -950,6 +961,19 @@ async function handler(req: NextApiRequest & { userId?: number | undefined }) {
let videoCallUrl;

if (originalRescheduledBooking?.uid) {
try {
// cancel workflow reminders from previous rescheduled booking
originalRescheduledBooking.workflowReminders.forEach((reminder) => {
if (reminder.method === WorkflowMethods.EMAIL) {
deleteScheduledEmailReminder(reminder.id, reminder.referenceId, true);
} else if (reminder.method === WorkflowMethods.SMS) {
deleteScheduledSMSReminder(reminder.id, reminder.referenceId);
}
});
} catch (error) {
log.error("Error while canceling scheduled workflow reminders", error);
}
Comment on lines +964 to +975

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Async delete operations inside forEach are not awaited.

The forEach loop calls async functions deleteScheduledEmailReminder and deleteScheduledSMSReminder without await. These will execute as fire-and-forget operations, and any errors won't be caught by the surrounding try-catch. Additionally, the reschedule flow may proceed before reminders are fully cancelled.

🔧 Suggested fix using Promise.all
   try {
     // cancel workflow reminders from previous rescheduled booking
-    originalRescheduledBooking.workflowReminders.forEach((reminder) => {
-      if (reminder.method === WorkflowMethods.EMAIL) {
-        deleteScheduledEmailReminder(reminder.id, reminder.referenceId, true);
-      } else if (reminder.method === WorkflowMethods.SMS) {
-        deleteScheduledSMSReminder(reminder.id, reminder.referenceId);
-      }
-    });
+    await Promise.all(
+      originalRescheduledBooking.workflowReminders.map(async (reminder) => {
+        if (reminder.method === WorkflowMethods.EMAIL) {
+          await deleteScheduledEmailReminder(reminder.id, reminder.referenceId, true);
+        } else if (reminder.method === WorkflowMethods.SMS) {
+          await deleteScheduledSMSReminder(reminder.id, reminder.referenceId);
+        }
+      })
+    );
   } catch (error) {
     log.error("Error while canceling scheduled workflow reminders", error);
   }
🤖 Prompt for AI Agents
In @packages/features/bookings/lib/handleNewBooking.ts around lines 964 - 975,
The current loop over originalRescheduledBooking.workflowReminders invokes async
functions deleteScheduledEmailReminder and deleteScheduledSMSReminder inside a
forEach without awaiting, so cancellations run fire-and-forget and errors escape
the try-catch; change this to collect the promises and await them (e.g., const
promises = originalRescheduledBooking.workflowReminders.map(r => r.method ===
WorkflowMethods.EMAIL ? deleteScheduledEmailReminder(r.id, r.referenceId, true)
: deleteScheduledSMSReminder(r.id, r.referenceId)); await
Promise.all(promises)); ensure the enclosing function (e.g., handleNewBooking)
is async so the await is valid and errors propagate to the existing catch for
proper handling.


// Use EventManager to conditionally use all needed integrations.
const updateManager = await eventManager.reschedule(
evt,
Expand Down
37 changes: 37 additions & 0 deletions packages/features/ee/workflows/api/scheduleEmailReminders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
import dayjs from "@calcom/dayjs";
import { defaultHandler } from "@calcom/lib/server";
import prisma from "@calcom/prisma";
import { Prisma, WorkflowReminder } from "@calcom/prisma/client";
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";

import customTemplate, { VariablesType } from "../lib/reminders/templates/customTemplate";
Expand Down Expand Up @@ -39,6 +40,42 @@ 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,
scheduledDate: {
lte: dayjs().add(1, "hour").toISOString(),
},
},
});
Comment on lines +44 to +51

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Missing filter for EMAIL method and referenceId in cancellation query.

This query fetches all cancelled reminders regardless of method, but this handler is specifically for email reminders. Additionally, reminders without a referenceId would cause the SendGrid API call to fail with null batch_id.

🔧 Suggested fix
   const remindersToCancel = await prisma.workflowReminder.findMany({
     where: {
       cancelled: true,
+      method: WorkflowMethods.EMAIL,
       scheduledDate: {
         lte: dayjs().add(1, "hour").toISOString(),
       },
+      NOT: {
+        referenceId: null,
+      },
     },
   });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const remindersToCancel = await prisma.workflowReminder.findMany({
where: {
cancelled: true,
scheduledDate: {
lte: dayjs().add(1, "hour").toISOString(),
},
},
});
const remindersToCancel = await prisma.workflowReminder.findMany({
where: {
cancelled: true,
method: WorkflowMethods.EMAIL,
scheduledDate: {
lte: dayjs().add(1, "hour").toISOString(),
},
NOT: {
referenceId: null,
},
},
});
🤖 Prompt for AI Agents
In @packages/features/ee/workflows/api/scheduleEmailReminders.ts around lines 44
- 51, The cancellation query fetches all cancelled reminders but needs to be
restricted to email reminders with a non-null referenceId to avoid SendGrid
calls with null batch_id; update the prisma.workflowReminder.findMany call
(variable remindersToCancel) to include where.method === "EMAIL" (or the
enum/constant used) and where.referenceId is not null while keeping cancelled
and scheduledDate filters so only cancelled email reminders that have a
referenceId are returned.


try {
const workflowRemindersToDelete: Prisma.Prisma__WorkflowReminderClient<WorkflowReminder, never>[] = [];

for (const reminder of remindersToCancel) {
await client.request({
url: "/v3/user/scheduled_sends",
method: "POST",
body: {
batch_id: reminder.referenceId,
status: "cancel",
},
});

const workflowReminderToDelete = prisma.workflowReminder.delete({
where: {
id: reminder.id,
},
});

workflowRemindersToDelete.push(workflowReminderToDelete);
}
await Promise.all(workflowRemindersToDelete);
} catch (error) {
console.log(`Error cancelling scheduled Emails: ${error}`);
}
Comment on lines +53 to +77

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Single API failure halts processing of remaining reminders.

If the SendGrid cancel request fails for one reminder, the loop breaks and remaining cancelled reminders are not processed. Consider processing each reminder independently and logging individual failures.

🔧 Suggested fix using Promise.allSettled for resilience
-  try {
-    const workflowRemindersToDelete: Prisma.Prisma__WorkflowReminderClient<WorkflowReminder, never>[] = [];
-
-    for (const reminder of remindersToCancel) {
-      await client.request({
-        url: "/v3/user/scheduled_sends",
-        method: "POST",
-        body: {
-          batch_id: reminder.referenceId,
-          status: "cancel",
-        },
-      });
-
-      const workflowReminderToDelete = prisma.workflowReminder.delete({
-        where: {
-          id: reminder.id,
-        },
-      });
-
-      workflowRemindersToDelete.push(workflowReminderToDelete);
-    }
-    await Promise.all(workflowRemindersToDelete);
-  } catch (error) {
-    console.log(`Error cancelling scheduled Emails: ${error}`);
-  }
+  const cancelResults = await Promise.allSettled(
+    remindersToCancel.map(async (reminder) => {
+      try {
+        await client.request({
+          url: "/v3/user/scheduled_sends",
+          method: "POST",
+          body: {
+            batch_id: reminder.referenceId,
+            status: "cancel",
+          },
+        });
+        await prisma.workflowReminder.delete({
+          where: {
+            id: reminder.id,
+          },
+        });
+      } catch (error) {
+        console.log(`Error cancelling scheduled Email reminder ${reminder.id}: ${error}`);
+      }
+    })
+  );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
const workflowRemindersToDelete: Prisma.Prisma__WorkflowReminderClient<WorkflowReminder, never>[] = [];
for (const reminder of remindersToCancel) {
await client.request({
url: "/v3/user/scheduled_sends",
method: "POST",
body: {
batch_id: reminder.referenceId,
status: "cancel",
},
});
const workflowReminderToDelete = prisma.workflowReminder.delete({
where: {
id: reminder.id,
},
});
workflowRemindersToDelete.push(workflowReminderToDelete);
}
await Promise.all(workflowRemindersToDelete);
} catch (error) {
console.log(`Error cancelling scheduled Emails: ${error}`);
}
const cancelResults = await Promise.allSettled(
remindersToCancel.map(async (reminder) => {
try {
await client.request({
url: "/v3/user/scheduled_sends",
method: "POST",
body: {
batch_id: reminder.referenceId,
status: "cancel",
},
});
await prisma.workflowReminder.delete({
where: {
id: reminder.id,
},
});
} catch (error) {
console.log(`Error cancelling scheduled Email reminder ${reminder.id}: ${error}`);
}
})
);
🤖 Prompt for AI Agents
In @packages/features/ee/workflows/api/scheduleEmailReminders.ts around lines 53
- 77, The loop over remindersToCancel stops on the first API error because each
iteration awaits client.request; change this so each reminder is processed
independently and concurrently using Promise.allSettled: for each reminder in
remindersToCancel create an async task that calls client.request({ url:
"/v3/user/scheduled_sends", method: "POST", body: { batch_id:
reminder.referenceId, status: "cancel" } }), logs any per-reminder API error
(including reminder.id/referenceId) but continues, and if the cancel succeeds
enqueues or runs prisma.workflowReminder.delete({ where: { id: reminder.id } })
and logs any delete error; then await Promise.allSettled on all tasks and
handle/log failures rather than letting one failure abort the whole operation
(refer to remindersToCancel, client.request, prisma.workflowReminder.delete, and
workflowRemindersToDelete).


//find all unscheduled Email reminders
const unscheduledReminders = await prisma.workflowReminder.findMany({
where: {
Expand Down
126 changes: 60 additions & 66 deletions packages/features/ee/workflows/components/WorkflowStepContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -387,81 +387,75 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
}}
/>
</div>
{(isPhoneNumberNeeded || isSenderIdNeeded) && (
{isPhoneNumberNeeded && (
<div className="mt-2 rounded-md bg-gray-50 p-4 pt-0">
{isPhoneNumberNeeded && (
<Label className="pt-4">{t("custom_phone_number")}</Label>
<div className="block sm:flex">
<PhoneInput<FormValues>
control={form.control}
name={`steps.${step.stepNumber - 1}.sendTo`}
placeholder={t("phone_number")}
id={`steps.${step.stepNumber - 1}.sendTo`}
className="min-w-fit sm:rounded-tl-md sm:rounded-bl-md sm:border-r-transparent"
required
onChange={() => {
const isAlreadyVerified = !!verifiedNumbers
?.concat([])
.find((number) => number === form.getValues(`steps.${step.stepNumber - 1}.sendTo`));
setNumberVerified(isAlreadyVerified);
}}
/>
<Button
color="secondary"
disabled={numberVerified || false}
className={classNames(
"-ml-[3px] h-[40px] min-w-fit sm:block sm:rounded-tl-none sm:rounded-bl-none ",
numberVerified ? "hidden" : "mt-3 sm:mt-0"
)}
onClick={() =>
sendVerificationCodeMutation.mutate({
phoneNumber: form.getValues(`steps.${step.stepNumber - 1}.sendTo`) || "",
})
}>
{t("send_code")}
</Button>
</div>

{form.formState.errors.steps &&
form.formState?.errors?.steps[step.stepNumber - 1]?.sendTo && (
<p className="mt-1 text-xs text-red-500">
{form.formState?.errors?.steps[step.stepNumber - 1]?.sendTo?.message || ""}
</p>
)}
{numberVerified ? (
<div className="mt-1">
<Badge variant="green">{t("number_verified")}</Badge>
</div>
) : (
<>
<Label className="pt-4">{t("custom_phone_number")}</Label>
<div className="block sm:flex">
<PhoneInput<FormValues>
control={form.control}
name={`steps.${step.stepNumber - 1}.sendTo`}
placeholder={t("phone_number")}
id={`steps.${step.stepNumber - 1}.sendTo`}
className="min-w-fit sm:rounded-tl-md sm:rounded-bl-md sm:border-r-transparent"
required
onChange={() => {
const isAlreadyVerified = !!verifiedNumbers
?.concat([])
.find(
(number) => number === form.getValues(`steps.${step.stepNumber - 1}.sendTo`)
);
setNumberVerified(isAlreadyVerified);
<div className="mt-3 flex">
<TextField
className=" border-r-transparent"
placeholder="Verification code"
value={verificationCode}
onChange={(e) => {
setVerificationCode(e.target.value);
}}
required
/>
<Button
color="secondary"
disabled={numberVerified || false}
className={classNames(
"-ml-[3px] h-[40px] min-w-fit sm:block sm:rounded-tl-none sm:rounded-bl-none ",
numberVerified ? "hidden" : "mt-3 sm:mt-0"
)}
onClick={() =>
sendVerificationCodeMutation.mutate({
className="-ml-[3px] rounded-tl-none rounded-bl-none "
disabled={verifyPhoneNumberMutation.isLoading}
onClick={() => {
verifyPhoneNumberMutation.mutate({
phoneNumber: form.getValues(`steps.${step.stepNumber - 1}.sendTo`) || "",
})
}>
{t("send_code")}
code: verificationCode,
});
}}>
Verify
</Button>
</div>

{form.formState.errors.steps &&
form.formState?.errors?.steps[step.stepNumber - 1]?.sendTo && (
<p className="mt-1 text-xs text-red-500">
{form.formState?.errors?.steps[step.stepNumber - 1]?.sendTo?.message || ""}
</p>
)}
{numberVerified ? (
<div className="mt-1">
<Badge variant="green">{t("number_verified")}</Badge>
</div>
) : (
<>
<div className="mt-3 flex">
<TextField
className=" border-r-transparent"
placeholder="Verification code"
value={verificationCode}
onChange={(e) => {
setVerificationCode(e.target.value);
}}
required
/>
<Button
color="secondary"
className="-ml-[3px] rounded-tl-none rounded-bl-none "
disabled={verifyPhoneNumberMutation.isLoading}
onClick={() => {
verifyPhoneNumberMutation.mutate({
phoneNumber: form.getValues(`steps.${step.stepNumber - 1}.sendTo`) || "",
code: verificationCode,
});
}}>
Verify
</Button>
</div>
</>
)}
</>
)}
Comment on lines 435 to 460

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix internationalization and improve UX for verification input.

The verification code input flow works correctly, but has the following issues:

  1. Lines 439 & 456: Hardcoded English strings "Verification code" and "Verify" break internationalization. Users in other locales will see these in English.
  2. Line 449: The Verify button should also be disabled when verificationCode is empty to prevent users from submitting blank codes.
  3. Line 438: Minor - unnecessary leading space in className.
Proposed fixes
  <TextField
-   className=" border-r-transparent"
+   className="border-r-transparent"
-   placeholder="Verification code"
+   placeholder={t("verification_code")}
    value={verificationCode}
    onChange={(e) => {
      setVerificationCode(e.target.value);
    }}
    required
  />
  <Button
    color="secondary"
    className="-ml-[3px] rounded-tl-none rounded-bl-none "
-   disabled={verifyPhoneNumberMutation.isLoading}
+   disabled={verifyPhoneNumberMutation.isLoading || !verificationCode.trim()}
    onClick={() => {
      verifyPhoneNumberMutation.mutate({
        phoneNumber: form.getValues(`steps.${step.stepNumber - 1}.sendTo`) || "",
        code: verificationCode,
      });
    }}>
-   Verify
+   {t("verify")}
  </Button>

Note: Ensure that the translation keys verification_code and verify are added to your localization files.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In @packages/features/ee/workflows/components/WorkflowStepContainer.tsx around
lines 435 - 460, Replace hardcoded UI strings by using the i18n translation keys
for the verification input and button (use t('verification_code') for the
TextField placeholder and t('verify') for the Button label), remove the
unnecessary leading space in the TextField className (" border-r-transparent" →
"border-r-transparent"), and make the Verify Button disabled both when
verifyPhoneNumberMutation.isLoading OR verificationCode is empty (i.e., disable
when !verificationCode || verifyPhoneNumberMutation.isLoading) so users cannot
submit blank codes; keep the existing onClick that mutates
verifyPhoneNumberMutation with form.getValues(...) and verificationCode.

</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,20 +194,41 @@ export const scheduleEmailReminder = async (
}
};

export const deleteScheduledEmailReminder = async (referenceId: string) => {
export const deleteScheduledEmailReminder = async (
reminderId: number,
referenceId: string | null,
immediateDelete?: boolean
) => {
try {
await client.request({
url: "/v3/user/scheduled_sends",
method: "POST",
body: {
batch_id: referenceId,
status: "cancel",
},
});
if (!referenceId) {
await prisma.workflowReminder.delete({
where: {
id: reminderId,
},
});

return;
}

await client.request({
url: `/v3/user/scheduled_sends/${referenceId}`,
method: "DELETE",
if (immediateDelete) {
await client.request({
url: "/v3/user/scheduled_sends",
method: "POST",
body: {
batch_id: referenceId,
status: "cancel",
},
});
return;
}
Comment on lines +213 to +223

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

immediateDelete path cancels in SendGrid but does not delete the reminder from the database.

When immediateDelete is true, the function cancels the scheduled send via the SendGrid API but returns without deleting the WorkflowReminder record. Callers like handleNewBooking.ts (line 968) pass immediateDelete: true, which would leave orphan records in the database.

🔧 Suggested fix to also delete from DB
     if (immediateDelete) {
       await client.request({
         url: "/v3/user/scheduled_sends",
         method: "POST",
         body: {
           batch_id: referenceId,
           status: "cancel",
         },
       });
+      await prisma.workflowReminder.delete({
+        where: {
+          id: reminderId,
+        },
+      });
       return;
     }

Committable suggestion skipped: line range outside the PR's diff.


await prisma.workflowReminder.update({
where: {
id: reminderId,
},
data: {
cancelled: true,
},
});
} catch (error) {
console.log(`Error canceling reminder with error ${error}`);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -174,9 +174,16 @@ export const scheduleSMSReminder = async (
}
};

export const deleteScheduledSMSReminder = async (referenceId: string) => {
export const deleteScheduledSMSReminder = async (reminderId: number, referenceId: string | null) => {
try {
await twilio.cancelSMS(referenceId);
if (referenceId) {
await twilio.cancelSMS(referenceId);
}
await prisma.workflowReminder.delete({
where: {
id: reminderId,
},
});
} catch (error) {
console.log(`Error canceling reminder with error ${error}`);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "WorkflowReminder" ADD COLUMN "cancelled" BOOLEAN;
1 change: 1 addition & 0 deletions packages/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -641,6 +641,7 @@ model WorkflowReminder {
scheduled Boolean
workflowStepId Int
workflowStep WorkflowStep @relation(fields: [workflowStepId], references: [id], onDelete: Cascade)
cancelled Boolean?
}

enum WorkflowTemplates {
Expand Down
Loading