Skip to content

Comprehensive workflow reminder management for booking lifecycle events#6

Open
akshayutture-augment wants to merge 1 commit into
workflow-queue-basefrom
workflow-queue-enhanced
Open

Comprehensive workflow reminder management for booking lifecycle events#6
akshayutture-augment wants to merge 1 commit into
workflow-queue-basefrom
workflow-queue-enhanced

Conversation

@akshayutture-augment

@akshayutture-augment akshayutture-augment commented Nov 14, 2025

Copy link
Copy Markdown

Test 6

Summary by CodeRabbit

  • New Features

    • Enhanced phone number verification flow with dedicated verification code input and verify button for workflow reminders.
  • Bug Fixes

    • Improved handling of scheduled workflow reminders when canceling or rescheduling bookings to ensure proper cancellation across email and SMS reminders.

Replicated from ai-code-review-evaluation/cal.com-coderabbit#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>
@akshayutture-augment

Copy link
Copy Markdown
Author

@coderabbitai review

@akshayutture-augment

Copy link
Copy Markdown
Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Nov 18, 2025

Copy link
Copy Markdown
✅ 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 Nov 18, 2025

Copy link
Copy Markdown

Walkthrough

The changes introduce a cancelled flag to workflow reminders and refactor reminder cancellation across booking and workflow management layers. Reminder deletion shifts from batch Prisma operations to direct per-reminder function calls that accept reminderId and referenceId. Phone number verification UI in the workflow component is restructured with dedicated verification input and button. A new migration adds the cancelled column to the WorkflowReminder table.

Changes

Cohort / File(s) Summary
Database schema and migration
packages/prisma/schema.prisma, packages/prisma/migrations/20230217230604_add_cancelled_to_workflow_reminder/migration.sql
Added optional cancelled boolean field to WorkflowReminder model with corresponding SQL migration to alter the table.
Reminder deletion manager refactor
packages/features/ee/workflows/lib/reminders/emailReminderManager.ts, packages/features/ee/workflows/lib/reminders/smsReminderManager.ts
Updated function signatures to accept (reminderId: number, referenceId: string | null). Replaced direct API DELETE with conditional logic: delete by ID when referenceId is null, or call cancel API when referenceId is present (email reminders support immediate cancel flag).
Booking cancellation and reschedule
packages/features/bookings/lib/handleCancelBooking.ts, packages/features/bookings/lib/handleNewBooking.ts, packages/trpc/server/routers/viewer/bookings.tsx
Refactored reminder deletion from batch Prisma operations to direct per-reminder function calls passing (reminder.id, reminder.referenceId). Added reschedule flow to cancel existing workflow reminders via try-catch block with per-method deletion.
Workflow reminder scheduling and cancellation
packages/features/ee/workflows/api/scheduleEmailReminders.ts, packages/trpc/server/routers/viewer/workflows.tsx
Added new cancellation workflow to process cancelled reminders and send cancel requests to external API. Updated import structure and reminder deletion calls to pass reminderId with referenceId and immediate cancel flag.
Phone number verification UI
packages/features/ee/workflows/components/WorkflowStepContainer.tsx
Restructured phone number input visibility to check only isPhoneNumberNeeded. Introduced dedicated verification UI with separate code input field and Verify button, replacing inline verification logic. Added verifyPhoneNumber mutation call on button click with error handling.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UI as WorkflowStepContainer
    participant API as verifyPhoneNumber API
    
    User->>UI: Enter phone number
    User->>UI: Click "Send Code"
    UI->>API: send_code action
    
    User->>UI: Enter verification code
    User->>UI: Click "Verify"
    UI->>API: verifyPhoneNumber mutation
    
    alt Verification Success
        API-->>UI: Success response
        UI->>UI: Show "number_verified" badge
    else Verification Fails
        API-->>UI: Error response
        UI->>UI: Display error message
    end
Loading
sequenceDiagram
    participant Booking as Booking Handler
    participant ReminderMgr as Reminder Manager
    participant Prisma as Database
    participant ExtAPI as External API
    
    Booking->>Booking: Reschedule booking
    Booking->>Prisma: Fetch original booking + workflowReminders
    
    loop For each workflow reminder
        Booking->>ReminderMgr: deleteScheduledEmailReminder(id, referenceId)
        alt referenceId exists
            ReminderMgr->>ExtAPI: POST /v3/user/scheduled_sends (cancel)
            ExtAPI-->>ReminderMgr: Cancel confirmation
            ReminderMgr->>Prisma: Update reminder.cancelled = true
        else referenceId is null
            ReminderMgr->>Prisma: Delete reminder by id
        end
    end
    
    Booking->>Booking: Continue rescheduling flow
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Signature changes across multiple reminder managers: The updated function signatures for deleteScheduledEmailReminder and deleteScheduledSMSReminder require careful verification that all call sites pass the correct parameters (reminderId, referenceId, optional immediateDelete flag).
  • Conditional cancellation logic: The new branching logic in email and SMS reminder managers (immediate API cancel vs. database update) needs verification for correctness and consistency across all invocation paths.
  • Phone number verification UI flow: The refactored verification component introduces new state management and mutation calls that should be tested for edge cases (network failures, validation errors, user interactions).
  • Migration and schema consistency: Ensure the database schema change is properly aligned with all code paths that read/write the cancelled field.
  • Cross-file reminder deletion calls: Multiple files now call the refactored deletion functions; verify all call sites use the new signatures correctly (handleCancelBooking, handleNewBooking, bookings.tsx, workflows.tsx, scheduleEmailReminders.ts).

Poem

A rabbit hops through cancellation flows,
With reminders marked and gentle blows.
IDs and refs dance side by side,
While phones get verified with pride! 🐰✨
Old batches gone, new logic shows,
The workflow path now gracefully goes.

Pre-merge checks and finishing touches

❌ 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%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ 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 clearly and specifically summarizes the main change: comprehensive improvements to workflow reminder management across the booking lifecycle.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch workflow-queue-enhanced

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (2)
packages/features/ee/workflows/api/scheduleEmailReminders.ts (1)

10-11: Tighten cancelled‑reminder query and error handling

The cancellation flow is generally correct, but a few small tweaks would make it more robust and future‑proof:

  • Explicitly filter by WorkflowMethods.EMAIL so using cancelled for other methods in future won’t accidentally call SendGrid for non‑email reminders.
  • Guard against a missing referenceId before calling the SendGrid cancel API.
  • Consider per‑reminder error handling so one failing cancel doesn’t prevent processing of remaining reminders.

Example adjustment:

-  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(),
+      },
+    },
+  });

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

Also applies to: 43-77

packages/features/ee/workflows/components/WorkflowStepContainer.tsx (1)

390-422: Guard against sending verification codes with empty phone and during in‑flight requests

Right now sendVerificationCodeMutation.mutate is called even if sendTo is empty, and the “Send code” button is only disabled when numberVerified is true, not when the mutation is loading. This can cause avoidable backend calls and accidental double‑sends.

Consider:

  • Early‑returning if !form.getValues(...sendTo) before calling mutate, and/or setting a field error.
  • Also disabling the button while sendVerificationCodeMutation.isLoading to avoid rapid repeated clicks.
📜 Review details

Configuration used: CodeRabbit UI

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 (1 hunks)
  • packages/features/bookings/lib/handleNewBooking.ts (4 hunks)
  • packages/features/ee/workflows/api/scheduleEmailReminders.ts (2 hunks)
  • packages/features/ee/workflows/components/WorkflowStepContainer.tsx (1 hunks)
  • packages/features/ee/workflows/lib/reminders/emailReminderManager.ts (1 hunks)
  • packages/features/ee/workflows/lib/reminders/smsReminderManager.ts (1 hunks)
  • packages/prisma/migrations/20230217230604_add_cancelled_to_workflow_reminder/migration.sql (1 hunks)
  • packages/prisma/schema.prisma (1 hunks)
  • packages/trpc/server/routers/viewer/bookings.tsx (1 hunks)
  • packages/trpc/server/routers/viewer/workflows.tsx (6 hunks)
🔇 Additional comments (7)
packages/prisma/migrations/20230217230604_add_cancelled_to_workflow_reminder/migration.sql (1)

1-2: Migration/schema alignment looks good

Adding a nullable cancelled BOOLEAN column is a safe, backwards‑compatible change and matches the Prisma field type.

packages/prisma/schema.prisma (1)

634-645: WorkflowReminder.cancelled field is consistent with migration

The new optional cancelled flag on WorkflowReminder aligns with the SQL migration and the new reminder cancellation flow.

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

197-235: Email reminder deletion logic matches new cancellation model

The deleteScheduledEmailReminder branching correctly handles:

  • DB‑only delete when there is no referenceId.
  • Immediate remote cancellation when immediateDelete is true.
  • Marking reminders as cancelled for later cron‑driven cancellation when immediateDelete is false.

Given current usage patterns this looks consistent and safe.

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

1-10: New imports are consistent with usage

Bringing in WorkflowMethods and the email/SMS reminder deletion helpers matches their later usage in the reschedule flow.

Also applies to: 39-41


771-773: Including workflowReminders is appropriate for reschedule cleanup

Adding workflowReminders: true to getOriginalRescheduledBooking provides the data needed to cancel previous reminders during reschedule.

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

177-190: SMS reminder deletion helper is correctly implemented

deleteScheduledSMSReminder cleanly handles both cases (with and without referenceId) and ensures the workflowReminder row is removed after any Twilio cancellation.

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

30-31: Reminder deletion helper imports are consistent

Importing deleteScheduledEmailReminder and deleteScheduledSMSReminder from the EE workflows layer fits their use in requestReschedule.

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

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Async reminder cancellations during booking cancel are not awaited

The new reminder‑cancellation loop:

updatedBookings.forEach((booking) => {
  booking.workflowReminders.forEach((reminder) => {
    if (reminder.method === WorkflowMethods.EMAIL) {
      deleteScheduledEmailReminder(reminder.id, reminder.referenceId);
    } else if (reminder.method === WorkflowMethods.SMS) {
      deleteScheduledSMSReminder(reminder.id, reminder.referenceId);
    }
  });
});

calls async helpers without awaiting them, so:

  • Failures become unhandled rejections.
  • Promise.all(prismaPromises.concat(apiDeletes)) does not wait for reminder cancellations.

Refactor to collect and await the reminder promises:

-  //Workflows - cancel all reminders for cancelled bookings
-  updatedBookings.forEach((booking) => {
-    booking.workflowReminders.forEach((reminder) => {
-      if (reminder.method === WorkflowMethods.EMAIL) {
-        deleteScheduledEmailReminder(reminder.id, reminder.referenceId);
-      } else if (reminder.method === WorkflowMethods.SMS) {
-        deleteScheduledSMSReminder(reminder.id, reminder.referenceId);
-      }
-    });
-  });
-
-  const prismaPromises: Promise<unknown>[] = [attendeeDeletes, bookingReferenceDeletes];
-
-  await Promise.all(prismaPromises.concat(apiDeletes));
+  // Workflows - cancel all reminders for cancelled bookings
+  const reminderCancels = updatedBookings.flatMap((booking) =>
+    booking.workflowReminders.map((reminder) => {
+      if (reminder.method === WorkflowMethods.EMAIL) {
+        return deleteScheduledEmailReminder(reminder.id, reminder.referenceId);
+      }
+      if (reminder.method === WorkflowMethods.SMS) {
+        return deleteScheduledSMSReminder(reminder.id, reminder.referenceId);
+      }
+      return Promise.resolve();
+    })
+  );
+
+  const prismaPromises: Promise<unknown>[] = [attendeeDeletes, bookingReferenceDeletes];
+
+  await Promise.all([...prismaPromises, ...apiDeletes, ...reminderCancels]);

Also applies to: 495-497

🤖 Prompt for AI Agents
In packages/features/bookings/lib/handleCancelBooking.ts around lines 484-493
(and similarly at 495-497) the reminder cancellation loop calls async helpers
(deleteScheduledEmailReminder/deleteScheduledSMSReminder) without collecting or
awaiting their returned promises, causing unhandled rejections and making the
overall cancel flow not wait for deletions; fix by creating an array (e.g.,
reminderDeletes), push each call's promise into it instead of calling directly,
then await Promise.all(reminderDeletes) (or include reminderDeletes into the
existing Promise.all by concatenating) so all reminder cancellations are awaited
and errors propagate.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Async reminder deletions on reschedule are not awaited

In the reschedule path, async helpers are called inside forEach without awaiting:

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);
  }
});

Issues:

  • Promises are fire‑and‑forget; failures become unhandled rejections.
  • The handler proceeds without knowing whether cancellations actually completed.

Refactor to collect and await the promises:

-    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);
-    }
+    try {
+      // cancel workflow reminders from previous rescheduled booking
+      await Promise.all(
+        originalRescheduledBooking.workflowReminders.map((reminder) => {
+          if (reminder.method === WorkflowMethods.EMAIL) {
+            return deleteScheduledEmailReminder(reminder.id, reminder.referenceId, true);
+          }
+          if (reminder.method === WorkflowMethods.SMS) {
+            return deleteScheduledSMSReminder(reminder.id, reminder.referenceId);
+          }
+          return Promise.resolve();
+        })
+      );
+    } catch (error) {
+      log.error("Error while canceling scheduled workflow reminders", error);
+    }
🤖 Prompt for AI Agents
In packages/features/bookings/lib/handleNewBooking.ts around lines 963 to 975,
async deletion helpers for workflow reminders are invoked inside a forEach and
not awaited, causing fire-and-forget Promises and unhandled failures; change the
loop to map reminders to Promises (calling deleteScheduledEmailReminder or
deleteScheduledSMSReminder accordingly), collect them into an array, and await
Promise.all (or Promise.allSettled if you want to tolerate individual failures)
inside the try block, and preserve the existing error logging in the catch.

Comment on lines 435 to 457
<>
<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>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Use an error toast for wrong codes and add minimal client‑side validation

In verifyPhoneNumberMutation.onSuccess, a wrong code currently shows t("wrong_code") with a "success" toast variant. That’s confusing feedback. Also, the Verify button allows submitting with an empty verificationCode and without a phone number guard.

Recommend:

  • Switching the toast variant for the !isVerified branch to an error/severity style.
  • Adding simple guards before verifyPhoneNumberMutation.mutate, e.g. bail out (or set a form error) when !form.getValues(...sendTo) or !verificationCode.trim().
🤖 Prompt for AI Agents
In packages/features/ee/workflows/components/WorkflowStepContainer.tsx around
lines 435 to 457, the verification success handler currently shows
t("wrong_code") with a "success" toast and the Verify button allows submitting
with empty phone or code; change the toast variant in the !isVerified branch to
an error/severity style (e.g., "error" or "danger") so wrong codes show an
error, and add minimal client-side guards before calling
verifyPhoneNumberMutation.mutate: check that
form.getValues(`steps.${step.stepNumber - 1}.sendTo`) is non-empty and that
verificationCode.trim() is non-empty, and if missing either bail out and show a
user-facing error toast or set a form error via the form API; keep the mutation
call only when both checks pass.

Comment on lines 488 to 495
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

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Async reminder deletions in requestReschedule are not awaited

In requestReschedule, workflow reminders are cancelled like this:

//cancel workflow reminders of previous booking
bookingToReschedule.workflowReminders.forEach((reminder) => {
  if (reminder.method === WorkflowMethods.EMAIL) {
    deleteScheduledEmailReminder(reminder.id, reminder.referenceId);
  } else if (reminder.method === WorkflowMethods.SMS) {
    deleteScheduledSMSReminder(reminder.id, reminder.referenceId);
  }
});

Because the async helpers are not awaited:

  • Errors become unhandled rejections.
  • The mutation resolves before cancellations complete, so follow‑up behaviour can be inconsistent.

Refactor to await all cancellations:

-        //cancel workflow reminders of previous booking
-        bookingToReschedule.workflowReminders.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 of previous booking
+        await Promise.all(
+          bookingToReschedule.workflowReminders.map((reminder) => {
+            if (reminder.method === WorkflowMethods.EMAIL) {
+              return deleteScheduledEmailReminder(reminder.id, reminder.referenceId);
+            }
+            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 488 to 495, the
code calls deleteScheduledEmailReminder/deleteScheduledSMSReminder inside a
forEach without awaiting them, causing unhandled rejections and the mutation to
resolve before cancellations complete; change the forEach to build an array of
promises (map over workflowReminders creating a promise for each delete call
depending on reminder.method), then await Promise.all(promises) so all
cancellations are awaited; ensure the enclosing function is async (or already
async) and consider catching/logging errors from Promise.all if needed.

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

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Await reminder cancellation helpers instead of firing them in forEach without handling errors

Across several places you’re calling deleteScheduledEmailReminder / deleteScheduledSMSReminder inside Array.forEach and not awaiting the result:

  • Delete workflow: lines 211–218.
  • Disable event types: lines 375–382.
  • Delete steps: lines 519–525.
  • Edit steps: lines 573–579.

Given these helpers are likely async (they previously replaced direct Prisma deletes / batch operations), this pattern means:

  • The mutation can return before cancellations complete.
  • Any rejection from these helpers is effectively dropped and can’t be surfaced or retried.
  • Database changes inside the helpers might race with later Prisma writes in the same mutation.

Safer patterns:

// Example pattern for one of these blocks
const cancellations: Promise<unknown>[] = [];

for (const reminder of scheduledReminders) {
  if (reminder.method === WorkflowMethods.EMAIL) {
    cancellations.push(deleteScheduledEmailReminder(reminder.id, reminder.referenceId, true));
  } else if (reminder.method === WorkflowMethods.SMS) {
    cancellations.push(deleteScheduledSMSReminder(reminder.id, reminder.referenceId));
  }
}

await Promise.all(cancellations);

or, if you prefer sequential behavior:

for (const reminder of scheduledReminders) {
  if (reminder.method === WorkflowMethods.EMAIL) {
    await deleteScheduledEmailReminder(reminder.id, reminder.referenceId, true);
  } else if (reminder.method === WorkflowMethods.SMS) {
    await deleteScheduledSMSReminder(reminder.id, reminder.referenceId);
  }
}

This ensures cancellations finish (or fail visibly) before the mutation completes and avoids silent errors.

Also applies to: 375-382, 519-525, 573-579

🤖 Prompt for AI Agents
In packages/trpc/server/routers/viewer/workflows.tsx around lines 211-218 (also
apply same fix at 375-382, 519-525, 573-579): currently
deleteScheduledEmailReminder / deleteScheduledSMSReminder are invoked inside
Array.forEach without awaiting, which drops rejections and allows the mutation
to return before cancellations complete; replace the forEach with either (a)
collect the returned promises into an array and await Promise.all on them, or
(b) use a for...of loop and await each call sequentially, and ensure any thrown
errors are allowed to propagate (or explicitly rethrow) so failures are visible
to the caller.

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