Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6df473c5fb | ||
|
|
4606fc6ace | ||
|
|
78ddf71823 | ||
|
|
15cb687307 | ||
|
|
c0ecfcd910 | ||
|
|
2e86f8404f | ||
|
|
b94b16316f | ||
|
|
f80b780aae | ||
|
|
00dc467f25 | ||
|
|
d644c09bac | ||
|
|
5d9c7bfde9 | ||
|
|
cfd1c5dfe3 | ||
|
|
d7e5a8e588 | ||
|
|
3b992fa9bd | ||
|
|
8ad442f2be | ||
|
|
695a4d7a59 | ||
|
|
9bcea0fa01 | ||
|
|
9a4c8c147b | ||
|
|
a48ad2219f | ||
|
|
55a97b71c8 | ||
|
|
b1b8b0135e | ||
|
|
08d4c800e9 | ||
|
|
efeb0a8f41 | ||
|
|
207147f5ff | ||
|
|
6bb8a7c6a9 | ||
|
|
aa7f9719c9 | ||
|
|
e045dcff11 | ||
|
|
80eb6daf1f | ||
|
|
b0bf274896 | ||
|
|
4762d475cb | ||
|
|
c7d9c63390 | ||
|
|
853f9bc436 | ||
|
|
ca19aa00c1 | ||
|
|
467c979aa7 | ||
|
|
9f9bf10040 | ||
|
|
8102192987 | ||
|
|
232f9d288c | ||
|
|
7dab100382 | ||
|
|
91d4f1c219 | ||
|
|
a64e7f9c2a | ||
|
|
3c1297aa72 | ||
|
|
96f7a01c5f | ||
|
|
a4a05878ef | ||
|
|
352d68ae1c | ||
|
|
ffd034679c | ||
|
|
d64ae3c901 | ||
|
|
7250e22257 | ||
|
|
f459e5d9c4 | ||
|
|
30bb0f1383 | ||
|
|
24c993a3d2 | ||
|
|
aa4276be5b | ||
|
|
413aba6e52 | ||
|
|
c2547acb3d | ||
|
|
cc1b6bac4a | ||
|
|
d5d90a8b87 | ||
|
|
40333c81c5 | ||
|
|
ff92979311 | ||
|
|
f63edef48d | ||
|
|
37e56ebbf9 | ||
|
|
07c38f1f33 | ||
|
|
192238f4bf | ||
|
|
94a26031a8 | ||
|
|
e9f20b1a0b | ||
|
|
7cb9e2b99b | ||
|
|
dd45d8cdbc | ||
|
|
3a181f1dac | ||
|
|
c3abfa51ba | ||
|
|
6498af167a | ||
|
|
30dce7bacd | ||
|
|
3be9e1bd38 | ||
|
|
db52f44293 | ||
|
|
20d6eb5200 | ||
|
|
424d94f16d | ||
|
|
73e7d6f986 | ||
|
|
145001d26e | ||
|
|
5d2d7ce979 | ||
|
|
4dc21ab2b0 | ||
|
|
df6ee73e05 | ||
|
|
587af33179 | ||
|
|
3b381b037c | ||
|
|
0968fc56eb | ||
|
|
13044cb5b0 | ||
|
|
580b03f2b9 | ||
|
|
543d4bb6b3 | ||
|
|
21d87d5a80 | ||
|
|
194719e7ba | ||
|
|
dc666fc770 | ||
|
|
50a76e6b27 | ||
|
|
be4d3170db | ||
|
|
7b97002230 | ||
|
|
6319d63e1f | ||
|
|
2e18478b5c | ||
|
|
0ef93edd56 | ||
|
|
d6a3661e58 | ||
|
|
be404436d7 | ||
|
|
b1ebefb854 | ||
|
|
f8aa2701b6 | ||
|
|
034dfc8e6b | ||
|
|
a6b6898697 | ||
|
|
0e5a0bfa7b | ||
|
|
6c01f5eace | ||
|
|
ef2c34c55f | ||
|
|
7c1582c06f | ||
|
|
367e2666c6 | ||
|
|
209a401113 | ||
|
|
837349e3d3 | ||
|
|
e66602aa5c | ||
|
|
082f8e1b04 | ||
|
|
07716474d3 | ||
|
|
f335cc4269 | ||
|
|
f19750cfaf | ||
|
|
46810f69ca | ||
|
|
a7247c594b | ||
|
|
8de7876f72 | ||
|
|
49f75f5070 | ||
|
|
4ff543efc7 | ||
|
|
8af5145b56 | ||
|
|
a31242cdc5 | ||
|
|
e20df1e7aa | ||
|
|
960a368175 | ||
|
|
49cb86dfe9 | ||
|
|
b644ec58af | ||
|
|
bd92b89bf0 | ||
|
|
495ac74255 | ||
|
|
c3d35fd8c8 | ||
|
|
87bc8d649a |
+10
-8
@@ -17,6 +17,7 @@
|
||||
#
|
||||
|
||||
# To enable enterprise-only features please add your environment variable to the .env file then make your way to /auth/setup to select your license and follow instructions.
|
||||
|
||||
CALCOM_LICENSE_KEY=
|
||||
# Signature token for the Cal.com License API (used for self-hosted integrations)
|
||||
# We will give you a token when we provide you with a license key this ensure you and only you can communicate with the Cal.com License API for your license key
|
||||
@@ -192,7 +193,10 @@ NEXT_PUBLIC_IS_PREMIUM_NEW_PLAN=0
|
||||
NEXT_PUBLIC_STRIPE_PREMIUM_NEW_PLAN_PRICE=
|
||||
STRIPE_TEAM_MONTHLY_PRICE_ID=
|
||||
STRIPE_TEAM_PRODUCT_ID=
|
||||
# It is a price ID in the product with id STRIPE_ORG_PRODUCT_ID
|
||||
STRIPE_ORG_MONTHLY_PRICE_ID=
|
||||
STRIPE_ORG_PRODUCT_ID=
|
||||
|
||||
STRIPE_WEBHOOK_SECRET=
|
||||
STRIPE_WEBHOOK_SECRET_APPS=
|
||||
STRIPE_PRIVATE_KEY=
|
||||
@@ -285,7 +289,7 @@ NEXT_PUBLIC_AVAILABILITY_SCHEDULE_INTERVAL=
|
||||
# use organizations
|
||||
ORGANIZATIONS_ENABLED=
|
||||
NEXT_PUBLIC_ORGANIZATIONS_MIN_SELF_SERVE_SEATS=30
|
||||
NEXT_PUBLIC_ORGANIZATIONS_SELF_SERVE_PRICE=3700 # $37.00 per seat
|
||||
NEXT_PUBLIC_ORGANIZATIONS_SELF_SERVE_PRICE_NEW=37 # $37.00 per seat
|
||||
|
||||
# This variable should only be set to 1 or true if you want to autolink external provider sign-ups with
|
||||
# existing organizations based on email domain address
|
||||
@@ -353,13 +357,6 @@ E2E_TEST_OIDC_USER_PASSWORD=
|
||||
|
||||
# ***********************************************************************************************************
|
||||
|
||||
# provide a value between 0 and 100 to ensure the percentage of traffic
|
||||
# redirected from the legacy to the future pages
|
||||
AB_TEST_BUCKET_PROBABILITY=50
|
||||
APP_ROUTER_APPS_SLUG_SETUP_ENABLED=0
|
||||
APP_ROUTER_APPS_ENABLED=0
|
||||
APP_ROUTER_TEAM_ENABLED=0
|
||||
|
||||
# disable setry server source maps
|
||||
SENTRY_DISABLE_SERVER_WEBPACK_PLUGIN=1
|
||||
|
||||
@@ -402,6 +399,11 @@ NEXT_PUBLIC_WEBSITE_TERMS_URL=
|
||||
# [0: silly & upwards, 1: trace & upwards, 2: debug & upwards, 3: info & upwards, 4: warn & upwards, 5: error & fatal, 6: fatal]
|
||||
NEXT_PUBLIC_LOGGER_LEVEL=
|
||||
|
||||
# For storing Daily Video recordings on S3 Bucket
|
||||
CAL_VIDEO_BUCKET_NAME=
|
||||
CAL_VIDEO_BUCKET_REGION=
|
||||
CAL_VIDEO_ASSUME_ROLE_ARN=
|
||||
|
||||
# Used to use Replexica SDK, a tool for real-time AI-powered localization
|
||||
REPLEXICA_API_KEY=
|
||||
|
||||
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
- "@calcom/app-store-cli"
|
||||
- "@calcom/platform-constants"
|
||||
- "@calcom/platform-enums"
|
||||
- "@calcom/platform-types"
|
||||
- "@calcom/platform-utils"
|
||||
- "@calcom/prisma"
|
||||
Vendored
Vendored
+6
@@ -0,0 +1,6 @@
|
||||
undecided:
|
||||
- "@calcom/app-store-cli"
|
||||
- "@calcom/platform-constants"
|
||||
- "@calcom/platform-enums"
|
||||
- "@calcom/platform-types"
|
||||
- "@calcom/platform-utils"
|
||||
Vendored
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
undecided:
|
||||
- "@calcom/prisma"
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
- "@calcom/prisma"
|
||||
Vendored
+3
@@ -0,0 +1,3 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
- "@calcom/prisma"
|
||||
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
- "@calcom/app-store-cli"
|
||||
- "@calcom/platform-constants"
|
||||
- "@calcom/platform-enums"
|
||||
- "@calcom/platform-types"
|
||||
- "@calcom/platform-utils"
|
||||
- "@calcom/prisma"
|
||||
@@ -1,9 +0,0 @@
|
||||
- [x] Refactor individual calendar links functions to create the variables and just use the variables in the string literal instead of functions being called in the string literal
|
||||
- [x] Must ensure during review that logically there is no change in the functionality of booking-single-view.tsx
|
||||
|
||||
|
||||
- Testing
|
||||
- [x] Verify Google Calendar, Ics, Office 365 - Compare with Production
|
||||
- [x] Event Description, Booking title, Booking location, Start time, End time - All correct
|
||||
- [x] Recurring event?
|
||||
- [x] Dynamic Group booking?
|
||||
@@ -38,7 +38,7 @@
|
||||
"@axiomhq/winston": "^1.2.0",
|
||||
"@calcom/platform-constants": "*",
|
||||
"@calcom/platform-enums": "*",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.147",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.152",
|
||||
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
|
||||
"@calcom/platform-types": "*",
|
||||
"@calcom/platform-utils": "*",
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
X_CAL_PLATFORM_EMBED,
|
||||
} from "@calcom/platform-constants";
|
||||
|
||||
import { CalendarServiceExceptionFilter } from "./filters/calendar-service-exception.filter";
|
||||
import { TRPCExceptionFilter } from "./filters/trpc-exception.filter";
|
||||
|
||||
export const bootstrap = (app: NestExpressApplication): NestExpressApplication => {
|
||||
@@ -76,6 +77,7 @@ export const bootstrap = (app: NestExpressApplication): NestExpressApplication =
|
||||
app.useGlobalFilters(new ZodExceptionFilter());
|
||||
app.useGlobalFilters(new HttpExceptionFilter());
|
||||
app.useGlobalFilters(new TRPCExceptionFilter());
|
||||
app.useGlobalFilters(new CalendarServiceExceptionFilter());
|
||||
|
||||
app.use(cookieParser());
|
||||
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
import { BookingsController_2024_04_15 } from "@/ee/bookings/2024-04-15/controllers/bookings.controller";
|
||||
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
|
||||
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
|
||||
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||
import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module";
|
||||
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
|
||||
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
|
||||
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||
import { BillingModule } from "@/modules/billing/billing.module";
|
||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
|
||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { RedisModule } from "@/modules/redis/redis.module";
|
||||
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
@@ -33,6 +38,11 @@ import { Module } from "@nestjs/common";
|
||||
ApiKeysRepository,
|
||||
OAuthClientUsersService,
|
||||
PlatformBookingsService,
|
||||
CalendarsService,
|
||||
CredentialsRepository,
|
||||
AppsRepository,
|
||||
CalendarsRepository,
|
||||
SelectedCalendarsRepository,
|
||||
],
|
||||
controllers: [BookingsController_2024_04_15],
|
||||
})
|
||||
|
||||
@@ -351,6 +351,64 @@ describe("Bookings Endpoints 2024-04-15", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should create a booking with split name", async () => {
|
||||
const bookingStart = "2040-05-21T11:30:00.000Z";
|
||||
const bookingEnd = "2040-05-21T12:30:00.000Z";
|
||||
const bookingEventTypeId = eventTypeId;
|
||||
const bookingTimeZone = "Europe/London";
|
||||
const bookingLanguage = "en";
|
||||
const bookingHashedLink = "";
|
||||
const bookingMetadata = {
|
||||
timeFormat: "12",
|
||||
meetingType: "organizer-phone",
|
||||
};
|
||||
const bookingResponses = {
|
||||
name: { firstName: "John", lastName: "Doe" },
|
||||
email: "tester@example.com",
|
||||
location: {
|
||||
value: "link",
|
||||
optionValue: "",
|
||||
},
|
||||
notes: "test",
|
||||
};
|
||||
|
||||
const body: CreateBookingInput_2024_04_15 = {
|
||||
start: bookingStart,
|
||||
end: bookingEnd,
|
||||
eventTypeId: bookingEventTypeId,
|
||||
timeZone: bookingTimeZone,
|
||||
language: bookingLanguage,
|
||||
metadata: bookingMetadata,
|
||||
hashedLink: bookingHashedLink,
|
||||
responses: bookingResponses,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(body)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: ApiSuccessResponse<Awaited<ReturnType<typeof handleNewBooking>>> =
|
||||
response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.userPrimaryEmail).toBeDefined();
|
||||
expect(responseBody.data.userPrimaryEmail).toEqual(userEmail);
|
||||
expect(responseBody.data.id).toBeDefined();
|
||||
expect(responseBody.data.uid).toBeDefined();
|
||||
expect(responseBody.data.startTime).toEqual(bookingStart);
|
||||
expect(responseBody.data.eventTypeId).toEqual(bookingEventTypeId);
|
||||
expect(responseBody.data.user.timeZone).toEqual(bookingTimeZone);
|
||||
expect(responseBody.data.metadata).toEqual(bookingMetadata);
|
||||
expect(responseBody.data.responses).toEqual({
|
||||
...bookingResponses,
|
||||
name: `${bookingResponses.name.firstName} ${bookingResponses.name.lastName}`,
|
||||
});
|
||||
|
||||
createdBooking = responseBody.data;
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await userRepositoryFixture.deleteByEmail(user.email);
|
||||
await bookingsRepositoryFixture.deleteAllBookings(user.id, user.email);
|
||||
|
||||
@@ -12,9 +12,42 @@ import {
|
||||
isEmail,
|
||||
Validate,
|
||||
} from "class-validator";
|
||||
import { ValidationOptions, registerDecorator } from "class-validator";
|
||||
|
||||
import { RESCHEDULED_BY_DOCS } from "@calcom/platform-types";
|
||||
|
||||
type BookingName = { firstName: string; lastName: string };
|
||||
|
||||
function ValidateBookingName(validationOptions?: ValidationOptions) {
|
||||
return function (target: object, propertyName: string) {
|
||||
registerDecorator({
|
||||
name: "validateBookingName",
|
||||
target: target.constructor,
|
||||
propertyName: propertyName,
|
||||
options: validationOptions,
|
||||
validator: {
|
||||
validate(value: any) {
|
||||
if (typeof value === "string") {
|
||||
return value.trim().length > 0;
|
||||
}
|
||||
if (typeof value === "object" && value !== null) {
|
||||
return (
|
||||
typeof value.firstName === "string" &&
|
||||
typeof value.lastName === "string" &&
|
||||
value.firstName.trim().length > 0 &&
|
||||
value.lastName.trim().length > 0
|
||||
);
|
||||
}
|
||||
return false;
|
||||
},
|
||||
defaultMessage(): string {
|
||||
return "Name must be either a non-empty string or an object with non-empty firstName and lastName";
|
||||
},
|
||||
},
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
class Location {
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
@@ -26,9 +59,14 @@ class Location {
|
||||
}
|
||||
|
||||
class Response {
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
name!: string;
|
||||
@ApiProperty({
|
||||
oneOf: [
|
||||
{ type: "string" },
|
||||
{ type: "object", properties: { firstName: { type: "string" }, lastName: { type: "string" } } },
|
||||
],
|
||||
})
|
||||
@ValidateBookingName()
|
||||
name!: string | BookingName;
|
||||
|
||||
@Validate((value: string) => !value || isEmail(value), {
|
||||
message: "Invalid response email",
|
||||
|
||||
@@ -4,19 +4,24 @@ import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bo
|
||||
import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/input.service";
|
||||
import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service";
|
||||
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
|
||||
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
|
||||
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||
import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module";
|
||||
import { EventTypesModule_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.module";
|
||||
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
||||
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
|
||||
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||
import { BillingModule } from "@/modules/billing/billing.module";
|
||||
import { BookingSeatModule } from "@/modules/booking-seat/booking-seat.module";
|
||||
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
|
||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
|
||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { RedisModule } from "@/modules/redis/redis.module";
|
||||
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
@@ -47,6 +52,11 @@ import { Module } from "@nestjs/common";
|
||||
BookingSeatRepository,
|
||||
ApiKeysRepository,
|
||||
PlatformBookingsService,
|
||||
CalendarsService,
|
||||
CredentialsRepository,
|
||||
AppsRepository,
|
||||
CalendarsRepository,
|
||||
SelectedCalendarsRepository,
|
||||
],
|
||||
controllers: [BookingsController_2024_08_13],
|
||||
exports: [InputBookingsService_2024_08_13, OutputBookingsService_2024_08_13, BookingsService_2024_08_13],
|
||||
|
||||
@@ -39,11 +39,15 @@ import { Request } from "express";
|
||||
import { BOOKING_READ, BOOKING_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import {
|
||||
CancelBookingInput,
|
||||
CancelBookingInput_2024_08_13,
|
||||
CancelBookingInputPipe,
|
||||
CancelSeatedBookingInput_2024_08_13,
|
||||
GetBookingOutput_2024_08_13,
|
||||
GetBookingsOutput_2024_08_13,
|
||||
RescheduleBookingInput,
|
||||
RescheduleBookingInput_2024_08_13,
|
||||
RescheduleBookingInputPipe,
|
||||
RescheduleSeatedBookingInput_2024_08_13,
|
||||
} from "@calcom/platform-types";
|
||||
import {
|
||||
CreateBookingInputPipe,
|
||||
@@ -65,9 +69,12 @@ import {
|
||||
@DocsTags("Bookings")
|
||||
@ApiHeader({
|
||||
name: "cal-api-version",
|
||||
description: `Must be set to \`2024-08-13\``,
|
||||
description: `Must be set to ${VERSION_2024_08_13}`,
|
||||
example: VERSION_2024_08_13,
|
||||
required: true,
|
||||
schema: {
|
||||
default: VERSION_2024_08_13,
|
||||
},
|
||||
})
|
||||
export class BookingsController_2024_08_13 {
|
||||
private readonly logger = new Logger("BookingsController_2024_08_13");
|
||||
@@ -99,7 +106,7 @@ export class BookingsController_2024_08_13 {
|
||||
],
|
||||
},
|
||||
description:
|
||||
"Accepts different types of booking input: CreateBookingInput_2024_08_13, CreateInstantBookingInput_2024_08_13, or CreateRecurringBookingInput_2024_08_13",
|
||||
"Accepts different types of booking input: Create Booking (Option 1), Create Instant Booking (Option 2), or Create Recurring Booking (Option 3)",
|
||||
})
|
||||
@ApiExtraModels(
|
||||
CreateBookingInput_2024_08_13,
|
||||
@@ -172,9 +179,19 @@ export class BookingsController_2024_08_13 {
|
||||
@UseGuards(BookingUidGuard)
|
||||
@ApiOperation({
|
||||
summary: "Reschedule a booking",
|
||||
description:
|
||||
"Reschedule a booking by passing `:bookingUid` of the booking that should be rescheduled and pass request body with a new start time to create a new booking.",
|
||||
description: "Reschedule a booking or seated booking",
|
||||
})
|
||||
@ApiBody({
|
||||
schema: {
|
||||
oneOf: [
|
||||
{ $ref: getSchemaPath(RescheduleBookingInput_2024_08_13) },
|
||||
{ $ref: getSchemaPath(RescheduleSeatedBookingInput_2024_08_13) },
|
||||
],
|
||||
},
|
||||
description:
|
||||
"Accepts different types of reschedule booking input: Reschedule Booking (Option 1) or Reschedule Seated Booking (Option 2)",
|
||||
})
|
||||
@ApiExtraModels(RescheduleBookingInput_2024_08_13, RescheduleSeatedBookingInput_2024_08_13)
|
||||
async rescheduleBooking(
|
||||
@Param("bookingUid") bookingUid: string,
|
||||
@Body(new RescheduleBookingInputPipe())
|
||||
@@ -199,6 +216,17 @@ export class BookingsController_2024_08_13 {
|
||||
For seated bookings to cancel one individual booking provide :bookingUid and :seatUid in the request body. For recurring seated bookings it is not possible to cancel all of them with 1 call
|
||||
like with non-seated recurring bookings by providing recurring bookind uid - you have to cancel each recurrence booking by its bookingUid + seatUid.`,
|
||||
})
|
||||
@ApiBody({
|
||||
schema: {
|
||||
oneOf: [
|
||||
{ $ref: getSchemaPath(CancelBookingInput_2024_08_13) },
|
||||
{ $ref: getSchemaPath(CancelSeatedBookingInput_2024_08_13) },
|
||||
],
|
||||
},
|
||||
description:
|
||||
"Accepts different types of cancel booking input: Cancel Booking (Option 1) or Cancel Seated Booking (Option 2)",
|
||||
})
|
||||
@ApiExtraModels(CancelBookingInput_2024_08_13, CancelSeatedBookingInput_2024_08_13)
|
||||
async cancelBooking(
|
||||
@Req() request: Request,
|
||||
@Param("bookingUid") bookingUid: string,
|
||||
|
||||
@@ -434,7 +434,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
}
|
||||
|
||||
describe("get individual booking", () => {
|
||||
it("should should get a seated booking with split name responses", async () => {
|
||||
it("should get a seated booking with split name responses", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings/${bookingWithSplitName.uid}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
@@ -457,7 +457,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should should get a seated booking with split name responses", async () => {
|
||||
it("should get a seated booking with split name responses", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings/${seatedBookingWithSplitName.uid}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
|
||||
@@ -40,6 +40,8 @@ import {
|
||||
import { PlatformOAuthClient, Team } from "@calcom/prisma/client";
|
||||
|
||||
describe("Bookings Endpoints 2024-08-13", () => {
|
||||
const googleMeetUrl = "https://meet.google.com/abc-def-ghi";
|
||||
|
||||
describe("Seated bookings", () => {
|
||||
let app: INestApplication;
|
||||
let organization: Team;
|
||||
@@ -330,7 +332,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
location: googleMeetUrl,
|
||||
bookingFieldsResponses: {
|
||||
codingLanguage: "TypeScript",
|
||||
},
|
||||
@@ -383,6 +385,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
bookingFieldsResponses: {
|
||||
name: body.attendee.name,
|
||||
...body.bookingFieldsResponses,
|
||||
location: {
|
||||
optionValue: googleMeetUrl,
|
||||
value: "link",
|
||||
},
|
||||
},
|
||||
metadata: body.metadata,
|
||||
});
|
||||
@@ -418,6 +424,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
bookingFieldsResponses: {
|
||||
name: body.attendee.name,
|
||||
...body.bookingFieldsResponses,
|
||||
location: {
|
||||
optionValue: googleMeetUrl,
|
||||
value: "link",
|
||||
},
|
||||
},
|
||||
metadata: body.metadata,
|
||||
});
|
||||
@@ -797,7 +807,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
location: googleMeetUrl,
|
||||
bookingFieldsResponses: {
|
||||
codingLanguage: "TypeScript",
|
||||
},
|
||||
@@ -850,6 +860,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
bookingFieldsResponses: {
|
||||
name: body.attendee.name,
|
||||
...body.bookingFieldsResponses,
|
||||
location: {
|
||||
optionValue: googleMeetUrl,
|
||||
value: "link",
|
||||
},
|
||||
},
|
||||
metadata: body.metadata,
|
||||
});
|
||||
@@ -885,6 +899,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
bookingFieldsResponses: {
|
||||
name: body.attendee.name,
|
||||
...body.bookingFieldsResponses,
|
||||
location: {
|
||||
optionValue: googleMeetUrl,
|
||||
value: "link",
|
||||
},
|
||||
},
|
||||
metadata: body.metadata,
|
||||
});
|
||||
@@ -910,7 +928,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
location: googleMeetUrl,
|
||||
bookingFieldsResponses: {
|
||||
codingLanguage: "TypeScript",
|
||||
},
|
||||
@@ -966,6 +984,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
bookingFieldsResponses: {
|
||||
name: nameAttendeeOne,
|
||||
...body.bookingFieldsResponses,
|
||||
location: {
|
||||
optionValue: googleMeetUrl,
|
||||
value: "link",
|
||||
},
|
||||
},
|
||||
metadata: createdRecurringSeatedBooking[0].attendees[0].metadata,
|
||||
});
|
||||
@@ -982,6 +1004,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
bookingFieldsResponses: {
|
||||
name: nameAttendeeTwo,
|
||||
...body.bookingFieldsResponses,
|
||||
location: {
|
||||
optionValue: googleMeetUrl,
|
||||
value: "link",
|
||||
},
|
||||
},
|
||||
metadata: body.metadata,
|
||||
});
|
||||
@@ -1017,6 +1043,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
bookingFieldsResponses: {
|
||||
name: nameAttendeeOne,
|
||||
...body.bookingFieldsResponses,
|
||||
location: {
|
||||
optionValue: googleMeetUrl,
|
||||
value: "link",
|
||||
},
|
||||
},
|
||||
metadata: createdRecurringSeatedBooking[0].attendees[0].metadata,
|
||||
});
|
||||
@@ -1030,6 +1060,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
bookingFieldsResponses: {
|
||||
name: nameAttendeeTwo,
|
||||
...body.bookingFieldsResponses,
|
||||
location: {
|
||||
optionValue: googleMeetUrl,
|
||||
value: "link",
|
||||
},
|
||||
},
|
||||
metadata: body.metadata,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
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";
|
||||
@@ -31,6 +32,7 @@ import {
|
||||
RecurringBookingOutput_2024_08_13,
|
||||
GetBookingsOutput_2024_08_13,
|
||||
GetSeatedBookingOutput_2024_08_13,
|
||||
RescheduleBookingInput_2024_08_13,
|
||||
} from "@calcom/platform-types";
|
||||
import { PlatformOAuthClient, Team } from "@calcom/prisma/client";
|
||||
|
||||
@@ -66,6 +68,8 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
const team2EventTypeSlug = `team-bookings-event-type-${randomString()}`;
|
||||
const phoneOnlyEventTypeSlug = `team-bookings-event-type-${randomString()}`;
|
||||
|
||||
let phoneBasedBooking: BookingOutput_2024_08_13;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
teamUserEmail,
|
||||
@@ -408,12 +412,13 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
|
||||
it("should create a phone based booking", async () => {
|
||||
const phoneNumber = "+919876543210";
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 15, 0, 0)).toISOString(),
|
||||
eventTypeId: phoneOnlyEventTypeId,
|
||||
attendee: {
|
||||
name: "alice",
|
||||
phoneNumber: "+919876543210",
|
||||
phoneNumber,
|
||||
timeZone: "Europe/Madrid",
|
||||
language: "es",
|
||||
},
|
||||
@@ -453,6 +458,8 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
expect(data.meetingUrl).toEqual(body.meetingUrl);
|
||||
expect(data.absentHost).toEqual(false);
|
||||
expect(data.bookingFieldsResponses.attendeePhoneNumber).toEqual(phoneNumber);
|
||||
phoneBasedBooking = data;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
@@ -523,7 +530,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
|
||||
describe("get team bookings", () => {
|
||||
it("should should get bookings by teamId", async () => {
|
||||
it("should get bookings by teamId", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings?teamId=${team1.id}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
@@ -542,7 +549,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should should get bookings by teamId", async () => {
|
||||
it("should get bookings by teamId", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings?teamId=${team2.id}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
@@ -598,7 +605,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should should get bookings by teamIds", async () => {
|
||||
it("should get bookings by teamIds", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings?teamIds=${team1.id},${team2.id}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
@@ -619,6 +626,59 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("reschedule", () => {
|
||||
it("should reschedule phone based booking", async () => {
|
||||
const body: RescheduleBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2035, 0, 8, 14, 0, 0)).toISOString(),
|
||||
reschedulingReason: "Flying to mars that day",
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${phoneBasedBooking.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();
|
||||
expect(responseDataIsBooking(responseBody.data)).toBe(true);
|
||||
|
||||
if (responseDataIsBooking(responseBody.data)) {
|
||||
const data: BookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.id).toBeDefined();
|
||||
expect(data.uid).toBeDefined();
|
||||
expect(data.hosts.length).toEqual(1);
|
||||
expect(data.hosts[0].id).toEqual(teamUser.id);
|
||||
expect(data.status).toEqual("accepted");
|
||||
expect(data.start).toEqual(body.start);
|
||||
expect(data.end).toEqual(new Date(Date.UTC(2035, 0, 8, 14, 15, 0)).toISOString());
|
||||
expect(data.duration).toEqual(15);
|
||||
expect(data.eventTypeId).toEqual(phoneOnlyEventTypeId);
|
||||
expect(data.attendees.length).toEqual(1);
|
||||
expect(data.attendees[0]).toEqual({
|
||||
name: phoneBasedBooking.attendees[0].name,
|
||||
email: phoneBasedBooking.attendees[0].email,
|
||||
phoneNumber: phoneBasedBooking.attendees[0].phoneNumber,
|
||||
timeZone: phoneBasedBooking.attendees[0].timeZone,
|
||||
language: phoneBasedBooking.attendees[0].language,
|
||||
absent: false,
|
||||
});
|
||||
expect(data.meetingUrl).toEqual(phoneBasedBooking.meetingUrl);
|
||||
expect(data.absentHost).toEqual(false);
|
||||
expect(data.bookingFieldsResponses.attendeePhoneNumber).toEqual(
|
||||
phoneBasedBooking.bookingFieldsResponses.attendeePhoneNumber
|
||||
);
|
||||
phoneBasedBooking = data;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibly recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
async function createOAuthClient(organizationId: number) {
|
||||
const data = {
|
||||
logo: "logo-url",
|
||||
|
||||
@@ -4,6 +4,7 @@ import { CancelBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs
|
||||
import { CreateBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/create-booking.output";
|
||||
import { MarkAbsentBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/mark-absent.output";
|
||||
import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output";
|
||||
import { CreateEventTypeOutput_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/outputs/create-event-type.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";
|
||||
@@ -27,10 +28,12 @@ import { withApiAuth } from "test/utils/withApiAuth";
|
||||
import {
|
||||
CAL_API_VERSION_HEADER,
|
||||
SUCCESS_STATUS,
|
||||
VERSION_2024_06_14,
|
||||
VERSION_2024_08_13,
|
||||
X_CAL_CLIENT_ID,
|
||||
} from "@calcom/platform-constants";
|
||||
import {
|
||||
CreateEventTypeInput_2024_06_14,
|
||||
GetBookingOutput_2024_08_13,
|
||||
GetBookingsOutput_2024_08_13,
|
||||
GetSeatedBookingOutput_2024_08_13,
|
||||
@@ -95,6 +98,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
|
||||
organization = await teamRepositoryFixture.create({
|
||||
name: `user-bookings-organization-${randomString()}`,
|
||||
isOrganization: true,
|
||||
});
|
||||
oAuthClient = await createOAuthClient(organization.id);
|
||||
|
||||
@@ -146,7 +150,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
startTime: new Date(Date.UTC(2020, 0, 8, 13, 0, 0)),
|
||||
endTime: new Date(Date.UTC(2020, 0, 8, 14, 0, 0)),
|
||||
title: "peer coding lets goo",
|
||||
uid: "booking-in-the-past",
|
||||
uid: `booking-in-the-past-${randomString()}`,
|
||||
eventType: {
|
||||
connect: {
|
||||
id: eventTypeId,
|
||||
@@ -326,6 +330,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
|
||||
it("should create a booking", async () => {
|
||||
const googleMeetUrl = "https://meet.google.com/abc-def-ghi";
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
|
||||
eventTypeId,
|
||||
@@ -335,7 +340,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
location: googleMeetUrl,
|
||||
bookingFieldsResponses: {
|
||||
customField: "customValue",
|
||||
},
|
||||
@@ -389,6 +394,10 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
email: body.attendee.email,
|
||||
...body.bookingFieldsResponses,
|
||||
guests: body.guests,
|
||||
location: {
|
||||
optionValue: googleMeetUrl,
|
||||
value: "link",
|
||||
},
|
||||
});
|
||||
expect(data.guests).toEqual(body.guests);
|
||||
|
||||
@@ -518,7 +527,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
|
||||
describe("get individual booking", () => {
|
||||
it("should should get a booking", async () => {
|
||||
it("should get a booking", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings/${createdBooking.uid}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
@@ -553,7 +562,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should should get a booking with rating", async () => {
|
||||
it("should get a booking with rating", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings/${bookingInThePast.uid}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
@@ -577,7 +586,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should should get 1 recurrence of a recurring booking", async () => {
|
||||
it("should get 1 recurrence of a recurring booking", async () => {
|
||||
const recurrenceUid = createdRecurringBooking[0].uid;
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings/${recurrenceUid}`)
|
||||
@@ -613,7 +622,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should should get all recurrences of the recurring bookings", async () => {
|
||||
it("should get all recurrences of the recurring bookings", async () => {
|
||||
const recurringBookingUid = createdRecurringBooking[0].recurringBookingUid;
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings/${recurringBookingUid}`)
|
||||
@@ -682,7 +691,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
|
||||
describe("get bookings", () => {
|
||||
it("should should get all bookings", async () => {
|
||||
it("should get all bookings", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
@@ -700,7 +709,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should should take bookings", async () => {
|
||||
it("should take bookings", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings?take=3`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
@@ -718,7 +727,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should should skip bookings", async () => {
|
||||
it("should skip bookings", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings?skip=2`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
@@ -736,7 +745,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should should get upcoming bookings", async () => {
|
||||
it("should get upcoming bookings", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings?status=upcoming`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
@@ -754,7 +763,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should should get past bookings", async () => {
|
||||
it("should get past bookings", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings?status=past`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
@@ -772,7 +781,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should should get upcoming and past bookings", async () => {
|
||||
it("should get upcoming and past bookings", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings?status=upcoming,past`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
@@ -790,7 +799,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should should get recurring booking recurrences", async () => {
|
||||
it("should get recurring booking recurrences", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings?status=recurring`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
@@ -808,7 +817,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should should get bookings by attendee email", async () => {
|
||||
it("should get bookings by attendee email", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings?attendeeEmail=mr_proper@gmail.com`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
@@ -826,7 +835,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should should get bookings by attendee name", async () => {
|
||||
it("should get bookings by attendee name", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings?attendeeName=Mr Proper Recurring`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
@@ -844,7 +853,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should should get bookings by eventTypeId", async () => {
|
||||
it("should get bookings by eventTypeId", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings?eventTypeId=${eventTypeId}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
@@ -862,7 +871,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should should get bookings by eventTypeIds", async () => {
|
||||
it("should get bookings by eventTypeIds", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings?eventTypeIds=${eventTypeId},${recurringEventTypeId}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
@@ -880,7 +889,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should should get bookings by after specified start time", async () => {
|
||||
it("should get bookings by after specified start time", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings?afterStart=${createdRecurringBooking[1].start}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
@@ -898,7 +907,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should should get bookings by before specified end time", async () => {
|
||||
it("should get bookings by before specified end time", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings?beforeEnd=${createdRecurringBooking[0].end}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
@@ -916,7 +925,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should should get bookings after specified update time", async () => {
|
||||
it("should get bookings after specified update time", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings?afterUpdatedAt=${createdRecurringBooking[1].updatedAt}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
@@ -934,7 +943,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should should get bookings before specified update time", async () => {
|
||||
it("should get bookings before specified update time", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings?beforeUpdatedAt=${createdRecurringBooking[0].updatedAt}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
@@ -952,7 +961,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should should get bookings in update range", async () => {
|
||||
it("should get bookings in update range", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(
|
||||
`/v2/bookings?afterUpdatedAt=${createdRecurringBooking[0].updatedAt}&beforeUpdatedAt=${createdRecurringBooking[2].updatedAt}`
|
||||
@@ -972,7 +981,65 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should should sort bookings by start in descending order", async () => {
|
||||
describe("createdAt filter", () => {
|
||||
it("should get bookings after specified createdAt time", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings?afterCreatedAt=${createdRecurringBooking[1].createdAt}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(2);
|
||||
});
|
||||
});
|
||||
|
||||
it("should get bookings before specified createdAt time", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings?beforeCreatedAt=${createdRecurringBooking[0].createdAt}`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(3);
|
||||
});
|
||||
});
|
||||
|
||||
it("should get bookings in createdAt range", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(
|
||||
`/v2/bookings?afterCreatedAt=${createdRecurringBooking[0].createdAt}&beforeCreatedAt=${createdRecurringBooking[1].createdAt}`
|
||||
)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingsOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
const data: (
|
||||
| BookingOutput_2024_08_13
|
||||
| RecurringBookingOutput_2024_08_13
|
||||
| GetSeatedBookingOutput_2024_08_13
|
||||
)[] = responseBody.data;
|
||||
expect(data.length).toEqual(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should sort bookings by start in descending order", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings?eventTypeId=${eventTypeId}&sortStart=desc`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
@@ -992,7 +1059,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should should sort bookings by start in ascending order", async () => {
|
||||
it("should sort bookings by start in ascending order", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings?eventTypeId=${eventTypeId}&sortStart=asc`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
@@ -1012,7 +1079,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should should sort bookings by end in descending order", async () => {
|
||||
it("should sort bookings by end in descending order", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings?eventTypeId=${eventTypeId}&sortEnd=desc`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
@@ -1032,7 +1099,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should should sort bookings by end in ascending order", async () => {
|
||||
it("should sort bookings by end in ascending order", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings?eventTypeId=${eventTypeId}&sortEnd=asc`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
@@ -1052,7 +1119,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should should sort bookings by created in descending order", async () => {
|
||||
it("should sort bookings by created in descending order", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings?eventTypeId=${eventTypeId}&sortCreated=desc`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
@@ -1072,7 +1139,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should should sort bookings by created in ascending order", async () => {
|
||||
it("should sort bookings by created in ascending order", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings?eventTypeId=${eventTypeId}&sortCreated=asc`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
@@ -1092,7 +1159,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should should sort bookings by updated in descending order", async () => {
|
||||
it("should sort bookings by updated in descending order", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings?eventTypeId=${eventTypeId}&sortUpdatedAt=desc`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
@@ -1112,7 +1179,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should should sort bookings by updated in ascending order", async () => {
|
||||
it("should sort bookings by updated in ascending order", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/bookings?eventTypeId=${eventTypeId}&sortUpdatedAt=asc`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
@@ -1134,7 +1201,7 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
|
||||
describe("reschedule bookings", () => {
|
||||
it("should should reschedule normal booking", async () => {
|
||||
it("should reschedule normal booking", async () => {
|
||||
const body: RescheduleBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2035, 0, 8, 14, 0, 0)).toISOString(),
|
||||
reschedulingReason: "Flying to mars that day",
|
||||
@@ -1537,6 +1604,440 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("booking location", () => {
|
||||
const address = "123 Main St";
|
||||
const link = "https://cal.com/join/123456";
|
||||
const phone = "+37121999999";
|
||||
|
||||
let eventTypeWithAllLocationsId: number;
|
||||
it("can create event type with all locations except google meet", async () => {
|
||||
const eventTypeBody: CreateEventTypeInput_2024_06_14 = {
|
||||
title: "book using any location",
|
||||
slug: "book-using-any-location",
|
||||
lengthInMinutes: 15,
|
||||
locations: [
|
||||
{
|
||||
type: "integration",
|
||||
integration: "cal-video",
|
||||
},
|
||||
{
|
||||
type: "address",
|
||||
address,
|
||||
public: true,
|
||||
},
|
||||
{
|
||||
type: "link",
|
||||
link,
|
||||
public: true,
|
||||
},
|
||||
{
|
||||
type: "phone",
|
||||
phone,
|
||||
public: true,
|
||||
},
|
||||
{
|
||||
type: "attendeeAddress",
|
||||
},
|
||||
{
|
||||
type: "attendeePhone",
|
||||
},
|
||||
{
|
||||
type: "attendeeDefined",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const eventTypeResponse = await request(app.getHttpServer())
|
||||
.post("/api/v2/event-types")
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.send(eventTypeBody)
|
||||
.expect(201);
|
||||
const eventTypeResponseBody: CreateEventTypeOutput_2024_06_14 = eventTypeResponse.body;
|
||||
const createdEventType = eventTypeResponseBody.data;
|
||||
expect(createdEventType).toHaveProperty("id");
|
||||
expect(createdEventType.locations).toHaveLength(7);
|
||||
expect(createdEventType.locations).toEqual(createdEventType.locations);
|
||||
eventTypeWithAllLocationsId = createdEventType.id;
|
||||
});
|
||||
|
||||
it("can book with cal video location", async () => {
|
||||
const bookingBody: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2040, 0, 9, 13, 0, 0)).toISOString(),
|
||||
eventTypeId: eventTypeWithAllLocationsId,
|
||||
attendee: {
|
||||
name: "Mr Proper",
|
||||
email: "mr_proper@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: {
|
||||
type: "integration",
|
||||
integration: "cal-video",
|
||||
},
|
||||
};
|
||||
|
||||
const bookingResponse = await request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(bookingBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201);
|
||||
|
||||
const bookingResponseBody: CreateBookingOutput_2024_08_13 = bookingResponse.body;
|
||||
const createdBooking = bookingResponseBody.data;
|
||||
expect(createdBooking).toHaveProperty("id");
|
||||
|
||||
if (responseDataIsBooking(createdBooking)) {
|
||||
expect(
|
||||
createdBooking.location?.startsWith("http") || createdBooking.location === "integrations:daily"
|
||||
).toEqual(true);
|
||||
await bookingsRepositoryFixture.deleteById(createdBooking.id);
|
||||
} else {
|
||||
throw new Error("Unexpected response data type");
|
||||
}
|
||||
});
|
||||
|
||||
it("can book with address location", async () => {
|
||||
const bookingBody: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2040, 0, 9, 13, 0, 0)).toISOString(),
|
||||
eventTypeId: eventTypeWithAllLocationsId,
|
||||
attendee: {
|
||||
name: "Mr Proper",
|
||||
email: "mr_proper@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: {
|
||||
type: "address",
|
||||
},
|
||||
};
|
||||
|
||||
const bookingResponse = await request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(bookingBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201);
|
||||
|
||||
const bookingResponseBody: CreateBookingOutput_2024_08_13 = bookingResponse.body;
|
||||
const createdBooking = bookingResponseBody.data;
|
||||
expect(createdBooking).toHaveProperty("id");
|
||||
|
||||
if (responseDataIsBooking(createdBooking)) {
|
||||
expect(createdBooking.location).toEqual(address);
|
||||
await bookingsRepositoryFixture.deleteById(createdBooking.id);
|
||||
} else {
|
||||
throw new Error("Unexpected response data type");
|
||||
}
|
||||
});
|
||||
|
||||
it("can book with link location", async () => {
|
||||
const bookingBody: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2040, 0, 9, 13, 0, 0)).toISOString(),
|
||||
eventTypeId: eventTypeWithAllLocationsId,
|
||||
attendee: {
|
||||
name: "Mr Proper",
|
||||
email: "mr_proper@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: {
|
||||
type: "link",
|
||||
},
|
||||
};
|
||||
|
||||
const bookingResponse = await request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(bookingBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201);
|
||||
|
||||
const bookingResponseBody: CreateBookingOutput_2024_08_13 = bookingResponse.body;
|
||||
const createdBooking = bookingResponseBody.data;
|
||||
expect(createdBooking).toHaveProperty("id");
|
||||
|
||||
if (responseDataIsBooking(createdBooking)) {
|
||||
expect(createdBooking.location).toEqual(link);
|
||||
await bookingsRepositoryFixture.deleteById(createdBooking.id);
|
||||
} else {
|
||||
throw new Error("Unexpected response data type");
|
||||
}
|
||||
});
|
||||
|
||||
it("can book with link location", async () => {
|
||||
const bookingBody: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2040, 0, 9, 13, 0, 0)).toISOString(),
|
||||
eventTypeId: eventTypeWithAllLocationsId,
|
||||
attendee: {
|
||||
name: "Mr Proper",
|
||||
email: "mr_proper@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: {
|
||||
type: "phone",
|
||||
},
|
||||
};
|
||||
|
||||
const bookingResponse = await request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(bookingBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201);
|
||||
|
||||
const bookingResponseBody: CreateBookingOutput_2024_08_13 = bookingResponse.body;
|
||||
const createdBooking = bookingResponseBody.data;
|
||||
expect(createdBooking).toHaveProperty("id");
|
||||
|
||||
if (responseDataIsBooking(createdBooking)) {
|
||||
expect(createdBooking.location).toEqual(phone);
|
||||
await bookingsRepositoryFixture.deleteById(createdBooking.id);
|
||||
} else {
|
||||
throw new Error("Unexpected response data type");
|
||||
}
|
||||
});
|
||||
|
||||
it("can book with attendeeAddress location", async () => {
|
||||
const attendeeAddress = "123 Example St, City, Valhalla";
|
||||
|
||||
const bookingBody: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2040, 0, 9, 13, 0, 0)).toISOString(),
|
||||
eventTypeId: eventTypeWithAllLocationsId,
|
||||
attendee: {
|
||||
name: "Mr Proper",
|
||||
email: "mr_proper@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: {
|
||||
type: "attendeeAddress",
|
||||
address: attendeeAddress,
|
||||
},
|
||||
};
|
||||
|
||||
const bookingResponse = await request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(bookingBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201);
|
||||
|
||||
const bookingResponseBody: CreateBookingOutput_2024_08_13 = bookingResponse.body;
|
||||
const createdBooking = bookingResponseBody.data;
|
||||
expect(createdBooking).toHaveProperty("id");
|
||||
|
||||
if (responseDataIsBooking(createdBooking)) {
|
||||
expect(createdBooking.location).toEqual(attendeeAddress);
|
||||
await bookingsRepositoryFixture.deleteById(createdBooking.id);
|
||||
} else {
|
||||
throw new Error("Unexpected response data type");
|
||||
}
|
||||
});
|
||||
|
||||
xit("can book with attendeeAddress location", async () => {
|
||||
const attendeePhone = "+37120993151";
|
||||
|
||||
const bookingBody: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2040, 0, 9, 13, 0, 0)).toISOString(),
|
||||
eventTypeId: eventTypeWithAllLocationsId,
|
||||
attendee: {
|
||||
name: "Mr Proper",
|
||||
email: "mr_proper@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: {
|
||||
type: "attendeePhone",
|
||||
phone: attendeePhone,
|
||||
},
|
||||
};
|
||||
|
||||
const bookingResponse = await request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(bookingBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201);
|
||||
|
||||
const bookingResponseBody: CreateBookingOutput_2024_08_13 = bookingResponse.body;
|
||||
const createdBooking = bookingResponseBody.data;
|
||||
expect(createdBooking).toHaveProperty("id");
|
||||
|
||||
if (responseDataIsBooking(createdBooking)) {
|
||||
expect(createdBooking.location).toEqual(attendeePhone);
|
||||
await bookingsRepositoryFixture.deleteById(createdBooking.id);
|
||||
} else {
|
||||
throw new Error("Unexpected response data type");
|
||||
}
|
||||
});
|
||||
|
||||
it("can book with attendeeDefined location", async () => {
|
||||
const attendeeDefinedLocation = "namek 100";
|
||||
|
||||
const bookingBody: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2040, 0, 9, 13, 0, 0)).toISOString(),
|
||||
eventTypeId: eventTypeWithAllLocationsId,
|
||||
attendee: {
|
||||
name: "Mr Proper",
|
||||
email: "mr_proper@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: {
|
||||
type: "attendeeDefined",
|
||||
location: attendeeDefinedLocation,
|
||||
},
|
||||
};
|
||||
|
||||
const bookingResponse = await request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(bookingBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201);
|
||||
|
||||
const bookingResponseBody: CreateBookingOutput_2024_08_13 = bookingResponse.body;
|
||||
const createdBooking = bookingResponseBody.data;
|
||||
expect(createdBooking).toHaveProperty("id");
|
||||
|
||||
if (responseDataIsBooking(createdBooking)) {
|
||||
expect(createdBooking.location).toEqual(attendeeDefinedLocation);
|
||||
await bookingsRepositoryFixture.deleteById(createdBooking.id);
|
||||
} else {
|
||||
throw new Error("Unexpected response data type");
|
||||
}
|
||||
});
|
||||
|
||||
it("can't book with not location that is not in event type", async () => {
|
||||
const bookingBody: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2040, 0, 9, 13, 0, 0)).toISOString(),
|
||||
eventTypeId: eventTypeWithAllLocationsId,
|
||||
attendee: {
|
||||
name: "Mr Proper",
|
||||
email: "mr_proper@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: {
|
||||
type: "integration",
|
||||
integration: "google-meet",
|
||||
},
|
||||
};
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(bookingBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it("can't book with invalid location type", async () => {
|
||||
const bookingBody = {
|
||||
start: new Date(Date.UTC(2040, 0, 9, 13, 0, 0)).toISOString(),
|
||||
eventTypeId: eventTypeWithAllLocationsId,
|
||||
attendee: {
|
||||
name: "Mr Proper",
|
||||
email: "mr_proper@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: {
|
||||
type: "blablabala",
|
||||
},
|
||||
};
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(bookingBody)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(400);
|
||||
});
|
||||
});
|
||||
|
||||
describe("rescheduling booking with invalid booking responses", () => {
|
||||
it("should reschedule", async () => {
|
||||
const email = `user-bookings-attendee-${randomString(10)}@gmail.com`;
|
||||
const username = `user-bookings-attendee-${randomString(10)}`;
|
||||
const locale = "it";
|
||||
const timeZone = "Europe/Rome";
|
||||
const attendee = await userRepositoryFixture.create({
|
||||
email,
|
||||
username,
|
||||
locale,
|
||||
timeZone,
|
||||
});
|
||||
|
||||
const booking = await bookingsRepositoryFixture.create({
|
||||
uid: `booking-uid-${randomString(10)}`,
|
||||
title: "booking title",
|
||||
startTime: "2060-09-05T11:00:00.000Z",
|
||||
endTime: "2060-09-05T12:00:00.000Z",
|
||||
eventType: {
|
||||
connect: {
|
||||
id: eventTypeId,
|
||||
},
|
||||
},
|
||||
status: "ACCEPTED",
|
||||
metadata: {},
|
||||
responses: "null",
|
||||
user: {
|
||||
connect: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
attendees: {
|
||||
create: {
|
||||
email: email,
|
||||
name: username,
|
||||
timeZone,
|
||||
locale,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const body: RescheduleBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2060, 0, 9, 14, 0, 0)).toISOString(),
|
||||
reschedulingReason: "Flying to mars that day",
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${booking.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();
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
const data: BookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.reschedulingReason).toEqual(body.reschedulingReason);
|
||||
expect(data.start).toEqual(body.start);
|
||||
expect(data.end).toEqual(new Date(Date.UTC(2060, 0, 9, 15, 0, 0)).toISOString());
|
||||
expect(data.rescheduledFromUid).toEqual(booking.uid);
|
||||
expect(data.id).toBeDefined();
|
||||
expect(data.uid).toBeDefined();
|
||||
expect(data.hosts[0].id).toEqual(user.id);
|
||||
expect(data.hosts[0].username).toEqual(user.username);
|
||||
expect(data.hosts[0].email).toEqual(user.email);
|
||||
expect(data.status).toEqual("accepted");
|
||||
expect(data.eventTypeId).toEqual(booking.eventTypeId);
|
||||
expect(data.attendees.length).toEqual(1);
|
||||
expect(data.attendees[0]).toEqual({
|
||||
name: attendee.username,
|
||||
email: attendee.email,
|
||||
timeZone: attendee.timeZone,
|
||||
language: attendee.locale,
|
||||
absent: false,
|
||||
});
|
||||
expect(data.bookingFieldsResponses).toEqual({
|
||||
name: attendee.username,
|
||||
email: attendee.email,
|
||||
rescheduledReason: body.reschedulingReason,
|
||||
guests: [],
|
||||
});
|
||||
await bookingsRepositoryFixture.deleteById(data.id);
|
||||
await userRepositoryFixture.delete(attendee.id);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function responseDataIsRecurranceBooking(data: any): data is RecurringBookingOutput_2024_08_13 {
|
||||
return (
|
||||
!Array.isArray(data) &&
|
||||
|
||||
@@ -229,8 +229,7 @@ export class BookingsService_2024_08_13 {
|
||||
const fetchedBookings: { bookings: { id: number }[] } = await getAllUserBookings({
|
||||
bookingListingByStatus: queryParams.status || [],
|
||||
skip: queryParams.skip ?? 0,
|
||||
// note(Lauris): we substract -1 because getAllUSerBookings child function adds +1 for some reason
|
||||
take: queryParams.take ? queryParams.take - 1 : 100,
|
||||
take: queryParams.take ?? 100,
|
||||
filters: this.inputService.transformGetBookingsFilters(queryParams),
|
||||
ctx: {
|
||||
user,
|
||||
@@ -564,7 +563,9 @@ export class BookingsService_2024_08_13 {
|
||||
throw new BadRequestException(`Booking with uid ${bookingUid} has no event type`);
|
||||
}
|
||||
|
||||
const eventType = await this.eventTypesRepository.getEventTypeByIdIncludeUsersAndTeam(booking.eventTypeId);
|
||||
const eventType = await this.eventTypesRepository.getEventTypeByIdIncludeUsersAndTeam(
|
||||
booking.eventTypeId
|
||||
);
|
||||
if (!eventType) {
|
||||
throw new BadRequestException(`Booking with uid ${bookingUid} has no event type`);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,10 @@ import {
|
||||
} from "@/ee/bookings/2024-08-13/services/output.service";
|
||||
import { PlatformBookingsService } from "@/ee/bookings/shared/platform-bookings.service";
|
||||
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
||||
import { OutputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/output-event-types.service";
|
||||
import { hashAPIKey, isApiKey, stripApiKey } from "@/lib/api-key";
|
||||
import { defaultBookingResponses } from "@/lib/safe-parse/default-responses-booking";
|
||||
import { safeParse } from "@/lib/safe-parse/safe-parse";
|
||||
import { ApiKeysRepository } from "@/modules/api-keys/api-keys-repository";
|
||||
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
|
||||
import { OAuthClientUsersService } from "@/modules/oauth-clients/services/oauth-clients-users.service";
|
||||
@@ -15,13 +18,17 @@ import { BadRequestException, Injectable, NotFoundException } from "@nestjs/comm
|
||||
import { Logger } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { CreationSource } from "@prisma/client";
|
||||
import { isURL, isPhoneNumber } from "class-validator";
|
||||
import { Request } from "express";
|
||||
import { DateTime } from "luxon";
|
||||
import { NextApiRequest } from "next/types";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { EventTypeMetaDataSchema } from "@calcom/platform-libraries/event-types";
|
||||
import {
|
||||
EventTypeMetaDataSchema,
|
||||
apiToInternalintegrationsMapping,
|
||||
} from "@calcom/platform-libraries/event-types";
|
||||
import {
|
||||
CancelBookingInput,
|
||||
CancelBookingInput_2024_08_13,
|
||||
@@ -35,6 +42,7 @@ import {
|
||||
RescheduleBookingInput_2024_08_13,
|
||||
RescheduleSeatedBookingInput_2024_08_13,
|
||||
} from "@calcom/platform-types";
|
||||
import { BookingInputLocation_2024_08_13 } from "@calcom/platform-types/bookings/2024-08-13/inputs/location.input";
|
||||
import { EventType } from "@calcom/prisma/client";
|
||||
|
||||
type BookingRequest = NextApiRequest & { userId: number | undefined } & OAuthRequestParams;
|
||||
@@ -77,6 +85,7 @@ export class InputBookingsService_2024_08_13 {
|
||||
private readonly config: ConfigService,
|
||||
private readonly apiKeyRepository: ApiKeysRepository,
|
||||
private readonly bookingSeatRepository: BookingSeatRepository,
|
||||
private readonly outputEventTypesService: OutputEventTypesService_2024_06_14,
|
||||
private readonly platformBookingsService: PlatformBookingsService,
|
||||
private readonly usersRepository: UsersRepository
|
||||
) {}
|
||||
@@ -91,23 +100,21 @@ export class InputBookingsService_2024_08_13 {
|
||||
const newRequest = { ...request };
|
||||
const userId = (await this.createBookingRequestOwnerId(request)) ?? undefined;
|
||||
|
||||
const location = request.body.location || request.body.meetingUrl;
|
||||
this.logger.log(`createBookingRequest_2024_04_15`, {
|
||||
this.logger.log(`createBookingRequest_2024_08_13`, {
|
||||
requestId: request.get("X-Request-Id"),
|
||||
ownerId: userId,
|
||||
location,
|
||||
oAuthClientParams,
|
||||
});
|
||||
|
||||
if (oAuthClientParams) {
|
||||
Object.assign(newRequest, { userId, ...oAuthClientParams, platformBookingLocation: location });
|
||||
Object.assign(newRequest, { userId, ...oAuthClientParams });
|
||||
newRequest.body = {
|
||||
...bodyTransformed,
|
||||
noEmail: !oAuthClientParams.arePlatformEmailsEnabled,
|
||||
creationSource: CreationSource.API_V2,
|
||||
};
|
||||
} else {
|
||||
Object.assign(newRequest, { userId, platformBookingLocation: location });
|
||||
Object.assign(newRequest, { userId });
|
||||
newRequest.body = { ...bodyTransformed, noEmail: false, creationSource: CreationSource.API_V2 };
|
||||
}
|
||||
|
||||
@@ -143,6 +150,10 @@ export class InputBookingsService_2024_08_13 {
|
||||
)
|
||||
: inputBooking.attendee.email;
|
||||
|
||||
const inputLocation = inputBooking.location || inputBooking.meetingUrl;
|
||||
this.isBookingLocationWithEventTypeLocations(inputLocation, eventType);
|
||||
const location = inputLocation ? this.transformLocation(inputLocation) : undefined;
|
||||
|
||||
return {
|
||||
start: startTime.toISO(),
|
||||
end: endTime.toISO(),
|
||||
@@ -159,6 +170,7 @@ export class InputBookingsService_2024_08_13 {
|
||||
email: attendeeEmail ?? "",
|
||||
attendeePhoneNumber: inputBooking.attendee.phoneNumber,
|
||||
guests,
|
||||
location,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -196,17 +208,14 @@ export class InputBookingsService_2024_08_13 {
|
||||
const newRequest = { ...request };
|
||||
const userId = (await this.createBookingRequestOwnerId(request)) ?? undefined;
|
||||
|
||||
const location = request.body.location || request.body.meetingUrl;
|
||||
|
||||
if (oAuthClientParams) {
|
||||
Object.assign(newRequest, {
|
||||
userId,
|
||||
...oAuthClientParams,
|
||||
platformBookingLocation: location,
|
||||
noEmail: !oAuthClientParams.arePlatformEmailsEnabled,
|
||||
});
|
||||
} else {
|
||||
Object.assign(newRequest, { userId, platformBookingLocation: location });
|
||||
Object.assign(newRequest, { userId });
|
||||
}
|
||||
|
||||
newRequest.body = bodyTransformed.map((event) => ({
|
||||
@@ -217,6 +226,144 @@ export class InputBookingsService_2024_08_13 {
|
||||
return newRequest as unknown as BookingRequest;
|
||||
}
|
||||
|
||||
transformLocation(location: string | BookingInputLocation_2024_08_13): {
|
||||
value: string;
|
||||
optionValue: string;
|
||||
} {
|
||||
if (typeof location === "string") {
|
||||
// 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.")) {
|
||||
return {
|
||||
value: "link",
|
||||
optionValue: location,
|
||||
};
|
||||
}
|
||||
|
||||
if (isPhoneNumber(location)) {
|
||||
return {
|
||||
value: "phone",
|
||||
optionValue: location,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
value: "somewhereElse",
|
||||
optionValue: location,
|
||||
};
|
||||
}
|
||||
|
||||
if (location.type === "integration") {
|
||||
const integration = apiToInternalintegrationsMapping[location.integration];
|
||||
if (!integration) {
|
||||
throw new BadRequestException(`Invalid integration: ${location.integration}`);
|
||||
}
|
||||
return {
|
||||
value: integration,
|
||||
optionValue: "",
|
||||
};
|
||||
}
|
||||
|
||||
if (location.type === "address") {
|
||||
return {
|
||||
value: "inPerson",
|
||||
optionValue: "",
|
||||
};
|
||||
}
|
||||
|
||||
if (location.type === "attendeeAddress") {
|
||||
return {
|
||||
value: "attendeeInPerson",
|
||||
optionValue: location.address,
|
||||
};
|
||||
}
|
||||
|
||||
if (location.type === "link") {
|
||||
return {
|
||||
value: "link",
|
||||
optionValue: "",
|
||||
};
|
||||
}
|
||||
|
||||
if (location.type === "phone") {
|
||||
return {
|
||||
value: "userPhone",
|
||||
optionValue: "",
|
||||
};
|
||||
}
|
||||
|
||||
if (location.type === "organizersDefaultApp") {
|
||||
return {
|
||||
value: "conferencing",
|
||||
optionValue: "",
|
||||
};
|
||||
}
|
||||
|
||||
if (location.type === "attendeePhone") {
|
||||
return {
|
||||
value: "phone",
|
||||
optionValue: location.phone,
|
||||
};
|
||||
}
|
||||
|
||||
if (location.type === "attendeeDefined") {
|
||||
return {
|
||||
value: "somewhereElse",
|
||||
optionValue: location.location,
|
||||
};
|
||||
}
|
||||
|
||||
throw new BadRequestException(
|
||||
`Booking location with type ${(location as BookingInputLocation_2024_08_13).type} not valid.`
|
||||
);
|
||||
}
|
||||
|
||||
private isBookingLocationWithEventTypeLocations(
|
||||
inputBookingLocation: undefined | string | BookingInputLocation_2024_08_13,
|
||||
dbEventType: EventType
|
||||
) {
|
||||
if (!inputBookingLocation || typeof inputBookingLocation === "string") {
|
||||
// note(Lauris): for backwards compatibility because we had string locations before so let them pass.
|
||||
return true;
|
||||
}
|
||||
|
||||
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=${
|
||||
dbEventType.id
|
||||
}. The event type has following location types: ${allowedLocationTypes.join(
|
||||
", "
|
||||
)}, and only these types are allowed for booking location.`
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
if (!isAllowedIntegration) {
|
||||
throw new BadRequestException(
|
||||
`Booking location with integration ${
|
||||
inputBookingLocation.integration
|
||||
} not valid for event type with id=${
|
||||
dbEventType.id
|
||||
}. The event type has following integrations: ${allowedIntegrations.join(
|
||||
", "
|
||||
)}, and only these integrations are allowed for booking location.`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async transformInputCreateRecurringBooking(
|
||||
inputBooking: CreateRecurringBookingInput_2024_08_13,
|
||||
platformClientId?: string
|
||||
@@ -262,6 +409,10 @@ export class InputBookingsService_2024_08_13 {
|
||||
)
|
||||
: inputBooking.attendee.email;
|
||||
|
||||
const inputLocation = inputBooking.location || inputBooking.meetingUrl;
|
||||
this.isBookingLocationWithEventTypeLocations(inputLocation, eventType);
|
||||
const location = inputLocation ? this.transformLocation(inputLocation) : undefined;
|
||||
|
||||
for (let i = 0; i < repeatsTimes; i++) {
|
||||
const endTime = startTime.plus({ minutes: eventType.length });
|
||||
|
||||
@@ -280,7 +431,9 @@ export class InputBookingsService_2024_08_13 {
|
||||
...(inputBooking.bookingFieldsResponses || {}),
|
||||
name: inputBooking.attendee.name,
|
||||
email: attendeeEmail,
|
||||
attendeePhoneNumber: inputBooking.attendee.phoneNumber,
|
||||
guests,
|
||||
location,
|
||||
},
|
||||
schedulingType: eventType.schedulingType,
|
||||
});
|
||||
@@ -417,8 +570,19 @@ export class InputBookingsService_2024_08_13 {
|
||||
throw new NotFoundException(`Event type with id=${booking.eventTypeId} not found`);
|
||||
}
|
||||
|
||||
const bookingResponses = bookingResponsesSchema.parse(booking.responses);
|
||||
const attendee = booking.attendees.find((attendee) => attendee.email === bookingResponses.email);
|
||||
const bookingResponses = safeParse(
|
||||
bookingResponsesSchema,
|
||||
booking.responses,
|
||||
defaultBookingResponses,
|
||||
false
|
||||
);
|
||||
const bookingResponsesMissing =
|
||||
bookingResponses.name === defaultBookingResponses.name &&
|
||||
bookingResponses.email === defaultBookingResponses.email;
|
||||
|
||||
const attendee = bookingResponsesMissing
|
||||
? booking.attendees[0]
|
||||
: booking.attendees.find((attendee) => attendee.email === bookingResponses.email);
|
||||
|
||||
if (!attendee) {
|
||||
throw new NotFoundException(
|
||||
@@ -426,6 +590,12 @@ export class InputBookingsService_2024_08_13 {
|
||||
);
|
||||
}
|
||||
|
||||
if (bookingResponsesMissing) {
|
||||
bookingResponses.name = attendee.name;
|
||||
bookingResponses.email = attendee.email;
|
||||
bookingResponses.attendeePhoneNumber = attendee.phoneNumber || undefined;
|
||||
}
|
||||
|
||||
const startTime = DateTime.fromISO(inputBooking.start, { zone: "utc" }).setZone(attendee.timeZone);
|
||||
const endTime = startTime.plus({ minutes: eventType.length });
|
||||
|
||||
@@ -482,6 +652,8 @@ export class InputBookingsService_2024_08_13 {
|
||||
queryParams.eventTypeIds || (queryParams.eventTypeId ? [queryParams.eventTypeId] : undefined),
|
||||
afterUpdatedDate: queryParams.afterUpdatedAt,
|
||||
beforeUpdatedDate: queryParams.beforeUpdatedAt,
|
||||
afterCreatedDate: queryParams.afterCreatedAt,
|
||||
beforeCreatedDate: queryParams.beforeCreatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,7 @@ import { Booking, BookingSeat } from "@calcom/prisma/client";
|
||||
export const bookingResponsesSchema = z
|
||||
.object({
|
||||
email: z.string(),
|
||||
attendeePhoneNumber: z.string().optional(),
|
||||
name: z.union([
|
||||
z.string(),
|
||||
z.object({
|
||||
@@ -45,6 +46,7 @@ export const seatedBookingDataSchema = z
|
||||
responses: z
|
||||
.object({
|
||||
email: z.string(),
|
||||
attendeePhoneNumber: z.string().optional(),
|
||||
name: z.union([
|
||||
z.string(),
|
||||
z.object({
|
||||
|
||||
@@ -42,6 +42,10 @@ export class PlatformBookingsService {
|
||||
} else if (eventType?.teamId) {
|
||||
oAuthClient = await this.oAuthClientRepository.getByTeamId(eventType.teamId);
|
||||
}
|
||||
// Last resort check the hosts of the event-type
|
||||
if (!oAuthClient && eventType?.teamId) {
|
||||
oAuthClient = await this.oAuthClientRepository.getByEventTypeHosts(eventTypeId);
|
||||
}
|
||||
|
||||
if (oAuthClient) {
|
||||
return {
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||
import { Process, Processor } from "@nestjs/bull";
|
||||
import { Logger } from "@nestjs/common";
|
||||
import { Job } from "bull";
|
||||
|
||||
export const DEFAULT_CALENDARS_JOB = "default_calendars_job";
|
||||
export const CALENDARS_QUEUE = "calendars";
|
||||
export type DefaultCalendarsJobDataType = {
|
||||
userId: number;
|
||||
};
|
||||
|
||||
@Processor(CALENDARS_QUEUE)
|
||||
export class CalendarsProcessor {
|
||||
private readonly logger = new Logger(CalendarsProcessor.name);
|
||||
|
||||
constructor(public readonly calendarsService: CalendarsService) {}
|
||||
|
||||
@Process(DEFAULT_CALENDARS_JOB)
|
||||
async handleEnsureDefaultCalendars(job: Job<DefaultCalendarsJobDataType>) {
|
||||
const { userId } = job.data;
|
||||
try {
|
||||
// getCalendars calls getConnectedDestinationCalendarsAndEnsureDefaultsInDb from platform libraries
|
||||
// which gets the calendars from third party providers and ensure default calendars are set in DB
|
||||
await this.calendarsService.getCalendars(userId);
|
||||
} catch (err) {
|
||||
this.logger.error(`Failed to load default calendars of user with id: ${userId}`, {
|
||||
userId,
|
||||
err,
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
+50
-1
@@ -116,6 +116,7 @@ describe("Event types Endpoints", () => {
|
||||
slug: "email",
|
||||
required: true,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
};
|
||||
|
||||
const defaultResponseBookingFieldLocation = {
|
||||
@@ -336,6 +337,46 @@ describe("Event types Endpoints", () => {
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
it("should not be able to create phone-only event type", async () => {
|
||||
const body: CreateEventTypeInput_2024_06_14 = {
|
||||
title: "Phone coding consultation",
|
||||
slug: "phone-coding-consultation",
|
||||
description: "Our team will review your codebase.",
|
||||
lengthInMinutes: 60,
|
||||
locations: [
|
||||
{
|
||||
type: "integration",
|
||||
integration: "cal-video",
|
||||
},
|
||||
],
|
||||
bookingFields: [
|
||||
{
|
||||
type: "email",
|
||||
required: false,
|
||||
label: "Email",
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
type: "phone",
|
||||
slug: "attendeePhoneNumber",
|
||||
required: true,
|
||||
label: "Phone number",
|
||||
hidden: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post("/api/v2/event-types")
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_06_14)
|
||||
.send(body)
|
||||
.expect(400);
|
||||
|
||||
expect(response.body.error.message).toBe(
|
||||
"checkIsEmailUserAccessible - Email booking field must be required and visible"
|
||||
);
|
||||
});
|
||||
|
||||
it("should create an event type", async () => {
|
||||
const nameBookingField: NameDefaultFieldInput_2024_06_14 = {
|
||||
type: "name",
|
||||
@@ -1191,7 +1232,14 @@ describe("Event types Endpoints", () => {
|
||||
|
||||
const expectedReturnSystemFields = [
|
||||
{ isDefault: true, required: true, slug: "name", type: "name", disableOnPrefill: false },
|
||||
{ isDefault: true, required: true, slug: "email", type: "email", disableOnPrefill: false },
|
||||
{
|
||||
isDefault: true,
|
||||
required: true,
|
||||
slug: "email",
|
||||
type: "email",
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "radioInput",
|
||||
@@ -1644,6 +1692,7 @@ describe("Event types Endpoints", () => {
|
||||
slug: "email",
|
||||
required: true,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
|
||||
+11
-2
@@ -29,7 +29,12 @@ import {
|
||||
} from "@nestjs/common";
|
||||
import { ApiHeader, ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
|
||||
import { EVENT_TYPE_READ, EVENT_TYPE_WRITE, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import {
|
||||
EVENT_TYPE_READ,
|
||||
EVENT_TYPE_WRITE,
|
||||
SUCCESS_STATUS,
|
||||
VERSION_2024_06_14,
|
||||
} from "@calcom/platform-constants";
|
||||
import {
|
||||
UpdateEventTypeInput_2024_06_14,
|
||||
GetEventTypesQuery_2024_06_14,
|
||||
@@ -45,8 +50,12 @@ import {
|
||||
@DocsTags("Event Types")
|
||||
@ApiHeader({
|
||||
name: "cal-api-version",
|
||||
description: `Must be set to \`2024-06-14\``,
|
||||
description: `Must be set to ${VERSION_2024_06_14}`,
|
||||
example: VERSION_2024_06_14,
|
||||
required: true,
|
||||
schema: {
|
||||
default: VERSION_2024_06_14,
|
||||
},
|
||||
})
|
||||
export class EventTypesController_2024_06_14 {
|
||||
constructor(
|
||||
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
updateEventType,
|
||||
getEventTypesPublic,
|
||||
EventTypesPublic,
|
||||
SystemField,
|
||||
CustomField,
|
||||
} from "@calcom/platform-libraries/event-types";
|
||||
import { GetEventTypesQuery_2024_06_14, InputEventTransformed_2024_06_14 } from "@calcom/platform-types";
|
||||
import { EventType } from "@calcom/prisma/client";
|
||||
@@ -31,6 +33,9 @@ export class EventTypesService_2024_06_14 {
|
||||
) {}
|
||||
|
||||
async createUserEventType(user: UserWithProfile, body: InputEventTransformed_2024_06_14) {
|
||||
if (body.bookingFields) {
|
||||
this.checkHasUserAccessibleEmailBookingField(body.bookingFields);
|
||||
}
|
||||
await this.checkCanCreateEventType(user.id, body);
|
||||
const eventTypeUser = await this.getUserToCreateEvent(user);
|
||||
const { destinationCalendar: _destinationCalendar, ...rest } = body;
|
||||
@@ -78,6 +83,16 @@ export class EventTypesService_2024_06_14 {
|
||||
await this.checkUserOwnsSchedule(userId, body.scheduleId);
|
||||
}
|
||||
|
||||
checkHasUserAccessibleEmailBookingField(bookingFields: (SystemField | CustomField)[]) {
|
||||
const emailField = bookingFields.find((field) => field.type === "email" && field.name === "email");
|
||||
const isEmailFieldRequiredAndVisible = emailField?.required && !emailField?.hidden;
|
||||
if (!isEmailFieldRequiredAndVisible) {
|
||||
throw new BadRequestException(
|
||||
"checkIsEmailUserAccessible - Email booking field must be required and visible"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async getEventTypeByUsernameAndSlug(
|
||||
username: string,
|
||||
eventTypeSlug: string,
|
||||
@@ -219,6 +234,9 @@ export class EventTypesService_2024_06_14 {
|
||||
body: Partial<InputEventTransformed_2024_06_14>,
|
||||
user: UserWithProfile
|
||||
) {
|
||||
if (body.bookingFields) {
|
||||
this.checkHasUserAccessibleEmailBookingField(body.bookingFields);
|
||||
}
|
||||
await this.checkCanUpdateEventType(user.id, eventTypeId, body.scheduleId);
|
||||
const eventTypeUser = await this.getUserToUpdateEvent(user);
|
||||
|
||||
|
||||
+25
-14
@@ -37,8 +37,6 @@ import {
|
||||
} from "@calcom/platform-types";
|
||||
import { BookerLayouts } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import { OutputEventTypesService_2024_06_14 } from "./output-event-types.service";
|
||||
|
||||
interface ValidationContext {
|
||||
eventTypeId?: number;
|
||||
seatsPerTimeSlot?: number | null;
|
||||
@@ -51,7 +49,6 @@ interface ValidationContext {
|
||||
export class InputEventTypesService_2024_06_14 {
|
||||
constructor(
|
||||
private readonly eventTypesRepository: EventTypesRepository_2024_06_14,
|
||||
private readonly outputEventTypesService: OutputEventTypesService_2024_06_14,
|
||||
private readonly calendarsService: CalendarsService
|
||||
) {}
|
||||
|
||||
@@ -128,12 +125,11 @@ export class InputEventTypesService_2024_06_14 {
|
||||
} = inputEventType;
|
||||
const confirmationPolicyTransformed = this.transformInputConfirmationPolicy(confirmationPolicy);
|
||||
|
||||
const hasMultipleLocations = (locations || defaultLocations).length > 1;
|
||||
const eventType = {
|
||||
...rest,
|
||||
length: lengthInMinutes,
|
||||
locations: this.transformInputLocations(locations || defaultLocations),
|
||||
bookingFields: this.transformInputBookingFields(bookingFields, hasMultipleLocations),
|
||||
bookingFields: this.transformInputBookingFields(bookingFields),
|
||||
bookingLimits: bookingLimitsCount ? this.transformInputIntervalLimits(bookingLimitsCount) : undefined,
|
||||
durationLimits: bookingLimitsDuration
|
||||
? this.transformInputIntervalLimits(bookingLimitsDuration)
|
||||
@@ -182,15 +178,12 @@ export class InputEventTypesService_2024_06_14 {
|
||||
: {};
|
||||
|
||||
const confirmationPolicyTransformed = this.transformInputConfirmationPolicy(confirmationPolicy);
|
||||
const hasMultipleLocations = !!(locations && locations?.length > 1);
|
||||
|
||||
const eventType = {
|
||||
...rest,
|
||||
length: lengthInMinutes,
|
||||
locations: locations ? this.transformInputLocations(locations) : undefined,
|
||||
bookingFields: bookingFields
|
||||
? this.transformInputBookingFields(bookingFields, hasMultipleLocations)
|
||||
: undefined,
|
||||
bookingFields: bookingFields ? this.transformInputBookingFields(bookingFields) : undefined,
|
||||
bookingLimits: bookingLimitsCount ? this.transformInputIntervalLimits(bookingLimitsCount) : undefined,
|
||||
durationLimits: bookingLimitsDuration
|
||||
? this.transformInputIntervalLimits(bookingLimitsDuration)
|
||||
@@ -220,10 +213,7 @@ export class InputEventTypesService_2024_06_14 {
|
||||
return transformLocationsApiToInternal(inputLocations);
|
||||
}
|
||||
|
||||
transformInputBookingFields(
|
||||
inputBookingFields: CreateEventTypeInput_2024_06_14["bookingFields"],
|
||||
hasMultipleLocations: boolean
|
||||
) {
|
||||
transformInputBookingFields(inputBookingFields: CreateEventTypeInput_2024_06_14["bookingFields"]) {
|
||||
const internalFields: (SystemField | CustomField)[] = inputBookingFields
|
||||
? transformBookingFieldsApiToInternal(inputBookingFields)
|
||||
: [];
|
||||
@@ -252,7 +242,28 @@ export class InputEventTypesService_2024_06_14 {
|
||||
systemCustomRescheduleReasonField || systemAfterFieldRescheduleReason,
|
||||
];
|
||||
|
||||
return [...defaultFieldsBefore, ...userCustomFields, ...defaultFieldsAfter];
|
||||
const bookingFields = [...defaultFieldsBefore, ...userCustomFields, ...defaultFieldsAfter];
|
||||
|
||||
if (!this.hasEmailOrPhoneOnlySetup(bookingFields)) {
|
||||
throw new BadRequestException(
|
||||
"Booking fields validation failed: visible and required email or visible and required attendee phone field is needed."
|
||||
);
|
||||
}
|
||||
|
||||
return bookingFields;
|
||||
}
|
||||
|
||||
hasEmailOrPhoneOnlySetup(bookingFields: (SystemField | CustomField)[]) {
|
||||
const emailField = bookingFields.find((field) => field.type === "email" && field.name === "email");
|
||||
const attendeePhoneNumberField = bookingFields.find(
|
||||
(field) => field.type === "phone" && field.name === "attendeePhoneNumber"
|
||||
);
|
||||
|
||||
const isEmailFieldRequiredAndVisible = emailField?.required && !emailField?.hidden;
|
||||
const isAttendeePhoneNumberFieldRequiredAndVisible =
|
||||
attendeePhoneNumberField?.required && !attendeePhoneNumberField?.hidden;
|
||||
|
||||
return isEmailFieldRequiredAndVisible || isAttendeePhoneNumberFieldRequiredAndVisible;
|
||||
}
|
||||
|
||||
isUserCustomField(field: SystemField | CustomField): field is CustomField {
|
||||
|
||||
+5
-1
@@ -39,8 +39,12 @@ import {
|
||||
@DocsTags("Schedules")
|
||||
@ApiHeader({
|
||||
name: "cal-api-version",
|
||||
description: `Must be set to \`2024-06-11\``,
|
||||
description: `Must be set to ${VERSION_2024_06_11}`,
|
||||
example: VERSION_2024_06_11,
|
||||
required: true,
|
||||
schema: {
|
||||
default: VERSION_2024_06_11,
|
||||
},
|
||||
})
|
||||
@ApiHeader({
|
||||
name: "Authorization",
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import { filterReqHeaders } from "@/lib/filterReqHeaders";
|
||||
import { ArgumentsHost, Catch, ExceptionFilter, Logger } from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
|
||||
import { ERROR_STATUS } from "@calcom/platform-constants";
|
||||
import {
|
||||
CalendarAppError,
|
||||
CalendarAppDelegationCredentialInvalidGrantError,
|
||||
CalendarAppDelegationCredentialError,
|
||||
CalendarAppDelegationCredentialConfigurationError,
|
||||
CalendarAppDelegationCredentialClientIdNotAuthorizedError,
|
||||
CalendarAppDelegationCredentialNotSetupError,
|
||||
} from "@calcom/platform-libraries/app-store";
|
||||
import { Response } from "@calcom/platform-types";
|
||||
|
||||
type CalendarError =
|
||||
| CalendarAppError
|
||||
| CalendarAppDelegationCredentialInvalidGrantError
|
||||
| CalendarAppDelegationCredentialError
|
||||
| CalendarAppDelegationCredentialConfigurationError
|
||||
| CalendarAppDelegationCredentialNotSetupError
|
||||
| CalendarAppDelegationCredentialClientIdNotAuthorizedError;
|
||||
@Catch(
|
||||
CalendarAppError,
|
||||
CalendarAppDelegationCredentialInvalidGrantError,
|
||||
CalendarAppDelegationCredentialError,
|
||||
CalendarAppDelegationCredentialConfigurationError,
|
||||
CalendarAppDelegationCredentialClientIdNotAuthorizedError,
|
||||
CalendarAppDelegationCredentialNotSetupError
|
||||
)
|
||||
export class CalendarServiceExceptionFilter implements ExceptionFilter {
|
||||
private readonly logger = new Logger("CalendarServiceExceptionFilter");
|
||||
|
||||
catch(exception: CalendarError, host: ArgumentsHost) {
|
||||
const ctx = host.switchToHttp();
|
||||
const response = ctx.getResponse<Response>();
|
||||
const request = ctx.getRequest<Request>();
|
||||
|
||||
const requestId = request.headers["X-Request-Id"];
|
||||
|
||||
this.logger.error(`Calendar Service Exception Filter: ${exception?.message}`, {
|
||||
exception,
|
||||
body: request.body,
|
||||
headers: filterReqHeaders(request.headers),
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
requestId,
|
||||
});
|
||||
|
||||
response.status(400).json({
|
||||
status: ERROR_STATUS,
|
||||
timestamp: new Date().toISOString(),
|
||||
path: request.url,
|
||||
error: { code: exception.name, message: exception.message },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -16,29 +16,74 @@ export class TRPCExceptionFilter implements ExceptionFilter {
|
||||
const request = ctx.getRequest<Request>();
|
||||
|
||||
let statusCode = 500;
|
||||
let errorMessage = exception.message;
|
||||
|
||||
switch (exception.code) {
|
||||
case "UNAUTHORIZED":
|
||||
statusCode = 401;
|
||||
errorMessage = "You are not authorized to access this resource";
|
||||
break;
|
||||
case "FORBIDDEN":
|
||||
statusCode = 403;
|
||||
errorMessage = "You don't have necessary permissions to access this resource";
|
||||
break;
|
||||
case "NOT_FOUND":
|
||||
statusCode = 404;
|
||||
errorMessage = "The requested resource was not found";
|
||||
break;
|
||||
case "INTERNAL_SERVER_ERROR":
|
||||
statusCode = 500;
|
||||
errorMessage = "An unexpected error occurred on the server. Please try again later";
|
||||
break;
|
||||
case "BAD_REQUEST":
|
||||
statusCode = 400;
|
||||
errorMessage = "Bad request: Please check your request data and try again";
|
||||
break;
|
||||
case "CONFLICT":
|
||||
statusCode = 409;
|
||||
errorMessage = "Could not process the request due to a resource conflict with the current state";
|
||||
break;
|
||||
case "TOO_MANY_REQUESTS":
|
||||
statusCode = 429;
|
||||
errorMessage = "You have exceeded the allowed number of requests";
|
||||
break;
|
||||
case "PAYLOAD_TOO_LARGE":
|
||||
statusCode = 413;
|
||||
errorMessage = "The request payload is too large";
|
||||
break;
|
||||
case "CLIENT_CLOSED_REQUEST":
|
||||
statusCode = 499;
|
||||
errorMessage =
|
||||
"The client closed the connection before the server could finish processing the request";
|
||||
break;
|
||||
case "METHOD_NOT_SUPPORTED":
|
||||
statusCode = 405;
|
||||
errorMessage = "The requested method is not supported for this resource";
|
||||
case "NOT_IMPLEMENTED":
|
||||
statusCode = 501;
|
||||
errorMessage = "The requested method is not implemented on the server";
|
||||
break;
|
||||
case "PRECONDITION_FAILED":
|
||||
statusCode = 412;
|
||||
errorMessage =
|
||||
"The server does not meet one of the preconditions that the requester put on the request";
|
||||
break;
|
||||
case "TIMEOUT":
|
||||
statusCode = 408;
|
||||
errorMessage = "The request took too long to complete. Please try again later";
|
||||
break;
|
||||
case "UNPROCESSABLE_CONTENT":
|
||||
statusCode = 422;
|
||||
errorMessage =
|
||||
"The server was unable to process the request because the request payload is semantically incorrect or because the request is syntactically incorrect";
|
||||
break;
|
||||
case "PARSE_ERROR":
|
||||
statusCode = 400;
|
||||
errorMessage = "The request could not be parsed due to invalid syntax.";
|
||||
break;
|
||||
default:
|
||||
statusCode = 500;
|
||||
errorMessage = exception.message;
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -57,7 +102,7 @@ export class TRPCExceptionFilter implements ExceptionFilter {
|
||||
status: ERROR_STATUS,
|
||||
timestamp: new Date().toISOString(),
|
||||
path: request.url,
|
||||
error: { code: exception.name, message: exception.message },
|
||||
error: { code: exception.name, message: errorMessage },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,19 +2,20 @@ import { IncomingHttpHeaders } from "http";
|
||||
|
||||
export function filterReqHeaders(headers: IncomingHttpHeaders): Partial<IncomingHttpHeaders> {
|
||||
return {
|
||||
"Content-Type": headers["content-type"],
|
||||
"User-Agent": headers["user-agent"],
|
||||
"Content-Type": headers["Content-Type"] ?? headers["content-type"],
|
||||
Authorization: headers["Authorization"] ?? headers["authorization"],
|
||||
"X-Cal-Client-Id": headers["X-Cal-Client-Id"] ?? headers["x-cal-client-id"],
|
||||
"Cal-Api-Version": headers["Cal-Api-Version"] ?? headers["cal-api-version"],
|
||||
"X-Request-Id": headers["X-Request-Id"] ?? headers["x-request-id"],
|
||||
// IP-related headers
|
||||
"User-Agent": headers["User-agent"] ?? headers["user-agent"],
|
||||
"X-Forwarded-For": headers["x-forwarded-for"],
|
||||
"X-Forwarded-Host": headers["x-forwarded-host"],
|
||||
"CF-Connecting-IP": headers["cf-connecting-ip"],
|
||||
"CloudFront-Viewer-Address": headers["cloudfront-viewer-address"],
|
||||
// Vercel headers
|
||||
"x-vercel-id": headers["x-vercel-id"],
|
||||
"x-vercel-deployment-url": headers["x-vercel-deployment-url"],
|
||||
"x-vercel-country": headers["x-vercel-country"],
|
||||
"x-vercel-region": headers["x-vercel-region"],
|
||||
Host: headers["host"],
|
||||
"X-Vercel-Id": headers["x-vercel-id"],
|
||||
"X-Vercel-Deployment-Url": headers["x-vercel-deployment-url"],
|
||||
"X-Vercel-Country": headers["x-vercel-country"],
|
||||
"X-Vercel-Region": headers["x-vercel-region"],
|
||||
Host: headers["Host"] ?? headers["host"],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ export const defaultBookingResponses = {
|
||||
name: "unknown",
|
||||
email: "unknown",
|
||||
guests: [],
|
||||
rescheduleReason: "unknown",
|
||||
rescheduledReason: "unknown",
|
||||
};
|
||||
|
||||
export const defaultBookingMetadata = { videoCallUrl: "unknown" };
|
||||
|
||||
@@ -21,13 +21,23 @@ export class ResponseInterceptor implements NestInterceptor {
|
||||
const { statusCode } = response;
|
||||
const responseTime = Date.now() - startTime;
|
||||
|
||||
let jsonBodyString = "{}";
|
||||
|
||||
try {
|
||||
if (data && typeof data === "object") {
|
||||
jsonBodyString = JSON.stringify(data);
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error("Could not parse request body");
|
||||
}
|
||||
|
||||
this.logger.log("Outgoing Response", {
|
||||
requestId,
|
||||
method,
|
||||
url,
|
||||
statusCode,
|
||||
responseTime,
|
||||
responseBody: data,
|
||||
responseBody: jsonBodyString,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
})
|
||||
|
||||
@@ -11,13 +11,22 @@ export class RequestIdMiddleware implements NestMiddleware {
|
||||
const requestId = uuid();
|
||||
req.headers["X-Request-Id"] = requestId;
|
||||
const { method, headers, body: requestBody, baseUrl } = req;
|
||||
let jsonBodyString = "{}";
|
||||
|
||||
try {
|
||||
if (requestBody && typeof requestBody === "object") {
|
||||
jsonBodyString = JSON.stringify(requestBody);
|
||||
}
|
||||
} catch (err) {
|
||||
this.logger.error("Could not parse request body");
|
||||
}
|
||||
|
||||
this.logger.log("Incoming Request", {
|
||||
requestId,
|
||||
method,
|
||||
url: baseUrl,
|
||||
headers: filterReqHeaders(headers),
|
||||
requestBody,
|
||||
requestBody: jsonBodyString,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
|
||||
|
||||
@@ -35,12 +35,13 @@ import {
|
||||
Redirect,
|
||||
UnauthorizedException,
|
||||
Req,
|
||||
HttpException,
|
||||
} from "@nestjs/common";
|
||||
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
import { ApiOperation, ApiParam, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
import { plainToInstance } from "class-transformer";
|
||||
import { Request } from "express";
|
||||
|
||||
import { GOOGLE_MEET, ZOOM, SUCCESS_STATUS, OFFICE_365_VIDEO } from "@calcom/platform-constants";
|
||||
import { GOOGLE_MEET, ZOOM, SUCCESS_STATUS, OFFICE_365_VIDEO, CAL_VIDEO } from "@calcom/platform-constants";
|
||||
|
||||
export type OAuthCallbackState = {
|
||||
accessToken: string;
|
||||
@@ -56,7 +57,7 @@ export type OAuthCallbackState = {
|
||||
})
|
||||
@DocsTags("Conferencing")
|
||||
export class ConferencingController {
|
||||
private readonly logger = new Logger("Platform Gcal Provider");
|
||||
private readonly logger = new Logger("ConferencingController");
|
||||
|
||||
constructor(
|
||||
private readonly tokensRepository: TokensRepository,
|
||||
@@ -70,6 +71,12 @@ export class ConferencingController {
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UseGuards(ApiAuthGuard)
|
||||
@ApiOperation({ summary: "Connect your conferencing application" })
|
||||
@ApiParam({
|
||||
name: "app",
|
||||
description: "Conferencing application type",
|
||||
enum: [GOOGLE_MEET],
|
||||
required: true,
|
||||
})
|
||||
async connect(
|
||||
@GetUser("id") userId: number,
|
||||
@Param("app") app: string
|
||||
@@ -92,6 +99,12 @@ export class ConferencingController {
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UseGuards(ApiAuthGuard)
|
||||
@ApiOperation({ summary: "Get OAuth conferencing app auth url" })
|
||||
@ApiParam({
|
||||
name: "app",
|
||||
description: "Conferencing application type",
|
||||
enum: [ZOOM, OFFICE_365_VIDEO],
|
||||
required: true,
|
||||
})
|
||||
async redirect(
|
||||
@Req() req: Request,
|
||||
@Headers("Authorization") authorization: string,
|
||||
@@ -137,6 +150,12 @@ export class ConferencingController {
|
||||
@UseGuards()
|
||||
@Redirect(undefined, 301)
|
||||
@ApiOperation({ summary: "conferencing apps oauths callback" })
|
||||
@ApiParam({
|
||||
name: "app",
|
||||
description: "Conferencing application type",
|
||||
enum: [ZOOM, OFFICE_365_VIDEO],
|
||||
required: true,
|
||||
})
|
||||
async save(
|
||||
@Query("state") state: string,
|
||||
@Param("app") app: string,
|
||||
@@ -144,6 +163,10 @@ export class ConferencingController {
|
||||
@Query("error") error: string | undefined,
|
||||
@Query("error_description") error_description: string | undefined
|
||||
): Promise<{ url: string }> {
|
||||
if (!state) {
|
||||
throw new BadRequestException("Missing `state` query param");
|
||||
}
|
||||
|
||||
const decodedCallbackState: OAuthCallbackState = JSON.parse(state);
|
||||
try {
|
||||
const userId = await this.tokensRepository.getAccessTokenOwnerId(decodedCallbackState.accessToken);
|
||||
@@ -169,6 +192,9 @@ export class ConferencingController {
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof HttpException || error instanceof Error) {
|
||||
this.logger.error(error.message);
|
||||
}
|
||||
return {
|
||||
url: decodedCallbackState.onErrorReturnTo ?? "",
|
||||
};
|
||||
@@ -195,6 +221,12 @@ export class ConferencingController {
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UseGuards(ApiAuthGuard)
|
||||
@ApiOperation({ summary: "Set your default conferencing application" })
|
||||
@ApiParam({
|
||||
name: "app",
|
||||
description: "Conferencing application type",
|
||||
enum: [GOOGLE_MEET, ZOOM, OFFICE_365_VIDEO, CAL_VIDEO],
|
||||
required: true,
|
||||
})
|
||||
async default(
|
||||
@GetUser() user: UserWithProfile,
|
||||
@Param("app") app: string
|
||||
@@ -216,6 +248,12 @@ export class ConferencingController {
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UseGuards(ApiAuthGuard)
|
||||
@ApiOperation({ summary: "Disconnect your conferencing application" })
|
||||
@ApiParam({
|
||||
name: "app",
|
||||
description: "Conferencing application type",
|
||||
enum: [GOOGLE_MEET, ZOOM, OFFICE_365_VIDEO],
|
||||
required: true,
|
||||
})
|
||||
async disconnect(
|
||||
@GetUser() user: UserWithProfile,
|
||||
@Param("app") app: string
|
||||
|
||||
+7
@@ -26,6 +26,7 @@ import { UserRepositoryFixture } from "test/fixtures/repository/users.repository
|
||||
import { randomString } from "test/utils/randomString";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { slugify } from "@calcom/platform-libraries";
|
||||
import { ApiSuccessResponse } from "@calcom/platform-types";
|
||||
|
||||
const CLIENT_REDIRECT_URI = "http://localhost:4321";
|
||||
@@ -228,6 +229,9 @@ describe("OAuth Client Users Endpoints", () => {
|
||||
expect(responseBody.data.user.timeFormat).toEqual(requestBody.timeFormat);
|
||||
expect(responseBody.data.user.locale).toEqual(requestBody.locale);
|
||||
expect(responseBody.data.user.avatarUrl).toEqual(requestBody.avatarUrl);
|
||||
const [emailUser, emailDomain] = responseBody.data.user.email.split("@");
|
||||
const [domainName, TLD] = emailDomain.split(".");
|
||||
expect(responseBody.data.user.username).toEqual(slugify(`${emailUser}-${domainName}-${TLD}`));
|
||||
expect(responseBody.data.accessToken).toBeDefined();
|
||||
expect(responseBody.data.refreshToken).toBeDefined();
|
||||
|
||||
@@ -546,6 +550,9 @@ describe("OAuth Client Users Endpoints", () => {
|
||||
expect(responseBody.data.email).toEqual(
|
||||
OAuthClientUsersService.getOAuthUserEmail(oAuthClient.id, userUpdatedEmail)
|
||||
);
|
||||
const [emailUser, emailDomain] = responseBody.data.email.split("@");
|
||||
const [domainName, TLD] = emailDomain.split(".");
|
||||
expect(responseBody.data.username).toEqual(slugify(`${emailUser}-${domainName}-${TLD}`));
|
||||
expect(responseBody.data.locale).toEqual(Locales.PT_BR);
|
||||
});
|
||||
|
||||
|
||||
+1
-14
@@ -29,19 +29,12 @@ import {
|
||||
NotFoundException,
|
||||
BadRequestException,
|
||||
} from "@nestjs/common";
|
||||
import {
|
||||
ApiOperation as DocsOperation,
|
||||
ApiCreatedResponse as DocsCreatedResponse,
|
||||
ApiTags,
|
||||
} from "@nestjs/swagger";
|
||||
import { ApiCreatedResponse as DocsCreatedResponse, ApiTags } from "@nestjs/swagger";
|
||||
import { User, MembershipRole } from "@prisma/client";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { CreateOAuthClientInput, UpdateOAuthClientInput, Pagination } from "@calcom/platform-types";
|
||||
|
||||
const AUTH_DOCUMENTATION = `⚠️ First, this endpoint requires \`Cookie: next-auth.session-token=eyJhbGciOiJ\` header. Log into Cal web app using owner of organization that was created after visiting \`/settings/organizations/new\`, refresh swagger docs, and the cookie will be added to requests automatically to pass the NextAuthGuard.
|
||||
Second, make sure that the logged in user has organizationId set to pass the OrganizationRolesGuard guard.`;
|
||||
|
||||
@Controller({
|
||||
path: "/v2/oauth-clients",
|
||||
version: API_VERSIONS_VALUES,
|
||||
@@ -61,7 +54,6 @@ export class OAuthClientsController {
|
||||
@Post("/")
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER])
|
||||
@DocsOperation({ description: AUTH_DOCUMENTATION })
|
||||
@DocsCreatedResponse({
|
||||
description: "Create an OAuth client",
|
||||
type: CreateOAuthClientResponseDto,
|
||||
@@ -90,7 +82,6 @@ export class OAuthClientsController {
|
||||
@Get("/")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER, MembershipRole.MEMBER])
|
||||
@DocsOperation({ description: AUTH_DOCUMENTATION })
|
||||
async getOAuthClients(@GetOrgId() organizationId: number): Promise<GetOAuthClientsResponseDto> {
|
||||
const clients = await this.oAuthClientsService.getOAuthClients(organizationId);
|
||||
return { status: SUCCESS_STATUS, data: clients };
|
||||
@@ -99,7 +90,6 @@ export class OAuthClientsController {
|
||||
@Get("/:clientId")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER, MembershipRole.MEMBER])
|
||||
@DocsOperation({ description: AUTH_DOCUMENTATION })
|
||||
@UseGuards(OAuthClientGuard)
|
||||
async getOAuthClientById(@Param("clientId") clientId: string): Promise<GetOAuthClientResponseDto> {
|
||||
const client = await this.oAuthClientsService.getOAuthClientById(clientId);
|
||||
@@ -109,7 +99,6 @@ export class OAuthClientsController {
|
||||
@Get("/:clientId/managed-users")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER, MembershipRole.MEMBER])
|
||||
@DocsOperation({ description: AUTH_DOCUMENTATION })
|
||||
@UseGuards(OAuthClientGuard)
|
||||
async getOAuthClientManagedUsersById(
|
||||
@Param("clientId") clientId: string,
|
||||
@@ -128,7 +117,6 @@ export class OAuthClientsController {
|
||||
@Patch("/:clientId")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER])
|
||||
@DocsOperation({ description: AUTH_DOCUMENTATION })
|
||||
@UseGuards(OAuthClientGuard)
|
||||
async updateOAuthClient(
|
||||
@Param("clientId") clientId: string,
|
||||
@@ -142,7 +130,6 @@ export class OAuthClientsController {
|
||||
@Delete("/:clientId")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@MembershipRoles([MembershipRole.ADMIN, MembershipRole.OWNER])
|
||||
@DocsOperation({ description: AUTH_DOCUMENTATION })
|
||||
@UseGuards(OAuthClientGuard)
|
||||
async deleteOAuthClient(@Param("clientId") clientId: string): Promise<GetOAuthClientResponseDto> {
|
||||
this.logger.log(`Deleting OAuth Client with ID: ${clientId}`);
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { CalendarsRepository } from "@/ee/calendars/calendars.repository";
|
||||
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||
import { EventTypesModule_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/event-types.module";
|
||||
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||
import { AppsRepository } from "@/modules/apps/apps.repository";
|
||||
import { AuthModule } from "@/modules/auth/auth.module";
|
||||
import { BillingModule } from "@/modules/billing/billing.module";
|
||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
||||
import { OAuthClientUsersController } from "@/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller";
|
||||
import { OAuthClientsController } from "@/modules/oauth-clients/controllers/oauth-clients/oauth-clients.controller";
|
||||
@@ -16,6 +20,7 @@ import { OrganizationsModule } from "@/modules/organizations/organizations.modul
|
||||
import { OrganizationsTeamsService } from "@/modules/organizations/teams/index/services/organizations-teams.service";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { RedisModule } from "@/modules/redis/redis.module";
|
||||
import { SelectedCalendarsRepository } from "@/modules/selected-calendars/selected-calendars.repository";
|
||||
import { StripeModule } from "@/modules/stripe/stripe.module";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
@@ -42,6 +47,11 @@ import { JwtService } from "@nestjs/jwt";
|
||||
OAuthClientRepository,
|
||||
TokensRepository,
|
||||
OAuthFlowService,
|
||||
CalendarsService,
|
||||
CredentialsRepository,
|
||||
AppsRepository,
|
||||
CalendarsRepository,
|
||||
SelectedCalendarsRepository,
|
||||
OAuthClientUsersService,
|
||||
OrganizationsTeamsService,
|
||||
OAuthClientsService,
|
||||
|
||||
@@ -118,4 +118,19 @@ export class OAuthClientRepository {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getByEventTypeHosts(eventTypeId: number) {
|
||||
const hostWithUserPlatformClient = await this.dbRead.prisma.host.findFirst({
|
||||
select: {
|
||||
user: { select: { platformOAuthClients: true } },
|
||||
},
|
||||
where: {
|
||||
eventTypeId: eventTypeId,
|
||||
user: {
|
||||
isPlatformManaged: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
return hostWithUserPlatformClient?.user?.platformOAuthClients?.[0] ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { CalendarsService } from "@/ee/calendars/services/calendars.service";
|
||||
import { EventTypesService_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/services/event-types.service";
|
||||
import { SchedulesService_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/services/schedules.service";
|
||||
import { GetManagedUsersInput } from "@/modules/oauth-clients/controllers/oauth-client-users/inputs/get-managed-users.input";
|
||||
@@ -5,18 +6,21 @@ import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
import { CreateManagedUserInput } from "@/modules/users/inputs/create-managed-user.input";
|
||||
import { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-user.input";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { BadRequestException, ConflictException, Injectable } from "@nestjs/common";
|
||||
import { BadRequestException, ConflictException, Injectable, Logger } from "@nestjs/common";
|
||||
import { User, CreationSource, PlatformOAuthClient } from "@prisma/client";
|
||||
|
||||
import { createNewUsersConnectToOrgIfExists, slugify } from "@calcom/platform-libraries";
|
||||
|
||||
@Injectable()
|
||||
export class OAuthClientUsersService {
|
||||
private readonly logger = new Logger("OAuthClientUsersService");
|
||||
|
||||
constructor(
|
||||
private readonly userRepository: UsersRepository,
|
||||
private readonly tokensRepository: TokensRepository,
|
||||
private readonly eventTypesService: EventTypesService_2024_04_15,
|
||||
private readonly schedulesService: SchedulesService_2024_04_15
|
||||
private readonly schedulesService: SchedulesService_2024_04_15,
|
||||
private readonly calendarsService: CalendarsService
|
||||
) {}
|
||||
|
||||
async createOAuthClientUser(oAuthClient: PlatformOAuthClient, body: CreateManagedUserInput) {
|
||||
@@ -87,6 +91,13 @@ export class OAuthClientUsersService {
|
||||
user.defaultScheduleId = defaultSchedule.id;
|
||||
}
|
||||
|
||||
try {
|
||||
this.logger.log(`Setting default calendars in db for user with id ${user.id}`);
|
||||
await this.calendarsService.getCalendars(user.id);
|
||||
} catch (err) {
|
||||
this.logger.error(`Could not get calendars of new managed user with id ${user.id}`);
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
tokens: {
|
||||
@@ -123,7 +134,9 @@ export class OAuthClientUsersService {
|
||||
if (body.email) {
|
||||
const emailWithOAuthId = OAuthClientUsersService.getOAuthUserEmail(oAuthClientId, body.email);
|
||||
body.email = emailWithOAuthId;
|
||||
const newUsername = slugify(emailWithOAuthId);
|
||||
const [emailUser, emailDomain] = emailWithOAuthId.split("@");
|
||||
const [domainName, TLD] = emailDomain.split(".");
|
||||
const newUsername = slugify(`${emailUser}-${domainName}-${TLD}`);
|
||||
await this.userRepository.updateUsername(userId, newUsername);
|
||||
}
|
||||
|
||||
|
||||
+3
-1
@@ -30,5 +30,7 @@ export class CreateDelegationCredentialInput {
|
||||
})
|
||||
@Expose()
|
||||
@Type(() => Object)
|
||||
serviceAccountKey!: GoogleServiceAccountKeyInput | MicrosoftServiceAccountKeyInput;
|
||||
serviceAccountKey!: (GoogleServiceAccountKeyInput | MicrosoftServiceAccountKeyInput) & {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
}
|
||||
|
||||
+18
-1
@@ -1,3 +1,5 @@
|
||||
import { CalendarsModule } from "@/ee/calendars/calendars.module";
|
||||
import { CALENDARS_QUEUE, CalendarsProcessor } from "@/ee/calendars/processors/calendars.processor";
|
||||
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
||||
import { OrganizationsDelegationCredentialController } from "@/modules/organizations/delegation-credentials/organizations-delegation-credential.controller";
|
||||
import { OrganizationsDelegationCredentialRepository } from "@/modules/organizations/delegation-credentials/organizations-delegation-credential.repository";
|
||||
@@ -5,16 +7,31 @@ import { OrganizationsRepository } from "@/modules/organizations/index/organizat
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { RedisModule } from "@/modules/redis/redis.module";
|
||||
import { StripeModule } from "@/modules/stripe/stripe.module";
|
||||
import { BullModule } from "@nestjs/bull";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
import { OrganizationsDelegationCredentialService } from "./services/organizations-delegation-credential.service";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, StripeModule, RedisModule, MembershipsModule],
|
||||
imports: [
|
||||
PrismaModule,
|
||||
StripeModule,
|
||||
RedisModule,
|
||||
CalendarsModule,
|
||||
MembershipsModule,
|
||||
BullModule.registerQueue({
|
||||
name: CALENDARS_QUEUE,
|
||||
limiter: {
|
||||
max: 1,
|
||||
duration: 1000,
|
||||
},
|
||||
}),
|
||||
],
|
||||
providers: [
|
||||
OrganizationsDelegationCredentialService,
|
||||
OrganizationsDelegationCredentialRepository,
|
||||
OrganizationsRepository,
|
||||
CalendarsProcessor,
|
||||
],
|
||||
controllers: [OrganizationsDelegationCredentialController],
|
||||
})
|
||||
|
||||
+12
@@ -28,4 +28,16 @@ export class OrganizationsDelegationCredentialRepository {
|
||||
include: { workspacePlatform: true },
|
||||
});
|
||||
}
|
||||
|
||||
async findDelegatedUserProfiles(orgId: number, domain: string) {
|
||||
return this.dbRead.prisma.profile.findMany({
|
||||
select: {
|
||||
userId: true,
|
||||
},
|
||||
where: {
|
||||
organizationId: orgId,
|
||||
user: { email: { endsWith: domain } },
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+56
-10
@@ -1,3 +1,8 @@
|
||||
import {
|
||||
CALENDARS_QUEUE,
|
||||
DEFAULT_CALENDARS_JOB,
|
||||
DefaultCalendarsJobDataType,
|
||||
} from "@/ee/calendars/processors/calendars.processor";
|
||||
import { CreateDelegationCredentialInput } from "@/modules/organizations/delegation-credentials/inputs/create-delegation-credential.input";
|
||||
import {
|
||||
GoogleServiceAccountKeyInput,
|
||||
@@ -5,9 +10,10 @@ import {
|
||||
} from "@/modules/organizations/delegation-credentials/inputs/service-account-key.input";
|
||||
import { UpdateDelegationCredentialInput } from "@/modules/organizations/delegation-credentials/inputs/update-delegation-credential.input";
|
||||
import { OrganizationsDelegationCredentialRepository } from "@/modules/organizations/delegation-credentials/organizations-delegation-credential.repository";
|
||||
import { DelegationCredentialOutput } from "@/modules/organizations/delegation-credentials/outputs/delegation-credential.output";
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { InjectQueue } from "@nestjs/bull";
|
||||
import { Injectable, NotFoundException, Logger } from "@nestjs/common";
|
||||
import { User } from "@prisma/client";
|
||||
import { Queue } from "bull";
|
||||
|
||||
import { encryptServiceAccountKey } from "@calcom/platform-libraries";
|
||||
import {
|
||||
@@ -17,8 +23,11 @@ import {
|
||||
|
||||
@Injectable()
|
||||
export class OrganizationsDelegationCredentialService {
|
||||
private logger = new Logger("OrganizationsDelegationCredentialService");
|
||||
|
||||
constructor(
|
||||
private readonly organizationsDelegationCredentialRepository: OrganizationsDelegationCredentialRepository
|
||||
private readonly organizationsDelegationCredentialRepository: OrganizationsDelegationCredentialRepository,
|
||||
@InjectQueue(CALENDARS_QUEUE) private readonly calendarsQueue: Queue
|
||||
) {}
|
||||
async createDelegationCredential(
|
||||
orgId: number,
|
||||
@@ -38,6 +47,15 @@ export class OrganizationsDelegationCredentialService {
|
||||
delegatedServiceAccountUser: User,
|
||||
body: UpdateDelegationCredentialInput
|
||||
) {
|
||||
const delegationCredential =
|
||||
await this.organizationsDelegationCredentialRepository.findByIdWithWorkspacePlatform(
|
||||
delegationCredentialId
|
||||
);
|
||||
|
||||
if (!delegationCredential) {
|
||||
throw new NotFoundException(`DelegationCredential with id ${delegationCredentialId} not found`);
|
||||
}
|
||||
|
||||
if (body.enabled !== undefined) {
|
||||
await this.updateDelegationCredentialEnabled(
|
||||
orgId,
|
||||
@@ -49,15 +67,43 @@ export class OrganizationsDelegationCredentialService {
|
||||
if (body.serviceAccountKey !== undefined) {
|
||||
await this.updateDelegationCredentialServiceAccountKey(delegationCredentialId, body.serviceAccountKey);
|
||||
}
|
||||
const delegationCredential =
|
||||
await this.organizationsDelegationCredentialRepository.findByIdWithWorkspacePlatform(
|
||||
delegationCredentialId
|
||||
);
|
||||
if (!delegationCredential) {
|
||||
throw new NotFoundException(`DelegationCredential with id ${delegationCredentialId} not found`);
|
||||
|
||||
// once delegation credentials are enabled, slowly set all the destination calendars of delegated users
|
||||
if (body.enabled === true && delegationCredential.enabled === false) {
|
||||
await this.ensureDefaultCalendars(orgId, delegationCredential.domain);
|
||||
}
|
||||
|
||||
return delegationCredential;
|
||||
return { ...delegationCredential, enabled: body?.enabled ?? delegationCredential?.enabled };
|
||||
}
|
||||
|
||||
async ensureDefaultCalendars(orgId: number, domain: string) {
|
||||
try {
|
||||
const delegatedUserProfiles =
|
||||
await this.organizationsDelegationCredentialRepository.findDelegatedUserProfiles(orgId, domain);
|
||||
|
||||
delegatedUserProfiles.forEach(async (profile) => {
|
||||
if (profile.userId) {
|
||||
const job = await this.calendarsQueue.getJob(`${DEFAULT_CALENDARS_JOB}_${profile.userId}`);
|
||||
if (job) {
|
||||
await job.remove();
|
||||
this.logger.log(`Removed default calendar job for user with id: ${profile.userId}`);
|
||||
}
|
||||
this.logger.log(`Adding default calendar job for user with id: ${profile.userId}`);
|
||||
await this.calendarsQueue.add(
|
||||
DEFAULT_CALENDARS_JOB,
|
||||
{
|
||||
userId: profile.userId,
|
||||
} satisfies DefaultCalendarsJobDataType,
|
||||
{ jobId: `${DEFAULT_CALENDARS_JOB}_${profile.userId}`, removeOnComplete: true }
|
||||
);
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
this.logger.error(
|
||||
err,
|
||||
`Could not ensure default calendars for delegated users in org with id:${orgId}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async updateDelegationCredentialEnabled(
|
||||
|
||||
+2
-2
@@ -81,7 +81,7 @@ export class OrganizationsEventTypesController {
|
||||
bodyEventType
|
||||
);
|
||||
|
||||
const eventType = await this.organizationsEventTypesService.createTeamEventType(
|
||||
const eventType = await this.organizationsEventTypesService.createOrganizationTeamEventType(
|
||||
user,
|
||||
teamId,
|
||||
orgId,
|
||||
@@ -212,7 +212,7 @@ export class OrganizationsEventTypesController {
|
||||
bodyEventType
|
||||
);
|
||||
|
||||
const eventType = await this.organizationsEventTypesService.updateTeamEventType(
|
||||
const eventType = await this.organizationsEventTypesService.updateOrganizationTeamEventType(
|
||||
eventTypeId,
|
||||
teamId,
|
||||
transformedBody,
|
||||
|
||||
+245
-8
@@ -324,7 +324,7 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
const data = responseBody.data;
|
||||
expect(data.title).toEqual(body.title);
|
||||
expect(data.hosts.length).toEqual(2);
|
||||
expect(data.schedulingType).toEqual("COLLECTIVE");
|
||||
expect(data.schedulingType).toEqual("collective");
|
||||
evaluateHost(body.hosts[0], data.hosts[0]);
|
||||
evaluateHost(body.hosts[1], data.hosts[1]);
|
||||
expect(data.bookingLimitsCount).toEqual(body.bookingLimitsCount);
|
||||
@@ -457,8 +457,8 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
const data = responseBody.data;
|
||||
expect(data.length).toEqual(2);
|
||||
|
||||
const eventTypeCollective = data.find((eventType) => eventType.schedulingType === "COLLECTIVE");
|
||||
const eventTypeManaged = data.find((eventType) => eventType.schedulingType === "MANAGED");
|
||||
const eventTypeCollective = data.find((eventType) => eventType.schedulingType === "collective");
|
||||
const eventTypeManaged = data.find((eventType) => eventType.schedulingType === "managed");
|
||||
|
||||
expect(eventTypeCollective?.title).toEqual(collectiveEventType.title);
|
||||
expect(eventTypeCollective?.hosts.length).toEqual(2);
|
||||
@@ -564,7 +564,7 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
).toEqual(newTitle);
|
||||
|
||||
const responseTeamEvent = responseBody.data.find(
|
||||
(eventType) => eventType.schedulingType === "MANAGED"
|
||||
(eventType) => eventType.schedulingType === "managed"
|
||||
);
|
||||
expect(responseTeamEvent).toBeDefined();
|
||||
expect(responseTeamEvent?.title).toEqual(newTitle);
|
||||
@@ -581,6 +581,123 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should be able to create phone-only event type", async () => {
|
||||
const body: CreateTeamEventTypeInput_2024_06_14 = {
|
||||
title: "Phone coding consultation",
|
||||
slug: "phone-coding-consultation",
|
||||
description: "Our team will review your codebase.",
|
||||
lengthInMinutes: 60,
|
||||
locations: [
|
||||
{
|
||||
type: "integration",
|
||||
integration: "cal-video",
|
||||
},
|
||||
{
|
||||
type: "organizersDefaultApp",
|
||||
},
|
||||
],
|
||||
schedulingType: "COLLECTIVE",
|
||||
hosts: [
|
||||
{
|
||||
userId: teammate1.id,
|
||||
mandatory: true,
|
||||
priority: "high",
|
||||
},
|
||||
],
|
||||
bookingFields: [
|
||||
{
|
||||
type: "email",
|
||||
required: false,
|
||||
label: "Email",
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
type: "phone",
|
||||
slug: "attendeePhoneNumber",
|
||||
required: true,
|
||||
label: "Phone number",
|
||||
hidden: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${org.id}/teams/${team.id}/event-types`)
|
||||
.send(body)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: ApiSuccessResponse<TeamEventTypeOutput_2024_06_14> = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
const data = responseBody.data;
|
||||
expect(data.bookingFields).toEqual([
|
||||
{
|
||||
isDefault: true,
|
||||
type: "name",
|
||||
slug: "name",
|
||||
required: true,
|
||||
disableOnPrefill: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "email",
|
||||
slug: "email",
|
||||
required: false,
|
||||
label: "Email",
|
||||
disableOnPrefill: false,
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "radioInput",
|
||||
slug: "location",
|
||||
required: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "phone",
|
||||
slug: "attendeePhoneNumber",
|
||||
required: true,
|
||||
hidden: false,
|
||||
label: "Phone number",
|
||||
disableOnPrefill: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "text",
|
||||
slug: "title",
|
||||
required: true,
|
||||
disableOnPrefill: false,
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "textarea",
|
||||
slug: "notes",
|
||||
required: false,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "multiemail",
|
||||
slug: "guests",
|
||||
required: false,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
type: "textarea",
|
||||
slug: "rescheduleReason",
|
||||
required: false,
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
it("should be able to configure phone-only event type", async () => {
|
||||
const body: UpdateTeamEventTypeInput_2024_06_14 = {
|
||||
bookingFields: [
|
||||
@@ -588,13 +705,14 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
type: "email",
|
||||
required: false,
|
||||
label: "Email",
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
type: "phone",
|
||||
slug: "attendeePhoneNumber",
|
||||
required: true,
|
||||
label: "Phone number",
|
||||
hidden: true,
|
||||
hidden: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -622,6 +740,7 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
required: false,
|
||||
label: "Email",
|
||||
disableOnPrefill: false,
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
isDefault: true,
|
||||
@@ -635,7 +754,7 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
type: "phone",
|
||||
slug: "attendeePhoneNumber",
|
||||
required: true,
|
||||
hidden: true,
|
||||
hidden: false,
|
||||
label: "Phone number",
|
||||
disableOnPrefill: false,
|
||||
},
|
||||
@@ -704,7 +823,7 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
expect(managedTeamEventTypes[0].assignAllTeamMembers).toEqual(true);
|
||||
|
||||
const responseTeamEvent = responseBody.data.find(
|
||||
(eventType) => eventType.schedulingType === "MANAGED"
|
||||
(eventType) => eventType.schedulingType === "managed"
|
||||
);
|
||||
expect(responseTeamEvent).toBeDefined();
|
||||
expect(responseTeamEvent?.teamId).toEqual(team.id);
|
||||
@@ -776,7 +895,14 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
|
||||
expect(fetchedEventType.bookingFields).toEqual([
|
||||
{ isDefault: true, required: true, slug: "name", type: "name", disableOnPrefill: false },
|
||||
{ isDefault: true, required: true, slug: "email", type: "email", disableOnPrefill: false },
|
||||
{
|
||||
isDefault: true,
|
||||
required: true,
|
||||
slug: "email",
|
||||
type: "email",
|
||||
disableOnPrefill: false,
|
||||
hidden: false,
|
||||
},
|
||||
{
|
||||
disableOnPrefill: false,
|
||||
isDefault: true,
|
||||
@@ -828,6 +954,117 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should create a round robin team event-type", async () => {
|
||||
const body: CreateTeamEventTypeInput_2024_06_14 = {
|
||||
successRedirectUrl: "https://masterchief.com/argentina/flan/video/1234",
|
||||
title: "Coding consultation round robin",
|
||||
slug: `organizations-event-types-round-robin-${randomString()}`,
|
||||
description: "Our team will review your codebase.",
|
||||
lengthInMinutes: 60,
|
||||
locations: [
|
||||
{
|
||||
type: "integration",
|
||||
integration: "cal-video",
|
||||
},
|
||||
],
|
||||
bookingFields: [
|
||||
{
|
||||
type: "select",
|
||||
label: "select which language is your codebase in",
|
||||
slug: "select-language",
|
||||
required: true,
|
||||
placeholder: "select language",
|
||||
options: ["javascript", "python", "cobol"],
|
||||
},
|
||||
],
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
schedulingType: "roundRobin",
|
||||
hosts: [
|
||||
{
|
||||
userId: teammate1.id,
|
||||
mandatory: true,
|
||||
priority: "high",
|
||||
},
|
||||
{
|
||||
userId: teammate2.id,
|
||||
mandatory: false,
|
||||
priority: "medium",
|
||||
},
|
||||
],
|
||||
bookingLimitsCount: {
|
||||
day: 2,
|
||||
week: 5,
|
||||
},
|
||||
onlyShowFirstAvailableSlot: true,
|
||||
bookingLimitsDuration: {
|
||||
day: 60,
|
||||
week: 100,
|
||||
},
|
||||
offsetStart: 30,
|
||||
bookingWindow: {
|
||||
type: BookingWindowPeriodInputTypeEnum_2024_06_14.calendarDays,
|
||||
value: 30,
|
||||
rolling: true,
|
||||
},
|
||||
bookerLayouts: {
|
||||
enabledLayouts: [
|
||||
BookerLayoutsInputEnum_2024_06_14.column,
|
||||
BookerLayoutsInputEnum_2024_06_14.month,
|
||||
BookerLayoutsInputEnum_2024_06_14.week,
|
||||
],
|
||||
defaultLayout: BookerLayoutsInputEnum_2024_06_14.month,
|
||||
},
|
||||
|
||||
confirmationPolicy: {
|
||||
type: ConfirmationPolicyEnum.TIME,
|
||||
noticeThreshold: {
|
||||
count: 60,
|
||||
unit: NoticeThresholdUnitEnum.MINUTES,
|
||||
},
|
||||
blockUnconfirmedBookingsInBooker: true,
|
||||
},
|
||||
requiresBookerEmailVerification: true,
|
||||
hideCalendarNotes: true,
|
||||
hideCalendarEventDetails: true,
|
||||
lockTimeZoneToggleOnBookingPage: true,
|
||||
color: {
|
||||
darkThemeHex: "#292929",
|
||||
lightThemeHex: "#fafafa",
|
||||
},
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${org.id}/teams/${team.id}/event-types`)
|
||||
.send(body)
|
||||
.expect(201)
|
||||
.then((response) => {
|
||||
const responseBody: ApiSuccessResponse<TeamEventTypeOutput_2024_06_14> = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
|
||||
const data = responseBody.data;
|
||||
expect(data.title).toEqual(body.title);
|
||||
expect(data.hosts.length).toEqual(2);
|
||||
expect(data.schedulingType).toEqual("roundRobin");
|
||||
evaluateHost(body.hosts[0], data.hosts[0]);
|
||||
evaluateHost(body.hosts[1], data.hosts[1]);
|
||||
expect(data.bookingLimitsCount).toEqual(body.bookingLimitsCount);
|
||||
expect(data.onlyShowFirstAvailableSlot).toEqual(body.onlyShowFirstAvailableSlot);
|
||||
expect(data.bookingLimitsDuration).toEqual(body.bookingLimitsDuration);
|
||||
expect(data.offsetStart).toEqual(body.offsetStart);
|
||||
expect(data.bookingWindow).toEqual(body.bookingWindow);
|
||||
expect(data.bookerLayouts).toEqual(body.bookerLayouts);
|
||||
expect(data.confirmationPolicy).toEqual(body.confirmationPolicy);
|
||||
expect(data.requiresBookerEmailVerification).toEqual(body.requiresBookerEmailVerification);
|
||||
expect(data.hideCalendarNotes).toEqual(body.hideCalendarNotes);
|
||||
expect(data.hideCalendarEventDetails).toEqual(body.hideCalendarEventDetails);
|
||||
expect(data.lockTimeZoneToggleOnBookingPage).toEqual(body.lockTimeZoneToggleOnBookingPage);
|
||||
expect(data.color).toEqual(body.color);
|
||||
expect(data.successRedirectUrl).toEqual("https://masterchief.com/argentina/flan/video/1234");
|
||||
collectiveEventType = responseBody.data;
|
||||
});
|
||||
});
|
||||
|
||||
function evaluateHost(expected: Host, received: Host | undefined) {
|
||||
expect(expected.userId).toEqual(received?.userId);
|
||||
expect(expected.mandatory).toEqual(received?.mandatory);
|
||||
|
||||
+4
-4
@@ -26,7 +26,7 @@ export class OrganizationsEventTypesService {
|
||||
private readonly usersService: UsersService
|
||||
) {}
|
||||
|
||||
async createTeamEventType(
|
||||
async createOrganizationTeamEventType(
|
||||
user: UserWithProfile,
|
||||
teamId: number,
|
||||
orgId: number,
|
||||
@@ -48,7 +48,7 @@ export class OrganizationsEventTypesService {
|
||||
},
|
||||
});
|
||||
|
||||
return this.teamsEventTypesService.updateTeamEventType(eventTypeCreated.id, teamId, body, user);
|
||||
return this.teamsEventTypesService.updateTeamEventType(eventTypeCreated.id, teamId, body, user, true);
|
||||
}
|
||||
|
||||
async getUserToCreateTeamEvent(user: UserWithProfile, organizationId: number) {
|
||||
@@ -91,13 +91,13 @@ export class OrganizationsEventTypesService {
|
||||
return await this.organizationEventTypesRepository.getOrganizationTeamsEventTypes(orgId, skip, take);
|
||||
}
|
||||
|
||||
async updateTeamEventType(
|
||||
async updateOrganizationTeamEventType(
|
||||
eventTypeId: number,
|
||||
teamId: number,
|
||||
body: TransformedUpdateTeamEventTypeInput,
|
||||
user: UserWithProfile
|
||||
): Promise<DatabaseTeamEventType | DatabaseTeamEventType[]> {
|
||||
return this.teamsEventTypesService.updateTeamEventType(eventTypeId, teamId, body, user);
|
||||
return this.teamsEventTypesService.updateTeamEventType(eventTypeId, teamId, body, user, true);
|
||||
}
|
||||
|
||||
async deleteTeamEventType(teamId: number, eventTypeId: number) {
|
||||
|
||||
@@ -100,7 +100,9 @@ export class OutputOrganizationsEventTypesService {
|
||||
teamId,
|
||||
ownerId: userId,
|
||||
parentEventTypeId: parentId,
|
||||
schedulingType: databaseEventType.schedulingType,
|
||||
schedulingType: databaseEventType.schedulingType
|
||||
? this.getResponseSchedulingType(databaseEventType.schedulingType)
|
||||
: databaseEventType.schedulingType,
|
||||
assignAllTeamMembers: teamId ? assignAllTeamMembers : undefined,
|
||||
team: {
|
||||
id: teamId,
|
||||
@@ -116,6 +118,19 @@ export class OutputOrganizationsEventTypesService {
|
||||
};
|
||||
}
|
||||
|
||||
getResponseSchedulingType(schedulingType: SchedulingType) {
|
||||
if (schedulingType === SchedulingType.COLLECTIVE) {
|
||||
return "collective";
|
||||
}
|
||||
if (schedulingType === SchedulingType.ROUND_ROBIN) {
|
||||
return "roundRobin";
|
||||
}
|
||||
if (schedulingType === SchedulingType.MANAGED) {
|
||||
return "managed";
|
||||
}
|
||||
return schedulingType;
|
||||
}
|
||||
|
||||
async getManagedEventTypeHosts(eventTypeId: number) {
|
||||
const children = await this.teamsEventTypesRepository.getEventTypeChildren(eventTypeId);
|
||||
const transformedHosts: TeamEventTypeResponseHost[] = [];
|
||||
@@ -144,7 +159,7 @@ export class OutputOrganizationsEventTypesService {
|
||||
transformedHosts.push({
|
||||
userId: databaseHost.userId,
|
||||
name: databaseUser?.name || "",
|
||||
mandatory: databaseHost.isFixed,
|
||||
mandatory: !!databaseHost.isFixed,
|
||||
priority: getPriorityLabel(databaseHost.priority || 2),
|
||||
avatarUrl: databaseUser?.avatarUrl,
|
||||
});
|
||||
|
||||
+4
-3
@@ -37,11 +37,12 @@ export class OrganizationsMembershipRepository {
|
||||
}
|
||||
|
||||
async createOrgMembership(organizationId: number, data: CreateOrgMembershipDto) {
|
||||
return this.dbWrite.prisma.membership.create({
|
||||
data: { ...data, teamId: organizationId },
|
||||
return this.dbWrite.prisma.membership.upsert({
|
||||
create: { ...data, teamId: organizationId },
|
||||
update: { role: data.role, accepted: data.accepted, disableImpersonation: data.disableImpersonation },
|
||||
where: { userId_teamId: { userId: data.userId, teamId: organizationId } },
|
||||
});
|
||||
}
|
||||
|
||||
async updateOrgMembership(organizationId: number, membershipId: number, data: UpdateOrgMembershipDto) {
|
||||
return this.dbWrite.prisma.membership.update({
|
||||
data: { ...data },
|
||||
|
||||
+1
-1
@@ -265,7 +265,7 @@ describe("Organizations TeamsBookings Endpoints 2024-08-13", () => {
|
||||
});
|
||||
|
||||
describe("get team bookings", () => {
|
||||
it("should should get bookings by teamId", async () => {
|
||||
it("should get bookings by teamId", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/organizations/${organization.id}/teams/${team1.id}/bookings`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
|
||||
+98
@@ -75,6 +75,9 @@ describe("Slots 2024-09-04 Endpoints", () => {
|
||||
const seatedEventTypeSlug = `slots-2024-09-04-seated-event-type-${randomString()}`;
|
||||
let seatedEventType: EventType;
|
||||
|
||||
const variableLengthEventTypeSlug = `slots-2024-09-04-variable-length-event-type-${randomString()}`;
|
||||
let variableLengthEventType: EventType;
|
||||
|
||||
let reservedSlot: ReserveSlotOutputData_2024_09_04;
|
||||
|
||||
beforeAll(async () => {
|
||||
@@ -168,6 +171,17 @@ describe("Slots 2024-09-04 Endpoints", () => {
|
||||
);
|
||||
seatedEventType = seatedEvent;
|
||||
|
||||
const variableLengthEvent = await eventTypesRepositoryFixture.create(
|
||||
{
|
||||
title: "frisbee match",
|
||||
slug: `slots-2024-09-04-variable-length-event-type-${randomString()}`,
|
||||
length: 15,
|
||||
metadata: { multipleDuration: [15, 30, 45, 60] },
|
||||
},
|
||||
user.id
|
||||
);
|
||||
variableLengthEventType = variableLengthEvent;
|
||||
|
||||
team = await teamRepositoryFixture.create({
|
||||
name: `slots-2024-09-04-team-${randomString()}`,
|
||||
isOrganization: false,
|
||||
@@ -1246,6 +1260,90 @@ describe("Slots 2024-09-04 Endpoints", () => {
|
||||
await bookingsRepositoryFixture.deleteById(booking.id);
|
||||
});
|
||||
|
||||
describe("variable length", () => {
|
||||
it("should not be able to reserve a slot for variable length event type with invalid duration", async () => {
|
||||
const slotStartTime = "2050-09-05T10:00:00.000Z";
|
||||
const reserveResponse = await request(app.getHttpServer())
|
||||
.post(`/v2/slots/reservations`)
|
||||
.send({
|
||||
eventTypeId: variableLengthEventType.id,
|
||||
slotStart: slotStartTime,
|
||||
slotDuration: 1000,
|
||||
})
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_09_04)
|
||||
.expect(400);
|
||||
|
||||
expect(reserveResponse.body.error.message).toEqual(
|
||||
"Provided 'slotDuration' is not one of the possible lengths for the event type. The possible lengths for this variable length event type are: 15, 30, 45, 60"
|
||||
);
|
||||
});
|
||||
|
||||
it("should reserve a slot with slot duration for variable event type length", async () => {
|
||||
// note(Lauris): mock current date to test slots release time
|
||||
const now = "2049-09-05T12:00:00.000Z";
|
||||
const newDate = DateTime.fromISO(now, { zone: "UTC" }).toJSDate();
|
||||
advanceTo(newDate);
|
||||
|
||||
const slotDuration = 60;
|
||||
const slotStartTime = "2050-09-05T10:00:00.000Z";
|
||||
const reserveResponse = await request(app.getHttpServer())
|
||||
.post(`/v2/slots/reservations`)
|
||||
.send({
|
||||
eventTypeId: variableLengthEventType.id,
|
||||
slotStart: slotStartTime,
|
||||
slotDuration,
|
||||
})
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_09_04)
|
||||
.expect(201);
|
||||
|
||||
const reserveResponseBody: ReserveSlotOutputResponse_2024_09_04 = reserveResponse.body;
|
||||
expect(reserveResponseBody.status).toEqual(SUCCESS_STATUS);
|
||||
const responseReservedSlot: ReserveSlotOutputData_2024_09_04 = reserveResponseBody.data;
|
||||
expect(responseReservedSlot.reservationUid).toBeDefined();
|
||||
expect(responseReservedSlot.eventTypeId).toEqual(variableLengthEventType.id);
|
||||
expect(responseReservedSlot.slotStart).toEqual(slotStartTime);
|
||||
expect(responseReservedSlot.slotDuration).toEqual(slotDuration);
|
||||
expect(responseReservedSlot.slotEnd).toEqual(
|
||||
DateTime.fromISO(slotStartTime, { zone: "UTC" }).plus({ minutes: slotDuration }).toISO()
|
||||
);
|
||||
expect(responseReservedSlot.reservationDuration).toEqual(5);
|
||||
|
||||
if (!responseReservedSlot.reservationUid) {
|
||||
throw new Error("Reserved slot uid is undefined");
|
||||
}
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.get(
|
||||
`/v2/slots?eventTypeId=${variableLengthEventType.id}&start=2050-09-05&end=2050-09-09&duration=60`
|
||||
)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_09_04)
|
||||
.expect(200);
|
||||
|
||||
const responseBody: GetSlotsOutput_2024_09_04 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
const slots = responseBody.data;
|
||||
|
||||
expect(slots).toBeDefined();
|
||||
const days = Object.keys(slots);
|
||||
expect(days.length).toEqual(5);
|
||||
|
||||
const expectedSlotsUTC2050_09_05 = expectedSlotsUTC["2050-09-05"].filter(
|
||||
(slot) => slot.start !== slotStartTime
|
||||
);
|
||||
expect(slots).toEqual({ ...expectedSlotsUTC, "2050-09-05": expectedSlotsUTC2050_09_05 });
|
||||
|
||||
const dbSlot = await selectedSlotsRepositoryFixture.getByUid(reservedSlot.reservationUid);
|
||||
expect(dbSlot).toBeDefined();
|
||||
if (dbSlot) {
|
||||
const dbReleaseAt = DateTime.fromJSDate(dbSlot.releaseAt, { zone: "UTC" }).toISO();
|
||||
const expectedReleaseAt = DateTime.fromISO(now, { zone: "UTC" }).plus({ minutes: 5 }).toISO();
|
||||
expect(dbReleaseAt).toEqual(expectedReleaseAt);
|
||||
expect(responseReservedSlot.reservationUntil).toEqual(expectedReleaseAt);
|
||||
}
|
||||
clear();
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await userRepositoryFixture.deleteByEmail(user.email);
|
||||
await userRepositoryFixture.deleteByEmail(unrelatedUser.email);
|
||||
|
||||
@@ -47,8 +47,12 @@ import { ApiResponse } from "@calcom/platform-types";
|
||||
@DocsTags("Slots")
|
||||
@ApiHeader({
|
||||
name: "cal-api-version",
|
||||
description: `Must be set to \`2024-09-04\``,
|
||||
description: `Must be set to ${VERSION_2024_09_04}`,
|
||||
example: VERSION_2024_09_04,
|
||||
required: true,
|
||||
schema: {
|
||||
default: VERSION_2024_09_04,
|
||||
},
|
||||
})
|
||||
export class SlotsController_2024_09_04 {
|
||||
constructor(private readonly slotsService: SlotsService_2024_09_04) {}
|
||||
|
||||
@@ -21,9 +21,11 @@ import { getAvailableSlots } from "@calcom/platform-libraries/slots";
|
||||
import { GetSlotsInput_2024_09_04, ReserveSlotInput_2024_09_04 } from "@calcom/platform-types";
|
||||
import { EventType } from "@calcom/prisma/client";
|
||||
|
||||
const eventTypeMetadataSchema = z.object({
|
||||
multipleDuration: z.number().array().optional(),
|
||||
});
|
||||
const eventTypeMetadataSchema = z
|
||||
.object({
|
||||
multipleDuration: z.number().array().optional(),
|
||||
})
|
||||
.nullable();
|
||||
|
||||
const DEFAULT_RESERVATION_DURATION = 5;
|
||||
|
||||
@@ -89,17 +91,8 @@ export class SlotsService_2024_09_04 {
|
||||
throw new BadRequestException("Invalid start date");
|
||||
}
|
||||
|
||||
const metadata = eventTypeMetadataSchema.parse(eventType);
|
||||
if (
|
||||
input.slotDuration &&
|
||||
metadata.multipleDuration &&
|
||||
!metadata.multipleDuration.includes(input.slotDuration)
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
`Provided 'slotDuration' is not one of the possible lengths for the event type. The possible lengths for this variable length event type are: ${metadata.multipleDuration.join(
|
||||
", "
|
||||
)}`
|
||||
);
|
||||
if (input.slotDuration) {
|
||||
this.validateSlotDuration(eventType, input.slotDuration);
|
||||
}
|
||||
|
||||
const endDate = startDate.plus({ minutes: input.slotDuration ?? eventType.length });
|
||||
@@ -160,6 +153,23 @@ export class SlotsService_2024_09_04 {
|
||||
return this.slotsOutputService.getReservationSlotCreated(slot, reservationDuration);
|
||||
}
|
||||
|
||||
validateSlotDuration(eventType: EventType, inputSlotDuration: number) {
|
||||
const eventTypeMetadata = eventTypeMetadataSchema.parse(eventType.metadata);
|
||||
if (!eventTypeMetadata?.multipleDuration) {
|
||||
throw new BadRequestException(
|
||||
"You passed 'slotDuration' but this event type is not a variable length event type."
|
||||
);
|
||||
}
|
||||
|
||||
if (!eventTypeMetadata.multipleDuration.includes(inputSlotDuration)) {
|
||||
throw new BadRequestException(
|
||||
`Provided 'slotDuration' is not one of the possible lengths for the event type. The possible lengths for this variable length event type are: ${eventTypeMetadata.multipleDuration.join(
|
||||
", "
|
||||
)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async canSpecifyCustomReservationDuration(authUserId: number, eventType: EventType) {
|
||||
if (eventType.userId) {
|
||||
return await this.canSpecifyCustomReservationDurationIndividualEvent(authUserId, eventType.userId);
|
||||
@@ -214,17 +224,8 @@ export class SlotsService_2024_09_04 {
|
||||
throw new BadRequestException("Invalid start date");
|
||||
}
|
||||
|
||||
const metadata = eventTypeMetadataSchema.parse(eventType);
|
||||
if (
|
||||
input.slotDuration &&
|
||||
metadata.multipleDuration &&
|
||||
!metadata.multipleDuration.includes(input.slotDuration)
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
`Provided 'slotDuration' is not one of the possible lengths for the event type. The possible lengths for this variable length event type are: ${metadata.multipleDuration.join(
|
||||
", "
|
||||
)}`
|
||||
);
|
||||
if (input.slotDuration) {
|
||||
this.validateSlotDuration(eventType, input.slotDuration);
|
||||
}
|
||||
|
||||
const endDate = startDate.plus({ minutes: input.slotDuration ?? eventType.length });
|
||||
|
||||
+55
-5
@@ -167,6 +167,56 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
return request(app.getHttpServer()).post(`/v2/teams/${team.id}/event-types`).send(body).expect(404);
|
||||
});
|
||||
|
||||
it("should not be able to create phone-only event type", async () => {
|
||||
const body: CreateTeamEventTypeInput_2024_06_14 = {
|
||||
title: "Phone coding consultation",
|
||||
slug: "phone-coding-consultation",
|
||||
description: "Our team will review your codebase.",
|
||||
lengthInMinutes: 60,
|
||||
locations: [
|
||||
{
|
||||
type: "integration",
|
||||
integration: "cal-video",
|
||||
},
|
||||
{
|
||||
type: "organizersDefaultApp",
|
||||
},
|
||||
],
|
||||
schedulingType: "COLLECTIVE",
|
||||
hosts: [
|
||||
{
|
||||
userId: teamMember1.id,
|
||||
},
|
||||
{
|
||||
userId: teamMember2.id,
|
||||
},
|
||||
],
|
||||
bookingFields: [
|
||||
{
|
||||
type: "email",
|
||||
required: false,
|
||||
label: "Email",
|
||||
hidden: true,
|
||||
},
|
||||
{
|
||||
type: "phone",
|
||||
slug: "attendeePhoneNumber",
|
||||
required: true,
|
||||
label: "Phone number",
|
||||
hidden: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post(`/v2/teams/${team.id}/event-types`)
|
||||
.send(body)
|
||||
.expect(400);
|
||||
expect(response.body.error.message).toBe(
|
||||
"checkIsEmailUserAccessible - Email booking field must be required and visible"
|
||||
);
|
||||
});
|
||||
|
||||
it("should create a collective team event-type", async () => {
|
||||
const body: CreateTeamEventTypeInput_2024_06_14 = {
|
||||
title: `teams-event-types-collective-${randomString()}`,
|
||||
@@ -253,7 +303,7 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
const data = responseBody.data;
|
||||
expect(data.title).toEqual(body.title);
|
||||
expect(data.hosts.length).toEqual(2);
|
||||
expect(data.schedulingType).toEqual("COLLECTIVE");
|
||||
expect(data.schedulingType).toEqual("collective");
|
||||
evaluateHost(body.hosts[0], data.hosts[0]);
|
||||
evaluateHost(body.hosts[1], data.hosts[1]);
|
||||
expect(data.bookingLimitsCount).toEqual(body.bookingLimitsCount);
|
||||
@@ -373,8 +423,8 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
const data = responseBody.data;
|
||||
expect(data.length).toEqual(2);
|
||||
|
||||
const eventTypeCollective = data.find((eventType) => eventType.schedulingType === "COLLECTIVE");
|
||||
const eventTypeManaged = data.find((eventType) => eventType.schedulingType === "MANAGED");
|
||||
const eventTypeCollective = data.find((eventType) => eventType.schedulingType === "collective");
|
||||
const eventTypeManaged = data.find((eventType) => eventType.schedulingType === "managed");
|
||||
|
||||
expect(eventTypeCollective?.title).toEqual(collectiveEventType.title);
|
||||
expect(eventTypeCollective?.hosts.length).toEqual(2);
|
||||
@@ -466,7 +516,7 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
).toEqual(newTitle);
|
||||
|
||||
const responseTeamEvent = responseBody.data.find(
|
||||
(eventType) => eventType.schedulingType === "MANAGED"
|
||||
(eventType) => eventType.schedulingType === "managed"
|
||||
);
|
||||
expect(responseTeamEvent).toBeDefined();
|
||||
expect(responseTeamEvent?.title).toEqual(newTitle);
|
||||
@@ -512,7 +562,7 @@ describe("Organizations Event Types Endpoints", () => {
|
||||
expect(managedTeamEventTypes[0].assignAllTeamMembers).toEqual(true);
|
||||
|
||||
const responseTeamEvent = responseBody.data.find(
|
||||
(eventType) => eventType.schedulingType === "MANAGED"
|
||||
(eventType) => eventType.schedulingType === "managed"
|
||||
);
|
||||
expect(responseTeamEvent).toBeDefined();
|
||||
expect(responseTeamEvent?.teamId).toEqual(team.id);
|
||||
|
||||
+2
-1
@@ -181,7 +181,8 @@ export class TeamsEventTypesController {
|
||||
eventTypeId,
|
||||
teamId,
|
||||
transformedBody,
|
||||
user
|
||||
user,
|
||||
false
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
@@ -30,6 +30,10 @@ export class TeamsEventTypesService {
|
||||
teamId: number,
|
||||
body: TransformedCreateTeamEventTypeInput
|
||||
): Promise<DatabaseTeamEventType | DatabaseTeamEventType[]> {
|
||||
// note(Lauris): once phone only event types / bookings are enabled for simple users remove checkHasUserAccessibleEmailBookingField check
|
||||
if (body.bookingFields) {
|
||||
this.eventTypesService.checkHasUserAccessibleEmailBookingField(body.bookingFields);
|
||||
}
|
||||
const eventTypeUser = await this.getUserToCreateTeamEvent(user);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { hosts, children, destinationCalendar, ...rest } = body;
|
||||
@@ -44,7 +48,7 @@ export class TeamsEventTypesService {
|
||||
},
|
||||
});
|
||||
|
||||
return this.updateTeamEventType(eventTypeCreated.id, teamId, body, user);
|
||||
return this.updateTeamEventType(eventTypeCreated.id, teamId, body, user, false);
|
||||
}
|
||||
|
||||
async validateEventTypeExists(teamId: number, eventTypeId: number) {
|
||||
@@ -105,8 +109,14 @@ export class TeamsEventTypesService {
|
||||
eventTypeId: number,
|
||||
teamId: number,
|
||||
body: TransformedUpdateTeamEventTypeInput,
|
||||
user: UserWithProfile
|
||||
user: UserWithProfile,
|
||||
// note(Lauris): once phone only event types / bookings are enabled for simple users remove isOrg parameter (right now only organization team event types support hidden / non-required email field)
|
||||
isOrg: boolean
|
||||
): Promise<DatabaseTeamEventType | DatabaseTeamEventType[]> {
|
||||
if (!isOrg && body.bookingFields) {
|
||||
// note(Lauris): once phone only event types / bookings are enabled for simple users remove checkHasUserAccessibleEmailBookingField check
|
||||
this.eventTypesService.checkHasUserAccessibleEmailBookingField(body.bookingFields);
|
||||
}
|
||||
await this.validateEventTypeExists(teamId, eventTypeId);
|
||||
const eventTypeUser = await this.eventTypesService.getUserToUpdateEvent(user);
|
||||
await updateEventType({
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -22,7 +22,8 @@
|
||||
"@calcom/platform-libraries/event-types": ["../../../packages/platform/libraries/event-types.ts"],
|
||||
"@calcom/platform-libraries/slots": ["../../../packages/platform/libraries/slots.ts"],
|
||||
"@calcom/platform-libraries/emails": ["../../../packages/platform/libraries/emails.ts"],
|
||||
"@calcom/platform-libraries/schedules": ["../../../packages/platform/libraries/schedules.ts"]
|
||||
"@calcom/platform-libraries/schedules": ["../../../packages/platform/libraries/schedules.ts"],
|
||||
"@calcom/platform-libraries/app-store": ["../../../packages/platform/libraries/app-store.ts"]
|
||||
},
|
||||
"incremental": true,
|
||||
"skipLibCheck": true,
|
||||
|
||||
@@ -11,7 +11,7 @@ import TypePage, { type PageProps as ClientPageProps } from "~/users/views/users
|
||||
const getData = withEmbedSsrAppDir<ClientPageProps>(getServerSideProps);
|
||||
|
||||
const ServerPage = async ({ params, searchParams }: ServerPageProps) => {
|
||||
const context = buildLegacyCtx(headers(), cookies(), params, searchParams);
|
||||
const context = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
|
||||
const props = await getData(context);
|
||||
return <TypePage {...props} />;
|
||||
};
|
||||
|
||||
@@ -13,7 +13,7 @@ import type { PageProps as LegacyPageProps } from "~/users/views/users-type-publ
|
||||
import LegacyPage from "~/users/views/users-type-public-view";
|
||||
|
||||
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
|
||||
const legacyCtx = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
|
||||
const props = await getData(legacyCtx);
|
||||
|
||||
const { booking, isSEOIndexable = true, eventData, isBrandingHidden } = props;
|
||||
@@ -31,7 +31,7 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||
})),
|
||||
],
|
||||
};
|
||||
const decodedParams = decodeParams(params);
|
||||
const decodedParams = decodeParams(await params);
|
||||
const metadata = await generateMeetingMetadata(
|
||||
meeting,
|
||||
(t) => `${rescheduleUid && !!booking ? t("reschedule") : ""} ${title} | ${profileName}`,
|
||||
@@ -52,7 +52,7 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||
const getData = withAppDirSsr<LegacyPageProps>(getServerSideProps);
|
||||
|
||||
const ServerPage = async ({ params, searchParams }: PageProps) => {
|
||||
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
|
||||
const legacyCtx = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
|
||||
const props = await getData(legacyCtx);
|
||||
|
||||
return <LegacyPage {...props} />;
|
||||
|
||||
@@ -11,7 +11,7 @@ import User, { type PageProps as ClientPageProps } from "~/users/views/users-pub
|
||||
const getData = withEmbedSsrAppDir<ClientPageProps>(getServerSideProps);
|
||||
|
||||
const ServerPage = async ({ params, searchParams }: ServerPageProps) => {
|
||||
const context = buildLegacyCtx(headers(), cookies(), params, searchParams);
|
||||
const context = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
|
||||
const props = await getData(context);
|
||||
return <User {...props} />;
|
||||
};
|
||||
|
||||
@@ -13,7 +13,9 @@ import type { PageProps as LegacyPageProps } from "~/users/views/users-public-vi
|
||||
import LegacyPage from "~/users/views/users-public-view";
|
||||
|
||||
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||
const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
|
||||
const props = await getData(
|
||||
buildLegacyCtx(await headers(), await cookies(), await params, await searchParams)
|
||||
);
|
||||
|
||||
const { profile, markdownStrippedBio, isOrgSEOIndexable, entity } = props;
|
||||
const isOrg = !!profile?.organization;
|
||||
@@ -31,7 +33,7 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||
() => markdownStrippedBio,
|
||||
false,
|
||||
getOrgFullOrigin(entity.orgSlug ?? null),
|
||||
`/${decodeParams(params).user}`
|
||||
`/${decodeParams(await params).user}`
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -45,7 +47,9 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||
|
||||
const getData = withAppDirSsr<LegacyPageProps>(getServerSideProps);
|
||||
const ServerPage = async ({ params, searchParams }: PageProps) => {
|
||||
const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
|
||||
const props = await getData(
|
||||
buildLegacyCtx(await headers(), await cookies(), await params, await searchParams)
|
||||
);
|
||||
|
||||
return <LegacyPage {...props} />;
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import { headers } from "next/headers";
|
||||
import PageWrapper from "@components/PageWrapperAppDir";
|
||||
|
||||
export default async function BookingPageWrapperLayout({ children }: { children: React.ReactNode }) {
|
||||
const h = headers();
|
||||
const h = await headers();
|
||||
const nonce = h.get("x-nonce") ?? undefined;
|
||||
|
||||
return (
|
||||
|
||||
@@ -15,7 +15,7 @@ const getData = withEmbedSsrAppDir<ClientPageProps>(getServerSideProps);
|
||||
export type ClientPageProps = UserTypePageProps | TeamTypePageProps;
|
||||
|
||||
const ServerPage = async ({ params, searchParams }: ServerPageProps) => {
|
||||
const context = buildLegacyCtx(headers(), cookies(), params, searchParams);
|
||||
const context = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
|
||||
const props = await getData(context);
|
||||
if ((props as TeamTypePageProps)?.teamId) return <TeamTypePage {...(props as TeamTypePageProps)} />;
|
||||
return <UserTypePage {...(props as UserTypePageProps)} />;
|
||||
|
||||
@@ -17,7 +17,7 @@ export type OrgTypePageProps = UserTypePageProps | TeamTypePageProps;
|
||||
const getData = withAppDirSsr<OrgTypePageProps>(getServerSideProps);
|
||||
|
||||
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
|
||||
const legacyCtx = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
|
||||
const props = await getData(legacyCtx);
|
||||
|
||||
const { booking, isSEOIndexable = true, eventData, isBrandingHidden } = props;
|
||||
@@ -36,7 +36,7 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||
})),
|
||||
],
|
||||
};
|
||||
const decodedParams = decodeParams(params);
|
||||
const decodedParams = decodeParams(await params);
|
||||
const metadata = await generateMeetingMetadata(
|
||||
meeting,
|
||||
(t) => `${rescheduleUid && !!booking ? t("reschedule") : ""} ${title} | ${profileName}`,
|
||||
@@ -56,7 +56,9 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||
};
|
||||
|
||||
const ServerPage = async ({ params, searchParams }: PageProps) => {
|
||||
const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
|
||||
const props = await getData(
|
||||
buildLegacyCtx(await headers(), await cookies(), await params, await searchParams)
|
||||
);
|
||||
if ((props as TeamTypePageProps)?.teamId) {
|
||||
return <TeamTypePage {...(props as TeamTypePageProps)} />;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ const getData = withEmbedSsrAppDir<ClientPageProps>(getServerSideProps);
|
||||
export type ClientPageProps = UserPageProps | TeamPageProps;
|
||||
|
||||
const ServerPage = async ({ params, searchParams }: ServerPageProps) => {
|
||||
const context = buildLegacyCtx(headers(), cookies(), params, searchParams);
|
||||
const context = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
|
||||
const props = await getData(context);
|
||||
if ((props as TeamPageProps)?.team) return <TeamPage {...(props as TeamPageProps)} />;
|
||||
return <UserPage {...(props as UserPageProps)} />;
|
||||
|
||||
@@ -18,7 +18,7 @@ export type OrgPageProps = UserPageProps | TeamPageProps;
|
||||
const getData = withAppDirSsr<OrgPageProps>(getServerSideProps);
|
||||
|
||||
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
|
||||
const legacyCtx = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
|
||||
const props = await getData(legacyCtx);
|
||||
|
||||
if ((props as TeamPageProps)?.team) {
|
||||
@@ -30,7 +30,7 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||
image: getOrgOrTeamAvatar(team),
|
||||
},
|
||||
};
|
||||
const decodedParams = decodeParams(params);
|
||||
const decodedParams = decodeParams(await params);
|
||||
return {
|
||||
...(await generateMeetingMetadata(
|
||||
meeting,
|
||||
@@ -78,7 +78,9 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||
};
|
||||
|
||||
const ServerPage = async ({ params, searchParams }: PageProps) => {
|
||||
const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
|
||||
const props = await getData(
|
||||
buildLegacyCtx(await headers(), await cookies(), await params, await searchParams)
|
||||
);
|
||||
if ((props as TeamPageProps)?.team) {
|
||||
return <TeamPage {...(props as TeamPageProps)} />;
|
||||
}
|
||||
|
||||
+5
-3
@@ -12,7 +12,7 @@ import type { Props } from "~/org/[orgSlug]/instant-meeting/team/[slug]/[type]/i
|
||||
import Page from "~/org/[orgSlug]/instant-meeting/team/[slug]/[type]/instant-meeting-view";
|
||||
|
||||
export const generateMetadata = async ({ params, searchParams }: _PageProps) => {
|
||||
const context = buildLegacyCtx(headers(), cookies(), params, searchParams);
|
||||
const context = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
|
||||
const { isBrandingHidden, eventData } = await getData(context);
|
||||
|
||||
const profileName = eventData?.profile.name ?? "";
|
||||
@@ -29,7 +29,7 @@ export const generateMetadata = async ({ params, searchParams }: _PageProps) =>
|
||||
})),
|
||||
],
|
||||
};
|
||||
const decodedParams = decodeParams(params);
|
||||
const decodedParams = decodeParams(await params);
|
||||
const metadata = await generateMeetingMetadata(
|
||||
meeting,
|
||||
() => `${title} | ${profileName}`,
|
||||
@@ -51,7 +51,9 @@ export const generateMetadata = async ({ params, searchParams }: _PageProps) =>
|
||||
const getData = withAppDirSsr<Props>(getServerSideProps);
|
||||
|
||||
const ServerPage = async ({ params, searchParams }: _PageProps) => {
|
||||
const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
|
||||
const props = await getData(
|
||||
buildLegacyCtx(await headers(), await cookies(), await params, await searchParams)
|
||||
);
|
||||
return <Page {...props} />;
|
||||
};
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import TypePage, { type PageProps as ClientPageProps } from "~/team/type-view";
|
||||
const getData = withEmbedSsrAppDir<ClientPageProps>(getServerSideProps);
|
||||
|
||||
const ServerPage = async ({ params, searchParams }: ServerPageProps) => {
|
||||
const context = buildLegacyCtx(headers(), cookies(), params, searchParams);
|
||||
const context = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
|
||||
const props = await getData(context);
|
||||
return <TypePage {...props} />;
|
||||
};
|
||||
|
||||
@@ -12,7 +12,7 @@ import LegacyPage from "~/team/type-view";
|
||||
import type { PageProps as LegacyPageProps } from "~/team/type-view";
|
||||
|
||||
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
|
||||
const legacyCtx = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
|
||||
const props = await getData(legacyCtx);
|
||||
const { booking, isSEOIndexable, eventData, isBrandingHidden } = props;
|
||||
|
||||
@@ -29,7 +29,7 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||
})),
|
||||
],
|
||||
};
|
||||
const decodedParams = decodeParams(params);
|
||||
const decodedParams = decodeParams(await params);
|
||||
const metadata = await generateMeetingMetadata(
|
||||
meeting,
|
||||
(t) => `${booking?.uid && !!booking ? t("reschedule") : ""} ${title} | ${profileName}`,
|
||||
@@ -51,7 +51,9 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||
const getData = withAppDirSsr<LegacyPageProps>(getServerSideProps);
|
||||
|
||||
const ServerPage = async ({ params, searchParams }: PageProps) => {
|
||||
const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
|
||||
const props = await getData(
|
||||
buildLegacyCtx(await headers(), await cookies(), await params, await searchParams)
|
||||
);
|
||||
return <LegacyPage {...props} />;
|
||||
};
|
||||
export default ServerPage;
|
||||
|
||||
@@ -10,7 +10,7 @@ import TeamPage, { type PageProps as ClientPageProps } from "~/team/team-view";
|
||||
const getData = withEmbedSsrAppDir<ClientPageProps>(getServerSideProps);
|
||||
|
||||
const ServerPage = async ({ params, searchParams }: ServerPageProps) => {
|
||||
const context = buildLegacyCtx(headers(), cookies(), params, searchParams);
|
||||
const context = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
|
||||
const props = await getData(context);
|
||||
return <TeamPage {...props} />;
|
||||
};
|
||||
|
||||
@@ -14,7 +14,7 @@ import LegacyPage from "~/team/team-view";
|
||||
|
||||
export const generateMetadata = async ({ params, searchParams }: _PageProps) => {
|
||||
const { team, markdownStrippedBio, isSEOIndexable, currentOrgDomain } = await getData(
|
||||
buildLegacyCtx(headers(), cookies(), params, searchParams)
|
||||
buildLegacyCtx(await headers(), await cookies(), await params, await searchParams)
|
||||
);
|
||||
|
||||
const meeting = {
|
||||
@@ -24,7 +24,7 @@ export const generateMetadata = async ({ params, searchParams }: _PageProps) =>
|
||||
image: getOrgOrTeamAvatar(team),
|
||||
},
|
||||
};
|
||||
const decodedParams = decodeParams(params);
|
||||
const decodedParams = decodeParams(await params);
|
||||
const metadata = await generateMeetingMetadata(
|
||||
meeting,
|
||||
(t) => team.name || t("nameless_team"),
|
||||
@@ -45,7 +45,9 @@ export const generateMetadata = async ({ params, searchParams }: _PageProps) =>
|
||||
const getData = withAppDirSsr<PageProps>(getServerSideProps);
|
||||
|
||||
const ServerPage = async ({ params, searchParams }: _PageProps) => {
|
||||
const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
|
||||
const props = await getData(
|
||||
buildLegacyCtx(await headers(), await cookies(), await params, await searchParams)
|
||||
);
|
||||
return <LegacyPage {...props} />;
|
||||
};
|
||||
export default ServerPage;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import type { LayoutProps } from "@calcom/features/shell/Shell";
|
||||
import { Button } from "@calcom/ui";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
|
||||
export const ShellMainAppDirBackButton = ({ backPath }: { backPath: LayoutProps["backPath"] }) => {
|
||||
const router = useRouter();
|
||||
|
||||
@@ -17,7 +17,7 @@ export const generateMetadata = async () => {
|
||||
};
|
||||
|
||||
const Page = async () => {
|
||||
// const session = await getServerSession({ req: buildLegacyRequest(headers(), cookies()) });
|
||||
// const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
|
||||
// const userId = session?.user?.id;
|
||||
// const orgId = session?.user?.org?.id;
|
||||
// if (!userId || !orgId) {
|
||||
|
||||
@@ -18,7 +18,7 @@ export const generateMetadata = async () =>
|
||||
);
|
||||
|
||||
const Page = async ({ params }: PageProps) => {
|
||||
const parsed = querySchema.safeParse(params);
|
||||
const parsed = querySchema.safeParse(await params);
|
||||
if (!parsed.success) {
|
||||
redirect("/bookings/upcoming");
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ export const generateMetadata = async () =>
|
||||
);
|
||||
|
||||
const Page = async ({ params, searchParams }: PageProps) => {
|
||||
const context = buildLegacyCtx(headers(), cookies(), params, searchParams);
|
||||
const context = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
|
||||
const session = await getServerSession({ req: context.req });
|
||||
|
||||
if (!session?.user?.id) {
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
|
||||
const Layout = ({ children }: { children: React.ReactNode }) => {
|
||||
return (
|
||||
<Shell withoutSeo={true} withoutMain={true}>
|
||||
{children}
|
||||
</Shell>
|
||||
);
|
||||
return <Shell withoutMain={true}>{children}</Shell>;
|
||||
};
|
||||
|
||||
export default Layout;
|
||||
|
||||
@@ -16,8 +16,9 @@ export const generateMetadata = async () =>
|
||||
(t) => t("create_manage_teams_collaborative")
|
||||
);
|
||||
|
||||
const ServerPage = async ({ searchParams }: ServerPageProps) => {
|
||||
const session = await getServerSession({ req: buildLegacyRequest(headers(), cookies()) });
|
||||
const ServerPage = async ({ searchParams: _searchParams }: ServerPageProps) => {
|
||||
const session = await getServerSession({ req: buildLegacyRequest(await headers(), await cookies()) });
|
||||
const searchParams = await _searchParams;
|
||||
const token = Array.isArray(searchParams?.token) ? searchParams.token[0] : searchParams?.token;
|
||||
const callbackUrl = token ? `/teams?token=${encodeURIComponent(token)}` : null;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { _generateMetadata, getTranslate } from "app/_utils";
|
||||
|
||||
import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { Button } from "@calcom/ui";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
|
||||
export const generateMetadata = () =>
|
||||
_generateMetadata(
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button, showToast } from "@calcom/ui";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { showToast } from "@calcom/ui/components/toast";
|
||||
|
||||
export default function CopyButton({ error }: { error: string }) {
|
||||
const { t } = useLocale();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { _generateMetadata, getTranslate } from "app/_utils";
|
||||
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import { Button } from "@calcom/ui";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
|
||||
import CopyButton from "./copy-button";
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ const paramsSchema = z.object({
|
||||
});
|
||||
|
||||
export const generateMetadata = async ({ params }: _PageProps) => {
|
||||
const p = paramsSchema.safeParse(params);
|
||||
const p = paramsSchema.safeParse(await params);
|
||||
|
||||
if (!p.success) {
|
||||
return notFound();
|
||||
@@ -33,7 +33,7 @@ export const generateMetadata = async ({ params }: _PageProps) => {
|
||||
};
|
||||
|
||||
async function Page({ params }: _PageProps) {
|
||||
const p = paramsSchema.safeParse(params);
|
||||
const p = paramsSchema.safeParse(await params);
|
||||
|
||||
if (!p.success) {
|
||||
return notFound();
|
||||
|
||||
@@ -9,7 +9,8 @@ import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
|
||||
import SetupView, { type PageProps as ClientPageProps } from "~/apps/[slug]/setup/setup-view";
|
||||
|
||||
export const generateMetadata = async ({ params }: ServerPageProps) => {
|
||||
export const generateMetadata = async ({ params: _params }: ServerPageProps) => {
|
||||
const params = await _params;
|
||||
const metadata = await _generateMetadata(
|
||||
() => `${params.slug}`,
|
||||
() => ""
|
||||
@@ -26,7 +27,7 @@ export const generateMetadata = async ({ params }: ServerPageProps) => {
|
||||
const getData = withAppDirSsr<ClientPageProps>(getServerSideProps);
|
||||
|
||||
const ServerPage = async ({ params, searchParams }: ServerPageProps) => {
|
||||
const context = buildLegacyCtx(headers(), cookies(), params, searchParams);
|
||||
const context = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
|
||||
|
||||
const { dehydratedState, ...props } = await getData(context);
|
||||
return <SetupView {...props} />;
|
||||
|
||||
@@ -13,7 +13,7 @@ const querySchema = z.object({
|
||||
});
|
||||
|
||||
async function Page({ params, searchParams }: PageProps) {
|
||||
const parsed = querySchema.safeParse({ ...params, ...searchParams });
|
||||
const parsed = querySchema.safeParse({ ...(await params), ...(await searchParams) });
|
||||
if (!parsed.success) {
|
||||
redirect("/apps/categories/calendar");
|
||||
}
|
||||
|
||||
@@ -10,7 +10,9 @@ import Page from "~/apps/categories/categories-view";
|
||||
const getData = withAppDirSsr(getServerSideProps);
|
||||
|
||||
async function ServerPage({ params, searchParams }: PageProps) {
|
||||
const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
|
||||
const props = await getData(
|
||||
buildLegacyCtx(await headers(), await cookies(), await params, await searchParams)
|
||||
);
|
||||
|
||||
return <Page {...props} />;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import type { OnboardingPageProps } from "~/apps/installation/[[...step]]/step-v
|
||||
import Page from "~/apps/installation/[[...step]]/step-view";
|
||||
|
||||
export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||
const legacyCtx = buildLegacyCtx(headers(), cookies(), params, searchParams);
|
||||
const legacyCtx = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
|
||||
|
||||
const { appMetadata } = await getData(legacyCtx);
|
||||
return await _generateMetadata(
|
||||
@@ -22,7 +22,9 @@ export const generateMetadata = async ({ params, searchParams }: PageProps) => {
|
||||
const getData = withAppDirSsr<OnboardingPageProps>(getServerSideProps);
|
||||
|
||||
const ServerPage = async ({ params, searchParams }: PageProps) => {
|
||||
const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
|
||||
const props = await getData(
|
||||
buildLegacyCtx(await headers(), await cookies(), await params, await searchParams)
|
||||
);
|
||||
return <Page {...props} />;
|
||||
};
|
||||
export default ServerPage;
|
||||
|
||||
@@ -19,7 +19,7 @@ export const generateMetadata = async () => {
|
||||
};
|
||||
|
||||
const InstalledAppsWrapper = async ({ params }: PageProps) => {
|
||||
const parsedParams = querySchema.safeParse(params);
|
||||
const parsedParams = querySchema.safeParse(await params);
|
||||
|
||||
if (!parsedParams.success) {
|
||||
redirect("/apps/installed/calendar");
|
||||
|
||||
@@ -18,7 +18,7 @@ export const generateMetadata = async () => {
|
||||
const getData = withAppDirSsr(getServerSideProps);
|
||||
|
||||
const ServerPage = async ({ params, searchParams }: PageProps) => {
|
||||
const context = buildLegacyCtx(headers(), cookies(), params, searchParams);
|
||||
const context = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
|
||||
|
||||
const props = await getData(context);
|
||||
return <AppsPage {...props} />;
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { FormProvider as ReactHookFormProvider, useForm } from "react-hook-form";
|
||||
|
||||
export default function FormProvider({ children }: { children: React.ReactNode }) {
|
||||
const methods = useForm();
|
||||
|
||||
return <ReactHookFormProvider {...methods}>{children}</ReactHookFormProvider>;
|
||||
}
|
||||
@@ -3,23 +3,25 @@ import type { PageProps as ServerPageProps } from "app/_types";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { cookies, headers } from "next/headers";
|
||||
|
||||
import type { routingServerSidePropsConfig } from "@calcom/app-store/routing-forms/pages/app-routing.config";
|
||||
import LayoutHandler from "@calcom/app-store/routing-forms/pages/layout-handler/[...appPages]";
|
||||
import { routingFormsComponents } from "@calcom/app-store/routing-forms/pages/app-routing.client-config";
|
||||
import type { routingServerSidePropsConfig } from "@calcom/app-store/routing-forms/pages/app-routing.server-config";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
|
||||
import { getServerSideProps } from "@lib/apps/routing-forms/[...pages]/getServerSideProps";
|
||||
import { buildLegacyCtx } from "@lib/buildLegacyCtx";
|
||||
|
||||
import FormProvider from "./FormProvider";
|
||||
|
||||
const normalizePages = (pages: string[] | string | undefined) => {
|
||||
const normalizedPages = Array.isArray(pages) ? pages : pages?.split("/") ?? [];
|
||||
return {
|
||||
mainPage: normalizedPages[0],
|
||||
mainPage: normalizedPages[0] ?? "forms",
|
||||
subPages: normalizedPages.slice(1),
|
||||
};
|
||||
};
|
||||
|
||||
export const generateMetadata = async ({ params }: { params: { pages: string[] } }) => {
|
||||
const { mainPage } = normalizePages(params.pages);
|
||||
export const generateMetadata = async ({ params }: { params: Promise<{ pages: string[] }> }) => {
|
||||
const { mainPage } = normalizePages((await params).pages);
|
||||
return await _generateMetadata(
|
||||
// TODO: Need to show the actual form name instead of "Form"
|
||||
(t) => (mainPage === "routing-link" ? `Form | Cal.com Forms` : `${t("routing_forms")} | Cal.com Forms`),
|
||||
@@ -32,22 +34,24 @@ type GetServerSidePropsResult =
|
||||
const getData = withAppDirSsr<GetServerSidePropsResult>(getServerSideProps);
|
||||
|
||||
const ServerPage = async ({ params, searchParams }: ServerPageProps) => {
|
||||
const context = buildLegacyCtx(headers(), cookies(), params, searchParams);
|
||||
const context = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
|
||||
const props = await getData(context);
|
||||
const { mainPage, subPages } = normalizePages(params.pages);
|
||||
const { mainPage } = normalizePages((await params).pages);
|
||||
|
||||
const componentProps = {
|
||||
...props,
|
||||
pages: subPages,
|
||||
};
|
||||
const Component = await routingFormsComponents[mainPage as keyof typeof routingFormsComponents]();
|
||||
const FinalComponent = () => (
|
||||
<FormProvider>
|
||||
<Component {...(props as any)} />
|
||||
</FormProvider>
|
||||
);
|
||||
|
||||
if (mainPage === "routing-link") {
|
||||
return <LayoutHandler {...componentProps} />;
|
||||
return <FinalComponent />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Shell withoutMain withoutSeo>
|
||||
<LayoutHandler {...componentProps} />
|
||||
<Shell withoutMain>
|
||||
<FinalComponent />
|
||||
</Shell>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -3,7 +3,8 @@ import { _generateMetadata, getTranslate } from "app/_utils";
|
||||
import Link from "next/link";
|
||||
import { z } from "zod";
|
||||
|
||||
import { Button, Icon } from "@calcom/ui";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
|
||||
import AuthContainer from "@components/ui/AuthContainer";
|
||||
|
||||
@@ -20,7 +21,7 @@ const querySchema = z.object({
|
||||
|
||||
const ServerPage = async ({ searchParams }: PageProps) => {
|
||||
const t = await getTranslate();
|
||||
const { error } = querySchema.parse({ error: searchParams?.error || undefined });
|
||||
const { error } = querySchema.parse({ error: (await searchParams)?.error || undefined });
|
||||
const errorMsg = t("error_during_login") + (error ? ` Error code: ${error}` : "");
|
||||
return (
|
||||
<AuthContainer>
|
||||
|
||||
@@ -19,7 +19,7 @@ export const generateMetadata = async () => {
|
||||
|
||||
const getData = withAppDirSsr<ClientPageProps>(getServerSideProps);
|
||||
const ServerPage = async ({ params, searchParams }: ServerPageProps) => {
|
||||
const context = buildLegacyCtx(headers(), cookies(), params, searchParams);
|
||||
const context = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
|
||||
const props = await getData(context);
|
||||
|
||||
return <SetNewUserPassword {...props} />;
|
||||
|
||||
@@ -18,7 +18,7 @@ export const generateMetadata = async () => {
|
||||
};
|
||||
|
||||
const ServerPage = async ({ params, searchParams }: ServerPageProps) => {
|
||||
const context = buildLegacyCtx(headers(), cookies(), params, searchParams);
|
||||
const context = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
|
||||
const session = await getServerSession({ req: context.req });
|
||||
|
||||
if (session) {
|
||||
|
||||
@@ -20,7 +20,9 @@ export const generateMetadata = async () => {
|
||||
const getData = withAppDirSsr<ClientPageProps>(getServerSideProps);
|
||||
|
||||
const ServerPage = async ({ params, searchParams }: ServerPageProps) => {
|
||||
const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
|
||||
const props = await getData(
|
||||
buildLegacyCtx(await headers(), await cookies(), await params, await searchParams)
|
||||
);
|
||||
return <Login {...props} />;
|
||||
};
|
||||
|
||||
|
||||
@@ -17,8 +17,8 @@ export const generateMetadata = async () => {
|
||||
|
||||
const Page = async ({ params, searchParams }: PageProps) => {
|
||||
// cookie will be cleared in `/apps/web/middleware.ts`
|
||||
const h = headers();
|
||||
const context = buildLegacyCtx(h, cookies(), params, searchParams);
|
||||
const h = await headers();
|
||||
const context = buildLegacyCtx(h, await cookies(), await params, await searchParams);
|
||||
await ssrInit(context);
|
||||
|
||||
return <Logout query={context.query} />;
|
||||
|
||||
@@ -20,7 +20,9 @@ export const generateMetadata = async () => {
|
||||
const getData = withAppDirSsr<ClientPageProps>(getServerSideProps);
|
||||
|
||||
const ServerPage = async ({ params, searchParams }: ServerPageProps) => {
|
||||
const props = await getData(buildLegacyCtx(headers(), cookies(), params, searchParams));
|
||||
const props = await getData(
|
||||
buildLegacyCtx(await headers(), await cookies(), await params, await searchParams)
|
||||
);
|
||||
return <Setup {...props} />;
|
||||
};
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import type { PageProps as ClientPageProps } from "~/auth/signin-view";
|
||||
|
||||
const getData = withAppDirSsr<ClientPageProps>(getServerSideProps);
|
||||
const ServerPage = async ({ params, searchParams }: ServerPageProps) => {
|
||||
const context = buildLegacyCtx(headers(), cookies(), params, searchParams);
|
||||
const context = buildLegacyCtx(await headers(), await cookies(), await params, await searchParams);
|
||||
const session = await getServerSession({ req: context.req });
|
||||
if (session) {
|
||||
redirect("/");
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user