diff --git a/apps/web/test/lib/getSchedule.test.ts b/apps/web/test/lib/getSchedule.test.ts index 1778819241..fa3df7711c 100644 --- a/apps/web/test/lib/getSchedule.test.ts +++ b/apps/web/test/lib/getSchedule.test.ts @@ -630,7 +630,8 @@ describe("getSchedule", () => { ...TestData.users.example, id: 101, schedules: [TestData.schedules.IstWorkHours], - credentials: [], + credentials: [getGoogleCalendarCredential()], + selectedCalendars: [TestData.selectedCalendars.google], }, ], bookings: [ @@ -642,7 +643,7 @@ describe("getSchedule", () => { status: "ACCEPTED" as BookingStatus, }, ], - // apps: [TestData.apps.googleCalendar], + apps: [TestData.apps.googleCalendar], }; await createBookingScenario(scenarioData); diff --git a/packages/core/getBusyTimes.ts b/packages/core/getBusyTimes.ts index fa36a336e9..c239a7c1b5 100644 --- a/packages/core/getBusyTimes.ts +++ b/packages/core/getBusyTimes.ts @@ -144,50 +144,43 @@ export async function getBusyTimes(params: { }); const bookingSeatCountMap: { [x: string]: number } = {}; - // Only take bookings as source of truth when there are no connected calendars - // this is because cancellations downstream aren't reflected in our Booking database. - // TODO: Reflect downstream cancellations in our Booking database and use Booking as - // source of truth. - const busyTimes = - credentials?.length > 0 - ? [] - : bookings.reduce( - (aggregate: EventBusyDetails[], { id, startTime, endTime, eventType, title, ...rest }) => { - if (rest._count?.seatsReferences) { - const bookedAt = `${dayjs(startTime).utc().format()}<>${dayjs(endTime).utc().format()}`; - bookingSeatCountMap[bookedAt] = bookingSeatCountMap[bookedAt] || 0; - bookingSeatCountMap[bookedAt]++; - // Seat references on the current event are non-blocking until the event is fully booked. - if ( - // there are still seats available. - bookingSeatCountMap[bookedAt] < (eventType?.seatsPerTimeSlot || 1) && - // and this is the seated event, other event types should be blocked. - eventTypeId === eventType?.id - ) { - // then we do not add the booking to the busyTimes. - return aggregate; - } - // if it does get blocked at this point; we remove the bookingSeatCountMap entry - // doing this allows using the map later to remove the ranges from calendar busy times. - delete bookingSeatCountMap[bookedAt]; - } - if (rest.uid === rescheduleUid) { - return aggregate; - } - aggregate.push({ - start: dayjs(startTime) - .subtract((eventType?.beforeEventBuffer || 0) + (afterEventBuffer || 0), "minute") - .toDate(), - end: dayjs(endTime) - .add((eventType?.afterEventBuffer || 0) + (beforeEventBuffer || 0), "minute") - .toDate(), - title, - source: `eventType-${eventType?.id}-booking-${id}`, - }); - return aggregate; - }, - [] - ); + const busyTimes = bookings.reduce( + (aggregate: EventBusyDetails[], { id, startTime, endTime, eventType, title, ...rest }) => { + if (rest._count?.seatsReferences) { + const bookedAt = `${dayjs(startTime).utc().format()}<>${dayjs(endTime).utc().format()}`; + bookingSeatCountMap[bookedAt] = bookingSeatCountMap[bookedAt] || 0; + bookingSeatCountMap[bookedAt]++; + // Seat references on the current event are non-blocking until the event is fully booked. + if ( + // there are still seats available. + bookingSeatCountMap[bookedAt] < (eventType?.seatsPerTimeSlot || 1) && + // and this is the seated event, other event types should be blocked. + eventTypeId === eventType?.id + ) { + // then we do not add the booking to the busyTimes. + return aggregate; + } + // if it does get blocked at this point; we remove the bookingSeatCountMap entry + // doing this allows using the map later to remove the ranges from calendar busy times. + delete bookingSeatCountMap[bookedAt]; + } + if (rest.uid === rescheduleUid) { + return aggregate; + } + aggregate.push({ + start: dayjs(startTime) + .subtract((eventType?.beforeEventBuffer || 0) + (afterEventBuffer || 0), "minute") + .toDate(), + end: dayjs(endTime) + .add((eventType?.afterEventBuffer || 0) + (beforeEventBuffer || 0), "minute") + .toDate(), + title, + source: `eventType-${eventType?.id}-booking-${id}`, + }); + return aggregate; + }, + [] + ); logger.debug( `Busy Time from Cal Bookings ${JSON.stringify({ diff --git a/packages/features/bookings/lib/handleNewBooking/test/fresh-booking.test.ts b/packages/features/bookings/lib/handleNewBooking/test/fresh-booking.test.ts index 8857811a4e..faba12aa50 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/fresh-booking.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/fresh-booking.test.ts @@ -2433,7 +2433,13 @@ describe("handleNewBooking", () => { email: "organizer@example.com", id: 101, schedules: [TestData.schedules.IstWorkHours], - credentials: [], + credentials: [getGoogleCalendarCredential()], + selectedCalendars: [TestData.selectedCalendars.google], + destinationCalendar: { + integration: "google_calendar", + externalId: "organizer@google-calendar.com", + primaryEmail: organizerOtherEmail, + }, }); await createBookingScenario( @@ -2449,10 +2455,15 @@ describe("handleNewBooking", () => { id: 101, }, ], + destinationCalendar: { + integration: "google_calendar", + externalId: "event-type-1@google-calendar.com", + primaryEmail: organizerDestinationCalendarEmailOnEventType, + }, }, ], organizer, - apps: [TestData.apps["daily-video"]], + apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]], }) ); @@ -2513,10 +2524,36 @@ describe("handleNewBooking", () => { meetingPassword: "MOCK_PASS", meetingUrl: "http://mock-dailyvideo.example.com/meeting-1", }, + { + type: appStoreMetadata.googlecalendar.type, + uid: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", + meetingId: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", + meetingPassword: "MOCK_PASSWORD", + meetingUrl: "https://UNUSED_URL", + }, ], iCalUID: createdBooking.iCalUID, }); + expectSuccessfulCalendarEventCreationInCalendar(calendarMock, { + calendarId: "event-type-1@google-calendar.com", + videoCallUrl: "http://mock-dailyvideo.example.com/meeting-1", + }); + + const iCalUID = expectICalUIDAsString(createdBooking.iCalUID); + + expectSuccessfulBookingCreationEmails({ + booking: { + uid: createdBooking.uid!, + urlOrigin: WEBSITE_URL, + }, + booker, + organizer, + emails, + iCalUID, + destinationEmail: organizerDestinationCalendarEmailOnEventType, + }); + await expect(async () => await handleNewBooking(req)).rejects.toThrowError( ErrorCode.NoAvailableUsersFound ); diff --git a/packages/features/bookings/lib/handleNewBooking/test/recurring-event.test.ts b/packages/features/bookings/lib/handleNewBooking/test/recurring-event.test.ts index a5ef6a4a54..8587acaaa6 100644 --- a/packages/features/bookings/lib/handleNewBooking/test/recurring-event.test.ts +++ b/packages/features/bookings/lib/handleNewBooking/test/recurring-event.test.ts @@ -266,8 +266,12 @@ describe("handleNewBooking", () => { email: "organizer@example.com", id: 101, schedules: [TestData.schedules.IstWorkHours], - credentials: [], - selectedCalendars: [], + credentials: [getGoogleCalendarCredential()], + selectedCalendars: [TestData.selectedCalendars.google], + destinationCalendar: { + integration: "google_calendar", + externalId: "organizer@google-calendar.com", + }, }); const recurrence = getRecurrence({ @@ -299,6 +303,10 @@ describe("handleNewBooking", () => { id: 101, }, ], + destinationCalendar: { + integration: "google_calendar", + externalId: "event-type-1@google-calendar.com", + }, }, ], bookings: [ @@ -312,7 +320,7 @@ describe("handleNewBooking", () => { }, ], organizer, - apps: [TestData.apps["daily-video"]], + apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]], }) ); @@ -829,7 +837,12 @@ describe("handleNewBooking", () => { ], // Has morning shift with some overlap with morning shift schedules: [TestData.schedules.IstMorningShift], - credentials: [], + credentials: [getGoogleCalendarCredential()], + selectedCalendars: [TestData.selectedCalendars.google], + destinationCalendar: { + integration: TestData.apps["google-calendar"].type, + externalId: "organizer@google-calendar.com", + }, }); const otherTeamMembers = [ @@ -843,7 +856,12 @@ describe("handleNewBooking", () => { id: 102, // Has Evening shift schedules: [TestData.schedules.IstMorningShift], - credentials: [], + credentials: [getGoogleCalendarCredential()], + selectedCalendars: [TestData.selectedCalendars.google], + destinationCalendar: { + integration: TestData.apps["google-calendar"].type, + externalId: "other-team-member-1@google-calendar.com", + }, }, ]; @@ -883,6 +901,10 @@ describe("handleNewBooking", () => { isFixed: true, }, ], + destinationCalendar: { + integration: "google_calendar", + externalId: "event-type-1@google-calendar.com", + }, }, ], bookings: [ @@ -901,7 +923,7 @@ describe("handleNewBooking", () => { ], organizer, usersApartFromOrganizer: otherTeamMembers, - apps: [TestData.apps["daily-video"]], + apps: [TestData.apps["google-calendar"], TestData.apps["daily-video"]], }) ); @@ -914,6 +936,13 @@ describe("handleNewBooking", () => { }, }); + const calendarMock = mockCalendarToHaveNoBusySlots("googlecalendar", { + create: { + id: "MOCKED_GOOGLE_CALENDAR_EVENT_ID", + iCalUID: "MOCKED_GOOGLE_CALENDAR_ICS_ID", + }, + }); + const recurringCountInRequest = 4; const mockBookingData1 = getMockRequestDataForBooking({ data: {