Skip to content
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
7e416b2
refactor(bookings): add round-robin host effective-limits foundation …
manurana26770 Apr 2, 2026
fd96d98
Merge branch 'main' of https://github.com/calcom/cal.com into feat/in…
manurana26770 Apr 2, 2026
a006556
feat(bookings): plumb host limit override fields into round-robin boo…
manurana26770 Apr 2, 2026
4bf1e79
refactor(users): dedupe normalized host projection for routing paths
manurana26770 Apr 2, 2026
9aa3339
Merge branch 'main' of https://github.com/manurana26770/cal.com into …
manurana26770 Apr 11, 2026
cd35eae
Merge branch 'calcom:main' into feat/individual-member-limits
manurana26770 Apr 11, 2026
28864fb
Merge branch 'main' of https://github.com/calcom/cal.com into feat/in…
manurana26770 Apr 12, 2026
5ca647c
feat(event-types): persist per-host round-robin limit overrides
manurana26770 Apr 12, 2026
a06f40a
feat(event-types-ui): add per-host limit controls in assignment flow
manurana26770 Apr 12, 2026
a8c06e9
feat(bookings): load host override limits in booking pipeline
manurana26770 Apr 12, 2026
7666def
feat(round-robin): enforce effective host limits in booking flows
manurana26770 Apr 12, 2026
f094cf4
feat(slots): bucket hosts by effective booking limits
manurana26770 Apr 12, 2026
af540ba
Merge branch 'feat/individual-member-limits' of https://github.com/ma…
manurana26770 Apr 12, 2026
c8d740a
fix(round-robin): address review findings for host overrides and slots
manurana26770 Apr 13, 2026
e0e90e8
Merge branch 'main' of https://github.com/calcom/cal.com into feat/in…
manurana26770 Apr 15, 2026
2cd0a29
fix(round-robin): resolve post-merge review findings
manurana26770 Apr 15, 2026
52840ed
fix(round-robin): retry host selection on limit conflicts
manurana26770 Apr 15, 2026
3b5b741
fix(eventtypes,slots): stabilize UTC date normalization and timezone …
manurana26770 Apr 15, 2026
192c88b
fix(round-robin): recheck recurring availability for retry candidates
manurana26770 Apr 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,17 @@ const getEventTypesFromDBSelect = {
isFixed: true,
priority: true,
weight: true,
overrideMinimumBookingNotice: true,
overrideBeforeEventBuffer: true,
overrideAfterEventBuffer: true,
overrideSlotInterval: true,
overrideBookingLimits: true,
overrideDurationLimits: true,
overridePeriodType: true,
overridePeriodStartDate: true,
overridePeriodEndDate: true,
overridePeriodDays: true,
overridePeriodCountCalendarDays: true,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
createdAt: true,
groupId: true,
location: {
Expand Down
47 changes: 39 additions & 8 deletions packages/features/bookings/lib/handleNewBooking/loadUsers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,45 @@ const loadUsersByEventType = async (eventType: EventType): Promise<NewBookingEve
eventType,
hosts: hosts ?? fallbackHosts,
});
return matchingHosts.map(({ user, isFixed, priority, weight, createdAt, groupId }) => ({
...user,
isFixed,
priority,
weight,
createdAt,
groupId,
}));
return matchingHosts.map(
({
user,
isFixed,
priority,
weight,
overrideMinimumBookingNotice,
overrideBeforeEventBuffer,
overrideAfterEventBuffer,
overrideSlotInterval,
overrideBookingLimits,
overrideDurationLimits,
overridePeriodType,
overridePeriodStartDate,
overridePeriodEndDate,
overridePeriodDays,
overridePeriodCountCalendarDays,
createdAt,
groupId,
}) => ({
...user,
isFixed,
priority,
weight,
overrideMinimumBookingNotice,
overrideBeforeEventBuffer,
overrideAfterEventBuffer,
overrideSlotInterval,
overrideBookingLimits,
overrideDurationLimits,
overridePeriodType,
overridePeriodStartDate,
overridePeriodEndDate,
overridePeriodDays,
overridePeriodCountCalendarDays,
createdAt,
groupId,
})
);
};

const loadDynamicUsers = async (dynamicUserList: string[], currentOrgDomain: string | null) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
import type { JsonValue } from "@calcom/prisma/client/runtime/library";
import { PeriodType, SchedulingType } from "@calcom/prisma/enums";

type EventLevelLimits = {
minimumBookingNotice: number;
beforeEventBuffer: number;
afterEventBuffer: number;
slotInterval: number | null;
bookingLimits: JsonValue | null;
durationLimits: JsonValue | null;
periodType: PeriodType;
periodDays: number | null;
periodCountCalendarDays: boolean | null;
periodStartDate: Date | null;
periodEndDate: Date | null;
};

export type RoundRobinHostLimitOverrides = {
minimumBookingNotice?: number | null;
beforeEventBuffer?: number | null;
afterEventBuffer?: number | null;
slotInterval?: number | null;
bookingLimits?: JsonValue | null;
durationLimits?: JsonValue | null;
periodType?: PeriodType | null;
periodDays?: number | null;
periodCountCalendarDays?: boolean | null;
periodStartDate?: Date | null;
periodEndDate?: Date | null;
};

export type RoundRobinHostLimitOverrideSource = {
overrideMinimumBookingNotice?: number | null;
overrideBeforeEventBuffer?: number | null;
overrideAfterEventBuffer?: number | null;
overrideSlotInterval?: number | null;
overrideBookingLimits?: JsonValue | null;
overrideDurationLimits?: JsonValue | null;
overridePeriodType?: PeriodType | null;
overridePeriodDays?: number | null;
overridePeriodCountCalendarDays?: boolean | null;
overridePeriodStartDate?: Date | null;
overridePeriodEndDate?: Date | null;
};

export type EffectiveHostLimits = EventLevelLimits;

export type EffectiveLimitBucket<THost> = {
profileKey: string;
effectiveLimits: EffectiveHostLimits;
hosts: THost[];
};

export function resolveRoundRobinHostEffectiveLimits({
schedulingType,
eventLimits,
hostOverrides,
}: {
schedulingType: SchedulingType | null;
eventLimits: EventLevelLimits;
hostOverrides?: RoundRobinHostLimitOverrides | null;
}): EffectiveHostLimits {
// Contract: overrides apply only to round-robin hosts.
if (schedulingType !== SchedulingType.ROUND_ROBIN || !hostOverrides) {
return { ...eventLimits };
}

return {
minimumBookingNotice: hostOverrides.minimumBookingNotice ?? eventLimits.minimumBookingNotice,
beforeEventBuffer: hostOverrides.beforeEventBuffer ?? eventLimits.beforeEventBuffer,
afterEventBuffer: hostOverrides.afterEventBuffer ?? eventLimits.afterEventBuffer,
slotInterval: hostOverrides.slotInterval ?? eventLimits.slotInterval,
bookingLimits: hostOverrides.bookingLimits ?? eventLimits.bookingLimits,
durationLimits: hostOverrides.durationLimits ?? eventLimits.durationLimits,
periodType: hostOverrides.periodType ?? eventLimits.periodType,
periodDays: hostOverrides.periodDays ?? eventLimits.periodDays,
periodCountCalendarDays:
hostOverrides.periodCountCalendarDays ?? eventLimits.periodCountCalendarDays,
periodStartDate: hostOverrides.periodStartDate ?? eventLimits.periodStartDate,
periodEndDate: hostOverrides.periodEndDate ?? eventLimits.periodEndDate,
};
}

export function hasAnyRoundRobinHostOverrides(
hostOverrides?: RoundRobinHostLimitOverrides | null
): boolean {
if (!hostOverrides) {
return false;
}

return Object.values(hostOverrides).some((value) => value !== null && value !== undefined);
}

export function getRoundRobinHostLimitOverrides(
host: RoundRobinHostLimitOverrideSource
): RoundRobinHostLimitOverrides | null {
const resolvedOverrides: RoundRobinHostLimitOverrides = {
minimumBookingNotice: host.overrideMinimumBookingNotice,
beforeEventBuffer: host.overrideBeforeEventBuffer,
afterEventBuffer: host.overrideAfterEventBuffer,
slotInterval: host.overrideSlotInterval,
bookingLimits: host.overrideBookingLimits,
durationLimits: host.overrideDurationLimits,
periodType: host.overridePeriodType,
periodDays: host.overridePeriodDays,
periodCountCalendarDays: host.overridePeriodCountCalendarDays,
periodStartDate: host.overridePeriodStartDate,
periodEndDate: host.overridePeriodEndDate,
};

return hasAnyRoundRobinHostOverrides(resolvedOverrides) ? resolvedOverrides : null;
}

export function buildEffectiveHostLimitsProfileKey(effectiveLimits: EffectiveHostLimits): string {
return JSON.stringify({
...effectiveLimits,
periodStartDate: effectiveLimits.periodStartDate?.toISOString() ?? null,
periodEndDate: effectiveLimits.periodEndDate?.toISOString() ?? null,
});
}

export function groupRoundRobinHostsByEffectiveLimits<THost>({
schedulingType,
eventLimits,
hosts,
getHostOverrides,
}: {
schedulingType: SchedulingType | null;
eventLimits: EventLevelLimits;
hosts: THost[];
getHostOverrides: (host: THost) => RoundRobinHostLimitOverrides | null | undefined;
}): EffectiveLimitBucket<THost>[] {
const bucketsByKey = new Map<string, EffectiveLimitBucket<THost>>();

for (const host of hosts) {
const effectiveLimits = resolveRoundRobinHostEffectiveLimits({
schedulingType,
eventLimits,
hostOverrides: getHostOverrides(host),
});

const profileKey = buildEffectiveHostLimitsProfileKey(effectiveLimits);
const existingBucket = bucketsByKey.get(profileKey);

if (existingBucket) {
existingBucket.hosts.push(host);
continue;
}

bucketsByKey.set(profileKey, {
profileKey,
effectiveLimits,
hosts: [host],
});
}

return [...bucketsByKey.values()];
}
Loading
Loading