Fix bug for reschedule on opt-in bookings (#3678)
* Fix bug for reschedule on opt-in bookings * Fix a missing query parameter being removed when changing month, useRouterQuery * Added test for rescheduling user type condition * Fix conditional that triggers input disabling when rescheduling Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com> Co-authored-by: Bailey Pumfleet <pumfleet@hey.com>
This commit is contained in:
co-authored by
kodiakhq[bot]
Bailey Pumfleet
parent
70d6c8d8e7
commit
775fe92960
@@ -287,10 +287,14 @@ const dateQuerySchema = z.object({
|
||||
|
||||
const useRouterQuery = <T extends string>(name: T) => {
|
||||
const router = useRouter();
|
||||
const query = z.object({ [name]: z.string().optional() }).parse(router.query);
|
||||
const existingQueryParams = router.asPath.split("?")[1];
|
||||
|
||||
const urlParams = new URLSearchParams(existingQueryParams);
|
||||
const query = Object.fromEntries(urlParams);
|
||||
|
||||
const setQuery = (newValue: string | number | null | undefined) => {
|
||||
router.replace({ query: { ...router.query, [name]: newValue } }, undefined, { shallow: true });
|
||||
router.replace({ query: { ...router.query, ...query, [name]: newValue } }, undefined, { shallow: true });
|
||||
};
|
||||
|
||||
return { [name]: query[name], setQuery } as {
|
||||
|
||||
@@ -218,11 +218,25 @@ const BookingPage = ({
|
||||
? booking?.attendees.slice(1).map((attendee) => attendee.email)
|
||||
: [];
|
||||
|
||||
// There should only exists one default userData variable for primaryAttendee.
|
||||
const defaultUserValues = {
|
||||
email: booking?.attendees[0].email
|
||||
? booking.attendees[0].email
|
||||
: router.query.email
|
||||
? (router.query.email as string)
|
||||
: "",
|
||||
name: booking?.attendees[0].name
|
||||
? booking.attendees[0].name
|
||||
: router.query.name
|
||||
? (router.query.name as string)
|
||||
: "",
|
||||
};
|
||||
|
||||
const defaultValues = () => {
|
||||
if (!rescheduleUid) {
|
||||
return {
|
||||
name: (router.query.name as string) || (!loggedInIsOwner && session?.user?.name) || "",
|
||||
email: (router.query.email as string) || (!loggedInIsOwner && session?.user?.email) || "",
|
||||
name: defaultUserValues.name || (!loggedInIsOwner && session?.user?.name) || "",
|
||||
email: defaultUserValues.email || (!loggedInIsOwner && session?.user?.email) || "",
|
||||
notes: (router.query.notes as string) || "",
|
||||
guests: ensureArray(router.query.guest) as string[],
|
||||
customInputs: eventType.customInputs.reduce(
|
||||
@@ -244,8 +258,8 @@ const BookingPage = ({
|
||||
|
||||
const customInputType = booking.customInputs;
|
||||
return {
|
||||
name: primaryAttendee.name || "",
|
||||
email: primaryAttendee.email || "",
|
||||
name: defaultUserValues.name,
|
||||
email: defaultUserValues.email || "",
|
||||
guests: guestListEmails,
|
||||
notes: booking.description || "",
|
||||
rescheduleReason: "",
|
||||
@@ -428,7 +442,8 @@ const BookingPage = ({
|
||||
}
|
||||
};
|
||||
|
||||
const disableInput = !!rescheduleUid;
|
||||
// Should be disabled when rescheduleUid is present and data was found in defaultUserValues name/email fields.
|
||||
const disableInput = !!rescheduleUid && !!defaultUserValues.email && !!defaultUserValues.name;
|
||||
const disabledExceptForOwner = disableInput && !loggedInIsOwner;
|
||||
const inputClassName =
|
||||
"focus:border-brand block w-full rounded-sm border-gray-300 focus:ring-black disabled:bg-gray-200 disabled:hover:cursor-not-allowed dark:border-gray-900 dark:bg-gray-700 dark:text-white dark:selection:bg-green-500 disabled:dark:text-gray-500 text-sm";
|
||||
@@ -859,7 +874,6 @@ const BookingPage = ({
|
||||
rows={3}
|
||||
className={inputClassName}
|
||||
placeholder={t("reschedule_placeholder")}
|
||||
disabled={disabledExceptForOwner}
|
||||
/>
|
||||
) : (
|
||||
<textarea
|
||||
|
||||
@@ -30,6 +30,7 @@ import type { BufferedBusyTime } from "@calcom/types/BufferedBusyTime";
|
||||
import type { AdditionalInformation, CalendarEvent } from "@calcom/types/Calendar";
|
||||
import type { EventResult, PartialReference } from "@calcom/types/EventManager";
|
||||
|
||||
import { getSession } from "@lib/auth";
|
||||
import { HttpError } from "@lib/core/http/error";
|
||||
import { ensureArray } from "@lib/ensureArray";
|
||||
import { getEventName } from "@lib/event";
|
||||
@@ -214,6 +215,9 @@ const getEventTypesFromDB = async (eventTypeId: number) => {
|
||||
type User = Prisma.UserGetPayload<typeof userSelect>;
|
||||
|
||||
async function handler(req: NextApiRequest) {
|
||||
const session = await getSession({ req });
|
||||
const currentUser = session?.user;
|
||||
|
||||
const { recurringCount, noEmail, eventTypeSlug, eventTypeId, hasHashedBookingLink, language, ...reqBody } =
|
||||
extendedBookingCreateBody.parse(req.body);
|
||||
|
||||
@@ -459,7 +463,7 @@ async function handler(req: NextApiRequest) {
|
||||
where: {
|
||||
uid,
|
||||
status: {
|
||||
in: [BookingStatus.ACCEPTED, BookingStatus.CANCELLED],
|
||||
in: [BookingStatus.ACCEPTED, BookingStatus.CANCELLED, BookingStatus.PENDING],
|
||||
},
|
||||
},
|
||||
include: {
|
||||
@@ -513,7 +517,14 @@ async function handler(req: NextApiRequest) {
|
||||
|
||||
const dynamicEventSlugRef = !eventTypeId ? eventTypeSlug : null;
|
||||
const dynamicGroupSlugRef = !eventTypeId ? (reqBody.user as string).toLowerCase() : null;
|
||||
const isConfirmedByDefault = (!eventType.requiresConfirmation && !eventType.price) || !!rescheduleUid;
|
||||
|
||||
// If the user is not the owner of the event, new booking should be always pending.
|
||||
// Otherwise, an owner rescheduling should be always accepted.
|
||||
const userReschedulingIsOwner = currentUser
|
||||
? originalRescheduledBooking?.user?.id === currentUser.id
|
||||
: false;
|
||||
const isConfirmedByDefault =
|
||||
(!eventType.requiresConfirmation && !eventType.price) || userReschedulingIsOwner;
|
||||
const newBookingData: Prisma.BookingCreateInput = {
|
||||
uid,
|
||||
title: evt.title,
|
||||
|
||||
@@ -41,6 +41,19 @@ export const createUsersFixture = (page: Page, workerInfo: WorkerInfo) => {
|
||||
price: 1000,
|
||||
},
|
||||
});
|
||||
await prisma.eventType.create({
|
||||
data: {
|
||||
users: {
|
||||
connect: {
|
||||
id: _user.id,
|
||||
},
|
||||
},
|
||||
title: "Opt in",
|
||||
slug: "opt-in",
|
||||
requiresConfirmation: true,
|
||||
length: 30,
|
||||
},
|
||||
});
|
||||
const user = await prisma.user.findUnique({
|
||||
rejectOnNotFound: true,
|
||||
where: { id: _user.id },
|
||||
|
||||
@@ -88,7 +88,6 @@ test.describe("Reschedule Tests", async () => {
|
||||
|
||||
await expect(page.locator('[name="name"]')).toBeDisabled();
|
||||
await expect(page.locator('[name="email"]')).toBeDisabled();
|
||||
await expect(page.locator('[name="rescheduleReason"]')).toBeDisabled();
|
||||
|
||||
await page.locator('[data-testid="confirm-reschedule-button"]').click();
|
||||
|
||||
@@ -155,4 +154,45 @@ test.describe("Reschedule Tests", async () => {
|
||||
|
||||
await payment.delete();
|
||||
});
|
||||
|
||||
test("Opt in event should be PENDING when rescheduled by USER", async ({ page, users, bookings }) => {
|
||||
const user = await users.create();
|
||||
const eventType = user.eventTypes.find((e) => e.slug === "opt-in")!;
|
||||
const booking = await bookings.create(user.id, user.username, eventType.id, {
|
||||
status: BookingStatus.ACCEPTED,
|
||||
});
|
||||
|
||||
await page.goto(`/${user.username}/${eventType.slug}?rescheduleUid=${booking.uid}`);
|
||||
|
||||
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||
|
||||
await page.locator('[data-testid="confirm-reschedule-button"]').click();
|
||||
|
||||
await expect(page).toHaveURL(/.*success/);
|
||||
|
||||
const newBooking = await prisma.booking.findFirst({ where: { fromReschedule: booking?.uid } });
|
||||
expect(newBooking).not.toBeNull();
|
||||
expect(newBooking?.status).toBe(BookingStatus.PENDING);
|
||||
});
|
||||
|
||||
test("Opt in event should be ACCEPTED when rescheduled by OWNER", async ({ page, users, bookings }) => {
|
||||
const user = await users.create();
|
||||
const eventType = user.eventTypes.find((e) => e.slug === "opt-in")!;
|
||||
const booking = await bookings.create(user.id, user.username, eventType.id, {
|
||||
status: BookingStatus.ACCEPTED,
|
||||
});
|
||||
await user.login();
|
||||
|
||||
await page.goto(`/${user.username}/${eventType.slug}?rescheduleUid=${booking.uid}`);
|
||||
|
||||
await selectFirstAvailableTimeSlotNextMonth(page);
|
||||
|
||||
await page.locator('[data-testid="confirm-reschedule-button"]').click();
|
||||
|
||||
await expect(page).toHaveURL(/.*success/);
|
||||
|
||||
const newBooking = await prisma.booking.findFirst({ where: { fromReschedule: booking?.uid } });
|
||||
expect(newBooking).not.toBeNull();
|
||||
expect(newBooking?.status).toBe(BookingStatus.ACCEPTED);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -314,7 +314,7 @@ export class CalendarEventBuilder implements ICalendarEventBuilder {
|
||||
|
||||
const queryParams = new URLSearchParams();
|
||||
queryParams.set("rescheduleUid", `${booking.uid}`);
|
||||
slug = `${slug}?${queryParams.toString()}`;
|
||||
slug = `${slug}`;
|
||||
const rescheduleLink = `${WEBAPP_URL}/${slug}?${queryParams.toString()}`;
|
||||
this.rescheduleLink = rescheduleLink;
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user