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:
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);
|
||||
|
||||
Reference in new issue
Block a user