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 <wollencarina@gmail.com>
This commit is contained in:
Carina WollendorferandCarinaWolli authored and GitHub committed 2022-11-23 16:38:13 +00:00
1 parent 22a030afab
commit 6dce57440e
6 files changed
+129 -85

No files matched your search

@@ -64,7 +64,7 @@ export default function ZapierSetup(props: IZapierSetupProps) {
<>
<div className="mt-1 text-xl">{t("your_unique_api_key")}</div>
<div className="my-2 mt-3 flex-wrap sm:flex sm:flex-nowrap">
<code className="h-full w-full whitespace-pre-wrap rounded-md bg-gray-100 py-2 pl-2 pr-2 sm:rounded-r-none sm:pr-5">
<code className="h-full w-full whitespace-pre-wrap rounded-md bg-gray-100 py-[6px] pl-2 pr-2 sm:rounded-r-none sm:pr-5">
{newApiKey}
</code>
<Tooltip side="top" content={t("copy_to_clipboard")}>
@@ -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);
@@ -71,7 +71,8 @@ export default function ApiKeyDialogForm({
</div>
<div>
<div className="flex">
<code className="mb-2 w-full truncate rounded-md rounded-r-none bg-gray-100 px-3 py-2 align-middle font-mono text-gray-800">
<code className="mb-2 w-full truncate rounded-md rounded-r-none bg-gray-100 py-[6px] pl-2 pr-2 align-middle font-mono text-gray-800">
{" "}
{apiKey}
</code>
<Tooltip side="top" content={t("copy_to_clipboard")}>
@@ -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
) {
@@ -125,7 +125,6 @@
"categories": ["video"],
"slug": "telegram",
"type": "telegram_video"
},
{
"dirName": "signal",
@@ -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);