fix: Broken reschedule page for migrated user's booking through request-reschedule (#16068)
Co-authored-by: Syed Ali Shahbaz <52925846+alishaz-polymath@users.noreply.github.com> Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com> Co-authored-by: Peer Richelsen <peeroke@gmail.com> Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com> Co-authored-by: zomars <zomars@me.com>
This commit is contained in:
8 files changed
+62
-52
No files matched your search
@@ -16,6 +16,16 @@ 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);
|
||||
|
||||
@@ -23,13 +33,12 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
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,
|
||||
@@ -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}`,
|
||||
|
||||
@@ -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;
|
||||
const log = logger.getSubLogger({ prefix: ["[bookingScenario]"] });
|
||||
|
||||
type InputWebhook = {
|
||||
|
||||
@@ -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.`,
|
||||
actual: actualEmailContent,
|
||||
expected: expectedEmailContent,
|
||||
};
|
||||
@@ -818,8 +817,6 @@ export function expectBookingRequestRescheduledEmails({
|
||||
loggedInUser,
|
||||
booker,
|
||||
booking,
|
||||
bookNewTimePath,
|
||||
organizer,
|
||||
}: {
|
||||
emails: Fixtures["emails"];
|
||||
organizer: ReturnType<typeof getOrganizer>;
|
||||
@@ -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",
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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";
|
||||
@@ -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,
|
||||
allowRescheduleForCancelledBooking,
|
||||
});
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
throw new Error(`buildRescheduleLink.error: ${error.message}`);
|
||||
|
||||
@@ -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();
|
||||
@@ -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 })
|
||||
@@ -65,7 +72,7 @@ export class CalendarEventDirector {
|
||||
this.builder.setUId(this.existingBooking.uid);
|
||||
this.builder.setCancellationReason(this.cancellationReason);
|
||||
this.builder.setDescription(this.existingBooking.description);
|
||||
await this.builder.buildRescheduleLink(this.existingBooking);
|
||||
await this.builder.buildRescheduleLink();
|
||||
} else {
|
||||
throw new Error("buildWithoutEventTypeForRescheduleEmail.missing.params.required");
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -252,7 +252,15 @@ 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);
|
||||
|
||||
@@ -260,13 +268,15 @@ export const getRescheduleLink = (calEvent: CalendarEvent, attendee?: Person): s
|
||||
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 = (
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user