diff --git a/apps/api/v1/pages/api/slots/_get.ts b/apps/api/v1/pages/api/slots/_get.ts index d0278bbe58..6c402cd9fa 100644 --- a/apps/api/v1/pages/api/slots/_get.ts +++ b/apps/api/v1/pages/api/slots/_get.ts @@ -8,7 +8,7 @@ import { HttpError } from "@calcom/lib/http-error"; import { defaultResponder } from "@calcom/lib/server/defaultResponder"; import { createContext } from "@calcom/trpc/server/createContext"; import { getScheduleSchema } from "@calcom/trpc/server/routers/viewer/slots/types"; -import { getAvailableSlots } from "@calcom/trpc/server/routers/viewer/slots/util"; +import { AvailableSlotsService } from "@calcom/trpc/server/routers/viewer/slots/util"; import { TRPCError } from "@trpc/server"; import { getHTTPStatusCodeFromError } from "@trpc/server/http"; @@ -27,7 +27,11 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { } const input = getScheduleSchema.parse({ usernameList: slugs, isTeamEvent: parsedIsTeamEvent, ...rest }); const timeZoneSupported = input.timeZone ? isSupportedTimeZone(input.timeZone) : false; - const availableSlots = await getAvailableSlots({ ctx: await createContext({ req, res }), input }); + const availableSlotsService = new AvailableSlotsService(); + const availableSlots = await availableSlotsService.getAvailableSlots({ + ctx: await createContext({ req, res }), + input, + }); const slotsInProvidedTimeZone = timeZoneSupported ? Object.keys(availableSlots.slots).reduce( (acc: Record, date) => { diff --git a/apps/api/v2/package.json b/apps/api/v2/package.json index 6fda2e479c..8fcc5fd92f 100644 --- a/apps/api/v2/package.json +++ b/apps/api/v2/package.json @@ -38,7 +38,7 @@ "@axiomhq/winston": "^1.2.0", "@calcom/platform-constants": "*", "@calcom/platform-enums": "*", - "@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.241", + "@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.243", "@calcom/platform-types": "*", "@calcom/platform-utils": "*", "@calcom/prisma": "*", diff --git a/apps/api/v2/src/modules/slots/slots-2024-04-15/controllers/slots.controller.ts b/apps/api/v2/src/modules/slots/slots-2024-04-15/controllers/slots.controller.ts index 7197f25b30..2c4c1f0f12 100644 --- a/apps/api/v2/src/modules/slots/slots-2024-04-15/controllers/slots.controller.ts +++ b/apps/api/v2/src/modules/slots/slots-2024-04-15/controllers/slots.controller.ts @@ -17,7 +17,7 @@ import { VERSION_2024_08_13, } from "@calcom/platform-constants"; import { TRPCError } from "@calcom/platform-libraries"; -import { getAvailableSlots } from "@calcom/platform-libraries/slots"; +import { AvailableSlotsService } from "@calcom/platform-libraries/slots"; import { RemoveSelectedSlotInput_2024_04_15, ReserveSlotInput_2024_04_15 } from "@calcom/platform-types"; import { ApiResponse, GetAvailableSlotsInput_2024_04_15 } from "@calcom/platform-types"; @@ -170,8 +170,10 @@ export class SlotsController_2024_04_15 { : query.isTeamEvent; // Do not use workers in E2E, not supported by TS-JEST - const availableSlots = this.config.get("e2e") - ? await getAvailableSlots({ + const availableSlotsService = this.config.get("e2e") ? new AvailableSlotsService() : null; + + const availableSlots = availableSlotsService + ? await availableSlotsService.getAvailableSlots({ input: { ...query, isTeamEvent, diff --git a/apps/api/v2/src/modules/slots/slots-2024-04-15/workers/slots.worker.ts b/apps/api/v2/src/modules/slots/slots-2024-04-15/workers/slots.worker.ts index ef1f746661..a58e880324 100644 --- a/apps/api/v2/src/modules/slots/slots-2024-04-15/workers/slots.worker.ts +++ b/apps/api/v2/src/modules/slots/slots-2024-04-15/workers/slots.worker.ts @@ -1,11 +1,13 @@ import { parentPort } from "worker_threads"; -import { getAvailableSlots } from "@calcom/platform-libraries/slots"; +import { AvailableSlotsService } from "@calcom/platform-libraries/slots"; parentPort?.on("message", async (data) => { try { const { input, ctx } = data; - const result = await getAvailableSlots({ input, ctx }); + const availableSlotsService = new AvailableSlotsService(); + + const result = await availableSlotsService.getAvailableSlots({ input, ctx }); parentPort?.postMessage({ success: true, data: result }); } catch (error) { parentPort?.postMessage({ diff --git a/apps/api/v2/src/modules/slots/slots-2024-09-04/services/slots.service.ts b/apps/api/v2/src/modules/slots/slots-2024-09-04/services/slots.service.ts index 898f30b85f..80e45c1bb2 100644 --- a/apps/api/v2/src/modules/slots/slots-2024-09-04/services/slots.service.ts +++ b/apps/api/v2/src/modules/slots/slots-2024-09-04/services/slots.service.ts @@ -17,7 +17,7 @@ import { import { DateTime } from "luxon"; import { z } from "zod"; -import { getAvailableSlots } from "@calcom/platform-libraries/slots"; +import { AvailableSlotsService } from "@calcom/platform-libraries/slots"; import { GetSlotsInput_2024_09_04, ReserveSlotInput_2024_09_04 } from "@calcom/platform-types"; import { Booking, EventType } from "@calcom/prisma/client"; @@ -31,6 +31,8 @@ const DEFAULT_RESERVATION_DURATION = 5; @Injectable() export class SlotsService_2024_09_04 { + private readonly availableSlotsService: AvailableSlotsService; + constructor( private readonly eventTypeRepository: EventTypesRepository_2024_06_14, private readonly slotsRepository: SlotsRepository_2024_09_04, @@ -39,12 +41,14 @@ export class SlotsService_2024_09_04 { private readonly membershipsService: MembershipsService, private readonly membershipsRepository: MembershipsRepository, private readonly teamsRepository: TeamsRepository - ) {} + ) { + this.availableSlotsService = new AvailableSlotsService(); + } async getAvailableSlots(query: GetSlotsInput_2024_09_04) { try { const queryTransformed = await this.slotsInputService.transformGetSlotsQuery(query); - const availableSlots: TimeSlots = await getAvailableSlots({ + const availableSlots: TimeSlots = await this.availableSlotsService.getAvailableSlots({ input: { ...queryTransformed, routingFormResponseId: queryTransformed.routingFormResponseId ?? undefined, diff --git a/apps/web/pages/api/get-inbound-dynamic-variables.ts b/apps/web/pages/api/get-inbound-dynamic-variables.ts index 4b94ac5588..ba9fb5b210 100644 --- a/apps/web/pages/api/get-inbound-dynamic-variables.ts +++ b/apps/web/pages/api/get-inbound-dynamic-variables.ts @@ -8,7 +8,7 @@ import type { TGetRetellLLMSchema } from "@calcom/features/ee/cal-ai-phone/zod-u import { fetcher } from "@calcom/lib/retellAIFetcher"; import { defaultHandler } from "@calcom/lib/server/defaultHandler"; import prisma from "@calcom/prisma"; -import { getAvailableSlots } from "@calcom/trpc/server/routers/viewer/slots/util"; +import { AvailableSlotsService } from "@calcom/trpc/server/routers/viewer/slots/util"; dayjs.extend(advancedFormat); @@ -86,8 +86,9 @@ async function handler(req: NextApiRequest, res: NextApiResponse) { const startTime = now.startOf("month").toISOString(); const endTime = now.add(2, "month").endOf("month").toISOString(); const orgSlug = eventType?.team?.parent?.slug ?? null; + const availableSlotsService = new AvailableSlotsService(); - const availableSlots = await getAvailableSlots({ + const availableSlots = await availableSlotsService.getAvailableSlots({ input: { startTime, endTime, diff --git a/apps/web/test/lib/getSchedule.test.ts b/apps/web/test/lib/getSchedule.test.ts index 4317e5a89e..a6360d8684 100644 --- a/apps/web/test/lib/getSchedule.test.ts +++ b/apps/web/test/lib/getSchedule.test.ts @@ -17,7 +17,7 @@ import { describe, vi, test } from "vitest"; import dayjs from "@calcom/dayjs"; import { SchedulingType, type BookingStatus } from "@calcom/prisma/enums"; -import { getAvailableSlots as getSchedule } from "@calcom/trpc/server/routers/viewer/slots/util"; +import { AvailableSlotsService } from "@calcom/trpc/server/routers/viewer/slots/util"; import { expect, expectedSlotsForSchedule } from "./getSchedule/expects"; import { setupAndTeardown } from "./getSchedule/setupAndTeardown"; @@ -31,6 +31,7 @@ vi.mock("@calcom/lib/constants", () => ({ })); describe("getSchedule", () => { + const availableSlotsService = new AvailableSlotsService(); setupAndTeardown(); // TODO: Move these inside describe('Team Event') @@ -113,7 +114,7 @@ describe("getSchedule", () => { bookings: [], }); - const scheduleWithLeadSkip = await getSchedule({ + const scheduleWithLeadSkip = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -134,7 +135,7 @@ describe("getSchedule", () => { } ); - const scheduleWithoutLeadSkip = await getSchedule({ + const scheduleWithoutLeadSkip = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -262,7 +263,7 @@ describe("getSchedule", () => { bookings: [], }); - const scheduleFixedHostLead = await getSchedule({ + const scheduleFixedHostLead = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -291,7 +292,7 @@ describe("getSchedule", () => { } ); - const scheduleRRHostLead = await getSchedule({ + const scheduleRRHostLead = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -392,7 +393,7 @@ describe("getSchedule", () => { bookings: [], }); - const scheduleWithLeadSkip = await getSchedule({ + const scheduleWithLeadSkip = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -526,7 +527,7 @@ describe("getSchedule", () => { bookings: [], }); - const scheduleWithLeadSkip = await getSchedule({ + const scheduleWithLeadSkip = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -623,7 +624,7 @@ describe("getSchedule", () => { bookings: [], }); - const scheduleWithLeadSkip = await getSchedule({ + const scheduleWithLeadSkip = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -732,7 +733,7 @@ describe("getSchedule", () => { bookings: [], }); - const scheduleWithLeadSkip = await getSchedule({ + const scheduleWithLeadSkip = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -829,7 +830,7 @@ describe("getSchedule", () => { bookings: [], }); - const scheduleWithLeadSkip = await getSchedule({ + const scheduleWithLeadSkip = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -926,7 +927,7 @@ describe("getSchedule", () => { bookings: [], }); - const scheduleWhenContactOwnerIsSkipped = await getSchedule({ + const scheduleWhenContactOwnerIsSkipped = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -1030,7 +1031,7 @@ describe("getSchedule", () => { }); // Day Plus 2 is completely free - It only has non accepted bookings - const scheduleOnCompletelyFreeDay = await getSchedule({ + const scheduleOnCompletelyFreeDay = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, // EventTypeSlug doesn't matter for non-dynamic events @@ -1064,7 +1065,7 @@ describe("getSchedule", () => { ); // Day plus 3 - const scheduleForDayWithOneBooking = await getSchedule({ + const scheduleForDayWithOneBooking = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -1130,7 +1131,7 @@ describe("getSchedule", () => { }); const { dateString: plus1DateString } = getDate({ dateIncrement: 1 }); const { dateString: plus2DateString } = getDate({ dateIncrement: 2 }); - const scheduleForEventWith30Length = await getSchedule({ + const scheduleForEventWith30Length = await availableSlotsService.getAvailableSlots({ input: { orgSlug: null, eventTypeId: 1, @@ -1167,17 +1168,18 @@ describe("getSchedule", () => { } ); - const scheduleForEventWith30minsLengthAndSlotInterval2hrs = await getSchedule({ - input: { - orgSlug: null, - eventTypeId: 2, - eventTypeSlug: "", - startTime: `${plus1DateString}T18:30:00.000Z`, - endTime: `${plus2DateString}T18:29:59.999Z`, - timeZone: Timezones["+5:30"], - isTeamEvent: false, - }, - }); + const scheduleForEventWith30minsLengthAndSlotInterval2hrs = + await availableSlotsService.getAvailableSlots({ + input: { + orgSlug: null, + eventTypeId: 2, + eventTypeSlug: "", + startTime: `${plus1DateString}T18:30:00.000Z`, + endTime: `${plus2DateString}T18:29:59.999Z`, + timeZone: Timezones["+5:30"], + isTeamEvent: false, + }, + }); // `slotInterval` takes precedence over `length` // 4:30 is utc so it is 10:00 in IST expect(scheduleForEventWith30minsLengthAndSlotInterval2hrs).toHaveTimeSlots( @@ -1227,7 +1229,7 @@ describe("getSchedule", () => { // Time Travel to the beginning of today after getting all the dates correctly. timeTravelToTheBeginningOfToday({ utcOffsetInHours: 5.5 }); - const scheduleForEventWithBookingNotice13Hrs = await getSchedule({ + const scheduleForEventWithBookingNotice13Hrs = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -1251,7 +1253,7 @@ describe("getSchedule", () => { } ); - const scheduleForEventWithBookingNotice10Hrs = await getSchedule({ + const scheduleForEventWithBookingNotice10Hrs = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 2, eventTypeSlug: "", @@ -1314,7 +1316,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const scheduleForEventOnADayWithNonCalBooking = await getSchedule({ + const scheduleForEventOnADayWithNonCalBooking = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -1389,7 +1391,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const scheduleForEventOnADayWithCalBooking = await getSchedule({ + const scheduleForEventOnADayWithCalBooking = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -1448,7 +1450,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const schedule = await getSchedule({ + const schedule = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -1513,7 +1515,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const scheduleForEventOnADayWithDateOverride = await getSchedule({ + const scheduleForEventOnADayWithDateOverride = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -1592,7 +1594,7 @@ describe("getSchedule", () => { // Requesting this user's availability for their // individual Event Type - const thisUserAvailability = await getSchedule({ + const thisUserAvailability = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 2, eventTypeSlug: "", @@ -1705,7 +1707,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const thisUserAvailabilityBookingLimitOne = await getSchedule({ + const thisUserAvailabilityBookingLimitOne = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -1717,7 +1719,7 @@ describe("getSchedule", () => { }, }); - const thisUserAvailabilityBookingLimitTwo = await getSchedule({ + const thisUserAvailabilityBookingLimitTwo = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 2, eventTypeSlug: "", @@ -1833,7 +1835,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const thisUserAvailabilityDurationLimitReached = await getSchedule({ + const thisUserAvailabilityDurationLimitReached = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -1845,7 +1847,7 @@ describe("getSchedule", () => { }, }); - const thisUserAvailabilityDurationLimitNotReached = await getSchedule({ + const thisUserAvailabilityDurationLimitNotReached = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 2, eventTypeSlug: "", @@ -1943,7 +1945,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const thisUserAvailabilityDurationLimit = await getSchedule({ + const thisUserAvailabilityDurationLimit = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -2039,7 +2041,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const weeklyAvailability = await getSchedule({ + const weeklyAvailability = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -2169,7 +2171,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const availabilityEventTypeOne = await getSchedule({ + const availabilityEventTypeOne = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -2190,7 +2192,7 @@ describe("getSchedule", () => { expect(availableSlotsInTz.filter((slot) => slot.format().startsWith(plus2DateString)).length).toBe(0); - const availabilityEventTypeTwo = await getSchedule({ + const availabilityEventTypeTwo = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 2, eventTypeSlug: "", @@ -2211,7 +2213,7 @@ describe("getSchedule", () => { expect(availableSlotsInTz2.filter((slot) => slot.format().startsWith(plus2DateString)).length).toBe(0); - const availabilityUserEventType = await getSchedule({ + const availabilityUserEventType = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 3, eventTypeSlug: "", @@ -2327,7 +2329,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const eventType1Availability = await getSchedule({ + const eventType1Availability = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -2339,7 +2341,7 @@ describe("getSchedule", () => { }, }); - const eventType2Availability = await getSchedule({ + const eventType2Availability = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 2, eventTypeSlug: "", @@ -2387,7 +2389,7 @@ describe("getSchedule", () => { ], }); - const eventType2AvailabilityUpdated = await getSchedule({ + const eventType2AvailabilityUpdated = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 2, eventTypeSlug: "", @@ -2468,7 +2470,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const thisUserAvailabilityBookingLimit = await getSchedule({ + const thisUserAvailabilityBookingLimit = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -2583,7 +2585,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const availabilityEventTypeOne = await getSchedule({ + const availabilityEventTypeOne = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -2604,7 +2606,7 @@ describe("getSchedule", () => { expect(availableSlotsInTz.filter((slot) => slot.format().startsWith(plus2DateString)).length).toBe(0); // 1 booking per day as limit - const availabilityEventTypeTwo = await getSchedule({ + const availabilityEventTypeTwo = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 2, eventTypeSlug: "", @@ -2624,7 +2626,7 @@ describe("getSchedule", () => { expect(availableSlotsInTz.filter((slot) => slot.format().startsWith(plus2DateString)).length).toBe(0); // 1 booking per day as limit - const availabilityUserEventType = await getSchedule({ + const availabilityUserEventType = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 3, eventTypeSlug: "", @@ -2708,7 +2710,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const thisUserAvailabilityBookingLimit = await getSchedule({ + const thisUserAvailabilityBookingLimit = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -2791,7 +2793,7 @@ describe("getSchedule", () => { // Requesting this user's availability for their // individual Event Type - const thisUserAvailability = await getSchedule({ + const thisUserAvailability = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 2, eventTypeSlug: "", @@ -2862,7 +2864,7 @@ describe("getSchedule", () => { ], }); - const schedule = await getSchedule({ + const schedule = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -2961,7 +2963,7 @@ describe("getSchedule", () => { ], }); - const scheduleForTeamEventOnADayWithNoBooking = await getSchedule({ + const scheduleForTeamEventOnADayWithNoBooking = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -2992,17 +2994,18 @@ describe("getSchedule", () => { } ); - const scheduleForTeamEventOnADayWithOneBookingForEachUser = await getSchedule({ - input: { - eventTypeId: 1, - eventTypeSlug: "", - startTime: `${plus1DateString}T18:30:00.000Z`, - endTime: `${plus2DateString}T18:29:59.999Z`, - timeZone: Timezones["+5:30"], - isTeamEvent: true, - orgSlug: null, - }, - }); + const scheduleForTeamEventOnADayWithOneBookingForEachUser = + await availableSlotsService.getAvailableSlots({ + input: { + eventTypeId: 1, + eventTypeSlug: "", + startTime: `${plus1DateString}T18:30:00.000Z`, + endTime: `${plus2DateString}T18:29:59.999Z`, + timeZone: Timezones["+5:30"], + isTeamEvent: true, + orgSlug: null, + }, + }); // A user with blocked time in another event, still affects Team Event availability // It's a collective availability, so both user 101 and 102 are considered for timeslots @@ -3101,17 +3104,18 @@ describe("getSchedule", () => { }, ], }); - const scheduleForTeamEventOnADayWithOneBookingForEachUserButOnDifferentTimeslots = await getSchedule({ - input: { - eventTypeId: 1, - eventTypeSlug: "", - startTime: `${plus1DateString}T18:30:00.000Z`, - endTime: `${plus2DateString}T18:29:59.999Z`, - timeZone: Timezones["+5:30"], - isTeamEvent: true, - orgSlug: null, - }, - }); + const scheduleForTeamEventOnADayWithOneBookingForEachUserButOnDifferentTimeslots = + await availableSlotsService.getAvailableSlots({ + input: { + eventTypeId: 1, + eventTypeSlug: "", + startTime: `${plus1DateString}T18:30:00.000Z`, + endTime: `${plus2DateString}T18:29:59.999Z`, + timeZone: Timezones["+5:30"], + isTeamEvent: true, + orgSlug: null, + }, + }); // A user with blocked time in another event, still affects Team Event availability expect(scheduleForTeamEventOnADayWithOneBookingForEachUserButOnDifferentTimeslots).toHaveTimeSlots( [ @@ -3130,17 +3134,18 @@ describe("getSchedule", () => { { dateString: plus2DateString } ); - const scheduleForTeamEventOnADayWithOneBookingForEachUserOnSameTimeSlot = await getSchedule({ - input: { - eventTypeId: 1, - eventTypeSlug: "", - startTime: `${plus2DateString}T18:30:00.000Z`, - endTime: `${plus3DateString}T18:29:59.999Z`, - timeZone: Timezones["+5:30"], - isTeamEvent: true, - orgSlug: null, - }, - }); + const scheduleForTeamEventOnADayWithOneBookingForEachUserOnSameTimeSlot = + await availableSlotsService.getAvailableSlots({ + input: { + eventTypeId: 1, + eventTypeSlug: "", + startTime: `${plus2DateString}T18:30:00.000Z`, + endTime: `${plus3DateString}T18:29:59.999Z`, + timeZone: Timezones["+5:30"], + isTeamEvent: true, + orgSlug: null, + }, + }); // A user with blocked time in another event, still affects Team Event availability expect(scheduleForTeamEventOnADayWithOneBookingForEachUserOnSameTimeSlot).toHaveTimeSlots( [ @@ -3195,7 +3200,7 @@ describe("getSchedule", () => { const { dateString: plus1DateString } = getDate({ dateIncrement: 1 }); const { dateString: plus2DateString } = getDate({ dateIncrement: 2 }); - const getScheduleRes = await getSchedule({ + const getScheduleRes = await availableSlotsService.getAvailableSlots({ input: { eventTypeSlug: scenario.eventTypes[0]?.slug, startTime: `${plus1DateString}T18:30:00.000Z`, @@ -3275,7 +3280,7 @@ describe("getSchedule", () => { bookings: [], }); - const scheduleWithHostChoosenSch = await getSchedule({ + const scheduleWithHostChoosenSch = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -3341,7 +3346,7 @@ describe("getSchedule", () => { bookings: [], }); - const scheduleWithEventCommonSch = await getSchedule({ + const scheduleWithEventCommonSch = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -3422,7 +3427,7 @@ describe("getSchedule", () => { ], }); - const schedule = await getSchedule({ + const schedule = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -3503,7 +3508,7 @@ describe("getSchedule", () => { ], }); - const schedule = await getSchedule({ + const schedule = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", diff --git a/apps/web/test/lib/getSchedule/calendarEvents.test.ts b/apps/web/test/lib/getSchedule/calendarEvents.test.ts index 31a9116889..179bc4b3ea 100644 --- a/apps/web/test/lib/getSchedule/calendarEvents.test.ts +++ b/apps/web/test/lib/getSchedule/calendarEvents.test.ts @@ -9,12 +9,13 @@ import { import { describe, test } from "vitest"; -import { getAvailableSlots as getSchedule } from "@calcom/trpc/server/routers/viewer/slots/util"; +import { AvailableSlotsService } from "@calcom/trpc/server/routers/viewer/slots/util"; import { expect, expectedSlotsForSchedule } from "./expects"; import { setupAndTeardown } from "./setupAndTeardown"; describe("getSchedule", () => { + const availableSlotsService = new AvailableSlotsService(); setupAndTeardown(); describe("Calendar event", () => { test("correctly identifies unavailable slots from selected calendars at user level", async () => { @@ -63,7 +64,7 @@ describe("getSchedule", () => { // An event with one accepted booking await createBookingScenario(scenarioData); - const scheduleForDayWithAGoogleCalendarBooking = await getSchedule({ + const scheduleForDayWithAGoogleCalendarBooking = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -133,7 +134,7 @@ describe("getSchedule", () => { // An event with one accepted booking await createBookingScenario(scenarioData); - const scheduleForDayWithAGoogleCalendarBooking = await getSchedule({ + const scheduleForDayWithAGoogleCalendarBooking = await availableSlotsService.getAvailableSlots({ input: { eventTypeId, eventTypeSlug: "", @@ -202,7 +203,7 @@ describe("getSchedule", () => { // An event with one accepted booking await createBookingScenario(scenarioData); - const scheduleForDayWithAGoogleCalendarBooking = await getSchedule({ + const scheduleForDayWithAGoogleCalendarBooking = await availableSlotsService.getAvailableSlots({ input: { eventTypeId, eventTypeSlug: "", @@ -275,7 +276,7 @@ describe("getSchedule", () => { // An event with one accepted booking await createBookingScenario(scenarioData); - const scheduleForDayWithAGoogleCalendarBooking = await getSchedule({ + const scheduleForDayWithAGoogleCalendarBooking = await availableSlotsService.getAvailableSlots({ input: { eventTypeId, eventTypeSlug: "", diff --git a/apps/web/test/lib/getSchedule/delegation-credential.test.ts b/apps/web/test/lib/getSchedule/delegation-credential.test.ts index 74df5ad78e..ed8997547e 100644 --- a/apps/web/test/lib/getSchedule/delegation-credential.test.ts +++ b/apps/web/test/lib/getSchedule/delegation-credential.test.ts @@ -13,13 +13,14 @@ import { expectNoAttemptToGetAvailability } from "../../utils/bookingScenario/ex import { describe, test } from "vitest"; import { MembershipRole } from "@calcom/prisma/enums"; -import { getAvailableSlots as getSchedule } from "@calcom/trpc/server/routers/viewer/slots/util"; +import { AvailableSlotsService } from "@calcom/trpc/server/routers/viewer/slots/util"; import { expect, expectedSlotsForSchedule } from "./expects"; import { setupAndTeardown } from "./setupAndTeardown"; describe("getSchedule", () => { setupAndTeardown(); + describe("Delegation Credential", () => { test("correctly identifies unavailable slots using DelegationCredential credentials", async () => { const { dateString: plus1DateString } = getDate({ dateIncrement: 1 }); @@ -89,8 +90,8 @@ describe("getSchedule", () => { }; await createBookingScenario(scenarioData); - - const scheduleForDayWithAGoogleCalendarBooking = await getSchedule({ + const availableSlotsService = new AvailableSlotsService(); + const scheduleForDayWithAGoogleCalendarBooking = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -181,8 +182,8 @@ describe("getSchedule", () => { }; await createBookingScenario(scenarioData); - - const scheduleForDayWithAGoogleCalendarBooking = await getSchedule({ + const availableSlotsService = new AvailableSlotsService(); + const scheduleForDayWithAGoogleCalendarBooking = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", diff --git a/apps/web/test/lib/getSchedule/futureLimit.timezone.test.ts b/apps/web/test/lib/getSchedule/futureLimit.timezone.test.ts index 28d5785e8e..8036cd00a9 100644 --- a/apps/web/test/lib/getSchedule/futureLimit.timezone.test.ts +++ b/apps/web/test/lib/getSchedule/futureLimit.timezone.test.ts @@ -9,7 +9,7 @@ import type { ScenarioData } from "../../utils/bookingScenario/bookingScenario"; import { describe, expect, vi, test } from "vitest"; import { PeriodType } from "@calcom/prisma/enums"; -import { getAvailableSlots as getSchedule } from "@calcom/trpc/server/routers/viewer/slots/util"; +import { AvailableSlotsService } from "@calcom/trpc/server/routers/viewer/slots/util"; import { expectedSlotsForSchedule } from "./expects"; import { setupAndTeardown } from "./setupAndTeardown"; @@ -70,6 +70,7 @@ vi.mock("@calcom/lib/constants", () => ({ })); describe("getSchedule", () => { + const availableSlotsService = new AvailableSlotsService(); setupAndTeardown(); describe("Future Limits", () => { describe("PeriodType=ROLLING", () => { @@ -112,7 +113,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const scheduleForEvent = await getSchedule({ + const scheduleForEvent = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -200,7 +201,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const scheduleForEvent = await getSchedule({ + const scheduleForEvent = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -284,7 +285,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const scheduleForEvent = await getSchedule({ + const scheduleForEvent = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -379,7 +380,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const scheduleForEvent = await getSchedule({ + const scheduleForEvent = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -483,7 +484,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const scheduleForEvent = await getSchedule({ + const scheduleForEvent = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -573,7 +574,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const scheduleForEvent = await getSchedule({ + const scheduleForEvent = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -657,7 +658,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const scheduleForEvent = await getSchedule({ + const scheduleForEvent = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -780,7 +781,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const scheduleForEvent = await getSchedule({ + const scheduleForEvent = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -878,7 +879,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const scheduleForEvent = await getSchedule({ + const scheduleForEvent = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -979,7 +980,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const scheduleForEvent = await getSchedule({ + const scheduleForEvent = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -1087,7 +1088,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const scheduleForEvent = await getSchedule({ + const scheduleForEvent = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -1207,7 +1208,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const scheduleForEvent = await getSchedule({ + const scheduleForEvent = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -1304,7 +1305,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const scheduleForEvent = await getSchedule({ + const scheduleForEvent = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -1413,7 +1414,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const scheduleForEvent = await getSchedule({ + const scheduleForEvent = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -1515,7 +1516,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const scheduleForEvent = await getSchedule({ + const scheduleForEvent = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -1601,7 +1602,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const scheduleForEventForPagoTz = await getSchedule({ + const scheduleForEventForPagoTz = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", diff --git a/apps/web/test/lib/getSchedule/restrictionSchedule.test.ts b/apps/web/test/lib/getSchedule/restrictionSchedule.test.ts index 1772a9f47e..7a9c2a62ae 100644 --- a/apps/web/test/lib/getSchedule/restrictionSchedule.test.ts +++ b/apps/web/test/lib/getSchedule/restrictionSchedule.test.ts @@ -11,7 +11,7 @@ import { describe, test, vi } from "vitest"; import type { z } from "zod"; import type { getScheduleSchema } from "@calcom/trpc/server/routers/viewer/slots/types"; -import { getAvailableSlots as getSchedule } from "@calcom/trpc/server/routers/viewer/slots/util"; +import { AvailableSlotsService } from "@calcom/trpc/server/routers/viewer/slots/util"; import { expect } from "./expects"; import { setupAndTeardown } from "./setupAndTeardown"; @@ -89,6 +89,7 @@ async function setupTeamAndFeatures() { } describe("getSchedule", () => { + const availableSlotsService = new AvailableSlotsService(); setupAndTeardown(); describe("Restriction Schedule", () => { @@ -156,7 +157,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const result = await getSchedule({ + const result = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -252,7 +253,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const result = await getSchedule({ + const result = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -353,7 +354,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const result = await getSchedule({ + const result = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -438,7 +439,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const result = await getSchedule({ + const result = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", diff --git a/apps/web/test/lib/getSchedule/selectedSlots.test.ts b/apps/web/test/lib/getSchedule/selectedSlots.test.ts index 61411fae8f..ef53a7c5cb 100644 --- a/apps/web/test/lib/getSchedule/selectedSlots.test.ts +++ b/apps/web/test/lib/getSchedule/selectedSlots.test.ts @@ -12,7 +12,7 @@ import type { z } from "zod"; import { prisma } from "@calcom/prisma"; import type { getScheduleSchema, GetScheduleOptions } from "@calcom/trpc/server/routers/viewer/slots/types"; -import { getAvailableSlots as getSchedule } from "@calcom/trpc/server/routers/viewer/slots/util"; +import { AvailableSlotsService } from "@calcom/trpc/server/routers/viewer/slots/util"; import { expect } from "./expects"; import { setupAndTeardown } from "./setupAndTeardown"; @@ -62,6 +62,7 @@ const getBaseScenarioData = (): ScheduleScenario => ({ }); describe("getSchedule", () => { + const availableSlotsService = new AvailableSlotsService(); setupAndTeardown(); describe("Reserved Slots", () => { @@ -93,7 +94,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const schedule = await getSchedule({ + const schedule = await availableSlotsService.getAvailableSlots({ input: getTestScheduleInput({ yesterdayDateString, plus5DateString }), }); @@ -128,7 +129,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const schedule = await getSchedule({ + const schedule = await availableSlotsService.getAvailableSlots({ input: getTestScheduleInput({ yesterdayDateString, plus5DateString }), ctx: { req: { @@ -183,7 +184,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const schedule = await getSchedule({ + const schedule = await availableSlotsService.getAvailableSlots({ input: getTestScheduleInput({ yesterdayDateString, plus5DateString }), }); @@ -238,7 +239,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const schedule = await getSchedule({ + const schedule = await availableSlotsService.getAvailableSlots({ input: getTestScheduleInput({ yesterdayDateString, plus5DateString }), }); @@ -287,7 +288,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const schedule = await getSchedule({ + const schedule = await availableSlotsService.getAvailableSlots({ input: getTestScheduleInput({ yesterdayDateString, plus5DateString }), }); @@ -366,7 +367,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - await getSchedule({ + await availableSlotsService.getAvailableSlots({ input: getTestScheduleInput({ yesterdayDateString, plus5DateString }), ctx: { req: { diff --git a/package.json b/package.json index ab7dfd8ccc..4ab7e95b65 100644 --- a/package.json +++ b/package.json @@ -116,6 +116,7 @@ }, "dependencies": { "@daily-co/daily-js": "^0.76.0", + "@evyweb/ioctopus": "^1.2.0", "@next/third-parties": "^14.2.5", "@vercel/functions": "^1.4.0", "city-timezones": "^1.2.1", diff --git a/packages/platform/libraries/slots.ts b/packages/platform/libraries/slots.ts index 0d61c8c2aa..420fc83b5b 100644 --- a/packages/platform/libraries/slots.ts +++ b/packages/platform/libraries/slots.ts @@ -1,3 +1,3 @@ -import { getAvailableSlots } from "@calcom/trpc/server/routers/viewer/slots/util"; +import { AvailableSlotsService } from "@calcom/trpc/server/routers/viewer/slots/util"; -export { getAvailableSlots }; +export { AvailableSlotsService }; diff --git a/packages/trpc/server/routers/viewer/highPerf/getTeamSchedule.handler.ts b/packages/trpc/server/routers/viewer/highPerf/getTeamSchedule.handler.ts index 9dac887cad..3db504ae0e 100644 --- a/packages/trpc/server/routers/viewer/highPerf/getTeamSchedule.handler.ts +++ b/packages/trpc/server/routers/viewer/highPerf/getTeamSchedule.handler.ts @@ -1,6 +1,6 @@ import type { IncomingMessage } from "http"; -import { getAvailableSlots } from "../slots/util"; +import { AvailableSlotsService } from "../slots/util"; import type { TGetTeamScheduleInputSchema } from "./getTeamSchedule.schema"; export type GetTeamScheduleOptions = { @@ -13,5 +13,6 @@ interface ContextForGetSchedule extends Record { } export const getTeamScheduleHandler = async ({ ctx, input }: GetTeamScheduleOptions) => { - return await getAvailableSlots({ ctx, input }); + const availableSlotsService = new AvailableSlotsService(); + return await availableSlotsService.getAvailableSlots({ ctx, input }); }; diff --git a/packages/trpc/server/routers/viewer/slots/getSchedule.handler.ts b/packages/trpc/server/routers/viewer/slots/getSchedule.handler.ts index cbea7c78e3..7898a0efbe 100644 --- a/packages/trpc/server/routers/viewer/slots/getSchedule.handler.ts +++ b/packages/trpc/server/routers/viewer/slots/getSchedule.handler.ts @@ -1,6 +1,7 @@ import type { GetScheduleOptions } from "./types"; -import { getAvailableSlots } from "./util"; +import { AvailableSlotsService } from "./util"; export const getScheduleHandler = async ({ ctx, input }: GetScheduleOptions) => { - return await getAvailableSlots({ ctx, input }); + const availableSlotsService = new AvailableSlotsService(); + return await availableSlotsService.getAvailableSlots({ ctx, input }); }; diff --git a/packages/trpc/server/routers/viewer/slots/util.ts b/packages/trpc/server/routers/viewer/slots/util.ts index 809cf85a17..f0030d404c 100644 --- a/packages/trpc/server/routers/viewer/slots/util.ts +++ b/packages/trpc/server/routers/viewer/slots/util.ts @@ -68,170 +68,6 @@ type GetAvailabilityUserWithDelegationCredentials = Omit user.id), - currentTimeInUtc, - })) || []; - - const slotsSelectedByOtherUsers = unexpiredSelectedSlots.filter((slot) => slot.uid !== bookerClientUid); - - await _cleanupExpiredSlots({ eventTypeId }); - - const reservedSlots = slotsSelectedByOtherUsers; - - return reservedSlots; - - async function _cleanupExpiredSlots({ eventTypeId }: { eventTypeId: number }) { - await SelectedSlotsRepository.deleteManyExpiredSlots({ eventTypeId, currentTimeInUtc }); - } -} - -const _getEventType = async ( - input: TGetScheduleInputSchema, - organizationDetails: { currentOrgDomain: string | null; isValidOrgDomain: boolean } -) => { - const { eventTypeSlug, usernameList, isTeamEvent } = input; - log.info("getEventType", safeStringify({ usernameList, eventTypeSlug, isTeamEvent, organizationDetails })); - const eventTypeId = - input.eventTypeId || - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - (await getEventTypeId({ - slug: usernameList?.[0], - eventTypeSlug: eventTypeSlug, - isTeamEvent, - organizationDetails, - })); - - if (!eventTypeId) { - return null; - } - - return await EventTypeRepository.findForSlots({ id: eventTypeId }); -}; - -export const getEventType = withReporting(_getEventType, "getEventType"); - -const _getDynamicEventType = async ( - input: TGetScheduleInputSchema, - organizationDetails: { currentOrgDomain: string | null; isValidOrgDomain: boolean } -) => { - const { currentOrgDomain, isValidOrgDomain } = organizationDetails; - // For dynamic booking, we need to get and update user credentials, schedule and availability in the eventTypeObject as they're required in the new availability logic - if (!input.eventTypeSlug) { - throw new TRPCError({ - message: "eventTypeSlug is required for dynamic booking", - code: "BAD_REQUEST", - }); - } - const dynamicEventType = getDefaultEvent(input.eventTypeSlug); - - const usersForDynamicEventType = await UserRepository.findManyUsersForDynamicEventType({ - currentOrgDomain: isValidOrgDomain ? currentOrgDomain : null, - usernameList: Array.isArray(input.usernameList) - ? input.usernameList - : input.usernameList - ? [input.usernameList] - : [], - }); - - const usersWithOldSelectedCalendars = usersForDynamicEventType.map((user) => withSelectedCalendars(user)); - - const isDynamicAllowed = !usersWithOldSelectedCalendars.some((user) => !user.allowDynamicBooking); - if (!isDynamicAllowed) { - throw new TRPCError({ - message: "Some of the users in this group do not allow dynamic booking", - code: "UNAUTHORIZED", - }); - } - return Object.assign({}, dynamicEventType, { - users: usersWithOldSelectedCalendars, - }); -}; - -export const getDynamicEventType = withReporting(_getDynamicEventType, "getDynamicEventType"); - -const _getRegularOrDynamicEventType = async ( - input: TGetScheduleInputSchema, - organizationDetails: { currentOrgDomain: string | null; isValidOrgDomain: boolean } -) => { - const isDynamicBooking = input.usernameList && input.usernameList.length > 1; - return isDynamicBooking - ? await getDynamicEventType(input, organizationDetails) - : await getEventType(input, organizationDetails); -}; - -export const getRegularOrDynamicEventType = withReporting( - _getRegularOrDynamicEventType, - "getRegularOrDynamicEventType" -); - -function applyOccupiedSeatsToCurrentSeats( - currentSeats: CurrentSeats, - occupiedSeats: { slotUtcStartDate: Date }[] -) { - const occupiedSeatsMap = new Map(); - - occupiedSeats.forEach((item) => { - const dateKey = item.slotUtcStartDate.toISOString(); - occupiedSeatsMap.set(dateKey, (occupiedSeatsMap.get(dateKey) || 0) + 1); - }); - - occupiedSeatsMap.forEach((count, date) => { - currentSeats.push({ - uid: uuid(), - startTime: dayjs(date).toDate(), - _count: { attendees: count }, - }); - }); - - return currentSeats; -} - export interface IGetAvailableSlots { slots: Record< string, @@ -249,626 +85,437 @@ export interface IGetAvailableSlots { troubleshooter?: any; } -const _getUsersWithCredentials = ({ - hosts, -}: { - hosts: { - isFixed?: boolean; - user: GetAvailabilityUserWithDelegationCredentials; - }[]; -}) => { - return hosts.map(({ isFixed, user }) => ({ isFixed, ...user })); -}; +export type GetAvailableSlotsResponse = Awaited< + ReturnType<(typeof AvailableSlotsService)["prototype"]["_getAvailableSlots"]> +>; -export const getUsersWithCredentials = withReporting(_getUsersWithCredentials, "getUsersWithCredentials"); - -const getStartTime = (startTimeInput: string, timeZone?: string, minimumBookingNotice?: number) => { - const startTimeMin = dayjs.utc().add(minimumBookingNotice || 1, "minutes"); - const startTime = timeZone === "Etc/GMT" ? dayjs.utc(startTimeInput) : dayjs(startTimeInput).tz(timeZone); - - return startTimeMin.isAfter(startTime) ? startTimeMin.tz(timeZone) : startTime; -}; - -export type GetAvailableSlotsResponse = Awaited>; -const _getAvailableSlots = async ({ input, ctx }: GetScheduleOptions): Promise => { - const { - _enableTroubleshooter: enableTroubleshooter = false, - _bypassCalendarBusyTimes: bypassBusyCalendarTimes = false, - _shouldServeCache, - routingFormResponseId, - queuedFormResponseId, - } = input; - const orgDetails = input?.orgSlug - ? { - currentOrgDomain: input.orgSlug, - isValidOrgDomain: !!input.orgSlug && !RESERVED_SUBDOMAINS.includes(input.orgSlug), - } - : orgDomainConfig(ctx?.req); - - if (process.env.INTEGRATION_TEST_MODE === "true") { - logger.settings.minLevel = 2; - } - - const eventType = await getRegularOrDynamicEventType(input, orgDetails); - - if (!eventType) { - throw new TRPCError({ code: "NOT_FOUND" }); - } - - const shouldServeCache = await getShouldServeCache(_shouldServeCache, eventType.team?.id); - if (isEventTypeLoggingEnabled({ eventTypeId: eventType.id })) { - logger.settings.minLevel = 2; - } - - const isRollingWindowPeriodType = eventType.periodType === PeriodType.ROLLING_WINDOW; - const startTimeAsIsoString = input.startTime; - const isStartTimeInPast = dayjs(startTimeAsIsoString).isBefore(dayjs().subtract(1, "day").startOf("day")); - - // If startTime is already sent in the past, we don't need to adjust it. - // We assume that the client is already sending startTime as per their requirement. - // Note: We could optimize it further to go back 1 month in past only for the 2nd month because that is what we are putting a hard limit at. - const startTimeAdjustedForRollingWindowComputation = - isStartTimeInPast || !isRollingWindowPeriodType - ? startTimeAsIsoString - : dayjs(startTimeAsIsoString).subtract(1, "month").toISOString(); - - const loggerWithEventDetails = logger.getSubLogger({ - type: "json", - prefix: ["getAvailableSlots", `${eventType.id}:${input.usernameList}/${input.eventTypeSlug}`], - }); - - const startTime = getStartTime( - startTimeAdjustedForRollingWindowComputation, - input.timeZone, - eventType.minimumBookingNotice - ); - const endTime = - input.timeZone === "Etc/GMT" ? dayjs.utc(input.endTime) : dayjs(input.endTime).utc().tz(input.timeZone); - - if (!startTime.isValid() || !endTime.isValid()) { - throw new TRPCError({ message: "Invalid time range given.", code: "BAD_REQUEST" }); - } - // when an empty array is given we should prefer to have it handled as if this wasn't given at all - // we don't want to return no availability in this case. - const routedTeamMemberIds = input.routedTeamMemberIds ?? []; - const contactOwnerEmailFromInput = input.teamMemberEmail ?? null; - - const skipContactOwner = shouldIgnoreContactOwner({ - skipContactOwner: input.skipContactOwner ?? null, - rescheduleUid: input.rescheduleUid ?? null, - routedTeamMemberIds: input.routedTeamMemberIds ?? null, - }); - - const contactOwnerEmail = skipContactOwner ? null : contactOwnerEmailFromInput; - - let routingFormResponse = null; - if (routingFormResponseId) { - routingFormResponse = await RoutingFormResponseRepository.findFormResponseIncludeForm({ - routingFormResponseId, - }); - } else if (queuedFormResponseId) { - routingFormResponse = await RoutingFormResponseRepository.findQueuedFormResponseIncludeForm({ - queuedFormResponseId, - }); - } - - const { qualifiedRRHosts, allFallbackRRHosts, fixedHosts } = - await findQualifiedHostsWithDelegationCredentials({ - eventType, - rescheduleUid: input.rescheduleUid ?? null, - routedTeamMemberIds, - contactOwnerEmail, - routingFormResponse, - }); - - const allHosts = [...qualifiedRRHosts, ...fixedHosts]; - - const twoWeeksFromNow = dayjs().add(2, "week"); - - const hasFallbackRRHosts = allFallbackRRHosts && allFallbackRRHosts.length > qualifiedRRHosts.length; - - let { allUsersAvailability, usersWithCredentials, currentSeats } = await calculateHostsAndAvailabilities({ - input, - eventType, - hosts: allHosts, - loggerWithEventDetails, - // adjust start time so we can check for available slots in the first two weeks - startTime: - hasFallbackRRHosts && startTime.isBefore(twoWeeksFromNow) - ? getStartTime(dayjs().format(), input.timeZone, eventType.minimumBookingNotice) - : startTime, - // adjust end time so we can check for available slots in the first two weeks - endTime: - hasFallbackRRHosts && endTime.isBefore(twoWeeksFromNow) - ? getStartTime(twoWeeksFromNow.format(), input.timeZone, eventType.minimumBookingNotice) - : endTime, - bypassBusyCalendarTimes, - shouldServeCache, - }); - - let aggregatedAvailability = getAggregatedAvailability(allUsersAvailability, eventType.schedulingType); - - // Fairness and Contact Owner have fallbacks because we check for within 2 weeks - if (hasFallbackRRHosts) { - let diff = 0; - if (startTime.isBefore(twoWeeksFromNow)) { - //check if first two week have availability - diff = - aggregatedAvailability.length > 0 ? aggregatedAvailability[0].start.diff(twoWeeksFromNow, "day") : 1; // no aggregatedAvailability so we diff to +1 - } else { - // if start time is not within first two weeks, check if there are any available slots - if (!aggregatedAvailability.length) { - // if no available slots check if first two weeks are available, otherwise fallback - const firstTwoWeeksAvailabilities = await calculateHostsAndAvailabilities({ - input, - eventType, - hosts: [...qualifiedRRHosts, ...fixedHosts], - loggerWithEventDetails, - startTime: dayjs(), - endTime: twoWeeksFromNow, - bypassBusyCalendarTimes, - shouldServeCache, - }); - if ( - !getAggregatedAvailability( - firstTwoWeeksAvailabilities.allUsersAvailability, - eventType.schedulingType - ).length - ) { - diff = 1; - } - } - } - - if (input.email) { - loggerWithEventDetails.info({ - email: input.email, - contactOwnerEmail, - qualifiedRRHosts: qualifiedRRHosts.map((host) => host.user.id), - fallbackRRHosts: allFallbackRRHosts.map((host) => host.user.id), - fallBackActive: diff > 0, - }); - } - - if (diff > 0) { - // if the first available slot is more than 2 weeks from now, round robin as normal - ({ allUsersAvailability, usersWithCredentials, currentSeats } = await calculateHostsAndAvailabilities({ - input, - eventType, - hosts: [...allFallbackRRHosts, ...fixedHosts], - loggerWithEventDetails, - startTime, - endTime, - bypassBusyCalendarTimes, - shouldServeCache, - })); - aggregatedAvailability = getAggregatedAvailability(allUsersAvailability, eventType.schedulingType); - } - } - - const isTeamEvent = - eventType.schedulingType === SchedulingType.COLLECTIVE || - eventType.schedulingType === SchedulingType.ROUND_ROBIN || - allUsersAvailability.length > 1; - - const timeSlots = getSlots({ - inviteeDate: startTime, - eventLength: input.duration || eventType.length, - offsetStart: eventType.offsetStart, - dateRanges: aggregatedAvailability, - minimumBookingNotice: eventType.minimumBookingNotice, - frequency: eventType.slotInterval || input.duration || eventType.length, - datesOutOfOffice: !isTeamEvent ? allUsersAvailability[0]?.datesOutOfOffice : undefined, - }); - - let availableTimeSlots: typeof timeSlots = []; - const bookerClientUid = ctx?.req?.cookies?.uid; - const isRestrictionScheduleFeatureEnabled = await isRestrictionScheduleEnabled(eventType.team?.id); - if (eventType.restrictionScheduleId && isRestrictionScheduleFeatureEnabled) { - const scheduleRepo = new ScheduleRepository(prisma); - const restrictionSchedule = await scheduleRepo.findScheduleByIdForBuildDateRanges({ - scheduleId: eventType.restrictionScheduleId, - }); - if (restrictionSchedule) { - if (!eventType.useBookerTimezone && !restrictionSchedule.timeZone) { - throw new TRPCError({ - message: "No timezone is set for the restricted schedule", - code: "BAD_REQUEST", - }); - } - - const restrictionTimezone = eventType.useBookerTimezone - ? input.timeZone - : restrictionSchedule.timeZone!; - const eventLength = input.duration || eventType.length; - - const restrictionAvailability = restrictionSchedule.availability.map((rule) => ({ - days: rule.days, - startTime: rule.startTime, - endTime: rule.endTime, - date: rule.date, - })); - - // Include travel schedules if restriction schedule is the user's default schedule - const isDefaultSchedule = restrictionSchedule.user.defaultScheduleId === restrictionSchedule.id; - const travelSchedules = - isDefaultSchedule && !eventType.useBookerTimezone - ? restrictionSchedule.user.travelSchedules.map((schedule) => ({ - startDate: dayjs(schedule.startDate), - endDate: schedule.endDate ? dayjs(schedule.endDate) : undefined, - timeZone: schedule.timeZone, - })) - : []; - - const { dateRanges: restrictionRanges } = buildDateRanges({ - availability: restrictionAvailability, - timeZone: restrictionTimezone || "UTC", - dateFrom: startTime, - dateTo: endTime, - travelSchedules, - }); - - availableTimeSlots = timeSlots.filter((slot) => { - const slotStart = slot.time; - const slotEnd = slot.time.add(eventLength, "minute"); - - return restrictionRanges.some( - (range) => - (slotStart.isAfter(range.start) || slotStart.isSame(range.start)) && - (slotEnd.isBefore(range.end) || slotEnd.isSame(range.end)) - ); - }); - } else { - availableTimeSlots = timeSlots; - } - } else { - availableTimeSlots = timeSlots; - } - - const reservedSlots = await _getReservedSlotsAndCleanupExpired({ +export class AvailableSlotsService { + private async _getReservedSlotsAndCleanupExpired({ bookerClientUid, - eventTypeId: eventType.id, usersWithCredentials, - }); + eventTypeId, + }: { + bookerClientUid: string | undefined; + usersWithCredentials: GetAvailabilityUser[]; + eventTypeId: number; + }) { + const currentTimeInUtc = dayjs.utc().format(); - const availabilityCheckProps = { - eventLength: input.duration || eventType.length, - currentSeats, - }; + const unexpiredSelectedSlots = + (await SelectedSlotsRepository.findManyUnexpiredSlots({ + userIds: usersWithCredentials.map((user) => user.id), + currentTimeInUtc, + })) || []; - if (reservedSlots?.length > 0) { - let occupiedSeats: typeof reservedSlots = reservedSlots.filter( - (item) => item.isSeat && item.eventTypeId === eventType.id + const slotsSelectedByOtherUsers = unexpiredSelectedSlots.filter((slot) => slot.uid !== bookerClientUid); + + await _cleanupExpiredSlots({ eventTypeId }); + + const reservedSlots = slotsSelectedByOtherUsers; + + return reservedSlots; + + async function _cleanupExpiredSlots({ eventTypeId }: { eventTypeId: number }) { + await SelectedSlotsRepository.deleteManyExpiredSlots({ eventTypeId, currentTimeInUtc }); + } + } + + private async _getDynamicEventType( + input: TGetScheduleInputSchema, + organizationDetails: { currentOrgDomain: string | null; isValidOrgDomain: boolean } + ) { + const { currentOrgDomain, isValidOrgDomain } = organizationDetails; + // For dynamic booking, we need to get and update user credentials, schedule and availability in the eventTypeObject as they're required in the new availability logic + if (!input.eventTypeSlug) { + throw new TRPCError({ + message: "eventTypeSlug is required for dynamic booking", + code: "BAD_REQUEST", + }); + } + const dynamicEventType = getDefaultEvent(input.eventTypeSlug); + + const usersForDynamicEventType = await UserRepository.findManyUsersForDynamicEventType({ + currentOrgDomain: isValidOrgDomain ? currentOrgDomain : null, + usernameList: Array.isArray(input.usernameList) + ? input.usernameList + : input.usernameList + ? [input.usernameList] + : [], + }); + + const usersWithOldSelectedCalendars = usersForDynamicEventType.map((user) => withSelectedCalendars(user)); + + const isDynamicAllowed = !usersWithOldSelectedCalendars.some((user) => !user.allowDynamicBooking); + if (!isDynamicAllowed) { + throw new TRPCError({ + message: "Some of the users in this group do not allow dynamic booking", + code: "UNAUTHORIZED", + }); + } + return Object.assign({}, dynamicEventType, { + users: usersWithOldSelectedCalendars, + }); + } + private getDynamicEventType = withReporting(this._getDynamicEventType, "getDynamicEventType"); + + private applyOccupiedSeatsToCurrentSeats( + currentSeats: CurrentSeats, + occupiedSeats: { slotUtcStartDate: Date }[] + ) { + const occupiedSeatsMap = new Map(); + + occupiedSeats.forEach((item) => { + const dateKey = item.slotUtcStartDate.toISOString(); + occupiedSeatsMap.set(dateKey, (occupiedSeatsMap.get(dateKey) || 0) + 1); + }); + + occupiedSeatsMap.forEach((count, date) => { + currentSeats.push({ + uid: uuid(), + startTime: dayjs(date).toDate(), + _count: { attendees: count }, + }); + }); + + return currentSeats; + } + + private async _getEventType( + input: TGetScheduleInputSchema, + organizationDetails: { currentOrgDomain: string | null; isValidOrgDomain: boolean } + ) { + const { eventTypeSlug, usernameList, isTeamEvent } = input; + log.info( + "getEventType", + safeStringify({ usernameList, eventTypeSlug, isTeamEvent, organizationDetails }) ); - if (occupiedSeats?.length) { - const addedToCurrentSeats: string[] = []; - if (typeof availabilityCheckProps.currentSeats !== "undefined") { - availabilityCheckProps.currentSeats = availabilityCheckProps.currentSeats.map((item) => { - const attendees = - occupiedSeats.filter( - (seat) => seat.slotUtcStartDate.toISOString() === item.startTime.toISOString() - )?.length || 0; - if (attendees) addedToCurrentSeats.push(item.startTime.toISOString()); - return { - ...item, - _count: { - attendees: item._count.attendees + attendees, - }, - }; - }); - occupiedSeats = occupiedSeats.filter( - (item) => !addedToCurrentSeats.includes(item.slotUtcStartDate.toISOString()) - ); - } + const eventTypeId = + input.eventTypeId || + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + (await this.getEventTypeId({ + slug: usernameList?.[0], + eventTypeSlug: eventTypeSlug, + isTeamEvent, + organizationDetails, + })); - availabilityCheckProps.currentSeats = applyOccupiedSeatsToCurrentSeats( - availabilityCheckProps.currentSeats || [], - occupiedSeats - ); - - currentSeats = availabilityCheckProps.currentSeats; - } - const busySlotsFromReservedSlots = reservedSlots.reduce((r, c) => { - if (!c.isSeat) { - r.push({ start: c.slotUtcStartDate, end: c.slotUtcEndDate }); - } - return r; - }, []); - - availableTimeSlots = availableTimeSlots - .map((slot) => { - if ( - !checkForConflicts({ - time: slot.time, - busy: busySlotsFromReservedSlots, - ...availabilityCheckProps, - }) - ) { - return slot; - } - return undefined; - }) - .filter( - ( - item: - | { - time: dayjs.Dayjs; - userIds?: number[] | undefined; - } - | undefined - ): item is { - time: dayjs.Dayjs; - userIds?: number[] | undefined; - } => { - return !!item; - } - ); - } - - // fr-CA uses YYYY-MM-DD - const formatter = new Intl.DateTimeFormat("fr-CA", { - year: "numeric", - month: "2-digit", - day: "2-digit", - timeZone: input.timeZone, - }); - - function _mapSlotsToDate() { - const currentSeatsMap = new Map(); - - if (currentSeats && currentSeats.length > 0) { - currentSeats.forEach((booking) => { - const timeKey = booking.startTime.toISOString(); - currentSeatsMap.set(timeKey, { - attendees: booking._count.attendees, - uid: booking.uid, - }); - }); + if (!eventTypeId) { + return null; } - return availableTimeSlots.reduce( - ( - r: Record, - { time, ...passThroughProps } - ) => { - // This used to be _time.tz(input.timeZone) but Dayjs tz() is slow. - // toLocaleDateString slugish, using Intl.DateTimeFormat we get the desired speed results. - const dateString = formatter.format(time.toDate()); - const timeISO = time.toISOString(); - - r[dateString] = r[dateString] || []; - if (eventType?.onlyShowFirstAvailableSlot && r[dateString].length > 0) { - return r; - } - - const existingBooking = currentSeatsMap.get(timeISO); - - r[dateString].push({ - ...passThroughProps, - time: timeISO, - ...(existingBooking && { - attendees: existingBooking.attendees, - bookingUid: existingBooking.uid, - }), - }); - return r; - }, - Object.create(null) - ); + return await EventTypeRepository.findForSlots({ id: eventTypeId }); } - const mapSlotsToDate = withReporting(_mapSlotsToDate, "mapSlotsToDate"); - const slotsMappedToDate = mapSlotsToDate(); - loggerWithEventDetails.debug({ slotsMappedToDate }); + private getEventType = withReporting(this._getEventType, "getEventType"); - const availableDates = Object.keys(slotsMappedToDate); - const allDatesWithBookabilityStatus = getAllDatesWithBookabilityStatus(availableDates); - loggerWithEventDetails.debug({ availableDates }); + private doesRangeStartFromToday(periodType: PeriodType) { + return periodType === PeriodType.ROLLING_WINDOW || periodType === PeriodType.ROLLING; + } - // timeZone isn't directly set on eventType now(So, it is legacy) - // schedule is always expected to be set for an eventType now so it must never fallback to allUsersAvailability[0].timeZone(fallback is again legacy behavior) - // TODO: Also, handleNewBooking only seems to be using eventType?.schedule?.timeZone which seems to confirm that we should simplify it as well. - const eventTimeZone = - eventType.timeZone || eventType?.schedule?.timeZone || allUsersAvailability?.[0]?.timeZone; + private _getAllDatesWithBookabilityStatus(availableDates: string[]) { + const availableDatesSet = new Set(availableDates); + const firstDate = dayjs(availableDates[0]); + const lastDate = dayjs(availableDates[availableDates.length - 1]); + const allDates: Record = {}; - const eventUtcOffset = getUTCOffsetByTimezone(eventTimeZone) ?? 0; - const bookerUtcOffset = input.timeZone ? getUTCOffsetByTimezone(input.timeZone) ?? 0 : 0; - const periodLimits = calculatePeriodLimits({ - periodType: eventType.periodType, - periodDays: eventType.periodDays, - periodCountCalendarDays: eventType.periodCountCalendarDays, - periodStartDate: eventType.periodStartDate, - periodEndDate: eventType.periodEndDate, - allDatesWithBookabilityStatusInBookerTz: allDatesWithBookabilityStatus, - eventUtcOffset, - bookerUtcOffset, - }); - let foundAFutureLimitViolation = false; - function _mapWithinBoundsSlotsToDate() { - // This should never happen. Just for type safety, we already check in the upper scope - if (!eventType) throw new TRPCError({ code: "NOT_FOUND" }); + let currentDate = firstDate; + while (currentDate <= lastDate) { + allDates[currentDate.format("YYYY-MM-DD")] = { + isBookable: availableDatesSet.has(currentDate.format("YYYY-MM-DD")), + }; - const withinBoundsSlotsMappedToDate = {} as typeof slotsMappedToDate; - const doesStartFromToday = doesRangeStartFromToday(eventType.periodType); - - for (const [date, slots] of Object.entries(slotsMappedToDate)) { - if (foundAFutureLimitViolation && doesStartFromToday) { - break; // Instead of continuing the loop, we can break since all future dates will be skipped - } - - const filteredSlots = slots.filter((slot) => { - const isFutureLimitViolationForTheSlot = isTimeViolatingFutureLimit({ - time: slot.time, - periodLimits, - }); - - if (isFutureLimitViolationForTheSlot) { - foundAFutureLimitViolation = true; - } - - return ( - !isFutureLimitViolationForTheSlot && - // TODO: Perf Optimization: Slots calculation logic already seems to consider the minimum booking notice and past booking time and thus there shouldn't be need to filter out slots here. - !isTimeOutOfBounds({ time: slot.time, minimumBookingNotice: eventType.minimumBookingNotice }) - ); - }); - - if (filteredSlots.length) { - withinBoundsSlotsMappedToDate[date] = filteredSlots; - } - } - - return withinBoundsSlotsMappedToDate; - } - const mapWithinBoundsSlotsToDate = withReporting(_mapWithinBoundsSlotsToDate, "mapWithinBoundsSlotsToDate"); - const withinBoundsSlotsMappedToDate = mapWithinBoundsSlotsToDate(); - - // We only want to run this on single targeted events and not dynamic - if (!Object.keys(withinBoundsSlotsMappedToDate).length && input.usernameList?.length === 1) { - try { - await handleNotificationWhenNoSlots({ - eventDetails: { - username: input.usernameList?.[0], - startTime: startTime, - endTime: endTime, - eventSlug: eventType.slug, - }, - orgDetails, - teamId: eventType.team?.id, - }); - } catch (e) { - loggerWithEventDetails.error( - `Something has gone wrong. Upstash could be down and we have caught the error to not block availability: - ${e}` - ); + currentDate = currentDate.add(1, "day"); } + return allDates; } - const troubleshooterData = enableTroubleshooter - ? { - troubleshooter: { - routedTeamMemberIds: routedTeamMemberIds, - // One that Salesforce asked for - askedContactOwner: contactOwnerEmailFromInput, - // One that we used as per Routing skipContactOwner flag - consideredContactOwner: contactOwnerEmail, - // All hosts that have been checked for availability. If no routedTeamMemberIds are provided, this will be same as hosts. - routedHosts: usersWithCredentials.map((user) => { - return { - userId: user.id, - }; - }), - hostsAfterSegmentMatching: allHosts.map((host) => ({ - userId: host.user.id, - })), - }, - } - : null; + private getAllDatesWithBookabilityStatus = withReporting( + this._getAllDatesWithBookabilityStatus, + "getAllDatesWithBookabilityStatus" + ); - return { - slots: withinBoundsSlotsMappedToDate, - ...troubleshooterData, - }; -}; + private async getUserIdFromUsername( + username: string, + organizationDetails: { currentOrgDomain: string | null; isValidOrgDomain: boolean } + ) { + const { currentOrgDomain, isValidOrgDomain } = organizationDetails; + log.info("getUserIdFromUsername", safeStringify({ organizationDetails, username })); + const [user] = await UserRepository.findUsersByUsername({ + usernameList: [username], + orgSlug: isValidOrgDomain ? currentOrgDomain : null, + }); + return user?.id; + } -export const getAvailableSlots = withReporting(_getAvailableSlots, "getAvailableSlots"); - -function doesRangeStartFromToday(periodType: PeriodType) { - return periodType === PeriodType.ROLLING_WINDOW || periodType === PeriodType.ROLLING; -} - -async function getUserIdFromUsername( - username: string, - organizationDetails: { currentOrgDomain: string | null; isValidOrgDomain: boolean } -) { - const { currentOrgDomain, isValidOrgDomain } = organizationDetails; - log.info("getUserIdFromUsername", safeStringify({ organizationDetails, username })); - const [user] = await UserRepository.findUsersByUsername({ - usernameList: [username], - orgSlug: isValidOrgDomain ? currentOrgDomain : null, - }); - return user?.id; -} - -async function getTeamIdFromSlug( - slug: string, - organizationDetails: { currentOrgDomain: string | null; isValidOrgDomain: boolean } -) { - const { currentOrgDomain, isValidOrgDomain } = organizationDetails; - const team = await TeamRepository.findFirstBySlugAndParentSlug({ + private async getEventTypeId({ slug, - parentSlug: isValidOrgDomain && currentOrgDomain ? currentOrgDomain : null, - select: { id: true }, - }); + eventTypeSlug, + isTeamEvent, + organizationDetails, + }: { + slug?: string; + eventTypeSlug?: string; + isTeamEvent: boolean; + organizationDetails?: { currentOrgDomain: string | null; isValidOrgDomain: boolean }; + }) { + if (!eventTypeSlug || !slug) return null; - return team?.id; -} - -const _getOOODates = async (startTimeDate: Date, endTimeDate: Date, allUserIds: number[]) => { - const oooRepo = new PrismaOOORepository(prisma); - return oooRepo.findManyOOO({ startTimeDate, endTimeDate, allUserIds }); -}; - -export const getOOODates = withReporting(_getOOODates, "getOOODates"); - -const _getAllDatesWithBookabilityStatus = (availableDates: string[]) => { - const availableDatesSet = new Set(availableDates); - const firstDate = dayjs(availableDates[0]); - const lastDate = dayjs(availableDates[availableDates.length - 1]); - const allDates: Record = {}; - - let currentDate = firstDate; - while (currentDate <= lastDate) { - allDates[currentDate.format("YYYY-MM-DD")] = { - isBookable: availableDatesSet.has(currentDate.format("YYYY-MM-DD")), - }; - - currentDate = currentDate.add(1, "day"); + let teamId; + let userId; + if (isTeamEvent) { + teamId = await this.getTeamIdFromSlug( + slug, + organizationDetails ?? { currentOrgDomain: null, isValidOrgDomain: false } + ); + } else { + userId = await this.getUserIdFromUsername( + slug, + organizationDetails ?? { currentOrgDomain: null, isValidOrgDomain: false } + ); + } + const eventType = await EventTypeRepository.findFirstEventTypeId({ slug: eventTypeSlug, teamId, userId }); + if (!eventType) { + throw new TRPCError({ code: "NOT_FOUND" }); + } + return eventType?.id; } - return allDates; -}; -export const getAllDatesWithBookabilityStatus = withReporting( - _getAllDatesWithBookabilityStatus, - "getAllDatesWithBookabilityStatus" -); + private async getTeamIdFromSlug( + slug: string, + organizationDetails: { currentOrgDomain: string | null; isValidOrgDomain: boolean } + ) { + const { currentOrgDomain, isValidOrgDomain } = organizationDetails; + const team = await TeamRepository.findFirstBySlugAndParentSlug({ + slug, + parentSlug: isValidOrgDomain && currentOrgDomain ? currentOrgDomain : null, + select: { id: true }, + }); -const _getBusyTimesFromLimitsForUsers = async ( - users: { id: number; email: string }[], - bookingLimits: IntervalLimit | null, - durationLimits: IntervalLimit | null, - dateFrom: Dayjs, - dateTo: Dayjs, - duration: number | undefined, - eventType: NonNullable, - timeZone: string, - rescheduleUid?: string -) => { - const userBusyTimesMap = new Map(); + return team?.id; + } + + private async _getBusyTimesFromLimitsForUsers( + users: { id: number; email: string }[], + bookingLimits: IntervalLimit | null, + durationLimits: IntervalLimit | null, + dateFrom: Dayjs, + dateTo: Dayjs, + duration: number | undefined, + eventType: NonNullable, + timeZone: string, + rescheduleUid?: string + ) { + const userBusyTimesMap = new Map(); + + if (!bookingLimits && !durationLimits) { + return userBusyTimesMap; + } + + const { limitDateFrom, limitDateTo } = getStartEndDateforLimitCheck( + dateFrom.toISOString(), + dateTo.toISOString(), + bookingLimits || durationLimits + ); + + const busyTimesFromLimitsBookings = await getBusyTimesForLimitChecks({ + userIds: users.map((user) => user.id), + eventTypeId: eventType.id, + startDate: limitDateFrom.format(), + endDate: limitDateTo.format(), + rescheduleUid, + bookingLimits, + durationLimits, + }); + + const globalLimitManager = new LimitManager(); + + if (bookingLimits) { + for (const key of descendingLimitKeys) { + const limit = bookingLimits?.[key]; + if (!limit) continue; + + const unit = intervalLimitKeyToUnit(key); + const periodStartDates = getPeriodStartDatesBetween(dateFrom, dateTo, unit, timeZone); + + for (const periodStart of periodStartDates) { + if (globalLimitManager.isAlreadyBusy(periodStart, unit, timeZone)) continue; + + const periodEnd = periodStart.endOf(unit); + let totalBookings = 0; + + for (const booking of busyTimesFromLimitsBookings) { + if (!isBookingWithinPeriod(booking, periodStart, periodEnd, timeZone)) { + continue; + } + + totalBookings++; + if (totalBookings >= limit) { + globalLimitManager.addBusyTime(periodStart, unit, timeZone); + break; + } + } + } + } + } + + for (const user of users) { + const userBookings = busyTimesFromLimitsBookings.filter((booking) => booking.userId === user.id); + const limitManager = new LimitManager(); + + limitManager.mergeBusyTimes(globalLimitManager); + + if (bookingLimits) { + for (const key of descendingLimitKeys) { + const limit = bookingLimits?.[key]; + if (!limit) continue; + + const unit = intervalLimitKeyToUnit(key); + const periodStartDates = getPeriodStartDatesBetween(dateFrom, dateTo, unit, timeZone); + + for (const periodStart of periodStartDates) { + if (limitManager.isAlreadyBusy(periodStart, unit, timeZone)) continue; + + if (unit === "year") { + try { + // TODO: DI checkBookingLimit + await checkBookingLimit({ + eventStartDate: periodStart.toDate(), + limitingNumber: limit, + eventId: eventType.id, + key, + user, + rescheduleUid, + timeZone, + }); + } catch (_) { + limitManager.addBusyTime(periodStart, unit, timeZone); + if ( + periodStartDates.every((start: Dayjs) => limitManager.isAlreadyBusy(start, unit, timeZone)) + ) { + break; + } + } + continue; + } + + const periodEnd = periodStart.endOf(unit); + let totalBookings = 0; + + for (const booking of userBookings) { + if (!isBookingWithinPeriod(booking, periodStart, periodEnd, timeZone)) { + continue; + } + + totalBookings++; + if (totalBookings >= limit) { + limitManager.addBusyTime(periodStart, unit, timeZone); + break; + } + } + } + } + } + + if (durationLimits) { + for (const key of descendingLimitKeys) { + const limit = durationLimits?.[key]; + if (!limit) continue; + + const unit = intervalLimitKeyToUnit(key); + const periodStartDates = getPeriodStartDatesBetween(dateFrom, dateTo, unit, timeZone); + + for (const periodStart of periodStartDates) { + if (limitManager.isAlreadyBusy(periodStart, unit, timeZone)) continue; + + const selectedDuration = (duration || eventType.length) ?? 0; + + if (selectedDuration > limit) { + limitManager.addBusyTime(periodStart, unit, timeZone); + continue; + } + + if (unit === "year") { + // TODO: DI getTotalBookingDuration + const totalYearlyDuration = await getTotalBookingDuration({ + eventId: eventType.id, + startDate: periodStart.toDate(), + endDate: periodStart.endOf(unit).toDate(), + rescheduleUid, + }); + if (totalYearlyDuration + selectedDuration > limit) { + limitManager.addBusyTime(periodStart, unit, timeZone); + if ( + periodStartDates.every((start: Dayjs) => limitManager.isAlreadyBusy(start, unit, timeZone)) + ) { + break; + } + } + continue; + } + + const periodEnd = periodStart.endOf(unit); + let totalDuration = selectedDuration; + + for (const booking of userBookings) { + if (!isBookingWithinPeriod(booking, periodStart, periodEnd, timeZone)) { + continue; + } + totalDuration += dayjs(booking.end).diff(dayjs(booking.start), "minute"); + if (totalDuration > limit) { + limitManager.addBusyTime(periodStart, unit, timeZone); + break; + } + } + } + } + } + + userBusyTimesMap.set(user.id, limitManager.getBusyTimes()); + } - if (!bookingLimits && !durationLimits) { return userBusyTimesMap; } - const { limitDateFrom, limitDateTo } = getStartEndDateforLimitCheck( - dateFrom.toISOString(), - dateTo.toISOString(), - bookingLimits || durationLimits + private getBusyTimesFromLimitsForUsers = withReporting( + this._getBusyTimesFromLimitsForUsers, + "getBusyTimesFromLimitsForUsers" ); - const busyTimesFromLimitsBookings = await getBusyTimesForLimitChecks({ - userIds: users.map((user) => user.id), - eventTypeId: eventType.id, - startDate: limitDateFrom.format(), - endDate: limitDateTo.format(), - rescheduleUid, - bookingLimits, - durationLimits, - }); + private async _getBusyTimesFromTeamLimitsForUsers( + users: { id: number; email: string }[], + bookingLimits: IntervalLimit, + dateFrom: Dayjs, + dateTo: Dayjs, + teamId: number, + includeManagedEvents: boolean, + timeZone: string, + rescheduleUid?: string + ) { + const { limitDateFrom, limitDateTo } = getStartEndDateforLimitCheck( + dateFrom.toISOString(), + dateTo.toISOString(), + bookingLimits + ); - const globalLimitManager = new LimitManager(); + const bookings = await BookingRepo.getAllAcceptedTeamBookingsOfUsers({ + users, + teamId, + startDate: limitDateFrom.toDate(), + endDate: limitDateTo.toDate(), + excludedUid: rescheduleUid, + includeManagedEvents, + }); + + const busyTimes = bookings.map(({ id, startTime, endTime, eventTypeId, title, userId }) => ({ + start: dayjs(startTime).toDate(), + end: dayjs(endTime).toDate(), + title, + source: `eventType-${eventTypeId}-booking-${id}`, + userId, + })); + + const globalLimitManager = new LimitManager(); - if (bookingLimits) { for (const key of descendingLimitKeys) { const limit = bookingLimits?.[key]; if (!limit) continue; @@ -882,7 +529,7 @@ const _getBusyTimesFromLimitsForUsers = async ( const periodEnd = periodStart.endOf(unit); let totalBookings = 0; - for (const booking of busyTimesFromLimitsBookings) { + for (const booking of busyTimes) { if (!isBookingWithinPeriod(booking, periodStart, periodEnd, timeZone)) { continue; } @@ -895,15 +542,28 @@ const _getBusyTimesFromLimitsForUsers = async ( } } } - } - for (const user of users) { - const userBookings = busyTimesFromLimitsBookings.filter((booking) => booking.userId === user.id); - const limitManager = new LimitManager(); + const userBusyTimesMap = new Map(); - limitManager.mergeBusyTimes(globalLimitManager); + for (const user of users) { + const userBusyTimes = busyTimes.filter((busyTime) => busyTime.userId === user.id); + const limitManager = new LimitManager(); + + limitManager.mergeBusyTimes(globalLimitManager); + + const bookingLimitsParams = { + bookings: userBusyTimes, + bookingLimits, + dateFrom, + dateTo, + limitManager, + rescheduleUid, + teamId, + user, + includeManagedEvents, + timeZone, + }; - if (bookingLimits) { for (const key of descendingLimitKeys) { const limit = bookingLimits?.[key]; if (!limit) continue; @@ -916,13 +576,15 @@ const _getBusyTimesFromLimitsForUsers = async ( if (unit === "year") { try { + // TODO: DI checkBookingLimit await checkBookingLimit({ eventStartDate: periodStart.toDate(), limitingNumber: limit, - eventId: eventType.id, key, + teamId, user, rescheduleUid, + includeManagedEvents, timeZone, }); } catch (_) { @@ -930,7 +592,7 @@ const _getBusyTimesFromLimitsForUsers = async ( if ( periodStartDates.every((start: Dayjs) => limitManager.isAlreadyBusy(start, unit, timeZone)) ) { - break; + return; } } continue; @@ -939,7 +601,7 @@ const _getBusyTimesFromLimitsForUsers = async ( const periodEnd = periodStart.endOf(unit); let totalBookings = 0; - for (const booking of userBookings) { + for (const booking of userBusyTimes) { if (!isBookingWithinPeriod(booking, periodStart, periodEnd, timeZone)) { continue; } @@ -952,387 +614,751 @@ const _getBusyTimesFromLimitsForUsers = async ( } } } + + userBusyTimesMap.set(user.id, limitManager.getBusyTimes()); } - if (durationLimits) { - for (const key of descendingLimitKeys) { - const limit = durationLimits?.[key]; - if (!limit) continue; - - const unit = intervalLimitKeyToUnit(key); - const periodStartDates = getPeriodStartDatesBetween(dateFrom, dateTo, unit, timeZone); - - for (const periodStart of periodStartDates) { - if (limitManager.isAlreadyBusy(periodStart, unit, timeZone)) continue; - - const selectedDuration = (duration || eventType.length) ?? 0; - - if (selectedDuration > limit) { - limitManager.addBusyTime(periodStart, unit, timeZone); - continue; - } - - if (unit === "year") { - const totalYearlyDuration = await getTotalBookingDuration({ - eventId: eventType.id, - startDate: periodStart.toDate(), - endDate: periodStart.endOf(unit).toDate(), - rescheduleUid, - }); - if (totalYearlyDuration + selectedDuration > limit) { - limitManager.addBusyTime(periodStart, unit, timeZone); - if ( - periodStartDates.every((start: Dayjs) => limitManager.isAlreadyBusy(start, unit, timeZone)) - ) { - break; - } - } - continue; - } - - const periodEnd = periodStart.endOf(unit); - let totalDuration = selectedDuration; - - for (const booking of userBookings) { - if (!isBookingWithinPeriod(booking, periodStart, periodEnd, timeZone)) { - continue; - } - totalDuration += dayjs(booking.end).diff(dayjs(booking.start), "minute"); - if (totalDuration > limit) { - limitManager.addBusyTime(periodStart, unit, timeZone); - break; - } - } - } - } - } - - userBusyTimesMap.set(user.id, limitManager.getBusyTimes()); + return userBusyTimesMap; } - - return userBusyTimesMap; -}; - -export const getBusyTimesFromLimitsForUsers = withReporting( - _getBusyTimesFromLimitsForUsers, - "getBusyTimesFromLimitsForUsers" -); - -const _getBusyTimesFromTeamLimitsForUsers = async ( - users: { id: number; email: string }[], - bookingLimits: IntervalLimit, - dateFrom: Dayjs, - dateTo: Dayjs, - teamId: number, - includeManagedEvents: boolean, - timeZone: string, - rescheduleUid?: string -) => { - const { limitDateFrom, limitDateTo } = getStartEndDateforLimitCheck( - dateFrom.toISOString(), - dateTo.toISOString(), - bookingLimits + private getBusyTimesFromTeamLimitsForUsers = withReporting( + this._getBusyTimesFromTeamLimitsForUsers, + "getBusyTimesFromTeamLimitsForUsers" ); - const bookings = await BookingRepo.getAllAcceptedTeamBookingsOfUsers({ - users, - teamId, - startDate: limitDateFrom.toDate(), - endDate: limitDateTo.toDate(), - excludedUid: rescheduleUid, - includeManagedEvents, - }); + private async _getOOODates(startTimeDate: Date, endTimeDate: Date, allUserIds: number[]) { + const oooRepo = new PrismaOOORepository(prisma); + return oooRepo.findManyOOO({ startTimeDate, endTimeDate, allUserIds }); + } + private getOOODates = withReporting(this._getOOODates, "getOOODates"); - const busyTimes = bookings.map(({ id, startTime, endTime, eventTypeId, title, userId }) => ({ - start: dayjs(startTime).toDate(), - end: dayjs(endTime).toDate(), - title, - source: `eventType-${eventTypeId}-booking-${id}`, - userId, - })); - - const globalLimitManager = new LimitManager(); - - for (const key of descendingLimitKeys) { - const limit = bookingLimits?.[key]; - if (!limit) continue; - - const unit = intervalLimitKeyToUnit(key); - const periodStartDates = getPeriodStartDatesBetween(dateFrom, dateTo, unit, timeZone); - - for (const periodStart of periodStartDates) { - if (globalLimitManager.isAlreadyBusy(periodStart, unit, timeZone)) continue; - - const periodEnd = periodStart.endOf(unit); - let totalBookings = 0; - - for (const booking of busyTimes) { - if (!isBookingWithinPeriod(booking, periodStart, periodEnd, timeZone)) { - continue; - } - - totalBookings++; - if (totalBookings >= limit) { - globalLimitManager.addBusyTime(periodStart, unit, timeZone); - break; - } - } - } + private _getUsersWithCredentials({ + hosts, + }: { + hosts: { + isFixed?: boolean; + user: GetAvailabilityUserWithDelegationCredentials; + }[]; + }) { + return hosts.map(({ isFixed, user }) => ({ isFixed, ...user })); } - const userBusyTimesMap = new Map(); + private getUsersWithCredentials = withReporting(this._getUsersWithCredentials, "getUsersWithCredentials"); - for (const user of users) { - const userBusyTimes = busyTimes.filter((busyTime) => busyTime.userId === user.id); - const limitManager = new LimitManager(); + private getStartTime(startTimeInput: string, timeZone?: string, minimumBookingNotice?: number) { + const startTimeMin = dayjs.utc().add(minimumBookingNotice || 1, "minutes"); + const startTime = timeZone === "Etc/GMT" ? dayjs.utc(startTimeInput) : dayjs(startTimeInput).tz(timeZone); - limitManager.mergeBusyTimes(globalLimitManager); + return startTimeMin.isAfter(startTime) ? startTimeMin.tz(timeZone) : startTime; + } + private async calculateHostsAndAvailabilities({ + input, + eventType, + hosts, + loggerWithEventDetails, + startTime, + endTime, + bypassBusyCalendarTimes, + shouldServeCache, + }: { + input: TGetScheduleInputSchema; + eventType: Exclude< + Awaited>, + null + >; + hosts: { + isFixed?: boolean; + user: GetAvailabilityUserWithDelegationCredentials; + }[]; + loggerWithEventDetails: Logger; + startTime: ReturnType<(typeof AvailableSlotsService)["prototype"]["getStartTime"]>; + endTime: Dayjs; + bypassBusyCalendarTimes: boolean; + shouldServeCache?: boolean; + }) { + const usersWithCredentials = this.getUsersWithCredentials({ + hosts, + }); - const bookingLimitsParams = { - bookings: userBusyTimes, - bookingLimits, - dateFrom, - dateTo, - limitManager, - rescheduleUid, - teamId, - user, - includeManagedEvents, - timeZone, + loggerWithEventDetails.debug("Using users", { + usersWithCredentials: usersWithCredentials.map((user) => user.email), + }); + + const durationToUse = input.duration || 0; + let currentSeats: CurrentSeats | undefined; + + const startTimeDate = + input.rescheduleUid && durationToUse + ? startTime.subtract(durationToUse, "minute").toDate() + : startTime.toDate(); + + const endTimeDate = + input.rescheduleUid && durationToUse ? endTime.add(durationToUse, "minute").toDate() : endTime.toDate(); + + const userIdAndEmailMap = new Map(usersWithCredentials.map((user) => [user.id, user.email])); + const allUserIds = Array.from(userIdAndEmailMap.keys()); + + const [currentBookingsAllUsers, outOfOfficeDaysAllUsers] = await Promise.all([ + BookingRepo.findAllExistingBookingsForEventTypeBetween({ + startDate: startTimeDate, + endDate: endTimeDate, + eventTypeId: eventType.id, + seatedEvent: Boolean(eventType.seatsPerTimeSlot), + userIdAndEmailMap, + }), + this.getOOODates(startTimeDate, endTimeDate, allUserIds), + ]); + + const bookingLimits = parseBookingLimit(eventType?.bookingLimits); + const durationLimits = parseDurationLimit(eventType?.durationLimits); + let busyTimesFromLimitsBookingsAllUsers: Awaited> = []; + + if (eventType && (bookingLimits || durationLimits)) { + // TODO: DI getBusyTimesForLimitChecks + busyTimesFromLimitsBookingsAllUsers = await getBusyTimesForLimitChecks({ + userIds: allUserIds, + eventTypeId: eventType.id, + startDate: startTime.format(), + endDate: endTime.format(), + rescheduleUid: input.rescheduleUid, + bookingLimits, + durationLimits, + }); + } + + let busyTimesFromLimitsMap: Map | undefined = undefined; + if (eventType && (bookingLimits || durationLimits)) { + const usersForLimits = usersWithCredentials.map((user) => ({ id: user.id, email: user.email })); + busyTimesFromLimitsMap = await this.getBusyTimesFromLimitsForUsers( + usersForLimits, + bookingLimits, + durationLimits, + startTime, + endTime, + typeof input.duration === "number" ? input.duration : undefined, + eventType, + usersWithCredentials[0]?.timeZone || "UTC", + input.rescheduleUid || undefined + ); + } + + const teamForBookingLimits = + eventType?.team ?? + (eventType?.parent?.team?.includeManagedEventsInLimits ? eventType?.parent?.team : null); + + const teamBookingLimits = parseBookingLimit(teamForBookingLimits?.bookingLimits); + + let teamBookingLimitsMap: Map | undefined = undefined; + if (teamForBookingLimits && teamBookingLimits) { + const usersForTeamLimits = usersWithCredentials.map((user) => ({ id: user.id, email: user.email })); + teamBookingLimitsMap = await this.getBusyTimesFromTeamLimitsForUsers( + usersForTeamLimits, + teamBookingLimits, + startTime, + endTime, + teamForBookingLimits.id, + teamForBookingLimits.includeManagedEventsInLimits, + usersWithCredentials[0]?.timeZone || "UTC", + input.rescheduleUid || undefined + ); + } + + function _enrichUsersWithData() { + return usersWithCredentials.map((currentUser) => { + return { + ...currentUser, + currentBookings: currentBookingsAllUsers + .filter( + (b) => b.userId === currentUser.id || b.attendees?.some((a) => a.email === currentUser.email) + ) + .map((bookings) => { + const { attendees: _attendees, ...bookingWithoutAttendees } = bookings; + return bookingWithoutAttendees; + }), + outOfOfficeDays: outOfOfficeDaysAllUsers.filter((o) => o.user.id === currentUser.id), + }; + }); + } + const enrichUsersWithData = withReporting(_enrichUsersWithData, "enrichUsersWithData"); + const users = enrichUsersWithData(); + + // TODO: DI getUsersAvailability + const premappedUsersAvailability = await getUsersAvailability({ + users, + query: { + dateFrom: startTime.format(), + dateTo: endTime.format(), + eventTypeId: eventType.id, + afterEventBuffer: eventType.afterEventBuffer, + beforeEventBuffer: eventType.beforeEventBuffer, + duration: input.duration || 0, + returnDateOverrides: false, + bypassBusyCalendarTimes, + shouldServeCache, + }, + initialData: { + eventType, + currentSeats, + rescheduleUid: input.rescheduleUid, + busyTimesFromLimitsBookings: busyTimesFromLimitsBookingsAllUsers, + busyTimesFromLimits: busyTimesFromLimitsMap, + eventTypeForLimits: eventType && (bookingLimits || durationLimits) ? eventType : null, + teamBookingLimits: teamBookingLimitsMap, + teamForBookingLimits: teamForBookingLimits, + }, + }); + /* We get all users working hours and busy slots */ + const allUsersAvailability = premappedUsersAvailability.map( + ( + { busy, dateRanges, oooExcludedDateRanges, currentSeats: _currentSeats, timeZone, datesOutOfOffice }, + index + ) => { + const currentUser = users[index]; + if (!currentSeats && _currentSeats) currentSeats = _currentSeats; + return { + timeZone, + dateRanges, + oooExcludedDateRanges, + busy, + user: currentUser, + datesOutOfOffice, + }; + } + ); + + return { + allUsersAvailability, + usersWithCredentials, + currentSeats, + }; + } + + private async _getRegularOrDynamicEventType( + input: TGetScheduleInputSchema, + organizationDetails: { currentOrgDomain: string | null; isValidOrgDomain: boolean } + ) { + const isDynamicBooking = input.usernameList && input.usernameList.length > 1; + return isDynamicBooking + ? await this.getDynamicEventType(input, organizationDetails) + : await this.getEventType(input, organizationDetails); + } + + private getRegularOrDynamicEventType = withReporting( + this._getRegularOrDynamicEventType, + "getRegularOrDynamicEventType" + ); + + getAvailableSlots = withReporting(this._getAvailableSlots, "getAvailableSlots"); + + async _getAvailableSlots({ input, ctx }: GetScheduleOptions): Promise { + const { + _enableTroubleshooter: enableTroubleshooter = false, + _bypassCalendarBusyTimes: bypassBusyCalendarTimes = false, + _shouldServeCache, + routingFormResponseId, + queuedFormResponseId, + } = input; + const orgDetails = input?.orgSlug + ? { + currentOrgDomain: input.orgSlug, + isValidOrgDomain: !!input.orgSlug && !RESERVED_SUBDOMAINS.includes(input.orgSlug), + } + : orgDomainConfig(ctx?.req); + + if (process.env.INTEGRATION_TEST_MODE === "true") { + logger.settings.minLevel = 2; + } + + const eventType = await this.getRegularOrDynamicEventType(input, orgDetails); + + if (!eventType) { + throw new TRPCError({ code: "NOT_FOUND" }); + } + + // TODO: DI getShouldServeCache + const shouldServeCache = await getShouldServeCache(_shouldServeCache, eventType.team?.id); + if (isEventTypeLoggingEnabled({ eventTypeId: eventType.id })) { + logger.settings.minLevel = 2; + } + + const isRollingWindowPeriodType = eventType.periodType === PeriodType.ROLLING_WINDOW; + const startTimeAsIsoString = input.startTime; + const isStartTimeInPast = dayjs(startTimeAsIsoString).isBefore(dayjs().subtract(1, "day").startOf("day")); + + // If startTime is already sent in the past, we don't need to adjust it. + // We assume that the client is already sending startTime as per their requirement. + // Note: We could optimize it further to go back 1 month in past only for the 2nd month because that is what we are putting a hard limit at. + const startTimeAdjustedForRollingWindowComputation = + isStartTimeInPast || !isRollingWindowPeriodType + ? startTimeAsIsoString + : dayjs(startTimeAsIsoString).subtract(1, "month").toISOString(); + + const loggerWithEventDetails = logger.getSubLogger({ + type: "json", + prefix: ["getAvailableSlots", `${eventType.id}:${input.usernameList}/${input.eventTypeSlug}`], + }); + + const startTime = this.getStartTime( + startTimeAdjustedForRollingWindowComputation, + input.timeZone, + eventType.minimumBookingNotice + ); + const endTime = + input.timeZone === "Etc/GMT" ? dayjs.utc(input.endTime) : dayjs(input.endTime).utc().tz(input.timeZone); + + if (!startTime.isValid() || !endTime.isValid()) { + throw new TRPCError({ message: "Invalid time range given.", code: "BAD_REQUEST" }); + } + // when an empty array is given we should prefer to have it handled as if this wasn't given at all + // we don't want to return no availability in this case. + const routedTeamMemberIds = input.routedTeamMemberIds ?? []; + const contactOwnerEmailFromInput = input.teamMemberEmail ?? null; + + const skipContactOwner = shouldIgnoreContactOwner({ + skipContactOwner: input.skipContactOwner ?? null, + rescheduleUid: input.rescheduleUid ?? null, + routedTeamMemberIds: input.routedTeamMemberIds ?? null, + }); + + const contactOwnerEmail = skipContactOwner ? null : contactOwnerEmailFromInput; + + let routingFormResponse = null; + if (routingFormResponseId) { + routingFormResponse = await RoutingFormResponseRepository.findFormResponseIncludeForm({ + routingFormResponseId, + }); + } else if (queuedFormResponseId) { + routingFormResponse = await RoutingFormResponseRepository.findQueuedFormResponseIncludeForm({ + queuedFormResponseId, + }); + } + + // TODO: DI findQualifiedHostsWithDelegationCredentials + const { qualifiedRRHosts, allFallbackRRHosts, fixedHosts } = + await findQualifiedHostsWithDelegationCredentials({ + eventType, + rescheduleUid: input.rescheduleUid ?? null, + routedTeamMemberIds, + contactOwnerEmail, + routingFormResponse, + }); + + const allHosts = [...qualifiedRRHosts, ...fixedHosts]; + + const twoWeeksFromNow = dayjs().add(2, "week"); + + const hasFallbackRRHosts = allFallbackRRHosts && allFallbackRRHosts.length > qualifiedRRHosts.length; + + let { allUsersAvailability, usersWithCredentials, currentSeats } = + await this.calculateHostsAndAvailabilities({ + input, + eventType, + hosts: allHosts, + loggerWithEventDetails, + // adjust start time so we can check for available slots in the first two weeks + startTime: + hasFallbackRRHosts && startTime.isBefore(twoWeeksFromNow) + ? this.getStartTime(dayjs().format(), input.timeZone, eventType.minimumBookingNotice) + : startTime, + // adjust end time so we can check for available slots in the first two weeks + endTime: + hasFallbackRRHosts && endTime.isBefore(twoWeeksFromNow) + ? this.getStartTime(twoWeeksFromNow.format(), input.timeZone, eventType.minimumBookingNotice) + : endTime, + bypassBusyCalendarTimes, + shouldServeCache, + }); + + let aggregatedAvailability = getAggregatedAvailability(allUsersAvailability, eventType.schedulingType); + + // Fairness and Contact Owner have fallbacks because we check for within 2 weeks + if (hasFallbackRRHosts) { + let diff = 0; + if (startTime.isBefore(twoWeeksFromNow)) { + //check if first two week have availability + diff = + aggregatedAvailability.length > 0 + ? aggregatedAvailability[0].start.diff(twoWeeksFromNow, "day") + : 1; // no aggregatedAvailability so we diff to +1 + } else { + // if start time is not within first two weeks, check if there are any available slots + if (!aggregatedAvailability.length) { + // if no available slots check if first two weeks are available, otherwise fallback + const firstTwoWeeksAvailabilities = await this.calculateHostsAndAvailabilities({ + input, + eventType, + hosts: [...qualifiedRRHosts, ...fixedHosts], + loggerWithEventDetails, + startTime: dayjs(), + endTime: twoWeeksFromNow, + bypassBusyCalendarTimes, + shouldServeCache, + }); + if ( + !getAggregatedAvailability( + firstTwoWeeksAvailabilities.allUsersAvailability, + eventType.schedulingType + ).length + ) { + diff = 1; + } + } + } + + if (input.email) { + loggerWithEventDetails.info({ + email: input.email, + contactOwnerEmail, + qualifiedRRHosts: qualifiedRRHosts.map((host) => host.user.id), + fallbackRRHosts: allFallbackRRHosts.map((host) => host.user.id), + fallBackActive: diff > 0, + }); + } + + if (diff > 0) { + // if the first available slot is more than 2 weeks from now, round robin as normal + ({ allUsersAvailability, usersWithCredentials, currentSeats } = + await this.calculateHostsAndAvailabilities({ + input, + eventType, + hosts: [...allFallbackRRHosts, ...fixedHosts], + loggerWithEventDetails, + startTime, + endTime, + bypassBusyCalendarTimes, + shouldServeCache, + })); + aggregatedAvailability = getAggregatedAvailability(allUsersAvailability, eventType.schedulingType); + } + } + + const isTeamEvent = + eventType.schedulingType === SchedulingType.COLLECTIVE || + eventType.schedulingType === SchedulingType.ROUND_ROBIN || + allUsersAvailability.length > 1; + + const timeSlots = getSlots({ + inviteeDate: startTime, + eventLength: input.duration || eventType.length, + offsetStart: eventType.offsetStart, + dateRanges: aggregatedAvailability, + minimumBookingNotice: eventType.minimumBookingNotice, + frequency: eventType.slotInterval || input.duration || eventType.length, + datesOutOfOffice: !isTeamEvent ? allUsersAvailability[0]?.datesOutOfOffice : undefined, + }); + + let availableTimeSlots: typeof timeSlots = []; + const bookerClientUid = ctx?.req?.cookies?.uid; + // TODO: DI isRestrictionScheduleEnabled + const isRestrictionScheduleFeatureEnabled = await isRestrictionScheduleEnabled(eventType.team?.id); + if (eventType.restrictionScheduleId && isRestrictionScheduleFeatureEnabled) { + const scheduleRepo = new ScheduleRepository(prisma); + const restrictionSchedule = await scheduleRepo.findScheduleByIdForBuildDateRanges({ + scheduleId: eventType.restrictionScheduleId, + }); + if (restrictionSchedule) { + if (!eventType.useBookerTimezone && !restrictionSchedule.timeZone) { + throw new TRPCError({ + message: "No timezone is set for the restricted schedule", + code: "BAD_REQUEST", + }); + } + + const restrictionTimezone = eventType.useBookerTimezone + ? input.timeZone + : restrictionSchedule.timeZone!; + const eventLength = input.duration || eventType.length; + + const restrictionAvailability = restrictionSchedule.availability.map((rule) => ({ + days: rule.days, + startTime: rule.startTime, + endTime: rule.endTime, + date: rule.date, + })); + + // Include travel schedules if restriction schedule is the user's default schedule + const isDefaultSchedule = restrictionSchedule.user.defaultScheduleId === restrictionSchedule.id; + const travelSchedules = + isDefaultSchedule && !eventType.useBookerTimezone + ? restrictionSchedule.user.travelSchedules.map((schedule) => ({ + startDate: dayjs(schedule.startDate), + endDate: schedule.endDate ? dayjs(schedule.endDate) : undefined, + timeZone: schedule.timeZone, + })) + : []; + + const { dateRanges: restrictionRanges } = buildDateRanges({ + availability: restrictionAvailability, + timeZone: restrictionTimezone || "UTC", + dateFrom: startTime, + dateTo: endTime, + travelSchedules, + }); + + availableTimeSlots = timeSlots.filter((slot) => { + const slotStart = slot.time; + const slotEnd = slot.time.add(eventLength, "minute"); + + return restrictionRanges.some( + (range) => + (slotStart.isAfter(range.start) || slotStart.isSame(range.start)) && + (slotEnd.isBefore(range.end) || slotEnd.isSame(range.end)) + ); + }); + } else { + availableTimeSlots = timeSlots; + } + } else { + availableTimeSlots = timeSlots; + } + + const reservedSlots = await this._getReservedSlotsAndCleanupExpired({ + bookerClientUid, + eventTypeId: eventType.id, + usersWithCredentials, + }); + + const availabilityCheckProps = { + eventLength: input.duration || eventType.length, + currentSeats, }; - for (const key of descendingLimitKeys) { - const limit = bookingLimits?.[key]; - if (!limit) continue; - - const unit = intervalLimitKeyToUnit(key); - const periodStartDates = getPeriodStartDatesBetween(dateFrom, dateTo, unit, timeZone); - - for (const periodStart of periodStartDates) { - if (limitManager.isAlreadyBusy(periodStart, unit, timeZone)) continue; - - if (unit === "year") { - try { - await checkBookingLimit({ - eventStartDate: periodStart.toDate(), - limitingNumber: limit, - key, - teamId, - user, - rescheduleUid, - includeManagedEvents, - timeZone, - }); - } catch (_) { - limitManager.addBusyTime(periodStart, unit, timeZone); - if (periodStartDates.every((start: Dayjs) => limitManager.isAlreadyBusy(start, unit, timeZone))) { - return; - } - } - continue; + if (reservedSlots?.length > 0) { + let occupiedSeats: typeof reservedSlots = reservedSlots.filter( + (item) => item.isSeat && item.eventTypeId === eventType.id + ); + if (occupiedSeats?.length) { + const addedToCurrentSeats: string[] = []; + if (typeof availabilityCheckProps.currentSeats !== "undefined") { + availabilityCheckProps.currentSeats = availabilityCheckProps.currentSeats.map((item) => { + const attendees = + occupiedSeats.filter( + (seat) => seat.slotUtcStartDate.toISOString() === item.startTime.toISOString() + )?.length || 0; + if (attendees) addedToCurrentSeats.push(item.startTime.toISOString()); + return { + ...item, + _count: { + attendees: item._count.attendees + attendees, + }, + }; + }); + occupiedSeats = occupiedSeats.filter( + (item) => !addedToCurrentSeats.includes(item.slotUtcStartDate.toISOString()) + ); } - const periodEnd = periodStart.endOf(unit); - let totalBookings = 0; + availabilityCheckProps.currentSeats = this.applyOccupiedSeatsToCurrentSeats( + availabilityCheckProps.currentSeats || [], + occupiedSeats + ); - for (const booking of userBusyTimes) { - if (!isBookingWithinPeriod(booking, periodStart, periodEnd, timeZone)) { - continue; - } - - totalBookings++; - if (totalBookings >= limit) { - limitManager.addBusyTime(periodStart, unit, timeZone); - break; - } + currentSeats = availabilityCheckProps.currentSeats; + } + const busySlotsFromReservedSlots = reservedSlots.reduce((r, c) => { + if (!c.isSeat) { + r.push({ start: c.slotUtcStartDate, end: c.slotUtcEndDate }); } + return r; + }, []); + + availableTimeSlots = availableTimeSlots + .map((slot) => { + if ( + !checkForConflicts({ + time: slot.time, + busy: busySlotsFromReservedSlots, + ...availabilityCheckProps, + }) + ) { + return slot; + } + return undefined; + }) + .filter( + ( + item: + | { + time: dayjs.Dayjs; + userIds?: number[] | undefined; + } + | undefined + ): item is { + time: dayjs.Dayjs; + userIds?: number[] | undefined; + } => { + return !!item; + } + ); + } + + // fr-CA uses YYYY-MM-DD + const formatter = new Intl.DateTimeFormat("fr-CA", { + year: "numeric", + month: "2-digit", + day: "2-digit", + timeZone: input.timeZone, + }); + + function _mapSlotsToDate() { + const currentSeatsMap = new Map(); + + if (currentSeats && currentSeats.length > 0) { + currentSeats.forEach((booking) => { + const timeKey = booking.startTime.toISOString(); + currentSeatsMap.set(timeKey, { + attendees: booking._count.attendees, + uid: booking.uid, + }); + }); + } + + return availableTimeSlots.reduce( + ( + r: Record, + { time, ...passThroughProps } + ) => { + // This used to be _time.tz(input.timeZone) but Dayjs tz() is slow. + // toLocaleDateString slugish, using Intl.DateTimeFormat we get the desired speed results. + const dateString = formatter.format(time.toDate()); + const timeISO = time.toISOString(); + + r[dateString] = r[dateString] || []; + if (eventType?.onlyShowFirstAvailableSlot && r[dateString].length > 0) { + return r; + } + + const existingBooking = currentSeatsMap.get(timeISO); + + r[dateString].push({ + ...passThroughProps, + time: timeISO, + ...(existingBooking && { + attendees: existingBooking.attendees, + bookingUid: existingBooking.uid, + }), + }); + return r; + }, + Object.create(null) + ); + } + const mapSlotsToDate = withReporting(_mapSlotsToDate, "mapSlotsToDate"); + const slotsMappedToDate = mapSlotsToDate(); + + loggerWithEventDetails.debug({ slotsMappedToDate }); + + const availableDates = Object.keys(slotsMappedToDate); + const allDatesWithBookabilityStatus = this.getAllDatesWithBookabilityStatus(availableDates); + loggerWithEventDetails.debug({ availableDates }); + + // timeZone isn't directly set on eventType now(So, it is legacy) + // schedule is always expected to be set for an eventType now so it must never fallback to allUsersAvailability[0].timeZone(fallback is again legacy behavior) + // TODO: Also, handleNewBooking only seems to be using eventType?.schedule?.timeZone which seems to confirm that we should simplify it as well. + const eventTimeZone = + eventType.timeZone || eventType?.schedule?.timeZone || allUsersAvailability?.[0]?.timeZone; + + const eventUtcOffset = getUTCOffsetByTimezone(eventTimeZone) ?? 0; + const bookerUtcOffset = input.timeZone ? getUTCOffsetByTimezone(input.timeZone) ?? 0 : 0; + const periodLimits = calculatePeriodLimits({ + periodType: eventType.periodType, + periodDays: eventType.periodDays, + periodCountCalendarDays: eventType.periodCountCalendarDays, + periodStartDate: eventType.periodStartDate, + periodEndDate: eventType.periodEndDate, + allDatesWithBookabilityStatusInBookerTz: allDatesWithBookabilityStatus, + eventUtcOffset, + bookerUtcOffset, + }); + + const _mapWithinBoundsSlotsToDate = () => { + let foundAFutureLimitViolation = false; + // This should never happen. Just for type safety, we already check in the upper scope + if (!eventType) throw new TRPCError({ code: "NOT_FOUND" }); + + const withinBoundsSlotsMappedToDate = {} as typeof slotsMappedToDate; + const doesStartFromToday = this.doesRangeStartFromToday(eventType.periodType); + + for (const [date, slots] of Object.entries(slotsMappedToDate)) { + if (foundAFutureLimitViolation && doesStartFromToday) { + break; // Instead of continuing the loop, we can break since all future dates will be skipped + } + + const filteredSlots = slots.filter((slot) => { + const isFutureLimitViolationForTheSlot = isTimeViolatingFutureLimit({ + time: slot.time, + periodLimits, + }); + + if (isFutureLimitViolationForTheSlot) { + foundAFutureLimitViolation = true; + } + + return ( + !isFutureLimitViolationForTheSlot && + // TODO: Perf Optimization: Slots calculation logic already seems to consider the minimum booking notice and past booking time and thus there shouldn't be need to filter out slots here. + !isTimeOutOfBounds({ time: slot.time, minimumBookingNotice: eventType.minimumBookingNotice }) + ); + }); + + if (filteredSlots.length) { + withinBoundsSlotsMappedToDate[date] = filteredSlots; + } + } + + return withinBoundsSlotsMappedToDate; + }; + const mapWithinBoundsSlotsToDate = withReporting( + _mapWithinBoundsSlotsToDate, + "mapWithinBoundsSlotsToDate" + ); + const withinBoundsSlotsMappedToDate = mapWithinBoundsSlotsToDate(); + + // We only want to run this on single targeted events and not dynamic + if (!Object.keys(withinBoundsSlotsMappedToDate).length && input.usernameList?.length === 1) { + try { + // TODO: DI handleNotificationWhenNoSlots + await handleNotificationWhenNoSlots({ + eventDetails: { + username: input.usernameList?.[0], + startTime: startTime, + endTime: endTime, + eventSlug: eventType.slug, + }, + orgDetails, + teamId: eventType.team?.id, + }); + } catch (e) { + loggerWithEventDetails.error( + `Something has gone wrong. Upstash could be down and we have caught the error to not block availability: + ${e}` + ); } } - userBusyTimesMap.set(user.id, limitManager.getBusyTimes()); + const troubleshooterData = enableTroubleshooter + ? { + troubleshooter: { + routedTeamMemberIds: routedTeamMemberIds, + // One that Salesforce asked for + askedContactOwner: contactOwnerEmailFromInput, + // One that we used as per Routing skipContactOwner flag + consideredContactOwner: contactOwnerEmail, + // All hosts that have been checked for availability. If no routedTeamMemberIds are provided, this will be same as hosts. + routedHosts: usersWithCredentials.map((user) => { + return { + userId: user.id, + }; + }), + hostsAfterSegmentMatching: allHosts.map((host) => ({ + userId: host.user.id, + })), + }, + } + : null; + + return { + slots: withinBoundsSlotsMappedToDate, + ...troubleshooterData, + }; } - - return userBusyTimesMap; -}; - -export const getBusyTimesFromTeamLimitsForUsers = withReporting( - _getBusyTimesFromTeamLimitsForUsers, - "getBusyTimesFromTeamLimitsForUsers" -); - -const calculateHostsAndAvailabilities = async ({ - input, - eventType, - hosts, - loggerWithEventDetails, - startTime, - endTime, - bypassBusyCalendarTimes, - shouldServeCache, -}: { - input: TGetScheduleInputSchema; - eventType: Exclude>, null>; - hosts: { - isFixed?: boolean; - user: GetAvailabilityUserWithDelegationCredentials; - }[]; - loggerWithEventDetails: Logger; - startTime: ReturnType; - endTime: Dayjs; - bypassBusyCalendarTimes: boolean; - shouldServeCache?: boolean; -}) => { - const usersWithCredentials = getUsersWithCredentials({ - hosts, - }); - - loggerWithEventDetails.debug("Using users", { - usersWithCredentials: usersWithCredentials.map((user) => user.email), - }); - - const durationToUse = input.duration || 0; - let currentSeats: CurrentSeats | undefined; - - const startTimeDate = - input.rescheduleUid && durationToUse - ? startTime.subtract(durationToUse, "minute").toDate() - : startTime.toDate(); - - const endTimeDate = - input.rescheduleUid && durationToUse ? endTime.add(durationToUse, "minute").toDate() : endTime.toDate(); - - const userIdAndEmailMap = new Map(usersWithCredentials.map((user) => [user.id, user.email])); - const allUserIds = Array.from(userIdAndEmailMap.keys()); - - const [currentBookingsAllUsers, outOfOfficeDaysAllUsers] = await Promise.all([ - BookingRepo.findAllExistingBookingsForEventTypeBetween({ - startDate: startTimeDate, - endDate: endTimeDate, - eventTypeId: eventType.id, - seatedEvent: Boolean(eventType.seatsPerTimeSlot), - userIdAndEmailMap, - }), - getOOODates(startTimeDate, endTimeDate, allUserIds), - ]); - - const bookingLimits = parseBookingLimit(eventType?.bookingLimits); - const durationLimits = parseDurationLimit(eventType?.durationLimits); - let busyTimesFromLimitsBookingsAllUsers: Awaited> = []; - - if (eventType && (bookingLimits || durationLimits)) { - busyTimesFromLimitsBookingsAllUsers = await getBusyTimesForLimitChecks({ - userIds: allUserIds, - eventTypeId: eventType.id, - startDate: startTime.format(), - endDate: endTime.format(), - rescheduleUid: input.rescheduleUid, - bookingLimits, - durationLimits, - }); - } - - let busyTimesFromLimitsMap: Map | undefined = undefined; - if (eventType && (bookingLimits || durationLimits)) { - const usersForLimits = usersWithCredentials.map((user) => ({ id: user.id, email: user.email })); - busyTimesFromLimitsMap = await getBusyTimesFromLimitsForUsers( - usersForLimits, - bookingLimits, - durationLimits, - startTime, - endTime, - typeof input.duration === "number" ? input.duration : undefined, - eventType, - usersWithCredentials[0]?.timeZone || "UTC", - input.rescheduleUid || undefined - ); - } - - const teamForBookingLimits = - eventType?.team ?? - (eventType?.parent?.team?.includeManagedEventsInLimits ? eventType?.parent?.team : null); - - const teamBookingLimits = parseBookingLimit(teamForBookingLimits?.bookingLimits); - - let teamBookingLimitsMap: Map | undefined = undefined; - if (teamForBookingLimits && teamBookingLimits) { - const usersForTeamLimits = usersWithCredentials.map((user) => ({ id: user.id, email: user.email })); - teamBookingLimitsMap = await getBusyTimesFromTeamLimitsForUsers( - usersForTeamLimits, - teamBookingLimits, - startTime, - endTime, - teamForBookingLimits.id, - teamForBookingLimits.includeManagedEventsInLimits, - usersWithCredentials[0]?.timeZone || "UTC", - input.rescheduleUid || undefined - ); - } - - function _enrichUsersWithData() { - return usersWithCredentials.map((currentUser) => { - return { - ...currentUser, - currentBookings: currentBookingsAllUsers - .filter( - (b) => b.userId === currentUser.id || b.attendees?.some((a) => a.email === currentUser.email) - ) - .map((bookings) => { - const { attendees: _attendees, ...bookingWithoutAttendees } = bookings; - return bookingWithoutAttendees; - }), - outOfOfficeDays: outOfOfficeDaysAllUsers.filter((o) => o.user.id === currentUser.id), - }; - }); - } - const enrichUsersWithData = withReporting(_enrichUsersWithData, "enrichUsersWithData"); - const users = enrichUsersWithData(); - - const premappedUsersAvailability = await getUsersAvailability({ - users, - query: { - dateFrom: startTime.format(), - dateTo: endTime.format(), - eventTypeId: eventType.id, - afterEventBuffer: eventType.afterEventBuffer, - beforeEventBuffer: eventType.beforeEventBuffer, - duration: input.duration || 0, - returnDateOverrides: false, - bypassBusyCalendarTimes, - shouldServeCache, - }, - initialData: { - eventType, - currentSeats, - rescheduleUid: input.rescheduleUid, - busyTimesFromLimitsBookings: busyTimesFromLimitsBookingsAllUsers, - busyTimesFromLimits: busyTimesFromLimitsMap, - eventTypeForLimits: eventType && (bookingLimits || durationLimits) ? eventType : null, - teamBookingLimits: teamBookingLimitsMap, - teamForBookingLimits: teamForBookingLimits, - }, - }); - /* We get all users working hours and busy slots */ - const allUsersAvailability = premappedUsersAvailability.map( - ( - { busy, dateRanges, oooExcludedDateRanges, currentSeats: _currentSeats, timeZone, datesOutOfOffice }, - index - ) => { - const currentUser = users[index]; - if (!currentSeats && _currentSeats) currentSeats = _currentSeats; - return { - timeZone, - dateRanges, - oooExcludedDateRanges, - busy, - user: currentUser, - datesOutOfOffice, - }; - } - ); - - return { - allUsersAvailability, - usersWithCredentials, - currentSeats, - }; -}; +} diff --git a/yarn.lock b/yarn.lock index d7981f9b19..a83ee2ac9c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2519,7 +2519,7 @@ __metadata: "@axiomhq/winston": ^1.2.0 "@calcom/platform-constants": "*" "@calcom/platform-enums": "*" - "@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.241" + "@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.243" "@calcom/platform-types": "*" "@calcom/platform-utils": "*" "@calcom/prisma": "*" @@ -3565,13 +3565,13 @@ __metadata: languageName: unknown linkType: soft -"@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.241": - version: 0.0.241 - resolution: "@calcom/platform-libraries@npm:0.0.241" +"@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.243": + version: 0.0.243 + resolution: "@calcom/platform-libraries@npm:0.0.243" dependencies: "@calcom/features": "*" "@calcom/lib": "*" - checksum: 1eec9dce749b6678427e00d0f2d2f383d798066f2f7ef7aca1ce90d5c649ed41d2fc1496be93c7448e7c22d2e015e333c8fe0b81b7c3b04b9637f6ea1910a898 + checksum: ce0a4778857a62a6df46b1c0b419fbaad1d874d7b0da92d43589348f6e07a7de912703692c9fbc1e21fb2e0bf4d323bcd7eae96b5198376216216c872374cc09 languageName: node linkType: hard @@ -5816,6 +5816,13 @@ __metadata: languageName: node linkType: hard +"@evyweb/ioctopus@npm:^1.2.0": + version: 1.2.0 + resolution: "@evyweb/ioctopus@npm:1.2.0" + checksum: f48382e2dfab9b48b59d02a0718ddbb8e9d81cbb6d214eb42b5378c2b9d9f9f8decb5b13019600972252e5b6c204901ebfbc36000a4a8ea0f69d6fd8b3417458 + languageName: node + linkType: hard + "@ewsjs/ntlm-client@npm:^3.0.1": version: 3.0.1 resolution: "@ewsjs/ntlm-client@npm:3.0.1" @@ -21766,6 +21773,7 @@ __metadata: "@changesets/changelog-github": ^0.5.1 "@changesets/cli": 2.29.4 "@daily-co/daily-js": ^0.76.0 + "@evyweb/ioctopus": ^1.2.0 "@jetstreamapp/soql-parser-js": ^6.1.0 "@next/third-parties": ^14.2.5 "@playwright/test": ^1.45.3