Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { Test } from "@nestjs/testing";
import type { TestingModule } from "@nestjs/testing";
import { SlotsInputService_2024_09_04 } from "@/modules/slots/slots-2024-09-04/services/slots-input.service";
import { TeamsEventTypesRepository } from "@/modules/teams/event-types/teams-event-types.repository";
import { TeamsRepository } from "@/modules/teams/teams/teams.repository";
import { UsersRepository } from "@/modules/users/users.repository";
import { EventTypesRepository_2024_06_14 } from "@/platform/event-types/event-types_2024_06_14/event-types.repository";

jest.mock(
"@calcom/platform-libraries",
() => ({
dynamicEvent: {
id: -1,
slug: "dynamic",
length: 30,
teamId: null,
},
}),
{ virtual: true }
);

describe("SlotsInputService_2024_09_04", () => {
let service: SlotsInputService_2024_09_04;
let eventTypeRepository: EventTypesRepository_2024_06_14;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
SlotsInputService_2024_09_04,
{
provide: EventTypesRepository_2024_06_14,
useValue: {
getEventTypeById: jest.fn(),
},
},
{
provide: UsersRepository,
useValue: {},
},
{
provide: TeamsRepository,
useValue: {},
},
{
provide: TeamsEventTypesRepository,
useValue: {},
},
],
}).compile();

service = module.get<SlotsInputService_2024_09_04>(SlotsInputService_2024_09_04);
eventTypeRepository = module.get<EventTypesRepository_2024_06_14>(EventTypesRepository_2024_06_14);

jest.clearAllMocks();
});

it("defaults v2 slot requests to UTC without disabling rolling-window bounds checks", async () => {
(eventTypeRepository.getEventTypeById as jest.Mock).mockResolvedValue({
id: 123,
slug: "discovery-call",
teamId: null,
});

const transformedQuery = await service.transformGetSlotsQuery({
type: "byEventTypeId",
eventTypeId: 123,
start: "2050-12-09",
end: "2050-12-10",
});

expect(transformedQuery).toMatchObject({
startTime: "2050-12-09T00:00:00.000Z",
endTime: "2050-12-10T23:59:59.000Z",
timeZone: "UTC",
});
expect(transformedQuery).not.toHaveProperty("disableRollingWindowAdjustment");
});

it("preserves an explicitly requested time zone", async () => {
(eventTypeRepository.getEventTypeById as jest.Mock).mockResolvedValue({
id: 123,
slug: "discovery-call",
teamId: null,
});

await expect(
service.transformGetSlotsQuery({
type: "byEventTypeId",
eventTypeId: 123,
start: "2050-12-09",
end: "2050-12-10",
timeZone: "Europe/Rome",
})
).resolves.toMatchObject({
timeZone: "Europe/Rome",
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ import {
} from "@calcom/platform-types";
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
import { DateTime } from "luxon";
import { EventTypesRepository_2024_06_14 } from "@/platform/event-types/event-types_2024_06_14/event-types.repository";
import { TeamsEventTypesRepository } from "@/modules/teams/event-types/teams-event-types.repository";
import { TeamsRepository } from "@/modules/teams/teams/teams.repository";
import { UsersRepository } from "@/modules/users/users.repository";
import { EventTypesRepository_2024_06_14 } from "@/platform/event-types/event-types_2024_06_14/event-types.repository";

export type InternalGetSlotsQuery = {
isTeamEvent: boolean;
Expand Down Expand Up @@ -57,7 +57,7 @@ export class SlotsInputService_2024_09_04 {
const eventTypeId = eventType.id;
const eventTypeSlug = eventType.slug;
const usernameList = "usernames" in query ? query.usernames : [];
const timeZone = query.timeZone;
const timeZone = query.timeZone ?? "UTC";
const orgSlug = "organizationSlug" in query ? query.organizationSlug : null;
const rescheduleUid = query.bookingUidToReschedule || null;

Expand Down
28 changes: 25 additions & 3 deletions packages/trpc/server/routers/viewer/slots/util.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { describe, it, expect } from "vitest";

import dayjs from "@calcom/dayjs";
import { BookingDateInPastError, isTimeOutOfBounds } from "@calcom/lib/isOutOfBounds";

import { TRPCError } from "@trpc/server";
import { describe, expect, it } from "vitest";
import { getStartTimeForRollingWindowComputation } from "./util";

describe("BookingDateInPastError handling", () => {
it("should convert BookingDateInPastError to TRPCError with BAD_REQUEST code", () => {
Expand Down Expand Up @@ -43,3 +43,25 @@ describe("BookingDateInPastError handling", () => {
expect(() => testFilteringLogic()).toThrow("Attempting to book a meeting in the past.");
});
});

describe("getStartTimeForRollingWindowComputation", () => {
const requestedStartTime = "2050-12-09T00:00:00.000Z";

it("moves rolling window requests back one month by default", () => {
expect(
getStartTimeForRollingWindowComputation({
startTime: requestedStartTime,
isRollingWindowPeriodType: true,
})
).toBe(dayjs(requestedStartTime).subtract(1, "month").toISOString());
});

it("keeps the requested start time for non-rolling window requests", () => {
expect(
getStartTimeForRollingWindowComputation({
startTime: requestedStartTime,
isRollingWindowPeriodType: false,
})
).toBe(requestedStartTime);
});
});
31 changes: 24 additions & 7 deletions packages/trpc/server/routers/viewer/slots/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,22 @@ export type GetAvailableSlotsResponse = Awaited<
ReturnType<(typeof AvailableSlotsService)["prototype"]["_getAvailableSlots"]>
>;

export function getStartTimeForRollingWindowComputation({
startTime,
isRollingWindowPeriodType,
}: {
startTime: string;
isRollingWindowPeriodType: boolean;
}): string {
const isStartTimeInPast = dayjs(startTime).isBefore(dayjs().subtract(1, "day").startOf("day"));

if (isStartTimeInPast || !isRollingWindowPeriodType) {
return startTime;
}

return dayjs(startTime).subtract(1, "month").toISOString();
}

export interface IAvailableSlotsService {
oooRepo: PrismaOOORepository;
scheduleRepo: ScheduleRepository;
Expand Down Expand Up @@ -569,7 +585,10 @@ export class AvailableSlotsService {

const selectedDuration = (duration || eventType.length) ?? 0;

const { title: durationTitle, source: durationSource } = LimitSources.eventDurationLimit({ limit, unit });
const { title: durationTitle, source: durationSource } = LimitSources.eventDurationLimit({
limit,
unit,
});

if (selectedDuration > limit) {
limitManager.addBusyTime({
Expand Down Expand Up @@ -925,16 +944,14 @@ export class AvailableSlotsService {
}

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 startTimeAdjustedForRollingWindowComputation = getStartTimeForRollingWindowComputation({
startTime: input.startTime,
isRollingWindowPeriodType,
});

const loggerWithEventDetails = logger.getSubLogger({
type: "json",
Expand Down
Loading