Skip to content

Comprehensive workflow reminder management for booking lifecycle events#4

Draft
ShashankFC wants to merge 1 commit into
workflow-queue-basefrom
workflow-queue-enhanced
Draft

Comprehensive workflow reminder management for booking lifecycle events#4
ShashankFC wants to merge 1 commit into
workflow-queue-basefrom
workflow-queue-enhanced

Conversation

@ShashankFC

@ShashankFC ShashankFC commented Dec 31, 2025

Copy link
Copy Markdown
Collaborator

Test 6nn

Summary by CodeRabbit

  • New Features

    • Enhanced phone number verification flow in workflow settings with improved code entry and verification interface.
  • Bug Fixes

    • Improved reminder cancellation and cleanup during booking cancellations and rescheduling operations.
    • Better tracking and management of cancelled reminders in workflows.

✏️ Tip: You can customize this high-level summary in your review settings.

nn---n*Replicated from [ai-code-review-evaluation/cal.com-coderabbit#6](https://github.com/ai-code-review-evaluation/cal.com-coderabbit/pull/6)*

…re 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 <wollencarina@gmail.com>
@ShashankFC

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jan 8, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jan 8, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This PR refactors reminder management to handle cancellation through direct manager functions passing both reminder IDs and reference IDs, replacing batch Prisma operations. A new cancelled field is added to the WorkflowReminder model, and cancellation logic is updated across booking handlers, workflow managers, and email scheduling with conditional cancellation flows.

Changes

Cohort / File(s) Summary
Reminder Manager Functions
packages/features/ee/workflows/lib/reminders/emailReminderManager.ts, packages/features/ee/workflows/lib/reminders/smsReminderManager.ts
Updated function signatures to accept reminderId and nullable referenceId instead of referenceId alone. Email reminder manager now includes immediateDelete flag for conditional API cancellation or database marking. SMS reminder manager performs deletion from database alongside API cancellation.
Booking Cancellation & Rescheduling
packages/features/bookings/lib/handleCancelBooking.ts, packages/features/bookings/lib/handleNewBooking.ts
Replaced batch Prisma promises for reminder deletion with direct calls to reminder manager functions. Added pre-reschedule cleanup that iterates over workflow reminders and cancels them individually with error handling.
Database Schema
packages/prisma/schema.prisma, packages/prisma/migrations/20230217230604_add_cancelled_to_workflow_reminder/migration.sql
Added optional cancelled Boolean field to WorkflowReminder model via migration.
Email Scheduling & Workflow APIs
packages/features/ee/workflows/api/scheduleEmailReminders.ts, packages/trpc/server/routers/viewer/workflows.tsx
Added cancellation workflow to query and cancel scheduled reminders within the next hour via external API. Updated reminder cancellation call sites to pass reminder ID, reference ID, and explicit cancellation flags.
Booking & Workflow TRPC Routes
packages/trpc/server/routers/viewer/bookings.tsx
Replaced batch deletion with per-reminder synchronous calls to cancellation functions. Minor comment update.
Workflow UI Components
packages/features/ee/workflows/components/WorkflowStepContainer.tsx
Narrowed phone-number UI rendering to only when phone number is needed. Introduced separate verification flow with code input, "Verify" button, and verification state tracking.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 Hop along, reminders take a new path,
No more batch promises in Prisma's wrath,
With IDs and references hand in hand,
Cancelled reminders now work as planned! 🌸

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the primary focus of the changeset: implementing comprehensive workflow reminder management across the booking lifecycle, including cancellation and cleanup during booking cancellations and reschedules.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 11

🤖 Fix all issues with AI agents
In @packages/features/bookings/lib/handleCancelBooking.ts:
- Around line 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.

In @packages/features/bookings/lib/handleNewBooking.ts:
- Around line 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.

In @packages/features/ee/workflows/api/scheduleEmailReminders.ts:
- Around line 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.
- Around line 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).

In @packages/features/ee/workflows/components/WorkflowStepContainer.tsx:
- Around line 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.

In @packages/trpc/server/routers/viewer/bookings.tsx:
- Around line 487-494: The reminder deletion calls inside
bookingToReschedule.workflowReminders.forEach are not awaited, so
deleteScheduledEmailReminder and deleteScheduledSMSReminder may not complete;
replace the forEach with an async-aware loop or collect promises (e.g., map to
calls to deleteScheduledEmailReminder/deleteScheduledSMSReminder) and await them
(await Promise.all(promises) or use for...of with await) so all scheduled
reminder deletions complete before proceeding.

In @packages/trpc/server/routers/viewer/workflows.tsx:
- Around line 211-218: The scheduledReminders.forEach block fires async
deletions without awaiting them; change it to await the deletions by iterating
with for...of (e.g., for (const reminder of scheduledReminders)) or map to
Promises and await Promise.all, and call
deleteScheduledEmailReminder(reminder.id, reminder.referenceId, true) and
deleteScheduledSMSReminder(reminder.id, reminder.referenceId) accordingly so
reminders are actually canceled before workflow deletion completes; reference
the scheduledReminders.forEach, deleteScheduledEmailReminder,
deleteScheduledSMSReminder, and WorkflowMethods symbols when applying the fix.
- Around line 517-526: The deletion loop over remindersFromStep is not awaiting
async deletions (using forEach), so ensure you await the calls to
deleteScheduledEmailReminder and deleteScheduledSMSReminder; replace the forEach
with either a Promise.all(remindersFromStep.map(...)) that returns the delete
promise for each reminder or a for...of loop that awaits each call, referencing
the existing remindersFromStep array and WorkflowMethods enum and calling
deleteScheduledEmailReminder(reminder.id, reminder.referenceId, true) or
deleteScheduledSMSReminder(reminder.id, reminder.referenceId) as appropriate so
all scheduled-reminder deletions complete before proceeding.
- Around line 572-580: The callback passed to remindersToUpdate.forEach is async
but not awaited, so scheduled-deletion can run unhandled or out of order;
replace the forEach with an awaited iteration (either await
Promise.all(remindersToUpdate.map(...)) or a for...of loop) and await
deleteScheduledEmailReminder/deleteScheduledSMSReminder calls accordingly,
ensuring the enclosing function is async so the deletions complete before
proceeding; reference remindersToUpdate, WorkflowMethods,
deleteScheduledEmailReminder, and deleteScheduledSMSReminder when making the
change.
- Around line 375-382: The cancellation loop over remindersToDelete uses forEach
and calls deleteScheduledEmailReminder/deleteScheduledSMSReminder without
awaiting them, which can cause race conditions; change it to collect and await
the async calls (e.g., await Promise.all(remindersToDelete.flat().map(reminder
=> reminder.method === WorkflowMethods.EMAIL ?
deleteScheduledEmailReminder(reminder.id, reminder.referenceId) :
deleteScheduledSMSReminder(reminder.id, reminder.referenceId)))) or use an async
for...of that awaits each delete, ensuring reminders are fully cancelled before
proceeding with the workflow update.
🧹 Nitpick comments (2)
packages/features/ee/workflows/components/WorkflowStepContainer.tsx (1)

392-429: Phone input section looks good overall.

The phone number input and "send code" button are correctly implemented with proper state management and error handling.

Minor optimization: Remove unnecessary array copy

On line 403, the .concat([]) creates an unnecessary shallow copy before calling .find(). Since the array isn't being modified, you can call .find() directly:

  onChange={() => {
    const isAlreadyVerified = !!verifiedNumbers
-     ?.concat([])
      .find((number) => number === form.getValues(`steps.${step.stepNumber - 1}.sendTo`));
    setNumberVerified(isAlreadyVerified);
  }}
packages/prisma/schema.prisma (1)

644-644: Consider adding a default value for the cancelled field.

The cancelled field is nullable without a default. For consistency with the scheduled field (line 641) which is a non-nullable Boolean, consider whether cancelled should default to false to simplify queries and make the intent clearer.

♻️ Proposed change
-  cancelled      Boolean?
+  cancelled      Boolean         @default(false)
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 235ec0e and 2ebfcf4.

📒 Files selected for processing (10)
  • packages/features/bookings/lib/handleCancelBooking.ts
  • packages/features/bookings/lib/handleNewBooking.ts
  • packages/features/ee/workflows/api/scheduleEmailReminders.ts
  • packages/features/ee/workflows/components/WorkflowStepContainer.tsx
  • packages/features/ee/workflows/lib/reminders/emailReminderManager.ts
  • packages/features/ee/workflows/lib/reminders/smsReminderManager.ts
  • packages/prisma/migrations/20230217230604_add_cancelled_to_workflow_reminder/migration.sql
  • packages/prisma/schema.prisma
  • packages/trpc/server/routers/viewer/bookings.tsx
  • packages/trpc/server/routers/viewer/workflows.tsx
🧰 Additional context used
🧬 Code graph analysis (7)
packages/features/ee/workflows/lib/reminders/smsReminderManager.ts (2)
packages/features/ee/workflows/lib/reminders/smsProviders/twilioProvider.ts (1)
  • twilio (8-12)
packages/prisma/index.ts (1)
  • prisma (14-14)
packages/features/ee/workflows/lib/reminders/emailReminderManager.ts (1)
packages/prisma/index.ts (1)
  • prisma (14-14)
packages/features/ee/workflows/api/scheduleEmailReminders.ts (1)
packages/prisma/index.ts (1)
  • prisma (14-14)
packages/trpc/server/routers/viewer/bookings.tsx (3)
packages/app-store/zapier/lib/nodeScheduler.ts (1)
  • cancelScheduledJobs (54-94)
packages/features/ee/workflows/lib/reminders/emailReminderManager.ts (1)
  • deleteScheduledEmailReminder (197-236)
packages/features/ee/workflows/lib/reminders/smsReminderManager.ts (1)
  • deleteScheduledSMSReminder (177-190)
packages/features/bookings/lib/handleCancelBooking.ts (2)
packages/features/ee/workflows/lib/reminders/emailReminderManager.ts (1)
  • deleteScheduledEmailReminder (197-236)
packages/features/ee/workflows/lib/reminders/smsReminderManager.ts (1)
  • deleteScheduledSMSReminder (177-190)
packages/features/ee/workflows/components/WorkflowStepContainer.tsx (6)
packages/ui/index.tsx (4)
  • Label (20-20)
  • Button (6-6)
  • Badge (3-3)
  • TextField (24-24)
packages/ui/form/fields.tsx (2)
  • Label (28-34)
  • TextField (100-102)
packages/features/ee/workflows/pages/workflow.tsx (1)
  • FormValues (32-39)
packages/ui/components/button/Button.tsx (1)
  • Button (165-267)
packages/lib/classNames.ts (1)
  • classNames (3-5)
packages/ui/components/badge/Badge.tsx (1)
  • Badge (32-62)
packages/features/bookings/lib/handleNewBooking.ts (2)
packages/features/ee/workflows/lib/reminders/emailReminderManager.ts (1)
  • deleteScheduledEmailReminder (197-236)
packages/features/ee/workflows/lib/reminders/smsReminderManager.ts (1)
  • deleteScheduledSMSReminder (177-190)
🔇 Additional comments (7)
packages/features/ee/workflows/components/WorkflowStepContainer.tsx (2)

390-390: LGTM - Intentional scope narrowing for phone number UI.

The condition has been intentionally narrowed to render the phone number input block only when a phone number is specifically needed, rather than when either a phone number or sender ID is needed. This aligns with the PR's goal of improving the verification flow.


430-434: LGTM - Verification badge.

The verified number badge provides clear visual feedback to users.

packages/prisma/migrations/20230217230604_add_cancelled_to_workflow_reminder/migration.sql (1)

1-2: LGTM!

The migration correctly adds the nullable cancelled column to the WorkflowReminder table, which is safe for existing data.

packages/features/ee/workflows/lib/reminders/smsReminderManager.ts (1)

177-189: LGTM!

The updated signature correctly handles both scheduled reminders (with referenceId) and unscheduled reminders (without referenceId). The conditional check before calling twilio.cancelSMS prevents errors when referenceId is null, and the reminder record is always cleaned up from the database.

packages/features/ee/workflows/lib/reminders/emailReminderManager.ts (1)

197-235: Function signature and behavior look reasonable, with one clarification.

The three-branch logic provides flexibility:

  1. No referenceId → direct DB delete (not yet scheduled in SendGrid)
  2. immediateDelete → cancel immediately in SendGrid (for rescheduling)
  3. Default → mark as cancelled for later cron processing

The design is sound pending the fix for the immediateDelete path noted above.

packages/features/bookings/lib/handleNewBooking.ts (1)

772-772: LGTM!

Adding workflowReminders: true to the booking include is necessary for the new cancellation logic.

packages/trpc/server/routers/viewer/workflows.tsx (1)

1-10: LGTM!

The import reorganization for PrismaPromise and Prisma is correct and aligns with the new usage patterns.

Comment on lines +484 to 493
//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);
});
});

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.

Comment on lines +964 to +975
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);
}

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.

Comment on lines +44 to +51
const remindersToCancel = await prisma.workflowReminder.findMany({
where: {
cancelled: true,
scheduledDate: {
lte: dayjs().add(1, "hour").toISOString(),
},
},
});

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.

Comment on lines +53 to +77
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}`);
}

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).

Comment on lines 435 to 460
<>
<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>
</>
)}
</>
)}

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.

Comment on lines +487 to 494
//cancel workflow reminders of previous booking
bookingToReschedule.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);
});

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.

Similar to the issue in handleCancelBooking.ts, the calls to deleteScheduledEmailReminder and deleteScheduledSMSReminder (lines 490, 492) are not awaited. This could result in reminders not being properly cancelled if an error occurs or the process completes before the deletions finish.

🔧 Proposed fix
        //cancel workflow reminders of previous booking
-        bookingToReschedule.workflowReminders.forEach((reminder) => {
+        await Promise.all(bookingToReschedule.workflowReminders.map((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();
+        }));
📝 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
//cancel workflow reminders of previous booking
bookingToReschedule.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);
});
//cancel workflow reminders of previous booking
await Promise.all(bookingToReschedule.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();
}));
🤖 Prompt for AI Agents
In @packages/trpc/server/routers/viewer/bookings.tsx around lines 487 - 494, The
reminder deletion calls inside bookingToReschedule.workflowReminders.forEach are
not awaited, so deleteScheduledEmailReminder and deleteScheduledSMSReminder may
not complete; replace the forEach with an async-aware loop or collect promises
(e.g., map to calls to deleteScheduledEmailReminder/deleteScheduledSMSReminder)
and await them (await Promise.all(promises) or use for...of with await) so all
scheduled reminder deletions complete before proceeding.

Comment on lines +211 to 218
//cancel workflow reminders of deleted workflow
scheduledReminders.forEach((reminder) => {
if (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, true);
} else if (reminder.method === WorkflowMethods.SMS) {
deleteScheduledSMSReminder(reminder.id, reminder.referenceId);
}
});

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.

Same issue as in handleNewBooking.ts: the async deleteScheduledEmailReminder and deleteScheduledSMSReminder calls are not awaited within forEach, making them fire-and-forget. The workflow deletion may complete before reminders are cancelled.

🔧 Suggested fix
-        //cancel workflow reminders of deleted workflow
-        scheduledReminders.forEach((reminder) => {
-          if (reminder.method === WorkflowMethods.EMAIL) {
-            deleteScheduledEmailReminder(reminder.id, reminder.referenceId, true);
-          } else if (reminder.method === WorkflowMethods.SMS) {
-            deleteScheduledSMSReminder(reminder.id, reminder.referenceId);
-          }
-        });
+        //cancel workflow reminders of deleted workflow
+        await Promise.all(
+          scheduledReminders.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);
+            }
+          })
+        );
📝 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
//cancel workflow reminders of deleted workflow
scheduledReminders.forEach((reminder) => {
if (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, true);
} else if (reminder.method === WorkflowMethods.SMS) {
deleteScheduledSMSReminder(reminder.id, reminder.referenceId);
}
});
//cancel workflow reminders of deleted workflow
await Promise.all(
scheduledReminders.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);
}
})
);
🤖 Prompt for AI Agents
In @packages/trpc/server/routers/viewer/workflows.tsx around lines 211 - 218,
The scheduledReminders.forEach block fires async deletions without awaiting
them; change it to await the deletions by iterating with for...of (e.g., for
(const reminder of scheduledReminders)) or map to Promises and await
Promise.all, and call deleteScheduledEmailReminder(reminder.id,
reminder.referenceId, true) and deleteScheduledSMSReminder(reminder.id,
reminder.referenceId) accordingly so reminders are actually canceled before
workflow deletion completes; reference the scheduledReminders.forEach,
deleteScheduledEmailReminder, deleteScheduledSMSReminder, and WorkflowMethods
symbols when applying the fix.

Comment on lines +375 to 382
//cancel workflow reminders for all bookings from event types that got disabled
remindersToDelete.flat().forEach((reminder) => {
//already scheduled reminders
if (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 deleteReminder = ctx.prisma.workflowReminder.deleteMany({
where: {
id: reminder.id,
booking: {
userId: ctx.user.id,
},
},
});
deleteReminderPromise.push(deleteReminder);
});

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

Same async/await issue in forEach for disabled event types.

The async delete operations are not awaited, which could lead to race conditions where the workflow update proceeds before reminders are properly marked as cancelled.

🔧 Suggested fix
-      //cancel workflow reminders for all bookings from event types that got disabled
-      remindersToDelete.flat().forEach((reminder) => {
-        if (reminder.method === WorkflowMethods.EMAIL) {
-          deleteScheduledEmailReminder(reminder.id, reminder.referenceId);
-        } else if (reminder.method === WorkflowMethods.SMS) {
-          deleteScheduledSMSReminder(reminder.id, reminder.referenceId);
-        }
-      });
+      //cancel workflow reminders for all bookings from event types that got disabled
+      await Promise.all(
+        remindersToDelete.flat().map(async (reminder) => {
+          if (reminder.method === WorkflowMethods.EMAIL) {
+            await deleteScheduledEmailReminder(reminder.id, reminder.referenceId);
+          } else if (reminder.method === WorkflowMethods.SMS) {
+            await deleteScheduledSMSReminder(reminder.id, reminder.referenceId);
+          }
+        })
+      );
📝 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
//cancel workflow reminders for all bookings from event types that got disabled
remindersToDelete.flat().forEach((reminder) => {
//already scheduled reminders
if (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 deleteReminder = ctx.prisma.workflowReminder.deleteMany({
where: {
id: reminder.id,
booking: {
userId: ctx.user.id,
},
},
});
deleteReminderPromise.push(deleteReminder);
});
//cancel workflow reminders for all bookings from event types that got disabled
await Promise.all(
remindersToDelete.flat().map(async (reminder) => {
if (reminder.method === WorkflowMethods.EMAIL) {
await deleteScheduledEmailReminder(reminder.id, reminder.referenceId);
} else if (reminder.method === WorkflowMethods.SMS) {
await deleteScheduledSMSReminder(reminder.id, reminder.referenceId);
}
})
);
🤖 Prompt for AI Agents
In @packages/trpc/server/routers/viewer/workflows.tsx around lines 375 - 382,
The cancellation loop over remindersToDelete uses forEach and calls
deleteScheduledEmailReminder/deleteScheduledSMSReminder without awaiting them,
which can cause race conditions; change it to collect and await the async calls
(e.g., await Promise.all(remindersToDelete.flat().map(reminder =>
reminder.method === WorkflowMethods.EMAIL ?
deleteScheduledEmailReminder(reminder.id, reminder.referenceId) :
deleteScheduledSMSReminder(reminder.id, reminder.referenceId)))) or use an async
for...of that awaits each delete, ensuring reminders are fully cancelled before
proceeding with the workflow update.

Comment on lines +517 to 526
// cancel all workflow reminders from deleted steps
if (remindersFromStep.length > 0) {
remindersFromStep.forEach((reminder) => {
if (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, true);
} else if (reminder.method === WorkflowMethods.SMS) {
deleteScheduledSMSReminder(reminder.id, reminder.referenceId);
}
});
}

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

Same async/await issue for deleted steps.

🔧 Suggested fix
-          // cancel all workflow reminders from deleted steps
-          if (remindersFromStep.length > 0) {
-            remindersFromStep.forEach((reminder) => {
-              if (reminder.method === WorkflowMethods.EMAIL) {
-                deleteScheduledEmailReminder(reminder.id, reminder.referenceId, true);
-              } else if (reminder.method === WorkflowMethods.SMS) {
-                deleteScheduledSMSReminder(reminder.id, reminder.referenceId);
-              }
-            });
-          }
+          // cancel all workflow reminders from deleted steps
+          if (remindersFromStep.length > 0) {
+            await Promise.all(
+              remindersFromStep.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);
+                }
+              })
+            );
+          }
📝 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
// cancel all workflow reminders from deleted steps
if (remindersFromStep.length > 0) {
remindersFromStep.forEach((reminder) => {
if (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, true);
} else if (reminder.method === WorkflowMethods.SMS) {
deleteScheduledSMSReminder(reminder.id, reminder.referenceId);
}
});
}
// cancel all workflow reminders from deleted steps
if (remindersFromStep.length > 0) {
await Promise.all(
remindersFromStep.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);
}
})
);
}
🤖 Prompt for AI Agents
In @packages/trpc/server/routers/viewer/workflows.tsx around lines 517 - 526,
The deletion loop over remindersFromStep is not awaiting async deletions (using
forEach), so ensure you await the calls to deleteScheduledEmailReminder and
deleteScheduledSMSReminder; replace the forEach with either a
Promise.all(remindersFromStep.map(...)) that returns the delete promise for each
reminder or a for...of loop that awaits each call, referencing the existing
remindersFromStep array and WorkflowMethods enum and calling
deleteScheduledEmailReminder(reminder.id, reminder.referenceId, true) or
deleteScheduledSMSReminder(reminder.id, reminder.referenceId) as appropriate so
all scheduled-reminder deletions complete before proceeding.

Comment on lines +572 to 580

//cancel all workflow reminders from steps that were edited
remindersToUpdate.forEach(async (reminder) => {
if (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);
}
await ctx.prisma.workflowReminder.deleteMany({
where: {
id: reminder.id,
},
});
});

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

Same async/await issue for edited steps.

🔧 Suggested fix
-          //cancel all workflow reminders from steps that were edited
-          remindersToUpdate.forEach(async (reminder) => {
-            if (reminder.method === WorkflowMethods.EMAIL) {
-              deleteScheduledEmailReminder(reminder.id, reminder.referenceId);
-            } else if (reminder.method === WorkflowMethods.SMS) {
-              deleteScheduledSMSReminder(reminder.id, reminder.referenceId);
-            }
-          });
+          //cancel all workflow reminders from steps that were edited
+          await Promise.all(
+            remindersToUpdate.map(async (reminder) => {
+              if (reminder.method === WorkflowMethods.EMAIL) {
+                await deleteScheduledEmailReminder(reminder.id, reminder.referenceId);
+              } else if (reminder.method === WorkflowMethods.SMS) {
+                await deleteScheduledSMSReminder(reminder.id, reminder.referenceId);
+              }
+            })
+          );
📝 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
//cancel all workflow reminders from steps that were edited
remindersToUpdate.forEach(async (reminder) => {
if (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);
}
await ctx.prisma.workflowReminder.deleteMany({
where: {
id: reminder.id,
},
});
});
//cancel all workflow reminders from steps that were edited
await Promise.all(
remindersToUpdate.map(async (reminder) => {
if (reminder.method === WorkflowMethods.EMAIL) {
await deleteScheduledEmailReminder(reminder.id, reminder.referenceId);
} else if (reminder.method === WorkflowMethods.SMS) {
await deleteScheduledSMSReminder(reminder.id, reminder.referenceId);
}
})
);
🤖 Prompt for AI Agents
In @packages/trpc/server/routers/viewer/workflows.tsx around lines 572 - 580,
The callback passed to remindersToUpdate.forEach is async but not awaited, so
scheduled-deletion can run unhandled or out of order; replace the forEach with
an awaited iteration (either await Promise.all(remindersToUpdate.map(...)) or a
for...of loop) and await deleteScheduledEmailReminder/deleteScheduledSMSReminder
calls accordingly, ensuring the enclosing function is async so the deletions
complete before proceeding; reference remindersToUpdate, WorkflowMethods,
deleteScheduledEmailReminder, and deleteScheduledSMSReminder when making the
change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants