From d9425a31770d9de305188fc31c8e4c652c7568bf Mon Sep 17 00:00:00 2001 From: Hariom Date: Fri, 17 Jan 2025 12:28:51 +0530 Subject: [PATCH] Make Google Meet default when DWD for google is enabled --- .../domain-wide-delegation-TODO.md | 1 + .../domain-wide-delegation.md | 5 +- .../features/bookings/lib/handleNewBooking.ts | 20 ++- .../getLocationValuesForDb.ts | 56 ++++-- .../test/domain-wide-delegation.test.ts | 168 +++++++++++++++++- packages/lib/domainWideDelegation/server.ts | 20 +++ 6 files changed, 243 insertions(+), 27 deletions(-) diff --git a/apps/web/app/settings/(settings-layout)/organizations/domain-wide-delegation/domain-wide-delegation-TODO.md b/apps/web/app/settings/(settings-layout)/organizations/domain-wide-delegation/domain-wide-delegation-TODO.md index 93d2e1cc87a2c4..bca5003f1f04df 100644 --- a/apps/web/app/settings/(settings-layout)/organizations/domain-wide-delegation/domain-wide-delegation-TODO.md +++ b/apps/web/app/settings/(settings-layout)/organizations/domain-wide-delegation/domain-wide-delegation-TODO.md @@ -94,6 +94,7 @@ ### Follow-up release - [ ] Confirmation for DwD toggling off - [ ] Confirmation for DwD deletion - Deletion isn't there at the moment. + - [ ] Make Google Meet "show up" as default conferencing app when DWD for Google is enabled. - [ ] Profile pic from Google with DWD might not be working - Fix it. ### Security diff --git a/apps/web/app/settings/(settings-layout)/organizations/domain-wide-delegation/domain-wide-delegation.md b/apps/web/app/settings/(settings-layout)/organizations/domain-wide-delegation/domain-wide-delegation.md index 425dc46ed0ff90..157162ee5b5a2c 100644 --- a/apps/web/app/settings/(settings-layout)/organizations/domain-wide-delegation/domain-wide-delegation.md +++ b/apps/web/app/settings/(settings-layout)/organizations/domain-wide-delegation/domain-wide-delegation.md @@ -41,9 +41,10 @@ Last Step (To Be Taken By Cal.com organization Owner/Admin): Assign Specific API ## Onboarding Improvement -- Just adding a member to the organization would allow the member to receive events in their calendar, even if they don't login to their account and complete the onboarding process. +- Just adding a member to the organization would do the following: + - Member to receive events in their calendar, even if they don't login to their account and complete the onboarding process. + - The booking location would be Google Meet, even if the user hasn't set it as default(Though Cal Video would show up as default, but we still use Google Meet in this case. We will fix it later.) - It would still not use their calendar for conflict checking, but user can complete the onboarding(just select one calendar there for conflict checking) - - It would still use CalVideo as the default location as the user hasn't confirmed their preferred conferencing app as Google Meet. They could do that in the onboarding process by just clicking 'Set Default' - Onboarding process: Google Calendar is pre-installed for any new member of the organization(assuming the user has an email of the DWD domain) and Destination Calendar and Selected Calendar are configurable. On next step, Google Meet is pre-installed and shown at the top and could be set as default. ## Restrictions after enabling DWD diff --git a/packages/features/bookings/lib/handleNewBooking.ts b/packages/features/bookings/lib/handleNewBooking.ts index 59928a6239f6e4..2bf7bc8e86e60e 100644 --- a/packages/features/bookings/lib/handleNewBooking.ts +++ b/packages/features/bookings/lib/handleNewBooking.ts @@ -50,6 +50,7 @@ import { import { getVideoCallUrlFromCalEvent } from "@calcom/lib/CalEventParser"; import { isRerouting, shouldIgnoreContactOwner } from "@calcom/lib/bookings/routing/utils"; import { getDefaultEvent, getUsernameList } from "@calcom/lib/defaultEvents"; +import { getFirstDwdConferencingCredentialAppLocation } from "@calcom/lib/domainWideDelegation/server"; import { enrichHostsWithDwdCredentials, enrichUsersWithDwdCredentials, @@ -505,12 +506,12 @@ async function handler( isSameHostReschedule: !!(eventType.rescheduleWithSameRoundRobinHost && reqBody.rescheduleUid), }); - const firstUser: (typeof allHostUsersWithoutHavingDwdCredentials)[number] | undefined = + const firstUserWithoutDwdCredentials: (typeof allHostUsersWithoutHavingDwdCredentials)[number] | undefined = allHostUsersWithoutHavingDwdCredentials[0]; // We use first user's org ID assuming that each and every member would be within the same organization. const firstUserOrgId = await getOrgIdFromMemberOrTeamId({ - memberId: firstUser?.id ?? null, + memberId: firstUserWithoutDwdCredentials?.id ?? null, teamId: eventType.teamId, }); @@ -521,12 +522,12 @@ async function handler( // We filter out users but ensure allHostUsers remain same. let users = allHostUsers; - - let { locationBodyString, organizerOrFirstDynamicGroupMemberDefaultLocationUrl } = getLocationValuesForDb( + const firstUser = users[0]; + let { locationBodyString, organizerOrFirstDynamicGroupMemberDefaultLocationUrl } = getLocationValuesForDb({ dynamicUserList, users, - location - ); + location, + }); await monitorCallbackAsync(checkBookingAndDurationLimits, { eventType, @@ -819,6 +820,11 @@ async function handler( locationBodyString = OrganizerDefaultConferencingAppType; } } + + const organizationDefaultLocation = getFirstDwdConferencingCredentialAppLocation({ + credentials: firstUser.credentials, + }); + // use host default if (locationBodyString == OrganizerDefaultConferencingAppType) { const metadataParseResult = userMetadataSchema.safeParse(organizerUser.metadata); @@ -830,6 +836,8 @@ async function handler( organizerOrFirstDynamicGroupMemberDefaultLocationUrl = organizerMetadata?.defaultConferencingApp?.appLink; } + } else if (organizationDefaultLocation) { + locationBodyString = organizationDefaultLocation; } else { locationBodyString = "integrations:daily"; } diff --git a/packages/features/bookings/lib/handleNewBooking/getLocationValuesForDb.ts b/packages/features/bookings/lib/handleNewBooking/getLocationValuesForDb.ts index 5efa56e02feb80..1d741a6f50c7fd 100644 --- a/packages/features/bookings/lib/handleNewBooking/getLocationValuesForDb.ts +++ b/packages/features/bookings/lib/handleNewBooking/getLocationValuesForDb.ts @@ -1,10 +1,10 @@ +import { getFirstDwdConferencingCredentialAppLocation } from "@calcom/lib/domainWideDelegation/server"; import { userMetadata as userMetadataSchema } from "@calcom/prisma/zod-utils"; -import type { loadAndValidateUsers } from "./loadAndValidateUsers"; - -type Users = Awaited>; - -const sortUsersByDynamicList = (users: Users, dynamicUserList: string[]) => { +const sortUsersByDynamicList = ( + users: TUser[], + dynamicUserList: string[] +) => { return users.sort((a, b) => { const aIndex = (a.username && dynamicUserList.indexOf(a.username)) || 0; const bIndex = (b.username && dynamicUserList.indexOf(b.username)) || 0; @@ -12,19 +12,41 @@ const sortUsersByDynamicList = (users: Users, dynamicUserList: string[]) => { }); }; -export const getLocationValuesForDb = ( - dynamicUserList: string[], - users: Users, - locationBodyString: string -) => { +export const getLocationValuesForDb = < + TUser extends { + username: string | null; + metadata: Prisma.JsonValue; + credentials: CredentialForCalendarService[]; + } +>({ + dynamicUserList, + users, + location: locationBodyString, +}: { + dynamicUserList: string[]; + users: TUser[]; + location: string; +}) => { + const isDynamicGroupBookingCase = dynamicUserList.length > 1; + let firstDynamicGroupMemberDefaultLocationUrl; // TODO: It's definition should be moved to getLocationValueForDb - let organizerOrFirstDynamicGroupMemberDefaultLocationUrl; - if (dynamicUserList.length > 1) { + if (isDynamicGroupBookingCase) { users = sortUsersByDynamicList(users, dynamicUserList); - const firstUsersMetadata = userMetadataSchema.parse(users[0].metadata); - locationBodyString = firstUsersMetadata?.defaultConferencingApp?.appLink || locationBodyString; - organizerOrFirstDynamicGroupMemberDefaultLocationUrl = - firstUsersMetadata?.defaultConferencingApp?.appLink; + const firstDynamicGroupMember = users[0]; + const firstDynamicGroupMemberMetadata = userMetadataSchema.parse(firstDynamicGroupMember.metadata); + const firstDynamicGroupMemberDwdConferencingAppLocation = getFirstDwdConferencingCredentialAppLocation({ + credentials: firstDynamicGroupMember.credentials, + }); + + firstDynamicGroupMemberDefaultLocationUrl = + firstDynamicGroupMemberMetadata?.defaultConferencingApp?.appLink || + firstDynamicGroupMemberDwdConferencingAppLocation; + + locationBodyString = firstDynamicGroupMemberDefaultLocationUrl || locationBodyString; } - return { locationBodyString, organizerOrFirstDynamicGroupMemberDefaultLocationUrl }; + + return { + locationBodyString, + organizerOrFirstDynamicGroupMemberDefaultLocationUrl: firstDynamicGroupMemberDefaultLocationUrl, + }; }; diff --git a/packages/features/bookings/lib/handleNewBooking/test/domain-wide-delegation.test.ts b/packages/features/bookings/lib/handleNewBooking/test/domain-wide-delegation.test.ts index 93843fa39250f9..802bd0f13601db 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/domain-wide-delegation.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/domain-wide-delegation.test.ts @@ -10,6 +10,7 @@ import { createBookingScenario, TestData, + getDate, getOrganizer, getBooker, getScenarioData, @@ -31,7 +32,10 @@ import { expectBookingToNotHaveReference, expectNoAttemptToCreateCalendarEvent, } from "@calcom/web/test/utils/bookingScenario/expects"; -import { getMockRequestDataForBooking } from "@calcom/web/test/utils/bookingScenario/getMockRequestDataForBooking"; +import { + getMockRequestDataForBooking, + getMockRequestDataForDynamicGroupBooking, +} from "@calcom/web/test/utils/bookingScenario/getMockRequestDataForBooking"; import { setupAndTeardown } from "@calcom/web/test/utils/bookingScenario/setupAndTeardown"; import { describe, expect } from "vitest"; @@ -51,6 +55,7 @@ describe("handleNewBooking", () => { `should create a successful booking using the domain wide delegation credential 1. Should create a booking in the database with reference having DWD credential 2. Should create an event in calendar with DWD credential + 3. Should use Google Meet as the location even when not explicitly set. `, async ({ emails }) => { const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default; @@ -97,7 +102,6 @@ describe("handleNewBooking", () => { id: 1, slotInterval: 30, length: 30, - location: BookingLocations.GoogleMeet, users: [ { id: organizer.id, @@ -394,5 +398,165 @@ describe("handleNewBooking", () => { }, timeout ); + + test( + `should create a successful dynamic group booking using the domain wide delegation credential + 1. Should create a booking in the database with reference having DWD credential + 2. Should create an event in calendar with DWD credential for both users + 3. Should use Google Meet as the location even when not explicitly set. +`, + async () => { + const handleNewBooking = (await import("@calcom/features/bookings/lib/handleNewBooking")).default; + + const org = await createOrganization({ + name: "Test Org", + slug: "testorg", + }); + + const payloadToMakePartOfOrganization = [ + { + membership: { + accepted: true, + role: MembershipRole.ADMIN, + }, + team: { + id: org.id, + name: "Test Org", + slug: "testorg", + }, + }, + ]; + + const booker = getBooker({ + email: "booker@example.com", + name: "Booker", + }); + + const groupUser1 = getOrganizer({ + name: "group-user-1", + username: "group-user-1", + email: "group-user-1@example.com", + id: 101, + schedules: [TestData.schedules.IstWorkHours], + selectedCalendars: [TestData.selectedCalendars.google], + teams: payloadToMakePartOfOrganization, + credentials: [], + destinationCalendar: [TestData.selectedCalendars.google], + }); + + const groupUser2 = getOrganizer({ + name: "group-user-2", + username: "group-user-2", + email: "group-user-2@example.com", + id: 102, + schedules: [TestData.schedules.IstWorkHours], + selectedCalendars: [TestData.selectedCalendars.google], + teams: payloadToMakePartOfOrganization, + credentials: [], + destinationCalendar: [TestData.selectedCalendars.google], + }); + + const dwd = await createDwdCredential(org.id); + + await createBookingScenario( + getScenarioData({ + webhooks: [ + { + userId: groupUser1.id, + eventTriggers: ["BOOKING_CREATED"], + subscriberUrl: "http://my-webhook.example.com", + active: true, + eventTypeId: 0, + appId: null, + }, + ], + workflows: [ + { + userId: groupUser1.id, + trigger: "NEW_EVENT", + action: "EMAIL_HOST", + template: "REMINDER", + activeOn: [0], + }, + ], + eventTypes: [], + users: [groupUser1, groupUser2], + apps: [TestData.apps["daily-video"], TestData.apps["google-calendar"]], + }) + ); + + // Mock a Scenario where iCalUID isn't returned by Google Calendar in which case booking UID is used as the ics UID + const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", { + create: { + id: "GOOGLE_CALENDAR_EVENT_ID", + uid: "MOCK_ID", + appSpecificData: { + googleCalendar: { + hangoutLink: "https://GOOGLE_MEET_URL_IN_CALENDAR_EVENT", + }, + }, + }, + }); + + const { dateString: plus1DateString } = getDate({ dateIncrement: 1 }); + + const mockBookingData = getMockRequestDataForDynamicGroupBooking({ + data: { + start: `${plus1DateString}T05:00:00.000Z`, + end: `${plus1DateString}T05:30:00.000Z`, + eventTypeId: 0, + eventTypeSlug: "group-user-1+group-user-2", + user: "group-user-1+group-user-2", + responses: { + email: booker.email, + name: booker.name, + // There is no location option during booking for Dynamic Group Bookings + // location: { optionValue: "", value: BookingLocations.CalVideo }, + }, + }, + }); + + const { req } = createMockNextJsRequest({ + method: "POST", + body: mockBookingData, + }); + + const createdBooking = await handleNewBooking(req); + + expect(createdBooking.responses).toEqual( + expect.objectContaining({ + email: booker.email, + name: booker.name, + }) + ); + + expect(createdBooking).toEqual( + expect.objectContaining({ + location: BookingLocations.GoogleMeet, + }) + ); + + await expectBookingToBeInDatabase({ + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + uid: createdBooking.uid!, + eventTypeId: null, + status: BookingStatus.ACCEPTED, + location: BookingLocations.GoogleMeet, + references: [ + { + type: appStoreMetadata.googlecalendar.type, + uid: "GOOGLE_CALENDAR_EVENT_ID", + meetingId: "GOOGLE_CALENDAR_EVENT_ID", + meetingPassword: "MOCK_PASSWORD", + meetingUrl: "https://GOOGLE_MEET_URL_IN_CALENDAR_EVENT", + // Verify DWD credential was used + domainWideDelegationCredentialId: dwd.id, + }, + ], + iCalUID: createdBooking.iCalUID, + }); + }, + timeout + ); }); }); diff --git a/packages/lib/domainWideDelegation/server.ts b/packages/lib/domainWideDelegation/server.ts index 278925d0daf465..889b47ecd6a099 100644 --- a/packages/lib/domainWideDelegation/server.ts +++ b/packages/lib/domainWideDelegation/server.ts @@ -395,3 +395,23 @@ export function getDwdOrRegularCredential _isConferencingCredential(credential)); +} + +export function getFirstDwdConferencingCredentialAppLocation({ + credentials, +}: { + credentials: CredentialForCalendarService[]; +}) { + const dwdConferencingCredential = getFirstDwdConferencingCredential({ credentials }); + if (dwdConferencingCredential?.appId === googleMeetMetadata.slug) { + return googleMeetMetadata.appData.location.type; + } + return null; +}