-
Notifications
You must be signed in to change notification settings - Fork 11.5k
fix: booking limits with seats not working (CAL-4553) #23298
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
fix: booking limits with seats not working (CAL-4553) #23298
Conversation
- Fix issue where slots with remaining seats were blocked once booking limit was hit - Add seat-aware logic to booking limits checking in _getBusyTimesFromBookingLimits - Only mark slots as busy when both booking limit is reached AND no seats remain - Update getBusyTimesFromTeamLimits to pass eventType parameter for seat checking - Maintain backward compatibility for non-seated events - Apply the same logic to user-specific booking limits in slots utility Resolves CAL-4553: Booking limits with seats not working
|
@Vansh5632 is attempting to deploy a commit to the cal Team on Vercel. A member of the Team first needs to authorize it. |
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the WalkthroughThe changes thread an eventType parameter through availability and interval-limit functions, enabling seat-aware booking-limit checks. Logic now distinguishes seated events: when a booking limit period is reached, the code computes remaining seats per time slot and marks busy only if no seats remain. Non-seated behavior stays unchanged. Adjustments occur in getUserAvailability, getBusyTimesFromLimits (and related helpers), and viewer slots utilities. Internal type aliases were renamed to private variants. Function signatures for busy-time retrieval were updated to accept and propagate eventType. Assessment against linked issues
Possibly related PRs
✨ Finishing Touches🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
Status, Documentation and Community
|
Graphite Automations"Add consumer team as reviewer" took an action on this PR • (08/23/25)1 reviewer was added to this PR based on Keith Williams's automation. "Add community label" took an action on this PR • (08/23/25)1 label was added to this PR based on Keith Williams's automation. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
packages/trpc/server/routers/viewer/slots/util.ts (1)
1191-1313: ThreadeventTypeinto the team‐limits path to enable seat‐aware blockingThe team‐level booking‐limits handler (
_getBusyTimesFromTeamLimitsForUsersin packages/trpc/server/routers/viewer/slots/util.ts) currently ignoreseventType.seatsPerTimeSlot, causing over‐blocking when seats are limited. You need to:• Extend the function signature to accept
eventType: EventType.
• PreserveeventTypeIdon each busy‐time entry.
• In both the global and per‐user loops, wheneventType.seatsPerTimeSlotis defined, count only bookings matching thateventType.idbefore marking the slot busy.
• Wire the neweventTypeargument through the call site at line 1416 of the same file.Suggested diff (in packages/trpc/server/routers/viewer/slots/util.ts):
--- a/packages/trpc/server/routers/viewer/slots/util.ts +++ b/packages/trpc/server/routers/viewer/slots/util.ts @@ 1191,7c1191,8 -const _getBusyTimesFromTeamLimitsForUsers = async ( +const _getBusyTimesFromTeamLimitsForUsers = async ( users: { id: number; email: string }[], bookingLimits: IntervalLimit, + eventType: NonNullable<EventType>, dateFrom: Dayjs, dateTo: Dayjs, teamId: number, @@ 1202,7c1203,8 - rescheduleUid?: string + rescheduleUid?: string, + // New param for seat limits ) => { @@ 1210,7c1212,8 - const busyTimes = bookings.map(({ id, startTime, endTime, eventTypeId, title, userId }) => ({ + const busyTimes = bookings.map(({ id, startTime, endTime, eventTypeId, title, userId }) => ({ start: dayjs(startTime).toDate(), end: dayjs(endTime).toDate(), title, @@ 1217,7c1221,8 userId, - })); + // Preserve for seat filtering + eventTypeId, + })); @@ 1240,7c1242,21 - globalLimitManager.addBusyTime(periodStart, unit, timeZone); + if (eventType.seatsPerTimeSlot) { + const slotCount = busyTimes.filter( + (b) => + b.eventTypeId === eventType.id && + dayjs(b.start).isSame(dayjs(booking.start)) && + isBookingWithinPeriod(b, periodStart, periodEnd, timeZone) + ).length; + if (slotCount >= eventType.seatsPerTimeSlot) { + globalLimitManager.addBusyTime(periodStart, unit, timeZone); + } + } else { + globalLimitManager.addBusyTime(periodStart, unit, timeZone); + } break; @@ 1290,7c1304,21 - limitManager.addBusyTime(periodStart, unit, timeZone); + if (eventType.seatsPerTimeSlot) { + const slotCount = userBusyTimes.filter( + (b) => + b.eventTypeId === eventType.id && + dayjs(b.start).isSame(dayjs(booking.start)) && + isBookingWithinPeriod(b, periodStart, periodEnd, timeZone) + ).length; + if (slotCount >= eventType.seatsPerTimeSlot) { + limitManager.addBusyTime(periodStart, unit, timeZone); + } + } else { + limitManager.addBusyTime(periodStart, unit, timeZone); + } break;And update the call at ~line 1416:
--- a/packages/trpc/server/routers/viewer/slots/util.ts:1414 +++ b/packages/trpc/server/routers/viewer/slots/util.ts:1414 - teamBookingLimitsMap = await getBusyTimesFromTeamLimitsForUsers( + teamBookingLimitsMap = await getBusyTimesFromTeamLimitsForUsers( usersForTeamLimits, teamBookingLimits, startTime, endTime, teamForBookingLimits.id, teamForBookingLimits.includeManagedEventsInLimits, usersWithCredentials[0]?.timeZone || "UTC", - input.rescheduleUid || undefined + input.rescheduleUid || undefined, + eventType );These changes ensure that team‐level limits respect per‐event‐type seat counts.
packages/lib/intervalLimits/server/getBusyTimesFromLimits.ts (2)
110-112: Timezone mismatch: period boundaries should use the provided timeZoneperiodStartDates are currently computed without passing timeZone, while isBookingWithinPeriod() uses timeZone. This can shift week/day boundaries and produce off-by-one-period bugs.
Apply timeZone consistently here and in duration limits:
-const periodStartDates = getPeriodStartDatesBetween(dateFrom, dateTo, unit); +const periodStartDates = getPeriodStartDatesBetween(dateFrom, dateTo, unit, timeZone || "UTC");And in _getBusyTimesFromDurationLimits:
-const periodStartDates = getPeriodStartDatesBetween(dateFrom, dateTo, unit); +const periodStartDates = getPeriodStartDatesBetween(dateFrom, dateTo, unit, timeZone);
274-281: Keep eventTypeId on mapped items so downstream seat checks can filter correctlyCurrently the mapper drops eventTypeId; retain it to enable per-eventType seat checks inside getBusyTimesFromBookingLimits.
-const busyTimes = bookings.map(({ id, startTime, endTime, eventTypeId, title, userId }) => ({ +const busyTimes = bookings.map(({ id, startTime, endTime, eventTypeId, title, userId }) => ({ start: dayjs(startTime).toDate(), end: dayjs(endTime).toDate(), title, source: `eventType-${eventTypeId}-booking-${id}`, userId, + eventTypeId, }));
🧹 Nitpick comments (2)
packages/trpc/server/routers/viewer/slots/util.ts (2)
1022-1042: Seat-aware block: logic directionally correct; tighten slot filtering and avoid quadratic scans
- Correctly defers marking periods busy for seated events until seats are exhausted.
- Two improvements:
- Restrict comparisons to exact instants without constructing Dayjs repeatedly in the inner loop to avoid O(N^2) overhead from filtering the entire array per booking.
- Use a precomputed map keyed by slotStartMillis to count bookings per period for better perf.
Apply this refactor inside the period loop to pre-aggregate by slot start:
- for (const booking of busyTimesFromLimitsBookings) { + // Pre-aggregate by slot start (ms) for this period only + const periodBookings = busyTimesFromLimitsBookings.filter((b) => + isBookingWithinPeriod(b, periodStart, periodEnd, timeZone) + ); + const bySlot = new Map<number, number>(); + for (const b of periodBookings) { + const k = new Date(b.start).getTime(); + bySlot.set(k, (bySlot.get(k) ?? 0) + 1); + } + for (const booking of periodBookings) { ... - const slotBookings = busyTimesFromLimitsBookings.filter( - (b) => - dayjs(b.start).isSame(dayjs(booking.start)) && - isBookingWithinPeriod(b, periodStart, periodEnd, timeZone) - ); - const totalAttendees = slotBookings.length; + const totalAttendees = bySlot.get(new Date(booking.start).getTime()) ?? 0; ... }
1099-1119: Duplicate seat-aware logic: extract helper or share pre-aggregation to reduce complexityYou’ve replicated the same seat-aware pattern in the per-user pass. Consider factoring a small helper or reusing the pre-aggregated map suggested above to keep the two loops consistent and cheaper.
📜 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 sources in your CodeRabbit configuration.
📒 Files selected for processing (3)
packages/lib/getUserAvailability.ts(2 hunks)packages/lib/intervalLimits/server/getBusyTimesFromLimits.ts(6 hunks)packages/trpc/server/routers/viewer/slots/util.ts(3 hunks)
🧰 Additional context used
📓 Path-based instructions (2)
**/*.ts
📄 CodeRabbit inference engine (.cursor/rules/review.mdc)
**/*.ts: For Prisma queries, only select data you need; never useinclude, always useselect
Ensure thecredential.keyfield is never returned from tRPC endpoints or APIs
Files:
packages/trpc/server/routers/viewer/slots/util.tspackages/lib/getUserAvailability.tspackages/lib/intervalLimits/server/getBusyTimesFromLimits.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/review.mdc)
Flag excessive Day.js use in performance-critical code; prefer native Date or Day.js
.utc()in hot paths like loops
Files:
packages/trpc/server/routers/viewer/slots/util.tspackages/lib/getUserAvailability.tspackages/lib/intervalLimits/server/getBusyTimesFromLimits.ts
🧠 Learnings (2)
📓 Common learnings
Learnt from: supalarry
PR: calcom/cal.com#23217
File: apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/output-event-types.service.ts:93-94
Timestamp: 2025-08-21T13:44:06.784Z
Learning: In apps/api/v2/src/ee/event-types/event-types_2024_06_14/event-types.repository.ts, repository functions that use explicit Prisma select clauses (like getEventTypeWithSeats) are used for specific purposes and don't need to include all EventType fields like bookingRequiresAuthentication. These methods don't feed into the general OutputEventTypesService_2024_06_14 flow.
📚 Learning: 2025-08-21T13:44:06.784Z
Learnt from: supalarry
PR: calcom/cal.com#23217
File: apps/api/v2/src/ee/event-types/event-types_2024_06_14/services/output-event-types.service.ts:93-94
Timestamp: 2025-08-21T13:44:06.784Z
Learning: In apps/api/v2/src/ee/event-types/event-types_2024_06_14/event-types.repository.ts, repository functions that use explicit Prisma select clauses (like getEventTypeWithSeats) are used for specific purposes and don't need to include all EventType fields like bookingRequiresAuthentication. These methods don't feed into the general OutputEventTypesService_2024_06_14 flow.
Applied to files:
packages/lib/intervalLimits/server/getBusyTimesFromLimits.ts
🧬 Code graph analysis (2)
packages/trpc/server/routers/viewer/slots/util.ts (2)
packages/lib/getUserAvailability.ts (1)
GetAvailabilityUser(193-193)packages/lib/intervalLimits/utils.ts (1)
isBookingWithinPeriod(42-56)
packages/lib/intervalLimits/server/getBusyTimesFromLimits.ts (2)
packages/lib/getUserAvailability.ts (1)
EventType(147-147)packages/lib/event-types/getEventTypeById.ts (1)
EventType(34-34)
⏰ 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). (2)
- GitHub Check: Install dependencies / Yarn install & cache
- GitHub Check: Codacy Static Code Analysis
🔇 Additional comments (6)
packages/trpc/server/routers/viewer/slots/util.ts (1)
63-65: Type alias rename is fineThe underscore prefix clarifies internal scope and avoids accidental exports. No issues.
packages/lib/intervalLimits/server/getBusyTimesFromLimits.ts (2)
46-47: Good: propagate eventType into booking-limits evaluationThis enables seated-event awareness down the stack.
248-258: Good: team limits now accept eventType for seat awarenessSignature change is correct and non-breaking for callers you updated.
packages/lib/getUserAvailability.ts (3)
269-269: Internal result type rename looks goodUnderscoring avoids accidental exports and clarifies intent.
391-393: Great: pass eventType to team-limit checksThis makes yearly/period checks seat-aware in the per-user path.
Note: If initialData.teamBookingLimits is provided (from slots/util.ts), it takes precedence and can still be seat-unaware. After updating slots/util.ts as suggested, this precedence remains correct.
351-368: Timezone argument consistently propagatedI’ve verified that every call to
getPeriodStartDatesBetweeninpackages/trpc/server/routers/viewer/slots/util.tsincludes thetimeZoneparameter, ensuring parity with the fallback logic’s use ofdateFrom.tz(timeZone)/dateTo.tz(timeZone)and preventing any off-by-one period issues. No further changes needed here.
| // For seated events, only mark slot as busy if there are no remaining seats | ||
| if (eventType?.seatsPerTimeSlot) { | ||
| // Get bookings for each specific time slot to check remaining seats | ||
| const slotBookings = bookings.filter( | ||
| (b) => | ||
| dayjs(b.start).isSame(dayjs(booking.start)) && | ||
| isBookingWithinPeriod(b, periodStart, periodEnd, timeZone || "UTC") | ||
| ); | ||
|
|
||
| // Count attendees across all bookings for this time slot | ||
| const totalAttendees = slotBookings.reduce((sum, _b) => { | ||
| // Each booking represents one booking, but we need to account for actual attendees | ||
| // For the purpose of booking limits, each accepted booking counts as 1 booking | ||
| // regardless of how many attendees it has | ||
| return sum + 1; | ||
| }, 0); | ||
|
|
||
| // Only mark as busy if no seats remain AND booking limit is reached | ||
| const remainingSeats = eventType.seatsPerTimeSlot - totalAttendees; | ||
| if (remainingSeats <= 0) { | ||
| limitManager.addBusyTime(periodStart, unit); | ||
| } | ||
| } else { | ||
| // For non-seated events, mark as busy when limit is reached | ||
| limitManager.addBusyTime(periodStart, unit); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Seat-aware filter should only count bookings from the current eventType when invoked via team limits
When this helper is called by getBusyTimesFromTeamLimits, the bookings array can include multiple event types. Seats must only be decremented by bookings for the same eventType.
- Preserve eventTypeId in the “bookings” you pass from the team path.
- Narrow slotBookings to the current eventType when eventTypeId is present.
- Also simplify attendee counting.
-const slotBookings = bookings.filter(
- (b) =>
- dayjs(b.start).isSame(dayjs(booking.start)) &&
- isBookingWithinPeriod(b, periodStart, periodEnd, timeZone || "UTC")
-);
-const totalAttendees = slotBookings.reduce((sum, _b) => { return sum + 1; }, 0);
+type EBWithEvent = EventBusyDetails & { eventTypeId?: number };
+const enriched = bookings as EBWithEvent[];
+let slotBookings = enriched.filter(
+ (b) =>
+ dayjs(b.start).isSame(dayjs(booking.start)) &&
+ isBookingWithinPeriod(b, periodStart, periodEnd, timeZone || "UTC")
+);
+if (slotBookings.length && typeof (slotBookings[0] as EBWithEvent).eventTypeId === "number") {
+ slotBookings = slotBookings.filter((b) => b.eventTypeId === eventType?.id);
+}
+const totalAttendees = slotBookings.length;📝 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.
| // For seated events, only mark slot as busy if there are no remaining seats | |
| if (eventType?.seatsPerTimeSlot) { | |
| // Get bookings for each specific time slot to check remaining seats | |
| const slotBookings = bookings.filter( | |
| (b) => | |
| dayjs(b.start).isSame(dayjs(booking.start)) && | |
| isBookingWithinPeriod(b, periodStart, periodEnd, timeZone || "UTC") | |
| ); | |
| // Count attendees across all bookings for this time slot | |
| const totalAttendees = slotBookings.reduce((sum, _b) => { | |
| // Each booking represents one booking, but we need to account for actual attendees | |
| // For the purpose of booking limits, each accepted booking counts as 1 booking | |
| // regardless of how many attendees it has | |
| return sum + 1; | |
| }, 0); | |
| // Only mark as busy if no seats remain AND booking limit is reached | |
| const remainingSeats = eventType.seatsPerTimeSlot - totalAttendees; | |
| if (remainingSeats <= 0) { | |
| limitManager.addBusyTime(periodStart, unit); | |
| } | |
| } else { | |
| // For non-seated events, mark as busy when limit is reached | |
| limitManager.addBusyTime(periodStart, unit); | |
| } | |
| // For seated events, only mark slot as busy if there are no remaining seats | |
| if (eventType?.seatsPerTimeSlot) { | |
| // Get bookings for each specific time slot to check remaining seats | |
| type EBWithEvent = EventBusyDetails & { eventTypeId?: number }; | |
| const enriched = bookings as EBWithEvent[]; | |
| let slotBookings = enriched.filter( | |
| (b) => | |
| dayjs(b.start).isSame(dayjs(booking.start)) && | |
| isBookingWithinPeriod(b, periodStart, periodEnd, timeZone || "UTC") | |
| ); | |
| if (slotBookings.length && typeof (slotBookings[0] as EBWithEvent).eventTypeId === "number") { | |
| slotBookings = slotBookings.filter((b) => b.eventTypeId === eventType?.id); | |
| } | |
| const totalAttendees = slotBookings.length; | |
| // Only mark as busy if no seats remain AND booking limit is reached | |
| const remainingSeats = eventType.seatsPerTimeSlot - totalAttendees; | |
| if (remainingSeats <= 0) { | |
| limitManager.addBusyTime(periodStart, unit); | |
| } | |
| } else { | |
| // For non-seated events, mark as busy when limit is reached | |
| limitManager.addBusyTime(periodStart, unit); | |
| } |
🤖 Prompt for AI Agents
In packages/lib/intervalLimits/server/getBusyTimesFromLimits.ts around lines 148
to 173, the seat-aware logic currently counts bookings across all event types
which is wrong when invoked from the team-limits path; ensure the bookings
passed in include eventTypeId and then filter slotBookings to only those with
the same eventTypeId (when eventTypeId is present) in addition to the existing
start/period/timeZone checks, and replace the manual attendee reduce with a
simple slotBookings.length (each booking counts as one) before computing
remainingSeats and calling limitManager.addBusyTime.
kart1ka
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you please resolve merge conflicts and address coderabbit suggestions?
|
@kart1ka okay working on them |
- Add eventTypeId to EventBusyDetails type to support event type filtering - Include eventTypeId in team booking limits busyTimes mapping - Filter slot bookings by eventTypeId when present (team-limits path) - Replace manual attendee reduce with simple slotBookings.length count - Ensure booking limits correctly isolate by event type instead of counting across all types Fixes CAL-4553
|
@kart1ka i am not understanding why the type-check errors are coming can you give me little bit explanation about it |
|
This PR is being marked as stale due to inactivity. |
What does this PR do?
Resolves CAL-4553: Booking limits with seats not working
Video Demo (if applicable):
Screencast.from.2025-08-23.13-11-44.mp4
Mandatory Tasks (DO NOT REMOVE)