Compare commits

...

1 Commits

Author SHA1 Message Date
Shivam Kalra d6a4daae53
feat: Add new events to webhook BOOKING_CONFIRMED, BOOKING_REJECTED (#8884)
* test: booking rejection

* test: check if webhook is called

* feat: BOOKING_REJECTED enum ,constant in backend

* feat: send booking rejected webhook to subscribers

* feat: add Booking rejected migration

* calendar event

* Revert "calendar event"

This reverts commit 28d45dccfd.

* feat: BOOKING_REQUESTED enum, constant, migration

* feat: Send BOOKING REQUESTED Webhook call

* feat: Add booking rejected/requested event in form

* feat: data-testid to rejection confirm btn

* test: BOOKING_REJECTED, BOOKING_REQUESTED

* fix: booking status PENDING, Linting

* feat: add new labels to common.json

* remove: meeting ended hook from request hook

* refactor: abstract handleWebhookTrigger

* fix: create a single file for migration

* refactor: reduce code repetition and fix test

* feat: add team webhooks to subscriberOptions

* refactor: subscriberOptions

---------

Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-05-25 15:51:11 +00:00
11 changed files with 479 additions and 160 deletions

View File

@ -318,6 +318,7 @@ function BookingListItem(booking: BookingItemProps) {
<DialogClose />
<Button
disabled={mutation.isLoading}
data-testid="rejection-confirm"
onClick={() => {
bookingConfirm(false);
}}>

View File

@ -1,131 +1,387 @@
import { expect } from "@playwright/test";
import { test } from "./lib/fixtures";
import { createHttpServer, selectFirstAvailableTimeSlotNextMonth, waitFor } from "./lib/testUtils";
import {
bookOptinEvent,
createHttpServer,
selectFirstAvailableTimeSlotNextMonth,
waitFor,
} from "./lib/testUtils";
test.afterEach(({ users }) => users.deleteAll());
test("add webhook & test that creating an event triggers a webhook call", async ({
page,
users,
}, testInfo) => {
const webhookReceiver = createHttpServer();
const user = await users.create();
const [eventType] = user.eventTypes;
await user.login();
await page.goto(`/settings/developer/webhooks`);
test.describe("BOOKING_CREATED", async () => {
test("add webhook & test that creating an event triggers a webhook call", async ({
page,
users,
}, testInfo) => {
const webhookReceiver = createHttpServer();
const user = await users.create();
const [eventType] = user.eventTypes;
await user.login();
await page.goto(`/settings/developer/webhooks`);
// --- add webhook
await page.click('[data-testid="new_webhook"]');
// --- add webhook
await page.click('[data-testid="new_webhook"]');
await page.fill('[name="subscriberUrl"]', webhookReceiver.url);
await page.fill('[name="subscriberUrl"]', webhookReceiver.url);
await page.fill('[name="secret"]', "secret");
await page.fill('[name="secret"]', "secret");
await Promise.all([
page.click("[type=submit]"),
page.waitForURL((url) => url.pathname.endsWith("/settings/developer/webhooks")),
]);
await Promise.all([
page.click("[type=submit]"),
page.waitForURL((url) => url.pathname.endsWith("/settings/developer/webhooks")),
]);
// page contains the url
expect(page.locator(`text='${webhookReceiver.url}'`)).toBeDefined();
// page contains the url
expect(page.locator(`text='${webhookReceiver.url}'`)).toBeDefined();
// --- Book the first available day next month in the pro user's "30min"-event
await page.goto(`/${user.username}/${eventType.slug}`);
await selectFirstAvailableTimeSlotNextMonth(page);
// --- Book the first available day next month in the pro user's "30min"-event
await page.goto(`/${user.username}/${eventType.slug}`);
await selectFirstAvailableTimeSlotNextMonth(page);
// --- fill form
await page.fill('[name="name"]', "Test Testson");
await page.fill('[name="email"]', "test@example.com");
await page.press('[name="email"]', "Enter");
// --- fill form
await page.fill('[name="name"]', "Test Testson");
await page.fill('[name="email"]', "test@example.com");
await page.press('[name="email"]', "Enter");
// --- check that webhook was called
await waitFor(() => {
expect(webhookReceiver.requestList.length).toBe(1);
});
// --- check that webhook was called
await waitFor(() => {
expect(webhookReceiver.requestList.length).toBe(1);
});
const [request] = webhookReceiver.requestList;
const body = request.body as any;
const [request] = webhookReceiver.requestList;
const body = request.body as any;
// remove dynamic properties that differs depending on where you run the tests
const dynamic = "[redacted/dynamic]";
body.createdAt = dynamic;
body.payload.startTime = dynamic;
body.payload.endTime = dynamic;
body.payload.location = dynamic;
for (const attendee of body.payload.attendees) {
attendee.timeZone = dynamic;
attendee.language = dynamic;
}
body.payload.organizer.id = dynamic;
body.payload.organizer.email = dynamic;
body.payload.organizer.timeZone = dynamic;
body.payload.organizer.language = dynamic;
body.payload.uid = dynamic;
body.payload.bookingId = dynamic;
body.payload.additionalInformation = dynamic;
body.payload.requiresConfirmation = dynamic;
body.payload.eventTypeId = dynamic;
body.payload.videoCallData = dynamic;
body.payload.appsStatus = dynamic;
body.payload.metadata.videoCallUrl = dynamic;
// remove dynamic properties that differs depending on where you run the tests
const dynamic = "[redacted/dynamic]";
body.createdAt = dynamic;
body.payload.startTime = dynamic;
body.payload.endTime = dynamic;
body.payload.location = dynamic;
for (const attendee of body.payload.attendees) {
attendee.timeZone = dynamic;
attendee.language = dynamic;
}
body.payload.organizer.id = dynamic;
body.payload.organizer.email = dynamic;
body.payload.organizer.timeZone = dynamic;
body.payload.organizer.language = dynamic;
body.payload.uid = dynamic;
body.payload.bookingId = dynamic;
body.payload.additionalInformation = dynamic;
body.payload.requiresConfirmation = dynamic;
body.payload.eventTypeId = dynamic;
body.payload.videoCallData = dynamic;
body.payload.appsStatus = dynamic;
body.payload.metadata.videoCallUrl = dynamic;
expect(body).toMatchObject({
triggerEvent: "BOOKING_CREATED",
createdAt: "[redacted/dynamic]",
payload: {
type: "30 min",
title: "30 min between Nameless and Test Testson",
description: "",
additionalNotes: "",
customInputs: {},
startTime: "[redacted/dynamic]",
endTime: "[redacted/dynamic]",
organizer: {
id: "[redacted/dynamic]",
name: "Nameless",
email: "[redacted/dynamic]",
timeZone: "[redacted/dynamic]",
language: "[redacted/dynamic]",
},
responses: {
email: {
value: "test@example.com",
label: "email_address",
},
name: {
value: "Test Testson",
label: "your_name",
},
},
userFieldsResponses: {},
attendees: [
{
email: "test@example.com",
name: "Test Testson",
expect(body).toMatchObject({
triggerEvent: "BOOKING_CREATED",
createdAt: "[redacted/dynamic]",
payload: {
type: "30 min",
title: "30 min between Nameless and Test Testson",
description: "",
additionalNotes: "",
customInputs: {},
startTime: "[redacted/dynamic]",
endTime: "[redacted/dynamic]",
organizer: {
id: "[redacted/dynamic]",
name: "Nameless",
email: "[redacted/dynamic]",
timeZone: "[redacted/dynamic]",
language: "[redacted/dynamic]",
},
],
location: "[redacted/dynamic]",
destinationCalendar: null,
hideCalendarNotes: false,
requiresConfirmation: "[redacted/dynamic]",
eventTypeId: "[redacted/dynamic]",
seatsShowAttendees: true,
seatsPerTimeSlot: null,
uid: "[redacted/dynamic]",
eventTitle: "30 min",
eventDescription: null,
price: 0,
currency: "usd",
length: 30,
bookingId: "[redacted/dynamic]",
metadata: { videoCallUrl: "[redacted/dynamic]" },
status: "ACCEPTED",
additionalInformation: "[redacted/dynamic]",
},
});
responses: {
email: {
value: "test@example.com",
label: "email_address",
},
name: {
value: "Test Testson",
label: "your_name",
},
},
userFieldsResponses: {},
attendees: [
{
email: "test@example.com",
name: "Test Testson",
timeZone: "[redacted/dynamic]",
language: "[redacted/dynamic]",
},
],
location: "[redacted/dynamic]",
destinationCalendar: null,
hideCalendarNotes: false,
requiresConfirmation: "[redacted/dynamic]",
eventTypeId: "[redacted/dynamic]",
seatsShowAttendees: true,
seatsPerTimeSlot: null,
uid: "[redacted/dynamic]",
eventTitle: "30 min",
eventDescription: null,
price: 0,
currency: "usd",
length: 30,
bookingId: "[redacted/dynamic]",
metadata: { videoCallUrl: "[redacted/dynamic]" },
status: "ACCEPTED",
additionalInformation: "[redacted/dynamic]",
},
});
webhookReceiver.close();
webhookReceiver.close();
});
});
test.describe("BOOKING_REJECTED", async () => {
test("can book an event that requires confirmation and then that booking can be rejected by organizer", async ({
page,
users,
}) => {
const webhookReceiver = createHttpServer();
// --- create a user
const user = await users.create();
// --- visit user page
await page.goto(`/${user.username}`);
// --- book the user's event
await bookOptinEvent(page);
// --- login as that user
await user.login();
await page.goto(`/settings/developer/webhooks`);
// --- add webhook
await page.click('[data-testid="new_webhook"]');
await page.fill('[name="subscriberUrl"]', webhookReceiver.url);
await page.fill('[name="secret"]', "secret");
await Promise.all([
page.click("[type=submit]"),
page.waitForURL((url) => url.pathname.endsWith("/settings/developer/webhooks")),
]);
// page contains the url
expect(page.locator(`text='${webhookReceiver.url}'`)).toBeDefined();
await page.goto("/bookings/unconfirmed");
await page.click('[data-testid="reject"]');
await page.click('[data-testid="rejection-confirm"]');
await page.waitForResponse((response) => response.url().includes("/api/trpc/bookings/confirm"));
// --- check that webhook was called
await waitFor(() => {
expect(webhookReceiver.requestList.length).toBe(1);
});
const [request] = webhookReceiver.requestList;
const body = request.body as any;
// remove dynamic properties that differs depending on where you run the tests
const dynamic = "[redacted/dynamic]";
body.createdAt = dynamic;
body.payload.startTime = dynamic;
body.payload.endTime = dynamic;
body.payload.location = dynamic;
for (const attendee of body.payload.attendees) {
attendee.timeZone = dynamic;
attendee.language = dynamic;
}
body.payload.organizer.id = dynamic;
body.payload.organizer.email = dynamic;
body.payload.organizer.timeZone = dynamic;
body.payload.organizer.language = dynamic;
body.payload.uid = dynamic;
body.payload.bookingId = dynamic;
body.payload.additionalInformation = dynamic;
body.payload.requiresConfirmation = dynamic;
body.payload.eventTypeId = dynamic;
body.payload.videoCallData = dynamic;
body.payload.appsStatus = dynamic;
// body.payload.metadata.videoCallUrl = dynamic;
expect(body).toMatchObject({
triggerEvent: "BOOKING_REJECTED",
createdAt: "[redacted/dynamic]",
payload: {
type: "Opt in",
title: "Opt in between Nameless and Test Testson",
customInputs: {},
startTime: "[redacted/dynamic]",
endTime: "[redacted/dynamic]",
organizer: {
id: "[redacted/dynamic]",
name: "Unnamed",
email: "[redacted/dynamic]",
timeZone: "[redacted/dynamic]",
language: "[redacted/dynamic]",
},
responses: {
email: {
value: "test@example.com",
label: "email",
},
name: {
value: "Test Testson",
label: "name",
},
},
userFieldsResponses: {},
attendees: [
{
email: "test@example.com",
name: "Test Testson",
timeZone: "[redacted/dynamic]",
language: "[redacted/dynamic]",
},
],
location: "[redacted/dynamic]",
destinationCalendar: null,
// hideCalendarNotes: false,
requiresConfirmation: "[redacted/dynamic]",
eventTypeId: "[redacted/dynamic]",
uid: "[redacted/dynamic]",
eventTitle: "Opt in",
eventDescription: null,
price: 0,
currency: "usd",
length: 30,
bookingId: "[redacted/dynamic]",
// metadata: { videoCallUrl: "[redacted/dynamic]" },
status: "REJECTED",
additionalInformation: "[redacted/dynamic]",
},
});
webhookReceiver.close();
});
});
test.describe("BOOKING_REQUESTED", async () => {
test("can book an event that requires confirmation and get a booking requested event", async ({
page,
users,
}) => {
const webhookReceiver = createHttpServer();
// --- create a user
const user = await users.create();
// --- login as that user
await user.login();
await page.goto(`/settings/developer/webhooks`);
// --- add webhook
await page.click('[data-testid="new_webhook"]');
await page.fill('[name="subscriberUrl"]', webhookReceiver.url);
await page.fill('[name="secret"]', "secret");
await Promise.all([
page.click("[type=submit]"),
page.waitForURL((url) => url.pathname.endsWith("/settings/developer/webhooks")),
]);
// page contains the url
expect(page.locator(`text='${webhookReceiver.url}'`)).toBeDefined();
// --- visit user page
await page.goto(`/${user.username}`);
// --- book the user's opt in
await bookOptinEvent(page);
// --- check that webhook was called
await waitFor(() => {
expect(webhookReceiver.requestList.length).toBe(1);
});
const [request] = webhookReceiver.requestList;
const body = request.body as any;
// remove dynamic properties that differs depending on where you run the tests
const dynamic = "[redacted/dynamic]";
body.createdAt = dynamic;
body.payload.startTime = dynamic;
body.payload.endTime = dynamic;
body.payload.location = dynamic;
for (const attendee of body.payload.attendees) {
attendee.timeZone = dynamic;
attendee.language = dynamic;
}
body.payload.organizer.id = dynamic;
body.payload.organizer.email = dynamic;
body.payload.organizer.timeZone = dynamic;
body.payload.organizer.language = dynamic;
body.payload.uid = dynamic;
body.payload.bookingId = dynamic;
body.payload.additionalInformation = dynamic;
body.payload.requiresConfirmation = dynamic;
body.payload.eventTypeId = dynamic;
body.payload.videoCallData = dynamic;
body.payload.appsStatus = dynamic;
body.payload.metadata.videoCallUrl = dynamic;
expect(body).toMatchObject({
triggerEvent: "BOOKING_REQUESTED",
createdAt: "[redacted/dynamic]",
payload: {
type: "Opt in",
title: "Opt in between Nameless and Test Testson",
customInputs: {},
startTime: "[redacted/dynamic]",
endTime: "[redacted/dynamic]",
organizer: {
id: "[redacted/dynamic]",
name: "Nameless",
email: "[redacted/dynamic]",
timeZone: "[redacted/dynamic]",
language: "[redacted/dynamic]",
},
responses: {
email: {
value: "test@example.com",
label: "email_address",
},
name: {
value: "Test Testson",
label: "your_name",
},
},
userFieldsResponses: {},
attendees: [
{
email: "test@example.com",
name: "Test Testson",
timeZone: "[redacted/dynamic]",
language: "[redacted/dynamic]",
},
],
location: "[redacted/dynamic]",
destinationCalendar: null,
requiresConfirmation: "[redacted/dynamic]",
eventTypeId: "[redacted/dynamic]",
uid: "[redacted/dynamic]",
eventTitle: "Opt in",
eventDescription: null,
price: 0,
currency: "usd",
length: 30,
bookingId: "[redacted/dynamic]",
status: "PENDING",
additionalInformation: "[redacted/dynamic]",
metadata: { videoCallUrl: "[redacted/dynamic]" },
},
});
webhookReceiver.close();
});
});

View File

@ -374,6 +374,8 @@
"booking_rescheduled": "Booking Rescheduled",
"recording_ready":"Recording Download Link Ready",
"booking_created": "Booking Created",
"booking_rejected":"Booking Rejected",
"booking_requested":"Booking Requested",
"meeting_ended": "Meeting Ended",
"form_submitted": "Form Submitted",
"event_triggers": "Event Triggers",

View File

@ -33,6 +33,7 @@ import {
} from "@calcom/emails";
import { getBookingFieldsWithSystemFields } from "@calcom/features/bookings/lib/getBookingFields";
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
import { handleWebhookTrigger } from "@calcom/features/bookings/lib/handleWebhookTrigger";
import {
allowDisablingAttendeeConfirmationEmails,
allowDisablingHostConfirmationEmails,
@ -40,6 +41,7 @@ import {
import { deleteScheduledEmailReminder } from "@calcom/features/ee/workflows/lib/reminders/emailReminderManager";
import { scheduleWorkflowReminders } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler";
import { deleteScheduledSMSReminder } from "@calcom/features/ee/workflows/lib/reminders/smsReminderManager";
import type { GetSubscriberOptions } from "@calcom/features/webhooks/lib/getWebhooks";
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser";
@ -72,7 +74,6 @@ import type { EventResult, PartialReference } from "@calcom/types/EventManager";
import type { WorkingHours, TimeRange as DateOverride } from "@calcom/types/schedule";
import type { EventTypeInfo } from "../../webhooks/lib/sendPayload";
import sendPayload from "../../webhooks/lib/sendPayload";
import getBookingResponsesSchema from "./getBookingResponsesSchema";
const translator = short();
@ -2092,16 +2093,44 @@ async function handler(
videoCallUrl: getVideoCallUrlFromCalEvent(evt),
}
: 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,
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,
};
const subscriberOptions: GetSubscriberOptions = {
userId: organizerUser.id,
eventTypeId,
triggerEvent: WebhookTriggerEvents.BOOKING_CREATED,
teamId: eventType.team?.id,
};
if (isConfirmedByDefault) {
const eventTrigger: WebhookTriggerEvents = rescheduleUid
? WebhookTriggerEvents.BOOKING_RESCHEDULED
: WebhookTriggerEvents.BOOKING_CREATED;
const subscriberOptions = {
userId: organizerUser.id,
eventTypeId,
triggerEvent: eventTrigger,
teamId: eventType.team?.id,
};
subscriberOptions.triggerEvent = eventTrigger;
const subscriberOptionsMeetingEnded = {
userId: organizerUser.id,
@ -2125,48 +2154,14 @@ async function handler(
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: requiresConfirmation || null,
price: paymentAppData.price,
currency: eventType.currency,
length: eventType.length,
};
const promises = subscribers.map((sub) =>
sendPayload(sub.secret, eventTrigger, new Date().toISOString(), sub, {
...evt,
...eventTypeInfo,
bookingId,
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,
}).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);
}
// Send Webhook call if hooked to BOOKING_CREATED & BOOKING_RESCHEDULED
await handleWebhookTrigger({ subscriberOptions, eventTrigger, webhookData });
} else if (eventType.requiresConfirmation) {
// if eventType requires confirmation we will trigger the BOOKING REQUESTED Webhook
const eventTrigger: WebhookTriggerEvents = WebhookTriggerEvents.BOOKING_REQUESTED;
subscriberOptions.triggerEvent = eventTrigger;
webhookData.status = "PENDING";
await handleWebhookTrigger({ subscriberOptions, eventTrigger, webhookData });
}
// Avoid passing referencesToCreate with id unique constrain values

View File

@ -0,0 +1,29 @@
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
import type { GetSubscriberOptions } from "@calcom/features/webhooks/lib/getWebhooks";
import type { WebhookDataType } from "@calcom/features/webhooks/lib/sendPayload";
import sendPayload from "@calcom/features/webhooks/lib/sendPayload";
import logger from "@calcom/lib/logger";
export async function handleWebhookTrigger(args: {
subscriberOptions: GetSubscriberOptions;
eventTrigger: string;
webhookData: Omit<WebhookDataType, "createdAt" | "triggerEvent">;
}) {
try {
const subscribers = await getWebhooks(args.subscriberOptions);
const promises = subscribers.map((sub) =>
sendPayload(sub.secret, args.eventTrigger, new Date().toISOString(), sub, args.webhookData).catch(
(e) => {
console.error(
`Error executing webhook for event: ${args.eventTrigger}, URL: ${sub.subscriberUrl}`,
e
);
}
)
);
await Promise.all(promises);
} catch (error) {
logger.error("Error while sending webhook", error);
}
}

View File

@ -32,6 +32,8 @@ const WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP_V2: Record<string, WebhookTriggerEve
core: [
{ value: WebhookTriggerEvents.BOOKING_CANCELLED, label: "booking_cancelled" },
{ value: WebhookTriggerEvents.BOOKING_CREATED, label: "booking_created" },
{ value: WebhookTriggerEvents.BOOKING_REJECTED, label: "booking_rejected"},
{ value: WebhookTriggerEvents.BOOKING_REQUESTED, label: "booking_requested"},
{ value: WebhookTriggerEvents.BOOKING_RESCHEDULED, label: "booking_rescheduled" },
{ value: WebhookTriggerEvents.MEETING_ENDED, label: "meeting_ended" },
{ value: WebhookTriggerEvents.RECORDING_READY, label: "recording_ready" },

View File

@ -8,6 +8,8 @@ export const WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP = {
WebhookTriggerEvents.BOOKING_CREATED,
WebhookTriggerEvents.BOOKING_RESCHEDULED,
WebhookTriggerEvents.MEETING_ENDED,
WebhookTriggerEvents.BOOKING_REQUESTED,
WebhookTriggerEvents.BOOKING_REJECTED,
WebhookTriggerEvents.RECORDING_READY,
] as const,
"routing-forms": [WebhookTriggerEvents.FORM_SUBMITTED] as const,

View File

@ -16,7 +16,7 @@ export type EventTypeInfo = {
length?: number | null;
};
type WebhookDataType = CalendarEvent &
export type WebhookDataType = CalendarEvent &
EventTypeInfo & {
metadata?: { [key: string]: string };
bookingId?: number;

View File

@ -0,0 +1,3 @@
-- AlterEnum
ALTER TYPE "WebhookTriggerEvents" ADD VALUE 'BOOKING_REQUESTED';
ALTER TYPE "WebhookTriggerEvents" ADD VALUE 'BOOKING_REJECTED';

View File

@ -495,7 +495,9 @@ enum PaymentOption {
enum WebhookTriggerEvents {
BOOKING_CREATED
BOOKING_RESCHEDULED
BOOKING_REQUESTED
BOOKING_CANCELLED
BOOKING_REJECTED
FORM_SUBMITTED
MEETING_ENDED
RECORDING_READY

View File

@ -4,10 +4,12 @@ import appStore from "@calcom/app-store";
import { sendDeclinedEmails } from "@calcom/emails";
import { getCalEventResponses } from "@calcom/features/bookings/lib/getCalEventResponses";
import { handleConfirmation } from "@calcom/features/bookings/lib/handleConfirmation";
import { handleWebhookTrigger } from "@calcom/features/bookings/lib/handleWebhookTrigger";
import type { EventTypeInfo } from "@calcom/features/webhooks/lib/sendPayload";
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
import { getTranslation } from "@calcom/lib/server";
import { prisma } from "@calcom/prisma";
import { BookingStatus, MembershipRole, SchedulingType } from "@calcom/prisma/enums";
import { BookingStatus, MembershipRole, SchedulingType, WebhookTriggerEvents } from "@calcom/prisma/enums";
import type { CalendarEvent } from "@calcom/types/Calendar";
import type { IAbstractPaymentService } from "@calcom/types/PaymentService";
@ -304,6 +306,31 @@ export const confirmHandler = async ({ ctx, input }: ConfirmOptions) => {
}
await sendDeclinedEmails(evt);
// send BOOKING_REJECTED webhooks
const subscriberOptions = {
userId: booking.userId,
eventTypeId: booking.eventTypeId,
triggerEvent: WebhookTriggerEvents.BOOKING_REJECTED,
teamId: booking.eventType?.teamId,
};
const eventTrigger: WebhookTriggerEvents = WebhookTriggerEvents.BOOKING_REJECTED;
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 webhookData = {
...evt,
...eventTypeInfo,
bookingId,
eventTypeId: booking.eventType?.id,
status: BookingStatus.REJECTED,
smsReminderNumber: booking.smsReminderNumber || undefined,
};
await handleWebhookTrigger({ subscriberOptions, eventTrigger, webhookData });
}
const message = "Booking " + confirmed ? "confirmed" : "rejected";