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>
This commit is contained in:
Kartik Saini
2025-09-04 09:32:31 +05:30
committed by GitHub
co-authored by Udit Takkar Anik Dhabal Babu
parent b62eba8128
commit 9c3ee262b4
8 changed files with 206 additions and 41 deletions
@@ -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));
@@ -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 },
@@ -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<unknown> = Promise.resolve();
const deleteWebhookScheduledTriggerPromises: Promise<unknown>[] = [];
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,
@@ -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 }
);
}
+151 -28
View File
@@ -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<Webhook, "id" | "userId" | "teamId" | "eventTypeId">
): Promise<Booking[]> {
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<Webhook, "id" | "userId" | "teamId" | "eventTypeId">;
}) {
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,
}
);
}
@@ -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 },
@@ -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 }));
@@ -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 });
}