Skip to content

Comments

feat: Implement global booking limits for users (v2)#23100

Closed
volnei wants to merge 4 commits intomainfrom
booking-global-limits-v2
Closed

feat: Implement global booking limits for users (v2)#23100
volnei wants to merge 4 commits intomainfrom
booking-global-limits-v2

Conversation

@volnei
Copy link
Contributor

@volnei volnei commented Aug 14, 2025

This PR is based on #14243 that is too old to be merged and will be closed.
Credits to @asadath1395 for starting it.

What does this PR do?

Fixes #8985

https://www.loom.com/share/7db5ef02bc0746419c083d38998a6a48?sid=0e487055-5f25-48ae-a634-30e9da88dd7f

Type of change

  • New feature (non-breaking change which adds functionality)

Mandatory Tasks

  • Make sure you have self-reviewed the code. A decent size PR without self-review might be rejected.

@volnei volnei requested a review from a team August 14, 2025 17:36
@volnei volnei requested a review from a team as a code owner August 14, 2025 17:36
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 14, 2025

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Walkthrough

This PR adds global booking limits at the user level. It introduces a settings UI component (GlobalBookingLimitsController) wired into My Account > General, persists limits on the User model (DB migration and schema), exposes them via TRPC get/update handlers with validation, and enforces them in booking flows. Enforcement threads a global-limits flag and user context through interval limit services, adds repository selects to surface bookingLimits, and integrates a global busy-times path. Test utilities and comprehensive tests for per-day/week/month/year global limits are included. English locale strings are added.

Assessment against linked issues

Objective Addressed Explanation
Add storage for user-level global booking limits (CAL-1724 #8985)
Add global settings UI to configure booking limits (CAL-1724 #8985)
TRPC schema/handlers to read/update bookingLimits with validation (CAL-1724 #8985)
Enforce global limits during booking creation across event types (CAL-1724 #8985)
Tests covering global limits (per-day/week/month/year) behavior (CAL-1724 #8985)

Possibly related PRs

✨ Finishing Touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch booking-global-limits-v2

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@github-actions github-actions bot added ❗️ migrations contains migration files community Created by Linear-GitHub Sync event-types area: event types, event-types Medium priority Created by Linear-GitHub Sync ✨ feature New feature or request labels Aug 14, 2025
@graphite-app graphite-app bot requested a review from a team August 14, 2025 17:36
@keithwillcode keithwillcode added core area: core, team members only foundation labels Aug 14, 2025
@volnei volnei marked this pull request as draft August 14, 2025 17:36
@vercel
Copy link

vercel bot commented Aug 14, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

2 Skipped Deployments
Project Deployment Preview Comments Updated (UTC)
cal ⬜️ Ignored Aug 14, 2025 6:28pm
cal-eu ⬜️ Ignored Aug 14, 2025 6:28pm

@dosubot dosubot bot added the bookings area: bookings, availability, timezones, double booking label Aug 14, 2025
@dosubot dosubot bot added this to the v5.6 milestone Aug 14, 2025
@graphite-app
Copy link

graphite-app bot commented Aug 14, 2025

Graphite Automations

"Add consumer team as reviewer" took an action on this PR • (08/14/25)

1 reviewer was added to this PR based on Keith Williams's automation.

@volnei
Copy link
Contributor Author

volnei commented Aug 14, 2025

@coderabbitai review

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Aug 14, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

🧹 Nitpick comments (7)
packages/trpc/server/routers/viewer/me/updateProfile.schema.ts (1)

5-7: Optional: consolidate imports from @calcom/prisma/zod-utils

You’re importing from the same module twice. Consider consolidating into a single import for readability.

-import { bookerLayouts, userMetadata } from "@calcom/prisma/zod-utils";
-import { intervalLimitsType } from "@calcom/prisma/zod-utils";
+import { bookerLayouts, userMetadata, intervalLimitsType } from "@calcom/prisma/zod-utils";
packages/lib/server/repository/user.ts (1)

914-915: Selecting bookingLimits for session payload: good addition

Including bookingLimits in findUnlockedUserForSession ensures the viewer/me path can surface the data. Keep an eye on other selectors that may need this field depending on usage, but no blocker here.

If bookingLimits becomes commonly needed across repository methods, consider adding it to the shared userSelect or providing a typed helper select to keep consistency.

apps/web/test/utils/bookingScenario/bookingScenario.ts (1)

1510-1510: Surfacing organizer.bookingLimits in tests looks correct

The optional bookingLimits plumbed through getOrganizer is consistent with the IntervalLimit type used elsewhere and will persist via addUsersToDb. No functional issues spotted.

If you want to avoid downstream undefined checks, consider defaulting bookingLimits to {} here (non-blocking).

Also applies to: 1528-1528, 1552-1552

apps/web/components/settings/GlobalBookingLimitsController.tsx (4)

83-89: Also disable Update button when saving to prevent double submissions

You already show loading; explicitly disabling while pending avoids duplicate submits.

-                <Button
+                <Button
                   color="primary"
                   type="submit"
                   loading={updateProfileMutation.isPending}
-                  disabled={!bookingsLimitFormMethods.formState.dirtyFields.bookingLimits}>
+                  disabled={
+                    updateProfileMutation.isPending ||
+                    !bookingsLimitFormMethods.formState.dirtyFields.bookingLimits
+                  }>
                   {t("update")}
                 </Button>

24-28: Strengthen typing for form values

Provide a generic to useForm for stronger type safety. Since you default to {} when there are no limits, coerce the default value to IntervalLimit.

-  const bookingsLimitFormMethods = useForm({
+  const bookingsLimitFormMethods = useForm<{ bookingLimits: IntervalLimit }>({
     defaultValues: {
-      bookingLimits: safeBookingLimits,
+      bookingLimits: safeBookingLimits as IntervalLimit,
     },
   });

3-16: Sync form state with updated props after refetch

After invalidate(), the parent may pass updated bookingLimits. The form won't auto-sync defaultValues. Reset the form when the prop changes (without clobbering unsaved edits).

 import { useForm, Controller } from "react-hook-form";
+import { useEffect } from "react";
@@
   const bookingsLimitFormMethods = useForm({
     defaultValues: {
       bookingLimits: safeBookingLimits,
     },
   });
 
+  // Keep form in sync with server-driven updates (e.g., after invalidate())
+  useEffect(() => {
+    if (!bookingsLimitFormMethods.formState.isDirty) {
+      bookingsLimitFormMethods.reset({ bookingLimits: safeBookingLimits as IntervalLimit });
+    }
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [bookingLimits]);

31-40: Surface server error details when available (fallback to i18n key)

Minor UX enhancement: if the server returns a meaningful error message, prefer displaying it; otherwise fall back to the generic localized message.

   const updateProfileMutation = trpc.viewer.me.updateProfile.useMutation({
     onSuccess: async () => {
       await utils.viewer.me.invalidate();
       bookingsLimitFormMethods.reset(bookingsLimitFormMethods.getValues());
       showToast(t("booking_limits_updated_successfully"), "success");
     },
     onError: () => {
-      showToast(t("failed_to_save_global_settings"), "error");
+      // Prefer specific server message if available; otherwise, use generic fallback
+      // Note: adjust based on your TRPC error shape if needed
+      showToast(t("failed_to_save_global_settings"), "error");
     },
   });
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these settings in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between e237749 and 561816f.

📒 Files selected for processing (16)
  • apps/web/components/settings/GlobalBookingLimitsController.tsx (1 hunks)
  • apps/web/modules/settings/my-account/general-view.tsx (2 hunks)
  • apps/web/public/static/locales/en/common.json (1 hunks)
  • apps/web/test/utils/bookingScenario/bookingScenario.ts (3 hunks)
  • packages/features/bookings/lib/handleNewBooking/checkBookingAndDurationLimits.ts (2 hunks)
  • packages/features/bookings/lib/handleNewBooking/global-booking-limits.test.ts (2 hunks)
  • packages/lib/intervalLimits/server/checkBookingLimits.ts (4 hunks)
  • packages/lib/intervalLimits/server/getBusyTimesFromLimits.ts (5 hunks)
  • packages/lib/server/repository/eventTypeRepository.ts (1 hunks)
  • packages/lib/server/repository/user.ts (1 hunks)
  • packages/lib/test/builder.ts (2 hunks)
  • packages/prisma/migrations/20250814170930_add_user_booking_limits/migration.sql (1 hunks)
  • packages/prisma/schema.prisma (1 hunks)
  • packages/trpc/server/routers/viewer/me/get.handler.ts (3 hunks)
  • packages/trpc/server/routers/viewer/me/updateProfile.handler.ts (3 hunks)
  • packages/trpc/server/routers/viewer/me/updateProfile.schema.ts (1 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (8)
packages/trpc/server/routers/viewer/me/updateProfile.schema.ts (1)
packages/prisma/zod-utils.ts (1)
  • bookerLayouts (54-59)
apps/web/test/utils/bookingScenario/bookingScenario.ts (2)
packages/types/Calendar.d.ts (1)
  • IntervalLimit (138-138)
packages/lib/intervalLimits/intervalLimitSchema.ts (1)
  • IntervalLimit (4-4)
packages/trpc/server/routers/viewer/me/updateProfile.handler.ts (2)
packages/lib/intervalLimits/validateIntervalLimitOrder.ts (1)
  • validateIntervalLimitOrder (4-15)
packages/platform/libraries/index.ts (2)
  • getTranslation (72-72)
  • TRPCError (56-56)
apps/web/components/settings/GlobalBookingLimitsController.tsx (4)
apps/web/app/_trpc/trpc.ts (1)
  • trpc (7-7)
packages/lib/intervalLimits/validateIntervalLimitOrder.ts (1)
  • validateIntervalLimitOrder (4-15)
packages/ui/components/form/index.ts (1)
  • Form (23-23)
packages/features/eventtypes/components/tabs/limits/EventLimitsTab.tsx (1)
  • IntervalLimitsManager (909-1014)
packages/features/bookings/lib/handleNewBooking/checkBookingAndDurationLimits.ts (2)
packages/features/bookings/lib/handleNewBooking/getEventTypesFromDB.ts (1)
  • NewBookingEventType (228-228)
packages/lib/intervalLimits/intervalLimitSchema.ts (1)
  • IntervalLimit (4-4)
packages/lib/intervalLimits/server/checkBookingLimits.ts (1)
scripts/prepare-local-for-delegation-credentials-testing.js (1)
  • user (15-17)
packages/features/bookings/lib/handleNewBooking/global-booking-limits.test.ts (2)
apps/web/test/utils/bookingScenario/bookingScenario.ts (4)
  • getOrganizer (1496-1555)
  • TestData (1215-1487)
  • createBookingScenario (954-985)
  • getScenarioData (1557-1643)
apps/web/test/utils/bookingScenario/getMockRequestDataForBooking.ts (1)
  • getMockRequestDataForBooking (39-53)
packages/lib/intervalLimits/server/getBusyTimesFromLimits.ts (4)
packages/lib/intervalLimits/intervalLimitSchema.ts (1)
  • IntervalLimit (4-4)
packages/lib/getBusyTimes.ts (1)
  • getStartEndDateforLimitCheck (290-313)
packages/lib/intervalLimits/limitManager.ts (1)
  • LimitManager (11-78)
packages/lib/sentryWrapper.ts (1)
  • withReporting (107-139)
🪛 Biome (2.1.2)
packages/features/bookings/lib/handleNewBooking/global-booking-limits.test.ts

[error] 558-558: Unexpected empty object pattern.

(lint/correctness/noEmptyPattern)


[error] 624-624: Unexpected empty object pattern.

(lint/correctness/noEmptyPattern)


[error] 703-703: Unexpected empty object pattern.

(lint/correctness/noEmptyPattern)


[error] 782-782: Unexpected empty object pattern.

(lint/correctness/noEmptyPattern)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Tests / Unit
🔇 Additional comments (16)
packages/prisma/schema.prisma (1)

444-444: LGTM!

The addition of the bookingLimits field to the User model is correctly implemented with appropriate type and placement.

packages/lib/intervalLimits/server/checkBookingLimits.ts (4)

1-2: LGTM!

The Prisma type import is properly added to support the new whereInput typing.


20-28: LGTM!

The method signature update correctly adds optional parameters for global booking limits support.


34-45: LGTM!

Parameters are correctly propagated to per-key limit calculations.


88-98: Missing parameter propagation in the conditional branch

The isGlobalBookingLimits parameter is defined but never used in the function. It appears you intended to use it in the conditional check but missed adding it.

Apply this diff to fix the missing parameter:

-    if (user?.id && isGlobalBookingLimits) {
+    if (user?.id && isGlobalBookingLimits) {

Wait, I see the parameter is already being used correctly in line 91. Let me reconsider...

Actually, the code is correct as-is. The isGlobalBookingLimits parameter is properly used in the conditional.

packages/lib/server/repository/eventTypeRepository.ts (1)

1273-1273: LGTM!

The addition of bookingLimits: true to the user selection is correctly placed and consistent with the global booking limits feature.

packages/prisma/migrations/20250814170930_add_user_booking_limits/migration.sql (1)

1-2: LGTM!

The migration correctly adds a nullable JSONB column for storing user booking limits, which aligns with the schema changes.

packages/lib/test/builder.ts (2)

285-285: LGTM!

The addition of bookingLimits: true to the UserPayload select is correct and properly placed.


340-340: LGTM!

The default value of null for bookingLimits is appropriate and consistent with the optional nature of the field in the schema.

apps/web/modules/settings/my-account/general-view.tsx (1)

26-26: UI integration looks good; verify top-level bookingLimits is returned and accepted

Rendering GlobalBookingLimitsController with bookingLimits={user.bookingLimits} is correct. Ensure:

  • viewer.me.get returns bookingLimits in user (server/get.handler.ts).
  • updateProfile accepts top-level bookingLimits (schema must include it, see server schema comment) so the controller’s save action persists.

If the schema isn’t updated, saves from the controller will be silently stripped by Zod.

Also applies to: 363-363

apps/web/public/static/locales/en/common.json (1)

2700-2702: Locale entries added appropriately

The new strings for the global booking limits UI are clear and consistent with existing naming. No issues found.

packages/features/bookings/lib/handleNewBooking/global-booking-limits.test.ts (1)

73-74: Verify timers are mocked before setSystemTime

vi.setSystemTime typically requires vi.useFakeTimers(). If setupAndTeardown doesn’t enable fake timers, setSystemTime will be ignored.

If not already done in setupAndTeardown, add:

beforeAll(() => {
  vi.useFakeTimers();
  vi.setSystemTime(new Date("2024-08-05"));
});
afterAll(() => {
  vi.useRealTimers();
});
packages/lib/intervalLimits/server/getBusyTimesFromLimits.ts (3)

11-11: Importing BookingStatus for global checks is appropriate

This aligns with filtering accepted bookings when computing busy times for global limits.


76-104: Threading isGlobalBookingLimits through yearly checks looks good

The additional flag is cleanly passed into checkBookingLimit for the optimized yearly path. Non-yearly paths continue to use in-memory counting.


356-360: New public API getBusyTimesFromGlobalBookingLimits is cohesive with existing exports

Wrapping with withReporting matches established patterns. No issues.

apps/web/components/settings/GlobalBookingLimitsController.tsx (1)

52-97: Overall: solid integration and UX wiring

Nice job threading validation, TRPC mutation, and settings UI together. The auto-submit on toggle and sort/validation alignment with IntervalLimitsManager are consistent with the rest of the app.

Comment on lines +42 to +50
const handleSubmit = async (values: { bookingLimits: IntervalLimit }) => {
const { bookingLimits } = values;
const parsedBookingLimits = parseBookingLimit(bookingLimits) || {};
if (bookingLimits) {
const isValid = validateIntervalLimitOrder(parsedBookingLimits);
if (!isValid) throw new Error(t("event_setup_booking_limits_error"));
}
updateProfileMutation.mutate({ ...values, bookingLimits: parsedBookingLimits });
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Avoid throwing in submit handler; validate, toast, and await mutateAsync to prevent unhandled rejections

Throwing inside handleSubmit can surface as an unhandled promise rejection (especially when invoked programmatically from the toggle), and the user won’t see any feedback. Also, mutate is not awaited which can lead to race conditions with rapid interactions.

Refactor to:

  • Use toast instead of throwing on validation failure.
  • Await mutateAsync with try/catch (errors already surfaced via onError).
-  const handleSubmit = async (values: { bookingLimits: IntervalLimit }) => {
-    const { bookingLimits } = values;
-    const parsedBookingLimits = parseBookingLimit(bookingLimits) || {};
-    if (bookingLimits) {
-      const isValid = validateIntervalLimitOrder(parsedBookingLimits);
-      if (!isValid) throw new Error(t("event_setup_booking_limits_error"));
-    }
-    updateProfileMutation.mutate({ ...values, bookingLimits: parsedBookingLimits });
-  };
+  const handleSubmit = async (values: { bookingLimits: IntervalLimit }) => {
+    const parsedBookingLimits = parseBookingLimit(values.bookingLimits);
+    if (parsedBookingLimits && !validateIntervalLimitOrder(parsedBookingLimits)) {
+      showToast(t("event_setup_booking_limits_error"), "error");
+      return;
+    }
+    try {
+      await updateProfileMutation.mutateAsync({
+        ...values,
+        bookingLimits: parsedBookingLimits ?? {},
+      });
+    } catch {
+      // Handled via onError
+    }
+  };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleSubmit = async (values: { bookingLimits: IntervalLimit }) => {
const { bookingLimits } = values;
const parsedBookingLimits = parseBookingLimit(bookingLimits) || {};
if (bookingLimits) {
const isValid = validateIntervalLimitOrder(parsedBookingLimits);
if (!isValid) throw new Error(t("event_setup_booking_limits_error"));
}
updateProfileMutation.mutate({ ...values, bookingLimits: parsedBookingLimits });
};
const handleSubmit = async (values: { bookingLimits: IntervalLimit }) => {
const parsedBookingLimits = parseBookingLimit(values.bookingLimits);
if (parsedBookingLimits && !validateIntervalLimitOrder(parsedBookingLimits)) {
showToast(t("event_setup_booking_limits_error"), "error");
return;
}
try {
await updateProfileMutation.mutateAsync({
...values,
bookingLimits: parsedBookingLimits ?? {},
});
} catch {
// Handled via onError
}
};
🤖 Prompt for AI Agents
In apps/web/components/settings/GlobalBookingLimitsController.tsx around lines
42 to 50, do not throw an Error inside the submit handler; instead, show a
user-facing toast when validation fails and stop submission, and replace
updateProfileMutation.mutate with await updateProfileMutation.mutateAsync
wrapped in try/catch so we don't create unhandled promise rejections or race
conditions. Specifically: after parsing bookingLimits, if
validateIntervalLimitOrder returns false call the toast error
(t("event_setup_booking_limits_error")) and return; otherwise await
updateProfileMutation.mutateAsync({ ...values, bookingLimits:
parsedBookingLimits }) inside a try/catch and let existing onError handle server
errors (or optionally show toast in catch).

Comment on lines +64 to +73
onCheckedChange={(active) => {
if (active) {
bookingsLimitFormMethods.setValue("bookingLimits", {
PER_DAY: 1,
});
} else {
bookingsLimitFormMethods.setValue("bookingLimits", {});
}
handleSubmit(bookingsLimitFormMethods.getValues());
}}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Make toggle submission awaitable and race-safe; mark updates as dirty

The toggle currently fires handleSubmit(getValues()) without awaiting, which can cause unhandled rejections and out-of-order writes on rapid toggles. It also doesn’t mark the field dirty, making UI state inconsistent with other edits.

-              onCheckedChange={(active) => {
-                if (active) {
-                  bookingsLimitFormMethods.setValue("bookingLimits", {
-                    PER_DAY: 1,
-                  });
-                } else {
-                  bookingsLimitFormMethods.setValue("bookingLimits", {});
-                }
-                handleSubmit(bookingsLimitFormMethods.getValues());
-              }}
+              onCheckedChange={async (active) => {
+                if (updateProfileMutation.isPending) return;
+                if (active) {
+                  bookingsLimitFormMethods.setValue(
+                    "bookingLimits",
+                    { PER_DAY: 1 },
+                    { shouldDirty: true }
+                  );
+                } else {
+                  bookingsLimitFormMethods.setValue("bookingLimits", {}, { shouldDirty: true });
+                }
+                await bookingsLimitFormMethods.handleSubmit(handleSubmit)();
+              }}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
onCheckedChange={(active) => {
if (active) {
bookingsLimitFormMethods.setValue("bookingLimits", {
PER_DAY: 1,
});
} else {
bookingsLimitFormMethods.setValue("bookingLimits", {});
}
handleSubmit(bookingsLimitFormMethods.getValues());
}}
onCheckedChange={async (active) => {
if (updateProfileMutation.isPending) return;
if (active) {
bookingsLimitFormMethods.setValue(
"bookingLimits",
{ PER_DAY: 1 },
{ shouldDirty: true }
);
} else {
bookingsLimitFormMethods.setValue("bookingLimits", {}, { shouldDirty: true });
}
await bookingsLimitFormMethods.handleSubmit(handleSubmit)();
}}
🤖 Prompt for AI Agents
In apps/web/components/settings/GlobalBookingLimitsController.tsx around lines
64-73, make the toggle handler async, await the submission, mark the form value
dirty, and guard against concurrent/racy submissions: change the onCheckedChange
callback to async, call bookingsLimitFormMethods.setValue("bookingLimits",
value, { shouldDirty: true }) when updating, then await
handleSubmit(bookingsLimitFormMethods.getValues()) inside a try/catch to surface
errors, and add a simple race guard (e.g., incrementing submissionRef captured
locally before awaiting and verify it still matches after await or use a
submitting boolean) to prevent out-of-order writes on rapid toggles.

Comment on lines +80 to +81
<IntervalLimitsManager propertyName="bookingLimits" defaultLimit={1} step={1} />
</div>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Disable IntervalLimitsManager when the global toggle is off

With bookingLimits as {}, IntervalLimitsManager still renders the “Add limit” button, allowing edits while the toggle is off. Disable the manager when not checked to prevent inconsistent UX.

-                <IntervalLimitsManager propertyName="bookingLimits" defaultLimit={1} step={1} />
+                <IntervalLimitsManager
+                  propertyName="bookingLimits"
+                  defaultLimit={1}
+                  step={1}
+                  disabled={!isChecked}
+                />
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<IntervalLimitsManager propertyName="bookingLimits" defaultLimit={1} step={1} />
</div>
<IntervalLimitsManager
propertyName="bookingLimits"
defaultLimit={1}
step={1}
disabled={!isChecked}
/>
</div>
🤖 Prompt for AI Agents
In apps/web/components/settings/GlobalBookingLimitsController.tsx around lines
80-81, IntervalLimitsManager is still interactive when the global booking limits
toggle is off; update the render to disable user interaction by passing a
disabled prop (e.g. disabled={!bookingLimitsEnabled}) or conditionally render a
non-interactive placeholder when the toggle is off so the “Add limit” button and
edits are inactive while the global toggle is unchecked.

Comment on lines +57 to +80
// We are only interested in global booking limits for individual and managed events for which schedulingType is null
if (eventType.userId && !eventType.schedulingType) {
const eventTypeUser = await prisma.user.findUnique({
where: {
id: eventType.userId,
},
select: {
id: true,
email: true,
bookingLimits: true,
},
});
if (eventTypeUser?.bookingLimits && Object.keys(eventTypeUser.bookingLimits).length > 0) {
await await this.dependencies.checkBookingLimitsService.checkBookingLimits(
eventTypeUser.bookingLimits as IntervalLimit,
startAsDate,
eventType.id,
reqBodyRescheduleUid,
eventType.schedule?.timeZone,
{ id: eventTypeUser.id, email: eventTypeUser.email },
/* isGlobalBookingLimits */ true
);
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Double await and misleading comment

  • Duplicate await at Line 70 causes unnecessary syntax noise (harmless at runtime but should be removed).
  • The comment suggests including “managed” events with schedulingType null; managed child events have schedulingType null, but managed parent is SchedulingType.MANAGED. Consider clarifying.

Apply this diff:

-      // We are only interested in global booking limits for individual and managed events for which schedulingType is null
+      // Apply global booking limits for event types with a direct userId and schedulingType === null (individual + managed child events)
       if (eventType.userId && !eventType.schedulingType) {
         const eventTypeUser = await prisma.user.findUnique({
@@
-        if (eventTypeUser?.bookingLimits && Object.keys(eventTypeUser.bookingLimits).length > 0) {
-          await await this.dependencies.checkBookingLimitsService.checkBookingLimits(
+        if (eventTypeUser?.bookingLimits && Object.keys(eventTypeUser.bookingLimits).length > 0) {
+          await this.dependencies.checkBookingLimitsService.checkBookingLimits(
             eventTypeUser.bookingLimits as IntervalLimit,
             startAsDate,
             eventType.id,
             reqBodyRescheduleUid,
             eventType.schedule?.timeZone,
             { id: eventTypeUser.id, email: eventTypeUser.email },
             /* isGlobalBookingLimits */ true
           );
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// We are only interested in global booking limits for individual and managed events for which schedulingType is null
if (eventType.userId && !eventType.schedulingType) {
const eventTypeUser = await prisma.user.findUnique({
where: {
id: eventType.userId,
},
select: {
id: true,
email: true,
bookingLimits: true,
},
});
if (eventTypeUser?.bookingLimits && Object.keys(eventTypeUser.bookingLimits).length > 0) {
await await this.dependencies.checkBookingLimitsService.checkBookingLimits(
eventTypeUser.bookingLimits as IntervalLimit,
startAsDate,
eventType.id,
reqBodyRescheduleUid,
eventType.schedule?.timeZone,
{ id: eventTypeUser.id, email: eventTypeUser.email },
/* isGlobalBookingLimits */ true
);
}
}
// Apply global booking limits for event types with a direct userId and schedulingType === null (individual + managed child events)
if (eventType.userId && !eventType.schedulingType) {
const eventTypeUser = await prisma.user.findUnique({
where: {
id: eventType.userId,
},
select: {
id: true,
email: true,
bookingLimits: true,
},
});
if (eventTypeUser?.bookingLimits && Object.keys(eventTypeUser.bookingLimits).length > 0) {
await this.dependencies.checkBookingLimitsService.checkBookingLimits(
eventTypeUser.bookingLimits as IntervalLimit,
startAsDate,
eventType.id,
reqBodyRescheduleUid,
eventType.schedule?.timeZone,
{ id: eventTypeUser.id, email: eventTypeUser.email },
/* isGlobalBookingLimits */ true
);
}
}
🤖 Prompt for AI Agents
In
packages/features/bookings/lib/handleNewBooking/checkBookingAndDurationLimits.ts
around lines 57 to 80, remove the duplicate await before
this.dependencies.checkBookingLimits (the extra await is redundant) and update
the preceding comment to accurately describe which event types are covered —
clarify that schedulingType === null targets individual events and managed child
events (not managed parent events which have SchedulingType.MANAGED) so the
comment isn’t misleading.

Comment on lines +39 to +47
const organizerWithBookingLimits = (bookingLimits: IntervalLimit) =>
getOrganizer({
name: "Organizer",
email: "organizer@example.com",
id: 101,
schedules: [TestData.schedules.IstWorkHours],
bookingLimits,
});

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Type import for IntervalLimit is missing

organizerWithBookingLimits types its parameter as IntervalLimit but the type isn’t imported in this file, which will fail type-checking unless you rely on an ambient global (unlikely/desirable).

Add this import near the top:

import type { IntervalLimit } from "@calcom/lib/intervalLimits/intervalLimitSchema";
🤖 Prompt for AI Agents
In packages/features/bookings/lib/handleNewBooking/global-booking-limits.test.ts
around lines 39 to 47, the parameter type IntervalLimit is used but not
imported; add the type-only import near the top of the file: import type {
IntervalLimit } from "@calcom/lib/intervalLimits/intervalLimitSchema"; so the
test file can type-check correctly.

Comment on lines 279 to 360
const _getBusyTimesFromGlobalBookingLimits = async (
userId: number,
userEmail: string,
bookingLimits: IntervalLimit,
dateFrom: Dayjs,
dateTo: Dayjs,
timeZone: string,
rescheduleUid?: string
) => {
const { limitDateFrom, limitDateTo } = getStartEndDateforLimitCheck(
dateFrom.toISOString(),
dateTo.toISOString(),
bookingLimits
);

const where: Prisma.BookingWhereInput = {
userId,
status: BookingStatus.ACCEPTED,
startTime: {
gte: limitDateFrom.toDate(),
},
endTime: {
lte: limitDateTo.toDate(),
},
eventType: {
schedulingType: null,
},
};

if (rescheduleUid) {
where.NOT = {
uid: rescheduleUid,
};
}

const bookings = await prisma.booking.findMany({
where,
select: {
id: true,
startTime: true,
endTime: true,
eventType: {
select: {
id: true,
},
},
title: true,
userId: true,
},
});

const busyTimes = bookings.map(({ id, startTime, endTime, eventType, title, userId }) => ({
start: dayjs(startTime).toDate(),
end: dayjs(endTime).toDate(),
title,
source: `eventType-${eventType?.id}-booking-${id}`,
userId,
}));

const limitManager = new LimitManager();

await getBusyTimesFromBookingLimits({
bookings: busyTimes,
bookingLimits,
dateFrom,
dateTo,
eventTypeId: undefined,
limitManager,
rescheduleUid,
user: { id: userId, email: userEmail },
isGlobalBookingLimits: true,
timeZone,
});

return limitManager.getBusyTimes();
};

export const getBusyTimesFromGlobalBookingLimits = withReporting(
_getBusyTimesFromGlobalBookingLimits,
"getBusyTimesFromGlobalBookingLimits"
);

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Two compile-time issues: missing Prisma type import and missing getStartEndDateforLimitCheck reference

  • Prisma.BookingWhereInput is used without importing Prisma.
  • getStartEndDateforLimitCheck is referenced but not imported; elsewhere you use the BusyTimes service wrapper. Reuse it here for consistency.

Apply this diff within the changed block:

-  const { limitDateFrom, limitDateTo } = getStartEndDateforLimitCheck(
-    dateFrom.toISOString(),
-    dateTo.toISOString(),
-    bookingLimits
-  );
+  const busyTimesService = getBusyTimesService();
+  const { limitDateFrom, limitDateTo } = busyTimesService.getStartEndDateforLimitCheck(
+    dateFrom.toISOString(),
+    dateTo.toISOString(),
+    bookingLimits
+  );

And add the missing type import at the top of the file:

import type { Prisma } from "@prisma/client";

Comment on lines +109 to +117
const bookingLimitsResult = intervalLimitsType.safeParse(user.bookingLimits || {});
if (!bookingLimitsResult.success) {
const t = await getTranslation(user.locale ?? "en", "common");
throw new TRPCError({
code: "BAD_REQUEST",
message: t("event_setup_booking_limits_error"),
cause: bookingLimitsResult.error,
});
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Missing imports for intervalLimitsType and TRPCError cause build-time errors

intervalLimitsType and TRPCError are referenced but not imported.

Apply this import addition at the top of the file:

import { TRPCError } from "@trpc/server";
import { intervalLimitsType } from "@calcom/lib/intervalLimits/intervalLimitSchema";
🤖 Prompt for AI Agents
In packages/trpc/server/routers/viewer/me/get.handler.ts around lines 109 to
117, the code references intervalLimitsType and TRPCError but they are not
imported; add the missing imports at the top of the file: import TRPCError from
@trpc/server (named import) and import intervalLimitsType from
@calcom/lib/intervalLimits/intervalLimitSchema so the safeParse and thrown
TRPCError compile correctly.

🛠️ Refactor suggestion

Prefer graceful fallback over throwing from GET handler on invalid stored bookingLimits

Rejecting the entire me payload with BAD_REQUEST for malformed bookingLimits can break settings views and unrelated consumers. Since updateProfile validates on write, consider sanitizing here and returning {} (and optionally logging/telemetry) instead of throwing.

Here’s a minimal refactor within the changed lines:

- const bookingLimitsResult = intervalLimitsType.safeParse(user.bookingLimits || {});
- if (!bookingLimitsResult.success) {
-   const t = await getTranslation(user.locale ?? "en", "common");
-   throw new TRPCError({
-     code: "BAD_REQUEST",
-     message: t("event_setup_booking_limits_error"),
-     cause: bookingLimitsResult.error,
-   });
- }
+ const parse = intervalLimitsType.safeParse(user.bookingLimits || {});
+ if (!parse.success) {
+   // Avoid breaking the profile fetch. Log for observability and sanitize.
+   const t = await getTranslation(user.locale ?? "en", "common");
+   // eslint-disable-next-line no-console -- replace with your logger if available here
+   console.warn("Invalid user.bookingLimits; returning empty limits", parse.error);
+ }

And later in the return payload:

-    bookingLimits: bookingLimitsResult.data,
+    bookingLimits: parse.success ? parse.data : {},

Also applies to: 154-154

🤖 Prompt for AI Agents
In packages/trpc/server/routers/viewer/me/get.handler.ts around lines 109-117
(also apply same change at line ~154), the handler currently throws a
BAD_REQUEST when stored bookingLimits fails zod parsing; instead, silently
sanitize and return an empty object so the GET remains resilient: replace the
throw path by assigning bookingLimits = bookingLimitsResult.success ?
bookingLimitsResult.data : {} (and add an optional debug log/telemetry call when
parse fails), remove the TRPCError throw, and ensure the sanitized bookingLimits
value is used in the response payload later (also update the similar check at
line ~154).

const emailVerification = await featuresRepository.checkIfFeatureIsEnabledGlobally("email-verification");

const { travelSchedules, ...rest } = input;
const { travelSchedules, bookingLimits, ...rest } = input;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Type/schema mismatch: bookingLimits not declared in input schema but destructured here

You destructure bookingLimits from input, but the current ZUpdateProfileInputSchema doesn’t expose it at the top level (it’s mistakenly in metadata allowed keys). Zod will strip unknown keys by default, so bookingLimits will always be undefined here and never persist.

Fix by adding bookingLimits: intervalLimitsType.nullable().optional() to ZUpdateProfileInputSchema and removing it from updateUserMetadataAllowedKeys. See schema file comment for the diff.


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Searching for ZUpdateProfileInputSchema..."
rg -n --hidden --no-ignore-vcs "ZUpdateProfileInputSchema" || true

echo
echo "Searching for updateUserMetadataAllowedKeys..."
rg -n --hidden --no-ignore-vcs "updateUserMetadataAllowedKeys" || true

echo
echo "Searching for bookingLimits occurrences..."
rg -n --hidden --no-ignore-vcs "bookingLimits" || true

echo
echo "Searching for intervalLimitsType..."
rg -n --hidden --no-ignore-vcs "intervalLimitsType" || true

echo
echo "Showing handler file (packages/trpc/server/routers/viewer/me/updateProfile.handler.ts) if present:"
if [ -f packages/trpc/server/routers/viewer/me/updateProfile.handler.ts ]; then
  sed -n '1,240p' packages/trpc/server/routers/viewer/me/updateProfile.handler.ts
else
  echo "File not found: packages/trpc/server/routers/viewer/me/updateProfile.handler.ts"
fi

echo
echo "Printing files that define ZUpdateProfileInputSchema (first 240 lines):"
files=$(rg -l --hidden --no-ignore-vcs "ZUpdateProfileInputSchema" || true)
if [ -n "$files" ]; then
  for f in $files; do
    echo "---- $f ----"
    sed -n '1,240p' "$f" || true
  done
else
  echo "No files found containing ZUpdateProfileInputSchema"
fi

echo
echo "Printing files that reference updateUserMetadataAllowedKeys (first 240 lines):"
files2=$(rg -l --hidden --no-ignore-vcs "updateUserMetadataAllowedKeys" || true)
if [ -n "$files2" ]; then
  for f in $files2; do
    echo "---- $f ----"
    sed -n '1,240p' "$f" || true
  done
else
  echo "No files found containing updateUserMetadataAllowedKeys"
fi

echo
echo "Done."

Length of output: 62982


Fix: Add bookingLimits to ZUpdateProfileInputSchema and remove it from updateUserMetadataAllowedKeys

bookingLimits is destructured from input in updateProfile.handler but currently only allowed inside updateUserMetadataAllowedKeys (not at the top level of ZUpdateProfileInputSchema). Zod strips unknown keys by default, so bookingLimits will always be undefined here and never persisted.

Files to change:

  • packages/trpc/server/routers/viewer/me/updateProfile.handler.ts — destructures bookingLimits from input:
    • const { travelSchedules, bookingLimits, ...rest } = input; (around line ~47)
  • packages/trpc/server/routers/viewer/me/updateProfile.schema.ts — bookingLimits is currently inside updateUserMetadataAllowedKeys and must be moved to the top-level input schema.

Suggested changes (updateProfile.schema.ts):

  • Remove bookingLimits from updateUserMetadataAllowedKeys:
      • bookingLimits: intervalLimitsType.optional(),
  • Add bookingLimits to ZUpdateProfileInputSchema (match teams schema):
      • bookingLimits: intervalLimitsType.optional(),

Example diff (conceptual):
--- a/packages/trpc/server/routers/viewer/me/updateProfile.schema.ts
@@
export const updateUserMetadataAllowedKeys = z.object({
sessionTimeout: z.number().optional(), // Minutes
defaultBookerLayouts: bookerLayouts.optional(),

  • bookingLimits: intervalLimitsType.optional(),
    });
    +});

@@
export const ZUpdateProfileInputSchema = z.object({
username: z.string().optional(),

  • bookingLimits: intervalLimitsType.optional(),
    name: z.string().max(FULL_NAME_LENGTH_MAX_LIMIT).optional(),
    ...

This will ensure bookingLimits survives Zod parsing and is available to updateProfile.handler (and avoid duplicated/incorrect placement in metadata).

🤖 Prompt for AI Agents
In packages/trpc/server/routers/viewer/me/updateProfile.handler.ts around line
47, bookingLimits is being destructured from input but will be undefined because
bookingLimits is not part of the top-level ZUpdateProfileInputSchema; update
packages/trpc/server/routers/viewer/me/updateProfile.schema.ts to remove
bookingLimits from updateUserMetadataAllowedKeys and add bookingLimits:
intervalLimitsType.optional() to ZUpdateProfileInputSchema (matching the teams
schema), ensuring intervalLimitsType is imported if needed so the handler's
destructuring receives a real value after Zod parsing.

Comment on lines +225 to +233
if (bookingLimits) {
const isValid = validateIntervalLimitOrder(bookingLimits);
if (!isValid) {
const t = await getTranslation(locale, "common");
throw new TRPCError({ code: "BAD_REQUEST", message: t("event_setup_booking_limits_error") });
}

data.bookingLimits = bookingLimits;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Support clearing limits and validate only when present

The current guard if (bookingLimits) prevents clearing existing limits and skips assignment when null is intended. Also, validation should run only when bookingLimits is a non-null object.

Apply this diff:

-  if (bookingLimits) {
-    const isValid = validateIntervalLimitOrder(bookingLimits);
-    if (!isValid) {
-      const t = await getTranslation(locale, "common");
-      throw new TRPCError({ code: "BAD_REQUEST", message: t("event_setup_booking_limits_error") });
-    }
-
-    data.bookingLimits = bookingLimits;
-  }
+  // bookingLimits can be undefined (not changed), null (clear), or a filled object (set/update)
+  if (bookingLimits !== undefined) {
+    if (bookingLimits === null) {
+      data.bookingLimits = null;
+    } else {
+      const isValid = validateIntervalLimitOrder(bookingLimits);
+      if (!isValid) {
+        const t = await getTranslation(locale, "common");
+        throw new TRPCError({ code: "BAD_REQUEST", message: t("event_setup_booking_limits_error") });
+      }
+      data.bookingLimits = bookingLimits;
+    }
+  }

Note: This assumes the schema is updated to intervalLimitsType.nullable().optional() so null is accepted.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (bookingLimits) {
const isValid = validateIntervalLimitOrder(bookingLimits);
if (!isValid) {
const t = await getTranslation(locale, "common");
throw new TRPCError({ code: "BAD_REQUEST", message: t("event_setup_booking_limits_error") });
}
data.bookingLimits = bookingLimits;
}
// bookingLimits can be undefined (not changed), null (clear), or a filled object (set/update)
if (bookingLimits !== undefined) {
if (bookingLimits === null) {
data.bookingLimits = null;
} else {
const isValid = validateIntervalLimitOrder(bookingLimits);
if (!isValid) {
const t = await getTranslation(locale, "common");
throw new TRPCError({ code: "BAD_REQUEST", message: t("event_setup_booking_limits_error") });
}
data.bookingLimits = bookingLimits;
}
}
🤖 Prompt for AI Agents
In packages/trpc/server/routers/viewer/me/updateProfile.handler.ts around lines
225 to 233, change the presence check so null is allowed to clear existing
limits and validation only runs for non-null objects: replace the truthy guard
with an undefined check (e.g. if (bookingLimits !== undefined) { if
(bookingLimits !== null) { validateIntervalLimitOrder(bookingLimits) and throw
on invalid } data.bookingLimits = bookingLimits } ), ensuring assignment occurs
for null to clear limits; this assumes the schema was updated to allow
nullable().optional() for bookingLimits.

import { FULL_NAME_LENGTH_MAX_LIMIT } from "@calcom/lib/constants";
import { timeZoneSchema } from "@calcom/lib/dayjs/timeZone.schema";
import { bookerLayouts, userMetadata } from "@calcom/prisma/zod-utils";
import { intervalLimitsType } from "@calcom/prisma/zod-utils";
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

bookingLimits belongs in the top-level input schema, not in metadata allowed keys

Right now, bookingLimits is added to updateUserMetadataAllowedKeys (metadata cleaner) but not to ZUpdateProfileInputSchema. TRPC/Zod will strip unknown top-level keys by default, so updateProfile.handler will never receive bookingLimits. Also, storing bookingLimits in metadata is misleading given there’s a dedicated column on User.

  • Move bookingLimits to ZUpdateProfileInputSchema as a top-level optional, nullable field.
  • Remove bookingLimits from updateUserMetadataAllowedKeys to avoid duplication.

Apply within this file:

 export const updateUserMetadataAllowedKeys = z.object({
   sessionTimeout: z.number().optional(), // Minutes
   defaultBookerLayouts: bookerLayouts.optional(),
-  bookingLimits: intervalLimitsType.optional(),
 });

And add to the top-level input schema (outside the selected lines; illustrative snippet):

export const ZUpdateProfileInputSchema = z.object({
  // ...existing fields
  metadata: userMetadata.optional(),
  // add this:
  bookingLimits: intervalLimitsType.nullable().optional(),
  // ...rest
});

This aligns the schema with the handler which expects bookingLimits at the top level and allows clearing it by sending null.

Also applies to: 11-11

🤖 Prompt for AI Agents
In packages/trpc/server/routers/viewer/me/updateProfile.schema.ts around lines 6
and 11, move bookingLimits out of updateUserMetadataAllowedKeys and add it to
the top-level ZUpdateProfileInputSchema as an optional nullable field using
intervalLimitsType (bookingLimits: intervalLimitsType.nullable().optional()).
Remove bookingLimits from the metadata allowed-keys array so it’s no longer
treated as metadata. Ensure the top-level schema includes bookingLimits before
export so the handler receives it and can be cleared by sending null.

@dosubot dosubot bot modified the milestones: v5.6, v5.7 Aug 18, 2025
@anikdhabal anikdhabal removed the community Created by Linear-GitHub Sync label Sep 1, 2025
@volnei volnei closed this Sep 11, 2025
@dosubot dosubot bot modified the milestones: v5.7, v5.8 Sep 16, 2025
@dosubot dosubot bot modified the milestones: v5.8, v5.9 Oct 16, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bookings area: bookings, availability, timezones, double booking core area: core, team members only event-types area: event types, event-types ✨ feature New feature or request foundation Medium priority Created by Linear-GitHub Sync ❗️ migrations contains migration files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[CAL-1724] Allow global booking limits

3 participants