fix: booking limits (#12172)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
This commit is contained in:
parent
2e3b3257db
commit
7bb42ba577
|
@ -4,6 +4,7 @@ import prismock from "../../../../tests/libs/__mocks__/prisma";
|
|||
import { diff } from "jest-diff";
|
||||
import { describe, expect, vi, beforeEach, afterEach, test } from "vitest";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import type { BookingStatus } from "@calcom/prisma/enums";
|
||||
import type { Slot } from "@calcom/trpc/server/routers/viewer/slots/types";
|
||||
import { getAvailableSlots as getSchedule } from "@calcom/trpc/server/routers/viewer/slots/util";
|
||||
|
@ -874,6 +875,124 @@ describe("getSchedule", () => {
|
|||
}
|
||||
);
|
||||
});
|
||||
test("test that booking limit is working correctly if user is all day available", 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,
|
||||
bookingLimits: {
|
||||
PER_DAY: 1,
|
||||
},
|
||||
users: [
|
||||
{
|
||||
id: 101,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
length: 60,
|
||||
beforeEventBuffer: 0,
|
||||
afterEventBuffer: 0,
|
||||
bookingLimits: {
|
||||
PER_DAY: 2,
|
||||
},
|
||||
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["+5:30"],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
bookings: [
|
||||
{
|
||||
userId: 101,
|
||||
eventTypeId: 1,
|
||||
startTime: `${plus2DateString}T08:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T08:29:59.999Z`,
|
||||
status: "ACCEPTED" as BookingStatus,
|
||||
},
|
||||
{
|
||||
userId: 101,
|
||||
eventTypeId: 2,
|
||||
startTime: `${plus2DateString}T08:30:00.000Z`,
|
||||
endTime: `${plus2DateString}T08:29:59.999Z`,
|
||||
status: "ACCEPTED" as BookingStatus,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await createBookingScenario(scenarioData);
|
||||
|
||||
const thisUserAvailabilityBookingLimitOne = await getSchedule({
|
||||
input: {
|
||||
eventTypeId: 1,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T00:00:00.000Z`,
|
||||
endTime: `${plus3DateString}T23:59:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
},
|
||||
});
|
||||
|
||||
const thisUserAvailabilityBookingLimitTwo = await getSchedule({
|
||||
input: {
|
||||
eventTypeId: 2,
|
||||
eventTypeSlug: "",
|
||||
startTime: `${plus1DateString}T00:00:00.000Z`,
|
||||
endTime: `${plus3DateString}T23:59:59.999Z`,
|
||||
timeZone: Timezones["+5:30"],
|
||||
isTeamEvent: false,
|
||||
},
|
||||
});
|
||||
|
||||
let availableSlotsInTz: dayjs.Dayjs[] = [];
|
||||
for (const date in thisUserAvailabilityBookingLimitOne.slots) {
|
||||
thisUserAvailabilityBookingLimitOne.slots[date].forEach((timeObj) => {
|
||||
availableSlotsInTz.push(dayjs(timeObj.time).tz(Timezones["+5:30"]));
|
||||
});
|
||||
}
|
||||
|
||||
expect(availableSlotsInTz.filter((slot) => slot.format().startsWith(plus2DateString)).length).toBe(0); // 1 booking per day as limit
|
||||
|
||||
availableSlotsInTz = [];
|
||||
for (const date in thisUserAvailabilityBookingLimitTwo.slots) {
|
||||
thisUserAvailabilityBookingLimitTwo.slots[date].forEach((timeObj) => {
|
||||
availableSlotsInTz.push(dayjs(timeObj.time).tz(Timezones["+5:30"]));
|
||||
});
|
||||
}
|
||||
expect(availableSlotsInTz.filter((slot) => slot.format().startsWith(plus2DateString)).length).toBe(23); // 2 booking per day as limit, only one booking on that
|
||||
});
|
||||
});
|
||||
|
||||
describe("Team Event", () => {
|
||||
|
|
|
@ -101,6 +101,9 @@ export type InputEventType = {
|
|||
requiresConfirmation?: boolean;
|
||||
destinationCalendar?: Prisma.DestinationCalendarCreateInput;
|
||||
schedule?: InputUser["schedules"][number];
|
||||
bookingLimits?: {
|
||||
PER_DAY?: number;
|
||||
};
|
||||
} & Partial<Omit<Prisma.EventTypeCreateInput, "users" | "schedule">>;
|
||||
|
||||
type WhiteListedBookingProps = {
|
||||
|
@ -199,6 +202,7 @@ async function addEventTypes(eventTypes: InputEventType[], usersStore: InputUser
|
|||
timeZone: null,
|
||||
beforeEventBuffer: 0,
|
||||
afterEventBuffer: 0,
|
||||
bookingLimits: {},
|
||||
schedulingType: null,
|
||||
length: 15,
|
||||
//TODO: What is the purpose of periodStartDate and periodEndDate? Test these?
|
||||
|
|
|
@ -867,7 +867,12 @@ async function handler(
|
|||
) {
|
||||
const startAsDate = dayjs(reqBody.start).toDate();
|
||||
if (eventType.bookingLimits) {
|
||||
await checkBookingLimits(eventType.bookingLimits as IntervalLimit, startAsDate, eventType.id);
|
||||
await checkBookingLimits(
|
||||
eventType.bookingLimits as IntervalLimit,
|
||||
startAsDate,
|
||||
eventType.id,
|
||||
eventType.schedule?.timeZone
|
||||
);
|
||||
}
|
||||
if (eventType.durationLimits) {
|
||||
await checkDurationLimits(eventType.durationLimits as IntervalLimit, startAsDate, eventType.id);
|
||||
|
@ -915,8 +920,8 @@ async function handler(
|
|||
}),
|
||||
},
|
||||
{
|
||||
dateFrom: reqBody.start,
|
||||
dateTo: reqBody.end,
|
||||
dateFrom: dayjs(reqBody.start).tz(reqBody.timeZone).format(),
|
||||
dateTo: dayjs(reqBody.end).tz(reqBody.timeZone).format(),
|
||||
timeZone: reqBody.timeZone,
|
||||
originalRescheduledBooking,
|
||||
},
|
||||
|
@ -983,7 +988,6 @@ async function handler(
|
|||
const attendeeTimezone = attendeeInfoOnReschedule ? attendeeInfoOnReschedule.timeZone : reqBody.timeZone;
|
||||
|
||||
const tAttendees = await getTranslation(attendeeLanguage ?? "en", "common");
|
||||
|
||||
// use host default
|
||||
if (isTeamEventType && locationBodyString === OrganizerDefaultConferencingAppType) {
|
||||
const metadataParseResult = userMetadataSchema.safeParse(organizerUser.metadata);
|
||||
|
|
|
@ -11,14 +11,15 @@ import { parseBookingLimit } from "../isBookingLimits";
|
|||
export async function checkBookingLimits(
|
||||
bookingLimits: IntervalLimit,
|
||||
eventStartDate: Date,
|
||||
eventId: number
|
||||
eventId: number,
|
||||
timeZone?: string | null
|
||||
) {
|
||||
const parsedBookingLimits = parseBookingLimit(bookingLimits);
|
||||
if (!parsedBookingLimits) return false;
|
||||
|
||||
// not iterating entries to preserve types
|
||||
const limitCalculations = ascendingLimitKeys.map((key) =>
|
||||
checkBookingLimit({ key, limitingNumber: parsedBookingLimits[key], eventStartDate, eventId })
|
||||
checkBookingLimit({ key, limitingNumber: parsedBookingLimits[key], eventStartDate, eventId, timeZone })
|
||||
);
|
||||
|
||||
try {
|
||||
|
@ -33,19 +34,23 @@ export async function checkBookingLimit({
|
|||
eventId,
|
||||
key,
|
||||
limitingNumber,
|
||||
timeZone,
|
||||
}: {
|
||||
eventStartDate: Date;
|
||||
eventId: number;
|
||||
key: keyof IntervalLimit;
|
||||
limitingNumber: number | undefined;
|
||||
timeZone?: string | null;
|
||||
}) {
|
||||
{
|
||||
const eventDateInOrganizerTz = timeZone ? dayjs(eventStartDate).tz(timeZone) : dayjs(eventStartDate);
|
||||
|
||||
if (!limitingNumber) return;
|
||||
|
||||
const unit = intervalLimitKeyToUnit(key);
|
||||
|
||||
const startDate = dayjs(eventStartDate).startOf(unit).toDate();
|
||||
const endDate = dayjs(eventStartDate).endOf(unit).toDate();
|
||||
const startDate = dayjs(eventDateInOrganizerTz).startOf(unit).toDate();
|
||||
const endDate = dayjs(eventDateInOrganizerTz).endOf(unit).toDate();
|
||||
|
||||
const bookingsInPeriod = await prisma.booking.count({
|
||||
where: {
|
||||
|
|
|
@ -205,7 +205,11 @@ export const stringOrNumber = z.union([
|
|||
z.number().int(),
|
||||
]);
|
||||
|
||||
export const stringToDayjs = z.string().transform((val) => dayjs(val));
|
||||
export const stringToDayjs = z.string().transform((val) => {
|
||||
const matches = val.match(/([+-]\d{2}:\d{2})$/);
|
||||
const timezone = matches ? matches[1] : "+00:00";
|
||||
return dayjs(val).utcOffset(timezone);
|
||||
});
|
||||
|
||||
export const bookingCreateBodySchema = z.object({
|
||||
end: z.string().optional(),
|
||||
|
|
Loading…
Reference in New Issue
Block a user