From 71c836b48e20f069f54d19a58830a622b8705e3f Mon Sep 17 00:00:00 2001 From: Himanshu Pandey Date: Thu, 6 Feb 2025 00:43:05 +0530 Subject: [PATCH 1/6] Add delete one past event functionality --- .../components/booking/BookingListItem.tsx | 45 ++++++++++++++++ apps/web/public/static/locales/en/common.json | 6 ++- .../routers/viewer/bookings/_router.tsx | 17 ++++++ .../routers/viewer/bookings/delete.handler.ts | 54 +++++++++++++++++++ .../routers/viewer/bookings/delete.schema.ts | 7 +++ 5 files changed, 128 insertions(+), 1 deletion(-) create mode 100644 packages/trpc/server/routers/viewer/bookings/delete.handler.ts create mode 100644 packages/trpc/server/routers/viewer/bookings/delete.schema.ts diff --git a/apps/web/components/booking/BookingListItem.tsx b/apps/web/components/booking/BookingListItem.tsx index ee86420da8e3be..0804b9952c9579 100644 --- a/apps/web/components/booking/BookingListItem.tsx +++ b/apps/web/components/booking/BookingListItem.tsx @@ -26,6 +26,7 @@ import type { ActionType } from "@calcom/ui"; import { Badge, Button, + ConfirmationDialogContent, Dialog, DialogClose, DialogContent, @@ -118,6 +119,7 @@ function BookingListItem(booking: BookingItemProps) { const [chargeCardDialogIsOpen, setChargeCardDialogIsOpen] = useState(false); const [viewRecordingsDialogIsOpen, setViewRecordingsDialogIsOpen] = useState(false); const [isNoShowDialogOpen, setIsNoShowDialogOpen] = useState(false); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const cardCharged = booking?.payment[0]?.success; const mutation = trpc.viewer.bookings.confirm.useMutation({ onSuccess: (data) => { @@ -289,6 +291,17 @@ function BookingListItem(booking: BookingItemProps) { }); } + if (isBookingInPast) { + editBookingActions.push({ + id: "delete", + label: t("delete_booking"), + onClick: () => { + setDeleteDialogOpen(true); + }, + icon: "trash", + }); + } + let bookedActions: ActionType[] = [ { id: "cancel", @@ -431,6 +444,22 @@ function BookingListItem(booking: BookingItemProps) { }; }); + const deleteMutation = trpc.viewer.bookings.delete.useMutation({ + onSuccess: () => { + showToast(t("booking_deleted_successfully"), "success"); + setDeleteDialogOpen(false); + // Invalidate the bookings query to refresh the list + utils.viewer.bookings.invalidate(); + }, + onError: (err) => { + showToast(err.message, "error"); + }, + }); + + const deleteBookingHandler = (id: number) => { + deleteMutation.mutate({ id }); + }; + return ( <> + + + { + e.preventDefault(); + deleteBookingHandler(booking.id); + }}> +

{t("delete_booking_description")}

+
+
+
{ + if (!UNSTABLE_HANDLER_CACHE.delete) { + UNSTABLE_HANDLER_CACHE.delete = await import("./delete.handler").then((mod) => mod.deleteHandler); + } + + if (!UNSTABLE_HANDLER_CACHE.delete) { + throw new Error("Failed to load handler"); + } + + return UNSTABLE_HANDLER_CACHE.delete({ + ctx, + input, + }); + }), }); diff --git a/packages/trpc/server/routers/viewer/bookings/delete.handler.ts b/packages/trpc/server/routers/viewer/bookings/delete.handler.ts new file mode 100644 index 00000000000000..20866e90aa064e --- /dev/null +++ b/packages/trpc/server/routers/viewer/bookings/delete.handler.ts @@ -0,0 +1,54 @@ +import { prisma } from "@calcom/prisma"; +import type { TrpcSessionUser } from "@calcom/trpc/server/trpc"; + +import type { TDeleteInputSchema } from "./delete.schema"; + +type DeleteOptions = { + ctx: { + user: NonNullable; + }; + input: TDeleteInputSchema; +}; + +export const deleteHandler = async ({ ctx, input }: DeleteOptions) => { + const { id } = input; + const { user } = ctx; + + const booking = await prisma.booking.findFirst({ + where: { + id: id, + OR: [ + { + userId: user.id, + }, + { + attendees: { + some: { + email: user.email, + }, + }, + }, + ], + }, + include: { + attendees: true, + references: true, + payment: true, + workflowReminders: true, + }, + }); + + if (!booking) { + throw new Error("Booking not found"); + } + + await prisma.booking.delete({ + where: { + id: booking.id, + }, + }); + + return { + message: "Booking deleted successfully", + }; +}; diff --git a/packages/trpc/server/routers/viewer/bookings/delete.schema.ts b/packages/trpc/server/routers/viewer/bookings/delete.schema.ts new file mode 100644 index 00000000000000..411d5e953b9165 --- /dev/null +++ b/packages/trpc/server/routers/viewer/bookings/delete.schema.ts @@ -0,0 +1,7 @@ +import { z } from "zod"; + +export const ZDeleteInputSchema = z.object({ + id: z.number(), +}); + +export type TDeleteInputSchema = z.infer; From 9557042b608424aad5bd72432a6393bfdb9105f8 Mon Sep 17 00:00:00 2001 From: Himanshu Pandey Date: Thu, 6 Feb 2025 02:24:07 +0530 Subject: [PATCH 2/6] Add all clear button --- .../booking/DeletePastBookingsSection.tsx | 70 +++++++++++++++++++ .../bookings/views/bookings-listing-view.tsx | 18 +++-- apps/web/public/static/locales/en/common.json | 5 +- .../routers/viewer/bookings/_router.tsx | 19 +++++ .../bookings/deletePastBookings.handler.ts | 37 ++++++++++ .../bookings/deletePastBookings.schema.ts | 7 ++ 6 files changed, 151 insertions(+), 5 deletions(-) create mode 100644 apps/web/components/booking/DeletePastBookingsSection.tsx create mode 100644 packages/trpc/server/routers/viewer/bookings/deletePastBookings.handler.ts create mode 100644 packages/trpc/server/routers/viewer/bookings/deletePastBookings.schema.ts diff --git a/apps/web/components/booking/DeletePastBookingsSection.tsx b/apps/web/components/booking/DeletePastBookingsSection.tsx new file mode 100644 index 00000000000000..34aa89aff41388 --- /dev/null +++ b/apps/web/components/booking/DeletePastBookingsSection.tsx @@ -0,0 +1,70 @@ +import { useState } from "react"; + +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { trpc } from "@calcom/trpc/react"; +import { Button, Dialog, DialogContent, DialogFooter, DialogHeader, showToast } from "@calcom/ui"; + +interface DeletePastBookingsSectionProps { + bookingsCount: number; + bookingIds: number[]; +} + +export const DeletePastBookingsSection = ({ bookingsCount, bookingIds }: DeletePastBookingsSectionProps) => { + const { t } = useLocale(); + const utils = trpc.useContext(); + + const deletePastBookingsMutation = trpc.viewer.bookings.deletePastBookings.useMutation({ + onSuccess: (data) => { + showToast(data.message, "success"); + utils.viewer.bookings.get.invalidate(); + }, + onError: (error) => { + showToast(error.message, "error"); + }, + }); + + const [isDialogOpen, setIsDialogOpen] = useState(false); + + const handleDeletePastBookings = () => { + deletePastBookingsMutation.mutate({ bookingIds }); + setIsDialogOpen(false); + }; + + return ( + <> + + + + + +

+ {t("confirm_delete_past_bookings")} +
+ + {bookingsCount} {bookingsCount === 1 ? "booking" : "bookings"} will be deleted. + +

+ + + + +
+
+ + ); +}; diff --git a/apps/web/modules/bookings/views/bookings-listing-view.tsx b/apps/web/modules/bookings/views/bookings-listing-view.tsx index 7d1cb8102e3487..727718bbd58ee3 100644 --- a/apps/web/modules/bookings/views/bookings-listing-view.tsx +++ b/apps/web/modules/bookings/views/bookings-listing-view.tsx @@ -26,6 +26,7 @@ import { Alert, EmptyScreen, HorizontalTabs } from "@calcom/ui"; import useMeQuery from "@lib/hooks/useMeQuery"; import BookingListItem from "@components/booking/BookingListItem"; +import { DeletePastBookingsSection } from "@components/booking/DeletePastBookingsSection"; import SkeletonLoader from "@components/booking/SkeletonLoader"; import type { validStatuses } from "~/bookings/lib/validStatuses"; @@ -96,7 +97,6 @@ type RowData = function BookingsContent({ status }: BookingsProps) { const { data: filterQuery } = useFilterQuery(); - const { t } = useLocale(); const user = useMeQuery().data; const [isFiltersVisible, setIsFiltersVisible] = useState(false); @@ -155,7 +155,7 @@ function BookingsContent({ status }: BookingsProps) { }, }), ]; - }, [user, status]); + }, [user, status, t]); const isEmpty = useMemo(() => !query.data?.pages[0]?.bookings.length, [query.data]); @@ -242,9 +242,19 @@ function BookingsContent({ status }: BookingsProps) { return (
-
+
- +
+ {status === "past" && ( + (item.type === "data" ? item.booking.id : null)) + .filter((id): id is number => id !== null)} + /> + )} + +
diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index e476005a53a2bb..11a3fdfd59f40e 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -2957,5 +2957,8 @@ "delete_booking": "Delete", "delete_booking_dialog_title": "Delete booking?", "confirm_delete_booking": "Confirm", - "delete_booking_description":"This action will delete booking from your history." + "delete_booking_description":"This action will delete booking from your history.", + "delete_past_bookings": "Delete Past Bookings", + "confirm_delete_past_bookings": "Are you sure you want to delete all past bookings? This action cannot be undone.", + "past_bookings_deleted_successfully": "Past bookings deleted successfully" } diff --git a/packages/trpc/server/routers/viewer/bookings/_router.tsx b/packages/trpc/server/routers/viewer/bookings/_router.tsx index 7a04e96e61a5a2..41ff842832ac77 100644 --- a/packages/trpc/server/routers/viewer/bookings/_router.tsx +++ b/packages/trpc/server/routers/viewer/bookings/_router.tsx @@ -4,6 +4,7 @@ import { router } from "../../../trpc"; import { ZAddGuestsInputSchema } from "./addGuests.schema"; import { ZConfirmInputSchema } from "./confirm.schema"; import { ZDeleteInputSchema } from "./delete.schema"; +import { ZDeletePastBookingsSchema } from "./deletePastBookings.schema"; import { ZEditLocationInputSchema } from "./editLocation.schema"; import { ZFindInputSchema } from "./find.schema"; import { ZGetInputSchema } from "./get.schema"; @@ -22,6 +23,7 @@ type BookingsRouterHandlerCache = { find?: typeof import("./find.handler").getHandler; getInstantBookingLocation?: typeof import("./getInstantBookingLocation.handler").getHandler; delete?: typeof import("./delete.handler").deleteHandler; + deletePastBookings?: typeof import("./deletePastBookings.handler").deletePastBookingsHandler; }; const UNSTABLE_HANDLER_CACHE: BookingsRouterHandlerCache = {}; @@ -182,4 +184,21 @@ export const bookingsRouter = router({ input, }); }), + + deletePastBookings: authedProcedure.input(ZDeletePastBookingsSchema).mutation(async ({ ctx, input }) => { + if (!UNSTABLE_HANDLER_CACHE.deletePastBookings) { + UNSTABLE_HANDLER_CACHE.deletePastBookings = await import("./deletePastBookings.handler").then( + (mod) => mod.deletePastBookingsHandler + ); + } + + if (!UNSTABLE_HANDLER_CACHE.deletePastBookings) { + throw new Error("Failed to load handler"); + } + + return UNSTABLE_HANDLER_CACHE.deletePastBookings({ + ctx, + input, + }); + }), }); diff --git a/packages/trpc/server/routers/viewer/bookings/deletePastBookings.handler.ts b/packages/trpc/server/routers/viewer/bookings/deletePastBookings.handler.ts new file mode 100644 index 00000000000000..fbb9137e254558 --- /dev/null +++ b/packages/trpc/server/routers/viewer/bookings/deletePastBookings.handler.ts @@ -0,0 +1,37 @@ +import { prisma } from "@calcom/prisma"; +import type { TrpcSessionUser } from "@calcom/trpc/server/trpc"; + +import type { TDeletePastBookingsSchema } from "./deletePastBookings.schema"; + +type DeletePastBookingsOptions = { + ctx: { + user: NonNullable; + }; + input: TDeletePastBookingsSchema; +}; + +export const deletePastBookingsHandler = async ({ ctx, input }: DeletePastBookingsOptions) => { + const { user } = ctx; + const { bookingIds } = input; + + const result = await prisma.booking.deleteMany({ + where: { + id: { in: bookingIds }, + OR: [ + { userId: user.id }, + { + attendees: { + some: { email: user.email }, + }, + }, + ], + status: { notIn: ["CANCELLED", "REJECTED"] }, + endTime: { lt: new Date() }, + }, + }); + + return { + count: result.count, + message: `${result.count} bookings deleted successfully`, + }; +}; diff --git a/packages/trpc/server/routers/viewer/bookings/deletePastBookings.schema.ts b/packages/trpc/server/routers/viewer/bookings/deletePastBookings.schema.ts new file mode 100644 index 00000000000000..0e60f9d9c28c91 --- /dev/null +++ b/packages/trpc/server/routers/viewer/bookings/deletePastBookings.schema.ts @@ -0,0 +1,7 @@ +import { z } from "zod"; + +export const ZDeletePastBookingsSchema = z.object({ + bookingIds: z.number().array(), +}); + +export type TDeletePastBookingsSchema = z.infer; From 68f14a45a5745379f4b14af2c1c77fcf47245a41 Mon Sep 17 00:00:00 2001 From: Himanshu Pandey Date: Thu, 6 Feb 2025 02:32:50 +0530 Subject: [PATCH 3/6] Add correct message --- apps/web/public/static/locales/en/common.json | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index 11a3fdfd59f40e..00ed3c8e29ea48 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -2958,6 +2958,7 @@ "delete_booking_dialog_title": "Delete booking?", "confirm_delete_booking": "Confirm", "delete_booking_description":"This action will delete booking from your history.", + "booking_deleted_successfully": "Your booking was deleted.", "delete_past_bookings": "Delete Past Bookings", "confirm_delete_past_bookings": "Are you sure you want to delete all past bookings? This action cannot be undone.", "past_bookings_deleted_successfully": "Past bookings deleted successfully" From 33c4e2efd5c73ee9970a3defdac95d544ad960f2 Mon Sep 17 00:00:00 2001 From: Himanshu Pandey Date: Thu, 6 Feb 2025 20:43:28 +0530 Subject: [PATCH 4/6] Add logic to not show button when booking is absent --- apps/web/modules/bookings/views/bookings-listing-view.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/modules/bookings/views/bookings-listing-view.tsx b/apps/web/modules/bookings/views/bookings-listing-view.tsx index 727718bbd58ee3..0f22f602145dc6 100644 --- a/apps/web/modules/bookings/views/bookings-listing-view.tsx +++ b/apps/web/modules/bookings/views/bookings-listing-view.tsx @@ -245,7 +245,7 @@ function BookingsContent({ status }: BookingsProps) {
- {status === "past" && ( + {status === "past" && flatData.length > 0 && ( Date: Wed, 12 Feb 2025 20:09:43 +0530 Subject: [PATCH 5/6] Add unauthorization check for delete function and test --- apps/web/test/lib/deleteBooking.test.ts | 118 ++++++++++++++++++ .../routers/viewer/bookings/delete.handler.ts | 4 + .../bookings/deletePastBookings.handler.ts | 11 ++ 3 files changed, 133 insertions(+) create mode 100644 apps/web/test/lib/deleteBooking.test.ts diff --git a/apps/web/test/lib/deleteBooking.test.ts b/apps/web/test/lib/deleteBooking.test.ts new file mode 100644 index 00000000000000..2a11934e4ebe9c --- /dev/null +++ b/apps/web/test/lib/deleteBooking.test.ts @@ -0,0 +1,118 @@ +import prismaMock from "../../../../tests/libs/__mocks__/prismaMock"; + +import { describe, expect, it, beforeEach, vi } from "vitest"; + +import { deleteHandler } from "@calcom/trpc/server/routers/viewer/bookings/delete.handler"; +import { deletePastBookingsHandler } from "@calcom/trpc/server/routers/viewer/bookings/deletePastBookings.handler"; + +describe("Booking deletion", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("should delete a single booking successfully", async () => { + const mockBooking = { + id: 1, + userId: 123, + }; + + prismaMock.booking.findFirst.mockResolvedValue(mockBooking); + prismaMock.booking.delete.mockResolvedValue(mockBooking); + + const ctx = { + user: { + id: 123, + }, + }; + + await deleteHandler({ + ctx, + input: { id: 1 }, + }); + + expect(prismaMock.booking.delete).toHaveBeenCalledTimes(1); + expect(prismaMock.booking.delete).toHaveBeenCalledWith({ + where: { id: 1 }, + }); + }); + + it("should delete multiple past bookings correctly", async () => { + const mockBookings = [ + { id: 1, userId: 123 }, + { id: 2, userId: 123 }, + ]; + + prismaMock.booking.findMany.mockResolvedValue(mockBookings); + prismaMock.booking.deleteMany.mockResolvedValue({ count: 2 }); + + const ctx = { + user: { + id: 123, + }, + }; + + await deletePastBookingsHandler({ + ctx, + input: { ids: [1, 2] }, + }); + + expect(prismaMock.booking.deleteMany).toHaveBeenCalledTimes(1); + expect(prismaMock.booking.deleteMany).toHaveBeenCalledWith({ + where: { + OR: [{ userId: 123 }, { attendees: { some: { email: undefined } } }], + endTime: { lt: expect.any(Date) }, + id: { in: undefined }, + status: { notIn: ["CANCELLED", "REJECTED"] }, + }, + }); + }); + + it("should prevent deletion of unauthorized bookings", async () => { + const mockBooking = { + id: 1, + userId: 456, + }; + + prismaMock.booking.findFirst.mockResolvedValue(mockBooking); + + const ctx = { + user: { + id: 123, + }, + }; + + await expect( + deleteHandler({ + ctx, + input: { id: 1 }, + }) + ).rejects.toThrow(/unauthorized/i); + + expect(prismaMock.booking.delete).not.toHaveBeenCalled(); + }); + + it("should prevent deletion of unauthorized multiple bookings", async () => { + const mockBooking = [ + { id: 1, userId: 456 }, + { id: 2, userId: 456 }, + ]; + + prismaMock.booking.findMany.mockResolvedValue(mockBooking); + prismaMock.booking.deleteMany.mockResolvedValue({ count: 0 }); + + const ctx = { + user: { + id: 123, + }, + }; + + await expect( + deletePastBookingsHandler({ + ctx, + input: { id: [1, 2] }, + }) + ).rejects.toThrow(/unauthorized/i); + + expect(prismaMock.booking.deleteMany).not.toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/trpc/server/routers/viewer/bookings/delete.handler.ts b/packages/trpc/server/routers/viewer/bookings/delete.handler.ts index 20866e90aa064e..698ca9f44843de 100644 --- a/packages/trpc/server/routers/viewer/bookings/delete.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/delete.handler.ts @@ -42,6 +42,10 @@ export const deleteHandler = async ({ ctx, input }: DeleteOptions) => { throw new Error("Booking not found"); } + if (booking.userId !== user.id) { + throw new Error("Unauthorized: You don't have permission to delete this booking"); + } + await prisma.booking.delete({ where: { id: booking.id, diff --git a/packages/trpc/server/routers/viewer/bookings/deletePastBookings.handler.ts b/packages/trpc/server/routers/viewer/bookings/deletePastBookings.handler.ts index fbb9137e254558..0f1de5c5f2e184 100644 --- a/packages/trpc/server/routers/viewer/bookings/deletePastBookings.handler.ts +++ b/packages/trpc/server/routers/viewer/bookings/deletePastBookings.handler.ts @@ -14,6 +14,17 @@ export const deletePastBookingsHandler = async ({ ctx, input }: DeletePastBookin const { user } = ctx; const { bookingIds } = input; + const bookings = await prisma.booking.findMany({ + where: { + id: { in: bookingIds }, + }, + }); + + const unauthorized = bookings.some((booking) => booking.userId !== user.id); + if (unauthorized) { + throw new Error("Unauthorized: Cannot delete bookings that don't belong to you"); + } + const result = await prisma.booking.deleteMany({ where: { id: { in: bookingIds }, From 65b25a3ece970ae9e7cc874cceaeeb50153440a6 Mon Sep 17 00:00:00 2001 From: Himanshu Pandey Date: Wed, 12 Feb 2025 21:20:02 +0530 Subject: [PATCH 6/6] Fix type error --- apps/web/test/lib/deleteBooking.test.ts | 128 ++++++++++++++++++++---- 1 file changed, 111 insertions(+), 17 deletions(-) diff --git a/apps/web/test/lib/deleteBooking.test.ts b/apps/web/test/lib/deleteBooking.test.ts index 2a11934e4ebe9c..5138d5373c0763 100644 --- a/apps/web/test/lib/deleteBooking.test.ts +++ b/apps/web/test/lib/deleteBooking.test.ts @@ -2,8 +2,12 @@ import prismaMock from "../../../../tests/libs/__mocks__/prismaMock"; import { describe, expect, it, beforeEach, vi } from "vitest"; +import type { Booking } from "@calcom/prisma/client"; +import { BookingStatus } from "@calcom/prisma/client"; +import { BookerLayouts } from "@calcom/prisma/zod-utils"; import { deleteHandler } from "@calcom/trpc/server/routers/viewer/bookings/delete.handler"; import { deletePastBookingsHandler } from "@calcom/trpc/server/routers/viewer/bookings/deletePastBookings.handler"; +import type { TrpcSessionUser } from "@calcom/trpc/server/trpc"; describe("Booking deletion", () => { beforeEach(() => { @@ -13,20 +17,44 @@ describe("Booking deletion", () => { it("should delete a single booking successfully", async () => { const mockBooking = { id: 1, + status: BookingStatus.ACCEPTED, userId: 123, - }; + uid: "test-uid", + } as Booking; prismaMock.booking.findFirst.mockResolvedValue(mockBooking); prismaMock.booking.delete.mockResolvedValue(mockBooking); - const ctx = { + const mockCtx = { user: { id: 123, - }, + email: "test@example.com", + username: "testuser", + name: "Test User", + avatar: "avatar.png", + organization: { + id: null, + isOrgAdmin: false, + metadata: null, + requestedSlug: null, + }, + organizationId: null, + locale: "en", + defaultBookerLayouts: { + enabledLayouts: [BookerLayouts.MONTH_VIEW], + defaultLayout: BookerLayouts.MONTH_VIEW, + }, + timeZone: "UTC", + weekStart: "Monday", + startTime: 0, + endTime: 1440, + bufferTime: 0, + destinationCalendar: null, + } as NonNullable, }; await deleteHandler({ - ctx, + ctx: mockCtx, input: { id: 1 }, }); @@ -45,23 +73,45 @@ describe("Booking deletion", () => { prismaMock.booking.findMany.mockResolvedValue(mockBookings); prismaMock.booking.deleteMany.mockResolvedValue({ count: 2 }); - const ctx = { + const mockCtx = { user: { id: 123, - }, + email: "test@example.com", + username: "testuser", + name: "Test User", + avatar: "avatar.png", + organization: { + id: null, + isOrgAdmin: false, + metadata: null, + requestedSlug: null, + }, + organizationId: null, + locale: "en", + defaultBookerLayouts: { + enabledLayouts: [BookerLayouts.MONTH_VIEW], + defaultLayout: BookerLayouts.MONTH_VIEW, + }, + timeZone: "UTC", + weekStart: "Monday", + startTime: 0, + endTime: 1440, + bufferTime: 0, + destinationCalendar: null, + } as NonNullable, }; await deletePastBookingsHandler({ - ctx, - input: { ids: [1, 2] }, + ctx: mockCtx, + input: { bookingIds: [1, 2] }, }); expect(prismaMock.booking.deleteMany).toHaveBeenCalledTimes(1); expect(prismaMock.booking.deleteMany).toHaveBeenCalledWith({ where: { - OR: [{ userId: 123 }, { attendees: { some: { email: undefined } } }], + OR: [{ userId: 123 }, { attendees: { some: { email: "test@example.com" } } }], endTime: { lt: expect.any(Date) }, - id: { in: undefined }, + id: { in: [1, 2] }, status: { notIn: ["CANCELLED", "REJECTED"] }, }, }); @@ -75,15 +125,37 @@ describe("Booking deletion", () => { prismaMock.booking.findFirst.mockResolvedValue(mockBooking); - const ctx = { + const mockCtx = { user: { id: 123, - }, + email: "test@example.com", + username: "testuser", + name: "Test User", + avatar: "avatar.png", + organization: { + id: null, + isOrgAdmin: false, + metadata: null, + requestedSlug: null, + }, + organizationId: null, + locale: "en", + defaultBookerLayouts: { + enabledLayouts: [BookerLayouts.MONTH_VIEW], + defaultLayout: BookerLayouts.MONTH_VIEW, + }, + timeZone: "UTC", + weekStart: "Monday", + startTime: 0, + endTime: 1440, + bufferTime: 0, + destinationCalendar: null, + } as NonNullable, }; await expect( deleteHandler({ - ctx, + ctx: mockCtx, input: { id: 1 }, }) ).rejects.toThrow(/unauthorized/i); @@ -100,16 +172,38 @@ describe("Booking deletion", () => { prismaMock.booking.findMany.mockResolvedValue(mockBooking); prismaMock.booking.deleteMany.mockResolvedValue({ count: 0 }); - const ctx = { + const mockCtx = { user: { id: 123, - }, + email: "test@example.com", + username: "testuser", + name: "Test User", + avatar: "avatar.png", + organization: { + id: null, + isOrgAdmin: false, + metadata: null, + requestedSlug: null, + }, + organizationId: null, + locale: "en", + defaultBookerLayouts: { + enabledLayouts: [BookerLayouts.MONTH_VIEW], + defaultLayout: BookerLayouts.MONTH_VIEW, + }, + timeZone: "UTC", + weekStart: "Monday", + startTime: 0, + endTime: 1440, + bufferTime: 0, + destinationCalendar: null, + } as NonNullable, }; await expect( deletePastBookingsHandler({ - ctx, - input: { id: [1, 2] }, + ctx: mockCtx, + input: { bookingIds: [1, 2] }, }) ).rejects.toThrow(/unauthorized/i);