Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/web/public/static/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -3677,6 +3677,8 @@
"webhook_organizer_email": "Email of the organizer",
"webhook_organizer_timezone": "Timezone of the organizer (e.g., 'America/New_York', 'Asia/Kolkata')",
"webhook_organizer_locale": "Locale of the organizer (e.g., 'en', 'fr')",
"webhook_organizer_username": "Username of the organizer (e.g., 'john.doe')",
"webhook_organizer_username_in_org": "Username of the organizer in their organization (e.g., 'john.doe')",
"webhook_attendee_name": "Name of the first attendee",
"webhook_attendee_email": "Email of the first attendee",
"webhook_attendee_timezone": "Timezone of the first attendee",
Expand Down
359 changes: 191 additions & 168 deletions apps/web/test/utils/bookingScenario/bookingScenario.ts

Large diffs are not rendered by default.

27 changes: 20 additions & 7 deletions apps/web/test/utils/bookingScenario/expects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,13 +306,13 @@ export function expectWebhookToHaveBeenCalledWith(

if (parsedBody.payload) {
if (data.payload) {
if (!!data.payload.metadata) {
if (data.payload.metadata) {
expect(parsedBody.payload.metadata).toEqual(expect.objectContaining(data.payload.metadata));
}
if (!!data.payload.responses)
if (data.payload.responses)
expect(parsedBody.payload.responses).toEqual(expect.objectContaining(data.payload.responses));

if (!!data.payload.organizer)
if (data.payload.organizer)
expect(parsedBody.payload.organizer).toEqual(expect.objectContaining(data.payload.organizer));

const { responses: _1, metadata: _2, organizer: _3, ...remainingPayload } = data.payload;
Expand Down Expand Up @@ -1031,6 +1031,7 @@ export function expectBookingRequestedWebhookToHaveBeenFired({
}

export function expectBookingCreatedWebhookToHaveBeenFired({
organizer,
booker,
location,
subscriberUrl,
Expand All @@ -1039,7 +1040,7 @@ export function expectBookingCreatedWebhookToHaveBeenFired({
isEmailHidden = false,
isAttendeePhoneNumberHidden = false,
}: {
organizer: { email: string; name: string };
organizer: { email: string; name: string; username?: string; usernameInOrg?: string };
booker: { email: string; name: string; attendeePhoneNumber?: string };
subscriberUrl: string;
location: string;
Expand All @@ -1048,6 +1049,11 @@ export function expectBookingCreatedWebhookToHaveBeenFired({
isEmailHidden?: boolean;
isAttendeePhoneNumberHidden?: boolean;
}) {
const organizerPayload = {
username: organizer.username,
...(organizer.usernameInOrg ? { usernameInOrg: organizer.usernameInOrg } : null),
};

if (!paidEvent) {
expectWebhookToHaveBeenCalledWith(subscriberUrl, {
triggerEvent: "BOOKING_CREATED",
Expand All @@ -1073,6 +1079,7 @@ export function expectBookingCreatedWebhookToHaveBeenFired({
isHidden: false,
},
},
organizer: organizerPayload,
},
});
} else {
Expand Down Expand Up @@ -1103,6 +1110,7 @@ export function expectBookingCreatedWebhookToHaveBeenFired({
value: { optionValue: "", value: location },
},
},
organizer: organizerPayload,
},
});
}
Expand Down Expand Up @@ -1144,21 +1152,28 @@ export function expectBookingRescheduledWebhookToHaveBeenFired({
}

export function expectBookingCancelledWebhookToHaveBeenFired({
organizer,
booker,
location,
subscriberUrl,
payload,
}: {
organizer: { email: string; name: string };
organizer: { email: string; name: string; username?: string; usernameInOrg?: string };
booker: { email: string; name: string };
subscriberUrl: string;
location: string;
payload?: Record<string, unknown>;
}) {
const organizerPayload = {
username: organizer.username,
...(organizer.usernameInOrg ? { usernameInOrg: organizer.usernameInOrg } : null),
};

expectWebhookToHaveBeenCalledWith(subscriberUrl, {
triggerEvent: "BOOKING_CANCELLED",
payload: {
...payload,
organizer: organizerPayload,
metadata: null,
responses: {
name: {
Expand Down Expand Up @@ -1387,14 +1402,12 @@ export function expectSuccessfulVideoMeetingDeletionInCalendar(
export async function expectBookingInDBToBeRescheduledFromTo({ from, to }: { from: any; to: any }) {
// Expect previous booking to be cancelled
await expectBookingToBeInDatabase({
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
...from,
status: BookingStatus.CANCELLED,
});

// Expect new booking to be created but status would depend on whether the new booking requires confirmation or not.
await expectBookingToBeInDatabase({
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
...to,
});
}
Expand Down
2 changes: 2 additions & 0 deletions packages/features/CalendarEventBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ export class CalendarEventBuilder {
name: string | null;
email: string;
username?: string;
usernameInOrg?: string;
timeZone: string;
timeFormat?: TimeFormat;
language: {
Expand All @@ -95,6 +96,7 @@ export class CalendarEventBuilder {
name: organizer.name || "Nameless",
email: organizer.email,
username: organizer.username,
usernameInOrg: organizer.usernameInOrg,
timeZone: organizer.timeZone,
language: organizer.language,
timeFormat: organizer.timeFormat,
Expand Down
1 change: 1 addition & 0 deletions packages/features/bookings/lib/handleCancelBooking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ async function handler(input: CancelBookingInput) {
organizer: {
id: organizer.id,
username: organizer.username || undefined,
usernameInOrg: ownerProfile?.username || undefined,
email: bookingToDelete?.userPrimaryEmail ?? organizer.email,
name: organizer.name ?? "Nameless",
timeZone: organizer.timeZone,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -384,13 +384,6 @@ describe("Cancel Booking", () => {
],
},
],
teams: [
{
id: 1,
name: "Test Team",
slug: "test-team",
},
],
users: [organizer, hostAttendee],
apps: [TestData.apps["daily-video"]],
})
Expand Down Expand Up @@ -786,13 +779,6 @@ describe("Cancel Booking", () => {
paymentOption: "HOLD",
},
],
teams: [
{
id: 1,
name: "Test Team",
slug: "test-team",
},
],
users: [organizer, teamMember],
apps: [TestData.apps["daily-video"]],
})
Expand Down Expand Up @@ -831,7 +817,6 @@ describe("Cancel Booking", () => {
const booker = getBooker({
email: "booker@example.com",
name: "Booker",
id: 999,
});

const organizer = getOrganizer({
Expand Down Expand Up @@ -942,4 +927,130 @@ describe("Cancel Booking", () => {

expect(result.success).toBe(true);
});

test("Should trigger BOOKING_CANCELLED webhook with username and usernameInOrg for organization bookings", async () => {
const handleCancelBooking = (await import("@calcom/features/bookings/lib/handleCancelBooking")).default;

const booker = getBooker({
email: "booker@example.com",
name: "Booker",
});

const organizer = getOrganizer({
name: "Organizer",
email: "organizer@example.com",
id: 101,
username: "organizer-username",
schedules: [TestData.schedules.IstWorkHours],
credentials: [getGoogleCalendarCredential()],
selectedCalendars: [TestData.selectedCalendars.google],
});

const uidOfBookingToBeCancelled = "org-booking-uid";
const idOfBookingToBeCancelled = 5080;
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });

await createBookingScenario(
getScenarioData(
{
webhooks: [
{
userId: organizer.id,
eventTriggers: ["BOOKING_CANCELLED"],
subscriberUrl: "http://my-webhook.example.com",
active: true,
eventTypeId: 1,
appId: null,
},
],
eventTypes: [
{
id: 1,
slotInterval: 30,
length: 30,
users: [
{
id: 101,
},
],
},
],
bookings: [
{
id: idOfBookingToBeCancelled,
uid: uidOfBookingToBeCancelled,
attendees: [
{
email: booker.email,
},
],
eventTypeId: 1,
userId: 101,
responses: {
email: booker.email,
name: booker.name,
location: { optionValue: "", value: BookingLocations.CalVideo },
},
status: BookingStatus.ACCEPTED,
startTime: `${plus1DateString}T05:00:00.000Z`,
endTime: `${plus1DateString}T05:15:00.000Z`,
metadata: {
videoCallUrl: "https://existing-daily-video-call-url.example.com",
},
},
],
organizer,
apps: [TestData.apps["daily-video"]],
},
{
id: 1,
profileUsername: "username-in-org",
}
)
);

mockSuccessfulVideoMeetingCreation({
metadataLookupKey: "dailyvideo",
videoMeetingData: {
id: "MOCK_ID",
password: "MOCK_PASS",
url: `http://mock-dailyvideo.example.com/meeting-org`,
},
});

mockCalendarToHaveNoBusySlots("googlecalendar", {
create: {
id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID_ORG",
},
});

await handleCancelBooking({
bookingData: {
id: idOfBookingToBeCancelled,
uid: uidOfBookingToBeCancelled,
cancelledBy: organizer.email,
cancellationReason: "Organization booking cancellation test",
},
});

expectBookingCancelledWebhookToHaveBeenFired({
booker,
organizer: {
...organizer,
usernameInOrg: "username-in-org",
},
location: BookingLocations.CalVideo,
subscriberUrl: "http://my-webhook.example.com",
payload: {
cancelledBy: organizer.email,
organizer: {
id: organizer.id,
username: organizer.username,
email: organizer.email,
name: organizer.name,
timeZone: organizer.timeZone,
},
},
});
});
});
2 changes: 1 addition & 1 deletion packages/features/bookings/lib/handleNewBooking.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1208,7 +1208,6 @@ async function handler(
const organizerOrganizationProfile = await prisma.profile.findFirst({
where: {
userId: organizerUser.id,
username: dynamicUserList[0],
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important change. For team events, organizerOrganizationProfile would always be null because dynamicUserList[0] would be teamSlug.

This never caused any problem because, for team events we were using different way to determine bookingUrl.

Now I need to use organizerOrganizationProfile to detemine the username correctly for team as well as user events.

Image

},
});

Expand Down Expand Up @@ -1268,6 +1267,7 @@ async function handler(
name: organizerUser.name || "Nameless",
email: organizerEmail,
username: organizerUser.username || undefined,
usernameInOrg: organizerOrganizationProfile?.username || undefined,
timeZone: organizerUser.timeZone,
language: { translate: tOrganizer, locale: organizerUser.locale ?? "en" },
timeFormat: getTimeFormatStringFromUserTimeFormat(organizerUser.timeFormat),
Expand Down
Loading
Loading