From ca8ee749a2238259877418ddb9aa2a03744a2708 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Omar=20L=C3=B3pez?= Date: Thu, 24 Apr 2025 19:33:55 -0300 Subject: [PATCH] fix: double end-date parsing when saving availability (#20963) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: double end-date parsing when saving availablity Signed-off-by: Omar López * fix: cache hits in current and next month * Update CalendarService.test.ts --------- Signed-off-by: Omar López --- .../lib/CalendarService.test.ts | 5 ++- .../googlecalendar/lib/CalendarService.ts | 12 ++++-- .../calendar-cache.repository.ts | 5 +-- .../calendar-cache/lib/datesForCache.test.ts | 24 ++++++++++- .../calendar-cache/lib/datesForCache.ts | 41 ++++++++----------- packages/lib/CalendarManager.ts | 2 +- 6 files changed, 54 insertions(+), 35 deletions(-) diff --git a/packages/app-store/googlecalendar/lib/CalendarService.test.ts b/packages/app-store/googlecalendar/lib/CalendarService.test.ts index e8aa729f8e..059d11dcb7 100644 --- a/packages/app-store/googlecalendar/lib/CalendarService.test.ts +++ b/packages/app-store/googlecalendar/lib/CalendarService.test.ts @@ -7,6 +7,7 @@ import { expect, test, beforeEach, vi, describe } from "vitest"; import "vitest-fetch-mock"; import { CalendarCache } from "@calcom/features/calendar-cache/calendar-cache"; +import { getTimeMin, getTimeMax } from "@calcom/features/calendar-cache/lib/datesForCache"; import { SelectedCalendarRepository } from "@calcom/lib/server/repository/selectedCalendar"; import type { CredentialForCalendarServiceWithEmail } from "@calcom/types/Credential"; @@ -187,8 +188,8 @@ test("Calendar Cache is being read on cache HIT", async () => { await calendarCache.upsertCachedAvailability( credentialInDb1.id, { - timeMin: dateFrom1, - timeMax: dateTo1, + timeMin: getTimeMin(dateFrom1), + timeMax: getTimeMax(dateTo1), items: [{ id: testSelectedCalendar.externalId }], }, JSON.parse( diff --git a/packages/app-store/googlecalendar/lib/CalendarService.ts b/packages/app-store/googlecalendar/lib/CalendarService.ts index 42ee69c6d9..04fdb4345d 100644 --- a/packages/app-store/googlecalendar/lib/CalendarService.ts +++ b/packages/app-store/googlecalendar/lib/CalendarService.ts @@ -663,7 +663,13 @@ export default class GoogleCalendarService implements Calendar { ): Promise { if (shouldServeCache === false) return await this.fetchAvailability(args); const calendarCache = await CalendarCache.init(null); - const cached = await calendarCache.getCachedAvailability(this.credential.id, args); + const cached = await calendarCache.getCachedAvailability(this.credential.id, { + // Expand the start date to the start of the month to increase cache hits + timeMin: getTimeMin(args.timeMin), + // Expand the end date to the end of the month to increase cache hits + timeMax: getTimeMax(args.timeMax), + items: args.items, + }); if (cached) return cached.value as unknown as calendar_v3.Schema$FreeBusyResponse; return await this.fetchAvailability(args); } @@ -1055,9 +1061,9 @@ export default class GoogleCalendarService implements Calendar { for (const [_eventTypeId, selectedCalendars] of Array.from(selectedCalendarsPerEventType.entries())) { const parsedArgs = { - /** Expand the start date to the start of the month */ + /** Expand the start date to the start of the month to increase cache hits */ timeMin: getTimeMin(), - /** Expand the end date to the end of the month */ + /** Expand the end date to the end of the month to increase cache hits */ timeMax: getTimeMax(), // Dont use eventTypeId in key because it can be used by any eventType // The only reason we are building it per eventType is because there can be different groups of calendars to lookup the availability for diff --git a/packages/features/calendar-cache/calendar-cache.repository.ts b/packages/features/calendar-cache/calendar-cache.repository.ts index f58913ea7c..6c941b1c3e 100644 --- a/packages/features/calendar-cache/calendar-cache.repository.ts +++ b/packages/features/calendar-cache/calendar-cache.repository.ts @@ -7,7 +7,6 @@ import prisma from "@calcom/prisma"; import type { Calendar, SelectedCalendarEventTypeIds } from "@calcom/types/Calendar"; import type { ICalendarCacheRepository } from "./calendar-cache.repository.interface"; -import { getTimeMax, getTimeMin } from "./lib/datesForCache"; const log = logger.getSubLogger({ prefix: ["CalendarCacheRepository"] }); @@ -19,8 +18,8 @@ function parseKeyForCache(args: FreeBusyArgs): string { // Ensure that calendarIds are unique const uniqueItems = uniqueBy(args.items, ["id"]); const key = JSON.stringify({ - timeMin: getTimeMin(args.timeMin), - timeMax: getTimeMax(args.timeMax), + timeMin: args.timeMin, + timeMax: args.timeMax, items: uniqueItems, }); return key; diff --git a/packages/features/calendar-cache/lib/datesForCache.test.ts b/packages/features/calendar-cache/lib/datesForCache.test.ts index 0a5a0c9114..58df690296 100644 --- a/packages/features/calendar-cache/lib/datesForCache.test.ts +++ b/packages/features/calendar-cache/lib/datesForCache.test.ts @@ -1,10 +1,11 @@ -import { describe, expect, it } from "vitest"; +import { describe, expect, it, vi } from "vitest"; import { getTimeMin, getTimeMax } from "./datesForCache"; describe("getTimeMin", () => { // Tested on multiple dates - // vi.setSystemTime("2030-04-21T00:00:13Z"); + vi.setSystemTime("2025-04-24T00:00:13Z"); + it("should return start of current month when no date is passed", () => { const result = getTimeMin(); const expected = new Date(); @@ -74,4 +75,23 @@ describe("getTimeMax", () => { const result = getTimeMax(testDate); expect(result).toMatchInlineSnapshot(`"2024-11-01T00:00:00.000Z"`); }); + + it("should handle year changes", () => { + const testDate = "2024-12-24T23:59:59Z"; + const result = getTimeMax(testDate); + expect(result).toMatchInlineSnapshot(`"2025-02-01T00:00:00.000Z"`); + }); + + it("should handle next month", () => { + const testDate = "2025-05-01T02:59:59.999Z"; + const result = getTimeMax(testDate); + expect(result).toMatchInlineSnapshot(`"2025-06-01T00:00:00.000Z"`); + }); + + it("should handle special case where timeMax is more than 2 months from now but less than 3 months", () => { + vi.setSystemTime("2025-04-24T00:00:13Z"); + const testDate = "2025-06-02T23:59:59.999Z"; + const result = getTimeMax(testDate); + expect(result).toMatchInlineSnapshot(`"2025-06-01T00:00:00.000Z"`); + }); }); diff --git a/packages/features/calendar-cache/lib/datesForCache.ts b/packages/features/calendar-cache/lib/datesForCache.ts index acad8ef769..a339fbf23e 100644 --- a/packages/features/calendar-cache/lib/datesForCache.ts +++ b/packages/features/calendar-cache/lib/datesForCache.ts @@ -23,44 +23,37 @@ export function getTimeMax(timeMax?: string) { const currentMonth = now.getUTCMonth(); const currentYear = now.getUTCFullYear(); + // If no date is passed, return start of the month two months from *now*. if (!timeMax) { - // If no date passed, return start of the overnext month from current date const result = new Date(Date.UTC(currentYear, currentMonth + 2, 1)); result.setUTCHours(0, 0, 0, 0); return result.toISOString(); } + // If a date is passed, determine the base month/year for calculation. const date = new Date(timeMax); const dateMonth = date.getUTCMonth(); const dateYear = date.getUTCFullYear(); - // If the date is within current month or next month, return start of overnext month - // Otherwise, return start of overnext month from the date - let targetYear = dateYear; - let targetMonth = dateMonth; + let baseYear = currentYear; + let baseMonth = currentMonth; - // Check if date is within current month or next month - const isWithinCurrentOrNextMonth = - (dateYear === currentYear && dateMonth <= currentMonth + 1) || - (dateYear === currentYear + 1 && currentMonth === 11 && dateMonth === 0); + // Check if date is within the current month or the next two months relative to *now*. + const isWithinCurrentOrNextTwoMonths = + (dateYear === currentYear && dateMonth <= currentMonth + 2) || + (dateYear === currentYear + 1 && + ((currentMonth === 10 && dateMonth === 0) || (currentMonth === 11 && dateMonth <= 1))); - if (isWithinCurrentOrNextMonth) { - // For dates within current month or next month, return start of overnext month - targetMonth = dateMonth + 2; - if (targetMonth > 11) { - targetMonth = 0; - targetYear++; - } - } else { - // For dates beyond overnext month, return start of overnext month from the date - targetMonth = dateMonth + 2; - if (targetMonth > 11) { - targetMonth = 0; - targetYear++; - } + // If the input date is beyond the next two months relative to *now*, + // use the *input date's* month/year as the base. + if (!isWithinCurrentOrNextTwoMonths) { + baseYear = dateYear; + baseMonth = dateMonth; } + // Otherwise, the base remains the current year/month. - const result = new Date(Date.UTC(targetYear, targetMonth, 1)); + // Calculate the start of the month two months after the determined base date. + const result = new Date(Date.UTC(baseYear, baseMonth + 2, 1)); result.setUTCHours(0, 0, 0, 0); return result.toISOString(); } diff --git a/packages/lib/CalendarManager.ts b/packages/lib/CalendarManager.ts index 8074c260c3..ddd988fdd9 100644 --- a/packages/lib/CalendarManager.ts +++ b/packages/lib/CalendarManager.ts @@ -169,7 +169,7 @@ export const getBusyCalendarTimes = async ( // Subtract 11 hours from the start date to avoid problems in UTC- time zones. const startDate = dayjs(dateFrom).subtract(11, "hours").format(); // Add 14 hours from the start date to avoid problems in UTC+ time zones. - const endDate = dayjs(dateTo).endOf("month").add(14, "hours").format(); + const endDate = dayjs(dateTo).add(14, "hours").format(); if (includeTimeZone) { results = await getCalendarsEventsWithTimezones(withCredentials, startDate, endDate, selectedCalendars);