diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/variable-length-bookings.e2e-spec.ts b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/variable-length-bookings.e2e-spec.ts index 66cd544e84..3a41065a93 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/variable-length-bookings.e2e-spec.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/controllers/e2e/variable-length-bookings.e2e-spec.ts @@ -21,7 +21,12 @@ import { randomString } from "test/utils/randomString"; import { withApiAuth } from "test/utils/withApiAuth"; import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants"; -import { CreateBookingInput_2024_08_13, BookingOutput_2024_08_13 } from "@calcom/platform-types"; +import { + CreateBookingInput_2024_08_13, + BookingOutput_2024_08_13, + CreateRecurringBookingInput_2024_08_13, + RecurringBookingOutput_2024_08_13, +} from "@calcom/platform-types"; import { PlatformOAuthClient, Team } from "@calcom/prisma/client"; describe("Bookings Endpoints 2024-08-13", () => { @@ -44,6 +49,8 @@ describe("Bookings Endpoints 2024-08-13", () => { const VARIABLE_LENGTH_EVENT_TYPE_SLUG = `variable-length-bookings-2024-08-13-event-type-${randomString()}`; let normalEventType: EventType; const NORMAL_EVENT_TYPE_SLUG = `variable-length-bookings-2024-08-13-event-type-${randomString()}`; + let variableLengthRecurringEventType: EventType; + const VARIABLE_LENGTH_RECURRING_EVENT_TYPE_SLUG = `variable-length-recurring-bookings-2024-08-13-event-type-${randomString()}`; beforeAll(async () => { const moduleRef = await withApiAuth( @@ -105,6 +112,17 @@ describe("Bookings Endpoints 2024-08-13", () => { user.id ); + variableLengthRecurringEventType = await eventTypesRepositoryFixture.create( + { + title: `variable-length-recurring-bookings-2024-08-13-event-type-${randomString()}`, + slug: VARIABLE_LENGTH_RECURRING_EVENT_TYPE_SLUG, + length: 15, + metadata: { multipleDuration: [15, 30, 60] }, + recurringEvent: { freq: 2, count: 3, interval: 1 }, // Weekly, 3 times, every 1 week + }, + user.id + ); + app = moduleRef.createNestApplication(); bootstrap(app as NestExpressApplication); @@ -246,6 +264,63 @@ describe("Bookings Endpoints 2024-08-13", () => { }); }); + describe("create recurring bookings", () => { + it("should create recurring bookings with custom lengthInMinutes", async () => { + const lengthInMinutes = 30; + const body: CreateRecurringBookingInput_2024_08_13 = { + start: new Date(Date.UTC(2030, 1, 1, 14, 0, 0)).toISOString(), + eventTypeId: variableLengthRecurringEventType.id, + lengthInMinutes, + recurrenceCount: 2, + attendee: { + name: "Mr Recurring", + email: "mr_recurring@gmail.com", + timeZone: "Europe/Rome", + language: "it", + }, + }; + + return request(app.getHttpServer()) + .post("/v2/bookings") + .send(body) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(201) + .then(async (response) => { + const responseBody: { status: string; data: RecurringBookingOutput_2024_08_13[] } = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + expect(Array.isArray(responseBody.data)).toBe(true); + expect(responseBody.data).toHaveLength(2); + + const firstBooking = responseBody.data[0]; + const secondBooking = responseBody.data[1]; + expect(firstBooking.duration).toEqual(lengthInMinutes); + expect(secondBooking.duration).toEqual(lengthInMinutes); + }); + }); + + it("should reject recurring booking with invalid lengthInMinutes", async () => { + const body: CreateRecurringBookingInput_2024_08_13 = { + start: new Date(Date.UTC(2030, 3, 1, 10, 0, 0)).toISOString(), + eventTypeId: variableLengthRecurringEventType.id, + lengthInMinutes: 45, // Not in allowed [15, 30, 60] + recurrenceCount: 2, + attendee: { + name: "Mr Invalid", + email: "mr_invalid@gmail.com", + timeZone: "Europe/Rome", + language: "it", + }, + }; + + return request(app.getHttpServer()) + .post("/v2/bookings") + .send(body) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(400); + }); + }); + afterAll(async () => { await oauthClientRepositoryFixture.delete(oAuthClient.id); await teamRepositoryFixture.delete(organization.id); diff --git a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts index 2533caa383..4ed19b9130 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/input.service.ts @@ -420,6 +420,9 @@ export class InputBookingsService_2024_08_13 { throw new NotFoundException(`Event type with id=${inputBooking.eventTypeId} is not a recurring event`); } + this.validateBookingLengthInMinutes(inputBooking, eventType); + const lengthInMinutes = inputBooking.lengthInMinutes ?? eventType.length; + const occurrence = recurringEventSchema.parse(eventType.recurringEvent); const repeatsEvery = occurrence.interval; @@ -456,7 +459,7 @@ export class InputBookingsService_2024_08_13 { const location = inputLocation ? this.transformLocation(inputLocation) : undefined; for (let i = 0; i < repeatsTimes; i++) { - const endTime = startTime.plus({ minutes: eventType.length }); + const endTime = startTime.plus({ minutes: lengthInMinutes }); events.push({ start: startTime.toISO(),