import prismock from "../../../../../tests/libs/__mocks__/prisma"; import { createBookingScenario, Timezones, TestData, type ScenarioData, } from "../../utils/bookingScenario/bookingScenario"; import { describe, test, vi } from "vitest"; import type { z } from "zod"; import { getAvailableSlotsService } from "@calcom/lib/di/containers/AvailableSlots"; import type { getScheduleSchema } from "@calcom/trpc/server/routers/viewer/slots/types"; import { expect } from "./expects"; import { setupAndTeardown } from "./setupAndTeardown"; // Mock the FeaturesRepository to enable restriction-schedule feature vi.mock("@calcom/features/flags/features.repository", () => ({ FeaturesRepository: vi.fn().mockImplementation(() => ({ checkIfTeamHasFeature: vi.fn().mockResolvedValue(true), checkIfFeatureIsEnabledGlobally: vi.fn().mockResolvedValue(true), getAllFeatures: vi.fn().mockResolvedValue([]), getFeatureFlagMap: vi.fn().mockResolvedValue({}), checkIfUserHasFeature: vi.fn().mockResolvedValue(true), })), })); type ScheduleScenario = { eventTypes: ScenarioData["eventTypes"]; users: ScenarioData["users"]; apps: ScenarioData["apps"]; selectedSlots?: ScenarioData["selectedSlots"]; }; const getTestScheduleInput = ({ yesterdayDateString, plus5DateString, }: { yesterdayDateString: string; plus5DateString: string; }): z.infer => ({ eventTypeId: 1, eventTypeSlug: "", startTime: `${yesterdayDateString}T18:30:00.000Z`, endTime: `${plus5DateString}T18:29:59.999Z`, timeZone: Timezones["+5:30"], isTeamEvent: true, orgSlug: null, }); const getBaseScenarioData = (): ScheduleScenario => ({ eventTypes: [ { id: 1, slotInterval: 60, length: 60, teamId: 1, hosts: [{ userId: 101 }], }, ], users: [ { ...TestData.users.example, id: 101, schedules: [TestData.schedules.IstWorkHours], teams: [ { membership: { role: "ADMIN", accepted: true }, team: { id: 1, name: "Test Team", slug: "test-team" }, }, ], }, ], apps: [], }); // Helper function to set up team for the tests async function setupTeamAndFeatures() { // Create the team (needed for event type configuration) await prismock.team.create({ data: { id: 1, name: "Test Team", slug: "test-team", }, }); } describe("getSchedule", () => { const availableSlotsService = getAvailableSlotsService(); setupAndTeardown(); describe("Restriction Schedule", () => { test("should respect date override rule in restrictionSchedule (Europe/London, useBookerTimezone=false)", async () => { await setupTeamAndFeatures(); vi.setSystemTime("2025-06-01T23:30:00Z"); const plus2DateString = "2025-06-02"; // Date with override const plus5DateString = "2025-06-05"; const scenarioData: ScheduleScenario = { ...getBaseScenarioData(), eventTypes: [ { id: 1, length: 60, teamId: 1, restrictionScheduleId: 50, useBookerTimezone: false, hosts: [{ userId: 101 }], }, ], users: [ { ...TestData.users.example, id: 101, schedules: [ { id: 1, name: "Test Schedule", timeZone: "Europe/London", availability: [ { days: [0, 1, 2, 3, 4, 5, 6], startTime: new Date("1970-01-01T09:00:00.000Z"), endTime: new Date("1970-01-01T17:00:00.000Z"), date: null, }, ], }, ], teams: [ { membership: { role: "ADMIN", accepted: true }, team: { id: 1, name: "Test Team", slug: "test-team" }, }, ], }, ], }; scenarioData.users[0].schedules.push({ id: 50, name: "Restriction Schedule - Only Date Override", timeZone: "Asia/Dubai", availability: [ { date: "2025-06-02T00:00:00.000Z", days: [], startTime: new Date("1970-01-01T10:00:00.000Z"), endTime: new Date("1970-01-01T18:00:00.000Z"), }, ], }); await createBookingScenario(scenarioData); const result = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", startTime: "2025-06-01T00:00:00.000Z", endTime: `${plus5DateString}T23:59:59.999Z`, timeZone: "Asia/Kolkata", isTeamEvent: true, orgSlug: null, }, }); // Expected slots based on intersection of: // - User availability: 9 AM - 5 PM London // - Restriction: 10 AM - 6 PM Dubai const expectedUtcSlots = [ "2025-06-02T08:00:00.000Z", // 9 AM London, 12 PM Dubai, 1:30 PM Kolkata "2025-06-02T09:00:00.000Z", // 10 AM London, 1 PM Dubai, 2:30 PM Kolkata "2025-06-02T10:00:00.000Z", // 11 AM London, 2 PM Dubai, 3:30 PM Kolkata "2025-06-02T11:00:00.000Z", // 12 PM London, 3 PM Dubai, 4:30 PM Kolkata "2025-06-02T12:00:00.000Z", // 1 PM London, 4 PM Dubai, 5:30 PM Kolkata "2025-06-02T13:00:00.000Z", // 2 PM London, 5 PM Dubai, 6:30 PM Kolkata "2025-06-02T14:00:00.000Z", // 3 PM London, 6 PM Dubai, 7:30 PM Kolkata ]; expect(result).toHaveTimeSlots(expectedUtcSlots, { dateString: plus2DateString, doExactMatch: false, }); expect(result).toHaveDateDisabled({ dateString: "2025-06-03" }); }); test("should respect recurring rule in restrictionSchedule (Europe/London, useBookerTimezone=false)", async () => { await setupTeamAndFeatures(); vi.setSystemTime("2025-06-01T23:30:00Z"); const plus2DateString = "2025-06-02"; // Monday const plus6DateString = "2025-06-06"; // Friday const scenarioData: ScheduleScenario = { ...getBaseScenarioData(), eventTypes: [ { id: 1, length: 60, teamId: 1, restrictionScheduleId: 50, useBookerTimezone: false, hosts: [{ userId: 101 }], }, ], users: [ { ...TestData.users.example, id: 101, schedules: [ { id: 1, name: "Test Schedule", timeZone: "Europe/London", availability: [ { days: [0, 1, 2, 3, 4, 5, 6], startTime: new Date("1970-01-01T09:00:00.000Z"), endTime: new Date("1970-01-01T17:00:00.000Z"), date: null, }, ], }, ], teams: [ { membership: { role: "ADMIN", accepted: true }, team: { id: 1, name: "Test Team", slug: "test-team" }, }, ], }, ], }; scenarioData.users[0].schedules.push({ id: 50, name: "Test Schedule", timeZone: "Asia/Dubai", availability: [ { days: [1, 2, 3, 4, 5], startTime: new Date("1970-01-01T10:00:00.000Z"), endTime: new Date("1970-01-01T18:00:00.000Z"), date: null, }, ], }); await createBookingScenario(scenarioData); const result = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", startTime: "2025-06-01T00:00:00.000Z", endTime: `${plus6DateString}T23:59:59.999Z`, timeZone: "Asia/Kolkata", isTeamEvent: true, orgSlug: null, }, }); // Expected slots based on intersection of: // - User availability: 9 AM - 5 PM London // - Restriction: 10 AM - 6 PM Dubai, weekdays only const getExpectedSlotsForDate = (dateString: string) => [ `${dateString}T08:00:00.000Z`, // 9 AM London, 12 PM Dubai, 1:30 PM Kolkata `${dateString}T09:00:00.000Z`, // 10 AM London, 1 PM Dubai, 2:30 PM Kolkata `${dateString}T10:00:00.000Z`, // 11 AM London, 2 PM Dubai, 3:30 PM Kolkata `${dateString}T11:00:00.000Z`, // 12 PM London, 3 PM Dubai, 4:30 PM Kolkata `${dateString}T12:00:00.000Z`, // 1 PM London, 4 PM Dubai, 5:30 PM Kolkata `${dateString}T13:00:00.000Z`, // 2 PM London, 5 PM Dubai, 6:30 PM Kolkata ]; // Verify weekday slots ["2025-06-02", "2025-06-03", "2025-06-04", "2025-06-05", "2025-06-06"].forEach((dateString) => { expect(result).toHaveTimeSlots(getExpectedSlotsForDate(dateString), { dateString, doExactMatch: false, }); }); // Verify weekend is disabled expect(result).toHaveDateDisabled({ dateString: "2025-06-07" }); expect(result).toHaveDateDisabled({ dateString: "2025-06-08" }); }); test("should respect recurring rule in restrictionSchedule (Europe/London, useBookerTimezone=true)", async () => { await setupTeamAndFeatures(); vi.setSystemTime("2025-06-01T23:30:00Z"); const plus2DateString = "2025-06-02"; // Monday const plus6DateString = "2025-06-06"; // Friday const scenarioData: ScheduleScenario = { ...getBaseScenarioData(), eventTypes: [ { id: 1, length: 60, teamId: 1, restrictionScheduleId: 50, useBookerTimezone: true, hosts: [{ userId: 101 }], }, ], users: [ { ...TestData.users.example, id: 101, schedules: [ { id: 1, name: "Test Schedule", timeZone: "Europe/London", availability: [ { days: [0, 1, 2, 3, 4, 5, 6], startTime: new Date("1970-01-01T09:00:00.000Z"), endTime: new Date("1970-01-01T17:00:00.000Z"), date: null, }, ], }, ], teams: [ { membership: { role: "ADMIN", accepted: true }, team: { id: 1, name: "Test Team", slug: "test-team" }, }, ], }, ], }; scenarioData.users[0].schedules.push({ id: 50, name: "Test Schedule", timeZone: "Asia/Dubai", availability: [ { days: [1, 2, 3, 4, 5], startTime: new Date("1970-01-01T10:00:00.000Z"), endTime: new Date("1970-01-01T18:00:00.000Z"), date: null, }, ], }); await createBookingScenario(scenarioData); const result = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", startTime: "2025-06-01T00:00:00.000Z", endTime: `${plus6DateString}T23:59:59.999Z`, timeZone: "Asia/Kolkata", isTeamEvent: true, orgSlug: null, }, }); // Expected slots based on: // - User availability: 9 AM - 5 PM London (1:30 PM - 9:30 PM Kolkata) // - Restriction: 10 AM - 6 PM Kolkata const getExpectedSlotsForDate = (dateString: string) => [ `${dateString}T08:00:00.000Z`, // 1:30 PM Kolkata, 9 AM London `${dateString}T09:00:00.000Z`, // 2:30 PM Kolkata, 10 AM London `${dateString}T10:00:00.000Z`, // 3:30 PM Kolkata, 11 AM London `${dateString}T11:00:00.000Z`, // 4:30 PM Kolkata, 12 PM London `${dateString}T12:00:00.000Z`, // 5:30 PM Kolkata, 1 PM London ]; // Verify weekday slots ["2025-06-02", "2025-06-03", "2025-06-04", "2025-06-05", "2025-06-06"].forEach((dateString) => { expect(result).toHaveTimeSlots(getExpectedSlotsForDate(dateString), { dateString, doExactMatch: false, }); }); // Verify weekend is disabled expect(result).toHaveDateDisabled({ dateString: "2025-06-07" }); expect(result).toHaveDateDisabled({ dateString: "2025-06-08" }); }); test("should return all slots when no restriction schedule is applied", async () => { await setupTeamAndFeatures(); vi.setSystemTime("2025-06-01T23:30:00Z"); const plus2DateString = "2025-06-02"; // Monday const plus6DateString = "2025-06-06"; // Friday const scenarioData: ScheduleScenario = { ...getBaseScenarioData(), eventTypes: [ { id: 1, length: 60, teamId: 1, useBookerTimezone: false, hosts: [{ userId: 101 }], }, ], users: [ { ...TestData.users.example, id: 101, schedules: [ { id: 1, name: "Test Schedule", timeZone: "Europe/London", availability: [ { days: [0, 1, 2, 3, 4, 5, 6], startTime: new Date("1970-01-01T09:00:00.000Z"), endTime: new Date("1970-01-01T17:00:00.000Z"), date: null, }, ], }, ], teams: [ { membership: { role: "ADMIN", accepted: true }, team: { id: 1, name: "Test Team", slug: "test-team" }, }, ], }, ], }; await createBookingScenario(scenarioData); const result = await availableSlotsService.getAvailableSlots({ input: { eventTypeId: 1, eventTypeSlug: "", startTime: "2025-06-01T00:00:00.000Z", endTime: `${plus6DateString}T23:59:59.999Z`, timeZone: "Asia/Kolkata", isTeamEvent: true, orgSlug: null, }, }); // Expected slots based on user's availability: 9 AM - 5 PM London const getExpectedSlotsForDate = (dateString: string) => [ `${dateString}T08:00:00.000Z`, // 9 AM London, 1:30 PM Kolkata `${dateString}T09:00:00.000Z`, // 10 AM London, 2:30 PM Kolkata `${dateString}T10:00:00.000Z`, // 11 AM London, 3:30 PM Kolkata `${dateString}T11:00:00.000Z`, // 12 PM London, 4:30 PM Kolkata `${dateString}T12:00:00.000Z`, // 1 PM London, 5:30 PM Kolkata `${dateString}T13:00:00.000Z`, // 2 PM London, 6:30 PM Kolkata `${dateString}T14:00:00.000Z`, // 3 PM London, 7:30 PM Kolkata `${dateString}T15:00:00.000Z`, // 4 PM London, 8:30 PM Kolkata `${dateString}T16:00:00.000Z`, // 5 PM London, 9:30 PM Kolkata ]; // Verify weekday slots ["2025-06-02", "2025-06-03", "2025-06-04", "2025-06-05", "2025-06-06"].forEach((dateString) => { expect(result).toHaveTimeSlots(getExpectedSlotsForDate(dateString), { dateString, doExactMatch: false, }); }); // Verify a late evening slot (8:30 PM Kolkata, 4 PM London) is present const lateEveningSlot = `${plus2DateString}T15:00:00.000Z`; expect(result.slots[plus2DateString].map((s) => s.time)).toContain(lateEveningSlot); }); }); });