-
Notifications
You must be signed in to change notification settings - Fork 8.3k
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
feat: global booking limits for teams #16614
Merged
Merged
Changes from 41 commits
Commits
Show all changes
48 commits
Select commit
Hold shift + click to select a range
3a3330c
add booking booking to team settings
694447f
add update mutation
5a786fb
add missing export
a9cbeb4
fix dirty state
e9ea093
first version of global team limits in getUserAvailability
ae8ac75
add test setup
899fcef
Merge branch 'main' into fix/global-booking-limits
b781c9f
create seperate test file for global limits
6a457f3
add tests for all units
5e233ee
move limitManager and booking limit functions outside of getUserAvail…
5775af5
add migration
3315380
clean up code
679191e
move yearly booking count to booking repository
f7657a0
code clean up
b6a9165
don't count rescheduling booking
a83adce
add test for getSchedule
df750c7
fix type error
9ebbdc9
fix type error
a82f667
fix type error
0195d9a
fix from and end date for fetching bookings
f3ff147
Merge branch 'main' into fix/global-booking-limits
CarinaWolli e1fb61b
reuse functions
54d657a
allow null for bookingLimits
b6b8022
remove bookings from managed event type
8ad7270
fix type error
ad8c9d5
code clean up
9f7989b
small fixes form clean up
4f1651a
Merge branch 'main' into fix/global-booking-limits
CarinaWolli 7341ee1
fix type issue
5d4b3ac
same fixes in teams/_post
b6e6210
Merge branch 'main' into fix/global-booking-limits
CarinaWolli 8c351c7
fix existing tz issue
e798a45
tests for fix
e025f51
adds missingn await
c3a7d5b
imrove description
89d578b
remove spreading
3400b03
fix reschedule issue with booking limits
fb7d7e3
fix reschedule error with booking durations
413dcba
remove useeffect
62926c9
undo commit
f7de41a
add bookingLimits to UpdateOrgTeamDto
910fff7
Merge branch 'main' into fix/global-booking-limits
1f184f3
Merge branch 'main' into fix/global-booking-limits
1e86cad
fix unit tests
8c337cd
Prepare view for app router migration
joeauyeung 014ba85
throw error if not in ascending order
ab66400
fix disabled update button
9be243e
Merge branch 'main' into fix/global-booking-limits
joeauyeung File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import TeamBookingLimitsView from "@calcom/features/ee/teams/pages/team-booking-limits-view"; | ||
|
||
import type { CalPageWrapper } from "@components/PageWrapper"; | ||
import PageWrapper from "@components/PageWrapper"; | ||
|
||
const Page = TeamBookingLimitsView as CalPageWrapper; | ||
Page.PageWrapper = PageWrapper; | ||
|
||
export default Page; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,7 +16,7 @@ import { | |
import { describe, vi, test } from "vitest"; | ||
|
||
import dayjs from "@calcom/dayjs"; | ||
import type { BookingStatus } from "@calcom/prisma/enums"; | ||
import { SchedulingType, type BookingStatus } from "@calcom/prisma/enums"; | ||
import { getAvailableSlots as getSchedule } from "@calcom/trpc/server/routers/viewer/slots/util"; | ||
|
||
import { expect } from "./getSchedule/expects"; | ||
|
@@ -1151,6 +1151,327 @@ describe("getSchedule", () => { | |
expect(availableSlotsInTz.filter((slot) => slot.format().startsWith(plus2DateString)).length).toBe(23); // 2 booking per day as limit, only one booking on that | ||
}); | ||
|
||
test("test that booking limit is working correctly if user is all day available and attendee is in different timezone than host", async () => { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 }); | ||
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 }); | ||
const { dateString: plus3DateString } = getDate({ dateIncrement: 3 }); | ||
|
||
const scenarioData = { | ||
eventTypes: [ | ||
{ | ||
id: 1, | ||
length: 60, | ||
beforeEventBuffer: 0, | ||
afterEventBuffer: 0, | ||
bookingLimits: { | ||
PER_DAY: 1, | ||
}, | ||
users: [ | ||
{ | ||
id: 101, | ||
}, | ||
], | ||
}, | ||
], | ||
users: [ | ||
{ | ||
...TestData.users.example, | ||
id: 101, | ||
schedules: [ | ||
{ | ||
id: 1, | ||
name: "All Day available", | ||
availability: [ | ||
{ | ||
userId: null, | ||
eventTypeId: null, | ||
days: [0, 1, 2, 3, 4, 5, 6], | ||
startTime: new Date("1970-01-01T00:00:00.000Z"), | ||
endTime: new Date("1970-01-01T23:59:59.999Z"), | ||
date: null, | ||
}, | ||
], | ||
timeZone: Timezones["+6:00"], | ||
}, | ||
], | ||
}, | ||
], | ||
// One bookings for each(E1 and E2) on plus2Date | ||
bookings: [ | ||
{ | ||
userId: 101, | ||
eventTypeId: 1, | ||
startTime: `${plus2DateString}T08:00:00.000Z`, | ||
endTime: `${plus2DateString}T09:00:00.000Z`, | ||
status: "ACCEPTED" as BookingStatus, | ||
}, | ||
], | ||
}; | ||
|
||
await createBookingScenario(scenarioData); | ||
|
||
const thisUserAvailabilityBookingLimit = await getSchedule({ | ||
input: { | ||
eventTypeId: 1, | ||
eventTypeSlug: "", | ||
startTime: `${plus1DateString}T00:00:00.000Z`, | ||
endTime: `${plus3DateString}T23:59:59.999Z`, | ||
timeZone: Timezones["-11:00"], //attendee timezone | ||
isTeamEvent: false, | ||
orgSlug: null, | ||
}, | ||
}); | ||
|
||
const availableSlotsInTz: dayjs.Dayjs[] = []; | ||
for (const date in thisUserAvailabilityBookingLimit.slots) { | ||
thisUserAvailabilityBookingLimit.slots[date].forEach((timeObj) => { | ||
availableSlotsInTz.push(dayjs(timeObj.time).tz(Timezones["+6:00"])); | ||
}); | ||
} | ||
|
||
expect(availableSlotsInTz.filter((slot) => slot.format().startsWith(plus2DateString)).length).toBe(0); // 1 booking per day as limit | ||
}); | ||
|
||
test("global team booking limit block slot if one fixed host reached limit", async () => { | ||
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 }); | ||
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 }); | ||
const { dateString: plus3DateString } = getDate({ dateIncrement: 3 }); | ||
|
||
const scenarioData = { | ||
eventTypes: [ | ||
{ | ||
id: 1, | ||
length: 60, | ||
beforeEventBuffer: 0, | ||
afterEventBuffer: 0, | ||
team: { | ||
id: 1, | ||
bookingLimits: { PER_DAY: 1 }, | ||
}, | ||
schedulingType: SchedulingType.COLLECTIVE, | ||
users: [ | ||
{ | ||
id: 101, | ||
}, | ||
{ | ||
id: 102, | ||
}, | ||
], | ||
}, | ||
{ | ||
id: 2, | ||
length: 60, | ||
beforeEventBuffer: 0, | ||
afterEventBuffer: 0, | ||
team: { | ||
id: 1, | ||
bookingLimits: { PER_DAY: 1 }, | ||
}, | ||
schedulingType: SchedulingType.COLLECTIVE, | ||
users: [ | ||
{ | ||
id: 101, | ||
}, | ||
{ | ||
id: 102, | ||
}, | ||
], | ||
}, | ||
{ | ||
id: 3, | ||
length: 60, | ||
beforeEventBuffer: 0, | ||
afterEventBuffer: 0, | ||
users: [ | ||
{ | ||
id: 101, | ||
}, | ||
], | ||
}, | ||
], | ||
users: [ | ||
{ | ||
...TestData.users.example, | ||
id: 101, | ||
schedules: [ | ||
{ | ||
id: 1, | ||
name: "All Day available", | ||
availability: [ | ||
{ | ||
userId: null, | ||
eventTypeId: null, | ||
days: [0, 1, 2, 3, 4, 5, 6], | ||
startTime: new Date("1970-01-01T00:00:00.000Z"), | ||
endTime: new Date("1970-01-01T23:59:59.999Z"), | ||
date: null, | ||
}, | ||
], | ||
timeZone: Timezones["+6:00"], | ||
}, | ||
], | ||
}, | ||
], | ||
bookings: [ | ||
{ | ||
userId: 101, | ||
eventTypeId: 1, | ||
startTime: `${plus2DateString}T08:00:00.000Z`, | ||
endTime: `${plus2DateString}T09:00:00.000Z`, | ||
status: "ACCEPTED" as BookingStatus, | ||
}, | ||
], | ||
}; | ||
|
||
await createBookingScenario(scenarioData); | ||
|
||
const availabilityEventTypeOne = await getSchedule({ | ||
input: { | ||
eventTypeId: 1, | ||
eventTypeSlug: "", | ||
startTime: `${plus1DateString}T00:00:00.000Z`, | ||
endTime: `${plus3DateString}T23:59:59.999Z`, | ||
timeZone: Timezones["+6:00"], | ||
isTeamEvent: false, | ||
orgSlug: null, | ||
}, | ||
}); | ||
|
||
const availableSlotsInTz: dayjs.Dayjs[] = []; | ||
for (const date in availabilityEventTypeOne.slots) { | ||
availabilityEventTypeOne.slots[date].forEach((timeObj) => { | ||
availableSlotsInTz.push(dayjs(timeObj.time).tz(Timezones["+6:00"])); | ||
}); | ||
} | ||
|
||
expect(availableSlotsInTz.filter((slot) => slot.format().startsWith(plus2DateString)).length).toBe(0); // 1 booking per day as limit | ||
|
||
const availabilityEventTypeTwo = await getSchedule({ | ||
input: { | ||
eventTypeId: 2, | ||
eventTypeSlug: "", | ||
startTime: `${plus1DateString}T00:00:00.000Z`, | ||
endTime: `${plus3DateString}T23:59:59.999Z`, | ||
timeZone: Timezones["+6:00"], | ||
isTeamEvent: false, | ||
orgSlug: null, | ||
}, | ||
}); | ||
|
||
for (const date in availabilityEventTypeTwo.slots) { | ||
availabilityEventTypeTwo.slots[date].forEach((timeObj) => { | ||
availableSlotsInTz.push(dayjs(timeObj.time).tz(Timezones["+6:00"])); | ||
}); | ||
} | ||
|
||
expect(availableSlotsInTz.filter((slot) => slot.format().startsWith(plus2DateString)).length).toBe(0); // 1 booking per day as limit | ||
|
||
const availabilityUserEventType = await getSchedule({ | ||
input: { | ||
eventTypeId: 3, | ||
eventTypeSlug: "", | ||
startTime: `${plus1DateString}T00:00:00.000Z`, | ||
endTime: `${plus3DateString}T23:59:59.999Z`, | ||
timeZone: Timezones["+6:00"], | ||
isTeamEvent: false, | ||
orgSlug: null, | ||
}, | ||
}); | ||
|
||
for (const date in availabilityUserEventType.slots) { | ||
availabilityUserEventType.slots[date].forEach((timeObj) => { | ||
availableSlotsInTz.push(dayjs(timeObj.time).tz(Timezones["+6:00"])); | ||
}); | ||
} | ||
|
||
expect(availableSlotsInTz.filter((slot) => slot.format().startsWith(plus2DateString)).length).toBe(23); | ||
}); | ||
|
||
test("global team booking limit blocks correct slots if attendee and host are in different timezone", async () => { | ||
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 }); | ||
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 }); | ||
const { dateString: plus3DateString } = getDate({ dateIncrement: 3 }); | ||
|
||
const scenarioData = { | ||
eventTypes: [ | ||
{ | ||
id: 1, | ||
length: 60, | ||
beforeEventBuffer: 0, | ||
afterEventBuffer: 0, | ||
team: { | ||
id: 1, | ||
bookingLimits: { PER_DAY: 1 }, | ||
}, | ||
schedulingType: SchedulingType.COLLECTIVE, | ||
users: [ | ||
{ | ||
id: 101, | ||
}, | ||
{ | ||
id: 102, | ||
}, | ||
], | ||
}, | ||
], | ||
users: [ | ||
{ | ||
...TestData.users.example, | ||
id: 101, | ||
schedules: [ | ||
{ | ||
id: 1, | ||
name: "All Day available", | ||
availability: [ | ||
{ | ||
userId: null, | ||
eventTypeId: null, | ||
days: [0, 1, 2, 3, 4, 5, 6], | ||
startTime: new Date("1970-01-01T00:00:00.000Z"), | ||
endTime: new Date("1970-01-01T23:59:59.999Z"), | ||
date: null, | ||
}, | ||
], | ||
timeZone: Timezones["+6:00"], | ||
}, | ||
], | ||
}, | ||
], | ||
bookings: [ | ||
{ | ||
userId: 101, | ||
eventTypeId: 1, | ||
startTime: `${plus2DateString}T08:00:00.000Z`, | ||
endTime: `${plus2DateString}T09:00:00.000Z`, | ||
status: "ACCEPTED" as BookingStatus, | ||
}, | ||
], | ||
}; | ||
|
||
await createBookingScenario(scenarioData); | ||
|
||
const thisUserAvailabilityBookingLimit = await getSchedule({ | ||
input: { | ||
eventTypeId: 1, | ||
eventTypeSlug: "", | ||
startTime: `${plus1DateString}T00:00:00.000Z`, | ||
endTime: `${plus3DateString}T23:59:59.999Z`, | ||
timeZone: Timezones["-11:00"], // attendee timezone | ||
isTeamEvent: false, | ||
orgSlug: null, | ||
}, | ||
}); | ||
|
||
const availableSlotsInTz: dayjs.Dayjs[] = []; | ||
for (const date in thisUserAvailabilityBookingLimit.slots) { | ||
thisUserAvailabilityBookingLimit.slots[date].forEach((timeObj) => { | ||
availableSlotsInTz.push(dayjs(timeObj.time).tz(Timezones["+6:00"])); | ||
}); | ||
} | ||
|
||
expect(availableSlotsInTz.filter((slot) => slot.format().startsWith(plus2DateString)).length).toBe(0); // 1 booking per day as limit | ||
}); | ||
|
||
test("a slot counts as being busy when the eventType is requiresConfirmation and requiresConfirmationWillBlockSlot", async () => { | ||
const { dateString: plus1DateString } = getDate({ dateIncrement: 1 }); | ||
const { dateString: plus2DateString } = getDate({ dateIncrement: 2 }); | ||
|
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
If we're letting users set the booking limits, via the API we should update the swagger docs to reflect that.