feat: Allow reschedule when max booker bookings have been reached (#21778)

* feat - Restrict same email to create more than 'n' active bookings at a time

* updated checkbookerbookinglimit function

* type fix

* minor change

* import fix

* minor fixes

* back to null on disable

* back

* type check

* managed edge cases

* chore: name changes

* name changes

* fix

* minor change

* changed name

* use default value for maxactivebookingsperbooker, and some minor changes

* disabling bookerbooking limit for recurring event

* disabling bookerbooking limit for recurring event

* type fix

* ui fix and backend eventtype update check

* Add `maxActiveBookingPerBookerOfferReschedule` to schema

* Create `MaxActiveBookingsPerBookerController` and offer reschedule option

* Add offer reschedule to event type form data

* Pass data through to HttpError

* When checking max bookings, return last booking info if applicable

* removed unused code

* minor changes

* update validation

* chore

* Do not check booking limits if rescheduling

* Add data for reschedule

* Add reschedule specific error code

* On maximum booking error, write to booker store reschedule params

* Add translations for error codes

* Write to error message previous booking time

* minor fix

* Write to error message previous booking time

* Type fixes

* Clean up comment

* Refactor eventType update errors

* Typo fix

* Type fix

* Type fix

* Type fix

* Fix test

* Fix test

* Add migration

* Addressed feedback and missed merges

---------

Co-authored-by: romit <romitgabani@icloud.com>
This commit is contained in:
Joe Au-Yeungandromit authored and GitHub committed 2025-06-13 23:54:33 +00:00
1 parent ad93099171
commit 54e518efad
23 files changed
+349 -141

No files matched your search

-5
View File
@@ -42,11 +42,6 @@ describe("GET /api/slots", () => {
await handler(req, res);
expect(res.statusCode).toBe(400);
expect(JSON.parse(res._getData())).toMatchInlineSnapshot(`
{
"message": "invalid_type in 'startTime': Required; invalid_type in 'endTime': Required",
}
`);
});
});
@@ -3246,6 +3246,9 @@
"meeting_start_time": "Meeting Start Time",
"members_affected_by_disabling_delegation_credential": "Affected members",
"no_members_affected_by_disabling_delegation_credential": "No members affected by disabling delegation credential",
"offer_to_reschedule_last_booking": "Offer to reschedule last active booking to chosen time slot",
"booker_limit_exceeded_error": "Booker maximum active booking limit exceeded",
"booker_limit_exceeded_error_reschedule": "You already have a booking for this event on {{date}}. Would you like to reschedule to the new selected time?",
"duration_min_error": "Duration must be at least {{min}} minute(s)",
"duration_max_error": "Duration cannot exceed {{max}} minutes (24 hours)",
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
@@ -237,7 +237,6 @@ const BookerComponent = ({
bookingForm={bookingForm}
eventQuery={event}
extraOptions={extraOptions}
rescheduleUid={rescheduleUid}
isVerificationCodeSending={isVerificationCodeSending}
confirmButtonDisabled={confirmButtonDisabled}
classNames={{
@@ -278,7 +277,6 @@ const BookerComponent = ({
loadingStates,
onGoBackInstantMeeting,
renderConfirmNotVerifyEmailButtonCond,
rescheduleUid,
seatedEventData,
setSeatedEventData,
setSelectedTimeslot,
@@ -7,14 +7,18 @@ import { useIsPlatformBookerEmbed } from "@calcom/atoms/hooks/useIsPlatformBooke
import type { BookerEvent } from "@calcom/features/bookings/types";
import ServerTrans from "@calcom/lib/components/ServerTrans";
import { WEBSITE_PRIVACY_POLICY_URL, WEBSITE_TERMS_URL } from "@calcom/lib/constants";
import { ErrorCode } from "@calcom/lib/errorCodes";
import { getPaymentAppData } from "@calcom/lib/getPaymentAppData";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { TimeFormat } from "@calcom/lib/timeFormat";
import { Alert } from "@calcom/ui/components/alert";
import { Button } from "@calcom/ui/components/button";
import { EmptyScreen } from "@calcom/ui/components/empty-screen";
import { Form } from "@calcom/ui/components/form";
import { useBookerStore } from "../../store";
import { formatEventFromTime } from "../../utils/dates";
import { useBookerTime } from "../hooks/useBookerTime";
import type { UseBookingFormReturnType } from "../hooks/useBookingForm";
import type { IUseBookingErrors, IUseBookingLoadingStates } from "../hooks/useBookings";
import { BookingFields } from "./BookingFields";
@@ -44,7 +48,6 @@ type BookEventFormProps = {
export const BookEventForm = ({
onCancel,
eventQuery,
rescheduleUid,
onSubmit,
errorRef,
errors,
@@ -65,18 +68,19 @@ export const BookEventForm = ({
isPending: boolean;
data?: Pick<BookerEvent, "price" | "currency" | "metadata" | "bookingFields" | "locations"> | null;
};
rescheduleUid: string | null;
}) => {
const eventType = eventQuery.data;
const setFormValues = useBookerStore((state) => state.setFormValues);
const bookingData = useBookerStore((state) => state.bookingData);
const rescheduleUid = useBookerStore((state) => state.rescheduleUid);
const timeslot = useBookerStore((state) => state.selectedTimeslot);
const username = useBookerStore((state) => state.username);
const isInstantMeeting = useBookerStore((state) => state.isInstantMeeting);
const isPlatformBookerEmbed = useIsPlatformBookerEmbed();
const { timeFormat, timezone } = useBookerTime();
const [responseVercelIdHeader] = useState<string | null>(null);
const { t } = useLocale();
const { t, i18n } = useLocale();
const isPaidEvent = useMemo(() => {
if (!eventType?.price) return false;
@@ -132,7 +136,15 @@ export const BookEventForm = ({
className="my-2"
severity="info"
title={rescheduleUid ? t("reschedule_fail") : t("booking_fail")}
message={getError(errors.formErrors, errors.dataErrors, t, responseVercelIdHeader)}
message={getError({
globalError: errors.formErrors,
dataError: errors.dataErrors,
t,
responseVercelIdHeader,
timeFormat,
timezone,
language: i18n.language,
})}
/>
</div>
) : isTimeslotUnavailable ? (
@@ -255,23 +267,46 @@ export const BookEventForm = ({
);
};
const getError = (
globalError: FieldError | undefined,
const getError = ({
globalError,
dataError,
t,
responseVercelIdHeader,
timeFormat,
timezone,
language,
}: {
globalError: FieldError | undefined;
// It feels like an implementation detail to reimplement the types of useMutation here.
// Since they don't matter for this function, I'd rather disable them then giving you
// the cognitive overload of thinking to update them here when anything changes.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
dataError: any,
t: TFunction,
responseVercelIdHeader: string | null
) => {
dataError: any;
t: TFunction;
responseVercelIdHeader: string | null;
timeFormat: TimeFormat;
timezone: string;
language: string;
}) => {
if (globalError) return globalError?.message;
const error = dataError;
let date = "";
if (error.message === ErrorCode.BookerLimitExceededReschedule) {
const formattedDate = formatEventFromTime({
date: error.data.startTime,
timeFormat,
timeZone: timezone,
language,
});
date = `${formattedDate.date} ${formattedDate.time}`;
}
return error?.message ? (
<>
{responseVercelIdHeader ?? ""} {t(error.message)}
{responseVercelIdHeader ?? ""} {t(error.message, { date })}
</>
) : (
<>{t("can_you_try_again")}</>
@@ -11,9 +11,11 @@ import { sdkActionManager } from "@calcom/embed-core/embed-iframe";
import { useBookerStore } from "@calcom/features/bookings/Booker/store";
import { updateQueryParam, getQueryParam } from "@calcom/features/bookings/Booker/utils/query-param";
import { createBooking, createRecurringBooking, createInstantBooking } from "@calcom/features/bookings/lib";
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
import type { BookerEvent } from "@calcom/features/bookings/types";
import { getFullName } from "@calcom/features/form-builder/utils";
import { useBookingSuccessRedirect } from "@calcom/lib/bookingSuccessRedirect";
import { ErrorCode } from "@calcom/lib/errorCodes";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { localStorage } from "@calcom/lib/webstorage";
import { BookingStatus } from "@calcom/prisma/enums";
@@ -285,6 +287,23 @@ export const useBookings = ({ event, hashedLink, bookingForm, metadata, teamMemb
onError: (err, _, ctx) => {
// eslint-disable-next-line @calcom/eslint/no-scroll-into-view-embed -- It is only called when user takes an action in embed
bookerFormErrorRef && bookerFormErrorRef.current?.scrollIntoView({ behavior: "smooth" });
const error = err as Error & {
data: { rescheduleUid: string; startTime: string; attendees: string[] };
};
if (error.message === ErrorCode.BookerLimitExceededReschedule && error.data?.rescheduleUid) {
useBookerStore.setState({
rescheduleUid: error.data?.rescheduleUid,
});
useBookerStore.setState({
bookingData: {
uid: error.data?.rescheduleUid,
startTime: error.data?.startTime,
attendees: error.data?.attendees,
} as unknown as GetBookingType,
});
}
},
});
@@ -52,6 +52,7 @@ import {
getFirstDelegationConferencingCredentialAppLocation,
} from "@calcom/lib/delegationCredential/server";
import { ErrorCode } from "@calcom/lib/errorCodes";
import type { ErrorWithCode } from "@calcom/lib/errors";
import { getErrorFromUnknown } from "@calcom/lib/errors";
import { getEventName, updateHostInEventName } from "@calcom/lib/event";
import { extractBaseEmail } from "@calcom/lib/extract-base-email";
@@ -454,11 +455,22 @@ async function handler(
await checkIfBookerEmailIsBlocked({ loggedInUserId: userId, bookerEmail });
if (!rawBookingData.rescheduleUid) {
await checkActiveBookingsLimitForBooker({
eventTypeId,
maxActiveBookingsPerBooker: eventType.maxActiveBookingsPerBooker,
bookerEmail,
});
try {
await checkActiveBookingsLimitForBooker({
eventTypeId,
maxActiveBookingsPerBooker: eventType.maxActiveBookingsPerBooker,
bookerEmail,
offerToRescheduleLastBooking: eventType.maxActiveBookingPerBookerOfferReschedule,
});
} catch (err) {
const error = err as ErrorWithCode;
throw new HttpError({
statusCode: 400,
message: error.message,
data: error.data,
});
}
}
if (isEventTypeLoggingEnabled({ eventTypeId, usernameOrTeamName: reqBody.user })) {
@@ -1,3 +1,5 @@
import { ErrorCode } from "@calcom/lib/errorCodes";
import { ErrorWithCode } from "@calcom/lib/errors";
import prisma from "@calcom/prisma";
import { BookingStatus } from "@calcom/prisma/enums";
@@ -5,15 +7,38 @@ export const checkActiveBookingsLimitForBooker = async ({
eventTypeId,
maxActiveBookingsPerBooker,
bookerEmail,
offerToRescheduleLastBooking,
}: {
eventTypeId: number;
maxActiveBookingsPerBooker: number | null;
bookerEmail: string;
offerToRescheduleLastBooking: boolean;
}) => {
if (!maxActiveBookingsPerBooker) {
return;
}
if (offerToRescheduleLastBooking) {
await checkActiveBookingsLimitAndOfferReschedule({
eventTypeId,
maxActiveBookingsPerBooker,
bookerEmail,
});
} else {
await checkActiveBookingsLimit({ eventTypeId, maxActiveBookingsPerBooker, bookerEmail });
}
};
/** If we don't need the last record then we should just use COUNT */
const checkActiveBookingsLimit = async ({
eventTypeId,
maxActiveBookingsPerBooker,
bookerEmail,
}: {
eventTypeId: number;
maxActiveBookingsPerBooker: number;
bookerEmail: string;
}) => {
const bookingsCount = await prisma.booking.count({
where: {
eventTypeId,
@@ -32,8 +57,61 @@ export const checkActiveBookingsLimitForBooker = async ({
});
if (bookingsCount >= maxActiveBookingsPerBooker) {
throw new Error("Booker maximum active booking limit exceeded");
throw new ErrorWithCode(ErrorCode.BookerLimitExceeded, ErrorCode.BookerLimitExceeded);
}
};
const checkActiveBookingsLimitAndOfferReschedule = async ({
eventTypeId,
maxActiveBookingsPerBooker,
bookerEmail,
}: {
eventTypeId: number;
maxActiveBookingsPerBooker: number;
bookerEmail: string;
}) => {
const bookingsCount = await prisma.booking.findMany({
where: {
eventTypeId,
startTime: {
gte: new Date(),
},
status: {
in: [BookingStatus.ACCEPTED],
},
attendees: {
some: {
email: bookerEmail,
},
},
},
orderBy: {
startTime: "desc",
},
take: maxActiveBookingsPerBooker,
select: {
uid: true,
startTime: true,
attendees: {
select: {
name: true,
email: true,
},
},
},
});
const lastBooking = bookingsCount[bookingsCount.length - 1];
if (bookingsCount.length >= maxActiveBookingsPerBooker) {
throw new ErrorWithCode(
ErrorCode.BookerLimitExceededReschedule,
ErrorCode.BookerLimitExceededReschedule,
{
rescheduleUid: lastBooking.uid,
startTime: lastBooking.startTime,
attendees: lastBooking.attendees,
}
);
}
return;
};
@@ -66,6 +66,7 @@ export const getEventTypesFromDB = async (eventTypeId: number) => {
includeNoShowInRRCalculation: true,
minimumBookingNotice: true,
maxActiveBookingsPerBooker: true,
maxActiveBookingPerBookerOfferReschedule: true,
userId: true,
price: true,
currency: true,
@@ -30,6 +30,8 @@ import {
SettingsToggle,
} from "@calcom/ui/components/form";
import MaxActiveBookingsPerBookerController from "./MaxActiveBookingsPerBookerController";
type IPeriodType = (typeof PeriodType)[keyof typeof PeriodType];
export type EventLimitsTabCustomClassNames = {
@@ -685,52 +687,7 @@ export const EventLimitsTab = ({ eventType, customClassNames }: EventLimitsTabPr
);
}}
/>
<Controller
name="maxActiveBookingsPerBooker"
render={({ field: { onChange, value } }) => {
const isChecked = maxActiveBookingsPerBookerToggle;
return (
<SettingsToggle
labelClassName={classNames("text-sm")}
disabled={isRecurringEvent}
tooltip={isRecurringEvent ? t("recurring_event_doesnt_support_booker_booking_limit") : ""}
toggleSwitchAtTheEnd={true}
switchContainerClassName={classNames(
"border-subtle mt-6 rounded-lg border py-6 px-4 sm:px-6",
isChecked && "rounded-b-none"
)}
childrenClassName={classNames("lg:ml-0")}
title={t("booker_booking_limit")}
description={t("booker_booking_limit_description")}
checked={isChecked}
onCheckedChange={(active) => {
if (active) {
onChange(1);
} else {
onChange(null);
}
setMaxActiveBookingsPerBookerToggle((state) => !state);
}}>
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
<TextField
required
type="number"
value={value}
onChange={(e) => {
onChange(parseInt(e.target.value, 10));
}}
placeholder="1"
min={1}
step={1}
containerClassName={classNames("max-w-80")}
addOnSuffix="bookings"
data-testid="booker-booking-limit-input"
/>
</div>
</SettingsToggle>
);
}}
/>
<MaxActiveBookingsPerBookerController />
<Controller
name="periodType"
render={({ field: { onChange, value } }) => {
@@ -0,0 +1,79 @@
import { useState } from "react";
import { Controller, useFormContext } from "react-hook-form";
import type { FormValues } from "@calcom/features/eventtypes/lib/types";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import classNames from "@calcom/ui/classNames";
import { SettingsToggle } from "@calcom/ui/components/form";
import { TextField, CheckboxField } from "@calcom/ui/components/form";
export default function MaxActiveBookingsPerBookerController() {
const { t } = useLocale();
const formMethods = useFormContext<FormValues>();
const [maxActiveBookingsPerBookerToggle, setMaxActiveBookingsPerBookerToggle] = useState(
(formMethods.getValues("maxActiveBookingsPerBooker") ?? 0) > 0
);
const isRecurringEvent = !!formMethods.getValues("recurringEvent");
const maxActiveBookingPerBookerOfferReschedule = formMethods.watch(
"maxActiveBookingPerBookerOfferReschedule"
);
return (
<Controller
name="maxActiveBookingsPerBooker"
render={({ field: { onChange, value } }) => {
const isChecked = maxActiveBookingsPerBookerToggle;
return (
<SettingsToggle
labelClassName={classNames("text-sm")}
disabled={isRecurringEvent}
tooltip={isRecurringEvent ? t("recurring_event_doesnt_support_booker_booking_limit") : ""}
toggleSwitchAtTheEnd={true}
switchContainerClassName={classNames(
"border-subtle mt-6 rounded-lg border py-6 px-4 sm:px-6",
isChecked && "rounded-b-none"
)}
childrenClassName={classNames("lg:ml-0")}
title={t("booker_booking_limit")}
description={t("booker_booking_limit_description")}
checked={isChecked}
onCheckedChange={(active) => {
if (active) {
onChange(1);
} else {
onChange(null);
}
setMaxActiveBookingsPerBookerToggle((state) => !state);
}}>
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
<TextField
required
type="number"
value={value ?? ""}
onChange={(e) => {
onChange(e.target.value === "" ? null : parseInt(e.target.value, 10));
}}
min={1}
step={1}
containerClassName={classNames("max-w-80")}
addOnSuffix="bookings"
data-testid="booker-booking-limit-input"
/>
<CheckboxField
checked={!!maxActiveBookingPerBookerOfferReschedule}
descriptionAsLabel
description={t("offer_to_reschedule_last_booking")}
onChange={(e) => {
formMethods.setValue("maxActiveBookingPerBookerOfferReschedule", e.target.checked, {
shouldDirty: true,
});
}}
/>
</div>
</SettingsToggle>
);
}}
/>
);
}
@@ -158,6 +158,7 @@ export type FormValues = {
enableAutomaticTranscription?: boolean;
redirectUrlOnExit?: string;
};
maxActiveBookingPerBookerOfferReschedule: boolean;
};
export type LocationFormValues = Pick<FormValues, "id" | "locations" | "bookingFields" | "seatsPerTimeSlot">;
+1
View File
@@ -105,6 +105,7 @@ const commons = {
requiresBookerEmailVerification: false,
bookingLimits: null,
maxActiveBookingsPerBooker: null,
maxActiveBookingPerBookerOfferReschedule: false,
durationLimits: null,
hidden: false,
userId: 0,
+2
View File
@@ -18,4 +18,6 @@ export enum ErrorCode {
CreatingOauthClientError = "creating_oauth_client_error",
BookingTimeOutOfBounds = "booking_time_out_of_bounds_error",
BookingConflict = "booking_conflict_error",
BookerLimitExceeded = "booker_limit_exceeded_error",
BookerLimitExceededReschedule = "booker_limit_exceeded_error_reschedule",
}
+8 -4
View File
@@ -6,10 +6,14 @@ async function http<T>(path: string, config: RequestInit): Promise<T> {
if (!response.ok) {
const errJson = await response.json();
const err = HttpError.fromRequest(request, {
...response,
statusText: errJson.message || response.statusText,
});
const err = HttpError.fromRequest(
request,
{
...response,
statusText: errJson.message || response.statusText,
},
errJson
);
throw err;
}
// may error if there is no body, return empty array
+14 -2
View File
@@ -4,8 +4,16 @@ export class HttpError<TCode extends number = number> extends Error {
public readonly message: string;
public readonly url: string | undefined;
public readonly method: string | undefined;
public readonly data?: Record<string, unknown>;
constructor(opts: { url?: string; method?: string; message?: string; statusCode: TCode; cause?: Error }) {
constructor(opts: {
url?: string;
method?: string;
message?: string;
statusCode: TCode;
cause?: Error;
data?: Record<string, unknown>;
}) {
super(opts.message ?? `HTTP Error ${opts.statusCode} `);
Object.setPrototypeOf(this, HttpError.prototype);
@@ -16,18 +24,22 @@ export class HttpError<TCode extends number = number> extends Error {
this.url = opts.url;
this.method = opts.method;
this.message = opts.message ?? `HTTP Error ${opts.statusCode}`;
this.data = opts.data;
if (opts.cause instanceof Error && opts.cause.stack) {
this.stack = opts.cause.stack;
}
}
public static fromRequest(request: Request, response: Response) {
public static fromRequest(request: Request, response: Response, parsedError: Record<string, unknown>) {
return new HttpError({
message: response.statusText,
url: response.url,
method: request.method,
statusCode: response.status,
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore the data property is a custom one from ErrorWithCode
data: parsedError.data as Record<string, unknown>,
});
}
}
+1 -1
View File
@@ -33,7 +33,7 @@ export function defaultResponder<T>(
}
return res
.status(error.statusCode)
.json({ message: error.message, url: error.url, method: error.method });
.json({ message: error.message, url: error.url, method: error.method, data: error?.data || null });
} finally {
performance.mark("End");
performance.measure(`[${ok ? "OK" : "ERROR"}][$1] ${req.method} '${req.url}'`, "Start", "End");
@@ -531,6 +531,7 @@ export class EventTypeRepository {
onlyShowFirstAvailableSlot: true,
durationLimits: true,
maxActiveBookingsPerBooker: true,
maxActiveBookingPerBookerOfferReschedule: true,
assignAllTeamMembers: true,
allowReschedulingPastBookings: true,
hideOrganizerEmail: true,
+1
View File
@@ -134,6 +134,7 @@ export const buildEventType = (eventType?: Partial<EventType>): EventType => {
scheduleId: null,
bookingLimits: null,
maxActiveBookingsPerBooker: null,
maxActiveBookingPerBookerOfferReschedule: false,
durationLimits: null,
assignAllTeamMembers: false,
rescheduleWithSameRoundRobinHost: false,
@@ -69,7 +69,6 @@ export const useEventTypeForm = ({
schedule: eventType.schedule || undefined,
instantMeetingSchedule: eventType.instantMeetingSchedule || undefined,
bookingLimits: eventType.bookingLimits || undefined,
maxActiveBookingsPerBooker: eventType.maxActiveBookingsPerBooker || undefined,
onlyShowFirstAvailableSlot: eventType.onlyShowFirstAvailableSlot || undefined,
durationLimits: eventType.durationLimits || undefined,
length: eventType.length,
@@ -138,6 +137,8 @@ export const useEventTypeForm = ({
useEventLevelSelectedCalendars: eventType.useEventLevelSelectedCalendars,
customReplyToEmail: eventType.customReplyToEmail || null,
calVideoSettings: eventType.calVideoSettings,
maxActiveBookingsPerBooker: eventType.maxActiveBookingsPerBooker || null,
maxActiveBookingPerBookerOfferReschedule: eventType.maxActiveBookingPerBookerOfferReschedule,
};
}, [eventType, periodDates]);
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "EventType" ADD COLUMN "maxActiveBookingPerBookerOfferReschedule" BOOLEAN NOT NULL DEFAULT false;
+48 -47
View File
@@ -134,64 +134,65 @@ model EventType {
requiresBookerEmailVerification Boolean @default(false)
canSendCalVideoTranscriptionEmails Boolean @default(true)
autoTranslateDescriptionEnabled Boolean @default(false)
autoTranslateDescriptionEnabled Boolean @default(false)
/// @zod.custom(imports.recurringEventType)
recurringEvent Json?
disableGuests Boolean @default(false)
hideCalendarNotes Boolean @default(false)
hideCalendarEventDetails Boolean @default(false)
recurringEvent Json?
disableGuests Boolean @default(false)
hideCalendarNotes Boolean @default(false)
hideCalendarEventDetails Boolean @default(false)
/// @zod.min(0)
minimumBookingNotice Int @default(120)
beforeEventBuffer Int @default(0)
afterEventBuffer Int @default(0)
seatsPerTimeSlot Int?
onlyShowFirstAvailableSlot Boolean @default(false)
disableCancelling Boolean? @default(false)
disableRescheduling Boolean? @default(false)
seatsShowAttendees Boolean? @default(false)
seatsShowAvailabilityCount Boolean? @default(true)
schedulingType SchedulingType?
schedule Schedule? @relation(fields: [scheduleId], references: [id])
scheduleId Int?
allowReschedulingCancelledBookings Boolean? @default(false)
minimumBookingNotice Int @default(120)
beforeEventBuffer Int @default(0)
afterEventBuffer Int @default(0)
seatsPerTimeSlot Int?
onlyShowFirstAvailableSlot Boolean @default(false)
disableCancelling Boolean? @default(false)
disableRescheduling Boolean? @default(false)
seatsShowAttendees Boolean? @default(false)
seatsShowAvailabilityCount Boolean? @default(true)
schedulingType SchedulingType?
schedule Schedule? @relation(fields: [scheduleId], references: [id])
scheduleId Int?
allowReschedulingCancelledBookings Boolean? @default(false)
// price is deprecated. It has now moved to metadata.apps.stripe.price. Plan to drop this column.
price Int @default(0)
price Int @default(0)
// currency is deprecated. It has now moved to metadata.apps.stripe.currency. Plan to drop this column.
currency String @default("usd")
slotInterval Int?
currency String @default("usd")
slotInterval Int?
/// @zod.custom(imports.EventTypeMetaDataSchema)
metadata Json?
metadata Json?
/// @zod.custom(imports.successRedirectUrl)
successRedirectUrl String?
forwardParamsSuccessRedirect Boolean? @default(true)
workflows WorkflowsOnEventTypes[]
successRedirectUrl String?
forwardParamsSuccessRedirect Boolean? @default(true)
workflows WorkflowsOnEventTypes[]
/// @zod.custom(imports.intervalLimitsType)
bookingLimits Json?
bookingLimits Json?
/// @zod.custom(imports.intervalLimitsType)
durationLimits Json?
isInstantEvent Boolean @default(false)
instantMeetingExpiryTimeOffsetInSeconds Int @default(90)
instantMeetingScheduleId Int?
instantMeetingSchedule Schedule? @relation("InstantMeetingSchedule", fields: [instantMeetingScheduleId], references: [id])
instantMeetingParameters String[]
assignAllTeamMembers Boolean @default(false)
durationLimits Json?
isInstantEvent Boolean @default(false)
instantMeetingExpiryTimeOffsetInSeconds Int @default(90)
instantMeetingScheduleId Int?
instantMeetingSchedule Schedule? @relation("InstantMeetingSchedule", fields: [instantMeetingScheduleId], references: [id])
instantMeetingParameters String[]
assignAllTeamMembers Boolean @default(false)
// It is applicable only when assignAllTeamMembers is true and it filters out all the team members using rrSegmentQueryValue
assignRRMembersUsingSegment Boolean @default(false)
assignRRMembersUsingSegment Boolean @default(false)
/// @zod.custom(imports.rrSegmentQueryValueSchema)
rrSegmentQueryValue Json?
useEventTypeDestinationCalendarEmail Boolean @default(false)
aiPhoneCallConfig AIPhoneCallConfiguration?
isRRWeightsEnabled Boolean @default(false)
fieldTranslations EventTypeTranslation[]
maxLeadThreshold Int?
includeNoShowInRRCalculation Boolean @default(false)
selectedCalendars SelectedCalendar[]
allowReschedulingPastBookings Boolean @default(false)
hideOrganizerEmail Boolean @default(false)
maxActiveBookingsPerBooker Int?
rrSegmentQueryValue Json?
useEventTypeDestinationCalendarEmail Boolean @default(false)
aiPhoneCallConfig AIPhoneCallConfiguration?
isRRWeightsEnabled Boolean @default(false)
fieldTranslations EventTypeTranslation[]
maxLeadThreshold Int?
includeNoShowInRRCalculation Boolean @default(false)
selectedCalendars SelectedCalendar[]
allowReschedulingPastBookings Boolean @default(false)
hideOrganizerEmail Boolean @default(false)
maxActiveBookingsPerBooker Int?
maxActiveBookingPerBookerOfferReschedule Boolean @default(false)
/// @zod.custom(imports.emailSchema)
customReplyToEmail String?
calVideoSettings CalVideoSettings?
customReplyToEmail String?
calVideoSettings CalVideoSettings?
/// @zod.custom(imports.eventTypeColor)
eventTypeColor Json?
+1
View File
@@ -663,6 +663,7 @@ export const allManagedEventTypeProps: { [k in keyof Omit<Prisma.EventTypeSelect
bookingFields: true,
durationLimits: true,
maxActiveBookingsPerBooker: true,
maxActiveBookingPerBookerOfferReschedule: true,
lockTimeZoneToggleOnBookingPage: true,
requiresBookerEmailVerification: true,
assignAllTeamMembers: true,
@@ -261,22 +261,26 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
data.bookingLimits = bookingLimits;
}
if (maxActiveBookingsPerBooker && maxActiveBookingsPerBooker < 1) {
throw new TRPCError({ code: "BAD_REQUEST", message: "Booker booking limit must be greater than 0." });
}
if (maxActiveBookingsPerBooker) {
if (maxActiveBookingsPerBooker && maxActiveBookingsPerBooker < 1) {
throw new TRPCError({ code: "BAD_REQUEST", message: "Booker booking limit must be greater than 0." });
}
if (maxActiveBookingsPerBooker && (recurringEvent || eventType.recurringEvent)) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Recurring Events and booker active bookings limit cannot be active at the same time.",
});
}
if (maxActiveBookingsPerBooker && (recurringEvent || eventType.recurringEvent)) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Recurring Events and booker active bookings limit cannot be active at the same time.",
});
}
if (eventType.maxActiveBookingsPerBooker && recurringEvent) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Recurring Events and booker active bookings limit cannot be active at the same time.",
});
if (eventType.maxActiveBookingsPerBooker && recurringEvent) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Recurring Events and booker active bookings limit cannot be active at the same time.",
});
}
data.maxActiveBookingsPerBooker = maxActiveBookingsPerBooker;
}
if (durationLimits) {