fix: allow new booking to overlap old one when rescheduling (#10424)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
This commit is contained in:
parent
c7dfa7bc89
commit
a6daf17909
|
@ -1,5 +1,6 @@
|
|||
import { expect } from "@playwright/test";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { BookingStatus } from "@calcom/prisma/enums";
|
||||
|
||||
|
@ -239,4 +240,27 @@ test.describe("Reschedule Tests", async () => {
|
|||
expect(newBooking).not.toBeNull();
|
||||
expect(newBooking?.status).toBe(BookingStatus.ACCEPTED);
|
||||
});
|
||||
|
||||
test("Should be able to book slot that overlaps with original rescheduled booking", async ({
|
||||
page,
|
||||
users,
|
||||
bookings,
|
||||
}) => {
|
||||
const user = await users.create();
|
||||
const eventType = user.eventTypes[0];
|
||||
const startTime = dayjs().add(1, "day").set("hour", 10).set("minute", 0).toDate();
|
||||
const endTime = dayjs().add(1, "day").set("hour", 10).set("minute", 30).toDate();
|
||||
|
||||
const booking = await bookings.create(user.id, user.username, eventType.id, {}, startTime, endTime);
|
||||
|
||||
await page.goto(`/reschedule/${booking.uid}`);
|
||||
|
||||
//book same slot again
|
||||
page.getByRole("button", { name: dayjs(startTime).format("D"), exact: true }).click();
|
||||
|
||||
page.getByRole("button", { name: dayjs(startTime).format("h:mmA") }).click();
|
||||
|
||||
await page.locator('[data-testid="confirm-reschedule-button"]').click();
|
||||
await expect(page).toHaveURL(/.*booking/);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -21,6 +21,8 @@ export async function getBusyTimes(params: {
|
|||
endTime: string;
|
||||
selectedCalendars: SelectedCalendar[];
|
||||
seatedEvent?: boolean;
|
||||
rescheduleUid?: string | null;
|
||||
duration?: number | null;
|
||||
}) {
|
||||
const {
|
||||
credentials,
|
||||
|
@ -33,6 +35,8 @@ export async function getBusyTimes(params: {
|
|||
afterEventBuffer,
|
||||
selectedCalendars,
|
||||
seatedEvent,
|
||||
rescheduleUid,
|
||||
duration,
|
||||
} = params;
|
||||
logger.silly(
|
||||
`Checking Busy time from Cal Bookings in range ${startTime} to ${endTime} for input ${JSON.stringify({
|
||||
|
@ -67,9 +71,14 @@ export async function getBusyTimes(params: {
|
|||
*/
|
||||
performance.mark("prismaBookingGetStart");
|
||||
|
||||
const startTimeDate =
|
||||
rescheduleUid && duration ? dayjs(startTime).subtract(duration, "minute").toDate() : new Date(startTime);
|
||||
const endTimeDate =
|
||||
rescheduleUid && duration ? dayjs(endTime).add(duration, "minute").toDate() : new Date(endTime);
|
||||
|
||||
const sharedQuery = {
|
||||
startTime: { gte: new Date(startTime) },
|
||||
endTime: { lte: new Date(endTime) },
|
||||
startTime: { gte: startTimeDate },
|
||||
endTime: { lte: endTimeDate },
|
||||
status: {
|
||||
in: [BookingStatus.ACCEPTED],
|
||||
},
|
||||
|
@ -96,6 +105,7 @@ export async function getBusyTimes(params: {
|
|||
},
|
||||
select: {
|
||||
id: true,
|
||||
uid: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
title: true,
|
||||
|
@ -138,6 +148,9 @@ export async function getBusyTimes(params: {
|
|||
// doing this allows using the map later to remove the ranges from calendar busy times.
|
||||
delete bookingSeatCountMap[bookedAt];
|
||||
}
|
||||
if (rest.uid === rescheduleUid) {
|
||||
return aggregate;
|
||||
}
|
||||
aggregate.push({
|
||||
start: dayjs(startTime)
|
||||
.subtract((eventType?.beforeEventBuffer || 0) + (afterEventBuffer || 0), "minute")
|
||||
|
@ -180,6 +193,17 @@ export async function getBusyTimes(params: {
|
|||
};
|
||||
});
|
||||
|
||||
if (rescheduleUid) {
|
||||
const originalRescheduleBooking = bookings.find((booking) => booking.uid === rescheduleUid);
|
||||
// calendar busy time from original rescheduled booking should not be blocked
|
||||
if (originalRescheduleBooking) {
|
||||
openSeatsDateRanges.push({
|
||||
start: dayjs(originalRescheduleBooking.startTime),
|
||||
end: dayjs(originalRescheduleBooking.endTime),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const result = subtract(
|
||||
calendarBusyTimes.map((value) => ({
|
||||
...value,
|
||||
|
|
|
@ -127,6 +127,7 @@ export const getUserAvailability = async function getUsersWorkingHoursLifeTheUni
|
|||
user?: User;
|
||||
eventType?: EventType;
|
||||
currentSeats?: CurrentSeats;
|
||||
rescheduleUid?: string | null;
|
||||
}
|
||||
) {
|
||||
const { username, userId, dateFrom, dateTo, eventTypeId, afterEventBuffer, beforeEventBuffer, duration } =
|
||||
|
@ -184,6 +185,8 @@ export const getUserAvailability = async function getUsersWorkingHoursLifeTheUni
|
|||
afterEventBuffer,
|
||||
selectedCalendars: user.selectedCalendars,
|
||||
seatedEvent: !!eventType?.seatsPerTimeSlot,
|
||||
rescheduleUid: initialData?.rescheduleUid || null,
|
||||
duration,
|
||||
});
|
||||
|
||||
const detailedBusyTimes: EventBusyDetails[] = [
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { useSearchParams } from "next/navigation";
|
||||
import { shallow } from "zustand/shallow";
|
||||
|
||||
import { useSchedule } from "@calcom/features/schedules";
|
||||
|
@ -58,6 +59,8 @@ export const useScheduleForEvent = ({
|
|||
(state) => [state.username, state.eventSlug, state.month, state.selectedDuration],
|
||||
shallow
|
||||
);
|
||||
const serachParams = useSearchParams();
|
||||
const rescheduleUid = serachParams.get("rescheduleUid");
|
||||
|
||||
return useSchedule({
|
||||
username: usernameFromStore ?? username,
|
||||
|
@ -65,6 +68,7 @@ export const useScheduleForEvent = ({
|
|||
eventId: event.data?.id ?? eventId,
|
||||
timezone,
|
||||
prefetchNextMonth,
|
||||
rescheduleUid,
|
||||
month: monthFromStore ?? month,
|
||||
duration: durationFromStore ?? duration,
|
||||
});
|
||||
|
|
|
@ -94,6 +94,7 @@ const log = logger.getChildLogger({ prefix: ["[api] book:user"] });
|
|||
|
||||
type User = Prisma.UserGetPayload<typeof userSelect>;
|
||||
type BufferedBusyTimes = BufferedBusyTime[];
|
||||
type BookingType = Prisma.PromiseReturnType<typeof getOriginalRescheduledBooking>;
|
||||
|
||||
interface IEventTypePaymentCredentialType {
|
||||
appId: EventTypeAppsList;
|
||||
|
@ -357,22 +358,35 @@ async function ensureAvailableUsers(
|
|||
eventType: Awaited<ReturnType<typeof getEventTypesFromDB>> & {
|
||||
users: IsFixedAwareUser[];
|
||||
},
|
||||
input: { dateFrom: string; dateTo: string; timeZone: string },
|
||||
input: { dateFrom: string; dateTo: string; timeZone: string; originalRescheduledBooking?: BookingType },
|
||||
recurringDatesInfo?: {
|
||||
allRecurringDates: string[] | undefined;
|
||||
currentRecurringIndex: number | undefined;
|
||||
}
|
||||
) {
|
||||
const availableUsers: IsFixedAwareUser[] = [];
|
||||
|
||||
const orginalBookingDuration = input.originalRescheduledBooking
|
||||
? dayjs(input.originalRescheduledBooking.endTime).diff(
|
||||
dayjs(input.originalRescheduledBooking.startTime),
|
||||
"minutes"
|
||||
)
|
||||
: undefined;
|
||||
|
||||
/** Let's start checking for availability */
|
||||
for (const user of eventType.users) {
|
||||
const { dateRanges, busy: bufferedBusyTimes } = await getUserAvailability(
|
||||
{
|
||||
userId: user.id,
|
||||
eventTypeId: eventType.id,
|
||||
duration: orginalBookingDuration,
|
||||
...input,
|
||||
},
|
||||
{ user, eventType }
|
||||
{
|
||||
user,
|
||||
eventType,
|
||||
rescheduleUid: input.originalRescheduledBooking?.uid ?? null,
|
||||
}
|
||||
);
|
||||
|
||||
if (!dateRanges.length) {
|
||||
|
@ -806,6 +820,34 @@ async function handler(
|
|||
}
|
||||
}
|
||||
|
||||
let rescheduleUid = reqBody.rescheduleUid;
|
||||
let bookingSeat: Prisma.BookingSeatGetPayload<{ include: { booking: true; attendee: true } }> | null = null;
|
||||
|
||||
let originalRescheduledBooking: BookingType = null;
|
||||
|
||||
if (rescheduleUid) {
|
||||
// rescheduleUid can be bookingUid and bookingSeatUid
|
||||
bookingSeat = await prisma.bookingSeat.findUnique({
|
||||
where: {
|
||||
referenceUid: rescheduleUid,
|
||||
},
|
||||
include: {
|
||||
booking: true,
|
||||
attendee: true,
|
||||
},
|
||||
});
|
||||
if (bookingSeat) {
|
||||
rescheduleUid = bookingSeat.booking.uid;
|
||||
}
|
||||
originalRescheduledBooking = await getOriginalRescheduledBooking(
|
||||
rescheduleUid,
|
||||
!!eventType.seatsPerTimeSlot
|
||||
);
|
||||
if (!originalRescheduledBooking) {
|
||||
throw new HttpError({ statusCode: 404, message: "Could not find original booking" });
|
||||
}
|
||||
}
|
||||
|
||||
if (!eventType.seatsPerTimeSlot) {
|
||||
const availableUsers = await ensureAvailableUsers(
|
||||
{
|
||||
|
@ -822,6 +864,7 @@ async function handler(
|
|||
dateFrom: reqBody.start,
|
||||
dateTo: reqBody.end,
|
||||
timeZone: reqBody.timeZone,
|
||||
originalRescheduledBooking,
|
||||
},
|
||||
{
|
||||
allRecurringDates,
|
||||
|
@ -860,34 +903,6 @@ async function handler(
|
|||
|
||||
const allCredentials = await getAllCredentials(organizerUser, eventType);
|
||||
|
||||
let rescheduleUid = reqBody.rescheduleUid;
|
||||
let bookingSeat: Prisma.BookingSeatGetPayload<{ include: { booking: true; attendee: true } }> | null = null;
|
||||
type BookingType = Prisma.PromiseReturnType<typeof getOriginalRescheduledBooking>;
|
||||
let originalRescheduledBooking: BookingType = null;
|
||||
|
||||
if (rescheduleUid) {
|
||||
// rescheduleUid can be bookingUid and bookingSeatUid
|
||||
bookingSeat = await prisma.bookingSeat.findUnique({
|
||||
where: {
|
||||
referenceUid: rescheduleUid,
|
||||
},
|
||||
include: {
|
||||
booking: true,
|
||||
attendee: true,
|
||||
},
|
||||
});
|
||||
if (bookingSeat) {
|
||||
rescheduleUid = bookingSeat.booking.uid;
|
||||
}
|
||||
originalRescheduledBooking = await getOriginalRescheduledBooking(
|
||||
rescheduleUid,
|
||||
!!eventType.seatsPerTimeSlot
|
||||
);
|
||||
if (!originalRescheduledBooking) {
|
||||
throw new HttpError({ statusCode: 404, message: "Could not find original booking" });
|
||||
}
|
||||
}
|
||||
|
||||
const isOrganizerRescheduling = organizerUser.id === userId;
|
||||
|
||||
const attendeeInfoOnReschedule =
|
||||
|
|
|
@ -10,6 +10,7 @@ type UseScheduleWithCacheArgs = {
|
|||
timezone?: string | null;
|
||||
prefetchNextMonth?: boolean;
|
||||
duration?: number | null;
|
||||
rescheduleUid?: string | null;
|
||||
};
|
||||
|
||||
export const useSchedule = ({
|
||||
|
@ -20,6 +21,7 @@ export const useSchedule = ({
|
|||
eventId,
|
||||
prefetchNextMonth,
|
||||
duration,
|
||||
rescheduleUid,
|
||||
}: UseScheduleWithCacheArgs) => {
|
||||
const monthDayjs = month ? dayjs(month) : dayjs();
|
||||
const nextMonthDayjs = monthDayjs.add(1, "month");
|
||||
|
@ -40,6 +42,7 @@ export const useSchedule = ({
|
|||
endTime: (prefetchNextMonth ? nextMonthDayjs : monthDayjs).endOf("month").toISOString(),
|
||||
timeZone: timezone!,
|
||||
duration: duration ? `${duration}` : undefined,
|
||||
rescheduleUid,
|
||||
},
|
||||
{
|
||||
trpc: {
|
||||
|
|
|
@ -20,6 +20,7 @@ export const getScheduleSchema = z
|
|||
.string()
|
||||
.optional()
|
||||
.transform((val) => val && parseInt(val)),
|
||||
rescheduleUid: z.string().optional().nullable(),
|
||||
})
|
||||
.transform((val) => {
|
||||
// Need this so we can pass a single username in the query string form public API
|
||||
|
|
|
@ -279,11 +279,7 @@ export async function getAvailableSlots(input: TGetScheduleInputSchema) {
|
|||
beforeEventBuffer: eventType.beforeEventBuffer,
|
||||
duration: input.duration || 0,
|
||||
},
|
||||
{
|
||||
user: currentUser,
|
||||
eventType,
|
||||
currentSeats,
|
||||
}
|
||||
{ user: currentUser, eventType, currentSeats, rescheduleUid: input.rescheduleUid }
|
||||
);
|
||||
if (!currentSeats && _currentSeats) currentSeats = _currentSeats;
|
||||
return {
|
||||
|
|
Loading…
Reference in New Issue
Block a user