feat: platform endpoints to fetch/cancel/reschedule bookings and hooks (#14164)
* init bookings endpoint * abstracting functions * e2e tests for bookings * hooks for bookings endpoint * bookings respository fixtures * typings for booking input * fixup * abstract booking info code and use it as handler * import handlers for bookings endpoint * add cancel booking input * add handleCancelBooking handler in platform libraries * cancel booking endpoint * abstract call into its own separate fn * cancel booking hook * e2e test for cancel booking endpoint * fix import * export getBookings function * move getAllUserBookings into lib * add bookings folder to package exports * use getAllUserBookings from lib * fix import path * fix: hooks, endpoint and example for cancel / reschedule / list / booking success page * fix: unit test mock classNames import from lib * fix: unit test mock classNames import from lib --------- Co-authored-by: Morgan Vernay <morgan@cal.com> Co-authored-by: Morgan <33722304+ThyMinimalDev@users.noreply.github.com>
This commit is contained in:
co-authored by
Morgan Vernay
Morgan
parent
a35683667b
commit
cc2164657c
@@ -0,0 +1,236 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { SchedulesRepository } from "@/ee/schedules/schedules.repository";
|
||||
import { SchedulesService } from "@/ee/schedules/services/schedules.service";
|
||||
import { AvailabilitiesModule } from "@/modules/availabilities/availabilities.module";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import { User } from "@prisma/client";
|
||||
import * as request from "supertest";
|
||||
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { withAccessTokenAuth } from "test/utils/withAccessTokenAuth";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
import {
|
||||
getAllUserBookings,
|
||||
handleNewBooking,
|
||||
getBookingInfo,
|
||||
handleNewRecurringBooking,
|
||||
handleInstantMeeting,
|
||||
} from "@calcom/platform-libraries";
|
||||
import { ApiSuccessResponse, ApiResponse } from "@calcom/platform-types";
|
||||
|
||||
describe("Bookings Endpoints", () => {
|
||||
describe("User Authenticated", () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let bookingsRepositoryFixture: BookingsRepositoryFixture;
|
||||
|
||||
const userEmail = "bookings-controller-e2e@api.com";
|
||||
let user: User;
|
||||
|
||||
let createdBooking: Awaited<ReturnType<typeof handleNewBooking>>;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withAccessTokenAuth(
|
||||
userEmail,
|
||||
Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, AvailabilitiesModule, UsersModule],
|
||||
providers: [SchedulesRepository, SchedulesService],
|
||||
})
|
||||
).compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
|
||||
|
||||
user = await userRepositoryFixture.create({
|
||||
email: userEmail,
|
||||
});
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it("should be defined", () => {
|
||||
expect(userRepositoryFixture).toBeDefined();
|
||||
expect(user).toBeDefined();
|
||||
});
|
||||
|
||||
it("should create a booking", async () => {
|
||||
const bookingStart = "2023-05-25T09:30:00.000Z";
|
||||
const bookingEnd = "2023-05-25T10:30:00.000Z";
|
||||
const bookingEventTypeId = 7;
|
||||
const bookingTimeZone = "Europe/Londom";
|
||||
const bookingLanguage = "en";
|
||||
const bookingHashedLink = "";
|
||||
const bookingMetadata = {};
|
||||
|
||||
const body = {
|
||||
start: bookingStart,
|
||||
end: bookingEnd,
|
||||
eventTypeId: bookingEventTypeId,
|
||||
timeZone: bookingTimeZone,
|
||||
language: bookingLanguage,
|
||||
metadata: bookingMetadata,
|
||||
hashedLink: bookingHashedLink,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/api/v2/ee/bookings")
|
||||
.send(body)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: ApiSuccessResponse<Awaited<ReturnType<typeof handleNewBooking>>> =
|
||||
response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.user.email).toBeDefined();
|
||||
expect(responseBody.data.user.email).toEqual(userEmail);
|
||||
expect(responseBody.data.id).toBeDefined();
|
||||
expect(responseBody.data.uid).toBeDefined();
|
||||
expect(responseBody.data.startTime).toEqual(bookingStart);
|
||||
expect(responseBody.data.eventTypeId).toEqual(bookingEventTypeId);
|
||||
expect(responseBody.data.user.timeZone).toEqual(bookingTimeZone);
|
||||
expect(responseBody.data.metadata).toEqual(bookingMetadata);
|
||||
|
||||
createdBooking = responseBody.data;
|
||||
});
|
||||
});
|
||||
|
||||
it("should get bookings", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get("/api/v2/ee/bookings")
|
||||
.then((response) => {
|
||||
const responseBody: ApiSuccessResponse<Awaited<ReturnType<typeof getAllUserBookings>>> =
|
||||
response.body;
|
||||
const fetchedBooking = responseBody.data.bookings[0];
|
||||
|
||||
expect(responseBody.data.bookings.length).toEqual(1);
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(fetchedBooking).toBeDefined();
|
||||
|
||||
expect(fetchedBooking.id).toEqual(createdBooking.id);
|
||||
expect(fetchedBooking.uid).toEqual(createdBooking.uid);
|
||||
expect(fetchedBooking.startTime).toEqual(createdBooking.startTime);
|
||||
expect(fetchedBooking.endTime).toEqual(createdBooking.endTime);
|
||||
expect(fetchedBooking.user?.email).toEqual(createdBooking.user.email);
|
||||
});
|
||||
});
|
||||
|
||||
it("should get booking", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/api/v2/ee/bookings/${createdBooking.uid}`)
|
||||
.then((response) => {
|
||||
const responseBody: ApiSuccessResponse<Awaited<ReturnType<typeof getBookingInfo>>> = response.body;
|
||||
const { bookingInfo } = responseBody.data;
|
||||
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(bookingInfo?.id).toBeDefined();
|
||||
expect(bookingInfo?.uid).toBeDefined();
|
||||
expect(bookingInfo?.id).toEqual(createdBooking.id);
|
||||
expect(bookingInfo?.uid).toEqual(createdBooking.uid);
|
||||
expect(bookingInfo?.eventTypeId).toEqual(createdBooking.eventTypeId);
|
||||
expect(bookingInfo?.startTime).toEqual(createdBooking.startTime);
|
||||
});
|
||||
});
|
||||
|
||||
it("should create a recurring booking", async () => {
|
||||
const bookingStart = "2023-05-25T09:30:00.000Z";
|
||||
const bookingEnd = "2023-05-25T10:30:00.000Z";
|
||||
const bookingEventTypeId = 7;
|
||||
const bookingTimeZone = "Europe/Londom";
|
||||
const bookingLanguage = "en";
|
||||
const bookingHashedLink = "";
|
||||
const bookingRecurringCount = 5;
|
||||
const currentBookingRecurringIndex = 0;
|
||||
|
||||
const body = {
|
||||
start: bookingStart,
|
||||
end: bookingEnd,
|
||||
eventTypeId: bookingEventTypeId,
|
||||
timeZone: bookingTimeZone,
|
||||
language: bookingLanguage,
|
||||
metadata: {},
|
||||
hashedLink: bookingHashedLink,
|
||||
recurringCount: bookingRecurringCount,
|
||||
currentRecurringIndex: currentBookingRecurringIndex,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/api/v2/ee/bookings/reccuring")
|
||||
.send(body)
|
||||
.expect(201)
|
||||
.then((response) => {
|
||||
const responseBody: ApiResponse<Awaited<ReturnType<typeof handleNewRecurringBooking>>> =
|
||||
response.body;
|
||||
|
||||
expect(responseBody.status).toEqual("recurring");
|
||||
});
|
||||
});
|
||||
|
||||
it("should create an instant booking", async () => {
|
||||
const bookingStart = "2023-05-25T09:30:00.000Z";
|
||||
const bookingEnd = "2023-05-25T10:30:00.000Z";
|
||||
const bookingEventTypeId = 7;
|
||||
const bookingTimeZone = "Europe/Londom";
|
||||
const bookingLanguage = "en";
|
||||
const bookingHashedLink = "";
|
||||
|
||||
const body = {
|
||||
start: bookingStart,
|
||||
end: bookingEnd,
|
||||
eventTypeId: bookingEventTypeId,
|
||||
timeZone: bookingTimeZone,
|
||||
language: bookingLanguage,
|
||||
metadata: {},
|
||||
hashedLink: bookingHashedLink,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/api/v2/ee/bookings/instant")
|
||||
.send(body)
|
||||
.expect(201)
|
||||
.then((response) => {
|
||||
const responseBody: ApiResponse<Awaited<ReturnType<typeof handleInstantMeeting>>> = response.body;
|
||||
|
||||
expect(responseBody.status).toEqual("instant");
|
||||
});
|
||||
});
|
||||
|
||||
it("should cancel a booking", async () => {
|
||||
const bookingId = createdBooking.id;
|
||||
|
||||
const body = {
|
||||
allRemainingBookings: false,
|
||||
cancellationReason: "Was fighting some unforseen rescheduling demons",
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/api/v2/ee/bookings/${bookingId}/cancel`)
|
||||
.send(body)
|
||||
.expect(201)
|
||||
.then((response) => {
|
||||
const responseBody: ApiResponse<{ status: typeof SUCCESS_STATUS | typeof ERROR_STATUS }> =
|
||||
response.body;
|
||||
|
||||
expect(bookingId).toBeDefined();
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await userRepositoryFixture.deleteByEmail(user.email);
|
||||
await bookingsRepositoryFixture.deleteAllBookings(user.id, user.email);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,11 @@
|
||||
import { CreateBookingInput } from "@/ee/bookings/inputs/create-booking.input";
|
||||
import { CreateReccuringBookingInput } from "@/ee/bookings/inputs/create-reccuring-booking.input";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import {
|
||||
Controller,
|
||||
Post,
|
||||
@@ -11,12 +14,23 @@ import {
|
||||
InternalServerErrorException,
|
||||
Body,
|
||||
HttpException,
|
||||
Param,
|
||||
Get,
|
||||
Query,
|
||||
NotFoundException,
|
||||
UseGuards,
|
||||
} from "@nestjs/common";
|
||||
import { User } from "@prisma/client";
|
||||
import { Request } from "express";
|
||||
import { NextApiRequest } from "next/types";
|
||||
|
||||
import { BOOKING_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import {
|
||||
getAllUserBookings,
|
||||
getBookingInfo,
|
||||
handleCancelBooking,
|
||||
getBookingForReschedule,
|
||||
} from "@calcom/platform-libraries";
|
||||
import {
|
||||
handleNewBooking,
|
||||
BookingResponse,
|
||||
@@ -24,7 +38,9 @@ import {
|
||||
handleNewRecurringBooking,
|
||||
handleInstantMeeting,
|
||||
} from "@calcom/platform-libraries";
|
||||
import { GetBookingsInput, CancelBookingInput } from "@calcom/platform-types";
|
||||
import { ApiResponse } from "@calcom/platform-types";
|
||||
import { PrismaClient } from "@calcom/prisma";
|
||||
|
||||
@Controller({
|
||||
path: "ee/bookings",
|
||||
@@ -34,7 +50,65 @@ import { ApiResponse } from "@calcom/platform-types";
|
||||
export class BookingsController {
|
||||
private readonly logger = new Logger("ee bookings controller");
|
||||
|
||||
constructor(private readonly oAuthFlowService: OAuthFlowService) {}
|
||||
constructor(
|
||||
private readonly oAuthFlowService: OAuthFlowService,
|
||||
private readonly prismaReadService: PrismaReadService
|
||||
) {}
|
||||
|
||||
// note(Rajiv): currently this endpoint is atoms only
|
||||
@Get("/")
|
||||
@UseGuards(AccessTokenGuard)
|
||||
async getBookings(
|
||||
@GetUser() user: User,
|
||||
@Query() queryParams: GetBookingsInput
|
||||
): Promise<ApiResponse<unknown>> {
|
||||
const { filters, cursor, limit } = queryParams;
|
||||
const bookings = await getAllUserBookings({
|
||||
bookingListingByStatus: filters.status,
|
||||
skip: cursor ?? 0,
|
||||
take: limit ?? 10,
|
||||
filters,
|
||||
ctx: {
|
||||
user: { email: user.email, id: user.id },
|
||||
prisma: this.prismaReadService.prisma as unknown as PrismaClient,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: bookings,
|
||||
};
|
||||
}
|
||||
|
||||
// note(Rajiv): currently this endpoint is atoms only
|
||||
@Get("/:bookingUid")
|
||||
async getBooking(@Param("bookingUid") bookingUid: string): Promise<ApiResponse<unknown>> {
|
||||
const { bookingInfo } = await getBookingInfo(bookingUid);
|
||||
|
||||
if (!bookingInfo) {
|
||||
throw new NotFoundException(`Booking with UID=${bookingUid} does not exist.`);
|
||||
}
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: bookingInfo,
|
||||
};
|
||||
}
|
||||
|
||||
// note(Rajiv): currently this endpoint is atoms only
|
||||
@Get("/:bookingUid/reschedule")
|
||||
async getBookingForReschedule(@Param("bookingUid") bookingUid: string): Promise<ApiResponse<unknown>> {
|
||||
const booking = await getBookingForReschedule(bookingUid);
|
||||
|
||||
if (!booking) {
|
||||
throw new NotFoundException(`Booking with UID=${bookingUid} does not exist.`);
|
||||
}
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: booking,
|
||||
};
|
||||
}
|
||||
|
||||
@Post("/")
|
||||
@Permissions([BOOKING_WRITE])
|
||||
@@ -56,6 +130,30 @@ export class BookingsController {
|
||||
throw new InternalServerErrorException("Could not create booking.");
|
||||
}
|
||||
|
||||
@Post("/:bookingId/cancel")
|
||||
@Permissions([BOOKING_WRITE])
|
||||
async cancelBooking(
|
||||
@Req() req: Request & { userId?: number },
|
||||
@Param("bookingId") bookingId: string,
|
||||
@Body() body: CancelBookingInput
|
||||
): Promise<ApiResponse> {
|
||||
if (bookingId) {
|
||||
req.userId = await this.getOwnerId(req);
|
||||
req.body = { ...body, id: parseInt(bookingId) };
|
||||
try {
|
||||
await handleCancelBooking(req as unknown as NextApiRequest & { userId?: number });
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
};
|
||||
} catch (err) {
|
||||
handleBookingErrors(err);
|
||||
}
|
||||
} else {
|
||||
throw new NotFoundException("Booking ID is required.");
|
||||
}
|
||||
throw new InternalServerErrorException("Could not cancel booking.");
|
||||
}
|
||||
|
||||
@Post("/reccuring")
|
||||
@Permissions([BOOKING_WRITE])
|
||||
async createReccuringBooking(
|
||||
|
||||
Reference in New Issue
Block a user