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:
8 files changed
+108
-4
No files matched your search
@@ -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."
|
||||
);
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user