feat: add setting to allow booking through a reschedule link (#21652)

* feat: add setting to disable rescheduling cancelled bookings

Co-Authored-By: hariom@cal.com <hariom@cal.com>

* fix: resolve type errors for disableReschedulingCancelledBookings field

Co-Authored-By: hariom@cal.com <hariom@cal.com>

* fix: update test expectations and builder to include disableReschedulingCancelledBookings field

Co-Authored-By: hariom@cal.com <hariom@cal.com>

* fix: add disableReschedulingCancelledBookings field to managed event types

Co-Authored-By: hariom@cal.com <hariom@cal.com>

* fix: remove duplicate disableReschedulingCancelledBookings property in test

Co-Authored-By: hariom@cal.com <hariom@cal.com>

* fix: change default value to true for disableReschedulingCancelledBookings

Co-Authored-By: hariom@cal.com <hariom@cal.com>

* fix: update managed event types to use true as default for disableReschedulingCancelledBookings

Co-Authored-By: hariom@cal.com <hariom@cal.com>

* test: add comprehensive tests for disableReschedulingCancelledBookings feature

Co-Authored-By: hariom@cal.com <hariom@cal.com>

* update and remove unnecesarry test

* update e2e test

* Update reschedule.e2e.ts

* update

* fix

* Reverse the meaning of column

* Simpify logic of rescheduling redirects

* fix test

* revert

---------

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Co-authored-by: hariom@cal.com <hariom@cal.com>
Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
Co-authored-by: unknown <adhabal2002@gmail.com>
Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
This commit is contained in:
authored and GitHub committed 2025-06-07 22:30:54 +02:00
1 parent 55a80ba253
commit 11bddbb71d
16 files changed
+132 -10

No files matched your search

@@ -60,6 +60,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
slug: true,
allowReschedulingPastBookings: true,
disableRescheduling: true,
allowReschedulingCancelledBookings: true,
team: {
select: {
parentId: true,
@@ -110,18 +111,31 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
profileEnrichedBookingUser: enrichedBookingUser,
});
const isForcedRescheduleForCancelledBooking = allowRescheduleForCancelledBooking;
// If booking is already REJECTED, we can't reschedule this booking. Take the user to the booking page which would show it's correct status and other details.
// If the booking is CANCELLED and allowRescheduleForCancelledBooking is false, we redirect the user to the original event link.
// A booking that has been rescheduled to a new booking will also have a status of CANCELLED
const isDisabledRescheduling = booking.eventType?.disableRescheduling;
if (
isDisabledRescheduling ||
(!allowRescheduleForCancelledBooking &&
(booking.status === BookingStatus.CANCELLED || booking.status === BookingStatus.REJECTED))
) {
// This comes from query param and thus is considered forced
const canRescheduleCancelledBooking =
isForcedRescheduleForCancelledBooking || booking.eventType?.allowReschedulingCancelledBookings;
const isNonRescheduleableBooking =
booking.status === BookingStatus.CANCELLED || booking.status === BookingStatus.REJECTED;
if (isDisabledRescheduling) {
return {
redirect: {
destination: booking.status === BookingStatus.CANCELLED ? eventUrl : `/booking/${uid}`,
destination: `/booking/${uid}`,
permanent: false,
},
};
}
if (isNonRescheduleableBooking) {
const canReschedule = booking.status === BookingStatus.CANCELLED && canRescheduleCancelledBooking;
return {
redirect: {
destination: canReschedule ? eventUrl : `/booking/${uid}`,
permanent: false,
},
};
@@ -71,7 +71,11 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
let booking: GetBookingType | null = null;
if (rescheduleUid) {
booking = await getBookingForReschedule(`${rescheduleUid}`, session?.user?.id);
if (booking?.status === BookingStatus.CANCELLED && !allowRescheduleForCancelledBooking) {
if (
booking?.status === BookingStatus.CANCELLED &&
!allowRescheduleForCancelledBooking &&
!eventData.allowReschedulingCancelledBookings
) {
return {
redirect: {
permanent: false,
@@ -212,6 +216,7 @@ const getTeamWithEventsData = async (
hidden: true,
disableCancelling: true,
disableRescheduling: true,
allowReschedulingCancelledBookings: true,
interfaceLanguage: true,
hosts: {
take: 3,
+2 -2
View File
@@ -252,8 +252,8 @@ test.describe("pro user", () => {
await page.goto(`/reschedule/${bookingCancelledId}`);
// Should be redirected to the original event link
await expect(page).toHaveURL(new RegExp(`/${pro.username}/${eventSlug}`));
expect(page.url()).not.toContain("rescheduleUid");
await expect(cancelledHeadline).toBeVisible();
});
test("can book an event that requires confirmation and then that booking can be accepted by organizer", async ({
+56
View File
@@ -389,6 +389,62 @@ test.describe("Reschedule Tests", async () => {
// It is tested in teams.e2e.ts
});
test("Should redirect to cancelled page when allowReschedulingCancelledBookings is false (default)", async ({
page,
users,
bookings,
}) => {
const user = await users.create();
const eventType = user.eventTypes[0];
await prisma.eventType.update({
where: {
id: eventType.id,
},
data: {
allowReschedulingCancelledBookings: false,
},
});
const booking = await bookings.create(user.id, user.username, eventType.id, {
status: BookingStatus.CANCELLED,
});
await page.goto(`/reschedule/${booking.uid}`);
expect(page.url()).not.toContain("rescheduleUid");
await expect(page.locator('[data-testid="cancelled-headline"]')).toBeVisible();
});
test("Should allow rescheduling when allowReschedulingCancelledBookings is true", async ({
page,
users,
bookings,
}) => {
const user = await users.create();
const eventType = user.eventTypes[0];
await prisma.eventType.update({
where: {
id: eventType.id,
},
data: {
allowReschedulingCancelledBookings: true,
},
});
const booking = await bookings.create(user.id, user.username, eventType.id, {
status: BookingStatus.CANCELLED,
});
await page.goto(`/reschedule/${booking.uid}`);
await selectFirstAvailableTimeSlotNextMonth(page);
await bookTimeSlot(page);
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
});
test.describe("Organization", () => {
test("Booking should be rescheduleable for a user that was moved to an organization through org domain", async ({
users,
@@ -1171,6 +1171,8 @@
"confirm_delete_api_key": "Revoke this API key",
"disable_rescheduling": "Disable Rescheduling",
"description_disable_rescheduling": "Guests can no longer reschedule the event with calendar invite or email",
"allow_rescheduling_cancelled_bookings": "Allow booking through reschedule link",
"description_allow_rescheduling_cancelled_bookings": "When enabled, users will be able to create a new booking when trying to reschedule a cancelled booking",
"disable_cancelling": "Disable Cancelling",
"description_disable_cancelling": "Guests can no longer cancel the event with calendar invite or email",
"revoke_api_key": "Revoke API key",
@@ -60,7 +60,9 @@ async function processReschedule({
booking === null ||
!booking.eventTypeId ||
(booking?.eventTypeId === props.eventData?.id &&
(booking.status !== BookingStatus.CANCELLED || allowRescheduleForCancelledBooking))
(booking.status !== BookingStatus.CANCELLED ||
allowRescheduleForCancelledBooking ||
!!(props.eventData as any)?.allowReschedulingCancelledBookings))
) {
props.booking = booking;
props.rescheduleUid = Array.isArray(rescheduleUid) ? rescheduleUid[0] : rescheduleUid;
@@ -147,6 +147,7 @@ describe("handleChildrenEventTypes", () => {
userId: 4,
rrSegmentQueryValue: undefined,
assignRRMembersUsingSegment: false,
allowReschedulingCancelledBookings: false,
},
});
expect(result.newUserIds).toEqual([4]);
@@ -206,6 +207,7 @@ describe("handleChildrenEventTypes", () => {
deleteMany: {},
},
instantMeetingScheduleId: undefined,
allowReschedulingCancelledBookings: false,
},
where: {
userId_parentId: {
@@ -315,6 +317,7 @@ describe("handleChildrenEventTypes", () => {
workflows: undefined,
rrSegmentQueryValue: undefined,
assignRRMembersUsingSegment: false,
allowReschedulingCancelledBookings: false,
},
});
expect(result.newUserIds).toEqual([4]);
@@ -371,6 +374,7 @@ describe("handleChildrenEventTypes", () => {
},
lockTimeZoneToggleOnBookingPage: false,
requiresBookerEmailVerification: false,
allowReschedulingCancelledBookings: false,
},
where: {
userId_parentId: {
@@ -476,6 +480,7 @@ describe("handleChildrenEventTypes", () => {
rrSegmentQueryValue: undefined,
assignRRMembersUsingSegment: false,
useEventLevelSelectedCalendars: false,
allowReschedulingCancelledBookings: false,
},
});
const { profileId, rrSegmentQueryValue, ...rest } = evType;