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:
alannnc
2022-08-05 17:08:47 +00:00
committed by GitHub
co-authored by kodiakhq[bot] Bailey Pumfleet
parent 70d6c8d8e7
commit 775fe92960
6 changed files with 93 additions and 11 deletions
@@ -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
+13 -2
View File
@@ -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,
+13
View File
@@ -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 },
+41 -1
View File
@@ -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) {