Compare commits
2 Commits
main
...
gh-readonl
Author | SHA1 | Date | |
---|---|---|---|
ba5368ea64 | |||
d2933b0b8f |
|
@ -318,6 +318,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||
<DialogClose />
|
||||
<Button
|
||||
disabled={mutation.isLoading}
|
||||
data-testid="rejection-confirm"
|
||||
onClick={() => {
|
||||
bookingConfirm(false);
|
||||
}}>
|
||||
|
|
|
@ -67,12 +67,14 @@ const AppearanceView = () => {
|
|||
|
||||
const {
|
||||
formState: { isSubmitting, isDirty },
|
||||
reset,
|
||||
} = formMethods;
|
||||
|
||||
const mutation = trpc.viewer.updateProfile.useMutation({
|
||||
onSuccess: async () => {
|
||||
onSuccess: async (data) => {
|
||||
await utils.viewer.me.invalidate();
|
||||
showToast(t("settings_updated_successfully"), "success");
|
||||
reset(data);
|
||||
},
|
||||
onError: () => {
|
||||
showToast(t("error_updating_settings"), "error");
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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" },
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -16,7 +16,7 @@ export type EventTypeInfo = {
|
|||
length?: number | null;
|
||||
};
|
||||
|
||||
type WebhookDataType = CalendarEvent &
|
||||
export type WebhookDataType = CalendarEvent &
|
||||
EventTypeInfo & {
|
||||
metadata?: { [key: string]: string };
|
||||
bookingId?: number;
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
-- AlterEnum
|
||||
ALTER TYPE "WebhookTriggerEvents" ADD VALUE 'BOOKING_REQUESTED';
|
||||
ALTER TYPE "WebhookTriggerEvents" ADD VALUE 'BOOKING_REJECTED';
|
|
@ -495,7 +495,9 @@ enum PaymentOption {
|
|||
enum WebhookTriggerEvents {
|
||||
BOOKING_CREATED
|
||||
BOOKING_RESCHEDULED
|
||||
BOOKING_REQUESTED
|
||||
BOOKING_CANCELLED
|
||||
BOOKING_REJECTED
|
||||
FORM_SUBMITTED
|
||||
MEETING_ENDED
|
||||
RECORDING_READY
|
||||
|
|
|
@ -137,4 +137,5 @@ export const updateProfileHandler = async ({ ctx, input }: UpdateProfileOptions)
|
|||
.then(() => console.info("Booking pages revalidated"))
|
||||
.catch((e) => console.error(e));
|
||||
}
|
||||
return input;
|
||||
};
|
||||
|
|
|
@ -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";
|
||||
|
|
Loading…
Reference in New Issue
Block a user