diff --git a/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts b/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts index 7e21b011ff..e02e617577 100644 --- a/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts +++ b/apps/api/v2/src/ee/bookings/2024-04-15/controllers/bookings.controller.ts @@ -232,6 +232,15 @@ export class BookingsController_2024_04_15 { } if (bookingUid) { + const { bookingInfo } = await getBookingInfo(bookingUid); + if (!bookingInfo) { + throw new NotFoundException(`Booking with UID=${bookingUid} does not exist.`); + } + if (bookingInfo.status === "CANCELLED") { + throw new BadRequestException( + `Can't cancel booking with uid=${bookingUid} because it has been cancelled already. Please provide uid of a booking that is not cancelled.` + ); + } try { req.body.uid = bookingUid; const bookingRequest = await this.createNextApiBookingRequest(req, oAuthClientId, undefined, isEmbed); diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/user-bookings.e2e-spec.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/user-bookings.e2e-spec.ts index e57b2a93f4..266eafca18 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/user-bookings.e2e-spec.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/user-bookings.e2e-spec.ts @@ -2553,6 +2553,59 @@ describe("Bookings Endpoints 2024-08-13", () => { }); }); + describe("cant't cancel already cancelled booking", () => { + it("should not be able to cancel alraedy cancelled booking", async () => { + const cancelledBooking = await bookingsRepositoryFixture.create({ + status: "CANCELLED", + user: { + connect: { + id: user.id, + }, + }, + startTime: new Date(Date.UTC(2050, 0, 8, 13, 0, 0)), + endTime: new Date(Date.UTC(2050, 0, 8, 14, 0, 0)), + title: "peer coding lets goo", + uid: `cancelled-booking-${randomString()}`, + eventType: { + connect: { + id: eventTypeId, + }, + }, + location: "integrations:daily", + customInputs: {}, + metadata: {}, + responses: { + name: "Oldie", + email: "oldie@gmail.com", + }, + attendees: { + create: { + email: "oldie@gmail.com", + name: "Oldie", + locale: "lv", + timeZone: "Europe/Rome", + }, + }, + }); + + const body: CancelBookingInput_2024_08_13 = { + cancellationReason: "Going on a vacation", + }; + + const response = await request(app.getHttpServer()) + .post(`/v2/bookings/${cancelledBooking.uid}/cancel`) + .send(body) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .set(X_CAL_CLIENT_ID, oAuthClient.id) + .expect(400); + + expect(response.body.error.message).toEqual( + `Can't cancel booking with uid=${cancelledBooking.uid} because it has been cancelled already. Please provide uid of a booking that is not cancelled.` + ); + await bookingsRepositoryFixture.deleteById(cancelledBooking.id); + }); + }); + describe("calendar events", () => { beforeEach(() => { jest.restoreAllMocks(); diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts index 9c177eaeee..e6c19b2613 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts @@ -711,6 +711,12 @@ export class InputBookingsService_2024_08_13 { throw new NotFoundException(`Booking with uid=${bookingUid} not found`); } + if (booking.status === "CANCELLED") { + throw new BadRequestException( + `Can't cancel booking with uid=${bookingUid} because it has been cancelled already. Please provide uid of a booking that is not cancelled.` + ); + } + const oAuthClientParams = booking.eventTypeId ? await this.platformBookingsService.getOAuthClientParams(booking.eventTypeId) : undefined; diff --git a/packages/features/bookings/lib/getBookingToDelete.ts b/packages/features/bookings/lib/getBookingToDelete.ts index 590d445075..e218ec254a 100644 --- a/packages/features/bookings/lib/getBookingToDelete.ts +++ b/packages/features/bookings/lib/getBookingToDelete.ts @@ -100,6 +100,7 @@ export async function getBookingToDelete(id: number | undefined, uid: string | u responses: true, iCalUID: true, iCalSequence: true, + status: true, }, }); } diff --git a/packages/features/bookings/lib/handleCancelBooking.ts b/packages/features/bookings/lib/handleCancelBooking.ts index 4ded8fb2a5..6ed8d3de75 100644 --- a/packages/features/bookings/lib/handleCancelBooking.ts +++ b/packages/features/bookings/lib/handleCancelBooking.ts @@ -89,6 +89,19 @@ async function handler(input: CancelBookingInput) { arePlatformEmailsEnabled, } = input; + /** + * Important: We prevent cancelling an already cancelled booking. + * A booking could have been CANCELLED due to a reschedule, + * in which case we simply update the existing calendar event and meeting. + * We want to avoid deleting them by a subsequent cancellation attempt. + */ + if (bookingToDelete.status === BookingStatus.CANCELLED) { + throw new HttpError({ + statusCode: 400, + message: "This booking has already been cancelled.", + }); + } + if (!bookingToDelete.userId || !bookingToDelete.user) { throw new HttpError({ statusCode: 400, message: "User not found" }); }