From 689ce1ec7f46d328e40315c134effd78d7dd8d51 Mon Sep 17 00:00:00 2001 From: Rajiv Sahal Date: Fri, 16 Jan 2026 19:23:50 +0530 Subject: [PATCH] fix: rescheduled booking with multiple durations does not preserve the duration (API v2) (#26746) * fix: make sure we dont implement directly from @calcom/features * fix: preserve the original booking duration instead of using the default event type length * chore: update tests * fix: merge conflicts * chore: revert linting changes * chore: implement PR feedback --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../e2e/variable-length-bookings.e2e-spec.ts | 75 +++++ .../2024-08-13/services/bookings.service.ts | 2 +- .../2024-08-13/services/input.service.ts | 302 ++++++++++++++---- packages/platform/libraries/index.ts | 14 +- 4 files changed, 317 insertions(+), 76 deletions(-) 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 e895c6a230..791eda9a34 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 @@ -4,6 +4,7 @@ import type { CreateBookingInput_2024_08_13, CreateRecurringBookingInput_2024_08_13, RecurringBookingOutput_2024_08_13, + RescheduleBookingInput_2024_08_13, } from "@calcom/platform-types"; import type { EventType, PlatformOAuthClient, Team, User } from "@calcom/prisma/client"; import { INestApplication } from "@nestjs/common"; @@ -17,9 +18,11 @@ import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository. import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture"; import { randomString } from "test/utils/randomString"; import { withApiAuth } from "test/utils/withApiAuth"; + import { AppModule } from "@/app.module"; import { bootstrap } from "@/bootstrap"; import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output"; +import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output"; import { CreateScheduleInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-schedule.input"; import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module"; import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service"; @@ -319,6 +322,78 @@ describe("Bookings Endpoints 2024-08-13", () => { }); }); + describe("reschedule bookings with non-default duration", () => { + let bookingWithNonDefaultDuration: BookingOutput_2024_08_13; + const nonDefaultLengthInMinutes = 30; + + it("should create a booking with non-default duration to be rescheduled", async () => { + const body: CreateBookingInput_2024_08_13 = { + lengthInMinutes: nonDefaultLengthInMinutes, + start: new Date(Date.UTC(2030, 0, 9, 10, 0, 0)).toISOString(), + eventTypeId: variableLengthEventType.id, + attendee: { + name: "Mr Reschedule", + email: "mr_reschedule@gmail.com", + timeZone: "Europe/Rome", + language: "it", + }, + location: "https://meet.google.com/abc-def-ghi", + }; + + 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: CreateBookingOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + expect(responseDataIsBooking(responseBody.data)).toBe(true); + + if (responseDataIsBooking(responseBody.data)) { + bookingWithNonDefaultDuration = responseBody.data; + expect(bookingWithNonDefaultDuration.duration).toEqual(nonDefaultLengthInMinutes); + } else { + throw new Error("Invalid response data - expected booking"); + } + }); + }); + + it("should reschedule and preserve the original non-default duration", async () => { + const newStartTime = new Date(Date.UTC(2030, 0, 9, 12, 0, 0)).toISOString(); + const body: RescheduleBookingInput_2024_08_13 = { + start: newStartTime, + reschedulingReason: "Testing duration preservation", + }; + + return request(app.getHttpServer()) + .post(`/v2/bookings/${bookingWithNonDefaultDuration.uid}/reschedule`) + .send(body) + .set(CAL_API_VERSION_HEADER, VERSION_2024_08_13) + .expect(201) + .then(async (response) => { + const responseBody: RescheduleBookingOutput_2024_08_13 = response.body; + expect(responseBody.status).toEqual(SUCCESS_STATUS); + expect(responseBody.data).toBeDefined(); + + const rescheduledBooking = responseBody.data as BookingOutput_2024_08_13; + + // verify the duration is preserved (should be 30 minutes, not the default 15) + expect(rescheduledBooking.duration).toEqual(nonDefaultLengthInMinutes); + expect(rescheduledBooking.duration).toEqual(bookingWithNonDefaultDuration.duration); + + // verify the end time is correct (start + 30 minutes) + const expectedEndTime = new Date(Date.UTC(2030, 0, 9, 12, nonDefaultLengthInMinutes, 0)).toISOString(); + expect(rescheduledBooking.start).toEqual(newStartTime); + expect(rescheduledBooking.end).toEqual(expectedEndTime); + + // verify it's linked to the original booking + expect(rescheduledBooking.rescheduledFromUid).toEqual(bookingWithNonDefaultDuration.uid); + }); + }); + }); + 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/bookings.service.ts b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts index 6b9011e2a2..072ac65ca4 100644 --- a/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts +++ b/apps/api/v2/src/ee/bookings/2024-08-13/services/bookings.service.ts @@ -1216,4 +1216,4 @@ export class BookingsService_2024_08_13 { t: await getTranslation("en", "common"), }); } -} +} \ No newline at end of file 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 165602b970..01acbd1903 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 @@ -19,12 +19,16 @@ import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repos import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service"; import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service"; import { UsersRepository } from "@/modules/users/users.repository"; -import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common"; +import { + BadRequestException, + Injectable, + NotFoundException, +} from "@nestjs/common"; import { Logger } from "@nestjs/common"; import { ConfigService } from "@nestjs/config"; import { isURL, isPhoneNumber } from "class-validator"; import { Request } from "express"; -import { DateTime } from "luxon"; +import { DateInput, DateTime } from "luxon"; import { NextApiRequest } from "next/types"; import { v4 as uuidv4 } from "uuid"; import { z } from "zod"; @@ -101,7 +105,10 @@ export class InputBookingsService_2024_08_13 { body: CreateBookingInput_2024_08_13 | CreateInstantBookingInput_2024_08_13, eventType: EventTypeWithOwnerAndTeam ): Promise { - const oAuthClientParams = await this.platformBookingsService.getOAuthClientParamsForEventType(eventType); + const oAuthClientParams = + await this.platformBookingsService.getOAuthClientParamsForEventType( + eventType + ); const bodyTransformed = await this.transformInputCreateBooking( body, eventType, @@ -109,7 +116,8 @@ export class InputBookingsService_2024_08_13 { ); const newRequest = { ...request }; - const userId = (await this.createBookingRequestOwnerId(request)) ?? undefined; + const userId = + (await this.createBookingRequestOwnerId(request)) ?? undefined; this.logger.log(`createBookingRequest_2024_08_13`, { requestId: request.get("X-Request-Id"), @@ -126,7 +134,11 @@ export class InputBookingsService_2024_08_13 { }; } else { Object.assign(newRequest, { userId }); - newRequest.body = { ...bodyTransformed, noEmail: false, creationSource: CreationSource.API_V2 }; + newRequest.body = { + ...bodyTransformed, + noEmail: false, + creationSource: CreationSource.API_V2, + }; } return newRequest as unknown as BookingRequest; @@ -140,14 +152,17 @@ export class InputBookingsService_2024_08_13 { this.validateBookingLengthInMinutes(inputBooking, eventType); const lengthInMinutes = inputBooking.lengthInMinutes ?? eventType.length; - const startTime = DateTime.fromISO(inputBooking.start, { zone: "utc" }).setZone( - inputBooking.attendee.timeZone - ); + const startTime = DateTime.fromISO(inputBooking.start, { + zone: "utc", + }).setZone(inputBooking.attendee.timeZone); const endTime = startTime.plus({ minutes: lengthInMinutes }); const guests = inputBooking.guests && platformClientId - ? await this.platformBookingsService.getPlatformAttendeesEmails(inputBooking.guests, platformClientId) + ? await this.platformBookingsService.getPlatformAttendeesEmails( + inputBooking.guests, + platformClientId + ) : inputBooking.guests; const attendeeEmail = inputBooking.attendee.email && platformClientId @@ -159,7 +174,9 @@ export class InputBookingsService_2024_08_13 { const inputLocation = inputBooking.location || inputBooking.meetingUrl; this.isBookingLocationWithEventTypeLocations(inputLocation, eventType); - const location = inputLocation ? this.transformLocation(inputLocation) : undefined; + const location = inputLocation + ? this.transformLocation(inputLocation) + : undefined; const needsSmsReminderNumber = eventType.bookingFields ? eventTypeBookingFieldsSchema @@ -183,7 +200,9 @@ export class InputBookingsService_2024_08_13 { name: inputBooking.attendee.name, email: attendeeEmail ?? "", attendeePhoneNumber: inputBooking.attendee.phoneNumber, - smsReminderNumber: needsSmsReminderNumber ? inputBooking.attendee.phoneNumber : undefined, + smsReminderNumber: needsSmsReminderNumber + ? inputBooking.attendee.phoneNumber + : undefined, guests, location, }, @@ -216,7 +235,10 @@ export class InputBookingsService_2024_08_13 { }; } - validateBookingLengthInMinutes(inputBooking: CreateBookingInput_2024_08_13, eventType: EventType) { + validateBookingLengthInMinutes( + inputBooking: CreateBookingInput_2024_08_13, + eventType: EventType + ) { const eventTypeMetadata = EventTypeMetaDataSchema.parse(eventType.metadata); if (inputBooking.lengthInMinutes && !eventTypeMetadata?.multipleDuration) { throw new BadRequestException( @@ -225,7 +247,9 @@ export class InputBookingsService_2024_08_13 { } if ( inputBooking.lengthInMinutes && - !eventTypeMetadata?.multipleDuration?.includes(inputBooking.lengthInMinutes) + !eventTypeMetadata?.multipleDuration?.includes( + inputBooking.lengthInMinutes + ) ) { throw new BadRequestException( `Provided 'lengthInMinutes' is not one of the possible lengths for the event type. The possible lengths are: ${eventTypeMetadata?.multipleDuration?.join( @@ -240,7 +264,10 @@ export class InputBookingsService_2024_08_13 { body: CreateRecurringBookingInput_2024_08_13, eventType: EventTypeWithOwnerAndTeam ): Promise { - const oAuthClientParams = await this.platformBookingsService.getOAuthClientParamsForEventType(eventType); + const oAuthClientParams = + await this.platformBookingsService.getOAuthClientParamsForEventType( + eventType + ); // note(Lauris): update to this.transformInputCreate when rescheduling is implemented const bodyTransformed = await this.transformInputCreateRecurringBooking( body, @@ -249,7 +276,8 @@ export class InputBookingsService_2024_08_13 { ); const newRequest = { ...request }; - const userId = (await this.createBookingRequestOwnerId(request)) ?? undefined; + const userId = + (await this.createBookingRequestOwnerId(request)) ?? undefined; if (oAuthClientParams) { Object.assign(newRequest, { @@ -283,7 +311,10 @@ export class InputBookingsService_2024_08_13 { // note(Lauris): this is for backwards compatibility because before switching to booking location objects // we only received a string. If someone is complaining that their location is not displaying as a URL // or whatever check that they are not providing a string for bookign location but one of the input objects. - if (isURL(location, { require_protocol: false }) || location.startsWith("www.")) { + if ( + isURL(location, { require_protocol: false }) || + location.startsWith("www.") + ) { return { value: "link", optionValue: location, @@ -304,9 +335,12 @@ export class InputBookingsService_2024_08_13 { } if (location.type === "integration") { - const integration = apiToInternalintegrationsMapping[location.integration]; + const integration = + apiToInternalintegrationsMapping[location.integration]; if (!integration) { - throw new BadRequestException(`Invalid integration: ${location.integration}`); + throw new BadRequestException( + `Invalid integration: ${location.integration}` + ); } return { value: integration, @@ -364,7 +398,9 @@ export class InputBookingsService_2024_08_13 { } throw new BadRequestException( - `Booking location with type ${(location as BookingInputLocation_2024_08_13).type} not valid.` + `Booking location with type ${ + (location as BookingInputLocation_2024_08_13).type + } not valid.` ); } @@ -377,13 +413,19 @@ export class InputBookingsService_2024_08_13 { return true; } - const eventTypeLocations = this.outputEventTypesService.transformLocations(dbEventType.locations); - const allowedLocationTypes = eventTypeLocations.map((location) => location.type); + const eventTypeLocations = this.outputEventTypesService.transformLocations( + dbEventType.locations + ); + const allowedLocationTypes = eventTypeLocations.map( + (location) => location.type + ); const isAllowed = allowedLocationTypes.includes(inputBookingLocation.type); if (!isAllowed) { throw new BadRequestException( - `Booking location with type ${inputBookingLocation.type} not valid for event type with id=${ + `Booking location with type ${ + inputBookingLocation.type + } not valid for event type with id=${ dbEventType.id }. The event type has following location types: ${allowedLocationTypes.join( ", " @@ -391,12 +433,17 @@ export class InputBookingsService_2024_08_13 { ); } - if (inputBookingLocation.type === "integration" && "integration" in inputBookingLocation) { + if ( + inputBookingLocation.type === "integration" && + "integration" in inputBookingLocation + ) { const allowedIntegrations = eventTypeLocations .filter((location) => location.type === "integration") .map((location) => location.integration); - const isAllowedIntegration = allowedIntegrations.includes(inputBookingLocation.integration); + const isAllowedIntegration = allowedIntegrations.includes( + inputBookingLocation.integration + ); if (!isAllowedIntegration) { throw new BadRequestException( `Booking location with integration ${ @@ -419,7 +466,9 @@ export class InputBookingsService_2024_08_13 { platformClientId?: string ) { if (!eventType.recurringEvent) { - throw new NotFoundException(`Event type with id=${inputBooking.eventTypeId} is not a recurring event`); + throw new NotFoundException( + `Event type with id=${inputBooking.eventTypeId} is not a recurring event` + ); } this.validateBookingLengthInMinutes(inputBooking, eventType); @@ -428,7 +477,10 @@ export class InputBookingsService_2024_08_13 { const occurrence = recurringEventSchema.parse(eventType.recurringEvent); const repeatsEvery = occurrence.interval; - if (inputBooking.recurrenceCount && inputBooking.recurrenceCount > occurrence.count) { + if ( + inputBooking.recurrenceCount && + inputBooking.recurrenceCount > occurrence.count + ) { throw new BadRequestException( "Provided recurrence count is higher than the event type's recurring event count." ); @@ -440,13 +492,16 @@ export class InputBookingsService_2024_08_13 { const events = []; const recurringEventId = uuidv4(); - let startTime = DateTime.fromISO(inputBooking.start, { zone: "utc" }).setZone( - inputBooking.attendee.timeZone - ); + let startTime = DateTime.fromISO(inputBooking.start, { + zone: "utc", + }).setZone(inputBooking.attendee.timeZone); const guests = inputBooking.guests && platformClientId - ? await this.platformBookingsService.getPlatformAttendeesEmails(inputBooking.guests, platformClientId) + ? await this.platformBookingsService.getPlatformAttendeesEmails( + inputBooking.guests, + platformClientId + ) : inputBooking.guests; const attendeeEmail = inputBooking.attendee.email && platformClientId @@ -458,7 +513,9 @@ export class InputBookingsService_2024_08_13 { const inputLocation = inputBooking.location || inputBooking.meetingUrl; this.isBookingLocationWithEventTypeLocations(inputLocation, eventType); - const location = inputLocation ? this.transformLocation(inputLocation) : undefined; + const location = inputLocation + ? this.transformLocation(inputLocation) + : undefined; for (let i = 0; i < repeatsTimes; i++) { const endTime = startTime.plus({ minutes: lengthInMinutes }); @@ -515,11 +572,16 @@ export class InputBookingsService_2024_08_13 { const bodyTransformed = isIndividualSeatReschedule && "seatUid" in body ? await this.transformInputRescheduleSeatedBooking(bookingUid, body) - : await this.transformInputRescheduleBooking(bookingUid, body, isIndividualSeatReschedule); + : await this.transformInputRescheduleBooking( + bookingUid, + body, + isIndividualSeatReschedule + ); - const oAuthClientParams = await this.platformBookingsService.getOAuthClientParams( - bodyTransformed.eventTypeId - ); + const oAuthClientParams = + await this.platformBookingsService.getOAuthClientParams( + bodyTransformed.eventTypeId + ); const newRequest = { ...request }; let userId: number | undefined = undefined; @@ -537,13 +599,19 @@ export class InputBookingsService_2024_08_13 { if (request.body.rescheduledBy) { if (request.body.rescheduledBy !== bodyTransformed.responses.email) { - userId = (await this.usersRepository.findByEmail(request.body.rescheduledBy))?.id; + userId = ( + await this.usersRepository.findByEmail(request.body.rescheduledBy) + )?.id; } } const location = await this.getRescheduleBookingLocation(bookingUid); if (oAuthClientParams) { - Object.assign(newRequest, { userId, ...oAuthClientParams, platformBookingLocation: location }); + Object.assign(newRequest, { + userId, + ...oAuthClientParams, + platformBookingLocation: location, + }); newRequest.body = { ...bodyTransformed, rescheduledBy: request.body.rescheduledBy, @@ -563,7 +631,9 @@ export class InputBookingsService_2024_08_13 { return newRequest as unknown as BookingRequest; } - isRescheduleSeatedBody(body: RescheduleBookingInput): body is RescheduleSeatedBookingInput_2024_08_13 { + isRescheduleSeatedBody( + body: RescheduleBookingInput + ): body is RescheduleSeatedBookingInput_2024_08_13 { return Object.prototype.hasOwnProperty.call(body, "seatUid"); } @@ -571,26 +641,44 @@ export class InputBookingsService_2024_08_13 { bookingUid: string, inputBooking: RescheduleSeatedBookingInput_2024_08_13 ) { - const booking = await this.bookingsRepository.getByUidWithAttendeesAndUserAndEvent(bookingUid); + const booking = + await this.bookingsRepository.getByUidWithAttendeesAndUserAndEvent( + bookingUid + ); // todo create booking seat module, repository and fetch the seat to get info if (!booking) { throw new NotFoundException(`Booking with uid=${bookingUid} not found`); } if (!booking.eventTypeId) { - throw new NotFoundException(`Booking with uid=${bookingUid} is missing event type`); + throw new NotFoundException( + `Booking with uid=${bookingUid} is missing event type` + ); } - const eventType = await this.eventTypesRepository.getEventTypeByIdWithOwnerAndTeam(booking.eventTypeId); + const eventType = + await this.eventTypesRepository.getEventTypeByIdWithOwnerAndTeam( + booking.eventTypeId + ); if (!eventType) { - throw new NotFoundException(`Event type with id=${booking.eventTypeId} not found`); + throw new NotFoundException( + `Event type with id=${booking.eventTypeId} not found` + ); } - const seat = await this.bookingSeatRepository.getByReferenceUid(inputBooking.seatUid); + const seat = await this.bookingSeatRepository.getByReferenceUid( + inputBooking.seatUid + ); if (!seat) { - throw new NotFoundException(`Seat with uid=${inputBooking.seatUid} does not exist.`); + throw new NotFoundException( + `Seat with uid=${inputBooking.seatUid} does not exist.` + ); } - const { responses: bookingResponses } = seatedBookingDataSchema.parse(seat.data); - const attendee = booking.attendees.find((attendee) => attendee.email === bookingResponses.email); + const { responses: bookingResponses } = seatedBookingDataSchema.parse( + seat.data + ); + const attendee = booking.attendees.find( + (attendee) => attendee.email === bookingResponses.email + ); if (!attendee) { throw new NotFoundException( @@ -598,8 +686,17 @@ export class InputBookingsService_2024_08_13 { ); } - const startTime = DateTime.fromISO(inputBooking.start, { zone: "utc" }).setZone(attendee.timeZone); - const endTime = startTime.plus({ minutes: eventType.length }); + // preserve the original booking duration instead of using the default event type length + // this ensures that bookings with non-default durations (from multi-duration event types) are preserved on reschedule + const originalDurationInMinutes = this.getOriginalBookingDuration( + booking.startTime, + booking.endTime + ); + + const startTime = DateTime.fromISO(inputBooking.start, { + zone: "utc", + }).setZone(attendee.timeZone); + const endTime = startTime.plus({ minutes: originalDurationInMinutes }); return { start: startTime.toISO(), @@ -621,16 +718,26 @@ export class InputBookingsService_2024_08_13 { inputBooking: RescheduleBookingInput_2024_08_13, isIndividualSeatReschedule: boolean ) { - const booking = await this.bookingsRepository.getByUidWithAttendeesAndUserAndEvent(bookingUid); + const booking = + await this.bookingsRepository.getByUidWithAttendeesAndUserAndEvent( + bookingUid + ); if (!booking) { throw new NotFoundException(`Booking with uid=${bookingUid} not found`); } if (!booking.eventTypeId) { - throw new NotFoundException(`Booking with uid=${bookingUid} is missing event type`); + throw new NotFoundException( + `Booking with uid=${bookingUid} is missing event type` + ); } - const eventType = await this.eventTypesRepository.getEventTypeByIdWithOwnerAndTeam(booking.eventTypeId); + const eventType = + await this.eventTypesRepository.getEventTypeByIdWithOwnerAndTeam( + booking.eventTypeId + ); if (!eventType) { - throw new NotFoundException(`Event type with id=${booking.eventTypeId} not found`); + throw new NotFoundException( + `Event type with id=${booking.eventTypeId} not found` + ); } if (eventType.seatsPerTimeSlot && !isIndividualSeatReschedule) { throw new BadRequestException( @@ -650,7 +757,9 @@ export class InputBookingsService_2024_08_13 { const attendee = bookingResponsesMissing ? booking.attendees[0] - : booking.attendees.find((attendee) => attendee.email === bookingResponses.email); + : booking.attendees.find( + (attendee) => attendee.email === bookingResponses.email + ); if (!attendee) { throw new NotFoundException( @@ -664,8 +773,17 @@ export class InputBookingsService_2024_08_13 { bookingResponses.attendeePhoneNumber = attendee.phoneNumber || undefined; } - const startTime = DateTime.fromISO(inputBooking.start, { zone: "utc" }).setZone(attendee.timeZone); - const endTime = startTime.plus({ minutes: eventType.length }); + // preserve the original booking duration instead of using the default event type length + // this ensures that bookings with non-default durations (from multi-duration event types) are preserved on reschedule + const originalDurationInMinutes = this.getOriginalBookingDuration( + booking.startTime, + booking.endTime + ); + + const startTime = DateTime.fromISO(inputBooking.start, { + zone: "utc", + }).setZone(attendee.timeZone); + const endTime = startTime.plus({ minutes: originalDurationInMinutes }); return { start: startTime.toISO(), end: endTime.toISO(), @@ -675,28 +793,47 @@ export class InputBookingsService_2024_08_13 { metadata: booking.metadata || {}, hasHashedBookingLink: false, guests: bookingResponses.guests, - responses: { ...bookingResponses, rescheduledReason: inputBooking.reschedulingReason }, + responses: { + ...bookingResponses, + rescheduledReason: inputBooking.reschedulingReason, + }, rescheduleUid: bookingUid, verificationCode: inputBooking.emailVerificationCode, }; } async getRescheduleBookingLocation(rescheduleBookingUid: string) { - const booking = await this.bookingsRepository.getByUid(rescheduleBookingUid); + const booking = await this.bookingsRepository.getByUid( + rescheduleBookingUid + ); if (!booking) { - throw new NotFoundException(`Booking with uid=${rescheduleBookingUid} not found`); + throw new NotFoundException( + `Booking with uid=${rescheduleBookingUid} not found` + ); } return booking.location; } - private async createBookingRequestOwnerId(req: Request): Promise { + private async createBookingRequestOwnerId( + req: Request + ): Promise { try { const bearerToken = req.get("Authorization")?.replace("Bearer ", ""); if (bearerToken) { - if (isApiKey(bearerToken, this.config.get("api.apiKeyPrefix") ?? "cal_")) { - const strippedApiKey = stripApiKey(bearerToken, this.config.get("api.keyPrefix")); + if ( + isApiKey( + bearerToken, + this.config.get("api.apiKeyPrefix") ?? "cal_" + ) + ) { + const strippedApiKey = stripApiKey( + bearerToken, + this.config.get("api.keyPrefix") + ); const apiKeyHash = sha256Hash(strippedApiKey); - const keyData = await this.apiKeyRepository.getApiKeyFromHash(apiKeyHash); + const keyData = await this.apiKeyRepository.getApiKeyFromHash( + apiKeyHash + ); return keyData?.userId; } else { // Access Token @@ -715,9 +852,12 @@ export class InputBookingsService_2024_08_13 { attendeeName: queryParams.attendeeName, afterStartDate: queryParams.afterStart, beforeEndDate: queryParams.beforeEnd, - teamIds: queryParams.teamsIds || (queryParams.teamId ? [queryParams.teamId] : undefined), + teamIds: + queryParams.teamsIds || + (queryParams.teamId ? [queryParams.teamId] : undefined), eventTypeIds: - queryParams.eventTypeIds || (queryParams.eventTypeId ? [queryParams.eventTypeId] : undefined), + queryParams.eventTypeIds || + (queryParams.eventTypeId ? [queryParams.eventTypeId] : undefined), afterUpdatedDate: queryParams.afterUpdatedAt, beforeUpdatedDate: queryParams.beforeUpdatedAt, afterCreatedDate: queryParams.afterCreatedAt, @@ -765,15 +905,21 @@ export class InputBookingsService_2024_08_13 { } const oAuthClientParams = booking.eventTypeId - ? await this.platformBookingsService.getOAuthClientParams(booking.eventTypeId) + ? await this.platformBookingsService.getOAuthClientParams( + booking.eventTypeId + ) : undefined; const newRequest = { ...request }; - const userId = (await this.createBookingRequestOwnerId(request)) ?? undefined; + const userId = + (await this.createBookingRequestOwnerId(request)) ?? undefined; if (oAuthClientParams) { Object.assign(newRequest, { userId, ...oAuthClientParams }); - newRequest.body = { ...bodyTransformed, noEmail: !oAuthClientParams.arePlatformEmailsEnabled }; + newRequest.body = { + ...bodyTransformed, + noEmail: !oAuthClientParams.arePlatformEmailsEnabled, + }; } else { Object.assign(newRequest, { userId }); newRequest.body = { ...bodyTransformed, noEmail: false }; @@ -782,12 +928,19 @@ export class InputBookingsService_2024_08_13 { return newRequest as unknown as BookingRequest; } - isCancelSeatedBody(body: CancelBookingInput): body is CancelSeatedBookingInput_2024_08_13 { + isCancelSeatedBody( + body: CancelBookingInput + ): body is CancelSeatedBookingInput_2024_08_13 { return Object.prototype.hasOwnProperty.call(body, "seatUid"); } - async transformInputCancelBooking(bookingUid: string, inputBooking: CancelBookingInput_2024_08_13) { - const recurringBooking = await this.bookingsRepository.getRecurringByUid(bookingUid); + async transformInputCancelBooking( + bookingUid: string, + inputBooking: CancelBookingInput_2024_08_13 + ) { + const recurringBooking = await this.bookingsRepository.getRecurringByUid( + bookingUid + ); // note(Lauris): isRecurring means that recurringEventId was passed as uid. isRecurring does not refer to the uid of 1 individual booking within a recurring booking consisting of many bookings. // That is what recurringEventId refers to. const isRecurringUid = !!recurringBooking.length; @@ -836,7 +989,9 @@ export class InputBookingsService_2024_08_13 { }; } - transformInputMarkAbsentBooking(inputBooking: MarkAbsentBookingInput_2024_08_13) { + transformInputMarkAbsentBooking( + inputBooking: MarkAbsentBookingInput_2024_08_13 + ) { return { noShowHost: inputBooking.host, attendees: inputBooking.attendees?.map((attendee) => ({ @@ -845,4 +1000,9 @@ export class InputBookingsService_2024_08_13 { })), }; } + + getOriginalBookingDuration(start: Date, end: Date) { + return DateTime.fromJSDate(end).diff(DateTime.fromJSDate(start), "minutes") + .minutes; + } } diff --git a/packages/platform/libraries/index.ts b/packages/platform/libraries/index.ts index 9e53b36e25..e7678bd851 100644 --- a/packages/platform/libraries/index.ts +++ b/packages/platform/libraries/index.ts @@ -19,7 +19,6 @@ import { credentialForCalendarServiceSelect } from "@calcom/prisma/selects/crede import { paymentDataSelect } from "@calcom/prisma/selects/payment"; import { createNewUsersConnectToOrgIfExists } from "@calcom/trpc/server/routers/viewer/teams/inviteMember/utils"; - export { slugify } from "@calcom/lib/slugify"; export { slugifyLenient } from "@calcom/lib/slugify-lenient"; export { getBookingForReschedule }; @@ -53,7 +52,10 @@ export { getConnectedDestinationCalendarsAndEnsureDefaultsInDb } from "@calcom/f export { getBusyCalendarTimes } from "@calcom/features/calendars/lib/CalendarManager"; -export type { BookingCreateBody, BookingResponse } from "@calcom/features/bookings/types"; +export type { + BookingCreateBody, + BookingResponse, +} from "@calcom/features/bookings/types"; export { MINUTES_TO_BOOK, ENABLE_ASYNC_TASKER } from "@calcom/lib/constants"; @@ -67,7 +69,11 @@ export { getAllUserBookings }; export { getBookingInfo }; export { handleCancelBooking }; -export { userMetadata, bookingMetadataSchema, teamMetadataSchema } from "@calcom/prisma/zod-utils"; +export { + userMetadata, + bookingMetadataSchema, + teamMetadataSchema, +} from "@calcom/prisma/zod-utils"; export { parseBookingLimit } from "@calcom/lib/intervalLimits/isBookingLimits"; @@ -144,4 +150,4 @@ export { generateSecret } from "@calcom/features/oauth/utils/generateSecret"; export { OAuthService } from "@calcom/features/oauth/services/OAuthService"; export { ProfileRepository } from "@calcom/features/profile/repositories/ProfileRepository"; -export type { OrgMembershipLookup } from "@calcom/trpc/server/routers/viewer/slots/util"; \ No newline at end of file +export type { OrgMembershipLookup } from "@calcom/trpc/server/routers/viewer/slots/util";