Skip to content

Commit 11bddbb

Browse files
devin-ai-integration[bot]hariom@cal.comanikdhabalhariombalhara
authored
feat: add setting to allow booking through a reschedule link (calcom#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>
1 parent 55a80ba commit 11bddbb

16 files changed

Lines changed: 132 additions & 10 deletions

File tree

apps/web/lib/reschedule/[uid]/getServerSideProps.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
6060
slug: true,
6161
allowReschedulingPastBookings: true,
6262
disableRescheduling: true,
63+
allowReschedulingCancelledBookings: true,
6364
team: {
6465
select: {
6566
parentId: true,
@@ -110,18 +111,31 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
110111
profileEnrichedBookingUser: enrichedBookingUser,
111112
});
112113

114+
const isForcedRescheduleForCancelledBooking = allowRescheduleForCancelledBooking;
113115
// 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.
114116
// If the booking is CANCELLED and allowRescheduleForCancelledBooking is false, we redirect the user to the original event link.
115117
// A booking that has been rescheduled to a new booking will also have a status of CANCELLED
116118
const isDisabledRescheduling = booking.eventType?.disableRescheduling;
117-
if (
118-
isDisabledRescheduling ||
119-
(!allowRescheduleForCancelledBooking &&
120-
(booking.status === BookingStatus.CANCELLED || booking.status === BookingStatus.REJECTED))
121-
) {
119+
// This comes from query param and thus is considered forced
120+
const canRescheduleCancelledBooking =
121+
isForcedRescheduleForCancelledBooking || booking.eventType?.allowReschedulingCancelledBookings;
122+
const isNonRescheduleableBooking =
123+
booking.status === BookingStatus.CANCELLED || booking.status === BookingStatus.REJECTED;
124+
125+
if (isDisabledRescheduling) {
126+
return {
127+
redirect: {
128+
destination: `/booking/${uid}`,
129+
permanent: false,
130+
},
131+
};
132+
}
133+
134+
if (isNonRescheduleableBooking) {
135+
const canReschedule = booking.status === BookingStatus.CANCELLED && canRescheduleCancelledBooking;
122136
return {
123137
redirect: {
124-
destination: booking.status === BookingStatus.CANCELLED ? eventUrl : `/booking/${uid}`,
138+
destination: canReschedule ? eventUrl : `/booking/${uid}`,
125139
permanent: false,
126140
},
127141
};

apps/web/lib/team/[slug]/[type]/getServerSideProps.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,11 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
7171
let booking: GetBookingType | null = null;
7272
if (rescheduleUid) {
7373
booking = await getBookingForReschedule(`${rescheduleUid}`, session?.user?.id);
74-
if (booking?.status === BookingStatus.CANCELLED && !allowRescheduleForCancelledBooking) {
74+
if (
75+
booking?.status === BookingStatus.CANCELLED &&
76+
!allowRescheduleForCancelledBooking &&
77+
!eventData.allowReschedulingCancelledBookings
78+
) {
7579
return {
7680
redirect: {
7781
permanent: false,
@@ -212,6 +216,7 @@ const getTeamWithEventsData = async (
212216
hidden: true,
213217
disableCancelling: true,
214218
disableRescheduling: true,
219+
allowReschedulingCancelledBookings: true,
215220
interfaceLanguage: true,
216221
hosts: {
217222
take: 3,

apps/web/playwright/booking-pages.e2e.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -252,8 +252,8 @@ test.describe("pro user", () => {
252252

253253
await page.goto(`/reschedule/${bookingCancelledId}`);
254254

255-
// Should be redirected to the original event link
256-
await expect(page).toHaveURL(new RegExp(`/${pro.username}/${eventSlug}`));
255+
expect(page.url()).not.toContain("rescheduleUid");
256+
await expect(cancelledHeadline).toBeVisible();
257257
});
258258

259259
test("can book an event that requires confirmation and then that booking can be accepted by organizer", async ({

apps/web/playwright/reschedule.e2e.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,62 @@ test.describe("Reschedule Tests", async () => {
389389
// It is tested in teams.e2e.ts
390390
});
391391

392+
test("Should redirect to cancelled page when allowReschedulingCancelledBookings is false (default)", async ({
393+
page,
394+
users,
395+
bookings,
396+
}) => {
397+
const user = await users.create();
398+
const eventType = user.eventTypes[0];
399+
400+
await prisma.eventType.update({
401+
where: {
402+
id: eventType.id,
403+
},
404+
data: {
405+
allowReschedulingCancelledBookings: false,
406+
},
407+
});
408+
409+
const booking = await bookings.create(user.id, user.username, eventType.id, {
410+
status: BookingStatus.CANCELLED,
411+
});
412+
413+
await page.goto(`/reschedule/${booking.uid}`);
414+
415+
expect(page.url()).not.toContain("rescheduleUid");
416+
await expect(page.locator('[data-testid="cancelled-headline"]')).toBeVisible();
417+
});
418+
419+
test("Should allow rescheduling when allowReschedulingCancelledBookings is true", async ({
420+
page,
421+
users,
422+
bookings,
423+
}) => {
424+
const user = await users.create();
425+
const eventType = user.eventTypes[0];
426+
427+
await prisma.eventType.update({
428+
where: {
429+
id: eventType.id,
430+
},
431+
data: {
432+
allowReschedulingCancelledBookings: true,
433+
},
434+
});
435+
436+
const booking = await bookings.create(user.id, user.username, eventType.id, {
437+
status: BookingStatus.CANCELLED,
438+
});
439+
440+
await page.goto(`/reschedule/${booking.uid}`);
441+
442+
await selectFirstAvailableTimeSlotNextMonth(page);
443+
await bookTimeSlot(page);
444+
445+
await expect(page.locator("[data-testid=success-page]")).toBeVisible();
446+
});
447+
392448
test.describe("Organization", () => {
393449
test("Booking should be rescheduleable for a user that was moved to an organization through org domain", async ({
394450
users,

apps/web/public/static/locales/en/common.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1171,6 +1171,8 @@
11711171
"confirm_delete_api_key": "Revoke this API key",
11721172
"disable_rescheduling": "Disable Rescheduling",
11731173
"description_disable_rescheduling": "Guests can no longer reschedule the event with calendar invite or email",
1174+
"allow_rescheduling_cancelled_bookings": "Allow booking through reschedule link",
1175+
"description_allow_rescheduling_cancelled_bookings": "When enabled, users will be able to create a new booking when trying to reschedule a cancelled booking",
11741176
"disable_cancelling": "Disable Cancelling",
11751177
"description_disable_cancelling": "Guests can no longer cancel the event with calendar invite or email",
11761178
"revoke_api_key": "Revoke API key",

apps/web/server/lib/[user]/[type]/getServerSideProps.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,9 @@ async function processReschedule({
6060
booking === null ||
6161
!booking.eventTypeId ||
6262
(booking?.eventTypeId === props.eventData?.id &&
63-
(booking.status !== BookingStatus.CANCELLED || allowRescheduleForCancelledBooking))
63+
(booking.status !== BookingStatus.CANCELLED ||
64+
allowRescheduleForCancelledBooking ||
65+
!!(props.eventData as any)?.allowReschedulingCancelledBookings))
6466
) {
6567
props.booking = booking;
6668
props.rescheduleUid = Array.isArray(rescheduleUid) ? rescheduleUid[0] : rescheduleUid;

apps/web/test/lib/handleChildrenEventTypes.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,7 @@ describe("handleChildrenEventTypes", () => {
147147
userId: 4,
148148
rrSegmentQueryValue: undefined,
149149
assignRRMembersUsingSegment: false,
150+
allowReschedulingCancelledBookings: false,
150151
},
151152
});
152153
expect(result.newUserIds).toEqual([4]);
@@ -206,6 +207,7 @@ describe("handleChildrenEventTypes", () => {
206207
deleteMany: {},
207208
},
208209
instantMeetingScheduleId: undefined,
210+
allowReschedulingCancelledBookings: false,
209211
},
210212
where: {
211213
userId_parentId: {
@@ -315,6 +317,7 @@ describe("handleChildrenEventTypes", () => {
315317
workflows: undefined,
316318
rrSegmentQueryValue: undefined,
317319
assignRRMembersUsingSegment: false,
320+
allowReschedulingCancelledBookings: false,
318321
},
319322
});
320323
expect(result.newUserIds).toEqual([4]);
@@ -371,6 +374,7 @@ describe("handleChildrenEventTypes", () => {
371374
},
372375
lockTimeZoneToggleOnBookingPage: false,
373376
requiresBookerEmailVerification: false,
377+
allowReschedulingCancelledBookings: false,
374378
},
375379
where: {
376380
userId_parentId: {
@@ -476,6 +480,7 @@ describe("handleChildrenEventTypes", () => {
476480
rrSegmentQueryValue: undefined,
477481
assignRRMembersUsingSegment: false,
478482
useEventLevelSelectedCalendars: false,
483+
allowReschedulingCancelledBookings: false,
479484
},
480485
});
481486
const { profileId, rrSegmentQueryValue, ...rest } = evType;

packages/features/ee/managed-event-types/lib/handleChildrenEventTypes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,7 @@ export default async function handleChildrenEventTypes({
187187
rrSegmentQueryValue: undefined,
188188
assignRRMembersUsingSegment: false,
189189
useEventLevelSelectedCalendars: false,
190+
allowReschedulingCancelledBookings: managedEventTypeValues.allowReschedulingCancelledBookings ?? false,
190191
},
191192
});
192193
})
@@ -259,6 +260,7 @@ export default async function handleChildrenEventTypes({
259260
: {
260261
deleteMany: {},
261262
},
263+
allowReschedulingCancelledBookings: managedEventTypeValues.allowReschedulingCancelledBookings ?? false,
262264
metadata: {
263265
...(eventType.metadata as Prisma.JsonObject),
264266
...(metadata?.multipleDuration && "length" in unlockedFieldProps

packages/features/eventtypes/components/tabs/advanced/EventAdvancedTab.tsx

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,9 @@ export const EventAdvancedTab = ({
497497

498498
const disableCancellingLocked = shouldLockDisableProps("disableCancelling");
499499
const disableReschedulingLocked = shouldLockDisableProps("disableRescheduling");
500+
const allowReschedulingCancelledBookingsLocked = shouldLockDisableProps(
501+
"allowReschedulingCancelledBookings"
502+
);
500503

501504
const { isLocked, ...eventNameLocked } = shouldLockDisableProps("eventName");
502505

@@ -508,6 +511,10 @@ export const EventAdvancedTab = ({
508511

509512
const [disableRescheduling, setDisableRescheduling] = useState(eventType.disableRescheduling || false);
510513

514+
const [allowReschedulingCancelledBookings, setallowReschedulingCancelledBookings] = useState(
515+
eventType.allowReschedulingCancelledBookings ?? false
516+
);
517+
511518
const closeEventNameTip = () => setShowEventNameTip(false);
512519

513520
const [isEventTypeColorChecked, setIsEventTypeColorChecked] = useState(!!eventType.eventTypeColor);
@@ -1028,6 +1035,26 @@ export const EventAdvancedTab = ({
10281035
/>
10291036
)}
10301037
/>
1038+
1039+
<Controller
1040+
name="allowReschedulingCancelledBookings"
1041+
render={({ field: { onChange } }) => (
1042+
<SettingsToggle
1043+
labelClassName="text-sm"
1044+
toggleSwitchAtTheEnd={true}
1045+
switchContainerClassName="border-subtle rounded-lg border py-6 px-4 sm:px-6"
1046+
title={t("allow_rescheduling_cancelled_bookings")}
1047+
data-testid="allow-rescheduling-cancelled-bookings-toggle"
1048+
{...allowReschedulingCancelledBookingsLocked}
1049+
description={t("description_allow_rescheduling_cancelled_bookings")}
1050+
checked={allowReschedulingCancelledBookings}
1051+
onCheckedChange={(val) => {
1052+
setallowReschedulingCancelledBookings(val);
1053+
onChange(val);
1054+
}}
1055+
/>
1056+
)}
1057+
/>
10311058
{!isPlatform && (
10321059
<>
10331060
<Controller

packages/features/eventtypes/lib/getPublicEvent.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ const getPublicEventSelect = (fetchAllUsers: boolean) => {
8484
seatsPerTimeSlot: true,
8585
disableCancelling: true,
8686
disableRescheduling: true,
87+
allowReschedulingCancelledBookings: true,
8788
seatsShowAvailabilityCount: true,
8889
bookingFields: true,
8990
teamId: true,
@@ -528,6 +529,7 @@ export const getPublicEvent = async (
528529
assignAllTeamMembers: event.assignAllTeamMembers,
529530
disableCancelling: event.disableCancelling,
530531
disableRescheduling: event.disableRescheduling,
532+
allowReschedulingCancelledBookings: event.allowReschedulingCancelledBookings,
531533
interfaceLanguage: event.interfaceLanguage,
532534
};
533535
};

0 commit comments

Comments
 (0)