From f319b662c549676086cbb9cec971f31c9d815344 Mon Sep 17 00:00:00 2001 From: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com> Date: Mon, 19 Jan 2026 20:24:31 +0530 Subject: [PATCH] fix: skip reminders when scheduled date is in the past (#26942) * flaky-e2e * workflow reminder * remove --- .../lib/reminders/emailReminderManager.ts | 15 +++- .../lib/reminders/smsReminderManager.ts | 11 +++ .../lib/reminders/whatsappReminderManager.ts | 11 +++ .../lib/service/WorkflowService.test.ts | 90 +++++++++++++++++-- .../workflows/lib/service/WorkflowService.ts | 10 +++ 5 files changed, 130 insertions(+), 7 deletions(-) diff --git a/packages/features/ee/workflows/lib/reminders/emailReminderManager.ts b/packages/features/ee/workflows/lib/reminders/emailReminderManager.ts index e4253cc0af..e4ac6b78ba 100644 --- a/packages/features/ee/workflows/lib/reminders/emailReminderManager.ts +++ b/packages/features/ee/workflows/lib/reminders/emailReminderManager.ts @@ -1,3 +1,4 @@ +import dayjs from "@calcom/dayjs"; import { BookingSeatRepository } from "@calcom/features/bookings/repositories/BookingSeatRepository"; import { EmailWorkflowService } from "@calcom/features/ee/workflows/lib/service/EmailWorkflowService"; import { WorkflowService } from "@calcom/features/ee/workflows/lib/service/WorkflowService"; @@ -7,7 +8,7 @@ import logger from "@calcom/lib/logger"; import { getTimeFormatStringFromUserTimeFormat } from "@calcom/lib/timeFormat"; import prisma from "@calcom/prisma"; import type { TimeUnit } from "@calcom/prisma/enums"; -import { WorkflowMethods, type WorkflowTemplates, type WorkflowTriggerEvents } from "@calcom/prisma/enums"; +import { WorkflowMethods, WorkflowTriggerEvents, type WorkflowTemplates } from "@calcom/prisma/enums"; import type { BookingInfo, FormSubmissionData, ScheduleEmailReminderAction } from "../types"; import { sendOrScheduleWorkflowEmails } from "./providers/emailProvider"; @@ -127,6 +128,18 @@ const scheduleEmailReminderForEvt = async (args: scheduleEmailReminderArgs & { e timeUnit: timeSpan.timeUnit, evt, }); + + if ( + scheduledDate && + triggerEvent === WorkflowTriggerEvents.BEFORE_EVENT && + dayjs(scheduledDate).isBefore(dayjs()) + ) { + log.debug( + `Skipping reminder for workflow step ${workflowStepId} - scheduled date ${scheduledDate} is in the past` + ); + return; + } + const workflowReminderRepository = new WorkflowReminderRepository(prisma); const bookingSeatRepository = new BookingSeatRepository(prisma); const emailWorkflowService = new EmailWorkflowService(workflowReminderRepository, bookingSeatRepository); diff --git a/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts b/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts index 0410f7da2f..42b2accd25 100644 --- a/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts +++ b/packages/features/ee/workflows/lib/reminders/smsReminderManager.ts @@ -147,6 +147,17 @@ const scheduleSMSReminderForEvt = async ( scheduledDate = timeSpan.time && timeUnit ? dayjs(endTime).add(timeSpan.time, timeUnit) : null; } + if ( + scheduledDate && + triggerEvent === WorkflowTriggerEvents.BEFORE_EVENT && + dayjs(scheduledDate).isBefore(dayjs()) + ) { + log.debug( + `Skipping reminder for workflow step ${workflowStepId} - scheduled date ${scheduledDate} is in the past` + ); + return; + } + const useTwilio = shouldUseTwilio(triggerEvent, scheduledDate); if (useTwilio) { const attendeeToBeUsedInSMS = getAttendeeToBeUsedInSMS(action, evt, reminderPhone); diff --git a/packages/features/ee/workflows/lib/reminders/whatsappReminderManager.ts b/packages/features/ee/workflows/lib/reminders/whatsappReminderManager.ts index 231d8bd578..80c1ac7eee 100644 --- a/packages/features/ee/workflows/lib/reminders/whatsappReminderManager.ts +++ b/packages/features/ee/workflows/lib/reminders/whatsappReminderManager.ts @@ -77,6 +77,17 @@ export const scheduleWhatsappReminder = async (args: ScheduleTextReminderArgs & scheduledDate = timeSpan.time && timeUnit ? dayjs(endTime).add(timeSpan.time, timeUnit) : null; } + if ( + scheduledDate && + triggerEvent === WorkflowTriggerEvents.BEFORE_EVENT && + dayjs(scheduledDate).isBefore(currentDate) + ) { + log.debug( + `Skipping reminder for workflow step ${workflowStepId} - scheduled date ${scheduledDate} is in the past` + ); + return; + } + const name = action === WorkflowActions.WHATSAPP_ATTENDEE ? evt.attendees[0].name : evt.organizer.name; const attendeeName = action === WorkflowActions.WHATSAPP_ATTENDEE ? evt.organizer.name : evt.attendees[0].name; diff --git a/packages/features/ee/workflows/lib/service/WorkflowService.test.ts b/packages/features/ee/workflows/lib/service/WorkflowService.test.ts index ec4ecda51f..89574df00b 100644 --- a/packages/features/ee/workflows/lib/service/WorkflowService.test.ts +++ b/packages/features/ee/workflows/lib/service/WorkflowService.test.ts @@ -1,3 +1,4 @@ +import dayjs from "@calcom/dayjs"; import { describe, expect, vi, beforeEach } from "vitest"; import { scheduleWorkflowReminders } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler"; @@ -249,10 +250,14 @@ describe("WorkflowService.scheduleLazyEmailWorkflow", () => { timeUnit: TimeUnit.HOUR, }; + // Use future dates to ensure scheduled date is not in the past + const futureStartTime = dayjs().add(3, "days").toISOString(); + const futureEndTime = dayjs().add(3, "days").add(1, "hour").toISOString(); + const mockEvt = { uid: "booking-123", - startTime: "2024-12-01T10:00:00Z", - endTime: "2024-12-01T11:00:00Z", + startTime: futureStartTime, + endTime: futureEndTime, }; const mockWorkflowReminder = { @@ -297,10 +302,14 @@ describe("WorkflowService.scheduleLazyEmailWorkflow", () => { timeUnit: TimeUnit.HOUR, }; + // Use future dates to ensure scheduled date is not in the past + const futureStartTime = dayjs().add(1, "day").toISOString(); + const futureEndTime = dayjs().add(1, "day").add(1, "hour").toISOString(); + const mockEvt = { uid: "booking-456", - startTime: "2024-12-01T10:00:00Z", - endTime: "2024-12-01T11:00:00Z", + startTime: futureStartTime, + endTime: futureEndTime, }; const mockWorkflowReminder = { @@ -337,10 +346,14 @@ describe("WorkflowService.scheduleLazyEmailWorkflow", () => { timeUnit: TimeUnit.HOUR, }; + // Use future dates to ensure scheduled date is not in the past + const futureStartTime = dayjs().add(3, "days").toISOString(); + const futureEndTime = dayjs().add(3, "days").add(1, "hour").toISOString(); + const mockEvt = { uid: "booking-789", - startTime: "2024-12-01T10:00:00Z", - endTime: "2024-12-01T11:00:00Z", + startTime: futureStartTime, + endTime: futureEndTime, }; const mockWorkflowReminder = { @@ -407,6 +420,71 @@ describe("WorkflowService.scheduleLazyEmailWorkflow", () => { expect(mockWorkflowReminderCreate).not.toHaveBeenCalled(); expect(mockTasker.create).not.toHaveBeenCalled(); }); + + test("should skip reminder if scheduled date is in the past for BEFORE_EVENT", async () => { + const mockWorkflow = { + time: 48, + timeUnit: TimeUnit.HOUR, + }; + + // Event is only 24 hours away, but reminder is set for 48 hours before + // This means the scheduled date would be in the past + const tomorrowStartTime = dayjs().add(1, "day").toISOString(); + const tomorrowEndTime = dayjs().add(1, "day").add(1, "hour").toISOString(); + + const mockEvt = { + uid: "booking-past-reminder", + startTime: tomorrowStartTime, + endTime: tomorrowEndTime, + }; + + await WorkflowService.scheduleLazyEmailWorkflow({ + workflowTriggerEvent: "BEFORE_EVENT", + workflowStepId: 1, + workflow: mockWorkflow, + evt: mockEvt, + }); + + // Should not create reminder or schedule task when scheduled date is in the past + expect(mockWorkflowReminderCreate).not.toHaveBeenCalled(); + expect(mockTasker.create).not.toHaveBeenCalled(); + }); + + test("should not skip reminder for AFTER_EVENT even if calculated date seems past", async () => { + const mockWorkflow = { + time: 1, + timeUnit: TimeUnit.HOUR, + }; + + // For AFTER_EVENT, we add time to endTime, so it should always be in the future + const pastStartTime = dayjs().subtract(1, "hour").toISOString(); + const pastEndTime = dayjs().subtract(30, "minutes").toISOString(); + + const mockEvt = { + uid: "booking-after-event", + startTime: pastStartTime, + endTime: pastEndTime, + }; + + const mockWorkflowReminder = { + id: 4, + uuid: "reminder-uuid-after", + }; + + mockWorkflowReminderCreate.mockResolvedValue(mockWorkflowReminder); + mockTasker.create.mockResolvedValue({ id: "task-after" }); + + await WorkflowService.scheduleLazyEmailWorkflow({ + workflowTriggerEvent: "AFTER_EVENT", + workflowStepId: 4, + workflow: mockWorkflow, + evt: mockEvt, + }); + + // AFTER_EVENT reminders should still be scheduled (they're relative to endTime + time) + expect(mockWorkflowReminderCreate).toHaveBeenCalled(); + expect(mockTasker.create).toHaveBeenCalled(); + }); }); describe("WorkflowService.processWorkflowScheduledDate", () => { diff --git a/packages/features/ee/workflows/lib/service/WorkflowService.ts b/packages/features/ee/workflows/lib/service/WorkflowService.ts index 7163ddbb14..a3b7608df1 100644 --- a/packages/features/ee/workflows/lib/service/WorkflowService.ts +++ b/packages/features/ee/workflows/lib/service/WorkflowService.ts @@ -281,6 +281,16 @@ export class WorkflowService { return; } + if ( + workflowTriggerEvent === WorkflowTriggerEvents.BEFORE_EVENT && + dayjs(scheduledDate).isBefore(dayjs()) + ) { + log.debug( + `Skipping lazy email reminder for workflow step ${workflowStepId} - scheduled date ${scheduledDate} is in the past` + ); + return; + } + let workflowReminder: { id: number | null; uuid: string | null }; try { const workflowReminderRepository = new WorkflowReminderRepository(prisma);