Skip to content
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

fix: Broken reschedule page for migrated user's booking through request-reschedule #16068

Merged
merged 9 commits into from
Aug 29, 2024
28 changes: 20 additions & 8 deletions apps/web/pages/reschedule/[uid].tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,29 @@ export default function Type() {
return null;
}

const querySchema = z.object({
uid: z.string(),
seatReferenceUid: z.string().optional(),
rescheduledBy: z.string().optional(),
allowRescheduleForCancelledBooking: z
.string()
.transform((value) => value === "true")
.optional(),
});

export async function getServerSideProps(context: GetServerSidePropsContext) {
const session = await getServerSession(context);

const {
uid: bookingUid,
seatReferenceUid,
rescheduledBy,
} = z
.object({
uid: z.string(),
seatReferenceUid: z.string().optional(),
rescheduledBy: z.string().optional(),
})
.parse(context.query);
/**
* This is for the case of request-reschedule where the booking is cancelled
*/
allowRescheduleForCancelledBooking,
} = querySchema.parse(context.query);

const coepFlag = context.query["flag.coep"];
const { uid, seatReferenceUid: maybeSeatReferenceUid } = await maybeGetBookingUidFromSeat(
prisma,
Expand Down Expand Up @@ -90,7 +99,10 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {

// If booking is already CANCELLED or REJECTED, we can't reschedule this booking. Take the user to the booking page which would show it's correct status and other details.
// A booking that has been rescheduled to a new booking will also have a status of CANCELLED
if (booking.status === BookingStatus.CANCELLED || booking.status === BookingStatus.REJECTED) {
if (
!allowRescheduleForCancelledBooking &&
(booking.status === BookingStatus.CANCELLED || booking.status === BookingStatus.REJECTED)
) {
return {
redirect: {
destination: `/booking/${uid}`,
Expand Down
2 changes: 1 addition & 1 deletion apps/web/test/utils/bookingScenario/bookingScenario.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ import type { EventBusyDate, IntervalLimit } from "@calcom/types/Calendar";
import { getMockPaymentService } from "./MockPaymentService";
import type { getMockRequestDataForBooking } from "./getMockRequestDataForBooking";

logger.settings.minLevel = 0;
logger.settings.minLevel = 1;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove the noise in tests. Can be enabled on demand

const log = logger.getSubLogger({ prefix: ["[bookingScenario]"] });

type InputWebhook = {
Expand Down
7 changes: 2 additions & 5 deletions apps/web/test/utils/bookingScenario/expects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,10 +143,9 @@ expect.extend({

if (!isEmailContentMatched) {
logger.silly("All Emails", JSON.stringify({ numEmails: emailsToLog.length, emailsToLog }));

return {
pass: false,
message: () => `Email content ${isNot ? "is" : "is not"} matching. ${JSON.stringify(emailsToLog)}`,
message: () => `Email content ${isNot ? "is" : "is not"} matching.`,
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reduce the noise. emailToLog could be huge

actual: actualEmailContent,
expected: expectedEmailContent,
};
Expand Down Expand Up @@ -818,8 +817,6 @@ export function expectBookingRequestRescheduledEmails({
loggedInUser,
booker,
booking,
bookNewTimePath,
organizer,
}: {
emails: Fixtures["emails"];
organizer: ReturnType<typeof getOrganizer>;
Expand All @@ -840,7 +837,7 @@ export function expectBookingRequestRescheduledEmails({
subHeading: "request_reschedule_subtitle",
links: [
{
href: `${bookingUrlOrigin}${bookNewTimePath}?rescheduleUid=${booking.uid}`,
href: `${bookingUrlOrigin}/reschedule/${booking.uid}?allowRescheduleForCancelledBooking=true`,
text: "Book a new time",
},
],
Expand Down
37 changes: 10 additions & 27 deletions packages/core/builders/CalendarEvent/builder.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import type { Booking } from "@prisma/client";
import { Prisma } from "@prisma/client";
import short from "short-uuid";
import { v5 as uuidv5 } from "uuid";

import dayjs from "@calcom/dayjs";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { getRescheduleLink } from "@calcom/lib/CalEventParser";
import logger from "@calcom/lib/logger";
import { safeStringify } from "@calcom/lib/safeStringify";
import { getTranslation } from "@calcom/lib/server/i18n";
Expand Down Expand Up @@ -238,32 +237,16 @@ export class CalendarEventBuilder implements ICalendarEventBuilder {
}
}

public buildRescheduleLink(booking: Partial<Booking>, eventType?: CalendarEventBuilder["eventType"]) {
public buildRescheduleLink({
allowRescheduleForCancelledBooking = false,
}: {
allowRescheduleForCancelledBooking?: boolean;
}) {
try {
if (!booking) {
throw new Error("Parameter booking is required to build reschedule link");
}
const isTeam = !!eventType && !!eventType.teamId;
const isDynamic = booking?.dynamicEventSlugRef && booking?.dynamicGroupSlugRef;

let slug = "";
if (isTeam && eventType?.team?.slug) {
slug = `team/${eventType.team?.slug}/${eventType.slug}`;
} else if (isDynamic) {
const dynamicSlug = isDynamic ? `${booking.dynamicGroupSlugRef}/${booking.dynamicEventSlugRef}` : "";
slug = dynamicSlug;
} else if (eventType?.slug) {
slug = `${this.users[0].username}/${eventType.slug}`;
}

const queryParams = new URLSearchParams();
queryParams.set("rescheduleUid", `${booking.uid}`);
slug = `${slug}`;

const rescheduleLink = `${
this.calendarEvent.bookerUrl ?? WEBAPP_URL
}/${slug}?${queryParams.toString()}`;
this.rescheduleLink = rescheduleLink;
this.rescheduleLink = getRescheduleLink({
calEvent: this.calendarEvent,
Copy link
Member Author

@hariombalhara hariombalhara Aug 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reused existing reschedule link generator. This fixes the bug.

allowRescheduleForCancelledBooking,
});
} catch (error) {
if (error instanceof Error) {
throw new Error(`buildRescheduleLink.error: ${error.message}`);
Expand Down
11 changes: 9 additions & 2 deletions packages/core/builders/CalendarEvent/director.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,14 @@ export class CalendarEventDirector {
this.cancellationReason = reason;
}

public async buildForRescheduleEmail(): Promise<void> {
public async buildForRescheduleEmail({
allowRescheduleForCancelledBooking = false,
}: {
/**
* By default we don't want to allow reschedule for cancelled bookings.
*/
allowRescheduleForCancelledBooking?: boolean;
} = {}): Promise<void> {
if (this.existingBooking && this.existingBooking.eventTypeId && this.existingBooking.uid) {
await this.builder.buildEventObjectFromInnerClass(this.existingBooking.eventTypeId);
await this.builder.buildUsersFromInnerClass();
Expand All @@ -47,7 +54,7 @@ export class CalendarEventDirector {
this.builder.setCancellationReason(this.cancellationReason);
this.builder.setDescription(this.builder.eventType.description);
this.builder.setNotes(this.existingBooking.description);
this.builder.buildRescheduleLink(this.existingBooking, this.builder.eventType);
this.builder.buildRescheduleLink({ allowRescheduleForCancelledBooking });
log.debug(
"buildForRescheduleEmail",
safeStringify({ existingBooking: this.existingBooking, builder: this.builder })
Expand Down
2 changes: 1 addition & 1 deletion packages/emails/src/components/ManageLink.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export function ManageLink(props: { calEvent: CalendarEvent; attendee: Person })
// Guests cannot
const t = props.attendee.language.translate;
const cancelLink = getCancelLink(props.calEvent, props.attendee);
const rescheduleLink = getRescheduleLink(props.calEvent, props.attendee);
const rescheduleLink = getRescheduleLink({ calEvent: props.calEvent, attendee: props.attendee });
const bookingLink = getBookingUrl(props.calEvent);

const isOriginalAttendee = props.attendee.email === props.calEvent.attendees[0]?.email;
Expand Down
22 changes: 16 additions & 6 deletions packages/lib/CalEventParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ export const getPlatformManageLink = (calEvent: CalendarEvent, t: TFunction) =>
if (!calEvent.recurringEvent && calEvent.platformRescheduleUrl) {
res += `${calEvent.platformCancelUrl ? ` ${t("or_lowercase")} ` : ""}${t(
"reschedule"
)}: ${getRescheduleLink(calEvent)}`;
)}: ${getRescheduleLink({ calEvent })}`;
}

return res;
Expand Down Expand Up @@ -252,21 +252,31 @@ export const getPlatformRescheduleLink = (
return "";
};

export const getRescheduleLink = (calEvent: CalendarEvent, attendee?: Person): string => {
export const getRescheduleLink = ({
calEvent,
allowRescheduleForCancelledBooking = false,
attendee,
}: {
calEvent: CalendarEvent;
allowRescheduleForCancelledBooking?: boolean;
attendee?: Person;
}): string => {
const Uid = getUid(calEvent);
const seatUid = getSeatReferenceId(calEvent);

if (calEvent.platformClientId) {
return getPlatformRescheduleLink(calEvent, Uid, seatUid);
}

const rescheduleLink = new URL(`${calEvent.bookerUrl ?? WEBAPP_URL}/reschedule/${seatUid ? seatUid : Uid}`);

const url = new URL(`${calEvent.bookerUrl ?? WEBAPP_URL}/reschedule/${seatUid ? seatUid : Uid}`);
if (allowRescheduleForCancelledBooking) {
url.searchParams.append("allowRescheduleForCancelledBooking", "true");
}
if (attendee?.email) {
rescheduleLink.searchParams.append("rescheduledBy", attendee.email);
url.searchParams.append("rescheduledBy", attendee.email);
}

return rescheduleLink.toString();
return url.toString();
};

export const getRichDescription = (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,8 @@ export const requestRescheduleHandler = async ({ ctx, input }: RequestReschedule
director.setExistingBooking(bookingToReschedule);
cancellationReason && director.setCancellationReason(cancellationReason);
if (Object.keys(event).length) {
await director.buildForRescheduleEmail();
// Request Reschedule flow first cancels the booking and then reschedule email is sent. So, we need to allow reschedule for cancelled booking
await director.buildForRescheduleEmail({ allowRescheduleForCancelledBooking: true });
} else {
await director.buildWithoutEventTypeForRescheduleEmail();
}
Expand Down
Loading