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:
co-authored by
Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent
f691258a5c
commit
689ce1ec7f
+75
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user