fix: Disable cancel button for past bookings (#22667)
* fix: Remove cancel button for past bookings * fix types and few improvements * add proper tests for backend changes * chore * Update [bookingUid].tsx * Update bookings-single-view.tsx * remove comment * Update tests --------- Co-authored-by: Kartik Saini <41051387+kart1ka@users.noreply.github.com> Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
This commit is contained in:
co-authored by
Kartik Saini
Anik Dhabal Babu
parent
216fb2141b
commit
a6101b3da9
@@ -475,6 +475,34 @@ describe("Booking Actions", () => {
|
||||
expect(isActionDisabled("cancel", context)).toBe(true);
|
||||
});
|
||||
|
||||
it("should disable cancelling all past bookings", () => {
|
||||
const pastConfirmedContext = createMockContext({
|
||||
isBookingInPast: true,
|
||||
isPending: false,
|
||||
isConfirmed: true,
|
||||
});
|
||||
|
||||
const pastPendingContext = createMockContext({
|
||||
isBookingInPast: true,
|
||||
isPending: true,
|
||||
isConfirmed: false,
|
||||
});
|
||||
|
||||
// Current implementation blocks ALL past bookings
|
||||
expect(isActionDisabled("cancel", pastConfirmedContext)).toBe(true);
|
||||
expect(isActionDisabled("cancel", pastPendingContext)).toBe(true);
|
||||
});
|
||||
|
||||
it("should allow cancelling future bookings when cancelling is not disabled", () => {
|
||||
const futureContext = createMockContext({
|
||||
isBookingInPast: false,
|
||||
isPending: true,
|
||||
isConfirmed: false,
|
||||
});
|
||||
|
||||
expect(isActionDisabled("cancel", futureContext)).toBe(false);
|
||||
});
|
||||
|
||||
it("should disable video actions for non-past bookings", () => {
|
||||
const context = createMockContext({ isBookingInPast: false });
|
||||
|
||||
|
||||
@@ -210,7 +210,7 @@ export function isActionDisabled(actionId: string, context: BookingActionContext
|
||||
case "reschedule_request":
|
||||
return (isBookingInPast && !booking.eventType.allowReschedulingPastBookings) || isDisabledRescheduling;
|
||||
case "cancel":
|
||||
return isDisabledCancelling || (isBookingInPast && isPending && !isConfirmed);
|
||||
return isDisabledCancelling || isBookingInPast;
|
||||
case "view_recordings":
|
||||
return !(isBookingInPast && booking.status === BookingStatus.ACCEPTED && context.isCalVideoLocation);
|
||||
case "meeting_session_details":
|
||||
|
||||
@@ -839,13 +839,13 @@ export default function Success(props: PageProps) {
|
||||
{t("reschedule")}
|
||||
</Link>
|
||||
</span>
|
||||
{canCancelAndReschedule && (
|
||||
{!isBookingInPast && canCancel && (
|
||||
<span className="mx-2">{t("or_lowercase")}</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{canCancel && (
|
||||
{!isBookingInPast && canCancel && (
|
||||
<button
|
||||
data-testid="cancel"
|
||||
className={classNames(
|
||||
|
||||
@@ -766,3 +766,31 @@ test.describe("GTM container", () => {
|
||||
expect(scriptContent).toContain("googletagmanager");
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Past booking cancellation", () => {
|
||||
test("Cancel button should be hidden for past bookings", async ({ page, users, bookings }) => {
|
||||
const user = await users.create({
|
||||
name: "Test User",
|
||||
});
|
||||
|
||||
await user.apiLogin();
|
||||
|
||||
const pastDate = new Date();
|
||||
pastDate.setDate(pastDate.getDate() - 1);
|
||||
const endDate = new Date(pastDate.getTime() + 30 * 60 * 1000);
|
||||
|
||||
const booking = await bookings.create(user.id, user.username, user.eventTypes[0].id, {
|
||||
title: "Past Meeting",
|
||||
startTime: pastDate,
|
||||
endTime: endDate,
|
||||
status: "ACCEPTED",
|
||||
});
|
||||
|
||||
await page.goto("/bookings/past");
|
||||
await page.locator('[data-testid="booking-actions-dropdown"]').nth(0).click();
|
||||
await expect(page.locator('[data-testid="cancel"]')).toBeDisabled();
|
||||
|
||||
await page.goto(`/booking/${booking.uid}`);
|
||||
await expect(page.locator('[data-testid="cancel"]')).toBeHidden();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -120,6 +120,13 @@ async function handler(input: CancelBookingInput) {
|
||||
});
|
||||
}
|
||||
|
||||
if (bookingToDelete.endTime && new Date() > new Date(bookingToDelete.endTime)) {
|
||||
throw new HttpError({
|
||||
statusCode: 400,
|
||||
message: "Cannot cancel a booking that has already ended",
|
||||
});
|
||||
}
|
||||
|
||||
// If the booking is a seated event and there is no seatReferenceUid we should validate that logged in user is host
|
||||
if (bookingToDelete.eventType?.seatsPerTimeSlot && !seatReferenceUid) {
|
||||
const userIsHost = bookingToDelete.eventType.hosts.find((host) => {
|
||||
|
||||
@@ -420,4 +420,73 @@ describe("Cancel Booking", () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test("Should block cancelling past bookings", async () => {
|
||||
const handleCancelBooking = (await import("@calcom/features/bookings/lib/handleCancelBooking")).default;
|
||||
|
||||
const booker = getBooker({
|
||||
email: "booker@example.com",
|
||||
name: "Booker",
|
||||
});
|
||||
|
||||
const organizer = getOrganizer({
|
||||
name: "Organizer",
|
||||
email: "organizer@example.com",
|
||||
id: 101,
|
||||
schedules: [TestData.schedules.IstWorkHours],
|
||||
credentials: [getGoogleCalendarCredential()],
|
||||
selectedCalendars: [TestData.selectedCalendars.google],
|
||||
});
|
||||
|
||||
const uidOfBookingToBeCancelled = "past-booking";
|
||||
const idOfBookingToBeCancelled = 3040;
|
||||
const { dateString: minus1DateString } = getDate({ dateIncrement: -1 });
|
||||
|
||||
await createBookingScenario(
|
||||
getScenarioData({
|
||||
eventTypes: [
|
||||
{
|
||||
id: 1,
|
||||
slotInterval: 30,
|
||||
length: 30,
|
||||
users: [
|
||||
{
|
||||
id: 101,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
bookings: [
|
||||
{
|
||||
id: idOfBookingToBeCancelled,
|
||||
uid: uidOfBookingToBeCancelled,
|
||||
eventTypeId: 1,
|
||||
userId: 101,
|
||||
responses: {
|
||||
email: booker.email,
|
||||
name: booker.name,
|
||||
location: { optionValue: "", value: BookingLocations.CalVideo },
|
||||
},
|
||||
status: BookingStatus.ACCEPTED,
|
||||
startTime: `${minus1DateString}T05:00:00.000Z`,
|
||||
endTime: `${minus1DateString}T05:30:00.000Z`,
|
||||
},
|
||||
],
|
||||
organizer,
|
||||
apps: [TestData.apps["daily-video"]],
|
||||
})
|
||||
);
|
||||
|
||||
// This should throw an error with current implementation
|
||||
await expect(
|
||||
handleCancelBooking({
|
||||
bookingData: {
|
||||
id: idOfBookingToBeCancelled,
|
||||
uid: uidOfBookingToBeCancelled,
|
||||
cancelledBy: organizer.email,
|
||||
cancellationReason: "Testing past booking cancellation",
|
||||
},
|
||||
})
|
||||
).rejects.toThrow("Cannot cancel a booking that has already ended");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user