diff --git a/packages/features/bookings/lib/handleCancelBooking.ts b/packages/features/bookings/lib/handleCancelBooking.ts index 8686379f92..4a3ac35ab9 100644 --- a/packages/features/bookings/lib/handleCancelBooking.ts +++ b/packages/features/bookings/lib/handleCancelBooking.ts @@ -1,4 +1,4 @@ -import type { Prisma, WebhookTriggerEvents, WorkflowReminder } from "@prisma/client"; +import type { Prisma, WorkflowReminder } from "@prisma/client"; import type { NextApiRequest } from "next"; import appStore from "@calcom/app-store"; @@ -23,7 +23,7 @@ import logger from "@calcom/lib/logger"; import { handleRefundError } from "@calcom/lib/payment/handleRefundError"; import { getTranslation } from "@calcom/lib/server/i18n"; import prisma, { bookingMinimalSelect } from "@calcom/prisma"; -import { BookingStatus, MembershipRole, WorkflowMethods } from "@calcom/prisma/enums"; +import { BookingStatus, MembershipRole, WorkflowMethods, WebhookTriggerEvents } from "@calcom/prisma/enums"; import { schemaBookingCancelParams } from "@calcom/prisma/zod-utils"; import type { CalendarEvent } from "@calcom/types/Calendar"; import type { IAbstractPaymentService, PaymentApp } from "@calcom/types/PaymentService"; @@ -126,9 +126,25 @@ async function handler(req: CustomRequest) { throw new HttpError({ statusCode: 400, message: "User not found" }); } - // If it's just an attendee of a booking then just remove them from that booking - const result = await handleSeatedEventCancellation(req); - if (result) return { success: true }; + // get webhooks + const eventTrigger: WebhookTriggerEvents = "BOOKING_CANCELLED"; + + const subscriberOptions = { + userId: bookingToDelete.userId, + eventTypeId: bookingToDelete.eventTypeId as number, + triggerEvent: eventTrigger, + teamId: bookingToDelete.eventType?.teamId, + }; + const eventTypeInfo: EventTypeInfo = { + eventTitle: bookingToDelete?.eventType?.title || null, + eventDescription: bookingToDelete?.eventType?.description || null, + requiresConfirmation: bookingToDelete?.eventType?.requiresConfirmation || null, + price: bookingToDelete?.eventType?.price || null, + currency: bookingToDelete?.eventType?.currency || null, + length: bookingToDelete?.eventType?.length || null, + }; + + const webhooks = await getWebhooks(subscriberOptions); const organizer = await prisma.user.findFirstOrThrow({ where: { @@ -207,6 +223,12 @@ async function handler(req: CustomRequest) { seatsShowAttendees: bookingToDelete.eventType?.seatsShowAttendees, }; + const dataForWebhooks = { evt, webhooks, eventTypeInfo }; + + // If it's just an attendee of a booking then just remove them from that booking + const result = await handleSeatedEventCancellation(req, dataForWebhooks); + if (result) return { success: true }; + // If it's just an attendee of a booking then just remove them from that booking if (seatReferenceUid && bookingToDelete.attendees.length > 1) { const seatReference = bookingToDelete.seatsReferences.find( @@ -308,26 +330,6 @@ async function handler(req: CustomRequest) { return { message: "No longer attending event" }; } - // Hook up the webhook logic here - const eventTrigger: WebhookTriggerEvents = "BOOKING_CANCELLED"; - // Send Webhook call if hooked to BOOKING.CANCELLED - const subscriberOptions = { - userId: bookingToDelete.userId, - eventTypeId: bookingToDelete.eventTypeId as number, - triggerEvent: eventTrigger, - teamId: bookingToDelete.eventType?.teamId, - }; - - const eventTypeInfo: EventTypeInfo = { - eventTitle: bookingToDelete?.eventType?.title || null, - eventDescription: bookingToDelete?.eventType?.description || null, - requiresConfirmation: bookingToDelete?.eventType?.requiresConfirmation || null, - price: bookingToDelete?.eventType?.price || null, - currency: bookingToDelete?.eventType?.currency || null, - length: bookingToDelete?.eventType?.length || null, - }; - - const webhooks = await getWebhooks(subscriberOptions); const promises = webhooks.map((webhook) => sendPayload(webhook.secret, eventTrigger, new Date().toISOString(), webhook, { ...evt, @@ -677,12 +679,31 @@ async function handler(req: CustomRequest) { return { message: "Booking successfully cancelled." }; } -async function handleSeatedEventCancellation(req: CustomRequest) { +async function handleSeatedEventCancellation( + req: CustomRequest, + dataForWebhooks: { + webhooks: { + id: string; + subscriberUrl: string; + payloadTemplate: string | null; + appId: string | null; + secret: string | null; + }[]; + evt: CalendarEvent; + eventTypeInfo: EventTypeInfo; + } +) { const { seatReferenceUid } = schemaBookingCancelParams.parse(req.body); + const { webhooks, evt, eventTypeInfo } = dataForWebhooks; if (!seatReferenceUid) return; - if (!req.bookingToDelete?.attendees.length || req.bookingToDelete.attendees.length < 2) return; + const bookingToDelete = req.bookingToDelete; + if (!bookingToDelete?.attendees.length || bookingToDelete.attendees.length < 2) return; - const seatReference = req.bookingToDelete.seatsReferences.find( + if (!bookingToDelete.userId) { + throw new HttpError({ statusCode: 400, message: "User not found" }); + } + + const seatReference = bookingToDelete.seatsReferences.find( (reference) => reference.referenceUid === seatReferenceUid ); @@ -701,6 +722,36 @@ async function handleSeatedEventCancellation(req: CustomRequest) { }), ]); req.statusCode = 200; + + const attendee = bookingToDelete?.attendees.find((attendee) => attendee.id === seatReference.attendeeId); + + evt.attendees = attendee + ? [ + { + ...attendee, + language: { + translate: await getTranslation(attendee.locale ?? "en", "common"), + locale: attendee.locale ?? "en", + }, + }, + ] + : []; + + const promises = webhooks.map((webhook) => + sendPayload(webhook.secret, WebhookTriggerEvents.BOOKING_CANCELLED, new Date().toISOString(), webhook, { + ...evt, + ...eventTypeInfo, + status: "CANCELLED", + smsReminderNumber: bookingToDelete.smsReminderNumber || undefined, + }).catch((e) => { + console.error( + `Error executing webhook for event: ${WebhookTriggerEvents.BOOKING_CANCELLED}, URL: ${webhook.subscriberUrl}`, + e + ); + }) + ); + await Promise.all(promises); + return { success: true }; } diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index f821aab9fd..b07e0f3ad1 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -1135,6 +1135,40 @@ async function handler( return deletedReferences; }; + // data needed for triggering webhooks + const eventTypeInfo: EventTypeInfo = { + eventTitle: eventType.title, + eventDescription: eventType.description, + requiresConfirmation: requiresConfirmation || null, + price: paymentAppData.price, + currency: eventType.currency, + length: eventType.length, + }; + + const teamId = await getTeamId({ eventType }); + + const subscriberOptions: GetSubscriberOptions = { + userId: organizerUser.id, + eventTypeId, + triggerEvent: WebhookTriggerEvents.BOOKING_CREATED, + teamId, + }; + + const eventTrigger: WebhookTriggerEvents = rescheduleUid + ? WebhookTriggerEvents.BOOKING_RESCHEDULED + : WebhookTriggerEvents.BOOKING_CREATED; + + subscriberOptions.triggerEvent = eventTrigger; + + const subscriberOptionsMeetingEnded = { + userId: organizerUser.id, + eventTypeId, + triggerEvent: WebhookTriggerEvents.MEETING_ENDED, + teamId, + }; + + const subscribersMeetingEnded = await getWebhooks(subscriberOptionsMeetingEnded); + const handleSeats = async () => { let resultBooking: | (Partial & { @@ -1167,6 +1201,9 @@ async function handler( startTime: true, user: true, status: true, + smsReminderNumber: true, + endTime: true, + scheduledJobs: true, }, }); @@ -1701,6 +1738,25 @@ async function handler( log.error("Error while scheduling workflow reminders", error); } + const webhookData = { + ...evt, + ...eventTypeInfo, + bookingId: booking?.id, + rescheduleUid, + rescheduleStartTime: originalRescheduledBooking?.startTime + ? dayjs(originalRescheduledBooking?.startTime).utc().format() + : undefined, + rescheduleEndTime: originalRescheduledBooking?.endTime + ? dayjs(originalRescheduledBooking?.endTime).utc().format() + : undefined, + metadata: { ...metadata, ...reqBody.metadata }, + eventTypeId, + status: "ACCEPTED", + smsReminderNumber: booking?.smsReminderNumber || undefined, + }; + + await handleWebhookTrigger({ subscriberOptions, eventTrigger, webhookData }); + return resultBooking; }; // For seats, if the booking already exists then we want to add the new attendee to the existing booking @@ -2231,14 +2287,6 @@ async function handler( } : undefined; - const eventTypeInfo: EventTypeInfo = { - eventTitle: eventType.title, - eventDescription: eventType.description, - requiresConfirmation: requiresConfirmation || null, - price: paymentAppData.price, - currency: eventType.currency, - length: eventType.length, - }; const webhookData = { ...evt, ...eventTypeInfo, @@ -2255,30 +2303,7 @@ async function handler( status: "ACCEPTED", smsReminderNumber: booking?.smsReminderNumber || undefined, }; - - const teamId = await getTeamId({ eventType }); - - const subscriberOptions: GetSubscriberOptions = { - userId: organizerUser.id, - eventTypeId, - triggerEvent: WebhookTriggerEvents.BOOKING_CREATED, - teamId, - }; - if (isConfirmedByDefault) { - const eventTrigger: WebhookTriggerEvents = rescheduleUid - ? WebhookTriggerEvents.BOOKING_RESCHEDULED - : WebhookTriggerEvents.BOOKING_CREATED; - - subscriberOptions.triggerEvent = eventTrigger; - - const subscriberOptionsMeetingEnded = { - userId: organizerUser.id, - eventTypeId, - triggerEvent: WebhookTriggerEvents.MEETING_ENDED, - teamId, - }; - try { const subscribersMeetingEnded = await getWebhooks(subscriberOptionsMeetingEnded);