From 6dce57440e79738b75359d4d1e04b221274ec728 Mon Sep 17 00:00:00 2001 From: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com> Date: Wed, 23 Nov 2022 17:38:13 +0100 Subject: [PATCH] Trigger webhooks when booking is confirmed not submitted (#5609) * design fix copy api key * fix webhooks on confirmation + scheduled workflows for confirmed recurring events * add removed comment * take rescheduled opt ins into account * code clean up Co-authored-by: CarinaWolli --- .../app-store/zapier/pages/setup/index.tsx | 2 +- .../features/bookings/lib/handleNewBooking.ts | 142 +++++++++--------- .../api-keys/components/ApiKeyDialogForm.tsx | 3 +- .../lib/reminders/reminderScheduler.ts | 7 +- packages/prisma/seed-app-store.config.json | 1 - .../trpc/server/routers/viewer/bookings.tsx | 59 ++++++-- 6 files changed, 129 insertions(+), 85 deletions(-) diff --git a/packages/app-store/zapier/pages/setup/index.tsx b/packages/app-store/zapier/pages/setup/index.tsx index 701a40c595..ba46da54fe 100644 --- a/packages/app-store/zapier/pages/setup/index.tsx +++ b/packages/app-store/zapier/pages/setup/index.tsx @@ -64,7 +64,7 @@ export default function ZapierSetup(props: IZapierSetupProps) { <>
{t("your_unique_api_key")}
- + {newApiKey} diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index 6bbbac3388..703bbfedfb 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -635,6 +635,12 @@ async function handler(req: NextApiRequest & { userId?: number | undefined }) { if (rescheduleUid) { originalRescheduledBooking = await getOriginalRescheduledBooking(rescheduleUid); } + // If the user is not the owner of the event, new booking should be always pending. + // Otherwise, an owner rescheduling should be always accepted. + // Before comparing make sure that userId is set, otherwise undefined === undefined + const userReschedulingIsOwner = userId && originalRescheduledBooking?.user?.id === userId; + const isConfirmedByDefault = + (!eventType.requiresConfirmation && !stripeAppData.price) || userReschedulingIsOwner; async function createBooking() { if (originalRescheduledBooking) { @@ -654,12 +660,6 @@ async function handler(req: NextApiRequest & { userId?: number | undefined }) { const dynamicEventSlugRef = !eventTypeId ? eventTypeSlug : null; const dynamicGroupSlugRef = !eventTypeId ? (reqBody.user as string).toLowerCase() : null; - // If the user is not the owner of the event, new booking should be always pending. - // Otherwise, an owner rescheduling should be always accepted. - // Before comparing make sure that userId is set, otherwise undefined === undefined - const userReschedulingIsOwner = userId && originalRescheduledBooking?.user?.id === userId; - const isConfirmedByDefault = - (!eventType.requiresConfirmation && !stripeAppData.price) || userReschedulingIsOwner; const newBookingData: Prisma.BookingCreateInput = { uid, title: evt.title, @@ -901,8 +901,7 @@ async function handler(req: NextApiRequest & { userId?: number | undefined }) { } } - // FIXME: instead of requiresConfirmation, we should check if isConfirmedByDefault is set or not. - if (eventType.requiresConfirmation && !rescheduleUid && noEmail !== true) { + if (!isConfirmedByDefault && noEmail !== true) { await sendOrganizerRequestEmail({ ...evt, additionalNotes }); await sendAttendeeRequestEmail({ ...evt, additionalNotes }, attendeesList[0]); } @@ -927,72 +926,74 @@ async function handler(req: NextApiRequest & { userId?: number | undefined }) { log.debug(`Booking ${organizerUser.username} completed`); - const eventTrigger: WebhookTriggerEvents = rescheduleUid - ? WebhookTriggerEvents.BOOKING_RESCHEDULED - : WebhookTriggerEvents.BOOKING_CREATED; - const subscriberOptions = { - userId: organizerUser.id, - eventTypeId, - triggerEvent: eventTrigger, - }; - - const subscriberOptionsMeetingEnded = { - userId: organizerUser.id, - eventTypeId, - triggerEvent: WebhookTriggerEvents.MEETING_ENDED, - }; - - try { - const subscribersMeetingEnded = await getWebhooks(subscriberOptionsMeetingEnded); - - subscribersMeetingEnded.forEach((subscriber) => { - if (rescheduleUid && originalRescheduledBooking) { - cancelScheduledJobs(originalRescheduledBooking, undefined, true); - } - if (booking && booking.status === BookingStatus.ACCEPTED) { - scheduleTrigger(booking, subscriber.subscriberUrl, subscriber); - } - }); - } catch (error) { - log.error("Error while running scheduledJobs for booking", error); - } - - try { - // Send Webhook call if hooked to BOOKING_CREATED & BOOKING_RESCHEDULED - const subscribers = await getWebhooks(subscriberOptions); - console.log("evt:", { - ...evt, - metadata: reqBody.metadata, - }); - const bookingId = booking?.id; - - const eventTypeInfo: EventTypeInfo = { - eventTitle: eventType.title, - eventDescription: eventType.description, - requiresConfirmation: eventType.requiresConfirmation || null, - price: stripeAppData.price, - currency: eventType.currency, - length: eventType.length, + if (isConfirmedByDefault) { + const eventTrigger: WebhookTriggerEvents = rescheduleUid + ? WebhookTriggerEvents.BOOKING_RESCHEDULED + : WebhookTriggerEvents.BOOKING_CREATED; + const subscriberOptions = { + userId: organizerUser.id, + eventTypeId, + triggerEvent: eventTrigger, }; - const promises = subscribers.map((sub) => - sendPayload(sub.secret, eventTrigger, new Date().toISOString(), sub, { + const subscriberOptionsMeetingEnded = { + userId: organizerUser.id, + eventTypeId, + triggerEvent: WebhookTriggerEvents.MEETING_ENDED, + }; + + try { + const subscribersMeetingEnded = await getWebhooks(subscriberOptionsMeetingEnded); + + subscribersMeetingEnded.forEach((subscriber) => { + if (rescheduleUid && originalRescheduledBooking) { + cancelScheduledJobs(originalRescheduledBooking, undefined, true); + } + if (booking && booking.status === BookingStatus.ACCEPTED) { + scheduleTrigger(booking, subscriber.subscriberUrl, subscriber); + } + }); + } catch (error) { + log.error("Error while running scheduledJobs for booking", error); + } + + try { + // Send Webhook call if hooked to BOOKING_CREATED & BOOKING_RESCHEDULED + const subscribers = await getWebhooks(subscriberOptions); + console.log("evt:", { ...evt, - ...eventTypeInfo, - bookingId, - rescheduleUid, metadata: reqBody.metadata, - eventTypeId, - //FIXME: FOr consistency b/w webhook and actual DB state, it should use isConfirmedByDefault here - status: eventType.requiresConfirmation ? "PENDING" : "ACCEPTED", - }).catch((e) => { - console.error(`Error executing webhook for event: ${eventTrigger}, URL: ${sub.subscriberUrl}`, e); - }) - ); - await Promise.all(promises); - } catch (error) { - log.error("Error while sending webhook", error); + }); + const bookingId = booking?.id; + + const eventTypeInfo: EventTypeInfo = { + eventTitle: eventType.title, + eventDescription: eventType.description, + requiresConfirmation: eventType.requiresConfirmation || null, + price: stripeAppData.price, + currency: eventType.currency, + length: eventType.length, + }; + + const promises = subscribers.map((sub) => + sendPayload(sub.secret, eventTrigger, new Date().toISOString(), sub, { + ...evt, + ...eventTypeInfo, + bookingId, + rescheduleUid, + metadata: reqBody.metadata, + eventTypeId, + status: "ACCEPTED", + }).catch((e) => { + console.error(`Error executing webhook for event: ${eventTrigger}, URL: ${sub.subscriberUrl}`, e); + }) + ); + await Promise.all(promises); + } catch (error) { + log.error("Error while sending webhook", error); + } } + // Avoid passing referencesToCreate with id unique constrain values // refresh hashed link if used const urlSeed = `${organizerUser.username}:${dayjs(reqBody.start).utc().format()}`; @@ -1038,7 +1039,8 @@ async function handler(req: NextApiRequest & { userId?: number | undefined }) { reqBody.smsReminderNumber as string | null, evt, evt.requiresConfirmation || false, - rescheduleUid ? true : false + rescheduleUid ? true : false, + true ); } catch (error) { log.error("Error while scheduling workflow reminders", error); diff --git a/packages/features/ee/api-keys/components/ApiKeyDialogForm.tsx b/packages/features/ee/api-keys/components/ApiKeyDialogForm.tsx index 968912d4d2..f8f25d9137 100644 --- a/packages/features/ee/api-keys/components/ApiKeyDialogForm.tsx +++ b/packages/features/ee/api-keys/components/ApiKeyDialogForm.tsx @@ -71,7 +71,8 @@ export default function ApiKeyDialogForm({
- + + {" "} {apiKey} diff --git a/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts b/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts index 49d26f6ff2..5990830bb8 100644 --- a/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts +++ b/packages/features/ee/workflows/lib/reminders/reminderScheduler.ts @@ -20,7 +20,8 @@ export const scheduleWorkflowReminders = async ( smsReminderNumber: string | null, evt: CalendarEvent, needsConfirmation: boolean, - isRescheduleEvent: boolean + isRescheduleEvent: boolean, + isFirstRecurringEvent: boolean ) => { if (workflows.length > 0 && !needsConfirmation) { workflows.forEach((workflowReference) => { @@ -29,7 +30,9 @@ export const scheduleWorkflowReminders = async ( const workflow = workflowReference.workflow; if ( workflow.trigger === WorkflowTriggerEvents.BEFORE_EVENT || - (workflow.trigger === WorkflowTriggerEvents.NEW_EVENT && !isRescheduleEvent) || + (workflow.trigger === WorkflowTriggerEvents.NEW_EVENT && + !isRescheduleEvent && + isFirstRecurringEvent) || (workflow.trigger === WorkflowTriggerEvents.RESCHEDULE_EVENT && isRescheduleEvent) || workflow.trigger === WorkflowTriggerEvents.AFTER_EVENT ) { diff --git a/packages/prisma/seed-app-store.config.json b/packages/prisma/seed-app-store.config.json index e6944d49ce..13bddc7dea 100644 --- a/packages/prisma/seed-app-store.config.json +++ b/packages/prisma/seed-app-store.config.json @@ -125,7 +125,6 @@ "categories": ["video"], "slug": "telegram", "type": "telegram_video" - }, { "dirName": "signal", diff --git a/packages/trpc/server/routers/viewer/bookings.tsx b/packages/trpc/server/routers/viewer/bookings.tsx index f1abd3eecb..3a4457a62e 100644 --- a/packages/trpc/server/routers/viewer/bookings.tsx +++ b/packages/trpc/server/routers/viewer/bookings.tsx @@ -30,7 +30,7 @@ import { } from "@calcom/emails"; import { scheduleWorkflowReminders } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler"; import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks"; -import sendPayload from "@calcom/features/webhooks/lib/sendPayload"; +import sendPayload, { EventTypeInfo } from "@calcom/features/webhooks/lib/sendPayload"; import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib"; import logger from "@calcom/lib/logger"; import { getTranslation } from "@calcom/lib/server"; @@ -665,6 +665,10 @@ export const bookingsRouter = router({ recurringEvent: true, title: true, requiresConfirmation: true, + currency: true, + length: true, + description: true, + price: true, workflows: { include: { workflow: { @@ -913,21 +917,23 @@ export const bookingsRouter = router({ } //Workflows - set reminders for confirmed events - try { - for (const updatedBooking of updatedBookings) { - if (updatedBooking.eventType?.workflows) { + for (let index = 0; index < updatedBookings.length; index++) { + if (updatedBookings[index].eventType?.workflows) { const evtOfBooking = evt; - evtOfBooking.startTime = updatedBooking.startTime.toISOString(); - evtOfBooking.endTime = updatedBooking.endTime.toISOString(); - evtOfBooking.uid = updatedBooking.uid; + evtOfBooking.startTime = updatedBookings[index].startTime.toISOString(); + evtOfBooking.endTime = updatedBookings[index].endTime.toISOString(); + evtOfBooking.uid = updatedBookings[index].uid; + + const isFirstBooking = index === 0; await scheduleWorkflowReminders( - updatedBooking.eventType.workflows, - updatedBooking.smsReminderNumber, + updatedBookings[index]?.eventType?.workflows || [], + updatedBookings[index].smsReminderNumber, evtOfBooking, false, - false + false, + isFirstBooking ); } } @@ -944,6 +950,14 @@ export const bookingsRouter = router({ triggerEvent: WebhookTriggerEvents.MEETING_ENDED, }; + const subscriberOptionsBookingCreated = { + userId: booking.userId || 0, + eventTypeId: booking.eventTypeId || 0, + triggerEvent: WebhookTriggerEvents.BOOKING_CREATED, + }; + + const subscribersBookingCreated = await getWebhooks(subscriberOptionsBookingCreated); + const subscribersMeetingEnded = await getWebhooks(subscriberOptionsMeetingEnded); subscribersMeetingEnded.forEach((subscriber) => { @@ -951,6 +965,31 @@ export const bookingsRouter = router({ scheduleTrigger(booking, subscriber.subscriberUrl, subscriber); }); }); + + const eventTypeInfo: EventTypeInfo = { + eventTitle: booking.eventType?.title, + eventDescription: booking.eventType?.description, + requiresConfirmation: booking.eventType?.requiresConfirmation || null, + price: booking.eventType?.price, + currency: booking.eventType?.currency, + length: booking.eventType?.length, + }; + + const promises = subscribersBookingCreated.map((sub) => + sendPayload(sub.secret, WebhookTriggerEvents.BOOKING_CREATED, new Date().toISOString(), sub, { + ...evt, + ...eventTypeInfo, + bookingId, + eventTypeId: booking.eventType?.id, + status: "ACCEPTED", + }).catch((e) => { + console.error( + `Error executing webhook for event: ${WebhookTriggerEvents.BOOKING_CREATED}, URL: ${sub.subscriberUrl}`, + e + ); + }) + ); + await Promise.all(promises); } catch (error) { // Silently fail console.error(error);