diff --git a/apps/api/v1/pages/api/slots/_get.ts b/apps/api/v1/pages/api/slots/_get.ts index 6c402cd9fa..d0278bbe58 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 { AvailableSlotsService } from "@calcom/trpc/server/routers/viewer/slots/util"; +import { getAvailableSlots } from "@calcom/trpc/server/routers/viewer/slots/util"; import { TRPCError } from "@trpc/server"; import { getHTTPStatusCodeFromError } from "@trpc/server/http"; @@ -27,11 +27,7 @@ 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 availableSlotsService = new AvailableSlotsService(); - const availableSlots = await availableSlotsService.getAvailableSlots({ - ctx: await createContext({ req, res }), - input, - }); + const availableSlots = await 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 8fcc5fd92f..6fda2e479c 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.243", + "@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.241", "@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 2c4c1f0f12..7197f25b30 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 { AvailableSlotsService } from "@calcom/platform-libraries/slots"; +import { getAvailableSlots } 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,10 +170,8 @@ export class SlotsController_2024_04_15 { : query.isTeamEvent; // Do not use workers in E2E, not supported by TS-JEST - const availableSlotsService = this.config.get("e2e") ? new AvailableSlotsService() : null; - - const availableSlots = availableSlotsService - ? await availableSlotsService.getAvailableSlots({ + const availableSlots = this.config.get("e2e") + ? await 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 a58e880324..ef1f746661 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,13 +1,11 @@ import { parentPort } from "worker_threads"; -import { AvailableSlotsService } from "@calcom/platform-libraries/slots"; +import { getAvailableSlots } from "@calcom/platform-libraries/slots"; parentPort?.on("message", async (data) => { try { const { input, ctx } = data; - const availableSlotsService = new AvailableSlotsService(); - - const result = await availableSlotsService.getAvailableSlots({ input, ctx }); + const result = await 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 80e45c1bb2..898f30b85f 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 { AvailableSlotsService } from "@calcom/platform-libraries/slots"; +import { getAvailableSlots } 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,8 +31,6 @@ 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, @@ -41,14 +39,12 @@ 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 this.availableSlotsService.getAvailableSlots({ + const availableSlots: TimeSlots = await 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 ba9fb5b210..4b94ac5588 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 { AvailableSlotsService } from "@calcom/trpc/server/routers/viewer/slots/util"; +import { getAvailableSlots } from "@calcom/trpc/server/routers/viewer/slots/util"; dayjs.extend(advancedFormat); @@ -86,9 +86,8 @@ 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 availableSlotsService.getAvailableSlots({ + const availableSlots = await getAvailableSlots({ input: { startTime, endTime, diff --git a/apps/web/test/lib/getSchedule.test.ts b/apps/web/test/lib/getSchedule.test.ts index a6360d8684..4317e5a89e 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 { AvailableSlotsService } from "@calcom/trpc/server/routers/viewer/slots/util"; +import { getAvailableSlots as getSchedule } from "@calcom/trpc/server/routers/viewer/slots/util"; import { expect, expectedSlotsForSchedule } from "./getSchedule/expects"; import { setupAndTeardown } from "./getSchedule/setupAndTeardown"; @@ -31,7 +31,6 @@ vi.mock("@calcom/lib/constants", () => ({ })); describe("getSchedule", () => { - const availableSlotsService = new AvailableSlotsService(); setupAndTeardown(); // TODO: Move these inside describe('Team Event') @@ -114,7 +113,7 @@ describe("getSchedule", () => { bookings: [], }); - const scheduleWithLeadSkip = await availableSlotsService.getAvailableSlots({ + const scheduleWithLeadSkip = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -135,7 +134,7 @@ describe("getSchedule", () => { } ); - const scheduleWithoutLeadSkip = await availableSlotsService.getAvailableSlots({ + const scheduleWithoutLeadSkip = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -263,7 +262,7 @@ describe("getSchedule", () => { bookings: [], }); - const scheduleFixedHostLead = await availableSlotsService.getAvailableSlots({ + const scheduleFixedHostLead = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -292,7 +291,7 @@ describe("getSchedule", () => { } ); - const scheduleRRHostLead = await availableSlotsService.getAvailableSlots({ + const scheduleRRHostLead = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -393,7 +392,7 @@ describe("getSchedule", () => { bookings: [], }); - const scheduleWithLeadSkip = await availableSlotsService.getAvailableSlots({ + const scheduleWithLeadSkip = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -527,7 +526,7 @@ describe("getSchedule", () => { bookings: [], }); - const scheduleWithLeadSkip = await availableSlotsService.getAvailableSlots({ + const scheduleWithLeadSkip = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -624,7 +623,7 @@ describe("getSchedule", () => { bookings: [], }); - const scheduleWithLeadSkip = await availableSlotsService.getAvailableSlots({ + const scheduleWithLeadSkip = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -733,7 +732,7 @@ describe("getSchedule", () => { bookings: [], }); - const scheduleWithLeadSkip = await availableSlotsService.getAvailableSlots({ + const scheduleWithLeadSkip = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -830,7 +829,7 @@ describe("getSchedule", () => { bookings: [], }); - const scheduleWithLeadSkip = await availableSlotsService.getAvailableSlots({ + const scheduleWithLeadSkip = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -927,7 +926,7 @@ describe("getSchedule", () => { bookings: [], }); - const scheduleWhenContactOwnerIsSkipped = await availableSlotsService.getAvailableSlots({ + const scheduleWhenContactOwnerIsSkipped = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -1031,7 +1030,7 @@ describe("getSchedule", () => { }); // Day Plus 2 is completely free - It only has non accepted bookings - const scheduleOnCompletelyFreeDay = await availableSlotsService.getAvailableSlots({ + const scheduleOnCompletelyFreeDay = await getSchedule({ input: { eventTypeId: 1, // EventTypeSlug doesn't matter for non-dynamic events @@ -1065,7 +1064,7 @@ describe("getSchedule", () => { ); // Day plus 3 - const scheduleForDayWithOneBooking = await availableSlotsService.getAvailableSlots({ + const scheduleForDayWithOneBooking = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -1131,7 +1130,7 @@ describe("getSchedule", () => { }); const { dateString: plus1DateString } = getDate({ dateIncrement: 1 }); const { dateString: plus2DateString } = getDate({ dateIncrement: 2 }); - const scheduleForEventWith30Length = await availableSlotsService.getAvailableSlots({ + const scheduleForEventWith30Length = await getSchedule({ input: { orgSlug: null, eventTypeId: 1, @@ -1168,18 +1167,17 @@ describe("getSchedule", () => { } ); - 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, - }, - }); + 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, + }, + }); // `slotInterval` takes precedence over `length` // 4:30 is utc so it is 10:00 in IST expect(scheduleForEventWith30minsLengthAndSlotInterval2hrs).toHaveTimeSlots( @@ -1229,7 +1227,7 @@ describe("getSchedule", () => { // Time Travel to the beginning of today after getting all the dates correctly. timeTravelToTheBeginningOfToday({ utcOffsetInHours: 5.5 }); - const scheduleForEventWithBookingNotice13Hrs = await availableSlotsService.getAvailableSlots({ + const scheduleForEventWithBookingNotice13Hrs = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -1253,7 +1251,7 @@ describe("getSchedule", () => { } ); - const scheduleForEventWithBookingNotice10Hrs = await availableSlotsService.getAvailableSlots({ + const scheduleForEventWithBookingNotice10Hrs = await getSchedule({ input: { eventTypeId: 2, eventTypeSlug: "", @@ -1316,7 +1314,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const scheduleForEventOnADayWithNonCalBooking = await availableSlotsService.getAvailableSlots({ + const scheduleForEventOnADayWithNonCalBooking = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -1391,7 +1389,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const scheduleForEventOnADayWithCalBooking = await availableSlotsService.getAvailableSlots({ + const scheduleForEventOnADayWithCalBooking = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -1450,7 +1448,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const schedule = await availableSlotsService.getAvailableSlots({ + const schedule = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -1515,7 +1513,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const scheduleForEventOnADayWithDateOverride = await availableSlotsService.getAvailableSlots({ + const scheduleForEventOnADayWithDateOverride = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -1594,7 +1592,7 @@ describe("getSchedule", () => { // Requesting this user's availability for their // individual Event Type - const thisUserAvailability = await availableSlotsService.getAvailableSlots({ + const thisUserAvailability = await getSchedule({ input: { eventTypeId: 2, eventTypeSlug: "", @@ -1707,7 +1705,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const thisUserAvailabilityBookingLimitOne = await availableSlotsService.getAvailableSlots({ + const thisUserAvailabilityBookingLimitOne = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -1719,7 +1717,7 @@ describe("getSchedule", () => { }, }); - const thisUserAvailabilityBookingLimitTwo = await availableSlotsService.getAvailableSlots({ + const thisUserAvailabilityBookingLimitTwo = await getSchedule({ input: { eventTypeId: 2, eventTypeSlug: "", @@ -1835,7 +1833,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const thisUserAvailabilityDurationLimitReached = await availableSlotsService.getAvailableSlots({ + const thisUserAvailabilityDurationLimitReached = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -1847,7 +1845,7 @@ describe("getSchedule", () => { }, }); - const thisUserAvailabilityDurationLimitNotReached = await availableSlotsService.getAvailableSlots({ + const thisUserAvailabilityDurationLimitNotReached = await getSchedule({ input: { eventTypeId: 2, eventTypeSlug: "", @@ -1945,7 +1943,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const thisUserAvailabilityDurationLimit = await availableSlotsService.getAvailableSlots({ + const thisUserAvailabilityDurationLimit = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -2041,7 +2039,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const weeklyAvailability = await availableSlotsService.getAvailableSlots({ + const weeklyAvailability = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -2171,7 +2169,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const availabilityEventTypeOne = await availableSlotsService.getAvailableSlots({ + const availabilityEventTypeOne = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -2192,7 +2190,7 @@ describe("getSchedule", () => { expect(availableSlotsInTz.filter((slot) => slot.format().startsWith(plus2DateString)).length).toBe(0); - const availabilityEventTypeTwo = await availableSlotsService.getAvailableSlots({ + const availabilityEventTypeTwo = await getSchedule({ input: { eventTypeId: 2, eventTypeSlug: "", @@ -2213,7 +2211,7 @@ describe("getSchedule", () => { expect(availableSlotsInTz2.filter((slot) => slot.format().startsWith(plus2DateString)).length).toBe(0); - const availabilityUserEventType = await availableSlotsService.getAvailableSlots({ + const availabilityUserEventType = await getSchedule({ input: { eventTypeId: 3, eventTypeSlug: "", @@ -2329,7 +2327,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const eventType1Availability = await availableSlotsService.getAvailableSlots({ + const eventType1Availability = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -2341,7 +2339,7 @@ describe("getSchedule", () => { }, }); - const eventType2Availability = await availableSlotsService.getAvailableSlots({ + const eventType2Availability = await getSchedule({ input: { eventTypeId: 2, eventTypeSlug: "", @@ -2389,7 +2387,7 @@ describe("getSchedule", () => { ], }); - const eventType2AvailabilityUpdated = await availableSlotsService.getAvailableSlots({ + const eventType2AvailabilityUpdated = await getSchedule({ input: { eventTypeId: 2, eventTypeSlug: "", @@ -2470,7 +2468,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const thisUserAvailabilityBookingLimit = await availableSlotsService.getAvailableSlots({ + const thisUserAvailabilityBookingLimit = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -2585,7 +2583,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const availabilityEventTypeOne = await availableSlotsService.getAvailableSlots({ + const availabilityEventTypeOne = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -2606,7 +2604,7 @@ describe("getSchedule", () => { expect(availableSlotsInTz.filter((slot) => slot.format().startsWith(plus2DateString)).length).toBe(0); // 1 booking per day as limit - const availabilityEventTypeTwo = await availableSlotsService.getAvailableSlots({ + const availabilityEventTypeTwo = await getSchedule({ input: { eventTypeId: 2, eventTypeSlug: "", @@ -2626,7 +2624,7 @@ describe("getSchedule", () => { expect(availableSlotsInTz.filter((slot) => slot.format().startsWith(plus2DateString)).length).toBe(0); // 1 booking per day as limit - const availabilityUserEventType = await availableSlotsService.getAvailableSlots({ + const availabilityUserEventType = await getSchedule({ input: { eventTypeId: 3, eventTypeSlug: "", @@ -2710,7 +2708,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const thisUserAvailabilityBookingLimit = await availableSlotsService.getAvailableSlots({ + const thisUserAvailabilityBookingLimit = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -2793,7 +2791,7 @@ describe("getSchedule", () => { // Requesting this user's availability for their // individual Event Type - const thisUserAvailability = await availableSlotsService.getAvailableSlots({ + const thisUserAvailability = await getSchedule({ input: { eventTypeId: 2, eventTypeSlug: "", @@ -2864,7 +2862,7 @@ describe("getSchedule", () => { ], }); - const schedule = await availableSlotsService.getAvailableSlots({ + const schedule = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -2963,7 +2961,7 @@ describe("getSchedule", () => { ], }); - const scheduleForTeamEventOnADayWithNoBooking = await availableSlotsService.getAvailableSlots({ + const scheduleForTeamEventOnADayWithNoBooking = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -2994,18 +2992,17 @@ describe("getSchedule", () => { } ); - 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, - }, - }); + 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, + }, + }); // 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 @@ -3104,18 +3101,17 @@ describe("getSchedule", () => { }, ], }); - 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, - }, - }); + 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, + }, + }); // A user with blocked time in another event, still affects Team Event availability expect(scheduleForTeamEventOnADayWithOneBookingForEachUserButOnDifferentTimeslots).toHaveTimeSlots( [ @@ -3134,18 +3130,17 @@ describe("getSchedule", () => { { dateString: plus2DateString } ); - 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, - }, - }); + 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, + }, + }); // A user with blocked time in another event, still affects Team Event availability expect(scheduleForTeamEventOnADayWithOneBookingForEachUserOnSameTimeSlot).toHaveTimeSlots( [ @@ -3200,7 +3195,7 @@ describe("getSchedule", () => { const { dateString: plus1DateString } = getDate({ dateIncrement: 1 }); const { dateString: plus2DateString } = getDate({ dateIncrement: 2 }); - const getScheduleRes = await availableSlotsService.getAvailableSlots({ + const getScheduleRes = await getSchedule({ input: { eventTypeSlug: scenario.eventTypes[0]?.slug, startTime: `${plus1DateString}T18:30:00.000Z`, @@ -3280,7 +3275,7 @@ describe("getSchedule", () => { bookings: [], }); - const scheduleWithHostChoosenSch = await availableSlotsService.getAvailableSlots({ + const scheduleWithHostChoosenSch = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -3346,7 +3341,7 @@ describe("getSchedule", () => { bookings: [], }); - const scheduleWithEventCommonSch = await availableSlotsService.getAvailableSlots({ + const scheduleWithEventCommonSch = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -3427,7 +3422,7 @@ describe("getSchedule", () => { ], }); - const schedule = await availableSlotsService.getAvailableSlots({ + const schedule = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -3508,7 +3503,7 @@ describe("getSchedule", () => { ], }); - const schedule = await availableSlotsService.getAvailableSlots({ + const schedule = await getSchedule({ 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 179bc4b3ea..31a9116889 100644 --- a/apps/web/test/lib/getSchedule/calendarEvents.test.ts +++ b/apps/web/test/lib/getSchedule/calendarEvents.test.ts @@ -9,13 +9,12 @@ import { import { describe, test } from "vitest"; -import { AvailableSlotsService } from "@calcom/trpc/server/routers/viewer/slots/util"; +import { getAvailableSlots as getSchedule } 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 () => { @@ -64,7 +63,7 @@ describe("getSchedule", () => { // An event with one accepted booking await createBookingScenario(scenarioData); - const scheduleForDayWithAGoogleCalendarBooking = await availableSlotsService.getAvailableSlots({ + const scheduleForDayWithAGoogleCalendarBooking = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -134,7 +133,7 @@ describe("getSchedule", () => { // An event with one accepted booking await createBookingScenario(scenarioData); - const scheduleForDayWithAGoogleCalendarBooking = await availableSlotsService.getAvailableSlots({ + const scheduleForDayWithAGoogleCalendarBooking = await getSchedule({ input: { eventTypeId, eventTypeSlug: "", @@ -203,7 +202,7 @@ describe("getSchedule", () => { // An event with one accepted booking await createBookingScenario(scenarioData); - const scheduleForDayWithAGoogleCalendarBooking = await availableSlotsService.getAvailableSlots({ + const scheduleForDayWithAGoogleCalendarBooking = await getSchedule({ input: { eventTypeId, eventTypeSlug: "", @@ -276,7 +275,7 @@ describe("getSchedule", () => { // An event with one accepted booking await createBookingScenario(scenarioData); - const scheduleForDayWithAGoogleCalendarBooking = await availableSlotsService.getAvailableSlots({ + const scheduleForDayWithAGoogleCalendarBooking = await getSchedule({ 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 ed8997547e..74df5ad78e 100644 --- a/apps/web/test/lib/getSchedule/delegation-credential.test.ts +++ b/apps/web/test/lib/getSchedule/delegation-credential.test.ts @@ -13,14 +13,13 @@ import { expectNoAttemptToGetAvailability } from "../../utils/bookingScenario/ex import { describe, test } from "vitest"; import { MembershipRole } from "@calcom/prisma/enums"; -import { AvailableSlotsService } from "@calcom/trpc/server/routers/viewer/slots/util"; +import { getAvailableSlots as getSchedule } 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 }); @@ -90,8 +89,8 @@ describe("getSchedule", () => { }; await createBookingScenario(scenarioData); - const availableSlotsService = new AvailableSlotsService(); - const scheduleForDayWithAGoogleCalendarBooking = await availableSlotsService.getAvailableSlots({ + + const scheduleForDayWithAGoogleCalendarBooking = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -182,8 +181,8 @@ describe("getSchedule", () => { }; await createBookingScenario(scenarioData); - const availableSlotsService = new AvailableSlotsService(); - const scheduleForDayWithAGoogleCalendarBooking = await availableSlotsService.getAvailableSlots({ + + const scheduleForDayWithAGoogleCalendarBooking = await getSchedule({ 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 8036cd00a9..28d5785e8e 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 { AvailableSlotsService } from "@calcom/trpc/server/routers/viewer/slots/util"; +import { getAvailableSlots as getSchedule } from "@calcom/trpc/server/routers/viewer/slots/util"; import { expectedSlotsForSchedule } from "./expects"; import { setupAndTeardown } from "./setupAndTeardown"; @@ -70,7 +70,6 @@ vi.mock("@calcom/lib/constants", () => ({ })); describe("getSchedule", () => { - const availableSlotsService = new AvailableSlotsService(); setupAndTeardown(); describe("Future Limits", () => { describe("PeriodType=ROLLING", () => { @@ -113,7 +112,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const scheduleForEvent = await availableSlotsService.getAvailableSlots({ + const scheduleForEvent = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -201,7 +200,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const scheduleForEvent = await availableSlotsService.getAvailableSlots({ + const scheduleForEvent = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -285,7 +284,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const scheduleForEvent = await availableSlotsService.getAvailableSlots({ + const scheduleForEvent = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -380,7 +379,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const scheduleForEvent = await availableSlotsService.getAvailableSlots({ + const scheduleForEvent = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -484,7 +483,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const scheduleForEvent = await availableSlotsService.getAvailableSlots({ + const scheduleForEvent = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -574,7 +573,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const scheduleForEvent = await availableSlotsService.getAvailableSlots({ + const scheduleForEvent = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -658,7 +657,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const scheduleForEvent = await availableSlotsService.getAvailableSlots({ + const scheduleForEvent = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -781,7 +780,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const scheduleForEvent = await availableSlotsService.getAvailableSlots({ + const scheduleForEvent = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -879,7 +878,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const scheduleForEvent = await availableSlotsService.getAvailableSlots({ + const scheduleForEvent = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -980,7 +979,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const scheduleForEvent = await availableSlotsService.getAvailableSlots({ + const scheduleForEvent = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -1088,7 +1087,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const scheduleForEvent = await availableSlotsService.getAvailableSlots({ + const scheduleForEvent = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -1208,7 +1207,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const scheduleForEvent = await availableSlotsService.getAvailableSlots({ + const scheduleForEvent = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -1305,7 +1304,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const scheduleForEvent = await availableSlotsService.getAvailableSlots({ + const scheduleForEvent = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -1414,7 +1413,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const scheduleForEvent = await availableSlotsService.getAvailableSlots({ + const scheduleForEvent = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -1516,7 +1515,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const scheduleForEvent = await availableSlotsService.getAvailableSlots({ + const scheduleForEvent = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -1602,7 +1601,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const scheduleForEventForPagoTz = await availableSlotsService.getAvailableSlots({ + const scheduleForEventForPagoTz = await getSchedule({ 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 7a9c2a62ae..1772a9f47e 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 { AvailableSlotsService } from "@calcom/trpc/server/routers/viewer/slots/util"; +import { getAvailableSlots as getSchedule } from "@calcom/trpc/server/routers/viewer/slots/util"; import { expect } from "./expects"; import { setupAndTeardown } from "./setupAndTeardown"; @@ -89,7 +89,6 @@ async function setupTeamAndFeatures() { } describe("getSchedule", () => { - const availableSlotsService = new AvailableSlotsService(); setupAndTeardown(); describe("Restriction Schedule", () => { @@ -157,7 +156,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const result = await availableSlotsService.getAvailableSlots({ + const result = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -253,7 +252,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const result = await availableSlotsService.getAvailableSlots({ + const result = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -354,7 +353,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const result = await availableSlotsService.getAvailableSlots({ + const result = await getSchedule({ input: { eventTypeId: 1, eventTypeSlug: "", @@ -439,7 +438,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const result = await availableSlotsService.getAvailableSlots({ + const result = await getSchedule({ 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 ef53a7c5cb..61411fae8f 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 { AvailableSlotsService } from "@calcom/trpc/server/routers/viewer/slots/util"; +import { getAvailableSlots as getSchedule } from "@calcom/trpc/server/routers/viewer/slots/util"; import { expect } from "./expects"; import { setupAndTeardown } from "./setupAndTeardown"; @@ -62,7 +62,6 @@ const getBaseScenarioData = (): ScheduleScenario => ({ }); describe("getSchedule", () => { - const availableSlotsService = new AvailableSlotsService(); setupAndTeardown(); describe("Reserved Slots", () => { @@ -94,7 +93,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const schedule = await availableSlotsService.getAvailableSlots({ + const schedule = await getSchedule({ input: getTestScheduleInput({ yesterdayDateString, plus5DateString }), }); @@ -129,7 +128,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const schedule = await availableSlotsService.getAvailableSlots({ + const schedule = await getSchedule({ input: getTestScheduleInput({ yesterdayDateString, plus5DateString }), ctx: { req: { @@ -184,7 +183,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const schedule = await availableSlotsService.getAvailableSlots({ + const schedule = await getSchedule({ input: getTestScheduleInput({ yesterdayDateString, plus5DateString }), }); @@ -239,7 +238,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const schedule = await availableSlotsService.getAvailableSlots({ + const schedule = await getSchedule({ input: getTestScheduleInput({ yesterdayDateString, plus5DateString }), }); @@ -288,7 +287,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - const schedule = await availableSlotsService.getAvailableSlots({ + const schedule = await getSchedule({ input: getTestScheduleInput({ yesterdayDateString, plus5DateString }), }); @@ -367,7 +366,7 @@ describe("getSchedule", () => { await createBookingScenario(scenarioData); - await availableSlotsService.getAvailableSlots({ + await getSchedule({ input: getTestScheduleInput({ yesterdayDateString, plus5DateString }), ctx: { req: { diff --git a/package.json b/package.json index 4ab7e95b65..ab7dfd8ccc 100644 --- a/package.json +++ b/package.json @@ -116,7 +116,6 @@ }, "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 420fc83b5b..0d61c8c2aa 100644 --- a/packages/platform/libraries/slots.ts +++ b/packages/platform/libraries/slots.ts @@ -1,3 +1,3 @@ -import { AvailableSlotsService } from "@calcom/trpc/server/routers/viewer/slots/util"; +import { getAvailableSlots } from "@calcom/trpc/server/routers/viewer/slots/util"; -export { AvailableSlotsService }; +export { getAvailableSlots }; diff --git a/packages/trpc/server/routers/viewer/highPerf/getTeamSchedule.handler.ts b/packages/trpc/server/routers/viewer/highPerf/getTeamSchedule.handler.ts index 3db504ae0e..9dac887cad 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 { AvailableSlotsService } from "../slots/util"; +import { getAvailableSlots } from "../slots/util"; import type { TGetTeamScheduleInputSchema } from "./getTeamSchedule.schema"; export type GetTeamScheduleOptions = { @@ -13,6 +13,5 @@ interface ContextForGetSchedule extends Record { } export const getTeamScheduleHandler = async ({ ctx, input }: GetTeamScheduleOptions) => { - const availableSlotsService = new AvailableSlotsService(); - return await availableSlotsService.getAvailableSlots({ ctx, input }); + return await 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 7898a0efbe..cbea7c78e3 100644 --- a/packages/trpc/server/routers/viewer/slots/getSchedule.handler.ts +++ b/packages/trpc/server/routers/viewer/slots/getSchedule.handler.ts @@ -1,7 +1,6 @@ import type { GetScheduleOptions } from "./types"; -import { AvailableSlotsService } from "./util"; +import { getAvailableSlots } from "./util"; export const getScheduleHandler = async ({ ctx, input }: GetScheduleOptions) => { - const availableSlotsService = new AvailableSlotsService(); - return await availableSlotsService.getAvailableSlots({ ctx, input }); + return await getAvailableSlots({ ctx, input }); }; diff --git a/packages/trpc/server/routers/viewer/slots/util.ts b/packages/trpc/server/routers/viewer/slots/util.ts index f0030d404c..809cf85a17 100644 --- a/packages/trpc/server/routers/viewer/slots/util.ts +++ b/packages/trpc/server/routers/viewer/slots/util.ts @@ -68,6 +68,170 @@ 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, @@ -85,437 +249,626 @@ export interface IGetAvailableSlots { troubleshooter?: any; } -export type GetAvailableSlotsResponse = Awaited< - ReturnType<(typeof AvailableSlotsService)["prototype"]["_getAvailableSlots"]> ->; +const _getUsersWithCredentials = ({ + hosts, +}: { + hosts: { + isFixed?: boolean; + user: GetAvailabilityUserWithDelegationCredentials; + }[]; +}) => { + return hosts.map(({ isFixed, user }) => ({ isFixed, ...user })); +}; -export class AvailableSlotsService { - private async _getReservedSlotsAndCleanupExpired({ - bookerClientUid, - usersWithCredentials, - eventTypeId, - }: { - bookerClientUid: string | undefined; - usersWithCredentials: GetAvailabilityUser[]; - eventTypeId: number; - }) { - const currentTimeInUtc = dayjs.utc().format(); +export const getUsersWithCredentials = withReporting(_getUsersWithCredentials, "getUsersWithCredentials"); - const unexpiredSelectedSlots = - (await SelectedSlotsRepository.findManyUnexpiredSlots({ - userIds: usersWithCredentials.map((user) => user.id), - currentTimeInUtc, - })) || []; +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); - const slotsSelectedByOtherUsers = unexpiredSelectedSlots.filter((slot) => slot.uid !== bookerClientUid); + return startTimeMin.isAfter(startTime) ? startTimeMin.tz(timeZone) : startTime; +}; - await _cleanupExpiredSlots({ eventTypeId }); +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); - const reservedSlots = slotsSelectedByOtherUsers; + if (process.env.INTEGRATION_TEST_MODE === "true") { + logger.settings.minLevel = 2; + } - return reservedSlots; + const eventType = await getRegularOrDynamicEventType(input, orgDetails); - async function _cleanupExpiredSlots({ eventTypeId }: { eventTypeId: number }) { - await SelectedSlotsRepository.deleteManyExpiredSlots({ eventTypeId, currentTimeInUtc }); + 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); } } - 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 isTeamEvent = + eventType.schedulingType === SchedulingType.COLLECTIVE || + eventType.schedulingType === SchedulingType.ROUND_ROBIN || + allUsersAvailability.length > 1; - const usersForDynamicEventType = await UserRepository.findManyUsersForDynamicEventType({ - currentOrgDomain: isValidOrgDomain ? currentOrgDomain : null, - usernameList: Array.isArray(input.usernameList) - ? input.usernameList - : input.usernameList - ? [input.usernameList] - : [], + 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 usersWithOldSelectedCalendars = usersForDynamicEventType.map((user) => withSelectedCalendars(user)); + const restrictionTimezone = eventType.useBookerTimezone + ? input.timeZone + : restrictionSchedule.timeZone!; + const eventLength = input.duration || eventType.length; - 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 }) - ); - const eventTypeId = - input.eventTypeId || - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - (await this.getEventTypeId({ - slug: usernameList?.[0], - eventTypeSlug: eventTypeSlug, - isTeamEvent, - organizationDetails, + const restrictionAvailability = restrictionSchedule.availability.map((rule) => ({ + days: rule.days, + startTime: rule.startTime, + endTime: rule.endTime, + date: rule.date, })); - if (!eventTypeId) { - return null; - } + // 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, + })) + : []; - return await EventTypeRepository.findForSlots({ id: eventTypeId }); - } + const { dateRanges: restrictionRanges } = buildDateRanges({ + availability: restrictionAvailability, + timeZone: restrictionTimezone || "UTC", + dateFrom: startTime, + dateTo: endTime, + travelSchedules, + }); - private getEventType = withReporting(this._getEventType, "getEventType"); + availableTimeSlots = timeSlots.filter((slot) => { + const slotStart = slot.time; + const slotEnd = slot.time.add(eventLength, "minute"); - private doesRangeStartFromToday(periodType: PeriodType) { - return periodType === PeriodType.ROLLING_WINDOW || periodType === PeriodType.ROLLING; - } - - private _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"); - } - return allDates; - } - - private getAllDatesWithBookabilityStatus = withReporting( - this._getAllDatesWithBookabilityStatus, - "getAllDatesWithBookabilityStatus" - ); - - 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; - } - - private async getEventTypeId({ - slug, - eventTypeSlug, - isTeamEvent, - organizationDetails, - }: { - slug?: string; - eventTypeSlug?: string; - isTeamEvent: boolean; - organizationDetails?: { currentOrgDomain: string | null; isValidOrgDomain: boolean }; - }) { - if (!eventTypeSlug || !slug) return null; - - let teamId; - let userId; - if (isTeamEvent) { - teamId = await this.getTeamIdFromSlug( - slug, - organizationDetails ?? { currentOrgDomain: null, isValidOrgDomain: false } - ); + return restrictionRanges.some( + (range) => + (slotStart.isAfter(range.start) || slotStart.isSame(range.start)) && + (slotEnd.isBefore(range.end) || slotEnd.isSame(range.end)) + ); + }); } else { - userId = await this.getUserIdFromUsername( - slug, - organizationDetails ?? { currentOrgDomain: null, isValidOrgDomain: false } + availableTimeSlots = timeSlots; + } + } else { + availableTimeSlots = timeSlots; + } + + const reservedSlots = await _getReservedSlotsAndCleanupExpired({ + bookerClientUid, + eventTypeId: eventType.id, + usersWithCredentials, + }); + + const availabilityCheckProps = { + eventLength: input.duration || eventType.length, + currentSeats, + }; + + 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()) + ); + } + + 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, + }); + }); + } + + 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 = 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, + }); + 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" }); + + 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}` ); } - const eventType = await EventTypeRepository.findFirstEventTypeId({ slug: eventTypeSlug, teamId, userId }); - if (!eventType) { - throw new TRPCError({ code: "NOT_FOUND" }); - } - return eventType?.id; } - 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 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 team?.id; + return { + slots: withinBoundsSlotsMappedToDate, + ...troubleshooterData, + }; +}; + +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({ + slug, + parentSlug: isValidOrgDomain && currentOrgDomain ? currentOrgDomain : null, + select: { id: true }, + }); + + 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"); } + return allDates; +}; - 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(); +export const getAllDatesWithBookabilityStatus = withReporting( + _getAllDatesWithBookabilityStatus, + "getAllDatesWithBookabilityStatus" +); - 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()); - } +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(); + if (!bookingLimits && !durationLimits) { return userBusyTimesMap; } - private getBusyTimesFromLimitsForUsers = withReporting( - this._getBusyTimesFromLimitsForUsers, - "getBusyTimesFromLimitsForUsers" + const { limitDateFrom, limitDateTo } = getStartEndDateforLimitCheck( + dateFrom.toISOString(), + dateTo.toISOString(), + 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 busyTimesFromLimitsBookings = await getBusyTimesForLimitChecks({ + userIds: users.map((user) => user.id), + eventTypeId: eventType.id, + startDate: limitDateFrom.format(), + endDate: limitDateTo.format(), + rescheduleUid, + bookingLimits, + durationLimits, + }); - 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(); + const globalLimitManager = new LimitManager(); + if (bookingLimits) { for (const key of descendingLimitKeys) { const limit = bookingLimits?.[key]; if (!limit) continue; @@ -529,7 +882,7 @@ export class AvailableSlotsService { const periodEnd = periodStart.endOf(unit); let totalBookings = 0; - for (const booking of busyTimes) { + for (const booking of busyTimesFromLimitsBookings) { if (!isBookingWithinPeriod(booking, periodStart, periodEnd, timeZone)) { continue; } @@ -542,28 +895,15 @@ export class AvailableSlotsService { } } } + } - const userBusyTimesMap = new Map(); + for (const user of users) { + const userBookings = busyTimesFromLimitsBookings.filter((booking) => booking.userId === user.id); + const limitManager = new LimitManager(); - 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, - }; + limitManager.mergeBusyTimes(globalLimitManager); + if (bookingLimits) { for (const key of descendingLimitKeys) { const limit = bookingLimits?.[key]; if (!limit) continue; @@ -576,15 +916,13 @@ export class AvailableSlotsService { if (unit === "year") { try { - // TODO: DI checkBookingLimit await checkBookingLimit({ eventStartDate: periodStart.toDate(), limitingNumber: limit, + eventId: eventType.id, key, - teamId, user, rescheduleUid, - includeManagedEvents, timeZone, }); } catch (_) { @@ -592,7 +930,7 @@ export class AvailableSlotsService { if ( periodStartDates.every((start: Dayjs) => limitManager.isAlreadyBusy(start, unit, timeZone)) ) { - return; + break; } } continue; @@ -601,7 +939,7 @@ export class AvailableSlotsService { const periodEnd = periodStart.endOf(unit); let totalBookings = 0; - for (const booking of userBusyTimes) { + for (const booking of userBookings) { if (!isBookingWithinPeriod(booking, periodStart, periodEnd, timeZone)) { continue; } @@ -614,751 +952,387 @@ export class AvailableSlotsService { } } } - - userBusyTimesMap.set(user.id, limitManager.getBusyTimes()); } - return userBusyTimesMap; - } - private getBusyTimesFromTeamLimitsForUsers = withReporting( - this._getBusyTimesFromTeamLimitsForUsers, - "getBusyTimesFromTeamLimitsForUsers" - ); + if (durationLimits) { + for (const key of descendingLimitKeys) { + const limit = durationLimits?.[key]; + if (!limit) continue; - 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 unit = intervalLimitKeyToUnit(key); + const periodStartDates = getPeriodStartDatesBetween(dateFrom, dateTo, unit, timeZone); - private _getUsersWithCredentials({ - hosts, - }: { - hosts: { - isFixed?: boolean; - user: GetAvailabilityUserWithDelegationCredentials; - }[]; - }) { - return hosts.map(({ isFixed, user }) => ({ isFixed, ...user })); - } + for (const periodStart of periodStartDates) { + if (limitManager.isAlreadyBusy(periodStart, unit, timeZone)) continue; - private getUsersWithCredentials = withReporting(this._getUsersWithCredentials, "getUsersWithCredentials"); + const selectedDuration = (duration || eventType.length) ?? 0; - 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); + if (selectedDuration > limit) { + limitManager.addBusyTime(periodStart, unit, timeZone); + continue; + } - 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, - }); + 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; + } - loggerWithEventDetails.debug("Using users", { - usersWithCredentials: usersWithCredentials.map((user) => user.email), - }); + const periodEnd = periodStart.endOf(unit); + let totalDuration = selectedDuration; - 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; + 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; + } } } } - - 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; + userBusyTimesMap.set(user.id, limitManager.getBusyTimes()); + } - 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, - }); + return userBusyTimesMap; +}; - 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", - }); +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 + ); + + 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(); + + 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; } - 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; + totalBookings++; + if (totalBookings >= limit) { + globalLimitManager.addBusyTime(periodStart, unit, timeZone); + break; + } + } + } + } + + const userBusyTimesMap = new Map(); + + 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, + }; + + 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; + } + + const periodEnd = periodStart.endOf(unit); + let totalBookings = 0; + + for (const booking of userBusyTimes) { + if (!isBookingWithinPeriod(booking, periodStart, periodEnd, timeZone)) { + continue; + } + + totalBookings++; + if (totalBookings >= limit) { + limitManager.addBusyTime(periodStart, unit, timeZone); + break; + } + } } - } else { - availableTimeSlots = timeSlots; } - const reservedSlots = await this._getReservedSlotsAndCleanupExpired({ - bookerClientUid, + userBusyTimesMap.set(user.id, limitManager.getBusyTimes()); + } + + 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, - usersWithCredentials, + 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, }); - - const availabilityCheckProps = { - eventLength: input.duration || eventType.length, - currentSeats, - }; - - 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()) - ); - } - - availabilityCheckProps.currentSeats = this.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, - }); - }); - } - - 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}` - ); - } - } - - 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, - }; } -} + + 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 a83ee2ac9c..d7981f9b19 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.243" + "@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.241" "@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.243": - version: 0.0.243 - resolution: "@calcom/platform-libraries@npm:0.0.243" +"@calcom/platform-libraries@npm:@calcom/platform-libraries@0.0.241": + version: 0.0.241 + resolution: "@calcom/platform-libraries@npm:0.0.241" dependencies: "@calcom/features": "*" "@calcom/lib": "*" - checksum: ce0a4778857a62a6df46b1c0b419fbaad1d874d7b0da92d43589348f6e07a7de912703692c9fbc1e21fb2e0bf4d323bcd7eae96b5198376216216c872374cc09 + checksum: 1eec9dce749b6678427e00d0f2d2f383d798066f2f7ef7aca1ce90d5c649ed41d2fc1496be93c7448e7c22d2e015e333c8fe0b81b7c3b04b9637f6ea1910a898 languageName: node linkType: hard @@ -5816,13 +5816,6 @@ __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" @@ -21773,7 +21766,6 @@ __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