Skip to content

Commit 9c97b2a

Browse files
feat: add usernameInOrg field to webhook organizer payload for organization users (calcom#23246)
* feat: add usernameInOrg field to webhook organizer payload for organization users - Add usernameInOrg field to CalendarEventBuilder organizer interface - Update handleNewBooking to pass organizerOrganizationProfile.username as usernameInOrg - Include usernameInOrg in webhook payload generation (sendPayload.ts) - Add webhook form variable for usernameInOrg with translation - Update Person type to include usernameInOrg field - Add test case for organization user webhook verification - Maintain backward compatibility by keeping existing username field unchanged Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * feat: add usernameInOrg to additional webhook sending locations - Update handleCancelBooking.ts to include usernameInOrg in organizer payload - Update confirm.handler.ts to include usernameInOrg for booking confirmations - Update getBooking.ts to include usernameInOrg in payment-related webhooks - Maintain backward compatibility with existing username field - All webhook sending locations now include organization profile username Co-Authored-By: hariom@cal.com <hariombalhara@gmail.com> * fixes * Add subteam event test for usernameInOrg * fix eslitn issues --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent 1e226a4 commit 9c97b2a

14 files changed

Lines changed: 581 additions & 243 deletions

File tree

apps/web/public/static/locales/en/common.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3677,6 +3677,8 @@
36773677
"webhook_organizer_email": "Email of the organizer",
36783678
"webhook_organizer_timezone": "Timezone of the organizer (e.g., 'America/New_York', 'Asia/Kolkata')",
36793679
"webhook_organizer_locale": "Locale of the organizer (e.g., 'en', 'fr')",
3680+
"webhook_organizer_username": "Username of the organizer (e.g., 'john.doe')",
3681+
"webhook_organizer_username_in_org": "Username of the organizer in their organization (e.g., 'john.doe')",
36803682
"webhook_attendee_name": "Name of the first attendee",
36813683
"webhook_attendee_email": "Email of the first attendee",
36823684
"webhook_attendee_timezone": "Timezone of the first attendee",

apps/web/test/utils/bookingScenario/bookingScenario.ts

Lines changed: 191 additions & 168 deletions
Large diffs are not rendered by default.

apps/web/test/utils/bookingScenario/expects.ts

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -306,13 +306,13 @@ export function expectWebhookToHaveBeenCalledWith(
306306

307307
if (parsedBody.payload) {
308308
if (data.payload) {
309-
if (!!data.payload.metadata) {
309+
if (data.payload.metadata) {
310310
expect(parsedBody.payload.metadata).toEqual(expect.objectContaining(data.payload.metadata));
311311
}
312-
if (!!data.payload.responses)
312+
if (data.payload.responses)
313313
expect(parsedBody.payload.responses).toEqual(expect.objectContaining(data.payload.responses));
314314

315-
if (!!data.payload.organizer)
315+
if (data.payload.organizer)
316316
expect(parsedBody.payload.organizer).toEqual(expect.objectContaining(data.payload.organizer));
317317

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

10331033
export function expectBookingCreatedWebhookToHaveBeenFired({
1034+
organizer,
10341035
booker,
10351036
location,
10361037
subscriberUrl,
@@ -1039,7 +1040,7 @@ export function expectBookingCreatedWebhookToHaveBeenFired({
10391040
isEmailHidden = false,
10401041
isAttendeePhoneNumberHidden = false,
10411042
}: {
1042-
organizer: { email: string; name: string };
1043+
organizer: { email: string; name: string; username?: string; usernameInOrg?: string };
10431044
booker: { email: string; name: string; attendeePhoneNumber?: string };
10441045
subscriberUrl: string;
10451046
location: string;
@@ -1048,6 +1049,11 @@ export function expectBookingCreatedWebhookToHaveBeenFired({
10481049
isEmailHidden?: boolean;
10491050
isAttendeePhoneNumberHidden?: boolean;
10501051
}) {
1052+
const organizerPayload = {
1053+
username: organizer.username,
1054+
...(organizer.usernameInOrg ? { usernameInOrg: organizer.usernameInOrg } : null),
1055+
};
1056+
10511057
if (!paidEvent) {
10521058
expectWebhookToHaveBeenCalledWith(subscriberUrl, {
10531059
triggerEvent: "BOOKING_CREATED",
@@ -1073,6 +1079,7 @@ export function expectBookingCreatedWebhookToHaveBeenFired({
10731079
isHidden: false,
10741080
},
10751081
},
1082+
organizer: organizerPayload,
10761083
},
10771084
});
10781085
} else {
@@ -1103,6 +1110,7 @@ export function expectBookingCreatedWebhookToHaveBeenFired({
11031110
value: { optionValue: "", value: location },
11041111
},
11051112
},
1113+
organizer: organizerPayload,
11061114
},
11071115
});
11081116
}
@@ -1144,21 +1152,28 @@ export function expectBookingRescheduledWebhookToHaveBeenFired({
11441152
}
11451153

11461154
export function expectBookingCancelledWebhookToHaveBeenFired({
1155+
organizer,
11471156
booker,
11481157
location,
11491158
subscriberUrl,
11501159
payload,
11511160
}: {
1152-
organizer: { email: string; name: string };
1161+
organizer: { email: string; name: string; username?: string; usernameInOrg?: string };
11531162
booker: { email: string; name: string };
11541163
subscriberUrl: string;
11551164
location: string;
11561165
payload?: Record<string, unknown>;
11571166
}) {
1167+
const organizerPayload = {
1168+
username: organizer.username,
1169+
...(organizer.usernameInOrg ? { usernameInOrg: organizer.usernameInOrg } : null),
1170+
};
1171+
11581172
expectWebhookToHaveBeenCalledWith(subscriberUrl, {
11591173
triggerEvent: "BOOKING_CANCELLED",
11601174
payload: {
11611175
...payload,
1176+
organizer: organizerPayload,
11621177
metadata: null,
11631178
responses: {
11641179
name: {
@@ -1387,14 +1402,12 @@ export function expectSuccessfulVideoMeetingDeletionInCalendar(
13871402
export async function expectBookingInDBToBeRescheduledFromTo({ from, to }: { from: any; to: any }) {
13881403
// Expect previous booking to be cancelled
13891404
await expectBookingToBeInDatabase({
1390-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
13911405
...from,
13921406
status: BookingStatus.CANCELLED,
13931407
});
13941408

13951409
// Expect new booking to be created but status would depend on whether the new booking requires confirmation or not.
13961410
await expectBookingToBeInDatabase({
1397-
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
13981411
...to,
13991412
});
14001413
}

packages/features/CalendarEventBuilder.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ export class CalendarEventBuilder {
8181
name: string | null;
8282
email: string;
8383
username?: string;
84+
usernameInOrg?: string;
8485
timeZone: string;
8586
timeFormat?: TimeFormat;
8687
language: {
@@ -95,6 +96,7 @@ export class CalendarEventBuilder {
9596
name: organizer.name || "Nameless",
9697
email: organizer.email,
9798
username: organizer.username,
99+
usernameInOrg: organizer.usernameInOrg,
98100
timeZone: organizer.timeZone,
99101
language: organizer.language,
100102
timeFormat: organizer.timeFormat,

packages/features/bookings/lib/handleCancelBooking.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,7 @@ async function handler(input: CancelBookingInput) {
257257
organizer: {
258258
id: organizer.id,
259259
username: organizer.username || undefined,
260+
usernameInOrg: ownerProfile?.username || undefined,
260261
email: bookingToDelete?.userPrimaryEmail ?? organizer.email,
261262
name: organizer.name ?? "Nameless",
262263
timeZone: organizer.timeZone,

packages/features/bookings/lib/handleCancelBooking/test/handleCancelBooking.test.ts

Lines changed: 126 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -384,13 +384,6 @@ describe("Cancel Booking", () => {
384384
],
385385
},
386386
],
387-
teams: [
388-
{
389-
id: 1,
390-
name: "Test Team",
391-
slug: "test-team",
392-
},
393-
],
394387
users: [organizer, hostAttendee],
395388
apps: [TestData.apps["daily-video"]],
396389
})
@@ -786,13 +779,6 @@ describe("Cancel Booking", () => {
786779
paymentOption: "HOLD",
787780
},
788781
],
789-
teams: [
790-
{
791-
id: 1,
792-
name: "Test Team",
793-
slug: "test-team",
794-
},
795-
],
796782
users: [organizer, teamMember],
797783
apps: [TestData.apps["daily-video"]],
798784
})
@@ -831,7 +817,6 @@ describe("Cancel Booking", () => {
831817
const booker = getBooker({
832818
email: "booker@example.com",
833819
name: "Booker",
834-
id: 999,
835820
});
836821

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

943928
expect(result.success).toBe(true);
944929
});
930+
931+
test("Should trigger BOOKING_CANCELLED webhook with username and usernameInOrg for organization bookings", async () => {
932+
const handleCancelBooking = (await import("@calcom/features/bookings/lib/handleCancelBooking")).default;
933+
934+
const booker = getBooker({
935+
email: "booker@example.com",
936+
name: "Booker",
937+
});
938+
939+
const organizer = getOrganizer({
940+
name: "Organizer",
941+
email: "organizer@example.com",
942+
id: 101,
943+
username: "organizer-username",
944+
schedules: [TestData.schedules.IstWorkHours],
945+
credentials: [getGoogleCalendarCredential()],
946+
selectedCalendars: [TestData.selectedCalendars.google],
947+
});
948+
949+
const uidOfBookingToBeCancelled = "org-booking-uid";
950+
const idOfBookingToBeCancelled = 5080;
951+
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 });
952+
953+
await createBookingScenario(
954+
getScenarioData(
955+
{
956+
webhooks: [
957+
{
958+
userId: organizer.id,
959+
eventTriggers: ["BOOKING_CANCELLED"],
960+
subscriberUrl: "http://my-webhook.example.com",
961+
active: true,
962+
eventTypeId: 1,
963+
appId: null,
964+
},
965+
],
966+
eventTypes: [
967+
{
968+
id: 1,
969+
slotInterval: 30,
970+
length: 30,
971+
users: [
972+
{
973+
id: 101,
974+
},
975+
],
976+
},
977+
],
978+
bookings: [
979+
{
980+
id: idOfBookingToBeCancelled,
981+
uid: uidOfBookingToBeCancelled,
982+
attendees: [
983+
{
984+
email: booker.email,
985+
},
986+
],
987+
eventTypeId: 1,
988+
userId: 101,
989+
responses: {
990+
email: booker.email,
991+
name: booker.name,
992+
location: { optionValue: "", value: BookingLocations.CalVideo },
993+
},
994+
status: BookingStatus.ACCEPTED,
995+
startTime: `${plus1DateString}T05:00:00.000Z`,
996+
endTime: `${plus1DateString}T05:15:00.000Z`,
997+
metadata: {
998+
videoCallUrl: "https://existing-daily-video-call-url.example.com",
999+
},
1000+
},
1001+
],
1002+
organizer,
1003+
apps: [TestData.apps["daily-video"]],
1004+
},
1005+
{
1006+
id: 1,
1007+
profileUsername: "username-in-org",
1008+
}
1009+
)
1010+
);
1011+
1012+
mockSuccessfulVideoMeetingCreation({
1013+
metadataLookupKey: "dailyvideo",
1014+
videoMeetingData: {
1015+
id: "MOCK_ID",
1016+
password: "MOCK_PASS",
1017+
url: `http://mock-dailyvideo.example.com/meeting-org`,
1018+
},
1019+
});
1020+
1021+
mockCalendarToHaveNoBusySlots("googlecalendar", {
1022+
create: {
1023+
id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID_ORG",
1024+
},
1025+
});
1026+
1027+
await handleCancelBooking({
1028+
bookingData: {
1029+
id: idOfBookingToBeCancelled,
1030+
uid: uidOfBookingToBeCancelled,
1031+
cancelledBy: organizer.email,
1032+
cancellationReason: "Organization booking cancellation test",
1033+
},
1034+
});
1035+
1036+
expectBookingCancelledWebhookToHaveBeenFired({
1037+
booker,
1038+
organizer: {
1039+
...organizer,
1040+
usernameInOrg: "username-in-org",
1041+
},
1042+
location: BookingLocations.CalVideo,
1043+
subscriberUrl: "http://my-webhook.example.com",
1044+
payload: {
1045+
cancelledBy: organizer.email,
1046+
organizer: {
1047+
id: organizer.id,
1048+
username: organizer.username,
1049+
email: organizer.email,
1050+
name: organizer.name,
1051+
timeZone: organizer.timeZone,
1052+
},
1053+
},
1054+
});
1055+
});
9451056
});

packages/features/bookings/lib/handleNewBooking.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1208,7 +1208,6 @@ async function handler(
12081208
const organizerOrganizationProfile = await prisma.profile.findFirst({
12091209
where: {
12101210
userId: organizerUser.id,
1211-
username: dynamicUserList[0],
12121211
},
12131212
});
12141213

@@ -1268,6 +1267,7 @@ async function handler(
12681267
name: organizerUser.name || "Nameless",
12691268
email: organizerEmail,
12701269
username: organizerUser.username || undefined,
1270+
usernameInOrg: organizerOrganizationProfile?.username || undefined,
12711271
timeZone: organizerUser.timeZone,
12721272
language: { translate: tOrganizer, locale: organizerUser.locale ?? "en" },
12731273
timeFormat: getTimeFormatStringFromUserTimeFormat(organizerUser.timeFormat),

0 commit comments

Comments
 (0)