From 96b58422fdea7c76d7f9ce0e122c29656a06de6a Mon Sep 17 00:00:00 2001 From: Imamuzzaki Abu Salam Date: Wed, 7 Aug 2024 18:08:04 +0700 Subject: [PATCH] feat: Add upcoming bookings feature to GET /api/bookings endpoint (#16060) * feat: Add upcoming bookings feature to GET /api/bookings endpoint - Added support for filtering upcoming bookings by adding the `upcoming` query parameter to the GET /api/bookings endpoint. - When `upcoming` is set to "true", only upcoming bookings are returned. - Regular users can now retrieve only their upcoming bookings by setting `upcoming` to "true". - System-wide admins and organization admins can also retrieve only upcoming bookings by setting `upcoming` to "true". * feat: Add status filter to booking API endpoint This commit adds a new `status` filter to the booking API endpoint. The `status` filter allows filtering bookings by their status, overriding the `dateFrom` and `dateTo` filters. The only valid value for the `status` filter is "upcoming". * Update _get.ts --------- Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com> --- apps/api/v1/lib/validations/booking.ts | 5 +- apps/api/v1/pages/api/bookings/_get.ts | 41 ++++++++- .../lib/bookings/_get.integration-test.ts | 83 +++++++++++++++++++ 3 files changed, 124 insertions(+), 5 deletions(-) diff --git a/apps/api/v1/lib/validations/booking.ts b/apps/api/v1/lib/validations/booking.ts index 867bc4d5ce..d326ce8233 100644 --- a/apps/api/v1/lib/validations/booking.ts +++ b/apps/api/v1/lib/validations/booking.ts @@ -1,6 +1,6 @@ import { z } from "zod"; -import { _BookingModel as Booking, _AttendeeModel, _UserModel, _PaymentModel } from "@calcom/prisma/zod"; +import { _AttendeeModel, _BookingModel as Booking, _PaymentModel, _UserModel } from "@calcom/prisma/zod"; import { extendedBookingCreateBody, iso8601 } from "@calcom/prisma/zod-utils"; import { schemaQueryUserId } from "./shared/queryUserId"; @@ -23,8 +23,11 @@ export const schemaBookingGetParams = z.object({ dateTo: iso8601.optional(), order: z.enum(["asc", "desc"]).default("asc"), sortBy: z.enum(["createdAt", "updatedAt"]).optional(), + status: z.enum(["upcoming"]).optional(), }); +export type Status = z.infer["status"]; + const schemaBookingEditParams = z .object({ title: z.string().optional(), diff --git a/apps/api/v1/pages/api/bookings/_get.ts b/apps/api/v1/pages/api/bookings/_get.ts index d1aab8b2ef..2174841e97 100644 --- a/apps/api/v1/pages/api/bookings/_get.ts +++ b/apps/api/v1/pages/api/bookings/_get.ts @@ -63,6 +63,25 @@ import { schemaQuerySingleOrMultipleUserIds } from "~/lib/validations/shared/que * schema: * type: string * enum: [createdAt, updatedAt] + * - in: query + * name: status + * required: false + * schema: + * type: string + * enum: [upcoming] + * description: Filter bookings by status, it will overwrite dateFrom and dateTo filters + * - in: query + * name: dateFrom + * required: false + * schema: + * type: string + * description: ISO 8601 date string to filter bookings by start time + * - in: query + * name: dateTo + * required: false + * schema: + * type: string + * description: ISO 8601 date string to filter bookings by end time * operationId: listBookings * tags: * - bookings @@ -130,6 +149,7 @@ type GetAdminArgsType = { requestedUserIds: number[]; userId: number; }; + /** * Constructs the WHERE clause for Prisma booking findMany operation. * @@ -148,7 +168,9 @@ function buildWhereClause( ) { const filterByAttendeeEmails = attendeeEmails.length > 0; const userFilter = userIds.length > 0 ? { userId: { in: userIds } } : !!userId ? { userId } : {}; + let whereClause = {}; + if (filterByAttendeeEmails) { whereClause = { AND: [ @@ -177,9 +199,7 @@ function buildWhereClause( }; } - return { - ...whereClause, - }; + return whereClause; } export async function handler(req: NextApiRequest) { @@ -189,7 +209,7 @@ export async function handler(req: NextApiRequest) { isOrganizationOwnerOrAdmin, pagination: { take, skip }, } = req; - const { dateFrom, dateTo, order, sortBy } = schemaBookingGetParams.parse(req.query); + const { dateFrom, dateTo, order, sortBy, status } = schemaBookingGetParams.parse(req.query); const args: Prisma.BookingFindManyArgs = {}; if (req.query.take && req.query.page) { @@ -275,6 +295,19 @@ export async function handler(req: NextApiRequest) { }; } + if (status) { + switch (status) { + case "upcoming": + args.where = { + ...args.where, + startTime: { gte: new Date().toISOString() }, + }; + break; + default: + throw new HttpError({ message: "Invalid status", statusCode: 400 }); + } + } + const data = await prisma.booking.findMany(args); return { bookings: data.map((booking) => schemaBookingReadPublic.parse(booking)) }; } diff --git a/apps/api/v1/test/lib/bookings/_get.integration-test.ts b/apps/api/v1/test/lib/bookings/_get.integration-test.ts index 7687f5e6c8..74206dd83b 100644 --- a/apps/api/v1/test/lib/bookings/_get.integration-test.ts +++ b/apps/api/v1/test/lib/bookings/_get.integration-test.ts @@ -105,4 +105,87 @@ describe("GET /api/bookings", async () => { expect(responseData.bookings.find((b) => b.id === proUserBooking.id)).toBeUndefined(); expect(groupedUsers.size).toBeGreaterThanOrEqual(2); }); + + describe("Upcoming bookings feature", () => { + it("Returns only upcoming bookings when status=upcoming for regular user", async () => { + const { req } = createMocks({ + method: "GET", + query: { + status: "upcoming", + }, + pagination: DefaultPagination, + }); + + req.userId = proUser.id; + + const responseData = await handler(req); + responseData.bookings.forEach((booking) => { + expect(new Date(booking.startTime).getTime()).toBeGreaterThanOrEqual(new Date().getTime()); + }); + }); + + it("Returns all bookings when status not specified for regular user", async () => { + const { req } = createMocks({ + method: "GET", + pagination: DefaultPagination, + }); + + req.userId = proUser.id; + + const responseData = await handler(req); + expect(responseData.bookings.find((b) => b.id === proUserBooking.id)).toBeDefined(); + + const { req: req2 } = createMocks({ + method: "GET", + pagination: DefaultPagination, + }); + + req2.userId = proUser.id; + + const responseData2 = await handler(req2); + expect(responseData2.bookings.find((b) => b.id === proUserBooking.id)).toBeDefined(); + }); + + it("Returns only upcoming bookings when status=upcoming for system-wide admin", async () => { + const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "owner1-acme@example.com" } }); + const { req } = createMocks({ + method: "GET", + query: { + status: "upcoming", + }, + pagination: { + take: 100, + skip: 0, + }, + }); + + req.isSystemWideAdmin = true; + req.userId = adminUser.id; + + const responseData = await handler(req); + responseData.bookings.forEach((booking) => { + console.log(booking); + expect(new Date(booking.startTime).getTime()).toBeGreaterThanOrEqual(new Date().getTime()); + }); + }); + + it("Returns only upcoming bookings when status=upcoming for org admin", async () => { + const adminUser = await prisma.user.findFirstOrThrow({ where: { email: "owner1-acme@example.com" } }); + const { req } = createMocks({ + method: "GET", + query: { + status: "upcoming", + }, + pagination: DefaultPagination, + }); + + req.userId = adminUser.id; + req.isOrganizationOwnerOrAdmin = true; + + const responseData = await handler(req); + responseData.bookings.forEach((booking) => { + expect(new Date(booking.startTime).getTime()).toBeGreaterThanOrEqual(new Date().getTime()); + }); + }); + }); });