Skip to content

Commit 89510e6

Browse files
CarinaWolliCarinaWollikodiakhq[bot]zomars
authored
Zapier Trigger: After Meeting ends (#3827)
* add new trigger with first simple job scheduling * fix DB update * use array to save scheduled jobs in booking * cancel scheduled jobs when zap disabled or zapier disconnected * schedule jobs only for confirmed bookings * schedule jobs for already existing bookings * fix bug to create workflow reminders when confirming recurring events * delete remove all zapier webhooks when api key is deleted * schedule job for all confirmed recurring bookings * fix zapier trigger and workflow reminders when cancelling recurring events * code clean up * code clean up * add migration * add type package for node-schedule * remove nodescheduler * add updated nodescheduler * move code to app-store * add meeting ended to webhook constants * udpate zapier README.md * implement QA suggestions * add default handler and fix imports * Type fix Co-authored-by: CarinaWolli <wollencarina@gmail.com> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: Omar López <zomars@me.com>
1 parent 297a965 commit 89510e6

File tree

18 files changed

+452
-83
lines changed

18 files changed

+452
-83
lines changed

apps/web/lib/webhooks/constants.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@ export const WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP = {
77
WebhookTriggerEvents.BOOKING_CANCELLED,
88
WebhookTriggerEvents.BOOKING_CREATED,
99
WebhookTriggerEvents.BOOKING_RESCHEDULED,
10-
] as ["BOOKING_CANCELLED", "BOOKING_CREATED", "BOOKING_RESCHEDULED"],
10+
WebhookTriggerEvents.MEETING_ENDED,
11+
] as const,
1112
routing_forms: [WebhookTriggerEvents.FORM_SUBMITTED] as ["FORM_SUBMITTED"],
1213
};
1314

1415
export const WEBHOOK_TRIGGER_EVENTS = [
1516
...WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP.core,
1617
...WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP.routing_forms,
17-
] as ["BOOKING_CANCELLED", "BOOKING_CREATED", "BOOKING_RESCHEDULED", "FORM_SUBMITTED"];
18+
] as const;

apps/web/lib/webhooks/subscriptions.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ const getWebhooks = async (options: GetSubscriberOptions) => {
3030
},
3131
},
3232
select: {
33+
id: true,
3334
subscriberUrl: true,
3435
payloadTemplate: true,
3536
appId: true,

apps/web/pages/api/book/confirm.ts

+108-6
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,19 @@
1-
import { Booking, BookingStatus, Prisma, SchedulingType, User } from "@prisma/client";
1+
import {
2+
Booking,
3+
BookingStatus,
4+
Prisma,
5+
SchedulingType,
6+
User,
7+
WebhookTriggerEvents,
8+
Workflow,
9+
WorkflowsOnEventTypes,
10+
WorkflowStep,
11+
} from "@prisma/client";
212
import type { NextApiRequest } from "next";
313
import { z } from "zod";
414

515
import { refund } from "@calcom/app-store/stripepayment/lib/server";
16+
import { scheduleTrigger } from "@calcom/app-store/zapier/lib/nodeScheduler";
617
import EventManager from "@calcom/core/EventManager";
718
import { sendDeclinedEmails, sendScheduledEmails } from "@calcom/emails";
819
import { scheduleWorkflowReminders } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler";
@@ -14,6 +25,7 @@ import type { AdditionalInformation, CalendarEvent } from "@calcom/types/Calenda
1425

1526
import { getSession } from "@lib/auth";
1627
import { HttpError } from "@lib/core/http/error";
28+
import getSubscribers from "@lib/webhooks/subscriptions";
1729

1830
import { getTranslation } from "@server/lib/i18n";
1931

@@ -30,6 +42,7 @@ const authorized = async (
3042
id: booking.eventTypeId || undefined,
3143
},
3244
select: {
45+
id: true,
3346
schedulingType: true,
3447
users: true,
3548
},
@@ -129,6 +142,7 @@ async function patchHandler(req: NextApiRequest) {
129142
recurringEventId: true,
130143
status: true,
131144
smsReminderNumber: true,
145+
scheduledJobs: true,
132146
},
133147
});
134148

@@ -237,6 +251,21 @@ async function patchHandler(req: NextApiRequest) {
237251
log.error(error);
238252
}
239253
}
254+
let updatedBookings: {
255+
scheduledJobs: string[];
256+
id: number;
257+
startTime: Date;
258+
endTime: Date;
259+
uid: string;
260+
smsReminderNumber: string | null;
261+
eventType: {
262+
workflows: (WorkflowsOnEventTypes & {
263+
workflow: Workflow & {
264+
steps: WorkflowStep[];
265+
};
266+
})[];
267+
} | null;
268+
}[] = [];
240269

241270
if (recurringEventId) {
242271
// The booking to confirm is a recurring event and comes from /booking/recurring, proceeding to mark all related
@@ -247,8 +276,9 @@ async function patchHandler(req: NextApiRequest) {
247276
status: BookingStatus.PENDING,
248277
},
249278
});
250-
unconfirmedRecurringBookings.map(async (recurringBooking) => {
251-
await prisma.booking.update({
279+
280+
const updateBookingsPromise = unconfirmedRecurringBookings.map((recurringBooking) => {
281+
return prisma.booking.update({
252282
where: {
253283
id: recurringBooking.id,
254284
},
@@ -258,12 +288,35 @@ async function patchHandler(req: NextApiRequest) {
258288
create: scheduleResult.referencesToCreate,
259289
},
260290
},
291+
select: {
292+
eventType: {
293+
select: {
294+
workflows: {
295+
include: {
296+
workflow: {
297+
include: {
298+
steps: true,
299+
},
300+
},
301+
},
302+
},
303+
},
304+
},
305+
uid: true,
306+
startTime: true,
307+
endTime: true,
308+
smsReminderNumber: true,
309+
id: true,
310+
scheduledJobs: true,
311+
},
261312
});
262313
});
314+
const updatedBookingsResult = await Promise.all(updateBookingsPromise);
315+
updatedBookings = updatedBookings.concat(updatedBookingsResult);
263316
} else {
264317
// @NOTE: be careful with this as if any error occurs before this booking doesn't get confirmed
265318
// Should perform update on booking (confirm) -> then trigger the rest handlers
266-
await prisma.booking.update({
319+
const updatedBooking = await prisma.booking.update({
267320
where: {
268321
id: bookingId,
269322
},
@@ -273,13 +326,62 @@ async function patchHandler(req: NextApiRequest) {
273326
create: scheduleResult.referencesToCreate,
274327
},
275328
},
329+
select: {
330+
eventType: {
331+
select: {
332+
workflows: {
333+
include: {
334+
workflow: {
335+
include: {
336+
steps: true,
337+
},
338+
},
339+
},
340+
},
341+
},
342+
},
343+
uid: true,
344+
startTime: true,
345+
endTime: true,
346+
smsReminderNumber: true,
347+
id: true,
348+
scheduledJobs: true,
349+
},
276350
});
351+
updatedBookings.push(updatedBooking);
277352
}
278353

279354
//Workflows - set reminders for confirmed events
280-
if (booking.eventType?.workflows) {
281-
await scheduleWorkflowReminders(booking.eventType.workflows, booking.smsReminderNumber, evt, false);
355+
for (const updatedBooking of updatedBookings) {
356+
if (updatedBooking.eventType?.workflows) {
357+
const evtOfBooking = evt;
358+
evtOfBooking.startTime = updatedBooking.startTime.toISOString();
359+
evtOfBooking.endTime = updatedBooking.endTime.toISOString();
360+
evtOfBooking.uid = updatedBooking.uid;
361+
362+
await scheduleWorkflowReminders(
363+
updatedBooking.eventType.workflows,
364+
updatedBooking.smsReminderNumber,
365+
evtOfBooking,
366+
false
367+
);
368+
}
282369
}
370+
371+
// schedule job for zapier trigger 'when meeting ends'
372+
const subscriberOptionsMeetingEnded = {
373+
userId: booking.userId || 0,
374+
eventTypeId: booking.eventTypeId || 0,
375+
triggerEvent: WebhookTriggerEvents.MEETING_ENDED,
376+
};
377+
378+
const subscribersMeetingEnded = await getSubscribers(subscriberOptionsMeetingEnded);
379+
380+
subscribersMeetingEnded.forEach((subscriber) => {
381+
updatedBookings.forEach((booking) => {
382+
scheduleTrigger(booking, subscriber.subscriberUrl, subscriber);
383+
});
384+
});
283385
} else {
284386
evt.rejectionReason = rejectionReason;
285387
if (recurringEventId) {

apps/web/pages/api/book/event.ts

+21-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import short from "short-uuid";
66
import { v5 as uuidv5 } from "uuid";
77

88
import { handlePayment } from "@calcom/app-store/stripepayment/lib/server";
9+
import { cancelScheduledJobs, scheduleTrigger } from "@calcom/app-store/zapier/lib/nodeScheduler";
910
import EventManager from "@calcom/core/EventManager";
1011
import { getUserAvailability } from "@calcom/core/getUserAvailability";
1112
import dayjs from "@calcom/dayjs";
@@ -791,13 +792,32 @@ async function handler(req: NextApiRequest) {
791792

792793
log.debug(`Booking ${organizerUser.username} completed`);
793794

794-
const eventTrigger: WebhookTriggerEvents = rescheduleUid ? "BOOKING_RESCHEDULED" : "BOOKING_CREATED";
795+
const eventTrigger: WebhookTriggerEvents = rescheduleUid
796+
? WebhookTriggerEvents.BOOKING_RESCHEDULED
797+
: WebhookTriggerEvents.BOOKING_CREATED;
795798
const subscriberOptions = {
796799
userId: organizerUser.id,
797800
eventTypeId,
798801
triggerEvent: eventTrigger,
799802
};
800803

804+
const subscriberOptionsMeetingEnded = {
805+
userId: organizerUser.id,
806+
eventTypeId,
807+
triggerEvent: WebhookTriggerEvents.MEETING_ENDED,
808+
};
809+
810+
const subscribersMeetingEnded = await getSubscribers(subscriberOptionsMeetingEnded);
811+
812+
subscribersMeetingEnded.forEach((subscriber) => {
813+
if (rescheduleUid && originalRescheduledBooking) {
814+
cancelScheduledJobs(originalRescheduledBooking);
815+
}
816+
if (booking && booking.status === BookingStatus.ACCEPTED) {
817+
scheduleTrigger(booking, subscriber.subscriberUrl, subscriber);
818+
}
819+
});
820+
801821
// Send Webhook call if hooked to BOOKING_CREATED & BOOKING_RESCHEDULED
802822
const subscribers = await getSubscribers(subscriberOptions);
803823
console.log("evt:", {

apps/web/pages/api/cancel.ts

+66-24
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,18 @@
1-
import { BookingStatus, Prisma, PrismaPromise, WebhookTriggerEvents, WorkflowMethods } from "@prisma/client";
1+
import {
2+
BookingStatus,
3+
Prisma,
4+
PrismaPromise,
5+
WebhookTriggerEvents,
6+
WorkflowMethods,
7+
WorkflowReminder,
8+
} from "@prisma/client";
29
import { NextApiRequest, NextApiResponse } from "next";
310
import z from "zod";
411

512
import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
613
import { FAKE_DAILY_CREDENTIAL } from "@calcom/app-store/dailyvideo/lib/VideoApiAdapter";
714
import { refund } from "@calcom/app-store/stripepayment/lib/server";
15+
import { cancelScheduledJobs } from "@calcom/app-store/zapier/lib/nodeScheduler";
816
import { deleteMeeting } from "@calcom/core/videoClient";
917
import dayjs from "@calcom/dayjs";
1018
import { sendCancelledEmails } from "@calcom/emails";
@@ -82,6 +90,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
8290
destinationCalendar: true,
8391
smsReminderNumber: true,
8492
workflowReminders: true,
93+
scheduledJobs: true,
8594
},
8695
});
8796

@@ -164,6 +173,21 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
164173
);
165174
await Promise.all(promises);
166175

176+
//Workflows - schedule reminders
177+
if (bookingToDelete.eventType?.workflows) {
178+
await sendCancelledReminders(
179+
bookingToDelete.eventType?.workflows,
180+
bookingToDelete.smsReminderNumber,
181+
evt
182+
);
183+
}
184+
185+
let updatedBookings: {
186+
uid: string;
187+
workflowReminders: WorkflowReminder[];
188+
scheduledJobs: string[];
189+
}[] = [];
190+
167191
// by cancelling first, and blocking whilst doing so; we can ensure a cancel
168192
// action always succeeds even if subsequent integrations fail cancellation.
169193
if (bookingToDelete.eventType?.recurringEvent && bookingToDelete.recurringEventId && allRemainingBookings) {
@@ -181,16 +205,36 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
181205
cancellationReason: cancellationReason,
182206
},
183207
});
208+
const allUpdatedBookings = await prisma.booking.findMany({
209+
where: {
210+
recurringEventId: bookingToDelete.recurringEventId,
211+
startTime: {
212+
gte: new Date(),
213+
},
214+
},
215+
select: {
216+
workflowReminders: true,
217+
uid: true,
218+
scheduledJobs: true,
219+
},
220+
});
221+
updatedBookings = updatedBookings.concat(allUpdatedBookings);
184222
} else {
185-
await prisma.booking.update({
223+
const updatedBooking = await prisma.booking.update({
186224
where: {
187225
uid,
188226
},
189227
data: {
190228
status: BookingStatus.CANCELLED,
191229
cancellationReason: cancellationReason,
192230
},
231+
select: {
232+
workflowReminders: true,
233+
uid: true,
234+
scheduledJobs: true,
235+
},
193236
});
237+
updatedBookings.push(updatedBooking);
194238
}
195239

196240
/** TODO: Remove this without breaking functionality */
@@ -299,37 +343,35 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
299343
},
300344
});
301345

302-
//Workflows - delete all reminders for that booking
346+
// delete scheduled jobs of cancelled bookings
347+
updatedBookings.forEach((booking) => {
348+
cancelScheduledJobs(booking);
349+
});
350+
351+
//Workflows - delete all reminders for bookings
303352
const remindersToDelete: PrismaPromise<Prisma.BatchPayload>[] = [];
304-
bookingToDelete.workflowReminders.forEach((reminder) => {
305-
if (reminder.scheduled && reminder.referenceId) {
306-
if (reminder.method === WorkflowMethods.EMAIL) {
307-
deleteScheduledEmailReminder(reminder.referenceId);
308-
} else if (reminder.method === WorkflowMethods.SMS) {
309-
deleteScheduledSMSReminder(reminder.referenceId);
353+
updatedBookings.forEach((booking) => {
354+
booking.workflowReminders.forEach((reminder) => {
355+
if (reminder.scheduled && reminder.referenceId) {
356+
if (reminder.method === WorkflowMethods.EMAIL) {
357+
deleteScheduledEmailReminder(reminder.referenceId);
358+
} else if (reminder.method === WorkflowMethods.SMS) {
359+
deleteScheduledSMSReminder(reminder.referenceId);
360+
}
310361
}
311-
}
312-
const reminderToDelete = prisma.workflowReminder.deleteMany({
313-
where: {
314-
id: reminder.id,
315-
},
362+
const reminderToDelete = prisma.workflowReminder.deleteMany({
363+
where: {
364+
id: reminder.id,
365+
},
366+
});
367+
remindersToDelete.push(reminderToDelete);
316368
});
317-
remindersToDelete.push(reminderToDelete);
318369
});
319370

320371
await Promise.all([apiDeletes, attendeeDeletes, bookingReferenceDeletes].concat(remindersToDelete));
321372

322373
await sendCancelledEmails(evt);
323374

324-
//Workflows - schedule reminders
325-
if (bookingToDelete.eventType?.workflows) {
326-
await sendCancelledReminders(
327-
bookingToDelete.eventType?.workflows,
328-
bookingToDelete.smsReminderNumber,
329-
evt
330-
);
331-
}
332-
333375
res.status(204).end();
334376
}
335377

0 commit comments

Comments
 (0)