diff --git a/apps/web/lib/reschedule/[uid]/getServerSideProps.ts b/apps/web/lib/reschedule/[uid]/getServerSideProps.ts index cdf3726e48..8d9a433619 100644 --- a/apps/web/lib/reschedule/[uid]/getServerSideProps.ts +++ b/apps/web/lib/reschedule/[uid]/getServerSideProps.ts @@ -4,14 +4,12 @@ import { URLSearchParams } from "url"; import { z } from "zod"; import { getServerSession } from "@calcom/features/auth/lib/getServerSession"; -import { getFullName } from "@calcom/features/form-builder/utils"; +import { determineReschedulePreventionRedirect } from "@calcom/features/bookings/lib/reschedule/determineReschedulePreventionRedirect"; import { buildEventUrlFromBooking } from "@calcom/lib/bookings/buildEventUrlFromBooking"; import { getDefaultEvent } from "@calcom/lib/defaultEvents"; -import { getSafe } from "@calcom/lib/getSafe"; import { maybeGetBookingUidFromSeat } from "@calcom/lib/server/maybeGetBookingUidFromSeat"; import { UserRepository } from "@calcom/lib/server/repository/user"; import prisma, { bookingMinimalSelect } from "@calcom/prisma"; -import { BookingStatus } from "@calcom/prisma/enums"; const querySchema = z.object({ uid: z.string(), @@ -63,6 +61,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { allowReschedulingCancelledBookings: true, team: { select: { + id: true, parentId: true, slug: true, }, @@ -98,7 +97,6 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { notFound: true, } as const; } - const eventType = booking.eventType ? booking.eventType : getDefaultEvent(dynamicEventSlugRef); const userRepo = new UserRepository(prisma); @@ -112,59 +110,36 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { profileEnrichedBookingUser: enrichedBookingUser, }); - const isForcedRescheduleForCancelledBooking = allowRescheduleForCancelledBooking; - // If booking is already REJECTED, we can't reschedule this booking. Take the user to the booking page which would show it's correct status and other details. - // If the booking is CANCELLED and allowRescheduleForCancelledBooking is false, we redirect the user to the original event link. - // A booking that has been rescheduled to a new booking will also have a status of CANCELLED - const isDisabledRescheduling = booking.eventType?.disableRescheduling; - // This comes from query param and thus is considered forced - const canBookThroughCancelledBookingRescheduleLink = booking.eventType?.allowReschedulingCancelledBookings; - const isNonRescheduleableBooking = - booking.status === BookingStatus.CANCELLED || booking.status === BookingStatus.REJECTED; - - if (isDisabledRescheduling) { - return { - redirect: { - destination: `/booking/${uid}`, - permanent: false, - }, - }; - } - - if (isNonRescheduleableBooking && !isForcedRescheduleForCancelledBooking) { - const canReschedule = - booking.status === BookingStatus.CANCELLED && canBookThroughCancelledBookingRescheduleLink; - return { - redirect: { - destination: canReschedule ? eventUrl : `/booking/${uid}`, - permanent: false, - }, - }; - } - if (!booking?.eventType && !booking?.dynamicEventSlugRef) { // TODO: Show something in UI to let user know that this booking is not rescheduleable return { notFound: true, - } as { - notFound: true; - }; + } as const; } - const isBookingInPast = booking.endTime && new Date(booking.endTime) < new Date(); - if (isBookingInPast && !eventType.allowReschedulingPastBookings) { - const destinationUrlSearchParams = new URLSearchParams(); - const responses = bookingSeat ? getSafe(bookingSeat.data, ["responses"]) : booking.responses; - const name = getFullName(getSafe(responses, ["name"])); - const email = getSafe(responses, ["email"]); + // Check if reschedule should be prevented based on booking status and event type settings + const reschedulePreventionRedirectUrl = determineReschedulePreventionRedirect({ + booking: { + uid, + status: booking.status, + endTime: booking.endTime, + responses: booking.responses, + eventType: { + disableRescheduling: !!eventType?.disableRescheduling, + allowReschedulingPastBookings: eventType.allowReschedulingPastBookings, + allowBookingFromCancelledBookingReschedule: !!eventType.allowReschedulingCancelledBookings, + teamId: eventType.team?.id ?? null, + }, + }, + eventUrl, + forceRescheduleForCancelledBooking: allowRescheduleForCancelledBooking, + bookingSeat, + }); - if (name) destinationUrlSearchParams.set("name", name); - if (email) destinationUrlSearchParams.set("email", email); - - const searchParamsString = destinationUrlSearchParams.toString(); + if (reschedulePreventionRedirectUrl) { return { redirect: { - destination: searchParamsString ? `${eventUrl}?${searchParamsString}` : eventUrl, + destination: reschedulePreventionRedirectUrl, permanent: false, }, }; diff --git a/packages/features/bookings/lib/reschedule/determineReschedulePreventionRedirect.test.ts b/packages/features/bookings/lib/reschedule/determineReschedulePreventionRedirect.test.ts new file mode 100644 index 0000000000..933e80cb2b --- /dev/null +++ b/packages/features/bookings/lib/reschedule/determineReschedulePreventionRedirect.test.ts @@ -0,0 +1,412 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; + +import * as constants from "@calcom/lib/constants"; +import { BookingStatus } from "@calcom/prisma/client"; +import type { JsonValue } from "@calcom/types/Json"; + +import { + determineReschedulePreventionRedirect, + type ReschedulePreventionRedirectInput, + type ReschedulePreventionRedirectResult, +} from "./determineReschedulePreventionRedirect"; + +// Mock the constants module +vi.mock("@calcom/lib/constants", async () => { + const actual = (await vi.importActual("@calcom/lib/constants")) as typeof import("@calcom/lib/constants"); + return { + ...actual, + ENV_PAST_BOOKING_RESCHEDULE_CHANGE_TEAM_IDS: undefined, // Default to undefined, will be overridden in tests + }; +}); + +const createTestBooking = (overrides?: { + uid?: string; + status?: BookingStatus; + endTime?: Date | null; + responses?: JsonValue; + eventType?: { + disableRescheduling?: boolean | null; + allowReschedulingPastBookings?: boolean | null; + allowBookingFromCancelledBookingReschedule?: boolean | null; + teamId?: number | null; + } | null; + dynamicEventSlugRef?: string | null; +}) => ({ + uid: overrides?.uid || "test-booking-uid", + status: overrides?.status || BookingStatus.ACCEPTED, + endTime: overrides?.endTime !== undefined ? overrides.endTime : futureDate(5), + responses: overrides?.responses || { + name: "John Doe", + email: "john.doe@example.com", + }, + eventType: + overrides?.eventType !== undefined + ? overrides.eventType + : // Default values from DB + { + disableRescheduling: false, + allowReschedulingPastBookings: false, + allowBookingFromCancelledBookingReschedule: false, + teamId: null, + }, + dynamicEventSlugRef: overrides?.dynamicEventSlugRef !== undefined ? overrides.dynamicEventSlugRef : null, +}); + +const createReschedulePreventionRedirectInput = (overrides?: { + booking?: ReturnType; + eventUrl?: string; + forceRescheduleForCancelledBooking?: boolean; +}): ReschedulePreventionRedirectInput => ({ + booking: overrides?.booking || createTestBooking(), + eventUrl: overrides?.eventUrl || "https://example.com/event", + forceRescheduleForCancelledBooking: overrides?.forceRescheduleForCancelledBooking || false, +}); + +const daysAgo = (days: number) => new Date(Date.now() - days * 24 * 60 * 60 * 1000); +const futureDate = (days: number) => new Date(Date.now() + days * 24 * 60 * 60 * 1000); + +const expectRedirectToBookingDetailsPage = ( + result: ReschedulePreventionRedirectResult, + bookingUid: string +) => { + expect(result).toBe(`/booking/${bookingUid}`); +}; + +const expectRedirectToEventBookingUrl = (result: ReschedulePreventionRedirectResult, eventUrl: string) => { + expect(result).toBe(eventUrl); +}; + +const expectRedirectToEventBookingPageWithParams = ({ + result, + eventUrl, + params, +}: { + result: ReschedulePreventionRedirectResult; + eventUrl: string; + params: Record; +}) => { + console.log("expectRedirectToEventBookingPageWithParams", { result, eventUrl, params }); + if (!result) { + throw new Error("We expected a redirect result"); + } + + const actualRedirectUrlObject = new URL(result); + Object.entries(params).forEach(([key, value]) => { + expect(actualRedirectUrlObject.searchParams.get(key)).toBe(value); + }); + expect(`${actualRedirectUrlObject.origin + actualRedirectUrlObject.pathname}`).toBe(eventUrl); +}; + +const expectToNotPreventReschedule = (result: ReschedulePreventionRedirectResult) => { + expect(result).toBeNull(); +}; + +describe("determineReschedulePreventionRedirect", () => { + beforeEach(() => { + vi.useFakeTimers(); + vi.setSystemTime(new Date("2024-01-15T10:00:00Z")); + // Reset mocked constant to default before each test + vi.mocked(constants).ENV_PAST_BOOKING_RESCHEDULE_CHANGE_TEAM_IDS = undefined; + }); + + afterEach(() => { + vi.useRealTimers(); + vi.clearAllMocks(); + }); + + describe("when booking status is accepted and booking is in future", () => { + it("should not prevent reschedule if disableRescheduling is false", () => { + const testData = { + booking: createTestBooking({ + status: BookingStatus.ACCEPTED, + endTime: futureDate(5), + }), + }; + + const input = createReschedulePreventionRedirectInput(testData); + const result = determineReschedulePreventionRedirect(input); + + expectToNotPreventReschedule(result); + }); + + it("should redirect to booking details/status page when rescheduling is disabled(disableRescheduling is true)", () => { + const input = createReschedulePreventionRedirectInput({ + booking: createTestBooking({ + status: BookingStatus.ACCEPTED, + endTime: futureDate(5), + eventType: { + disableRescheduling: true, + }, + }), + }); + const result = determineReschedulePreventionRedirect(input); + + expectRedirectToBookingDetailsPage(result, input.booking.uid); + }); + }); + + describe("when booking status is cancelled", () => { + it("should redirect to booking details/status page by default", () => { + const input = createReschedulePreventionRedirectInput({ + booking: createTestBooking({ + status: BookingStatus.CANCELLED, + }), + }); + const result = determineReschedulePreventionRedirect(input); + + expectRedirectToBookingDetailsPage(result, input.booking.uid); + }); + + it("should redirect to new booking page when `allowBookingFromCancelledBookingReschedule` is true", () => { + const testData = { + booking: createTestBooking({ + status: BookingStatus.CANCELLED, + eventType: { + allowBookingFromCancelledBookingReschedule: true, + }, + }), + eventUrl: "https://example.com/event", + }; + + const input = createReschedulePreventionRedirectInput(testData); + const result = determineReschedulePreventionRedirect(input); + + expectRedirectToEventBookingUrl(result, testData.eventUrl); + }); + + it("should not prevent reschedule when `forceRescheduleForCancelledBooking` is true regardless of `allowBookingFromCancelledBookingReschedule`", () => { + const inputWhenallowBookingFromCancelledBookingRescheduleIsFalse = + createReschedulePreventionRedirectInput({ + booking: createTestBooking({ + status: BookingStatus.CANCELLED, + eventType: { + allowBookingFromCancelledBookingReschedule: false, + }, + }), + forceRescheduleForCancelledBooking: true, + }); + + const resultWhenallowBookingFromCancelledBookingRescheduleIsFalse = + determineReschedulePreventionRedirect(inputWhenallowBookingFromCancelledBookingRescheduleIsFalse); + expectToNotPreventReschedule(resultWhenallowBookingFromCancelledBookingRescheduleIsFalse); + + const inputWhenallowBookingFromCancelledBookingRescheduleIsTrue = + createReschedulePreventionRedirectInput({ + booking: createTestBooking({ + status: BookingStatus.CANCELLED, + eventType: { + allowBookingFromCancelledBookingReschedule: true, + }, + }), + forceRescheduleForCancelledBooking: true, + }); + const resultWhenallowBookingFromCancelledBookingRescheduleIsTrue = + determineReschedulePreventionRedirect(inputWhenallowBookingFromCancelledBookingRescheduleIsTrue); + + expectToNotPreventReschedule(resultWhenallowBookingFromCancelledBookingRescheduleIsTrue); + }); + }); + + describe("when booking status is rejected", () => { + it("should redirect to booking details/status page by default", () => { + const input = createReschedulePreventionRedirectInput({ + booking: createTestBooking({ + status: BookingStatus.REJECTED, + }), + }); + const result = determineReschedulePreventionRedirect(input); + + expectRedirectToBookingDetailsPage(result, input.booking.uid); + }); + + it("should redirect to booking details/status page even when `allowBookingFromCancelledBookingReschedule` is true as that config doesn't apply to rejected bookings", () => { + const input = createReschedulePreventionRedirectInput({ + booking: createTestBooking({ + status: BookingStatus.REJECTED, + eventType: { + allowBookingFromCancelledBookingReschedule: true, + }, + }), + }); + const result = determineReschedulePreventionRedirect(input); + + expectRedirectToBookingDetailsPage(result, input.booking.uid); + }); + + it("Current Behavior: Current behaviour is to not prevent reschedule. But EXPECTED(should redirect to booking details page even when `forceRescheduleForCancelledBooking` as that config doesn't apply to rejected bookings). ", () => { + const inputWhenallowBookingFromCancelledBookingRescheduleIsFalse = + createReschedulePreventionRedirectInput({ + booking: createTestBooking({ + status: BookingStatus.REJECTED, + eventType: { + allowBookingFromCancelledBookingReschedule: false, + }, + }), + forceRescheduleForCancelledBooking: true, + }); + + const resultWhenallowBookingFromCancelledBookingRescheduleIsFalse = + determineReschedulePreventionRedirect(inputWhenallowBookingFromCancelledBookingRescheduleIsFalse); + // expectRedirectToBookingDetailsPage( + // resultWhenallowBookingFromCancelledBookingRescheduleIsFalse, + // inputWhenallowBookingFromCancelledBookingRescheduleIsFalse.booking.uid + // ); + expectToNotPreventReschedule(resultWhenallowBookingFromCancelledBookingRescheduleIsFalse); + + const inputWhenallowBookingFromCancelledBookingRescheduleIsTrue = + createReschedulePreventionRedirectInput({ + booking: createTestBooking({ + status: BookingStatus.REJECTED, + eventType: { + allowBookingFromCancelledBookingReschedule: true, + }, + }), + forceRescheduleForCancelledBooking: true, + }); + const resultWhenallowBookingFromCancelledBookingRescheduleIsTrue = + determineReschedulePreventionRedirect(inputWhenallowBookingFromCancelledBookingRescheduleIsTrue); + + // expectRedirectToBookingDetailsPage( + // resultWhenallowBookingFromCancelledBookingRescheduleIsTrue, + // inputWhenallowBookingFromCancelledBookingRescheduleIsTrue.booking.uid + // ); + expectToNotPreventReschedule(resultWhenallowBookingFromCancelledBookingRescheduleIsTrue); + }); + }); + + describe("when booking is in the past - Default behaviour(without ENV_PAST_BOOKING_RESCHEDULE_CHANGE_TEAM_IDS)", () => { + it("should redirect to new booking page with prefilled params when allowReschedulingPastBookings is false", () => { + const input = createReschedulePreventionRedirectInput({ + booking: createTestBooking({ + endTime: daysAgo(5), + responses: { + name: "John Doe", + email: "john.doe@example.com", + }, + eventType: { + allowReschedulingPastBookings: false, + }, + }), + }); + const result = determineReschedulePreventionRedirect(input); + + expectRedirectToEventBookingPageWithParams({ + result, + eventUrl: "https://example.com/event", + params: { + name: "John Doe", + email: "john.doe@example.com", + }, + }); + }); + + it("should not prevent reschedule when allowReschedulingPastBookings is true", () => { + const input = createReschedulePreventionRedirectInput({ + booking: createTestBooking({ + endTime: daysAgo(5), + eventType: { + disableRescheduling: false, + allowReschedulingPastBookings: true, + allowBookingFromCancelledBookingReschedule: false, + }, + }), + }); + const result = determineReschedulePreventionRedirect(input); + + expectToNotPreventReschedule(result); + }); + }); + + describe("when booking is in the past - New behaviour(based on ENV_PAST_BOOKING_RESCHEDULE_CHANGE_TEAM_IDS)", () => { + beforeEach(() => { + vi.mocked(constants).ENV_PAST_BOOKING_RESCHEDULE_CHANGE_TEAM_IDS = "123,456,789"; + }); + + it("should redirect to booking status page instead of event URL when the event's teamId is in the environment variable", () => { + const input = createReschedulePreventionRedirectInput({ + booking: createTestBooking({ + uid: "team-booking-uid", + status: BookingStatus.ACCEPTED, + endTime: daysAgo(3), + eventType: { + teamId: 456, // This team ID is in the environment variable + }, + }), + }); + const result = determineReschedulePreventionRedirect(input); + + expectRedirectToBookingDetailsPage(result, input.booking.uid); + }); + + it("should redirect to event URL when the event's teamId is not in the environment variable", () => { + const input = createReschedulePreventionRedirectInput({ + booking: createTestBooking({ + uid: "non-team-booking-uid", + status: BookingStatus.ACCEPTED, + endTime: daysAgo(3), + eventType: { + disableRescheduling: false, + allowReschedulingPastBookings: false, + allowBookingFromCancelledBookingReschedule: false, + teamId: 999, // This team ID is NOT in the environment variable + }, + }), + }); + const result = determineReschedulePreventionRedirect(input); + + // Should redirect to eventUrl with name/email params (fallback behavior) + expectRedirectToEventBookingPageWithParams({ + result, + eventUrl: "https://example.com/event", + params: { + name: "John Doe", + email: "john.doe@example.com", + }, + }); + }); + + it("should redirect to event URL when booking has no team (individual event)", () => { + const input = createReschedulePreventionRedirectInput({ + booking: createTestBooking({ + uid: "no-team-booking-uid", + status: BookingStatus.ACCEPTED, + endTime: daysAgo(3), + eventType: { + disableRescheduling: false, + allowReschedulingPastBookings: false, + allowBookingFromCancelledBookingReschedule: false, + teamId: null, // No team ID + }, + }), + }); + const result = determineReschedulePreventionRedirect(input); + + // Should redirect to eventUrl with name/email params (fallback behavior) + expectRedirectToEventBookingPageWithParams({ + result, + eventUrl: "https://example.com/event", + params: { + name: "John Doe", + email: "john.doe@example.com", + }, + }); + }); + + it("should not prevent reschedule when allowReschedulingPastBookings is true even if the teamId is in the environment variable", () => { + const input = createReschedulePreventionRedirectInput({ + booking: createTestBooking({ + uid: "allowed-past-reschedule-uid", + status: BookingStatus.ACCEPTED, + endTime: daysAgo(3), + eventType: { + allowReschedulingPastBookings: true, // Past reschedule explicitly allowed + teamId: 456, // Team ID is in environment variable + }, + }), + }); + const result = determineReschedulePreventionRedirect(input); + + expectToNotPreventReschedule(result); + }); + }); +}); diff --git a/packages/features/bookings/lib/reschedule/determineReschedulePreventionRedirect.ts b/packages/features/bookings/lib/reschedule/determineReschedulePreventionRedirect.ts new file mode 100644 index 0000000000..8ecf467122 --- /dev/null +++ b/packages/features/bookings/lib/reschedule/determineReschedulePreventionRedirect.ts @@ -0,0 +1,111 @@ +import { URLSearchParams } from "url"; + +import { getFullName } from "@calcom/features/form-builder/utils"; +import { ENV_PAST_BOOKING_RESCHEDULE_CHANGE_TEAM_IDS } from "@calcom/lib/constants"; +import { getSafe } from "@calcom/lib/getSafe"; +import { BookingStatus } from "@calcom/prisma/enums"; +import type { JsonValue } from "@calcom/types/Json"; + +export type ReschedulePreventionRedirectInput = { + booking: { + uid: string; + status: BookingStatus; + endTime: Date | null; + responses?: JsonValue; + eventType: { + disableRescheduling: boolean; + allowReschedulingPastBookings: boolean; + allowBookingFromCancelledBookingReschedule: boolean; + teamId: number | null; + }; + }; + eventUrl: string; + forceRescheduleForCancelledBooking?: boolean; + bookingSeat?: { + data: JsonValue; + booking: { + uid: string; + id: number; + }; + }; +}; + +export type ReschedulePreventionRedirectResult = string | null; + +/** + * + * Parses the PAST_BOOKING_RESCHEDULE_NO_BOOKING_BEHAVIOUR environment variable and checks if the given team ID has the changed behaviour for past booking reschedule enabled + * + * The behaviour is that it does not allow allowing booking through the reschedule link of a past booking by default. If allowReschedulingPastBookings is true, then this behaviour isn't applicable + */ +function isPastBookingRescheduleBehaviourToPreventBooking(teamId: number | null | undefined): boolean { + if (!teamId) { + return false; + } + + if (!ENV_PAST_BOOKING_RESCHEDULE_CHANGE_TEAM_IDS) { + return false; + } + + const configuredTeamIds = ENV_PAST_BOOKING_RESCHEDULE_CHANGE_TEAM_IDS.split(",") + .map((id) => id.trim()) + .filter((id) => id !== "") + .map((id) => parseInt(id, 10)) + .filter((id) => !isNaN(id)); + + return configuredTeamIds.includes(teamId); +} + +/** + * Determines the appropriate redirect URL for a reschedule request based on booking status and event type settings + * Returns the redirect URL string if a redirect is needed, null if reschedule should proceed normally + */ +export function determineReschedulePreventionRedirect( + input: ReschedulePreventionRedirectInput +): ReschedulePreventionRedirectResult { + const { booking, eventUrl, forceRescheduleForCancelledBooking, bookingSeat } = input; + + const isDisabledRescheduling = booking.eventType.disableRescheduling; + if (isDisabledRescheduling) { + return `/booking/${booking.uid}`; + } + + const isNonRescheduleableBooking = + booking.status === BookingStatus.CANCELLED || booking.status === BookingStatus.REJECTED; + const isForcedRescheduleForCancelledBooking = forceRescheduleForCancelledBooking; + + if (isNonRescheduleableBooking && !isForcedRescheduleForCancelledBooking) { + const canBookThroughCancelledBookingRescheduleLink = + booking.eventType.allowBookingFromCancelledBookingReschedule; + const allowedToBeBookedThroughCancelledBookingRescheduleLink = + booking.status === BookingStatus.CANCELLED && canBookThroughCancelledBookingRescheduleLink; + + return allowedToBeBookedThroughCancelledBookingRescheduleLink ? eventUrl : `/booking/${booking.uid}`; + } + + const isBookingInPast = booking.endTime && new Date(booking.endTime) < new Date(); + if (isBookingInPast && !booking.eventType.allowReschedulingPastBookings) { + // Check if this team should apply the redirect behavior for past bookings + const isNewPastBookingRescheduleBehaviour = isPastBookingRescheduleBehaviourToPreventBooking( + booking.eventType.teamId + ); + + if (isNewPastBookingRescheduleBehaviour) { + return `/booking/${booking.uid}`; + } + + const destinationUrlSearchParams = new URLSearchParams(); + const responses = bookingSeat ? getSafe(bookingSeat.data, ["responses"]) : booking.responses; + const name = getFullName(getSafe(responses, ["name"])); + const email = getSafe(responses, ["email"]); + + if (name) destinationUrlSearchParams.set("name", name); + if (email) destinationUrlSearchParams.set("email", email); + + const searchParamsString = destinationUrlSearchParams.toString(); + return searchParamsString ? `${eventUrl}?${searchParamsString}` : eventUrl; + } + + // Allow reschedule to proceed - default behaviour + return null; +} diff --git a/packages/lib/constants.ts b/packages/lib/constants.ts index 3fee151ca6..defb3cff46 100644 --- a/packages/lib/constants.ts +++ b/packages/lib/constants.ts @@ -249,3 +249,8 @@ export const RETELL_AI_TEST_EVENT_TYPE_MAP = (() => { return null; } })(); + +// Environment variable for configuring past booking reschedule behavior per team. A comma separated list of team IDs(e.g. '1,2,3') +/* This is an internal environment variable and is not meant to be used by the self-hosters. It is planned to be removed later by either having it as an option in Event Type or by some other customer configurable approaches*/ +export const ENV_PAST_BOOKING_RESCHEDULE_CHANGE_TEAM_IDS = + process.env._CAL_INTERNAL_PAST_BOOKING_RESCHEDULE_CHANGE_TEAM_IDS; diff --git a/packages/lib/defaultEvents.ts b/packages/lib/defaultEvents.ts index 511c135288..d282126f94 100644 --- a/packages/lib/defaultEvents.ts +++ b/packages/lib/defaultEvents.ts @@ -92,6 +92,7 @@ const commons = { disableRescheduling: false, onlyShowFirstAvailableSlot: false, allowReschedulingPastBookings: false, + allowReschedulingCancelledBookings: false, hideOrganizerEmail: false, showOptimizedSlots: false, id: 0, diff --git a/turbo.json b/turbo.json index 5ab7c99b39..d6301c1d1d 100644 --- a/turbo.json +++ b/turbo.json @@ -279,7 +279,8 @@ "ATOMS_E2E_APPLE_ID", "ATOMS_E2E_APPLE_CONNECT_APP_SPECIFIC_PASSCODE", "INTERCOM_API_TOKEN", - "NEXT_PUBLIC_INTERCOM_APP_ID" + "NEXT_PUBLIC_INTERCOM_APP_ID", + "_CAL_INTERNAL_PAST_BOOKING_RESCHEDULE_CHANGE_TEAM_IDS" ], "tasks": { "@calcom/prisma#build": {