Skip to content

Commit bb9d99c

Browse files
authored
feat: api v2 event type settings - booker bookings limit, emails, round robin reschedule (calcom#24169)
* feat: event type bookerActiveBookingsLimit * test: booking with bookerActiveBookingsLimit * fix: typecheck * feat: org team event type emailSettings & rescheduleWithSameRoundRobinHost * docs: regenerate swagger * chore: default booker name and email in examples app * refactor: email settings description * refactor: rename email setting properties * refactor: function to add email settings to metadata * fix: try to fix e2e * fix: dont return input bookerActiveBookingsLimit in transformed object * fix: dont allow recurrence and bookerActiveBookingsLimit * fix: output service return bookerActiveBookingsLimit
1 parent 1aac140 commit bb9d99c

21 files changed

Lines changed: 938 additions & 69 deletions

File tree

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

Lines changed: 146 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2494,7 +2494,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
24942494
.expect(400);
24952495

24962496
expect(response.body.error.message).toEqual(
2497-
`Missing attendee phone number - it is required by the event type. Pass it as \"attendee.phoneNumber\" string in the request.`
2497+
`Missing attendee phone number - it is required by the event type. Pass it as "attendee.phoneNumber" string in the request.`
24982498
);
24992499
});
25002500

@@ -3026,13 +3026,154 @@ describe("Bookings Endpoints 2024-08-13", () => {
30263026
});
30273027
});
30283028
});
3029+
3030+
describe("event type with max bookings count per booker", () => {
3031+
it("should not allow booking more than maximumActiveBookings per attendee", async () => {
3032+
const eventTypeIdWithMaxBookerBookings = await eventTypesRepositoryFixture.create(
3033+
{
3034+
slug: `max-bookings-count-per-booker-${randomString(10)}`,
3035+
length: 60,
3036+
title: "Event Type with max bookings count per booker",
3037+
maxActiveBookingsPerBooker: 1,
3038+
},
3039+
user.id
3040+
);
3041+
3042+
const body: CreateBookingInput_2024_08_13 = {
3043+
start: new Date(Date.UTC(2040, 0, 13, 10, 0, 0)).toISOString(),
3044+
eventTypeId: eventTypeIdWithMaxBookerBookings.id,
3045+
attendee: {
3046+
name: "alice",
3047+
email: "alice@gmail.com",
3048+
timeZone: "Europe/Rome",
3049+
language: "it",
3050+
},
3051+
};
3052+
3053+
const response = await request(app.getHttpServer())
3054+
.post("/v2/bookings")
3055+
.send(body)
3056+
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
3057+
.expect(201);
3058+
3059+
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
3060+
expect(responseBody.status).toEqual(SUCCESS_STATUS);
3061+
expect(responseBody.data).toBeDefined();
3062+
expect(responseDataIsBooking(responseBody.data)).toBe(true);
3063+
3064+
if (responseDataIsBooking(responseBody.data)) {
3065+
const data: BookingOutput_2024_08_13 = responseBody.data;
3066+
expect(data.id).toBeDefined();
3067+
} else {
3068+
throw new Error(
3069+
"Invalid response data - expected booking but received array of possibly recurring bookings"
3070+
);
3071+
}
3072+
3073+
const response2 = await request(app.getHttpServer())
3074+
.post("/v2/bookings")
3075+
.send(body)
3076+
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
3077+
.expect(400);
3078+
3079+
expect(response2.body.error.message).toBe(
3080+
"Attendee with this email can't book because the maximum number of active bookings has been reached."
3081+
);
3082+
3083+
const body2: CreateBookingInput_2024_08_13 = {
3084+
start: new Date(Date.UTC(2040, 0, 13, 12, 0, 0)).toISOString(),
3085+
eventTypeId: eventTypeIdWithMaxBookerBookings.id,
3086+
attendee: {
3087+
name: "bob",
3088+
email: "bob@gmail.com",
3089+
timeZone: "Europe/Rome",
3090+
language: "it",
3091+
},
3092+
};
3093+
3094+
const response3 = await request(app.getHttpServer())
3095+
.post("/v2/bookings")
3096+
.send(body2)
3097+
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
3098+
.expect(201);
3099+
3100+
const responseBody2: CreateBookingOutput_2024_08_13 = response3.body;
3101+
expect(responseBody2.status).toEqual(SUCCESS_STATUS);
3102+
expect(responseBody2.data).toBeDefined();
3103+
expect(responseDataIsBooking(responseBody2.data)).toBe(true);
3104+
3105+
if (responseDataIsBooking(responseBody2.data)) {
3106+
const data: BookingOutput_2024_08_13 = responseBody2.data;
3107+
expect(data.id).toBeDefined();
3108+
} else {
3109+
throw new Error(
3110+
"Invalid response data - expected booking but received array of possibly recurring bookings"
3111+
);
3112+
}
3113+
});
3114+
3115+
it("should not allow booking more than maximumActiveBookings per attendee and offer rescheduling", async () => {
3116+
const eventTypeIdWithMaxBookerBookings = await eventTypesRepositoryFixture.create(
3117+
{
3118+
slug: `max-bookings-count-per-booker-with-reschedule-${randomString(10)}`,
3119+
length: 60,
3120+
title: "Event Type with max bookings count per booker with reschedule",
3121+
maxActiveBookingsPerBooker: 1,
3122+
maxActiveBookingPerBookerOfferReschedule: true,
3123+
},
3124+
user.id
3125+
);
3126+
3127+
const body: CreateBookingInput_2024_08_13 = {
3128+
start: new Date(Date.UTC(2040, 0, 13, 14, 0, 0)).toISOString(),
3129+
eventTypeId: eventTypeIdWithMaxBookerBookings.id,
3130+
attendee: {
3131+
name: "charlie",
3132+
email: "charlie@gmail.com",
3133+
timeZone: "Europe/Rome",
3134+
language: "it",
3135+
},
3136+
};
3137+
3138+
const response = await request(app.getHttpServer())
3139+
.post("/v2/bookings")
3140+
.send(body)
3141+
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
3142+
.expect(201);
3143+
3144+
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
3145+
expect(responseBody.status).toEqual(SUCCESS_STATUS);
3146+
expect(responseBody.data).toBeDefined();
3147+
expect(responseDataIsBooking(responseBody.data)).toBe(true);
3148+
3149+
if (responseDataIsBooking(responseBody.data)) {
3150+
const data: BookingOutput_2024_08_13 = responseBody.data;
3151+
expect(data.id).toBeDefined();
3152+
} else {
3153+
throw new Error(
3154+
"Invalid response data - expected booking but received array of possibly recurring bookings"
3155+
);
3156+
}
3157+
3158+
const response2 = await request(app.getHttpServer())
3159+
.post("/v2/bookings")
3160+
.send(body)
3161+
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
3162+
.expect(400);
3163+
3164+
expect(response2.body.error.message).toBe(
3165+
`Attendee with this email can't book because the maximum number of active bookings has been reached. You can reschedule your existing booking (${responseBody.data.uid}) to a new timeslot instead.`
3166+
);
3167+
});
3168+
});
30293169
});
30303170
});
30313171

3032-
function responseDataIsRecurranceBooking(data: any): data is RecurringBookingOutput_2024_08_13 {
3172+
function responseDataIsRecurranceBooking(data: unknown): data is RecurringBookingOutput_2024_08_13 {
30333173
return (
30343174
!Array.isArray(data) &&
30353175
typeof data === "object" &&
3176+
data !== null &&
30363177
data &&
30373178
"id" in data &&
30383179
"recurringBookingUid" in data
@@ -3048,11 +3189,11 @@ describe("Bookings Endpoints 2024-08-13", () => {
30483189
});
30493190
});
30503191

3051-
function responseDataIsBooking(data: any): data is BookingOutput_2024_08_13 {
3052-
return !Array.isArray(data) && typeof data === "object" && data && "id" in data;
3192+
function responseDataIsBooking(data: unknown): data is BookingOutput_2024_08_13 {
3193+
return !Array.isArray(data) && typeof data === "object" && data !== null && data && "id" in data;
30533194
}
30543195

3055-
function responseDataIsRecurringBooking(data: any): data is RecurringBookingOutput_2024_08_13[] {
3196+
function responseDataIsRecurringBooking(data: unknown): data is RecurringBookingOutput_2024_08_13[] {
30563197
return Array.isArray(data);
30573198
}
30583199
});

apps/api/v2/src/ee/bookings/2024-08-13/services/errors.service.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,19 @@ export class ErrorsBookingsService_2024_08_13 {
4848
throw new BadRequestException("Attempting to book a meeting in the past.");
4949
} else if (error.message === "hosts_unavailable_for_booking") {
5050
throw new BadRequestException(hostsUnavaile);
51+
} else if (error.message === "booker_limit_exceeded_error") {
52+
throw new BadRequestException(
53+
"Attendee with this email can't book because the maximum number of active bookings has been reached."
54+
);
55+
} else if (error.message === "booker_limit_exceeded_error_reschedule") {
56+
const errorData =
57+
"data" in error ? (error.data as { rescheduleUid: string }) : { rescheduleUid: undefined };
58+
let message =
59+
"Attendee with this email can't book because the maximum number of active bookings has been reached.";
60+
if (errorData?.rescheduleUid) {
61+
message += ` You can reschedule your existing booking (${errorData.rescheduleUid}) to a new timeslot instead.`;
62+
}
63+
throw new BadRequestException(message);
5164
}
5265
}
5366
throw error;

0 commit comments

Comments
 (0)