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>
This commit is contained in:
Rajiv Sahal
2026-01-16 13:53:50 +00:00
committed by GitHub
co-authored by Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent f691258a5c
commit 689ce1ec7f
4 changed files with 317 additions and 76 deletions
@@ -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);
@@ -1216,4 +1216,4 @@ export class BookingsService_2024_08_13 {
t: await getTranslation("en", "common"),
});
}
}
}
@@ -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<BookingRequest> {
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<BookingRequest> {
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<number | undefined> {
private async createBookingRequestOwnerId(
req: Request
): Promise<number | undefined> {
try {
const bearerToken = req.get("Authorization")?.replace("Bearer ", "");
if (bearerToken) {
if (isApiKey(bearerToken, this.config.get<string>("api.apiKeyPrefix") ?? "cal_")) {
const strippedApiKey = stripApiKey(bearerToken, this.config.get<string>("api.keyPrefix"));
if (
isApiKey(
bearerToken,
this.config.get<string>("api.apiKeyPrefix") ?? "cal_"
)
) {
const strippedApiKey = stripApiKey(
bearerToken,
this.config.get<string>("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;
}
}
+10 -4
View File
@@ -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";
export type { OrgMembershipLookup } from "@calcom/trpc/server/routers/viewer/slots/util";