Skip to content

Commit 9d96626

Browse files
authored
docs: v2 cancelling seated booking as a host (calcom#21744)
* docs: cancel seated bookings as attendee or host * test: cancelling seated booking as host
1 parent 2d41c71 commit 9d96626

6 files changed

Lines changed: 205 additions & 81 deletions

File tree

apps/api/v2/src/ee/bookings/2024-08-13/controllers/bookings.controller.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,14 @@ export class BookingsController_2024_08_13 {
267267
@ApiOperation({
268268
summary: "Cancel a booking",
269269
description: `:bookingUid can be :bookingUid of an usual booking, individual recurrence or recurring booking to cancel all recurrences.
270-
For seated bookings to cancel one individual booking provide :bookingUid and :seatUid in the request body. For recurring seated bookings it is not possible to cancel all of them with 1 call
270+
271+
\nCancelling seated bookings:
272+
It is possible to cancel specific seat within a booking as an attendee or all of the seats as the host.
273+
\n1. As an attendee - provide :bookingUid in the request URL \`/bookings/:bookingUid/cancel\` and seatUid in the request body \`{"seatUid": "123-123-123"}\` . This will remove this particular attendance from the booking.
274+
\n2. As the host - host can cancel booking for all attendees aka for every seat. Provide :bookingUid in the request URL \`/bookings/:bookingUid/cancel\` and cancellationReason in the request body \`{"cancellationReason": "Will travel"}\` and \`Authorization: Bearer token\` request header where token is event type owner (host) credential. This will cancel the booking for all attendees.
275+
276+
\nCancelling recurring seated bookings:
277+
For recurring seated bookings it is not possible to cancel all of them with 1 call
271278
like with non-seated recurring bookings by providing recurring bookind uid - you have to cancel each recurrence booking by its bookingUid + seatUid.`,
272279
})
273280
@ApiBody({

apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/seated-bookings.e2e-spec.ts

Lines changed: 190 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -14,29 +14,27 @@ import { Test } from "@nestjs/testing";
1414
import { User } from "@prisma/client";
1515
import { DateTime } from "luxon";
1616
import * as request from "supertest";
17+
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
18+
import { BookingSeatRepositoryFixture } from "test/fixtures/repository/booking-seat.repository.fixture";
1719
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
1820
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
1921
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
2022
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
2123
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
2224
import { randomString } from "test/utils/randomString";
23-
import { withApiAuth } from "test/utils/withApiAuth";
2425

2526
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
2627
import {
28+
CancelBookingInput_2024_08_13,
2729
CancelSeatedBookingInput_2024_08_13,
2830
CreateRecurringSeatedBookingOutput_2024_08_13,
2931
CreateSeatedBookingOutput_2024_08_13,
3032
GetBookingOutput_2024_08_13,
3133
GetBookingsOutput_2024_08_13,
32-
GetRecurringSeatedBookingOutput_2024_08_13,
3334
GetSeatedBookingOutput_2024_08_13,
3435
RescheduleSeatedBookingInput_2024_08_13,
3536
} from "@calcom/platform-types";
36-
import {
37-
CreateBookingInput_2024_08_13,
38-
CreateRecurringBookingInput_2024_08_13,
39-
} from "@calcom/platform-types";
37+
import { CreateBookingInput_2024_08_13 } from "@calcom/platform-types";
4038
import { PlatformOAuthClient, Team } from "@calcom/prisma/client";
4139

4240
describe("Bookings Endpoints 2024-08-13", () => {
@@ -51,31 +49,31 @@ describe("Bookings Endpoints 2024-08-13", () => {
5149
let schedulesService: SchedulesService_2024_04_15;
5250
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
5351
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
54-
let oAuthClient: PlatformOAuthClient;
5552
let teamRepositoryFixture: TeamRepositoryFixture;
53+
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
54+
let bookingSeatRepositoryFixture: BookingSeatRepositoryFixture;
5655

5756
const userEmail = `seated-bookings-user-${randomString()}@api.com`;
5857
let user: User;
58+
let apiKeyString: string;
5959

6060
let seatedEventTypeId: number;
6161
const maxRecurrenceCount = 3;
6262

6363
const seatedEventSlug = `seated-bookings-event-type-${randomString()}`;
6464

6565
let createdSeatedBooking: CreateSeatedBookingOutput_2024_08_13;
66+
let createdSeatedBooking2: CreateSeatedBookingOutput_2024_08_13;
6667

6768
const emailAttendeeOne = `seated-bookings-attendee1-${randomString()}@api.com`;
6869
const nameAttendeeOne = `Attendee One ${randomString()}`;
6970
const emailAttendeeTwo = `seated-bookings-attendee2-${randomString()}@api.com`;
7071
const nameAttendeeTwo = `Attendee Two ${randomString()}`;
7172

7273
beforeAll(async () => {
73-
const moduleRef = await withApiAuth(
74-
userEmail,
75-
Test.createTestingModule({
76-
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
77-
})
78-
)
74+
const moduleRef = await Test.createTestingModule({
75+
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
76+
})
7977
.overrideGuard(PermissionsGuard)
8078
.useValue({
8179
canActivate: () => true,
@@ -87,22 +85,21 @@ describe("Bookings Endpoints 2024-08-13", () => {
8785
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
8886
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
8987
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
88+
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef);
9089
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
90+
bookingSeatRepositoryFixture = new BookingSeatRepositoryFixture(moduleRef);
9191

9292
organization = await teamRepositoryFixture.create({
9393
name: `seated-bookings-organization-${randomString()}`,
9494
});
95-
oAuthClient = await createOAuthClient(organization.id);
9695

9796
user = await userRepositoryFixture.create({
9897
email: userEmail,
99-
platformOAuthClients: {
100-
connect: {
101-
id: oAuthClient.id,
102-
},
103-
},
10498
});
10599

100+
const { keyString } = await apiKeysRepositoryFixture.createApiKey(user.id, null);
101+
apiKeyString = `cal_test_${keyString}`;
102+
106103
const userSchedule: CreateScheduleInput_2024_04_15 = {
107104
name: `seated-bookings-2024-08-13-schedule-${randomString()}`,
108105
timeZone: "Europe/Rome",
@@ -118,6 +115,14 @@ describe("Bookings Endpoints 2024-08-13", () => {
118115
seatsShowAttendees: true,
119116
seatsShowAvailabilityCount: true,
120117
locations: [{ type: "inPerson", address: "via 10, rome, italy" }],
118+
metadata: {
119+
disableStandardEmails: {
120+
all: {
121+
attendee: true,
122+
host: true,
123+
},
124+
},
125+
},
121126
},
122127
user.id
123128
);
@@ -129,19 +134,6 @@ describe("Bookings Endpoints 2024-08-13", () => {
129134
await app.init();
130135
});
131136

132-
async function createOAuthClient(organizationId: number) {
133-
const data = {
134-
logo: "logo-url",
135-
name: "name",
136-
redirectUris: ["http://localhost:5555"],
137-
permissions: 32,
138-
};
139-
const secret = "secret";
140-
141-
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
142-
return client;
143-
}
144-
145137
it("should book an event type with seats for the first time", async () => {
146138
const body: CreateBookingInput_2024_08_13 = {
147139
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
@@ -333,6 +325,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
333325
return request(app.getHttpServer())
334326
.get("/v2/bookings?sortCreated=asc")
335327
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
328+
.set("Authorization", `Bearer ${apiKeyString}`)
336329
.expect(200)
337330
.then(async (response) => {
338331
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
@@ -409,67 +402,186 @@ describe("Bookings Endpoints 2024-08-13", () => {
409402
});
410403
});
411404

412-
it("should cancel seated booking", async () => {
413-
const body: CancelSeatedBookingInput_2024_08_13 = {
414-
seatUid: createdSeatedBooking.seatUid,
415-
};
405+
describe("cancel seated booking", () => {
406+
describe("cancel seated booking as attendee", () => {
407+
it("should cancel seated booking", async () => {
408+
const body: CancelSeatedBookingInput_2024_08_13 = {
409+
seatUid: createdSeatedBooking.seatUid,
410+
};
411+
412+
return request(app.getHttpServer())
413+
.post(`/v2/bookings/${createdSeatedBooking.uid}/cancel`)
414+
.send(body)
415+
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
416+
.expect(200)
417+
.then(async (response) => {
418+
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
419+
expect(responseBody.status).toEqual(SUCCESS_STATUS);
420+
expect(responseBody.data).toBeDefined();
421+
expect(responseDataIsGetSeatedBooking(responseBody.data)).toBe(true);
422+
423+
if (responseDataIsGetSeatedBooking(responseBody.data)) {
424+
const data: GetSeatedBookingOutput_2024_08_13 = responseBody.data;
425+
expect(data.id).toBeDefined();
426+
expect(data.uid).toBeDefined();
427+
expect(data.hosts[0].id).toEqual(user.id);
428+
expect(data.status).toEqual("cancelled");
429+
expect(data.start).toEqual(createdSeatedBooking.start);
430+
expect(data.end).toEqual(createdSeatedBooking.end);
431+
expect(data.duration).toEqual(60);
432+
expect(data.eventTypeId).toEqual(seatedEventTypeId);
433+
expect(data.eventType).toEqual({
434+
id: seatedEventTypeId,
435+
slug: seatedEventSlug,
436+
});
437+
expect(data.attendees.length).toEqual(0);
438+
expect(data.location).toBeDefined();
439+
expect(data.absentHost).toEqual(false);
440+
} else {
441+
throw new Error("Invalid response data - expected booking but received array response");
442+
}
443+
});
444+
});
445+
});
416446

417-
return request(app.getHttpServer())
418-
.post(`/v2/bookings/${createdSeatedBooking.uid}/cancel`)
419-
.send(body)
420-
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
421-
.expect(200)
422-
.then(async (response) => {
423-
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
424-
expect(responseBody.status).toEqual(SUCCESS_STATUS);
425-
expect(responseBody.data).toBeDefined();
426-
expect(responseDataIsGetSeatedBooking(responseBody.data)).toBe(true);
447+
describe("cancel seated booking as host", () => {
448+
it("should book an event type with seats", async () => {
449+
const body: CreateBookingInput_2024_08_13 = {
450+
start: new Date(Date.UTC(2030, 0, 8, 14, 0, 0)).toISOString(),
451+
eventTypeId: seatedEventTypeId,
452+
attendee: {
453+
name: nameAttendeeOne,
454+
email: emailAttendeeOne,
455+
timeZone: "Europe/Rome",
456+
language: "it",
457+
},
458+
bookingFieldsResponses: {
459+
codingLanguage: "TypeScript",
460+
},
461+
metadata: {
462+
userId: "100",
463+
},
464+
};
465+
466+
return request(app.getHttpServer())
467+
.post("/v2/bookings")
468+
.send(body)
469+
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
470+
.expect(201)
471+
.then(async (response) => {
472+
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
473+
expect(responseBody.status).toEqual(SUCCESS_STATUS);
474+
expect(responseBody.data).toBeDefined();
475+
expect(responseDataIsCreateSeatedBooking(responseBody.data)).toBe(true);
476+
477+
if (responseDataIsCreateSeatedBooking(responseBody.data)) {
478+
const data: CreateSeatedBookingOutput_2024_08_13 = responseBody.data;
479+
expect(data.seatUid).toBeDefined();
480+
const seatUid = data.seatUid;
481+
expect(data.id).toBeDefined();
482+
expect(data.uid).toBeDefined();
483+
expect(data.hosts[0].id).toEqual(user.id);
484+
expect(data.status).toEqual("accepted");
485+
expect(data.start).toEqual(body.start);
486+
expect(data.end).toEqual(
487+
DateTime.fromISO(body.start, { zone: "utc" }).plus({ hours: 1 }).toISO()
488+
);
489+
expect(data.duration).toEqual(60);
490+
expect(data.eventTypeId).toEqual(seatedEventTypeId);
491+
expect(data.eventType).toEqual({
492+
id: seatedEventTypeId,
493+
slug: seatedEventSlug,
494+
});
495+
expect(data.attendees.length).toEqual(1);
496+
expect(data.attendees[0]).toEqual({
497+
name: body.attendee.name,
498+
email: body.attendee.email,
499+
timeZone: body.attendee.timeZone,
500+
language: body.attendee.language,
501+
absent: false,
502+
seatUid,
503+
bookingFieldsResponses: {
504+
name: body.attendee.name,
505+
...body.bookingFieldsResponses,
506+
},
507+
metadata: body.metadata,
508+
});
509+
expect(data.location).toBeDefined();
510+
expect(data.absentHost).toEqual(false);
511+
createdSeatedBooking2 = data;
512+
} else {
513+
throw new Error(
514+
"Invalid response data - expected recurring booking but received non array response"
515+
);
516+
}
517+
});
518+
});
427519

428-
if (responseDataIsGetSeatedBooking(responseBody.data)) {
429-
const data: GetSeatedBookingOutput_2024_08_13 = responseBody.data;
430-
expect(data.id).toBeDefined();
431-
expect(data.uid).toBeDefined();
432-
expect(data.hosts[0].id).toEqual(user.id);
433-
expect(data.status).toEqual("cancelled");
434-
expect(data.start).toEqual(createdSeatedBooking.start);
435-
expect(data.end).toEqual(createdSeatedBooking.end);
436-
expect(data.duration).toEqual(60);
437-
expect(data.eventTypeId).toEqual(seatedEventTypeId);
438-
expect(data.eventType).toEqual({
439-
id: seatedEventTypeId,
440-
slug: seatedEventSlug,
520+
it("should not be able to cancel without cancellation reason", async () => {
521+
const response = await request(app.getHttpServer())
522+
.post(`/v2/bookings/${createdSeatedBooking2.uid}/cancel`)
523+
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
524+
.set("Authorization", `Bearer ${apiKeyString}`)
525+
.expect(400);
526+
527+
expect(response.body.message).toEqual("Cancellation reason is required when you are the host");
528+
});
529+
530+
it("should cancel seated booking", async () => {
531+
const body: CancelBookingInput_2024_08_13 = {
532+
cancellationReason: "I will be travelling without internet",
533+
};
534+
535+
return request(app.getHttpServer())
536+
.post(`/v2/bookings/${createdSeatedBooking2.uid}/cancel`)
537+
.send(body)
538+
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
539+
.set("Authorization", `Bearer ${apiKeyString}`)
540+
.expect(200)
541+
.then(async (response) => {
542+
const responseBody: RescheduleBookingOutput_2024_08_13 = response.body;
543+
expect(responseBody.status).toEqual(SUCCESS_STATUS);
544+
expect(responseBody.data).toBeDefined();
545+
expect(responseDataIsGetSeatedBooking(responseBody.data)).toBe(true);
546+
547+
if (responseDataIsGetSeatedBooking(responseBody.data)) {
548+
const data: GetSeatedBookingOutput_2024_08_13 = responseBody.data;
549+
expect(data.id).toBeDefined();
550+
expect(data.uid).toBeDefined();
551+
expect(data.hosts[0].id).toEqual(user.id);
552+
expect(data.status).toEqual("cancelled");
553+
expect(data.start).toEqual(createdSeatedBooking2.start);
554+
expect(data.end).toEqual(createdSeatedBooking2.end);
555+
expect(data.duration).toEqual(60);
556+
expect(data.eventTypeId).toEqual(seatedEventTypeId);
557+
expect(data.eventType).toEqual({
558+
id: seatedEventTypeId,
559+
slug: seatedEventSlug,
560+
});
561+
expect(data.attendees.length).toEqual(0);
562+
expect(data.location).toBeDefined();
563+
expect(data.absentHost).toEqual(false);
564+
expect(data.cancellationReason).toEqual("I will be travelling without internet");
565+
566+
const seats = await bookingSeatRepositoryFixture.findAllByBookingId(data.id);
567+
expect(seats.length).toEqual(0);
568+
} else {
569+
throw new Error("Invalid response data - expected booking but received array response");
570+
}
441571
});
442-
expect(data.attendees.length).toEqual(0);
443-
expect(data.location).toBeDefined();
444-
expect(data.absentHost).toEqual(false);
445-
} else {
446-
throw new Error("Invalid response data - expected booking but received array response");
447-
}
448572
});
573+
});
449574
});
450575

451576
function responseDataIsCreateSeatedBooking(data: any): data is CreateSeatedBookingOutput_2024_08_13 {
452577
return data.hasOwnProperty("seatUid");
453578
}
454579

455-
function responseDataIsCreateRecurringSeatedBooking(
456-
data: any
457-
): data is CreateRecurringSeatedBookingOutput_2024_08_13[] {
458-
return Array.isArray(data);
459-
}
460-
461580
function responseDataIsGetSeatedBooking(data: any): data is GetSeatedBookingOutput_2024_08_13 {
462581
return data?.attendees?.every((attendee: any) => attendee?.hasOwnProperty("seatUid"));
463582
}
464583

465-
function responseDataIsGetRecurringSeatedBooking(
466-
data: any
467-
): data is GetRecurringSeatedBookingOutput_2024_08_13[] {
468-
return Array.isArray(data);
469-
}
470-
471584
afterAll(async () => {
472-
await oauthClientRepositoryFixture.delete(oAuthClient.id);
473585
await teamRepositoryFixture.delete(organization.id);
474586
await userRepositoryFixture.deleteByEmail(user.email);
475587
await bookingsRepositoryFixture.deleteAllBookings(user.id, user.email);

0 commit comments

Comments
 (0)