fix: don't allow seats for recurring events (#19257)

* fix: Don't allow seats for recurring events #19061

* update the commit with English

* Update common.json

* Update common.json

* Update common.json

* Add edge case when people have already enabled seats as well as recurring event

* fix type issue

* Handle case where seats and recurring is already enabled

* minor fix

* minor change

* typo

* Block backend functionality

* blocking new booking and other enhancements

* final-changes

* minor-changes

---------

Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: Tushar Bhatt <95581504+TusharBhatt1@users.noreply.github.com>
Co-authored-by: Tushar <tusharbhatt0135@gmail.com>
This commit is contained in:
authored and GitHub committed 2025-04-15 21:07:57 +00:00
1 parent 3e466022b8
commit 58315e96c9
8 files changed
+108 -4

No files matched your search

+33
View File
@@ -658,3 +658,36 @@ test.describe("Event type with disabled cancellation and rescheduling", () => {
expect(responseBody.message).toBe("This event type does not allow cancellations");
});
});
test("Should throw error when both seatsPerTimeSlot and recurringEvent are set", async ({ page, users }) => {
const user = await users.create({
name: `Test-user-${randomString(4)}`,
eventTypes: [
{
title: "Seats With Recurrence",
slug: "seats-with-recurrence",
length: 30,
seatsPerTimeSlot: 3,
recurringEvent: {
freq: 1,
count: 4,
interval: 1,
},
},
],
});
// Way to book the event
await page.goto(`/${user.username}/seats-with-recurrence`);
await selectFirstAvailableTimeSlotNextMonth(page);
await page.locator('[name="name"]').fill("Test name");
await page.locator('[name="email"]').fill(`${randomString(4)}@example.com`);
page.locator("[data-testid=confirm-book-button]").click();
// Expect an error message to be displayed
const alertError = page.locator("[data-testid=booking-fail]");
await expect(alertError).toBeVisible();
await expect(alertError).toContainText(
"Could not book the meeting. Recurring event doesn't support seats feature. Disable seats feature or make the event non-recurring."
);
});
+33
View File
@@ -387,6 +387,39 @@ test.describe("Event Types tests", () => {
await unCheckDisplayLocation(page);
});
});
test("Should not allow enabling both recurring event and offer seats at the same time", async ({
page,
}) => {
const nonce = randomString(3);
const eventTitle = `Conflict event ${nonce}`;
await createNewEventType(page, { eventTitle });
await page.goto("/event-types");
await page.click(`text=${eventTitle}`);
// Go to Advanced tab and enable offerSeats
await page.click("[data-testid=vertical-tab-event_advanced_tab_title]");
const offerSeatsToggle = page.locator("[data-testid=offer-seats-toggle]");
await offerSeatsToggle.click();
// Try enabling recurring - should be disabled
await page.click("[data-testid=vertical-tab-recurring]");
const recurringEventToggle = page.locator("[data-testid=recurring-event-check]");
await expect(recurringEventToggle).toBeDisabled();
// Go back and disable offerSeats
await page.click("[data-testid=vertical-tab-event_advanced_tab_title]");
await offerSeatsToggle.click(); // turn it off
// Enable recurring now
await page.click("[data-testid=vertical-tab-recurring]");
await recurringEventToggle.click();
await expect(page.locator("[data-testid=recurring-event-collapsible]")).toBeVisible();
// After enabling recurring, offerSeats should now be disabled
await page.click("[data-testid=vertical-tab-event_advanced_tab_title]");
await expect(offerSeatsToggle).toBeDisabled();
});
});
});
@@ -3053,6 +3053,9 @@
"tip_username_plus": "Tip: You can a '+' between usernames: cal.com/anna+brian to make a dynamic group meeting",
"user_has_no_team_yet": "You don't have a team yet",
"no_team_members": "You don't have team members yet",
"recurring_event_doesnt_support_seats": "Recurring event doesn't support seats feature. Make it non-recurring to enable seats.",
"seats_doesnt_support_recurring": "Seats feature doesn't support recurring event. Disable seats feature to make it recurring.",
"recurring_event_seats_error": "Recurring event doesn't support seats feature. Disable seats feature or make the event non-recurring.",
"segment": "Segment",
"rename": "Rename",
"rename_segment": "Rename Segment",
@@ -405,6 +405,14 @@ async function handler(
const dynamicUserList = Array.isArray(reqBody.user) ? reqBody.user : getUsernameList(reqBody.user);
if (!eventType) throw new HttpError({ statusCode: 404, message: "event_type_not_found" });
if (eventType.seatsPerTimeSlot && eventType.recurringEvent) {
throw new HttpError({
statusCode: 400,
message: "recurring_event_seats_error",
});
}
const shouldServeCache = await getShouldServeCache(_shouldServeCache, eventType.team?.id);
const isTeamEventType =
@@ -444,6 +444,7 @@ export const EventAdvancedTab = ({
formMethods.getValues("metadata")?.apps?.stripe?.enabled === true &&
formMethods.getValues("metadata")?.apps?.stripe?.paymentOption === "HOLD";
const isRecurringEvent = !!formMethods.getValues("recurringEvent");
const isRoundRobinEventType =
eventType.schedulingType && eventType.schedulingType === SchedulingType.ROUND_ROBIN;
@@ -853,12 +854,14 @@ export const EventAdvancedTab = ({
{...seatsLocked}
description={t("offer_seats_description")}
checked={value}
disabled={noShowFeeEnabled || multiLocation}
disabled={noShowFeeEnabled || multiLocation || (!seatsEnabled && isRecurringEvent)}
tooltip={
multiLocation
? t("multilocation_doesnt_support_seats")
: noShowFeeEnabled
? t("no_show_fee_doesnt_support_seats")
: isRecurringEvent
? t("recurring_event_doesnt_support_seats")
: undefined
}
onCheckedChange={(e) => {
@@ -12,11 +12,11 @@ import type { FormValues } from "@calcom/features/eventtypes/lib/types";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Frequency } from "@calcom/prisma/zod-utils";
import type { RecurringEvent } from "@calcom/types/Calendar";
import classNames from "@calcom/ui/classNames";
import { Alert } from "@calcom/ui/components/alert";
import { Select } from "@calcom/ui/components/form";
import { SettingsToggle } from "@calcom/ui/components/form";
import { TextField } from "@calcom/ui/components/form";
import { Alert } from "@calcom/ui/components/alert";
import classNames from "@calcom/ui/classNames";
export type RecurringEventControllerProps = {
eventType: EventTypeSetup;
@@ -49,6 +49,7 @@ export default function RecurringEventController({
const [recurringEventState, setRecurringEventState] = useState<RecurringEvent | null>(
formMethods.getValues("recurringEvent")
);
const isSeatsOffered = !!formMethods.getValues("seatsPerTimeSlot");
/* Just yearly-0, monthly-1 and weekly-2 */
const recurringEventFreqOptions = Object.entries(Frequency)
.filter(([key, value]) => isNaN(Number(key)) && Number(value) < 3)
@@ -90,8 +91,10 @@ export default function RecurringEventController({
title={t("recurring_event")}
{...recurringLocked}
description={t("recurring_event_description")}
checked={recurringEventState !== null}
checked={!!recurringEventState}
data-testid="recurring-event-check"
disabled={!recurringEventState && isSeatsOffered}
tooltip={isSeatsOffered ? t("seats_doesnt_support_recurring") : undefined}
onCheckedChange={(e) => {
if (!e) {
formMethods.setValue("recurringEvent", null, { shouldDirty: true });
@@ -298,6 +298,13 @@ export const useEventTypeForm = ({
} = dirtyValues;
if (length && !Number(length)) throw new Error(t("event_setup_length_error"));
const finalSeatsPerTimeSlot = seatsPerTimeSlot ?? values.seatsPerTimeSlot;
const finalRecurringEvent = recurringEvent ?? values.recurringEvent;
if (finalSeatsPerTimeSlot && finalRecurringEvent) {
throw new Error(t("recurring_event_seats_error"));
}
if (bookingLimits) {
const isValid = validateIntervalLimitOrder(bookingLimits);
if (!isValid) throw new Error(t("event_setup_booking_limits_error"));
@@ -82,6 +82,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
autoTranslateDescriptionEnabled,
description: newDescription,
title: newTitle,
seatsPerTimeSlot,
...rest
} = input;
@@ -90,6 +91,8 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
select: {
title: true,
description: true,
seatsPerTimeSlot: true,
recurringEvent: true,
fieldTranslations: {
select: {
field: true,
@@ -160,6 +163,16 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
const finalSeatsPerTimeSlot = seatsPerTimeSlot ?? eventType.seatsPerTimeSlot;
const finalRecurringEvent = recurringEvent ?? eventType.recurringEvent;
if (finalSeatsPerTimeSlot && finalRecurringEvent) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Recurring Events and Offer Seats cannot be active at the same time.",
});
}
const teamId = input.teamId || eventType.team?.id;
const guestsField = bookingFields?.find((field) => field.name === "guests");
@@ -187,6 +200,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
disableGuests: guestsField?.hidden ?? false,
};
data.locations = locations ?? undefined;
if (periodType) {
data.periodType = handlePeriodType(periodType);
}