From 9c3ee262b4b20a1ee4bf7ede2ea0cbff19caa020 Mon Sep 17 00:00:00 2001 From: Kartik Saini <41051387+kart1ka@users.noreply.github.com> Date: Thu, 4 Sep 2025 09:32:31 +0530 Subject: [PATCH] fix: no show webhook triggers not updating (#21345) * fix: no show webhook triggers not updating * fix: add missing booking uid * refactor: fetch bookings from webhook * use allSettled --------- Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com> Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com> --- .../bookings/lib/handleCancelBooking.ts | 6 +- .../bookings/lib/handleConfirmation.ts | 2 + .../features/bookings/lib/handleNewBooking.ts | 39 +++- .../scheduleNoShowTriggers.ts | 5 +- .../features/webhooks/lib/scheduleTrigger.ts | 179 +++++++++++++++--- .../loggedInViewer/connectAndJoin.handler.ts | 1 + .../bookings/requestReschedule.handler.ts | 6 +- .../routers/viewer/webhook/edit.handler.ts | 9 + 8 files changed, 206 insertions(+), 41 deletions(-) diff --git a/packages/features/bookings/lib/handleCancelBooking.ts b/packages/features/bookings/lib/handleCancelBooking.ts index d53aa7f4f1..2c0b11710f 100644 --- a/packages/features/bookings/lib/handleCancelBooking.ts +++ b/packages/features/bookings/lib/handleCancelBooking.ts @@ -9,7 +9,10 @@ import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventR import { sendCancelledReminders } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler"; import type { GetSubscriberOptions } from "@calcom/features/webhooks/lib/getWebhooks"; import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks"; -import { deleteWebhookScheduledTriggers } from "@calcom/features/webhooks/lib/scheduleTrigger"; +import { + deleteWebhookScheduledTriggers, + cancelNoShowTasksForBooking, +} from "@calcom/features/webhooks/lib/scheduleTrigger"; import sendPayload from "@calcom/features/webhooks/lib/sendOrSchedulePayload"; import type { EventTypeInfo } from "@calcom/features/webhooks/lib/sendPayload"; import EventManager from "@calcom/lib/EventManager"; @@ -517,6 +520,7 @@ async function handler(input: CancelBookingInput) { for (const booking of updatedBookings) { // delete scheduled webhook triggers of cancelled bookings webhookTriggerPromises.push(deleteWebhookScheduledTriggers({ booking })); + webhookTriggerPromises.push(cancelNoShowTasksForBooking({ bookingUid: booking.uid })); //Workflows - cancel all reminders for cancelled bookings workflowReminderPromises.push(WorkflowRepository.deleteAllWorkflowReminders(booking.workflowReminders)); diff --git a/packages/features/bookings/lib/handleConfirmation.ts b/packages/features/bookings/lib/handleConfirmation.ts index 4062eddb50..f1b62db10d 100644 --- a/packages/features/bookings/lib/handleConfirmation.ts +++ b/packages/features/bookings/lib/handleConfirmation.ts @@ -42,6 +42,7 @@ export async function handleConfirmation(args: { booking: { startTime: Date; id: number; + uid: string; eventType: { currency: string; description: string | null; @@ -444,6 +445,7 @@ export async function handleConfirmation(args: { startTime: booking.startTime, id: booking.id, location: booking.location, + uid: booking.uid, }, triggerForUser, organizerUser: { id: booking.userId }, diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index 890cd7fa71..cf0997ebe0 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -44,6 +44,7 @@ import type { GetSubscriberOptions } from "@calcom/features/webhooks/lib/getWebh import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks"; import { deleteWebhookScheduledTriggers, + cancelNoShowTasksForBooking, scheduleTrigger, } from "@calcom/features/webhooks/lib/scheduleTrigger"; import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser"; @@ -2261,15 +2262,22 @@ async function handler( const subscribersMeetingEnded = await getWebhooks(subscriberOptionsMeetingEnded); const subscribersMeetingStarted = await getWebhooks(subscriberOptionsMeetingStarted); - let deleteWebhookScheduledTriggerPromise: Promise = Promise.resolve(); + const deleteWebhookScheduledTriggerPromises: Promise[] = []; const scheduleTriggerPromises = []; if (rescheduleUid && originalRescheduledBooking) { //delete all scheduled triggers for meeting ended and meeting started of booking - deleteWebhookScheduledTriggerPromise = deleteWebhookScheduledTriggers({ - booking: originalRescheduledBooking, - isDryRun, - }); + deleteWebhookScheduledTriggerPromises.push( + deleteWebhookScheduledTriggers({ + booking: originalRescheduledBooking, + isDryRun, + }) + ); + deleteWebhookScheduledTriggerPromises.push( + cancelNoShowTasksForBooking({ + bookingUid: originalRescheduledBooking.uid, + }) + ); } if (booking && booking.status === BookingStatus.ACCEPTED) { @@ -2302,12 +2310,20 @@ async function handler( } } - await Promise.all([deleteWebhookScheduledTriggerPromise, ...scheduleTriggerPromises]).catch((error) => { + const scheduledTriggerResults = await Promise.allSettled([ + ...deleteWebhookScheduledTriggerPromises, + ...scheduleTriggerPromises, + ]); + const failures = scheduledTriggerResults.filter((result) => result.status === "rejected"); + + if (failures.length > 0) { loggerWithEventDetails.error( "Error while scheduling or canceling webhook triggers", - JSON.stringify({ error }) + safeStringify({ + errors: failures.map((f) => f.reason), + }) ); - }); + } // Send Webhook call if hooked to BOOKING_CREATED & BOOKING_RESCHEDULED await handleWebhookTrigger({ @@ -2410,7 +2426,12 @@ async function handler( try { if (isConfirmedByDefault) { await scheduleNoShowTriggers({ - booking: { startTime: booking.startTime, id: booking.id, location: booking.location }, + booking: { + startTime: booking.startTime, + id: booking.id, + location: booking.location, + uid: booking.uid, + }, triggerForUser, organizerUser: { id: organizerUser.id }, eventTypeId, diff --git a/packages/features/bookings/lib/handleNewBooking/scheduleNoShowTriggers.ts b/packages/features/bookings/lib/handleNewBooking/scheduleNoShowTriggers.ts index 11dc5d3a3b..c85452202f 100644 --- a/packages/features/bookings/lib/handleNewBooking/scheduleNoShowTriggers.ts +++ b/packages/features/bookings/lib/handleNewBooking/scheduleNoShowTriggers.ts @@ -10,6 +10,7 @@ type ScheduleNoShowTriggersArgs = { startTime: Date; id: number; location: string | null; + uid: string; }; triggerForUser?: number | true | null; organizerUser: { id: number | null }; @@ -62,7 +63,7 @@ const _scheduleNoShowTriggers = async (args: ScheduleNoShowTriggersArgs) => { // Prevents null values from being serialized webhook: { ...webhook, time: webhook.time, timeUnit: webhook.timeUnit }, }, - { scheduledAt } + { scheduledAt, referenceUid: booking.uid } ); } return Promise.resolve(); @@ -93,7 +94,7 @@ const _scheduleNoShowTriggers = async (args: ScheduleNoShowTriggersArgs) => { // Prevents null values from being serialized webhook: { ...webhook, time: webhook.time, timeUnit: webhook.timeUnit }, }, - { scheduledAt } + { scheduledAt, referenceUid: booking.uid } ); } diff --git a/packages/features/webhooks/lib/scheduleTrigger.ts b/packages/features/webhooks/lib/scheduleTrigger.ts index c1b42282ca..2dd4d406c6 100644 --- a/packages/features/webhooks/lib/scheduleTrigger.ts +++ b/packages/features/webhooks/lib/scheduleTrigger.ts @@ -1,9 +1,11 @@ -import type { Prisma, Webhook, Booking } from "@prisma/client"; +import type { Booking, Prisma, Webhook } from "@prisma/client"; import { v4 } from "uuid"; import { selectOOOEntries } from "@calcom/app-store/zapier/api/subscriptions/listOOOEntries"; +import dayjs from "@calcom/dayjs"; import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses"; -import { getHumanReadableLocationValue } from "@calcom/lib/location"; +import tasker from "@calcom/features/tasker"; +import { DailyLocationType, getHumanReadableLocationValue } from "@calcom/lib/location"; import logger from "@calcom/lib/logger"; import { safeStringify } from "@calcom/lib/safeStringify"; import { withReporting } from "@calcom/lib/sentryWrapper"; @@ -17,6 +19,11 @@ const SCHEDULING_TRIGGER: WebhookTriggerEvents[] = [ WebhookTriggerEvents.MEETING_STARTED, ]; +const NO_SHOW_TRIGGERS: WebhookTriggerEvents[] = [ + WebhookTriggerEvents.AFTER_HOSTS_CAL_VIDEO_NO_SHOW, + WebhookTriggerEvents.AFTER_GUESTS_CAL_VIDEO_NO_SHOW, +]; + const log = logger.getSubLogger({ prefix: ["[node-scheduler]"] }); export async function addSubscription({ @@ -367,20 +374,9 @@ export const deleteWebhookScheduledTriggers = withReporting( "deleteWebhookScheduledTriggers" ); -export async function updateTriggerForExistingBookings( - webhook: Webhook, - existingEventTriggers: WebhookTriggerEvents[], - updatedEventTriggers: WebhookTriggerEvents[] -) { - const addedEventTriggers = updatedEventTriggers.filter( - (trigger) => !existingEventTriggers.includes(trigger) && SCHEDULING_TRIGGER.includes(trigger) - ); - const removedEventTriggers = existingEventTriggers.filter( - (trigger) => !updatedEventTriggers.includes(trigger) && SCHEDULING_TRIGGER.includes(trigger) - ); - - if (addedEventTriggers.length === 0 && removedEventTriggers.length === 0) return; - +async function fetchBookingsFromWebhook( + webhook: Pick +): Promise { const currentTime = new Date(); const where: Prisma.BookingWhereInput = { AND: [{ status: BookingStatus.ACCEPTED }], @@ -470,27 +466,71 @@ export async function updateTriggerForExistingBookings( } } + return bookings; +} + +export async function updateTriggerForExistingBookings( + webhook: Webhook, + existingEventTriggers: WebhookTriggerEvents[], + updatedEventTriggers: WebhookTriggerEvents[] +) { + const addedEventTriggers = updatedEventTriggers.filter( + (trigger) => !existingEventTriggers.includes(trigger) && SCHEDULING_TRIGGER.includes(trigger) + ); + const removedEventTriggers = existingEventTriggers.filter( + (trigger) => !updatedEventTriggers.includes(trigger) && SCHEDULING_TRIGGER.includes(trigger) + ); + + const addedNoShowTriggers = updatedEventTriggers.filter( + (trigger) => !existingEventTriggers.includes(trigger) && NO_SHOW_TRIGGERS.includes(trigger) + ); + const removedNoShowTriggers = existingEventTriggers.filter( + (trigger) => !updatedEventTriggers.includes(trigger) && NO_SHOW_TRIGGERS.includes(trigger) + ); + + if ( + addedEventTriggers.length === 0 && + removedEventTriggers.length === 0 && + addedNoShowTriggers.length === 0 && + removedNoShowTriggers.length === 0 + ) + return; + + const bookings = await fetchBookingsFromWebhook(webhook); + if (bookings.length === 0) return; - if (addedEventTriggers.length > 0) { - const promise = bookings.map((booking) => { - return addedEventTriggers.map((triggerEvent) => { - if ( - triggerEvent === WebhookTriggerEvents.AFTER_GUESTS_CAL_VIDEO_NO_SHOW || - triggerEvent === WebhookTriggerEvents.AFTER_HOSTS_CAL_VIDEO_NO_SHOW - ) - return Promise.resolve(); - - scheduleTrigger({ booking, subscriberUrl: webhook.subscriberUrl, subscriber: webhook, triggerEvent }); - }); + if (addedEventTriggers.length > 0 || addedNoShowTriggers.length > 0 || removedNoShowTriggers.length > 0) { + const allPromises = bookings.flatMap((booking) => { + return [ + ...addedEventTriggers.map(async (triggerEvent) => { + if (NO_SHOW_TRIGGERS.includes(triggerEvent)) return; + await scheduleTrigger({ + booking, + subscriberUrl: webhook.subscriberUrl, + subscriber: webhook, + triggerEvent, + }); + }), + ...addedNoShowTriggers.map(async (triggerEvent) => { + await scheduleNoShowTaskForBooking(booking, webhook, triggerEvent); + }), + ...removedNoShowTriggers.map((triggerEvent) => + cancelNoShowTasksForBooking({ + bookingUid: booking.uid, + triggerEvent, + }) + ), + ]; }); - await Promise.all(promise); + await Promise.all(allPromises); } const promise = removedEventTriggers.map((triggerEvent) => deleteWebhookScheduledTriggers({ triggerEvent, webhookId: webhook.id }) ); + await Promise.all(promise); } @@ -546,3 +586,86 @@ export async function listOOOEntries( ); } } + +export async function cancelNoShowTasksForBooking({ + bookingUid, + triggerEvent, + webhook, +}: { + bookingUid?: string; + triggerEvent?: WebhookTriggerEvents; + webhook?: Pick; +}) { + if (bookingUid) { + if (triggerEvent && !NO_SHOW_TRIGGERS.includes(triggerEvent)) return; + + if (triggerEvent === WebhookTriggerEvents.AFTER_HOSTS_CAL_VIDEO_NO_SHOW) { + await tasker.cancelWithReference(bookingUid, "triggerHostNoShowWebhook"); + } else if (triggerEvent === WebhookTriggerEvents.AFTER_GUESTS_CAL_VIDEO_NO_SHOW) { + await tasker.cancelWithReference(bookingUid, "triggerGuestNoShowWebhook"); + } else { + await prisma.task.deleteMany({ + where: { + referenceUid: bookingUid, + }, + }); + } + } else if (webhook) { + const bookings = await fetchBookingsFromWebhook(webhook); + + if (bookings.length === 0) return; + + const promises = bookings.map(async (booking) => { + return await prisma.task.deleteMany({ + where: { + referenceUid: booking.uid, + }, + }); + }); + + await Promise.all(promises); + } +} + +export async function scheduleNoShowTaskForBooking( + booking: { id: number; uid: string; startTime: Date; location: string | null }, + webhook: Webhook, + triggerEvent: WebhookTriggerEvents +) { + if (!webhook.time || !webhook.timeUnit || !booking.startTime || !booking.location) return; + + const isCalVideoLocation = booking.location === DailyLocationType || booking.location?.trim() === ""; + if (!isCalVideoLocation) return; + + if ( + triggerEvent !== WebhookTriggerEvents.AFTER_HOSTS_CAL_VIDEO_NO_SHOW && + triggerEvent !== WebhookTriggerEvents.AFTER_GUESTS_CAL_VIDEO_NO_SHOW + ) + return; + + const scheduledAt = dayjs(booking.startTime) + .add(webhook.time ?? 0, webhook.timeUnit?.toLowerCase() as dayjs.ManipulateType) + .toDate(); + + const taskType = + triggerEvent === WebhookTriggerEvents.AFTER_HOSTS_CAL_VIDEO_NO_SHOW + ? "triggerHostNoShowWebhook" + : "triggerGuestNoShowWebhook"; + + await tasker.create( + taskType, + { + triggerEvent, + bookingId: booking.id, + webhook: { + ...webhook, + time: webhook.time ?? 0, + timeUnit: webhook.timeUnit ?? "HOUR", + }, + }, + { + scheduledAt, + referenceUid: booking.uid, + } + ); +} diff --git a/packages/trpc/server/routers/loggedInViewer/connectAndJoin.handler.ts b/packages/trpc/server/routers/loggedInViewer/connectAndJoin.handler.ts index d51fb697c7..2f12e57a1a 100644 --- a/packages/trpc/server/routers/loggedInViewer/connectAndJoin.handler.ts +++ b/packages/trpc/server/routers/loggedInViewer/connectAndJoin.handler.ts @@ -239,6 +239,7 @@ export const Handler = async ({ ctx, input }: Options) => { startTime: updatedBooking.startTime, id: updatedBooking.id, location: updatedBooking.location, + uid: updatedBooking.uid, }, triggerForUser: !eventType?.teamId || (eventType?.teamId && eventType?.parentId), organizerUser: { id: user.id }, diff --git a/packages/trpc/server/routers/viewer/bookings/requestReschedule.handler.ts b/packages/trpc/server/routers/viewer/bookings/requestReschedule.handler.ts index 9911b5e6f4..3dfbe4f4de 100644 --- a/packages/trpc/server/routers/viewer/bookings/requestReschedule.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/requestReschedule.handler.ts @@ -6,7 +6,10 @@ import dayjs from "@calcom/dayjs"; import { sendRequestRescheduleEmailAndSMS } from "@calcom/emails"; import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses"; import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks"; -import { deleteWebhookScheduledTriggers } from "@calcom/features/webhooks/lib/scheduleTrigger"; +import { + deleteWebhookScheduledTriggers, + cancelNoShowTasksForBooking, +} from "@calcom/features/webhooks/lib/scheduleTrigger"; import sendPayload from "@calcom/features/webhooks/lib/sendOrSchedulePayload"; import { CalendarEventBuilder } from "@calcom/lib/builders/CalendarEvent/builder"; import { CalendarEventDirector } from "@calcom/lib/builders/CalendarEvent/director"; @@ -154,6 +157,7 @@ export const requestRescheduleHandler = async ({ ctx, input }: RequestReschedule // delete scheduled jobs of previous booking const webhookPromises = []; webhookPromises.push(deleteWebhookScheduledTriggers({ booking: bookingToReschedule })); + webhookPromises.push(cancelNoShowTasksForBooking({ bookingUid: bookingToReschedule.uid })); await Promise.all(webhookPromises).catch((error) => { log.error("Error while deleting scheduled webhook triggers", JSON.stringify({ error })); diff --git a/packages/trpc/server/routers/viewer/webhook/edit.handler.ts b/packages/trpc/server/routers/viewer/webhook/edit.handler.ts index 4f34469398..3d9a37253b 100644 --- a/packages/trpc/server/routers/viewer/webhook/edit.handler.ts +++ b/packages/trpc/server/routers/viewer/webhook/edit.handler.ts @@ -1,6 +1,7 @@ import { updateTriggerForExistingBookings, deleteWebhookScheduledTriggers, + cancelNoShowTasksForBooking, } from "@calcom/features/webhooks/lib/scheduleTrigger"; import { prisma } from "@calcom/prisma"; import type { TrpcSessionUser } from "@calcom/trpc/server/types"; @@ -51,6 +52,14 @@ export const editHandler = async ({ input, ctx }: EditOptions) => { const activeTriggersBefore = webhook.active ? webhook.eventTriggers : []; await updateTriggerForExistingBookings(webhook, activeTriggersBefore, updatedWebhook.eventTriggers); } else if (!data.active && webhook.active) { + await cancelNoShowTasksForBooking({ + webhook: { + id: webhook.id, + userId: webhook.userId, + teamId: webhook.teamId, + eventTypeId: webhook.eventTypeId, + }, + }); await deleteWebhookScheduledTriggers({ webhookId: webhook.id }); }