Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f8f6c31e40 | ||
|
|
7246fa4084 | ||
|
|
039a0b7a85 | ||
|
|
12c5005305 | ||
|
|
1ff6465384 | ||
|
|
ae706bb6d6 | ||
|
|
538a39328e | ||
|
|
3589767099 | ||
|
|
ff2754f663 | ||
|
|
65484e7d01 | ||
|
|
4d942a454a | ||
|
|
7dbcb62724 | ||
|
|
c197c33fd0 | ||
|
|
815065d67c | ||
|
|
02cecd6707 | ||
|
|
81386d4ec0 | ||
|
|
204e411585 | ||
|
|
1ec1554cb5 | ||
|
|
241579eba6 | ||
|
|
8ccad29ab6 | ||
|
|
74b4d7a94a | ||
|
|
5c6e60b726 | ||
|
|
18c7c3c940 | ||
|
|
fbd113b976 | ||
|
|
c7f11c6795 | ||
|
|
84fb9bb371 | ||
|
|
1aac70f643 | ||
|
|
0dde8c1db9 | ||
|
|
0da3a9fa11 | ||
|
|
eb8f5bd194 | ||
|
|
82901fee94 | ||
|
|
4e0e3a47f5 | ||
|
|
dc400d80d0 | ||
|
|
6889592d4f | ||
|
|
95b833bc1c | ||
|
|
b06683458a | ||
|
|
01d764c6d3 | ||
|
|
49333addc2 | ||
|
|
6c40d29745 | ||
|
|
fc5ad53a6d | ||
|
|
4d38c6d0c7 | ||
|
|
cd15335eed | ||
|
|
aaa75100d4 | ||
|
|
ae9063804d | ||
|
|
30f9d9b980 | ||
|
|
42a264380a | ||
|
|
62aaba32cf | ||
|
|
aaf630f01f | ||
|
|
838e8b9e89 | ||
|
|
ce373c475c | ||
|
|
ed662b7956 | ||
|
|
2e49110ce6 | ||
|
|
fc35f9de85 | ||
|
|
9c67ee7375 | ||
|
|
a1477dc7e1 | ||
|
|
25d39b4273 | ||
|
|
4976e24e12 | ||
|
|
5ae6e1e983 | ||
|
|
6080ce0c85 | ||
|
|
79f0754bf7 | ||
|
|
540bd478dc | ||
|
|
9873e373dc | ||
|
|
52093862ce | ||
|
|
e101f35b4f | ||
|
|
54cc2dc094 | ||
|
|
88e57d48fe | ||
|
|
6619701c88 | ||
|
|
ebb8bb9c67 | ||
|
|
993bf23986 | ||
|
|
4b38d73120 | ||
|
|
8f92c9b884 | ||
|
|
edecc7e60e | ||
|
|
4eefb08a0d | ||
|
|
3f7e632d5c | ||
|
|
ef0fd67646 | ||
|
|
8e7567b6a3 | ||
|
|
193855d60f | ||
|
|
92fafa7344 | ||
|
|
21aaff01fa | ||
|
|
e07a28a2d1 | ||
|
|
9a1b5ad149 | ||
|
|
1c20a257d3 | ||
|
|
656fcafcde | ||
|
|
6d7080d9fc | ||
|
|
f943debda2 | ||
|
|
803fc488c5 | ||
|
|
423b284278 | ||
|
|
f1111c9d42 | ||
|
|
ed6d88e30f | ||
|
|
e675ad0c6c | ||
|
|
a01d6aa932 | ||
|
|
f868b86e6c | ||
|
|
db0a7f3797 | ||
|
|
5646229cd4 | ||
|
|
f39e817d86 | ||
|
|
42c4c134fa | ||
|
|
ffc4b69edd | ||
|
|
7263f3b00b | ||
|
|
ddfbe25903 | ||
|
|
dd944bace1 |
+5
-2
@@ -102,6 +102,11 @@ NEXT_PUBLIC_INTERCOM_APP_ID=
|
||||
# Secret to enable Intercom Identity Verification
|
||||
INTERCOM_SECRET=
|
||||
|
||||
# Posthog Config
|
||||
NEXT_PUBLIC_POSTHOG_KEY=
|
||||
|
||||
NEXT_PUBLIC_POSTHOG_HOST=
|
||||
|
||||
# Zendesk Config
|
||||
NEXT_PUBLIC_ZENDESK_KEY=
|
||||
|
||||
@@ -337,8 +342,6 @@ AB_TEST_BUCKET_PROBABILITY=50
|
||||
# whether we redirect to the future/event-types from event-types or not
|
||||
APP_ROUTER_EVENT_TYPES_ENABLED=0
|
||||
APP_ROUTER_SETTINGS_DEVELOPER_ENABLED=0
|
||||
APP_ROUTER_SETTINGS_MY_ACCOUNT_ENABLED=0
|
||||
APP_ROUTER_SETTINGS_PLATFORM_ENABLED=0
|
||||
APP_ROUTER_SETTINGS_ORG_ENABLED=0
|
||||
APP_ROUTER_APPS_INSTALLED_CATEGORY_ENABLED=0
|
||||
APP_ROUTER_APPS_SLUG_ENABLED=0
|
||||
|
||||
@@ -199,6 +199,7 @@ async function handler(req: NextApiRequest) {
|
||||
eventTypeId,
|
||||
userId,
|
||||
returnDateOverrides: true,
|
||||
bypassBusyCalendarTimes: false,
|
||||
});
|
||||
const team = await prisma.team.findUnique({
|
||||
where: { id: teamId },
|
||||
@@ -235,6 +236,7 @@ async function handler(req: NextApiRequest) {
|
||||
dateTo,
|
||||
eventTypeId,
|
||||
returnDateOverrides: true,
|
||||
bypassBusyCalendarTimes: false,
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -81,7 +81,7 @@ describe("retrieveScopedAccessibleUsers tests", () => {
|
||||
where: { email: "member2-acme@example.com" },
|
||||
});
|
||||
|
||||
expect(accessibleUserIds.length).toBe(3);
|
||||
expect(accessibleUserIds.length).toBe(11);
|
||||
expect(accessibleUserIds).toContain(memberOneUser.id);
|
||||
expect(accessibleUserIds).toContain(memberTwoUser.id);
|
||||
expect(accessibleUserIds).toContain(adminUser.id);
|
||||
|
||||
@@ -28,3 +28,7 @@ API_KEY_PREFIX=cal_
|
||||
GET_LICENSE_KEY_URL="https://console.cal.com/api/license"
|
||||
IS_E2E=false
|
||||
DOCS_URL=
|
||||
|
||||
# Axiom logging
|
||||
AXIOM_DATASET=
|
||||
AXIOM_TOKEN=
|
||||
|
||||
@@ -19,16 +19,17 @@
|
||||
"test:watch": "yarn dev:build && jest --watch",
|
||||
"test:cov": "yarn dev:build && jest --coverage",
|
||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||
"test:e2e": "yarn dev:build && NODE_OPTIONS='--max_old_space_size=8192' jest --ci --forceExit --config ./jest-e2e.json",
|
||||
"test:e2e": "yarn dev:build && NODE_OPTIONS='--max_old_space_size=8192 --experimental-vm-modules' jest --ci --forceExit --config ./jest-e2e.json",
|
||||
"test:e2e:watch": "yarn dev:build && jest --runInBand --detectOpenHandles --forceExit --config ./jest-e2e.json --watch",
|
||||
"prisma": "yarn workspace @calcom/prisma prisma",
|
||||
"generate-schemas": "yarn prisma generate && yarn prisma format",
|
||||
"copy-swagger-module": "ts-node -r tsconfig-paths/register swagger/copy-swagger-module.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@axiomhq/winston": "^1.2.0",
|
||||
"@calcom/platform-constants": "*",
|
||||
"@calcom/platform-enums": "*",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.56",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.60",
|
||||
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
|
||||
"@calcom/platform-types": "*",
|
||||
"@calcom/platform-utils": "*",
|
||||
@@ -45,7 +46,9 @@
|
||||
"@nestjs/platform-express": "^10.0.0",
|
||||
"@nestjs/swagger": "^7.3.0",
|
||||
"@nestjs/throttler": "6.2.1",
|
||||
"@sentry/nestjs": "^8.37.1",
|
||||
"@sentry/node": "^8.8.0",
|
||||
"@sentry/profiling-node": "^8.37.1",
|
||||
"body-parser": "^1.20.2",
|
||||
"bull": "^4.12.4",
|
||||
"class-transformer": "^0.5.1",
|
||||
@@ -67,7 +70,8 @@
|
||||
"rxjs": "^7.8.1",
|
||||
"stripe": "^15.3.0",
|
||||
"uuid": "^8.3.2",
|
||||
"winston": "^3.11.0",
|
||||
"winston": "^3.13.0",
|
||||
"winston-transport": "^4.7.0",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -17,13 +17,15 @@ import { ThrottlerStorageRedisService } from "@nest-lab/throttler-storage-redis"
|
||||
import { BullModule } from "@nestjs/bull";
|
||||
import { MiddlewareConsumer, Module, NestModule, RequestMethod } from "@nestjs/common";
|
||||
import { ConfigModule } from "@nestjs/config";
|
||||
import { APP_GUARD, APP_INTERCEPTOR } from "@nestjs/core";
|
||||
import { APP_GUARD, APP_INTERCEPTOR, APP_FILTER } from "@nestjs/core";
|
||||
import { seconds, ThrottlerModule } from "@nestjs/throttler";
|
||||
import { SentryModule, SentryGlobalFilter } from "@sentry/nestjs/setup";
|
||||
|
||||
import { AppController } from "./app.controller";
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
SentryModule.forRoot(),
|
||||
ConfigModule.forRoot({
|
||||
ignoreEnvFile: true,
|
||||
isGlobal: true,
|
||||
@@ -59,6 +61,10 @@ import { AppController } from "./app.controller";
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [
|
||||
{
|
||||
provide: APP_FILTER,
|
||||
useClass: SentryGlobalFilter,
|
||||
},
|
||||
{
|
||||
provide: ThrottlerStorageRedisService,
|
||||
useFactory: (redisService: RedisService) => {
|
||||
|
||||
@@ -7,7 +7,6 @@ import type { ValidationError } from "@nestjs/common";
|
||||
import { BadRequestException, ValidationPipe, VersioningType } from "@nestjs/common";
|
||||
import { BaseExceptionFilter, HttpAdapterHost } from "@nestjs/core";
|
||||
import type { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import * as Sentry from "@sentry/node";
|
||||
import * as cookieParser from "cookie-parser";
|
||||
import { Request } from "express";
|
||||
import helmet from "helmet";
|
||||
@@ -71,9 +70,6 @@ export const bootstrap = (app: NestExpressApplication): NestExpressApplication =
|
||||
|
||||
// Exception filters, new filters go at the bottom, keep the order
|
||||
const { httpAdapter } = app.get(HttpAdapterHost);
|
||||
if (process.env.SENTRY_DSN) {
|
||||
Sentry.setupNestErrorHandler(app, new BaseExceptionFilter(httpAdapter));
|
||||
}
|
||||
app.useGlobalFilters(new PrismaExceptionFilter());
|
||||
app.useGlobalFilters(new ZodExceptionFilter());
|
||||
app.useGlobalFilters(new HttpExceptionFilter());
|
||||
|
||||
@@ -7,6 +7,7 @@ import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_20
|
||||
import { ApiKeyRepository } from "@/modules/api-key/api-key-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 { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
@@ -27,6 +28,7 @@ import { Module } from "@nestjs/common";
|
||||
OutputBookingsService_2024_08_13,
|
||||
BookingsRepository_2024_08_13,
|
||||
EventTypesRepository_2024_06_14,
|
||||
BookingSeatRepository,
|
||||
ApiKeyRepository,
|
||||
],
|
||||
controllers: [BookingsController_2024_08_13],
|
||||
|
||||
@@ -56,6 +56,17 @@ export class BookingsRepository_2024_08_13 {
|
||||
});
|
||||
}
|
||||
|
||||
async getByUidWithUser(bookingUid: string) {
|
||||
return this.dbRead.prisma.booking.findUnique({
|
||||
where: {
|
||||
uid: bookingUid,
|
||||
},
|
||||
include: {
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getByIdWithAttendeesAndUserAndEvent(id: number) {
|
||||
return this.dbRead.prisma.booking.findUnique({
|
||||
where: {
|
||||
|
||||
@@ -2,6 +2,7 @@ import { BookingUidGuard } from "@/ee/bookings/2024-08-13/guards/booking-uid.gua
|
||||
import { CancelBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/cancel-booking.output";
|
||||
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 { ReassignBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reassign-booking.output";
|
||||
import { RescheduleBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reschedule-booking.output";
|
||||
import { BookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/bookings.service";
|
||||
import { VERSION_2024_08_13_VALUE } from "@/lib/api-versions";
|
||||
@@ -9,6 +10,7 @@ import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import {
|
||||
Controller,
|
||||
Post,
|
||||
@@ -46,12 +48,12 @@ import {
|
||||
CreateBookingInputPipe,
|
||||
CreateBookingInput,
|
||||
GetBookingsInput_2024_08_13,
|
||||
RescheduleBookingInput_2024_08_13,
|
||||
CancelBookingInput_2024_08_13,
|
||||
ReassignToUserBookingInput_2024_08_13,
|
||||
MarkAbsentBookingInput_2024_08_13,
|
||||
CreateBookingInput_2024_08_13,
|
||||
CreateInstantBookingInput_2024_08_13,
|
||||
CreateRecurringBookingInput_2024_08_13,
|
||||
DeclineBookingInput_2024_08_13,
|
||||
} from "@calcom/platform-types";
|
||||
|
||||
@Controller({
|
||||
@@ -227,4 +229,104 @@ export class BookingsController_2024_08_13 {
|
||||
data: booking,
|
||||
};
|
||||
}
|
||||
|
||||
@Post("/:bookingUid/reassign")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Permissions([BOOKING_WRITE])
|
||||
@UseGuards(ApiAuthGuard, BookingUidGuard)
|
||||
@ApiHeader({
|
||||
name: "Authorization",
|
||||
description:
|
||||
"value must be `Bearer <token>` where `<token>` either managed user access token or api key prefixed with cal_",
|
||||
required: true,
|
||||
})
|
||||
@ApiOperation({ summary: "Automatically reassign booking to a new host" })
|
||||
async reassignBooking(
|
||||
@Param("bookingUid") bookingUid: string,
|
||||
@GetUser() user: UserWithProfile
|
||||
): Promise<ReassignBookingOutput_2024_08_13> {
|
||||
const booking = await this.bookingsService.reassignBooking(bookingUid, user);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: booking,
|
||||
};
|
||||
}
|
||||
|
||||
@Post("/:bookingUid/reassign/:userId")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Permissions([BOOKING_WRITE])
|
||||
@UseGuards(ApiAuthGuard, BookingUidGuard)
|
||||
@ApiHeader({
|
||||
name: "Authorization",
|
||||
description:
|
||||
"value must be `Bearer <token>` where `<token>` either managed user access token or api key prefixed with cal_",
|
||||
required: true,
|
||||
})
|
||||
@ApiOperation({ summary: "Reassign a booking to a specific user" })
|
||||
async reassignBookingToUser(
|
||||
@Param("bookingUid") bookingUid: string,
|
||||
@Param("userId") userId: number,
|
||||
@GetUser("id") reassignedById: number,
|
||||
@Body() body: ReassignToUserBookingInput_2024_08_13
|
||||
): Promise<ReassignBookingOutput_2024_08_13> {
|
||||
const booking = await this.bookingsService.reassignBookingToUser(
|
||||
bookingUid,
|
||||
userId,
|
||||
reassignedById,
|
||||
body
|
||||
);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: booking,
|
||||
};
|
||||
}
|
||||
|
||||
@Post("/:bookingUid/confirm")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Permissions([BOOKING_WRITE])
|
||||
@UseGuards(ApiAuthGuard, BookingUidGuard)
|
||||
@ApiHeader({
|
||||
name: "Authorization",
|
||||
description:
|
||||
"value must be `Bearer <token>` where `<token>` either managed user access token or api key prefixed with cal_",
|
||||
required: true,
|
||||
})
|
||||
@ApiOperation({ summary: "Confirm booking that requires a confirmation" })
|
||||
async confirmBooking(
|
||||
@Param("bookingUid") bookingUid: string,
|
||||
@GetUser() user: UserWithProfile
|
||||
): Promise<GetBookingOutput_2024_08_13> {
|
||||
const booking = await this.bookingsService.confirmBooking(bookingUid, user);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: booking,
|
||||
};
|
||||
}
|
||||
|
||||
@Post("/:bookingUid/decline")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Permissions([BOOKING_WRITE])
|
||||
@UseGuards(ApiAuthGuard, BookingUidGuard)
|
||||
@ApiHeader({
|
||||
name: "Authorization",
|
||||
description:
|
||||
"value must be `Bearer <token>` where `<token>` either managed user access token or api key prefixed with cal_",
|
||||
required: true,
|
||||
})
|
||||
@ApiOperation({ summary: "Decline booking that requires a confirmation" })
|
||||
async declineBooking(
|
||||
@Param("bookingUid") bookingUid: string,
|
||||
@Body() body: DeclineBookingInput_2024_08_13,
|
||||
@GetUser() user: UserWithProfile
|
||||
): Promise<GetBookingOutput_2024_08_13> {
|
||||
const booking = await this.bookingsService.declineBooking(bookingUid, user, body.reason);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: booking,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,262 @@
|
||||
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 { 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";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import { User } from "@prisma/client";
|
||||
import * as request from "supertest";
|
||||
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
|
||||
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
|
||||
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
|
||||
import {
|
||||
AttendeeScheduledEmail,
|
||||
OrganizerScheduledEmail,
|
||||
AttendeeDeclinedEmail,
|
||||
} from "@calcom/platform-libraries";
|
||||
import { BookingOutput_2024_08_13, GetBookingOutput_2024_08_13 } from "@calcom/platform-types";
|
||||
import { Booking, PlatformOAuthClient, Team } from "@calcom/prisma/client";
|
||||
|
||||
jest.spyOn(AttendeeScheduledEmail.prototype as any, "getHtml").mockImplementation(async function () {
|
||||
return "<html><body>Mocked Email Content</body></html>";
|
||||
});
|
||||
|
||||
jest.spyOn(OrganizerScheduledEmail.prototype as any, "getHtml").mockImplementation(async function () {
|
||||
return "<html><body>Mocked Email Content</body></html>";
|
||||
});
|
||||
|
||||
jest.spyOn(AttendeeDeclinedEmail.prototype as any, "getHtml").mockImplementation(async function () {
|
||||
return "<html><body>Mocked Email Content</body></html>";
|
||||
});
|
||||
|
||||
describe("Bookings Endpoints 2024-08-13", () => {
|
||||
describe("Bookings confirmation", () => {
|
||||
let app: INestApplication;
|
||||
let organization: Team;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let bookingsRepositoryFixture: BookingsRepositoryFixture;
|
||||
let schedulesService: SchedulesService_2024_04_15;
|
||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
let oAuthClient: PlatformOAuthClient;
|
||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||
|
||||
const userEmail = "bookings-controller-e2e@api.com";
|
||||
let user: User;
|
||||
|
||||
let eventTypeId: number;
|
||||
const eventTypeSlug = "peer-coding";
|
||||
|
||||
let createdBooking1: Booking;
|
||||
let createdBooking2: Booking;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
userEmail,
|
||||
Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
|
||||
})
|
||||
)
|
||||
.overrideGuard(PermissionsGuard)
|
||||
.useValue({
|
||||
canActivate: () => true,
|
||||
})
|
||||
.compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
|
||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
|
||||
|
||||
organization = await teamRepositoryFixture.create({ name: "organization bookings" });
|
||||
oAuthClient = await createOAuthClient(organization.id);
|
||||
|
||||
user = await userRepositoryFixture.create({
|
||||
email: userEmail,
|
||||
});
|
||||
|
||||
const userSchedule: CreateScheduleInput_2024_04_15 = {
|
||||
name: "working time",
|
||||
timeZone: "Europe/Rome",
|
||||
isDefault: true,
|
||||
};
|
||||
await schedulesService.createUserSchedule(user.id, userSchedule);
|
||||
const event = await eventTypesRepositoryFixture.create(
|
||||
{ title: "peer coding", slug: eventTypeSlug, length: 60 },
|
||||
user.id
|
||||
);
|
||||
eventTypeId = event.id;
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
async function createOAuthClient(organizationId: number) {
|
||||
const data = {
|
||||
logo: "logo-url",
|
||||
name: "name",
|
||||
redirectUris: ["http://localhost:5555"],
|
||||
permissions: 32,
|
||||
};
|
||||
const secret = "secret";
|
||||
|
||||
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
|
||||
return client;
|
||||
}
|
||||
|
||||
it("should be defined", () => {
|
||||
expect(userRepositoryFixture).toBeDefined();
|
||||
expect(user).toBeDefined();
|
||||
});
|
||||
|
||||
it("should confirm a booking", async () => {
|
||||
const status = "PENDING";
|
||||
createdBooking1 = await bookingsRepositoryFixture.create({
|
||||
user: {
|
||||
connect: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
startTime: new Date(Date.UTC(2050, 0, 7, 13, 0, 0)),
|
||||
endTime: new Date(Date.UTC(2050, 0, 7, 14, 0, 0)),
|
||||
title: "peer coding",
|
||||
uid: "peer-coding-one",
|
||||
eventType: {
|
||||
connect: {
|
||||
id: eventTypeId,
|
||||
},
|
||||
},
|
||||
location: "via 10, rome, italy",
|
||||
customInputs: {},
|
||||
metadata: {},
|
||||
responses: {
|
||||
name: "Bob",
|
||||
email: "bob@gmail.com",
|
||||
},
|
||||
attendees: {
|
||||
create: {
|
||||
email: "bob@gmail.com",
|
||||
name: "Bob",
|
||||
locale: "it",
|
||||
timeZone: "Europe/Rome",
|
||||
},
|
||||
},
|
||||
status,
|
||||
});
|
||||
expect(createdBooking1.status).toEqual("PENDING");
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${createdBooking1.uid}/confirm`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingOutput_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.uid).toBeDefined();
|
||||
expect(data.status).toEqual("accepted");
|
||||
|
||||
const dbBooking = await bookingsRepositoryFixture.getByUid(data.uid);
|
||||
expect(dbBooking?.status).toEqual("ACCEPTED");
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should decline a booking", async () => {
|
||||
const status = "PENDING";
|
||||
createdBooking2 = await bookingsRepositoryFixture.create({
|
||||
user: {
|
||||
connect: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
startTime: new Date(Date.UTC(2050, 0, 7, 10, 0, 0)),
|
||||
endTime: new Date(Date.UTC(2050, 0, 7, 11, 0, 0)),
|
||||
title: "peer coding",
|
||||
uid: "peer-coding-two",
|
||||
eventType: {
|
||||
connect: {
|
||||
id: eventTypeId,
|
||||
},
|
||||
},
|
||||
location: "via 10, rome, italy",
|
||||
customInputs: {},
|
||||
metadata: {},
|
||||
responses: {
|
||||
name: "Bob",
|
||||
email: "bob@gmail.com",
|
||||
},
|
||||
attendees: {
|
||||
create: {
|
||||
email: "bob@gmail.com",
|
||||
name: "Bob",
|
||||
locale: "it",
|
||||
timeZone: "Europe/Rome",
|
||||
},
|
||||
},
|
||||
status,
|
||||
});
|
||||
expect(createdBooking2.status).toEqual("PENDING");
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${createdBooking2.uid}/decline`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetBookingOutput_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.uid).toBeDefined();
|
||||
expect(data.status).toEqual("rejected");
|
||||
|
||||
const dbBooking = await bookingsRepositoryFixture.getByUid(data.uid);
|
||||
expect(dbBooking?.status).toEqual("REJECTED");
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||
await teamRepositoryFixture.delete(organization.id);
|
||||
await userRepositoryFixture.deleteByEmail(user.email);
|
||||
await bookingsRepositoryFixture.deleteAllBookings(user.id, user.email);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
|
||||
function responseDataIsBooking(data: any): data is BookingOutput_2024_08_13 {
|
||||
return !Array.isArray(data) && typeof data === "object" && data && "id" in data;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,337 @@
|
||||
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 { ReassignBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/reassign-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";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import { Booking, User } from "@prisma/client";
|
||||
import * as request from "supertest";
|
||||
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
|
||||
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
||||
import { HostsRepositoryFixture } from "test/fixtures/repository/hosts.repository.fixture";
|
||||
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
|
||||
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
|
||||
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
|
||||
import {
|
||||
CreateBookingInput_2024_08_13,
|
||||
BookingOutput_2024_08_13,
|
||||
RecurringBookingOutput_2024_08_13,
|
||||
GetBookingsOutput_2024_08_13,
|
||||
GetSeatedBookingOutput_2024_08_13,
|
||||
} from "@calcom/platform-types";
|
||||
import { PlatformOAuthClient, Team } from "@calcom/prisma/client";
|
||||
|
||||
describe("Bookings Endpoints 2024-08-13", () => {
|
||||
describe("Reassign bookings", () => {
|
||||
let app: INestApplication;
|
||||
let organization: Team;
|
||||
let team: Team;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let bookingsRepositoryFixture: BookingsRepositoryFixture;
|
||||
let schedulesService: SchedulesService_2024_04_15;
|
||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
let oAuthClient: PlatformOAuthClient;
|
||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||
let hostsRepositoryFixture: HostsRepositoryFixture;
|
||||
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
||||
let profileRepositoryFixture: ProfileRepositoryFixture;
|
||||
|
||||
const teamUserEmail = "orgUser1team1@api.com";
|
||||
const teamUserEmail2 = "orgUser2team1@api.com";
|
||||
let teamUser1: User;
|
||||
let teamUser2: User;
|
||||
|
||||
let teamRoundRobinEventTypeId: number;
|
||||
|
||||
let roundRobinBooking: Booking;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
teamUserEmail,
|
||||
Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
|
||||
})
|
||||
)
|
||||
.overrideGuard(PermissionsGuard)
|
||||
.useValue({
|
||||
canActivate: () => true,
|
||||
})
|
||||
.compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
|
||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
|
||||
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
||||
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||
hostsRepositoryFixture = new HostsRepositoryFixture(moduleRef);
|
||||
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
|
||||
|
||||
organization = await organizationsRepositoryFixture.create({ name: "organization team bookings" });
|
||||
team = await teamRepositoryFixture.create({
|
||||
name: "team 1",
|
||||
isOrganization: false,
|
||||
parent: { connect: { id: organization.id } },
|
||||
});
|
||||
|
||||
oAuthClient = await createOAuthClient(organization.id);
|
||||
|
||||
teamUser1 = await userRepositoryFixture.create({
|
||||
email: teamUserEmail,
|
||||
locale: "it",
|
||||
name: "orgUser1team1",
|
||||
});
|
||||
|
||||
teamUser2 = await userRepositoryFixture.create({
|
||||
email: teamUserEmail2,
|
||||
locale: "it",
|
||||
name: "orgUser2team1",
|
||||
});
|
||||
|
||||
const userSchedule: CreateScheduleInput_2024_04_15 = {
|
||||
name: "working time",
|
||||
timeZone: "Europe/Rome",
|
||||
isDefault: true,
|
||||
};
|
||||
await schedulesService.createUserSchedule(teamUser1.id, userSchedule);
|
||||
await schedulesService.createUserSchedule(teamUser2.id, userSchedule);
|
||||
|
||||
await profileRepositoryFixture.create({
|
||||
uid: `usr-${teamUser1.id}`,
|
||||
username: teamUserEmail,
|
||||
organization: {
|
||||
connect: {
|
||||
id: organization.id,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
connect: {
|
||||
id: teamUser1.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await profileRepositoryFixture.create({
|
||||
uid: `usr-${teamUser2.id}`,
|
||||
username: teamUserEmail2,
|
||||
organization: {
|
||||
connect: {
|
||||
id: organization.id,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
connect: {
|
||||
id: teamUser2.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "MEMBER",
|
||||
team: { connect: { id: team.id } },
|
||||
user: { connect: { id: teamUser1.id } },
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "MEMBER",
|
||||
team: { connect: { id: team.id } },
|
||||
user: { connect: { id: teamUser2.id } },
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
const team1EventType = await eventTypesRepositoryFixture.createTeamEventType({
|
||||
schedulingType: "ROUND_ROBIN",
|
||||
team: {
|
||||
connect: { id: team.id },
|
||||
},
|
||||
users: {
|
||||
connect: [{ id: teamUser1.id }, { id: teamUser2.id }],
|
||||
},
|
||||
title: "Round Robin Event Type",
|
||||
slug: "round-robin-event-type",
|
||||
length: 60,
|
||||
assignAllTeamMembers: false,
|
||||
bookingFields: [],
|
||||
locations: [{ type: "inPerson", address: "via 10, rome, italy" }],
|
||||
});
|
||||
|
||||
teamRoundRobinEventTypeId = team1EventType.id;
|
||||
|
||||
await hostsRepositoryFixture.create({
|
||||
isFixed: false,
|
||||
user: {
|
||||
connect: {
|
||||
id: teamUser1.id,
|
||||
},
|
||||
},
|
||||
eventType: {
|
||||
connect: {
|
||||
id: team1EventType.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await hostsRepositoryFixture.create({
|
||||
isFixed: false,
|
||||
user: {
|
||||
connect: {
|
||||
id: teamUser2.id,
|
||||
},
|
||||
},
|
||||
eventType: {
|
||||
connect: {
|
||||
id: team1EventType.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
roundRobinBooking = await bookingsRepositoryFixture.create({
|
||||
user: {
|
||||
connect: {
|
||||
id: teamUser1.id,
|
||||
},
|
||||
},
|
||||
startTime: new Date(Date.UTC(2050, 0, 7, 13, 0, 0)),
|
||||
endTime: new Date(Date.UTC(2050, 0, 7, 14, 0, 0)),
|
||||
title: "round robin coding lets goo",
|
||||
uid: "round-robin-coding",
|
||||
eventType: {
|
||||
connect: {
|
||||
id: teamRoundRobinEventTypeId,
|
||||
},
|
||||
},
|
||||
location: "via 10, rome, italy",
|
||||
customInputs: {},
|
||||
metadata: {},
|
||||
responses: {
|
||||
name: "Bob",
|
||||
email: "bob@gmail.com",
|
||||
},
|
||||
attendees: {
|
||||
create: {
|
||||
email: "bob@gmail.com",
|
||||
name: "Bob",
|
||||
locale: "it",
|
||||
timeZone: "Europe/Rome",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it("should reassign round robin booking", async () => {
|
||||
const booking = await bookingsRepositoryFixture.getByUid(roundRobinBooking.uid);
|
||||
expect(booking?.userId).toEqual(teamUser1.id);
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${roundRobinBooking.uid}/reassign`)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: ReassignBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
|
||||
const data: ReassignBookingOutput_2024_08_13["data"] = responseBody.data;
|
||||
expect(data.bookingUid).toEqual(roundRobinBooking.uid);
|
||||
expect(data.reassignedTo).toEqual({
|
||||
id: teamUser2.id,
|
||||
name: teamUser2.name,
|
||||
email: teamUser2.email,
|
||||
});
|
||||
|
||||
const reassigned = await bookingsRepositoryFixture.getByUid(roundRobinBooking.uid);
|
||||
expect(reassigned?.userId).toEqual(teamUser2.id);
|
||||
});
|
||||
});
|
||||
|
||||
it("should reassign round robin booking to a specific user", async () => {
|
||||
const booking = await bookingsRepositoryFixture.getByUid(roundRobinBooking.uid);
|
||||
expect(booking?.userId).toEqual(teamUser2.id);
|
||||
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 13, 0, 0)).toISOString(),
|
||||
eventTypeId: teamRoundRobinEventTypeId,
|
||||
attendee: {
|
||||
name: "alice",
|
||||
email: "alice@gmail.com",
|
||||
timeZone: "Europe/Madrid",
|
||||
language: "es",
|
||||
},
|
||||
meetingUrl: "https://meet.google.com/abc-def-ghi",
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/bookings/${roundRobinBooking.uid}/reassign/${teamUser1.id}`)
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: ReassignBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
|
||||
const data: ReassignBookingOutput_2024_08_13["data"] = responseBody.data;
|
||||
expect(data.bookingUid).toEqual(roundRobinBooking.uid);
|
||||
expect(data.reassignedTo).toEqual({
|
||||
id: teamUser1.id,
|
||||
name: teamUser1.name,
|
||||
email: teamUser1.email,
|
||||
});
|
||||
|
||||
const reassigned = await bookingsRepositoryFixture.getByUid(roundRobinBooking.uid);
|
||||
expect(reassigned?.userId).toEqual(teamUser1.id);
|
||||
});
|
||||
});
|
||||
|
||||
async function createOAuthClient(organizationId: number) {
|
||||
const data = {
|
||||
logo: "logo-url",
|
||||
name: "name",
|
||||
redirectUris: ["http://localhost:5555"],
|
||||
permissions: 32,
|
||||
};
|
||||
const secret = "secret";
|
||||
|
||||
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
|
||||
return client;
|
||||
}
|
||||
|
||||
function responseDataIsBooking(data: any): data is BookingOutput_2024_08_13 {
|
||||
return !Array.isArray(data) && typeof data === "object" && data && "id" in data;
|
||||
}
|
||||
|
||||
afterAll(async () => {
|
||||
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||
await teamRepositoryFixture.delete(organization.id);
|
||||
await userRepositoryFixture.deleteByEmail(teamUser1.email);
|
||||
await userRepositoryFixture.deleteByEmail(teamUserEmail2);
|
||||
await bookingsRepositoryFixture.deleteAllBookings(teamUser1.id, teamUser1.email);
|
||||
await bookingsRepositoryFixture.deleteAllBookings(teamUser2.id, teamUser2.email);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -642,4 +642,374 @@ describe("Bookings Endpoints 2024-08-13", () => {
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Recurring seated bookings creation", () => {
|
||||
let app: INestApplication;
|
||||
let organization: Team;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let bookingsRepositoryFixture: BookingsRepositoryFixture;
|
||||
let schedulesService: SchedulesService_2024_04_15;
|
||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
let oAuthClient: PlatformOAuthClient;
|
||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||
|
||||
const userEmail = "seated-bookings-controller-e2e@api.com";
|
||||
let user: User;
|
||||
|
||||
const seatedTventTypeSlug = "peer-coding-seated";
|
||||
const recurringSeatedTventTypeSlug = "peer-coding-recurring-seated";
|
||||
let seatedEventTypeId: number;
|
||||
let recurringSeatedEventTypeId: number;
|
||||
const maxRecurrenceCount = 3;
|
||||
|
||||
let createdSeatedBooking: CreateSeatedBookingOutput_2024_08_13;
|
||||
let createdRecurringSeatedBooking: CreateRecurringSeatedBookingOutput_2024_08_13[];
|
||||
|
||||
const emailAttendeeOne = "mr_proper_first@gmail.com";
|
||||
const nameAttendeeOne = "Mr Proper First";
|
||||
const emailAttendeeTwo = "mr_proper_second@gmail.com";
|
||||
const nameAttendeeTwo = "Mr Proper Second";
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
userEmail,
|
||||
Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
|
||||
})
|
||||
)
|
||||
.overrideGuard(PermissionsGuard)
|
||||
.useValue({
|
||||
canActivate: () => true,
|
||||
})
|
||||
.compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
|
||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
|
||||
|
||||
organization = await teamRepositoryFixture.create({ name: "organization bookings" });
|
||||
oAuthClient = await createOAuthClient(organization.id);
|
||||
|
||||
user = await userRepositoryFixture.create({
|
||||
email: userEmail,
|
||||
});
|
||||
|
||||
const userSchedule: CreateScheduleInput_2024_04_15 = {
|
||||
name: "working time",
|
||||
timeZone: "Europe/Rome",
|
||||
isDefault: true,
|
||||
};
|
||||
await schedulesService.createUserSchedule(user.id, userSchedule);
|
||||
const seatedEvent = await eventTypesRepositoryFixture.create(
|
||||
{
|
||||
title: "peer coding",
|
||||
slug: seatedTventTypeSlug,
|
||||
length: 60,
|
||||
seatsPerTimeSlot: 5,
|
||||
seatsShowAttendees: true,
|
||||
seatsShowAvailabilityCount: true,
|
||||
locations: [{ type: "inPerson", address: "via 10, rome, italy" }],
|
||||
},
|
||||
user.id
|
||||
);
|
||||
seatedEventTypeId = seatedEvent.id;
|
||||
|
||||
const recurringSeatedEvent = await eventTypesRepositoryFixture.create(
|
||||
// note(Lauris): freq 2 means weekly, interval 1 means every week and count 3 means 3 weeks in a row
|
||||
{
|
||||
title: "peer coding recurring",
|
||||
slug: recurringSeatedTventTypeSlug,
|
||||
length: 60,
|
||||
recurringEvent: { freq: 2, count: maxRecurrenceCount, interval: 1 },
|
||||
seatsPerTimeSlot: 5,
|
||||
seatsShowAttendees: true,
|
||||
seatsShowAvailabilityCount: true,
|
||||
locations: [{ type: "inPerson", address: "via 10, rome, italy" }],
|
||||
},
|
||||
user.id
|
||||
);
|
||||
recurringSeatedEventTypeId = recurringSeatedEvent.id;
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
async function createOAuthClient(organizationId: number) {
|
||||
const data = {
|
||||
logo: "logo-url",
|
||||
name: "name",
|
||||
redirectUris: ["http://localhost:5555"],
|
||||
permissions: 32,
|
||||
};
|
||||
const secret = "secret";
|
||||
|
||||
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
|
||||
return client;
|
||||
}
|
||||
|
||||
it("should book a recurring event type with seats for the first time", async () => {
|
||||
const recurrenceCount = 2;
|
||||
const body: CreateRecurringBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 11, 0, 0)).toISOString(),
|
||||
eventTypeId: recurringSeatedEventTypeId,
|
||||
attendee: {
|
||||
name: nameAttendeeOne,
|
||||
email: emailAttendeeOne,
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
bookingFieldsResponses: {
|
||||
codingLanguage: "TypeScript",
|
||||
},
|
||||
recurrenceCount,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseDataIsCreateRecurringSeatedBooking(responseBody.data)).toBe(true);
|
||||
|
||||
if (responseDataIsCreateRecurringSeatedBooking(responseBody.data)) {
|
||||
const data: CreateRecurringSeatedBookingOutput_2024_08_13[] = responseBody.data;
|
||||
expect(data.length).toEqual(recurrenceCount);
|
||||
|
||||
const firstBooking = data[0];
|
||||
expect(firstBooking.seatUid).toBeDefined();
|
||||
const seatUid = firstBooking.seatUid;
|
||||
expect(firstBooking.id).toBeDefined();
|
||||
expect(firstBooking.uid).toBeDefined();
|
||||
expect(firstBooking.hosts[0].id).toEqual(user.id);
|
||||
expect(firstBooking.status).toEqual("accepted");
|
||||
expect(firstBooking.start).toEqual(body.start);
|
||||
expect(firstBooking.end).toEqual(
|
||||
DateTime.fromISO(body.start, { zone: "utc" }).plus({ hours: 1 }).toISO()
|
||||
);
|
||||
expect(firstBooking.duration).toEqual(60);
|
||||
expect(firstBooking.eventTypeId).toEqual(recurringSeatedEventTypeId);
|
||||
expect(firstBooking.eventType).toEqual({
|
||||
id: recurringSeatedEventTypeId,
|
||||
slug: recurringSeatedTventTypeSlug,
|
||||
});
|
||||
expect(firstBooking.attendees.length).toEqual(1);
|
||||
expect(firstBooking.attendees[0]).toEqual({
|
||||
name: body.attendee.name,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
seatUid,
|
||||
bookingFieldsResponses: {
|
||||
name: body.attendee.name,
|
||||
...body.bookingFieldsResponses,
|
||||
},
|
||||
});
|
||||
expect(firstBooking.location).toEqual(body.location);
|
||||
expect(firstBooking.absentHost).toEqual(false);
|
||||
|
||||
const secondBooking = data[1];
|
||||
expect(secondBooking.seatUid).toBeDefined();
|
||||
const secondSeatUid = secondBooking.seatUid;
|
||||
expect(secondBooking.id).toBeDefined();
|
||||
expect(secondBooking.uid).toBeDefined();
|
||||
expect(secondBooking.hosts[0].id).toEqual(user.id);
|
||||
expect(secondBooking.status).toEqual("accepted");
|
||||
const expectedStart = DateTime.fromISO(body.start, { zone: "utc" }).plus({ weeks: 1 }).toISO();
|
||||
expect(secondBooking.start).toEqual(expectedStart);
|
||||
expect(secondBooking.end).toEqual(
|
||||
DateTime.fromISO(expectedStart!, { zone: "utc" }).plus({ hours: 1 }).toISO()
|
||||
);
|
||||
expect(secondBooking.duration).toEqual(60);
|
||||
expect(secondBooking.eventTypeId).toEqual(recurringSeatedEventTypeId);
|
||||
expect(secondBooking.eventType).toEqual({
|
||||
id: recurringSeatedEventTypeId,
|
||||
slug: recurringSeatedTventTypeSlug,
|
||||
});
|
||||
expect(secondBooking.attendees.length).toEqual(1);
|
||||
expect(secondBooking.attendees[0]).toEqual({
|
||||
name: body.attendee.name,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
seatUid: secondSeatUid,
|
||||
bookingFieldsResponses: {
|
||||
name: body.attendee.name,
|
||||
...body.bookingFieldsResponses,
|
||||
},
|
||||
});
|
||||
expect(secondBooking.location).toEqual(body.location);
|
||||
expect(secondBooking.absentHost).toEqual(false);
|
||||
createdRecurringSeatedBooking = data;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected recurring booking but received non array response"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should book a recurring event type with seats for the second time", async () => {
|
||||
const recurrenceCount = 2;
|
||||
const body: CreateRecurringBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 11, 0, 0)).toISOString(),
|
||||
eventTypeId: recurringSeatedEventTypeId,
|
||||
attendee: {
|
||||
name: nameAttendeeTwo,
|
||||
email: emailAttendeeTwo,
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
bookingFieldsResponses: {
|
||||
codingLanguage: "TypeScript",
|
||||
},
|
||||
recurrenceCount,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseDataIsCreateRecurringSeatedBooking(responseBody.data)).toBe(true);
|
||||
|
||||
if (responseDataIsCreateRecurringSeatedBooking(responseBody.data)) {
|
||||
const data: CreateRecurringSeatedBookingOutput_2024_08_13[] = responseBody.data;
|
||||
expect(data.length).toEqual(recurrenceCount);
|
||||
|
||||
const firstBooking = data[0];
|
||||
expect(firstBooking.seatUid).toBeDefined();
|
||||
const seatUid = firstBooking.seatUid;
|
||||
expect(firstBooking.id).toBeDefined();
|
||||
expect(firstBooking.uid).toBeDefined();
|
||||
expect(firstBooking.hosts[0].id).toEqual(user.id);
|
||||
expect(firstBooking.status).toEqual("accepted");
|
||||
expect(firstBooking.start).toEqual(body.start);
|
||||
expect(firstBooking.end).toEqual(
|
||||
DateTime.fromISO(body.start, { zone: "utc" }).plus({ hours: 1 }).toISO()
|
||||
);
|
||||
expect(firstBooking.duration).toEqual(60);
|
||||
expect(firstBooking.eventTypeId).toEqual(recurringSeatedEventTypeId);
|
||||
expect(firstBooking.eventType).toEqual({
|
||||
id: recurringSeatedEventTypeId,
|
||||
slug: recurringSeatedTventTypeSlug,
|
||||
});
|
||||
expect(firstBooking.attendees.length).toEqual(2);
|
||||
expect(firstBooking.attendees[0]).toEqual({
|
||||
name: nameAttendeeOne,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
seatUid: createdRecurringSeatedBooking[0].seatUid,
|
||||
bookingFieldsResponses: {
|
||||
name: nameAttendeeOne,
|
||||
...body.bookingFieldsResponses,
|
||||
},
|
||||
});
|
||||
expect(firstBooking.attendees[1]).toEqual({
|
||||
name: nameAttendeeTwo,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
seatUid,
|
||||
bookingFieldsResponses: {
|
||||
name: nameAttendeeTwo,
|
||||
...body.bookingFieldsResponses,
|
||||
},
|
||||
});
|
||||
expect(firstBooking.location).toEqual(body.location);
|
||||
expect(firstBooking.absentHost).toEqual(false);
|
||||
|
||||
const secondBooking = data[1];
|
||||
expect(secondBooking.seatUid).toBeDefined();
|
||||
const secondSeatUid = secondBooking.seatUid;
|
||||
expect(secondBooking.id).toBeDefined();
|
||||
expect(secondBooking.uid).toBeDefined();
|
||||
expect(secondBooking.hosts[0].id).toEqual(user.id);
|
||||
expect(secondBooking.status).toEqual("accepted");
|
||||
const expectedStart = DateTime.fromISO(body.start, { zone: "utc" }).plus({ weeks: 1 }).toISO();
|
||||
expect(secondBooking.start).toEqual(expectedStart);
|
||||
expect(secondBooking.end).toEqual(
|
||||
DateTime.fromISO(expectedStart!, { zone: "utc" }).plus({ hours: 1 }).toISO()
|
||||
);
|
||||
expect(secondBooking.duration).toEqual(60);
|
||||
expect(secondBooking.eventTypeId).toEqual(recurringSeatedEventTypeId);
|
||||
expect(secondBooking.eventType).toEqual({
|
||||
id: recurringSeatedEventTypeId,
|
||||
slug: recurringSeatedTventTypeSlug,
|
||||
});
|
||||
expect(secondBooking.attendees.length).toEqual(2);
|
||||
expect(secondBooking.attendees[0]).toEqual({
|
||||
name: nameAttendeeOne,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
seatUid: createdRecurringSeatedBooking[1].seatUid,
|
||||
bookingFieldsResponses: {
|
||||
name: nameAttendeeOne,
|
||||
...body.bookingFieldsResponses,
|
||||
},
|
||||
});
|
||||
expect(secondBooking.attendees[1]).toEqual({
|
||||
name: nameAttendeeTwo,
|
||||
timeZone: body.attendee.timeZone,
|
||||
language: body.attendee.language,
|
||||
absent: false,
|
||||
seatUid: secondSeatUid,
|
||||
bookingFieldsResponses: {
|
||||
name: nameAttendeeTwo,
|
||||
...body.bookingFieldsResponses,
|
||||
},
|
||||
});
|
||||
expect(secondBooking.location).toEqual(body.location);
|
||||
expect(secondBooking.absentHost).toEqual(false);
|
||||
createdRecurringSeatedBooking = data;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected recurring booking but received non array response"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
function responseDataIsCreateRecurringSeatedBooking(
|
||||
data: any
|
||||
): data is CreateRecurringSeatedBookingOutput_2024_08_13[] {
|
||||
return Array.isArray(data);
|
||||
}
|
||||
|
||||
function responseDataIsGetSeatedBooking(data: any): data is GetSeatedBookingOutput_2024_08_13 {
|
||||
return data?.attendees?.every((attendee: any) => attendee?.hasOwnProperty("seatUid"));
|
||||
}
|
||||
|
||||
function responseDataIsGetRecurringSeatedBooking(
|
||||
data: any
|
||||
): data is GetRecurringSeatedBookingOutput_2024_08_13[] {
|
||||
return Array.isArray(data);
|
||||
}
|
||||
|
||||
afterAll(async () => {
|
||||
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||
await teamRepositoryFixture.delete(organization.id);
|
||||
await userRepositoryFixture.deleteByEmail(user.email);
|
||||
await bookingsRepositoryFixture.deleteAllBookings(user.id, user.email);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+251
@@ -0,0 +1,251 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { CancelBookingOutput_2024_08_13 } from "@/ee/bookings/2024-08-13/outputs/cancel-booking.output";
|
||||
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 { 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";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import { EventType, User } from "@prisma/client";
|
||||
import * as request from "supertest";
|
||||
import { BookingsRepositoryFixture } from "test/fixtures/repository/bookings.repository.fixture";
|
||||
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
|
||||
import { CAL_API_VERSION_HEADER, SUCCESS_STATUS, VERSION_2024_08_13 } from "@calcom/platform-constants";
|
||||
import { CreateBookingInput_2024_08_13, BookingOutput_2024_08_13 } from "@calcom/platform-types";
|
||||
import { PlatformOAuthClient, Team } from "@calcom/prisma/client";
|
||||
|
||||
describe("Bookings Endpoints 2024-08-13", () => {
|
||||
describe("User bookings", () => {
|
||||
let app: INestApplication;
|
||||
let organization: Team;
|
||||
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let bookingsRepositoryFixture: BookingsRepositoryFixture;
|
||||
let schedulesService: SchedulesService_2024_04_15;
|
||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
let oAuthClient: PlatformOAuthClient;
|
||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||
|
||||
const userEmail = "bookings-controller-e2e@api.com";
|
||||
let user: User;
|
||||
|
||||
let variableLengthEventType: EventType;
|
||||
const variableLengthEventTypeSlug = "variable-length-event";
|
||||
let normalEventType: EventType;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
userEmail,
|
||||
Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, SchedulesModule_2024_04_15],
|
||||
})
|
||||
)
|
||||
.overrideGuard(PermissionsGuard)
|
||||
.useValue({
|
||||
canActivate: () => true,
|
||||
})
|
||||
.compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
bookingsRepositoryFixture = new BookingsRepositoryFixture(moduleRef);
|
||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
schedulesService = moduleRef.get<SchedulesService_2024_04_15>(SchedulesService_2024_04_15);
|
||||
|
||||
organization = await teamRepositoryFixture.create({ name: "organization bookings" });
|
||||
oAuthClient = await createOAuthClient(organization.id);
|
||||
|
||||
user = await userRepositoryFixture.create({
|
||||
email: userEmail,
|
||||
});
|
||||
|
||||
const userSchedule: CreateScheduleInput_2024_04_15 = {
|
||||
name: "working time",
|
||||
timeZone: "Europe/Rome",
|
||||
isDefault: true,
|
||||
};
|
||||
await schedulesService.createUserSchedule(user.id, userSchedule);
|
||||
|
||||
variableLengthEventType = await eventTypesRepositoryFixture.create(
|
||||
{
|
||||
title: "variable length event",
|
||||
slug: variableLengthEventTypeSlug,
|
||||
length: 15,
|
||||
metadata: { multipleDuration: [15, 30, 60] },
|
||||
},
|
||||
user.id
|
||||
);
|
||||
|
||||
normalEventType = await eventTypesRepositoryFixture.create(
|
||||
{ title: "normal event", slug: "normal-event", length: 15 },
|
||||
user.id
|
||||
);
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
async function createOAuthClient(organizationId: number) {
|
||||
const data = {
|
||||
logo: "logo-url",
|
||||
name: "name",
|
||||
redirectUris: ["http://localhost:5555"],
|
||||
permissions: 32,
|
||||
};
|
||||
const secret = "secret";
|
||||
|
||||
const client = await oauthClientRepositoryFixture.create(organizationId, data, secret);
|
||||
return client;
|
||||
}
|
||||
|
||||
it("should be defined", () => {
|
||||
expect(userRepositoryFixture).toBeDefined();
|
||||
expect(user).toBeDefined();
|
||||
});
|
||||
|
||||
describe("create bookings", () => {
|
||||
it("should not be able to specify length of booking for non variable length event type", async () => {
|
||||
const lengthInMinutes = 30;
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 10, 0, 0)).toISOString(),
|
||||
eventTypeId: normalEventType.id,
|
||||
lengthInMinutes,
|
||||
attendee: {
|
||||
name: "Mr Proper",
|
||||
email: "mr_proper@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
bookingFieldsResponses: {
|
||||
customField: "customValue",
|
||||
},
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it("should create a booking with default length", async () => {
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
start: new Date(Date.UTC(2030, 0, 8, 10, 0, 0)).toISOString(),
|
||||
eventTypeId: variableLengthEventType.id,
|
||||
attendee: {
|
||||
name: "Mr Proper",
|
||||
email: "mr_proper@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
bookingFieldsResponses: {
|
||||
customField: "customValue",
|
||||
},
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseDataIsBooking(responseBody.data)).toBe(true);
|
||||
|
||||
if (responseDataIsBooking(responseBody.data)) {
|
||||
const data: BookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.id).toBeDefined();
|
||||
expect(data.uid).toBeDefined();
|
||||
expect(data.status).toEqual("accepted");
|
||||
expect(data.start).toEqual(body.start);
|
||||
expect(data.end).toEqual(
|
||||
new Date(Date.UTC(2030, 0, 8, 10, variableLengthEventType.length, 0)).toISOString()
|
||||
);
|
||||
expect(data.duration).toEqual(variableLengthEventType.length);
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should create a booking with specified length that is not default length", async () => {
|
||||
const lengthInMinutes = 30;
|
||||
const body: CreateBookingInput_2024_08_13 = {
|
||||
lengthInMinutes,
|
||||
start: new Date(Date.UTC(2030, 0, 8, 11, 0, 0)).toISOString(),
|
||||
eventTypeId: variableLengthEventType.id,
|
||||
attendee: {
|
||||
name: "Mr Proper",
|
||||
email: "mr_proper@gmail.com",
|
||||
timeZone: "Europe/Rome",
|
||||
language: "it",
|
||||
},
|
||||
location: "https://meet.google.com/abc-def-ghi",
|
||||
bookingFieldsResponses: {
|
||||
customField: "customValue",
|
||||
},
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/v2/bookings")
|
||||
.send(body)
|
||||
.set(CAL_API_VERSION_HEADER, VERSION_2024_08_13)
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: CreateBookingOutput_2024_08_13 = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseDataIsBooking(responseBody.data)).toBe(true);
|
||||
|
||||
if (responseDataIsBooking(responseBody.data)) {
|
||||
const data: BookingOutput_2024_08_13 = responseBody.data;
|
||||
expect(data.id).toBeDefined();
|
||||
expect(data.uid).toBeDefined();
|
||||
expect(data.status).toEqual("accepted");
|
||||
expect(data.start).toEqual(body.start);
|
||||
expect(data.end).toEqual(new Date(Date.UTC(2030, 0, 8, 11, lengthInMinutes, 0)).toISOString());
|
||||
expect(data.duration).toEqual(lengthInMinutes);
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid response data - expected booking but received array of possibily recurring bookings"
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await oauthClientRepositoryFixture.delete(oAuthClient.id);
|
||||
await teamRepositoryFixture.delete(organization.id);
|
||||
await userRepositoryFixture.deleteByEmail(user.email);
|
||||
await bookingsRepositoryFixture.deleteAllBookings(user.id, user.email);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
|
||||
function responseDataIsBooking(data: any): data is BookingOutput_2024_08_13 {
|
||||
return !Array.isArray(data) && typeof data === "object" && data && "id" in data;
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import { ApiProperty, getSchemaPath } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsEnum, ValidateNested } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
import { ReassignBookingOutput_2024_08_13 as ReassignBookingOutputData_2024_08_13 } from "@calcom/platform-types";
|
||||
|
||||
export class ReassignBookingOutput_2024_08_13 {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@ApiProperty({
|
||||
oneOf: [{ $ref: getSchemaPath(ReassignBookingOutputData_2024_08_13) }],
|
||||
description:
|
||||
"Booking data, which can be either a ReassignAutoBookingOutput object or a ReassignManualBookingOutput object",
|
||||
})
|
||||
@ValidateNested()
|
||||
@Type(() => Object)
|
||||
data!: ReassignBookingOutputData_2024_08_13;
|
||||
}
|
||||
@@ -3,7 +3,10 @@ import { InputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/servic
|
||||
import { OutputBookingsService_2024_08_13 } from "@/ee/bookings/2024-08-13/services/output.service";
|
||||
import { EventTypesRepository_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/event-types.repository";
|
||||
import { BillingService } from "@/modules/billing/services/billing.service";
|
||||
import { BookingSeatRepository } from "@/modules/booking-seat/booking-seat.repository";
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { UsersService } from "@/modules/users/services/users.service";
|
||||
import { UsersRepository, UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { Injectable, Logger, NotFoundException } from "@nestjs/common";
|
||||
import { BadRequestException } from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
@@ -14,7 +17,10 @@ import {
|
||||
getAllUserBookings,
|
||||
handleInstantMeeting,
|
||||
handleCancelBooking,
|
||||
roundRobinReassignment,
|
||||
roundRobinManualReassignment,
|
||||
handleMarkNoShow,
|
||||
confirmBookingHandler,
|
||||
} from "@calcom/platform-libraries";
|
||||
import {
|
||||
CreateBookingInput_2024_08_13,
|
||||
@@ -22,8 +28,8 @@ import {
|
||||
CreateRecurringBookingInput_2024_08_13,
|
||||
GetBookingsInput_2024_08_13,
|
||||
CreateInstantBookingInput_2024_08_13,
|
||||
CancelBookingInput_2024_08_13,
|
||||
MarkAbsentBookingInput_2024_08_13,
|
||||
ReassignToUserBookingInput_2024_08_13,
|
||||
BookingOutput_2024_08_13,
|
||||
RecurringBookingOutput_2024_08_13,
|
||||
GetSeatedBookingOutput_2024_08_13,
|
||||
@@ -46,9 +52,12 @@ export class BookingsService_2024_08_13 {
|
||||
private readonly inputService: InputBookingsService_2024_08_13,
|
||||
private readonly outputService: OutputBookingsService_2024_08_13,
|
||||
private readonly bookingsRepository: BookingsRepository_2024_08_13,
|
||||
private readonly bookingSeatRepository: BookingSeatRepository,
|
||||
private readonly eventTypesRepository: EventTypesRepository_2024_06_14,
|
||||
private readonly prismaReadService: PrismaReadService,
|
||||
private readonly billingService: BillingService
|
||||
private readonly billingService: BillingService,
|
||||
private readonly usersService: UsersService,
|
||||
private readonly usersRepository: UsersRepository
|
||||
) {}
|
||||
|
||||
async createBooking(request: Request, body: CreateBookingInput) {
|
||||
@@ -107,7 +116,7 @@ export class BookingsService_2024_08_13 {
|
||||
const bookingRequest = await this.inputService.createRecurringBookingRequest(request, body);
|
||||
const bookings = await handleNewRecurringBooking(bookingRequest);
|
||||
return this.outputService.getOutputCreateRecurringSeatedBookings(
|
||||
bookings.map((booking) => ({ id: booking.id || 0, seatUid: booking.seatReferenceUid || "" }))
|
||||
bookings.map((booking) => ({ uid: booking.uid || "", seatUid: booking.seatReferenceUid || "" }))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -282,6 +291,20 @@ export class BookingsService_2024_08_13 {
|
||||
}
|
||||
|
||||
async cancelBooking(request: Request, bookingUid: string, body: CancelBookingInput) {
|
||||
if (this.inputService.isCancelSeatedBody(body)) {
|
||||
const seat = await this.bookingSeatRepository.getByReferenceUid(body.seatUid);
|
||||
|
||||
if (!seat) {
|
||||
throw new BadRequestException(
|
||||
"Invalid seatUid: this seat does not exist or has already been cancelled."
|
||||
);
|
||||
}
|
||||
|
||||
if (seat && bookingUid !== seat.booking.uid) {
|
||||
throw new BadRequestException("Invalid seatUid: this seat does not belong to this booking.");
|
||||
}
|
||||
}
|
||||
|
||||
const bookingRequest = await this.inputService.createCancelBookingRequest(request, bookingUid, body);
|
||||
await handleCancelBooking(bookingRequest);
|
||||
return this.getBooking(bookingUid);
|
||||
@@ -342,4 +365,95 @@ export class BookingsService_2024_08_13 {
|
||||
fromReschedule: oldBookingUid,
|
||||
});
|
||||
}
|
||||
|
||||
async reassignBooking(bookingUid: string, requestUser: UserWithProfile) {
|
||||
const booking = await this.bookingsRepository.getByUid(bookingUid);
|
||||
if (!booking) {
|
||||
throw new NotFoundException(`Booking with uid=${bookingUid} was not found in the database`);
|
||||
}
|
||||
|
||||
const profile = this.usersService.getUserMainProfile(requestUser);
|
||||
|
||||
await roundRobinReassignment({
|
||||
bookingId: booking.id,
|
||||
orgId: profile?.organizationId || null,
|
||||
});
|
||||
|
||||
const reassigned = await this.bookingsRepository.getByUidWithUser(bookingUid);
|
||||
if (!reassigned) {
|
||||
throw new NotFoundException(`Reassigned booking with uid=${bookingUid} was not found in the database`);
|
||||
}
|
||||
|
||||
return this.outputService.getOutputReassignedBooking(reassigned);
|
||||
}
|
||||
|
||||
async reassignBookingToUser(
|
||||
bookingUid: string,
|
||||
newUserId: number,
|
||||
reassignedById: number,
|
||||
body: ReassignToUserBookingInput_2024_08_13
|
||||
) {
|
||||
const booking = await this.bookingsRepository.getByUid(bookingUid);
|
||||
if (!booking) {
|
||||
throw new NotFoundException(`Booking with uid=${bookingUid} was not found in the database`);
|
||||
}
|
||||
|
||||
const user = await this.usersRepository.findByIdWithProfile(newUserId);
|
||||
if (!user) {
|
||||
throw new NotFoundException(`User with id=${newUserId} was not found in the database`);
|
||||
}
|
||||
|
||||
const profile = this.usersService.getUserMainProfile(user);
|
||||
|
||||
const reassigned = await roundRobinManualReassignment({
|
||||
bookingId: booking.id,
|
||||
newUserId,
|
||||
orgId: profile?.organizationId || null,
|
||||
reassignReason: body.reason,
|
||||
reassignedById,
|
||||
});
|
||||
|
||||
return this.outputService.getOutputReassignedBooking(reassigned);
|
||||
}
|
||||
|
||||
async confirmBooking(bookingUid: string, requestUser: UserWithProfile) {
|
||||
const booking = await this.bookingsRepository.getByUid(bookingUid);
|
||||
if (!booking) {
|
||||
throw new NotFoundException(`Booking with uid=${bookingUid} was not found in the database`);
|
||||
}
|
||||
|
||||
await confirmBookingHandler({
|
||||
ctx: {
|
||||
user: requestUser,
|
||||
},
|
||||
input: {
|
||||
bookingId: booking.id,
|
||||
confirmed: true,
|
||||
recurringEventId: booking.recurringEventId,
|
||||
},
|
||||
});
|
||||
|
||||
return this.getBooking(bookingUid);
|
||||
}
|
||||
|
||||
async declineBooking(bookingUid: string, requestUser: UserWithProfile, reason?: string) {
|
||||
const booking = await this.bookingsRepository.getByUid(bookingUid);
|
||||
if (!booking) {
|
||||
throw new NotFoundException(`Booking with uid=${bookingUid} was not found in the database`);
|
||||
}
|
||||
|
||||
await confirmBookingHandler({
|
||||
ctx: {
|
||||
user: requestUser,
|
||||
},
|
||||
input: {
|
||||
bookingId: booking.id,
|
||||
confirmed: false,
|
||||
recurringEventId: booking.recurringEventId,
|
||||
reason,
|
||||
},
|
||||
});
|
||||
|
||||
return this.getBooking(bookingUid);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import { v4 as uuidv4 } from "uuid";
|
||||
import { z } from "zod";
|
||||
|
||||
import { X_CAL_CLIENT_ID } from "@calcom/platform-constants";
|
||||
import { EventTypeMetaDataSchema } from "@calcom/platform-libraries";
|
||||
import {
|
||||
CancelBookingInput,
|
||||
CancelBookingInput_2024_08_13,
|
||||
@@ -32,6 +33,7 @@ import {
|
||||
RescheduleBookingInput_2024_08_13,
|
||||
RescheduleSeatedBookingInput_2024_08_13,
|
||||
} from "@calcom/platform-types";
|
||||
import { EventType } from "@calcom/prisma/client";
|
||||
|
||||
type BookingRequest = NextApiRequest & { userId: number | undefined } & OAuthRequestParams;
|
||||
|
||||
@@ -123,10 +125,13 @@ export class InputBookingsService_2024_08_13 {
|
||||
throw new NotFoundException(`Event type with id=${inputBooking.eventTypeId} not found`);
|
||||
}
|
||||
|
||||
this.validateBookingLengthInMinutes(inputBooking, eventType);
|
||||
|
||||
const lengthInMinutes = inputBooking.lengthInMinutes ?? eventType.length;
|
||||
const startTime = DateTime.fromISO(inputBooking.start, { zone: "utc" }).setZone(
|
||||
inputBooking.attendee.timeZone
|
||||
);
|
||||
const endTime = startTime.plus({ minutes: eventType.length });
|
||||
const endTime = startTime.plus({ minutes: lengthInMinutes });
|
||||
|
||||
return {
|
||||
start: startTime.toISO(),
|
||||
@@ -150,6 +155,25 @@ export class InputBookingsService_2024_08_13 {
|
||||
};
|
||||
}
|
||||
|
||||
validateBookingLengthInMinutes(inputBooking: CreateBookingInput_2024_08_13, eventType: EventType) {
|
||||
const eventTypeMetadata = EventTypeMetaDataSchema.parse(eventType.metadata);
|
||||
if (inputBooking.lengthInMinutes && !eventTypeMetadata?.multipleDuration) {
|
||||
throw new BadRequestException(
|
||||
"Can't specify 'lengthInMinutes' because event type does not have multiple possible lengths. Please, remove the 'lengthInMinutes' field from the request."
|
||||
);
|
||||
}
|
||||
if (
|
||||
inputBooking.lengthInMinutes &&
|
||||
!eventTypeMetadata?.multipleDuration?.includes(inputBooking.lengthInMinutes)
|
||||
) {
|
||||
throw new BadRequestException(
|
||||
`Provided 'lengthInMinutes' is not one of the possible lengths for the event type. The possible lengths are: ${eventTypeMetadata?.multipleDuration?.join(
|
||||
", "
|
||||
)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async createRecurringBookingRequest(
|
||||
request: Request,
|
||||
body: CreateRecurringBookingInput_2024_08_13
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
CreateSeatedBookingOutput_2024_08_13,
|
||||
GetRecurringSeatedBookingOutput_2024_08_13,
|
||||
GetSeatedBookingOutput_2024_08_13,
|
||||
ReassignBookingOutput_2024_08_13,
|
||||
RecurringBookingOutput_2024_08_13,
|
||||
SeatedAttendee,
|
||||
} from "@calcom/platform-types";
|
||||
@@ -50,6 +51,8 @@ type DatabaseBooking = Booking & {
|
||||
}[];
|
||||
} & { user: { id: number; name: string | null; email: string } | null };
|
||||
|
||||
type BookingWithUser = Booking & { user: { id: number; name: string | null; email: string } | null };
|
||||
|
||||
@Injectable()
|
||||
export class OutputBookingsService_2024_08_13 {
|
||||
constructor(private readonly bookingsRepository: BookingsRepository_2024_08_13) {}
|
||||
@@ -230,14 +233,14 @@ export class OutputBookingsService_2024_08_13 {
|
||||
return transformed.sort((a, b) => new Date(a.start).getTime() - new Date(b.start).getTime());
|
||||
}
|
||||
|
||||
async getOutputCreateRecurringSeatedBookings(bookings: { id: number; seatUid: string }[]) {
|
||||
async getOutputCreateRecurringSeatedBookings(bookings: { uid: string; seatUid: string }[]) {
|
||||
const transformed = [];
|
||||
|
||||
for (const booking of bookings) {
|
||||
const databaseBooking =
|
||||
await this.bookingsRepository.getByIdWithAttendeesWithBookingSeatAndUserAndEvent(booking.id);
|
||||
await this.bookingsRepository.getByUidWithAttendeesWithBookingSeatAndUserAndEvent(booking.uid);
|
||||
if (!databaseBooking) {
|
||||
throw new Error(`Booking with id=${booking.id} was not found in the database`);
|
||||
throw new Error(`Booking with uid=${booking.uid} was not found in the database`);
|
||||
}
|
||||
transformed.push(this.getOutputCreateRecurringSeatedBooking(databaseBooking, booking.seatUid));
|
||||
}
|
||||
@@ -308,4 +311,17 @@ export class OutputBookingsService_2024_08_13 {
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
getOutputReassignedBooking(
|
||||
databaseBooking: Pick<BookingWithUser, "uid" | "user">
|
||||
): ReassignBookingOutput_2024_08_13 {
|
||||
return {
|
||||
bookingUid: databaseBooking.uid,
|
||||
reassignedTo: {
|
||||
id: databaseBooking?.user?.id || 0,
|
||||
name: databaseBooking?.user?.name || "unknown",
|
||||
email: databaseBooking?.user?.email || "unknown",
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+8
@@ -212,6 +212,7 @@ describe("Event types Endpoints", () => {
|
||||
slug: "coding-class",
|
||||
description: "Let's learn how to code like a pro.",
|
||||
lengthInMinutes: 60,
|
||||
lengthInMinutesOptions: [30, 60, 90],
|
||||
locations: [
|
||||
{
|
||||
type: "integration",
|
||||
@@ -300,6 +301,7 @@ describe("Event types Endpoints", () => {
|
||||
expect(createdEventType.title).toEqual(body.title);
|
||||
expect(createdEventType.description).toEqual(body.description);
|
||||
expect(createdEventType.lengthInMinutes).toEqual(body.lengthInMinutes);
|
||||
expect(createdEventType.lengthInMinutesOptions).toEqual(body.lengthInMinutesOptions);
|
||||
expect(createdEventType.locations).toEqual(body.locations);
|
||||
expect(createdEventType.ownerId).toEqual(user.id);
|
||||
expect(createdEventType.scheduleId).toEqual(firstSchedule.id);
|
||||
@@ -360,6 +362,7 @@ describe("Event types Endpoints", () => {
|
||||
expect(fetchedEventType?.title).toEqual(eventType.title);
|
||||
expect(fetchedEventType?.description).toEqual(eventType.description);
|
||||
expect(fetchedEventType?.lengthInMinutes).toEqual(eventType.lengthInMinutes);
|
||||
expect(fetchedEventType?.lengthInMinutesOptions).toEqual(eventType.lengthInMinutesOptions);
|
||||
expect(fetchedEventType?.locations).toEqual(eventType.locations);
|
||||
expect(fetchedEventType?.bookingFields).toEqual(eventType.bookingFields);
|
||||
expect(fetchedEventType?.ownerId).toEqual(user.id);
|
||||
@@ -665,6 +668,7 @@ describe("Event types Endpoints", () => {
|
||||
const body: UpdateEventTypeInput_2024_06_14 = {
|
||||
title: newTitle,
|
||||
scheduleId: secondSchedule.id,
|
||||
lengthInMinutesOptions: [15, 30],
|
||||
bookingFields: [
|
||||
nameBookingField,
|
||||
{
|
||||
@@ -733,6 +737,7 @@ describe("Event types Endpoints", () => {
|
||||
|
||||
expect(updatedEventType.id).toEqual(eventType.id);
|
||||
expect(updatedEventType.title).toEqual(newTitle);
|
||||
expect(updatedEventType.lengthInMinutesOptions).toEqual(body.lengthInMinutesOptions);
|
||||
expect(updatedEventType.description).toEqual(eventType.description);
|
||||
expect(updatedEventType.lengthInMinutes).toEqual(eventType.lengthInMinutes);
|
||||
expect(updatedEventType.locations).toEqual(eventType.locations);
|
||||
@@ -772,6 +777,7 @@ describe("Event types Endpoints", () => {
|
||||
|
||||
eventType.title = newTitle;
|
||||
eventType.scheduleId = secondSchedule.id;
|
||||
eventType.lengthInMinutesOptions = updatedEventType.lengthInMinutesOptions;
|
||||
eventType.bookingLimitsCount = updatedEventType.bookingLimitsCount;
|
||||
eventType.onlyShowFirstAvailableSlot = updatedEventType.onlyShowFirstAvailableSlot;
|
||||
eventType.bookingLimitsDuration = updatedEventType.bookingLimitsDuration;
|
||||
@@ -819,6 +825,7 @@ describe("Event types Endpoints", () => {
|
||||
expect(fetchedEventType.title).toEqual(eventType.title);
|
||||
expect(fetchedEventType.description).toEqual(eventType.description);
|
||||
expect(fetchedEventType.lengthInMinutes).toEqual(eventType.lengthInMinutes);
|
||||
expect(fetchedEventType.lengthInMinutesOptions).toEqual(eventType.lengthInMinutesOptions);
|
||||
expect(fetchedEventType.locations).toEqual(eventType.locations);
|
||||
expect(fetchedEventType.bookingFields).toEqual(eventType.bookingFields);
|
||||
expect(fetchedEventType.ownerId).toEqual(user.id);
|
||||
@@ -857,6 +864,7 @@ describe("Event types Endpoints", () => {
|
||||
expect(fetchedEventType?.title).toEqual(eventType.title);
|
||||
expect(fetchedEventType?.description).toEqual(eventType.description);
|
||||
expect(fetchedEventType?.lengthInMinutes).toEqual(eventType.lengthInMinutes);
|
||||
expect(fetchedEventType?.lengthInMinutesOptions).toEqual(eventType.lengthInMinutesOptions);
|
||||
expect(fetchedEventType?.locations).toEqual(eventType.locations);
|
||||
expect(fetchedEventType?.bookingFields).toEqual(eventType.bookingFields);
|
||||
expect(fetchedEventType?.ownerId).toEqual(user.id);
|
||||
|
||||
+4
@@ -104,6 +104,7 @@ export class InputEventTypesService_2024_06_14 {
|
||||
|
||||
const {
|
||||
lengthInMinutes,
|
||||
lengthInMinutesOptions,
|
||||
locations,
|
||||
bookingFields,
|
||||
bookingLimitsCount,
|
||||
@@ -135,6 +136,7 @@ export class InputEventTypesService_2024_06_14 {
|
||||
bookerLayouts: this.transformInputBookerLayouts(bookerLayouts),
|
||||
requiresConfirmationThreshold:
|
||||
confirmationPolicyTransformed?.requiresConfirmationThreshold ?? undefined,
|
||||
multipleDuration: lengthInMinutesOptions,
|
||||
},
|
||||
requiresConfirmation: confirmationPolicyTransformed?.requiresConfirmation ?? undefined,
|
||||
requiresConfirmationWillBlockSlot:
|
||||
@@ -152,6 +154,7 @@ export class InputEventTypesService_2024_06_14 {
|
||||
async transformInputUpdateEventType(inputEventType: UpdateEventTypeInput_2024_06_14, eventTypeId: number) {
|
||||
const {
|
||||
lengthInMinutes,
|
||||
lengthInMinutesOptions,
|
||||
locations,
|
||||
bookingFields,
|
||||
bookingLimitsCount,
|
||||
@@ -191,6 +194,7 @@ export class InputEventTypesService_2024_06_14 {
|
||||
bookerLayouts: this.transformInputBookerLayouts(bookerLayouts),
|
||||
requiresConfirmationThreshold:
|
||||
confirmationPolicyTransformed?.requiresConfirmationThreshold ?? undefined,
|
||||
multipleDuration: lengthInMinutesOptions,
|
||||
},
|
||||
recurringEvent: recurrence ? this.transformInputRecurrignEvent(recurrence) : undefined,
|
||||
requiresConfirmation: confirmationPolicyTransformed?.requiresConfirmation ?? undefined,
|
||||
|
||||
+1
@@ -153,6 +153,7 @@ export class OutputEventTypesService_2024_06_14 {
|
||||
id,
|
||||
ownerId,
|
||||
lengthInMinutes: length,
|
||||
lengthInMinutesOptions: metadata.multipleDuration,
|
||||
title,
|
||||
slug,
|
||||
description: description || "",
|
||||
|
||||
@@ -10,6 +10,8 @@ export type Environment = {
|
||||
DATABASE_URL: string;
|
||||
JWT_SECRET: string;
|
||||
SENTRY_DSN: string;
|
||||
SENTRY_TRACES_SAMPLE_RATE?: number;
|
||||
SENTRY_PROFILES_SAMPLE_RATE?: number;
|
||||
LOG_LEVEL: keyof typeof logLevels;
|
||||
REDIS_URL: string;
|
||||
STRIPE_API_KEY: string;
|
||||
@@ -26,6 +28,8 @@ export type Environment = {
|
||||
RATE_LIMIT_DEFAULT_LIMIT_ACCESS_TOKEN: number;
|
||||
RATE_LIMIT_DEFAULT_LIMIT: number;
|
||||
RATE_LIMIT_DEFAULT_BLOCK_DURATION_MS: number;
|
||||
AXIOM_DATASET: string;
|
||||
AXIOM_TOKEN: string;
|
||||
};
|
||||
|
||||
export const getEnv = <K extends keyof Environment>(key: K, fallback?: Environment[K]): Environment[K] => {
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import { getEnv } from "@/env";
|
||||
import * as Sentry from "@sentry/node";
|
||||
import * as Sentry from "@sentry/nestjs";
|
||||
import { nodeProfilingIntegration } from "@sentry/profiling-node";
|
||||
|
||||
if (process.env.SENTRY_DSN) {
|
||||
// Ensure to call this before requiring any other modules!
|
||||
Sentry.init({
|
||||
dsn: getEnv("SENTRY_DSN"),
|
||||
// Add Performance Monitoring by setting tracesSampleRate
|
||||
// We recommend adjusting this value in production
|
||||
// todo: Evaluate? tracesSampleRate: 1.0
|
||||
integrations: [nodeProfilingIntegration()],
|
||||
// Performance Monitoring
|
||||
tracesSampleRate: getEnv("SENTRY_TRACES_SAMPLE_RATE") ?? 1.0, // Capture 100% of the transactions
|
||||
// Set sampling rate for profiling - this is relative to tracesSampleRate
|
||||
profilesSampleRate: getEnv("SENTRY_PROFILES_SAMPLE_RATE") ?? 1.0,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { WinstonTransport as AxiomTransport } from "@axiomhq/winston";
|
||||
import type { LoggerOptions } from "winston";
|
||||
import { format, transports } from "winston";
|
||||
import { format, transports as Transports } from "winston";
|
||||
import type Transport from "winston-transport";
|
||||
|
||||
const formattedTimestamp = format.timestamp({
|
||||
format: "YYYY-MM-DD HH:mm:ss.SSS",
|
||||
@@ -36,13 +38,25 @@ export const logLevels = {
|
||||
export const loggerConfig = (): LoggerOptions => {
|
||||
const isProduction = process.env.NODE_ENV === "production";
|
||||
|
||||
const transports: Transport[] = [];
|
||||
transports.push(new Transports.Console());
|
||||
|
||||
if (!!process.env.AXIOM_TOKEN && !!process.env.AXIOM_DATASET) {
|
||||
transports.push(
|
||||
new AxiomTransport({
|
||||
token: process.env.AXIOM_TOKEN,
|
||||
dataset: process.env.AXIOM_DATASET,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
levels: logLevels,
|
||||
level: process.env.LOG_LEVEL ?? "info",
|
||||
format: isProduction ? WINSTON_PROD_FORMAT : WINSTON_DEV_FORMAT,
|
||||
transports: [new transports.Console()],
|
||||
exceptionHandlers: [new transports.Console()],
|
||||
rejectionHandlers: [new transports.Console()],
|
||||
transports,
|
||||
exceptionHandlers: transports,
|
||||
rejectionHandlers: transports,
|
||||
defaultMeta: {
|
||||
service: "cal-platform-api",
|
||||
},
|
||||
|
||||
+12
-4
@@ -1,4 +1,5 @@
|
||||
import type { AppConfig } from "@/config/type";
|
||||
import { getEnv } from "@/env";
|
||||
import { Logger } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { NestFactory } from "@nestjs/core";
|
||||
@@ -107,13 +108,20 @@ async function generateSwagger(app: NestExpressApplication<Server>) {
|
||||
const document = SwaggerModule.createDocument(app, config);
|
||||
document.paths = groupAndSortPathsByFirstTag(document.paths);
|
||||
|
||||
const outputFile = "./swagger/documentation.json";
|
||||
const swaggerOutputFile = "./swagger/documentation.json";
|
||||
const docsOutputFile = "../../../docs/api-reference/v2/openapi.json";
|
||||
const stringifiedContents = JSON.stringify(document, null, 2);
|
||||
|
||||
if (fs.existsSync(outputFile)) {
|
||||
fs.unlinkSync(outputFile);
|
||||
if (fs.existsSync(swaggerOutputFile)) {
|
||||
fs.unlinkSync(swaggerOutputFile);
|
||||
}
|
||||
|
||||
fs.writeFileSync(outputFile, JSON.stringify(document, null, 2), { encoding: "utf8" });
|
||||
fs.writeFileSync(swaggerOutputFile, stringifiedContents, { encoding: "utf8" });
|
||||
|
||||
if (fs.existsSync(docsOutputFile) && getEnv("NODE_ENV") === "development") {
|
||||
fs.unlinkSync(docsOutputFile);
|
||||
fs.writeFileSync(docsOutputFile, stringifiedContents, { encoding: "utf8" });
|
||||
}
|
||||
|
||||
if (!process.env.DOCS_URL) {
|
||||
SwaggerModule.setup("docs", app, document, {
|
||||
|
||||
@@ -46,9 +46,9 @@ export class BillingProcessor {
|
||||
return;
|
||||
}
|
||||
|
||||
const stripeSubscription = await this.stripeService.stripe.subscriptions.retrieve(
|
||||
billingSubscription.subscriptionId
|
||||
);
|
||||
const stripeSubscription = await this.stripeService
|
||||
.getStripe()
|
||||
.subscriptions.retrieve(billingSubscription.subscriptionId);
|
||||
if (!stripeSubscription?.id) {
|
||||
this.logger.error(`Failed to retrieve stripe subscription (${billingSubscription.subscriptionId})`, {
|
||||
teamId,
|
||||
@@ -69,7 +69,7 @@ export class BillingProcessor {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.stripeService.stripe.subscriptionItems.createUsageRecord(meteredItem.id, {
|
||||
await this.stripeService.getStripe().subscriptionItems.createUsageRecord(meteredItem.id, {
|
||||
action: "increment",
|
||||
quantity: 1,
|
||||
timestamp: "now",
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { PlatformPlan } from "@/modules/billing/types";
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { Injectable, Logger } from "@nestjs/common";
|
||||
|
||||
@Injectable()
|
||||
export class BillingRepository {
|
||||
private readonly logger = new Logger("BillingRepository");
|
||||
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||
|
||||
getBillingForTeam = (teamId: number) =>
|
||||
@@ -30,6 +31,35 @@ export class BillingRepository {
|
||||
billingCycleEnd: billingEnd,
|
||||
subscriptionId,
|
||||
plan: plan.toString(),
|
||||
overdue: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async updateBillingOverdue(subId: string, cusId: string, overdue: boolean) {
|
||||
try {
|
||||
return this.dbWrite.prisma.platformBilling.update({
|
||||
where: {
|
||||
subscriptionId: subId,
|
||||
customerId: cusId,
|
||||
},
|
||||
data: {
|
||||
overdue,
|
||||
},
|
||||
});
|
||||
} catch (err) {
|
||||
this.logger.error("Could not update billing overdue", {
|
||||
subId,
|
||||
cusId,
|
||||
err,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async deleteBilling(id: number) {
|
||||
return this.dbWrite.prisma.platformBilling.delete({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,211 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { StripeService } from "@/modules/stripe/stripe.service";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import Stripe from "stripe";
|
||||
import * as request from "supertest";
|
||||
import { PlatformBillingRepositoryFixture } from "test/fixtures/repository/billing.repository.fixture";
|
||||
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||
import { OAuthClientRepositoryFixture } from "test/fixtures/repository/oauth-client.repository.fixture";
|
||||
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
|
||||
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { withApiAuth } from "test/utils/withApiAuth";
|
||||
|
||||
import { Team, PlatformOAuthClient, PlatformBilling } from "@calcom/prisma/client";
|
||||
|
||||
describe("Platform Billing Controller (e2e)", () => {
|
||||
let app: INestApplication;
|
||||
const userEmail = "platform-billing-controller-e2e@api.com";
|
||||
let user: UserWithProfile;
|
||||
let billing: PlatformBilling;
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
||||
let profileRepositoryFixture: ProfileRepositoryFixture;
|
||||
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||
let oauthClientRepositoryFixture: OAuthClientRepositoryFixture;
|
||||
let oAuthClient: PlatformOAuthClient;
|
||||
let platformBillingRepositoryFixture: PlatformBillingRepositoryFixture;
|
||||
let organization: Team;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
userEmail,
|
||||
Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
|
||||
})
|
||||
).compile();
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
|
||||
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
||||
oauthClientRepositoryFixture = new OAuthClientRepositoryFixture(moduleRef);
|
||||
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||
platformBillingRepositoryFixture = new PlatformBillingRepositoryFixture(moduleRef);
|
||||
organization = await organizationsRepositoryFixture.create({ name: "platform billing" });
|
||||
|
||||
user = await userRepositoryFixture.create({
|
||||
email: userEmail,
|
||||
username: userEmail,
|
||||
});
|
||||
|
||||
await profileRepositoryFixture.create({
|
||||
uid: `usr-${user.id}`,
|
||||
username: userEmail,
|
||||
organization: {
|
||||
connect: {
|
||||
id: organization.id,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
connect: {
|
||||
id: user.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "OWNER",
|
||||
team: { connect: { id: organization.id } },
|
||||
user: { connect: { id: user.id } },
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
billing = await platformBillingRepositoryFixture.create(organization.id);
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
userRepositoryFixture.deleteByEmail(user.email);
|
||||
await app.close();
|
||||
});
|
||||
|
||||
it("/billing/webhook (POST) should set billing free plan for org", () => {
|
||||
jest.spyOn(StripeService.prototype, "getStripe").mockImplementation(
|
||||
() =>
|
||||
({
|
||||
webhooks: {
|
||||
constructEventAsync: async () => {
|
||||
return {
|
||||
type: "checkout.session.completed",
|
||||
data: {
|
||||
object: {
|
||||
metadata: {
|
||||
teamId: organization.id,
|
||||
plan: "FREE",
|
||||
},
|
||||
mode: "subscription",
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
} as unknown as Stripe)
|
||||
);
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/v2/billing/webhook")
|
||||
.expect(200)
|
||||
.then(async (res) => {
|
||||
const billing = await platformBillingRepositoryFixture.get(organization.id);
|
||||
expect(billing?.plan).toEqual("FREE");
|
||||
});
|
||||
});
|
||||
it("/billing/webhook (POST) failed payment should set billing free plan to overdue", () => {
|
||||
jest.spyOn(StripeService.prototype, "getStripe").mockImplementation(
|
||||
() =>
|
||||
({
|
||||
webhooks: {
|
||||
constructEventAsync: async () => {
|
||||
return {
|
||||
type: "invoice.payment_failed",
|
||||
data: {
|
||||
object: {
|
||||
customer: billing?.customerId,
|
||||
subscription: billing?.subscriptionId,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
} as unknown as Stripe)
|
||||
);
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/v2/billing/webhook")
|
||||
.expect(200)
|
||||
.then(async (res) => {
|
||||
const billing = await platformBillingRepositoryFixture.get(organization.id);
|
||||
expect(billing?.overdue).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
it("/billing/webhook (POST) success payment should set billing free plan to not overdue", () => {
|
||||
jest.spyOn(StripeService.prototype, "getStripe").mockImplementation(
|
||||
() =>
|
||||
({
|
||||
webhooks: {
|
||||
constructEventAsync: async () => {
|
||||
return {
|
||||
type: "invoice.payment_succeeded",
|
||||
data: {
|
||||
object: {
|
||||
customer: billing?.customerId,
|
||||
subscription: billing?.subscriptionId,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
} as unknown as Stripe)
|
||||
);
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/v2/billing/webhook")
|
||||
.expect(200)
|
||||
.then(async (res) => {
|
||||
const billing = await platformBillingRepositoryFixture.get(organization.id);
|
||||
expect(billing?.overdue).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
it("/billing/webhook (POST) should delete subscription", () => {
|
||||
jest.spyOn(StripeService.prototype, "getStripe").mockImplementation(
|
||||
() =>
|
||||
({
|
||||
webhooks: {
|
||||
constructEventAsync: async () => {
|
||||
return {
|
||||
type: "customer.subscription.deleted",
|
||||
data: {
|
||||
object: {
|
||||
metadata: {
|
||||
teamId: organization.id,
|
||||
plan: "FREE",
|
||||
},
|
||||
id: billing?.subscriptionId,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
},
|
||||
} as unknown as Stripe)
|
||||
);
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/v2/billing/webhook")
|
||||
.expect(200)
|
||||
.then(async (res) => {
|
||||
const billing = await platformBillingRepositoryFixture.get(organization.id);
|
||||
expect(billing).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -102,13 +102,26 @@ export class BillingController {
|
||||
@Req() request: Request,
|
||||
@Headers("stripe-signature") stripeSignature: string
|
||||
): Promise<ApiResponse> {
|
||||
const event = await this.billingService.stripeService.stripe.webhooks.constructEventAsync(
|
||||
request.body,
|
||||
stripeSignature,
|
||||
this.stripeWhSecret
|
||||
);
|
||||
const event = await this.billingService.stripeService
|
||||
.getStripe()
|
||||
.webhooks.constructEventAsync(request.body, stripeSignature, this.stripeWhSecret);
|
||||
|
||||
await this.billingService.createOrUpdateStripeSubscription(event);
|
||||
switch (event.type) {
|
||||
case "checkout.session.completed":
|
||||
await this.billingService.handleStripeCheckoutEvents(event);
|
||||
break;
|
||||
case "customer.subscription.deleted":
|
||||
await this.billingService.handleStripeSubscriptionDeleted(event);
|
||||
break;
|
||||
case "invoice.payment_failed":
|
||||
await this.billingService.handleStripePaymentFailed(event);
|
||||
break;
|
||||
case "invoice.payment_succeeded":
|
||||
await this.billingService.handleStripePaymentSuccess(event);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return {
|
||||
status: "success",
|
||||
|
||||
@@ -64,7 +64,7 @@ export class BillingService implements OnModuleDestroy {
|
||||
}
|
||||
|
||||
async redirectToSubscribeCheckout(teamId: number, plan: PlatformPlan, customerId?: string) {
|
||||
const { url } = await this.stripeService.stripe.checkout.sessions.create({
|
||||
const { url } = await this.stripeService.getStripe().checkout.sessions.create({
|
||||
customer: customerId,
|
||||
line_items: [
|
||||
{
|
||||
@@ -101,7 +101,7 @@ export class BillingService implements OnModuleDestroy {
|
||||
const teamWithBilling = await this.teamsRepository.findByIdIncludeBilling(teamId);
|
||||
const customerId = teamWithBilling?.platformBilling?.customerId;
|
||||
|
||||
const { url } = await this.stripeService.stripe.checkout.sessions.create({
|
||||
const { url } = await this.stripeService.getStripe().checkout.sessions.create({
|
||||
customer: customerId,
|
||||
success_url: `${this.webAppUrl}/settings/platform/`,
|
||||
cancel_url: `${this.webAppUrl}/settings/platform/plans`,
|
||||
@@ -131,41 +131,92 @@ export class BillingService implements OnModuleDestroy {
|
||||
);
|
||||
}
|
||||
|
||||
async createOrUpdateStripeSubscription(event: Stripe.Event) {
|
||||
if (event.type === "checkout.session.completed") {
|
||||
const subscription = event.data.object as Stripe.Checkout.Session;
|
||||
|
||||
if (!subscription.metadata?.teamId) {
|
||||
return {
|
||||
status: "success",
|
||||
};
|
||||
}
|
||||
|
||||
const teamId = Number.parseInt(subscription.metadata.teamId);
|
||||
const plan = subscription.metadata.plan;
|
||||
if (!plan || !teamId) {
|
||||
this.logger.log("Webhook received but not pertaining to Platform, discarding.");
|
||||
return {
|
||||
status: "success",
|
||||
};
|
||||
}
|
||||
|
||||
if (subscription.mode === "subscription") {
|
||||
await this.setSubscriptionForTeam(
|
||||
async handleStripeSubscriptionDeleted(event: Stripe.Event) {
|
||||
const subscription = event.data.object as Stripe.Subscription;
|
||||
const teamId = subscription?.metadata?.teamId;
|
||||
const plan = PlatformPlan[subscription?.metadata?.plan?.toUpperCase() as keyof typeof PlatformPlan];
|
||||
if (teamId && plan) {
|
||||
const currentBilling = await this.billingRepository.getBillingForTeam(Number.parseInt(teamId));
|
||||
if (currentBilling?.subscriptionId === subscription.id) {
|
||||
await this.billingRepository.deleteBilling(currentBilling.id);
|
||||
this.logger.log(`Stripe Subscription deleted`, {
|
||||
customerId: currentBilling.customerId,
|
||||
subscriptionId: currentBilling.subscriptionId,
|
||||
teamId,
|
||||
subscription.subscription as string,
|
||||
PlatformPlan[plan.toUpperCase() as keyof typeof PlatformPlan]
|
||||
);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (subscription.mode === "setup") {
|
||||
await this.updateStripeSubscriptionForTeam(teamId, plan as PlatformPlan);
|
||||
}
|
||||
|
||||
return {
|
||||
status: "success",
|
||||
};
|
||||
this.logger.log("No platform billing found.");
|
||||
return;
|
||||
}
|
||||
this.logger.log("Webhook received but not pertaining to Platform, discarding.");
|
||||
return;
|
||||
}
|
||||
|
||||
getSubscriptionIdFromInvoice(invoice: Stripe.Invoice): string | null {
|
||||
if (typeof invoice.subscription === "string") {
|
||||
return invoice.subscription;
|
||||
} else if (invoice.subscription && typeof invoice.subscription === "object") {
|
||||
return invoice.subscription.id;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
getCustomerIdFromInvoice(invoice: Stripe.Invoice): string | null {
|
||||
if (typeof invoice.customer === "string") {
|
||||
return invoice.customer;
|
||||
} else if (invoice.customer && typeof invoice.customer === "object") {
|
||||
return invoice.customer.id;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async handleStripePaymentSuccess(event: Stripe.Event) {
|
||||
const invoice = event.data.object as Stripe.Invoice;
|
||||
const subscriptionId = this.getSubscriptionIdFromInvoice(invoice);
|
||||
const customerId = this.getCustomerIdFromInvoice(invoice);
|
||||
if (subscriptionId && customerId) {
|
||||
await this.billingRepository.updateBillingOverdue(subscriptionId, customerId, false);
|
||||
}
|
||||
}
|
||||
|
||||
async handleStripePaymentFailed(event: Stripe.Event) {
|
||||
const invoice = event.data.object as Stripe.Invoice;
|
||||
const subscriptionId = this.getSubscriptionIdFromInvoice(invoice);
|
||||
const customerId = this.getCustomerIdFromInvoice(invoice);
|
||||
if (subscriptionId && customerId) {
|
||||
await this.billingRepository.updateBillingOverdue(subscriptionId, customerId, true);
|
||||
}
|
||||
}
|
||||
|
||||
async handleStripeCheckoutEvents(event: Stripe.Event) {
|
||||
const checkoutSession = event.data.object as Stripe.Checkout.Session;
|
||||
|
||||
if (!checkoutSession.metadata?.teamId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const teamId = Number.parseInt(checkoutSession.metadata.teamId);
|
||||
const plan = checkoutSession.metadata.plan;
|
||||
if (!plan || !teamId) {
|
||||
this.logger.log("Webhook received but not pertaining to Platform, discarding.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (checkoutSession.mode === "subscription") {
|
||||
await this.setSubscriptionForTeam(
|
||||
teamId,
|
||||
checkoutSession.subscription as string,
|
||||
PlatformPlan[plan.toUpperCase() as keyof typeof PlatformPlan]
|
||||
);
|
||||
}
|
||||
|
||||
if (checkoutSession.mode === "setup") {
|
||||
await this.updateStripeSubscriptionForTeam(teamId, plan as PlatformPlan);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
async updateStripeSubscriptionForTeam(teamId: number, plan: PlatformPlan) {
|
||||
@@ -175,9 +226,9 @@ export class BillingService implements OnModuleDestroy {
|
||||
throw new NotFoundException("Team plan not found");
|
||||
}
|
||||
|
||||
const existingUserSubscription = await this.stripeService.stripe.subscriptions.retrieve(
|
||||
teamWithBilling?.platformBilling?.subscriptionId
|
||||
);
|
||||
const existingUserSubscription = await this.stripeService
|
||||
.getStripe()
|
||||
.subscriptions.retrieve(teamWithBilling?.platformBilling?.subscriptionId);
|
||||
const currentLicensedItem = existingUserSubscription.items.data.find(
|
||||
(item) => item.price?.recurring?.usage_type === "licensed"
|
||||
);
|
||||
@@ -193,21 +244,23 @@ export class BillingService implements OnModuleDestroy {
|
||||
throw new NotFoundException("There is no overage item present in the subscription");
|
||||
}
|
||||
|
||||
await this.stripeService.stripe.subscriptions.update(teamWithBilling?.platformBilling?.subscriptionId, {
|
||||
items: [
|
||||
{
|
||||
id: currentLicensedItem.id,
|
||||
price: this.billingConfigService.get(plan)?.base,
|
||||
},
|
||||
{
|
||||
id: currentOverageItem.id,
|
||||
price: this.billingConfigService.get(plan)?.overage,
|
||||
clear_usage: false,
|
||||
},
|
||||
],
|
||||
billing_cycle_anchor: "now",
|
||||
proration_behavior: "create_prorations",
|
||||
});
|
||||
await this.stripeService
|
||||
.getStripe()
|
||||
.subscriptions.update(teamWithBilling?.platformBilling?.subscriptionId, {
|
||||
items: [
|
||||
{
|
||||
id: currentLicensedItem.id,
|
||||
price: this.billingConfigService.get(plan)?.base,
|
||||
},
|
||||
{
|
||||
id: currentOverageItem.id,
|
||||
price: this.billingConfigService.get(plan)?.overage,
|
||||
clear_usage: false,
|
||||
},
|
||||
],
|
||||
billing_cycle_anchor: "now",
|
||||
proration_behavior: "create_prorations",
|
||||
});
|
||||
|
||||
await this.setSubscriptionForTeam(
|
||||
teamId,
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
export enum PlatformPlan {
|
||||
FREE = "FREE",
|
||||
STARTER = "STARTER",
|
||||
ESSENTIALS = "ESSENTIALS",
|
||||
SCALE = "SCALE",
|
||||
ENTERPRISE = "ENTERPRISE",
|
||||
}
|
||||
|
||||
export type PlatformPlanType = "STARTER" | "ESSENTIALS" | "SCALE" | "ENTERPRISE";
|
||||
export type PlatformPlanType = "FREE" | "STARTER" | "ESSENTIALS" | "SCALE" | "ENTERPRISE";
|
||||
|
||||
@@ -12,6 +12,9 @@ export class BookingSeatRepository {
|
||||
where: {
|
||||
referenceUid,
|
||||
},
|
||||
include: {
|
||||
booking: { select: { uid: true } },
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+1
-36
@@ -510,7 +510,7 @@ describe("OAuth Client Users Endpoints", () => {
|
||||
expect(oAuthClient1).toBeDefined();
|
||||
});
|
||||
|
||||
it(`should create managed user and update team event-types of OAuthClient marked as assignAllTeamMembers: true`, async () => {
|
||||
it(`should create managed user and update team event-types`, async () => {
|
||||
const requestBody: CreateManagedUserInput = {
|
||||
email: userEmail,
|
||||
timeZone: userTimeZone,
|
||||
@@ -532,48 +532,13 @@ describe("OAuth Client Users Endpoints", () => {
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
|
||||
await userHasCorrectEventTypes(responseBody.data.user.id);
|
||||
await teamHasCorrectEventTypes(team1.id);
|
||||
expect(responseBody.data.user.name).toEqual(requestBody.name);
|
||||
});
|
||||
|
||||
async function userHasCorrectEventTypes(userId: number) {
|
||||
const eventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes(userId);
|
||||
|
||||
expect(eventTypes?.length).toEqual(5);
|
||||
|
||||
// note(Lauris): managed event-types with assignAllTeamMembers: true
|
||||
expect(eventTypes?.find((eventType) => eventType.slug === managedEventType1.slug)).toBeTruthy();
|
||||
|
||||
// note(Lauris): default event types
|
||||
expect(
|
||||
eventTypes?.find((eventType) => eventType.slug === DEFAULT_EVENT_TYPES.thirtyMinutes.slug)
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
eventTypes?.find((eventType) => eventType.slug === DEFAULT_EVENT_TYPES.sixtyMinutes.slug)
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
eventTypes?.find((eventType) => eventType.slug === DEFAULT_EVENT_TYPES.thirtyMinutesVideo.slug)
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
eventTypes?.find((eventType) => eventType.slug === DEFAULT_EVENT_TYPES.sixtyMinutesVideo.slug)
|
||||
).toBeTruthy();
|
||||
}
|
||||
|
||||
async function teamHasCorrectEventTypes(teamId: number) {
|
||||
const eventTypes = await eventTypesRepositoryFixture.getAllTeamEventTypes(teamId);
|
||||
|
||||
expect(eventTypes?.length).toEqual(2);
|
||||
|
||||
// note(Lauris): managed event-types with assignAllTeamMembers: true
|
||||
expect(eventTypes?.find((eventType) => eventType.slug === managedEventType1.slug)).toBeTruthy();
|
||||
|
||||
// note(Lauris): check if managed user added to collective event-type hosts given that it has assignAllTeamMembers: true
|
||||
const collective = eventTypes?.find((eventType) => eventType.schedulingType === "COLLECTIVE");
|
||||
expect(collective).toBeTruthy();
|
||||
expect(collective?.hosts).toBeDefined();
|
||||
expect(collective?.hosts?.length).toEqual(1);
|
||||
expect(collective?.hosts[0].userId).toEqual(postResponseData.user.id);
|
||||
}
|
||||
|
||||
afterAll(async () => {
|
||||
|
||||
+3
-29
@@ -25,9 +25,9 @@ import {
|
||||
import {
|
||||
ApiTags as DocsTags,
|
||||
ApiExcludeController as DocsExcludeController,
|
||||
ApiExcludeEndpoint as DocsExcludeEndpoint,
|
||||
ApiOperation as DocsOperation,
|
||||
ApiOkResponse as DocsOkResponse,
|
||||
ApiExcludeEndpoint as DocsExcludeEndpoint,
|
||||
ApiBadRequestResponse as DocsBadRequestResponse,
|
||||
ApiHeader as DocsHeader,
|
||||
} from "@nestjs/swagger";
|
||||
@@ -50,20 +50,7 @@ export class OAuthFlowController {
|
||||
@Post("/authorize")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@UseGuards(NextAuthGuard)
|
||||
@DocsOperation({
|
||||
summary: "Authorize an OAuth client",
|
||||
description:
|
||||
"Redirects the user to the specified 'redirect_uri' with an authorization code in query parameter if the client is authorized successfully. The code is then exchanged for access and refresh tokens via the `/exchange` endpoint.",
|
||||
})
|
||||
@DocsOkResponse({
|
||||
description:
|
||||
"The user is redirected to the 'redirect_uri' with an authorization code in query parameter e.g. `redirectUri?code=secretcode.`",
|
||||
})
|
||||
@DocsBadRequestResponse({
|
||||
description:
|
||||
"Bad request if the OAuth client is not found, if the redirect URI is invalid, or if the user has already authorized the client.",
|
||||
})
|
||||
@DocsExcludeEndpoint(getEnv("NODE_ENV") === "production")
|
||||
@DocsExcludeEndpoint()
|
||||
async authorize(
|
||||
@Param("clientId") clientId: string,
|
||||
@Body() body: OAuthAuthorizeInput,
|
||||
@@ -97,20 +84,7 @@ export class OAuthFlowController {
|
||||
|
||||
@Post("/exchange")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@DocsOperation({
|
||||
summary: "Exchange authorization code for access tokens",
|
||||
description:
|
||||
"Exchanges the authorization code received from the `/authorize` endpoint for access and refresh tokens. The authorization code should be provided in the 'Authorization' header prefixed with 'Bearer '.",
|
||||
})
|
||||
@DocsOkResponse({
|
||||
type: KeysResponseDto,
|
||||
description: "Successfully exchanged authorization code for access and refresh tokens.",
|
||||
})
|
||||
@DocsBadRequestResponse({
|
||||
description:
|
||||
"Bad request if the authorization code is missing, invalid, or if the client ID and secret do not match.",
|
||||
})
|
||||
@DocsExcludeEndpoint(getEnv("NODE_ENV") === "production")
|
||||
@DocsExcludeEndpoint()
|
||||
async exchange(
|
||||
@Headers("Authorization") authorization: string,
|
||||
@Param("clientId") clientId: string,
|
||||
|
||||
@@ -83,8 +83,6 @@ export class OAuthClientUsersService {
|
||||
user.defaultScheduleId = defaultSchedule.id;
|
||||
}
|
||||
|
||||
await this.organizationsTeamsService.addUserToPlatformTeamEvents(user.id, organizationId, oAuthClientId);
|
||||
|
||||
return {
|
||||
user,
|
||||
tokens: {
|
||||
|
||||
+46
-2
@@ -9,8 +9,9 @@ import { UsersModule } from "@/modules/users/users.module";
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import { User } from "@prisma/client";
|
||||
import { EventType, User } from "@prisma/client";
|
||||
import * as request from "supertest";
|
||||
import { EventTypesRepositoryFixture } from "test/fixtures/repository/event-types.repository.fixture";
|
||||
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||
import { OrganizationRepositoryFixture } from "test/fixtures/repository/organization.repository.fixture";
|
||||
import { ProfileRepositoryFixture } from "test/fixtures/repository/profiles.repository.fixture";
|
||||
@@ -26,6 +27,7 @@ describe("Organizations Teams Memberships Endpoints", () => {
|
||||
describe("User Authentication - User is Org Admin", () => {
|
||||
let app: INestApplication;
|
||||
|
||||
let eventTypesRepositoryFixture: EventTypesRepositoryFixture;
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
||||
let teamsRepositoryFixture: TeamRepositoryFixture;
|
||||
@@ -36,6 +38,8 @@ describe("Organizations Teams Memberships Endpoints", () => {
|
||||
let org: Team;
|
||||
let orgTeam: Team;
|
||||
let nonOrgTeam: Team;
|
||||
let teamEventType: EventType;
|
||||
let managedEventType: EventType;
|
||||
let membership: Membership;
|
||||
let membership2: Membership;
|
||||
let membershipCreatedViaApi: OrgTeamMembershipOutputDto;
|
||||
@@ -66,6 +70,7 @@ describe("Organizations Teams Memberships Endpoints", () => {
|
||||
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
||||
|
||||
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||
eventTypesRepositoryFixture = new EventTypesRepositoryFixture(moduleRef);
|
||||
|
||||
user = await userRepositoryFixture.create({
|
||||
email: userEmail,
|
||||
@@ -97,6 +102,32 @@ describe("Organizations Teams Memberships Endpoints", () => {
|
||||
parent: { connect: { id: org.id } },
|
||||
});
|
||||
|
||||
teamEventType = await eventTypesRepositoryFixture.createTeamEventType({
|
||||
schedulingType: "COLLECTIVE",
|
||||
team: {
|
||||
connect: { id: orgTeam.id },
|
||||
},
|
||||
title: "Collective Event Type",
|
||||
slug: "collective-event-type",
|
||||
length: 30,
|
||||
assignAllTeamMembers: true,
|
||||
bookingFields: [],
|
||||
locations: [],
|
||||
});
|
||||
|
||||
managedEventType = await eventTypesRepositoryFixture.createTeamEventType({
|
||||
schedulingType: "MANAGED",
|
||||
team: {
|
||||
connect: { id: orgTeam.id },
|
||||
},
|
||||
title: "Managed Event Type",
|
||||
slug: "managed-event-type",
|
||||
length: 60,
|
||||
assignAllTeamMembers: true,
|
||||
bookingFields: [],
|
||||
locations: [],
|
||||
});
|
||||
|
||||
nonOrgTeam = await teamsRepositoryFixture.create({
|
||||
name: "Non Org Team",
|
||||
isOrganization: false,
|
||||
@@ -264,7 +295,7 @@ describe("Organizations Teams Memberships Endpoints", () => {
|
||||
.expect(404);
|
||||
});
|
||||
|
||||
it("should create the membership of the org's team", async () => {
|
||||
it("should have created the membership of the org's team and assigned team wide events", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${org.id}/teams/${orgTeam.id}/memberships`)
|
||||
.send({
|
||||
@@ -281,9 +312,22 @@ describe("Organizations Teams Memberships Endpoints", () => {
|
||||
expect(membershipCreatedViaApi.role).toEqual("MEMBER");
|
||||
expect(membershipCreatedViaApi.userId).toEqual(userToInviteViaApi.id);
|
||||
expect(membershipCreatedViaApi.user.email).toEqual(userToInviteViaApi.email);
|
||||
userHasCorrectEventTypes(membershipCreatedViaApi.userId);
|
||||
});
|
||||
});
|
||||
|
||||
async function userHasCorrectEventTypes(userId: number) {
|
||||
const managedEventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes(userId);
|
||||
const teamEventTypes = await eventTypesRepositoryFixture.getAllTeamEventTypes(orgTeam.id);
|
||||
expect(managedEventTypes?.length).toEqual(1);
|
||||
expect(teamEventTypes?.length).toEqual(2);
|
||||
const collectiveEvenType = teamEventTypes?.find((eventType) => eventType.slug === teamEventType.slug);
|
||||
expect(collectiveEvenType).toBeTruthy();
|
||||
const userHost = collectiveEvenType?.hosts.find((host) => host.userId === userId);
|
||||
expect(userHost).toBeTruthy();
|
||||
expect(managedEventTypes?.find((eventType) => eventType.slug === managedEventType.slug)).toBeTruthy();
|
||||
}
|
||||
|
||||
it("should fail to create the membership of the org's team for a non org user", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${org.id}/teams/${orgTeam.id}/memberships`)
|
||||
|
||||
+32
-2
@@ -15,6 +15,7 @@ import {
|
||||
OrgTeamMembershipsOutputResponseDto,
|
||||
OrgTeamMembershipOutputResponseDto,
|
||||
} from "@/modules/organizations/outputs/organization-teams-memberships.output";
|
||||
import { OrganizationsEventTypesService } from "@/modules/organizations/services/event-types/organizations-event-types.service";
|
||||
import { OrganizationsTeamsMembershipsService } from "@/modules/organizations/services/organizations-teams-memberships.service";
|
||||
import {
|
||||
Controller,
|
||||
@@ -30,11 +31,13 @@ import {
|
||||
HttpStatus,
|
||||
HttpCode,
|
||||
UnprocessableEntityException,
|
||||
Logger,
|
||||
} from "@nestjs/common";
|
||||
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
import { plainToClass } from "class-transformer";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { updateNewTeamMemberEventTypes } from "@calcom/platform-libraries";
|
||||
import { SkipTakePagination } from "@calcom/platform-types";
|
||||
|
||||
@Controller({
|
||||
@@ -44,8 +47,11 @@ import { SkipTakePagination } from "@calcom/platform-types";
|
||||
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, IsTeamInOrg, PlatformPlanGuard, IsAdminAPIEnabledGuard)
|
||||
@DocsTags("Orgs / Teams / Memberships")
|
||||
export class OrganizationsTeamsMembershipsController {
|
||||
private logger = new Logger("OrganizationsTeamsMembershipsController");
|
||||
|
||||
constructor(
|
||||
private organizationsTeamsMembershipsService: OrganizationsTeamsMembershipsService,
|
||||
private oganizationsEventTypesService: OrganizationsEventTypesService,
|
||||
private readonly organizationsRepository: OrganizationsRepository
|
||||
) {}
|
||||
|
||||
@@ -112,6 +118,9 @@ export class OrganizationsTeamsMembershipsController {
|
||||
teamId,
|
||||
membershipId
|
||||
);
|
||||
|
||||
await this.oganizationsEventTypesService.deleteUserTeamEventTypesAndHosts(membership.userId, teamId);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToClass(OrgTeamMembershipOutputDto, membership, { strategy: "excludeAll" }),
|
||||
@@ -129,15 +138,29 @@ export class OrganizationsTeamsMembershipsController {
|
||||
@Param("membershipId", ParseIntPipe) membershipId: number,
|
||||
@Body() data: UpdateOrgTeamMembershipDto
|
||||
): Promise<OrgTeamMembershipOutputResponseDto> {
|
||||
const membership = await this.organizationsTeamsMembershipsService.updateOrgTeamMembership(
|
||||
const currentMembership = await this.organizationsTeamsMembershipsService.getOrgTeamMembership(
|
||||
orgId,
|
||||
teamId,
|
||||
membershipId
|
||||
);
|
||||
const updatedMembership = await this.organizationsTeamsMembershipsService.updateOrgTeamMembership(
|
||||
orgId,
|
||||
teamId,
|
||||
membershipId,
|
||||
data
|
||||
);
|
||||
|
||||
if (!currentMembership.accepted && updatedMembership.accepted) {
|
||||
try {
|
||||
await updateNewTeamMemberEventTypes(updatedMembership.userId, teamId);
|
||||
} catch (err) {
|
||||
this.logger.error("Could not update new team member eventTypes", err);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToClass(OrgTeamMembershipOutputDto, membership, { strategy: "excludeAll" }),
|
||||
data: plainToClass(OrgTeamMembershipOutputDto, updatedMembership, { strategy: "excludeAll" }),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -158,6 +181,13 @@ export class OrganizationsTeamsMembershipsController {
|
||||
}
|
||||
|
||||
const membership = await this.organizationsTeamsMembershipsService.createOrgTeamMembership(teamId, data);
|
||||
if (membership.accepted) {
|
||||
try {
|
||||
await updateNewTeamMemberEventTypes(user.id, teamId);
|
||||
} catch (err) {
|
||||
this.logger.error("Could not update new team member eventTypes", err);
|
||||
}
|
||||
}
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToClass(OrgTeamMembershipOutputDto, membership, { strategy: "excludeAll" }),
|
||||
|
||||
-21
@@ -469,34 +469,13 @@ describe("Organizations Users Endpoints", () => {
|
||||
|
||||
const userData = body.data;
|
||||
expect(body.status).toBe(SUCCESS_STATUS);
|
||||
userHasCorrectEventTypes(userData.id);
|
||||
createdUser = userData;
|
||||
teamHasCorrectEventTypes(team.id);
|
||||
});
|
||||
|
||||
async function userHasCorrectEventTypes(userId: number) {
|
||||
const eventTypes = await eventTypesRepositoryFixture.getAllUserEventTypes(userId);
|
||||
|
||||
expect(eventTypes?.length).toEqual(1);
|
||||
|
||||
// note(Lauris): managed event-types with assignAllTeamMembers: true
|
||||
expect(eventTypes?.find((eventType) => eventType.slug === managedEventType.slug)).toBeTruthy();
|
||||
}
|
||||
|
||||
async function teamHasCorrectEventTypes(teamId: number) {
|
||||
const eventTypes = await eventTypesRepositoryFixture.getAllTeamEventTypes(teamId);
|
||||
|
||||
expect(eventTypes?.length).toEqual(2);
|
||||
|
||||
// note(Lauris): managed event-types with assignAllTeamMembers: true
|
||||
expect(eventTypes?.find((eventType) => eventType.slug === managedEventType.slug)).toBeTruthy();
|
||||
|
||||
// note(Lauris): check if managed user added to collective event-type hosts given that it has assignAllTeamMembers: true
|
||||
const collective = eventTypes?.find((eventType) => eventType.schedulingType === "COLLECTIVE");
|
||||
expect(collective).toBeTruthy();
|
||||
expect(collective?.hosts).toBeDefined();
|
||||
expect(collective?.hosts?.length).toEqual(1);
|
||||
expect(collective?.hosts[0].userId).toEqual(createdUser.id);
|
||||
}
|
||||
|
||||
afterAll(async () => {
|
||||
|
||||
@@ -33,7 +33,7 @@ export class OrganizationsRepository {
|
||||
}
|
||||
|
||||
async createNewBillingRelation(orgId: number, plan?: PlatformPlan) {
|
||||
const { id } = await this.stripeService.stripe.customers.create({
|
||||
const { id } = await this.stripeService.getStripe().customers.create({
|
||||
metadata: {
|
||||
createdBy: "oauth_client_no_csid", // mark in case this is needed in the future.
|
||||
},
|
||||
|
||||
+22
@@ -70,4 +70,26 @@ export class OrganizationsEventTypesRepository {
|
||||
include: { children: true },
|
||||
});
|
||||
}
|
||||
|
||||
async deleteUserManagedTeamEventTypes(userId: number, teamId: number) {
|
||||
return this.dbWrite.prisma.eventType.deleteMany({
|
||||
where: {
|
||||
parent: {
|
||||
teamId,
|
||||
},
|
||||
userId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async removeUserFromTeamEventTypesHosts(userId: number, teamId: number) {
|
||||
return this.dbWrite.prisma.host.deleteMany({
|
||||
where: {
|
||||
userId,
|
||||
eventType: {
|
||||
teamId,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+16
-1
@@ -6,13 +6,15 @@ import { DatabaseTeamEventType } from "@/modules/organizations/services/event-ty
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { UsersService } from "@/modules/users/services/users.service";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
import { Injectable, NotFoundException, Logger } from "@nestjs/common";
|
||||
|
||||
import { createEventType, updateEventType } from "@calcom/platform-libraries";
|
||||
import { InputTeamEventTransformed_2024_06_14 } from "@calcom/platform-types";
|
||||
|
||||
@Injectable()
|
||||
export class OrganizationsEventTypesService {
|
||||
private readonly logger = new Logger("OrganizationsEventTypesService");
|
||||
|
||||
constructor(
|
||||
private readonly eventTypesService: EventTypesService_2024_06_14,
|
||||
private readonly dbWrite: PrismaWriteService,
|
||||
@@ -145,4 +147,17 @@ export class OrganizationsEventTypesService {
|
||||
|
||||
return this.eventTypesRepository.deleteEventType(eventTypeId);
|
||||
}
|
||||
|
||||
async deleteUserTeamEventTypesAndHosts(userId: number, teamId: number) {
|
||||
try {
|
||||
await this.organizationEventTypesRepository.deleteUserManagedTeamEventTypes(userId, teamId);
|
||||
await this.organizationEventTypesRepository.removeUserFromTeamEventTypesHosts(userId, teamId);
|
||||
} catch (err) {
|
||||
this.logger.error("Could not remove user from all team event-types.", {
|
||||
error: err,
|
||||
userId,
|
||||
teamId,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,6 @@ import { OrganizationsTeamsRepository } from "@/modules/organizations/repositori
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { updateNewTeamMemberEventTypes } from "@calcom/platform-libraries";
|
||||
|
||||
@Injectable()
|
||||
export class OrganizationsTeamsService {
|
||||
constructor(
|
||||
@@ -69,23 +67,4 @@ export class OrganizationsTeamsService {
|
||||
}
|
||||
return team;
|
||||
}
|
||||
|
||||
async addUserToTeamEvents(userId: number, organizationId: number) {
|
||||
const orgTeams = await this.organizationsTeamRepository.findOrgTeams(organizationId);
|
||||
|
||||
for (const team of orgTeams) {
|
||||
await updateNewTeamMemberEventTypes(userId, team.id);
|
||||
}
|
||||
}
|
||||
|
||||
async addUserToPlatformTeamEvents(userId: number, organizationId: number, oAuthClientId: string) {
|
||||
const oAuthClientTeams = await this.organizationsTeamRepository.getPlatformOrgTeams(
|
||||
organizationId,
|
||||
oAuthClientId
|
||||
);
|
||||
|
||||
for (const team of oAuthClientTeams) {
|
||||
await updateNewTeamMemberEventTypes(userId, team.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,8 +79,6 @@ export class OrganizationsUsersService {
|
||||
updateUserBody
|
||||
);
|
||||
|
||||
await this.organizationsTeamsService.addUserToTeamEvents(user.id, org.id);
|
||||
|
||||
// Need to send email to new user to create password
|
||||
await this.emailService.sendSignupToOrganizationEmail({
|
||||
usernameOrEmail,
|
||||
|
||||
@@ -28,7 +28,7 @@ type IntegrationOAuthCallbackState = {
|
||||
|
||||
@Injectable()
|
||||
export class StripeService {
|
||||
public stripe: Stripe;
|
||||
private stripe: Stripe;
|
||||
private redirectUri = `${this.config.get("api.url")}/stripe/save`;
|
||||
|
||||
constructor(
|
||||
@@ -44,6 +44,10 @@ export class StripeService {
|
||||
});
|
||||
}
|
||||
|
||||
getStripe() {
|
||||
return this.stripe;
|
||||
}
|
||||
|
||||
async getStripeRedirectUrl(state: string, userEmail?: string, userName?: string | null) {
|
||||
const { client_id } = await this.getStripeAppKeys();
|
||||
|
||||
|
||||
@@ -82,7 +82,10 @@ export class WebhooksController {
|
||||
}
|
||||
|
||||
@Get("/")
|
||||
@ApiOperation({ summary: "Get all webooks (paginated)" })
|
||||
@ApiOperation({
|
||||
summary: "Get all webooks",
|
||||
description: "Gets a paginated list of webhooks for the authenticated user.",
|
||||
})
|
||||
async getWebhooks(
|
||||
@GetUser() user: UserWithProfile,
|
||||
@Query() query: SkipTakePagination
|
||||
|
||||
@@ -3257,6 +3257,230 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/bookings/{bookingUid}/reassign": {
|
||||
"post": {
|
||||
"operationId": "BookingsController_2024_08_13_reassignBooking",
|
||||
"summary": "Automatically reassign booking to a new host",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "cal-api-version",
|
||||
"in": "header",
|
||||
"description": "Must be set to `2024-08-13`",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "bookingUid",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Authorization",
|
||||
"in": "header",
|
||||
"description": "value must be `Bearer <token>` where `<token>` either managed user access token or api key prefixed with cal_",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ReassignBookingOutput_2024_08_13"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Bookings"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/bookings/{bookingUid}/reassign/{userId}": {
|
||||
"post": {
|
||||
"operationId": "BookingsController_2024_08_13_reassignBookingToUser",
|
||||
"summary": "Reassign a booking to a specific user",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "cal-api-version",
|
||||
"in": "header",
|
||||
"description": "Must be set to `2024-08-13`",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "bookingUid",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "userId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Authorization",
|
||||
"in": "header",
|
||||
"description": "value must be `Bearer <token>` where `<token>` either managed user access token or api key prefixed with cal_",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ReassignToUserBookingInput_2024_08_13"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ReassignBookingOutput_2024_08_13"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Bookings"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/bookings/{bookingUid}/confirm": {
|
||||
"post": {
|
||||
"operationId": "BookingsController_2024_08_13_confirmBooking",
|
||||
"summary": "Confirm booking that requires a confirmation",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "cal-api-version",
|
||||
"in": "header",
|
||||
"description": "Must be set to `2024-08-13`",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "bookingUid",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Authorization",
|
||||
"in": "header",
|
||||
"description": "value must be `Bearer <token>` where `<token>` either managed user access token or api key prefixed with cal_",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/GetBookingOutput_2024_08_13"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Bookings"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/bookings/{bookingUid}/decline": {
|
||||
"post": {
|
||||
"operationId": "BookingsController_2024_08_13_declineBooking",
|
||||
"summary": "Decline booking that requires a confirmation",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "cal-api-version",
|
||||
"in": "header",
|
||||
"description": "Must be set to `2024-08-13`",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "bookingUid",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "Authorization",
|
||||
"in": "header",
|
||||
"description": "value must be `Bearer <token>` where `<token>` either managed user access token or api key prefixed with cal_",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/DeclineBookingInput_2024_08_13"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/GetBookingOutput_2024_08_13"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Bookings"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/calendars/ics-feed/save": {
|
||||
"post": {
|
||||
"operationId": "CalendarsController_createIcsFeed",
|
||||
@@ -4214,97 +4438,6 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/oauth/{clientId}/authorize": {
|
||||
"post": {
|
||||
"operationId": "OAuthFlowController_authorize",
|
||||
"summary": "Authorize an OAuth client",
|
||||
"description": "Redirects the user to the specified 'redirect_uri' with an authorization code in query parameter if the client is authorized successfully. The code is then exchanged for access and refresh tokens via the `/exchange` endpoint.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "clientId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/OAuthAuthorizeInput"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "The user is redirected to the 'redirect_uri' with an authorization code in query parameter e.g. `redirectUri?code=secretcode.`"
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request if the OAuth client is not found, if the redirect URI is invalid, or if the user has already authorized the client."
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"OAuth"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/oauth/{clientId}/exchange": {
|
||||
"post": {
|
||||
"operationId": "OAuthFlowController_exchange",
|
||||
"summary": "Exchange authorization code for access tokens",
|
||||
"description": "Exchanges the authorization code received from the `/authorize` endpoint for access and refresh tokens. The authorization code should be provided in the 'Authorization' header prefixed with 'Bearer '.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "Authorization",
|
||||
"required": true,
|
||||
"in": "header",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "clientId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ExchangeAuthorizationCodeInput"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Successfully exchanged authorization code for access and refresh tokens.",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/KeysResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad request if the authorization code is missing, invalid, or if the client ID and secret do not match."
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"OAuth"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/oauth/{clientId}/refresh": {
|
||||
"post": {
|
||||
"operationId": "OAuthFlowController_refreshAccessToken",
|
||||
@@ -5033,6 +5166,37 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/stripe/check/{teamId}": {
|
||||
"get": {
|
||||
"operationId": "StripeController_checkTeamStripeConnection",
|
||||
"summary": "Check team stripe connection",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "teamId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/StripCredentialsCheckOutputResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Stripe"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/timezones": {
|
||||
"get": {
|
||||
"operationId": "TimezonesController_getTimeZones",
|
||||
@@ -5088,7 +5252,8 @@
|
||||
},
|
||||
"get": {
|
||||
"operationId": "WebhooksController_getWebhooks",
|
||||
"summary": "Get all webooks (paginated)",
|
||||
"summary": "Get all webooks",
|
||||
"description": "Gets a paginated list of webhooks for the authenticated user.",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "take",
|
||||
@@ -5818,28 +5983,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"OAuthAuthorizeInput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"redirectUri": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"redirectUri"
|
||||
]
|
||||
},
|
||||
"ExchangeAuthorizationCodeInput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"clientSecret": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"clientSecret"
|
||||
]
|
||||
},
|
||||
"RefreshTokenInput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -6720,6 +6863,18 @@
|
||||
"type": "number",
|
||||
"example": 60
|
||||
},
|
||||
"lengthInMinutesOptions": {
|
||||
"example": [
|
||||
15,
|
||||
30,
|
||||
60
|
||||
],
|
||||
"description": "If you want that user can choose between different lengths of the event you can specify them here. Must include the provided `lengthInMinutes`.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"example": "Learn the secrets of masterchief!"
|
||||
@@ -6943,6 +7098,7 @@
|
||||
},
|
||||
"required": [
|
||||
"lengthInMinutes",
|
||||
"lengthInMinutesOptions",
|
||||
"title",
|
||||
"slug"
|
||||
]
|
||||
@@ -8085,6 +8241,18 @@
|
||||
"type": "number",
|
||||
"example": 60
|
||||
},
|
||||
"lengthInMinutesOptions": {
|
||||
"example": [
|
||||
15,
|
||||
30,
|
||||
60
|
||||
],
|
||||
"description": "If you want that user can choose between different lengths of the event you can specify them here. Must include the provided `lengthInMinutes`.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"example": "Learn the secrets of masterchief!"
|
||||
@@ -8308,6 +8476,7 @@
|
||||
"required": [
|
||||
"id",
|
||||
"lengthInMinutes",
|
||||
"lengthInMinutesOptions",
|
||||
"title",
|
||||
"slug",
|
||||
"description",
|
||||
@@ -8423,6 +8592,18 @@
|
||||
"type": "number",
|
||||
"example": 60
|
||||
},
|
||||
"lengthInMinutesOptions": {
|
||||
"example": [
|
||||
15,
|
||||
30,
|
||||
60
|
||||
],
|
||||
"description": "If you want that user can choose between different lengths of the event you can specify them here. Must include the provided `lengthInMinutes`.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"example": "Learn the secrets of masterchief!"
|
||||
@@ -8643,7 +8824,10 @@
|
||||
"hideCalendarEventDetails": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"lengthInMinutesOptions"
|
||||
]
|
||||
},
|
||||
"UpdateEventTypeOutput_2024_06_14": {
|
||||
"type": "object",
|
||||
@@ -9902,6 +10086,18 @@
|
||||
"type": "number",
|
||||
"example": 60
|
||||
},
|
||||
"lengthInMinutesOptions": {
|
||||
"example": [
|
||||
15,
|
||||
30,
|
||||
60
|
||||
],
|
||||
"description": "If you want that user can choose between different lengths of the event you can specify them here. Must include the provided `lengthInMinutes`.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"example": "Learn the secrets of masterchief!"
|
||||
@@ -10138,6 +10334,7 @@
|
||||
},
|
||||
"required": [
|
||||
"lengthInMinutes",
|
||||
"lengthInMinutesOptions",
|
||||
"title",
|
||||
"slug",
|
||||
"schedulingType",
|
||||
@@ -10219,6 +10416,19 @@
|
||||
"minimum": 1,
|
||||
"example": 60
|
||||
},
|
||||
"lengthInMinutesOptions": {
|
||||
"example": [
|
||||
15,
|
||||
30,
|
||||
60
|
||||
],
|
||||
"description": "If you want that user can choose between different lengths of the event you can specify them here. Must include the provided `lengthInMinutes`.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "number",
|
||||
"minimum": 1
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"example": "Learn the secrets of masterchief!"
|
||||
@@ -10644,6 +10854,18 @@
|
||||
"type": "number",
|
||||
"example": 60
|
||||
},
|
||||
"lengthInMinutesOptions": {
|
||||
"example": [
|
||||
15,
|
||||
30,
|
||||
60
|
||||
],
|
||||
"description": "If you want that user can choose between different lengths of the event you can specify them here. Must include the provided `lengthInMinutes`.",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"title": {
|
||||
"type": "string",
|
||||
"example": "Learn the secrets of masterchief!"
|
||||
@@ -10874,7 +11096,10 @@
|
||||
"type": "boolean",
|
||||
"description": "If true, all current and future team members will be assigned to this event type"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"lengthInMinutesOptions"
|
||||
]
|
||||
},
|
||||
"UpdateTeamEventTypeOutput": {
|
||||
"type": "object",
|
||||
@@ -12584,6 +12809,11 @@
|
||||
"description": "The start time of the booking in ISO 8601 format in UTC timezone.",
|
||||
"example": "2024-08-13T09:00:00Z"
|
||||
},
|
||||
"lengthInMinutes": {
|
||||
"type": "number",
|
||||
"example": 30,
|
||||
"description": "If it is an event type that has multiple possible lengths that attendee can pick from, you can pass the desired booking length here.\n If not provided then event type default length will be used for the booking."
|
||||
},
|
||||
"eventTypeId": {
|
||||
"type": "number",
|
||||
"description": "The ID of the event type that is booked.",
|
||||
@@ -12629,6 +12859,7 @@
|
||||
},
|
||||
"required": [
|
||||
"start",
|
||||
"lengthInMinutes",
|
||||
"eventTypeId",
|
||||
"attendee"
|
||||
]
|
||||
@@ -12641,6 +12872,11 @@
|
||||
"description": "The start time of the booking in ISO 8601 format in UTC timezone.",
|
||||
"example": "2024-08-13T09:00:00Z"
|
||||
},
|
||||
"lengthInMinutes": {
|
||||
"type": "number",
|
||||
"example": 30,
|
||||
"description": "If it is an event type that has multiple possible lengths that attendee can pick from, you can pass the desired booking length here.\n If not provided then event type default length will be used for the booking."
|
||||
},
|
||||
"eventTypeId": {
|
||||
"type": "number",
|
||||
"description": "The ID of the event type that is booked.",
|
||||
@@ -12691,6 +12927,7 @@
|
||||
},
|
||||
"required": [
|
||||
"start",
|
||||
"lengthInMinutes",
|
||||
"eventTypeId",
|
||||
"attendee",
|
||||
"instant"
|
||||
@@ -12704,6 +12941,11 @@
|
||||
"description": "The start time of the booking in ISO 8601 format in UTC timezone.",
|
||||
"example": "2024-08-13T09:00:00Z"
|
||||
},
|
||||
"lengthInMinutes": {
|
||||
"type": "number",
|
||||
"example": 30,
|
||||
"description": "If it is an event type that has multiple possible lengths that attendee can pick from, you can pass the desired booking length here.\n If not provided then event type default length will be used for the booking."
|
||||
},
|
||||
"eventTypeId": {
|
||||
"type": "number",
|
||||
"description": "The ID of the event type that is booked.",
|
||||
@@ -12754,6 +12996,7 @@
|
||||
},
|
||||
"required": [
|
||||
"start",
|
||||
"lengthInMinutes",
|
||||
"eventTypeId",
|
||||
"attendee"
|
||||
]
|
||||
@@ -13278,6 +13521,78 @@
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"ReassignedToDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number",
|
||||
"example": 123
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"example": "John Doe"
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
"example": "john.doe@example.com"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"email"
|
||||
]
|
||||
},
|
||||
"ReassignBookingOutput_2024_08_13": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "success",
|
||||
"enum": [
|
||||
"success",
|
||||
"error"
|
||||
]
|
||||
},
|
||||
"data": {
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/ReassignBookingOutput_2024_08_13"
|
||||
}
|
||||
],
|
||||
"description": "Booking data, which can be either a ReassignAutoBookingOutput object or a ReassignManualBookingOutput object",
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/ReassignBookingOutput_2024_08_13"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"ReassignToUserBookingInput_2024_08_13": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"reason": {
|
||||
"type": "string",
|
||||
"example": "Host has to take another call",
|
||||
"description": "Reason for reassigning the booking"
|
||||
}
|
||||
}
|
||||
},
|
||||
"DeclineBookingInput_2024_08_13": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"reason": {
|
||||
"type": "string",
|
||||
"example": "Host has to take another call",
|
||||
"description": "Reason for declining a booking that requires a confirmation"
|
||||
}
|
||||
}
|
||||
},
|
||||
"ReserveSlotInput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -23,6 +23,14 @@ export class PlatformBillingRepositoryFixture {
|
||||
});
|
||||
}
|
||||
|
||||
async get(orgId: number) {
|
||||
return this.prismaWriteClient.platformBilling.findFirst({
|
||||
where: {
|
||||
id: orgId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async deleteSubscriptionForTeam(teamId: number) {
|
||||
// silently try to delete the subscription
|
||||
try {
|
||||
|
||||
@@ -8,8 +8,6 @@ import { FUTURE_ROUTES_ENABLED_COOKIE_NAME, FUTURE_ROUTES_OVERRIDE_COOKIE_NAME }
|
||||
const ROUTES: [URLPattern, boolean][] = [
|
||||
["/event-types", process.env.APP_ROUTER_EVENT_TYPES_ENABLED === "1"] as const,
|
||||
["/settings/developer/:path*", process.env.APP_ROUTER_SETTINGS_DEVELOPER_ENABLED === "1"] as const,
|
||||
["/settings/my-account/:path*", process.env.APP_ROUTER_SETTINGS_MY_ACCOUNT_ENABLED === "1"] as const,
|
||||
["/settings/platform/:path*", process.env.APP_ROUTER_SETTINGS_PLATFORM_ENABLED === "1"] as const,
|
||||
["/settings/organizations/:path*", process.env.APP_ROUTER_SETTINGS_ORG_ENABLED === "1"] as const,
|
||||
["/apps/installed/:category", process.env.APP_ROUTER_APPS_INSTALLED_CATEGORY_ENABLED === "1"] as const,
|
||||
["/apps/:slug", process.env.APP_ROUTER_APPS_SLUG_ENABLED === "1"] as const,
|
||||
|
||||
@@ -5,7 +5,7 @@ import EditView from "~/settings/platform/oauth-clients/[clientId]/edit/edit-vie
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
() => "OAuth client updation form",
|
||||
(t) => t("oAuth_client_updation_form"),
|
||||
() => ""
|
||||
);
|
||||
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ import EditWebhooksView from "~/settings/platform/oauth-clients/[clientId]/edit/
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
() => "OAuth client updation form",
|
||||
(t) => t("oAuth_client_updation_form"),
|
||||
() => ""
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import PlatformBillingUpgrade from "~/settings/platform/billing/billing-view";
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
return await _generateMetadata(
|
||||
(t) => t("platform_billing"),
|
||||
(t) => t("manage_billing_description")
|
||||
);
|
||||
};
|
||||
|
||||
export default WithLayout({
|
||||
Page: PlatformBillingUpgrade,
|
||||
});
|
||||
@@ -0,0 +1,21 @@
|
||||
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import { getServerSideProps } from "@lib/settings/organizations/new/getServerSideProps";
|
||||
import { type inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
|
||||
import LegacyPage, { LayoutWrapper } from "~/settings/platform/new/create-new-view";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
(t) => t("set_up_your_platform_organization"),
|
||||
(t) => t("platform_organization_description")
|
||||
);
|
||||
|
||||
export default WithLayout({
|
||||
getLayout: LayoutWrapper,
|
||||
Page: LegacyPage,
|
||||
getData: withAppDirSsr<inferSSRProps<typeof getServerSideProps>>(getServerSideProps),
|
||||
requiresLicense: true,
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import EditView from "~/settings/platform/oauth-clients/[clientId]/edit/edit-view";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
(t) => t("oAuth_client_updation_form"),
|
||||
() => ""
|
||||
);
|
||||
|
||||
export default WithLayout({
|
||||
getLayout: null,
|
||||
Page: EditView,
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import EditWebhooksView from "~/settings/platform/oauth-clients/[clientId]/edit/edit-webhooks-view";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
(t) => t("webhook_update_form"),
|
||||
() => ""
|
||||
);
|
||||
|
||||
export default WithLayout({
|
||||
getLayout: null,
|
||||
Page: EditWebhooksView,
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import CreateNewView from "~/settings/platform/oauth-clients/create-new-view";
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
return await _generateMetadata(
|
||||
(t) => t("oAuth_client_creation_form"),
|
||||
() => ""
|
||||
);
|
||||
};
|
||||
|
||||
export default WithLayout({
|
||||
getLayout: null,
|
||||
Page: CreateNewView,
|
||||
});
|
||||
@@ -0,0 +1,16 @@
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import PlatformView from "~/settings/platform/platform-view";
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
return await _generateMetadata(
|
||||
(t) => t("platform"),
|
||||
(t) => t("platform_description")
|
||||
);
|
||||
};
|
||||
|
||||
export default WithLayout({
|
||||
getLayout: null,
|
||||
Page: PlatformView,
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import { _generateMetadata } from "app/_utils";
|
||||
import { WithLayout } from "app/layoutHOC";
|
||||
|
||||
import PlatformPlansView from "~/settings/platform/plans/platform-plans-view";
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
return await _generateMetadata(
|
||||
(t) => `${t("platform")} ${t("plans")}`,
|
||||
() => ""
|
||||
);
|
||||
};
|
||||
|
||||
export default WithLayout({
|
||||
Page: PlatformPlansView,
|
||||
});
|
||||
@@ -40,6 +40,8 @@ function PageWrapper(props: AppProps) {
|
||||
pageStatus = "404";
|
||||
} else if (router.pathname === "/500") {
|
||||
pageStatus = "500";
|
||||
} else if (router.pathname === "/403") {
|
||||
pageStatus = "403";
|
||||
}
|
||||
|
||||
// On client side don't let nonce creep into DOM
|
||||
|
||||
@@ -31,6 +31,8 @@ function PageWrapper(props: PageWrapperProps) {
|
||||
pageStatus = "404";
|
||||
} else if (pathname === "/500") {
|
||||
pageStatus = "500";
|
||||
} else if (pathname === "/403") {
|
||||
pageStatus = "403";
|
||||
}
|
||||
|
||||
// On client side don't let nonce creep into DOM
|
||||
|
||||
@@ -132,6 +132,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
});
|
||||
|
||||
const isUpcoming = new Date(booking.endTime) >= new Date();
|
||||
const isOngoing = isUpcoming && new Date() >= new Date(booking.startTime);
|
||||
const isBookingInPast = new Date(booking.endTime) < new Date();
|
||||
const isCancelled = booking.status === BookingStatus.CANCELLED;
|
||||
const isConfirmed = booking.status === BookingStatus.ACCEPTED;
|
||||
@@ -269,7 +270,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
});
|
||||
}
|
||||
|
||||
if (isBookingInPast) {
|
||||
if (isBookingInPast || isOngoing) {
|
||||
editBookingActions.push({
|
||||
id: "no_show",
|
||||
label: t("mark_as_no_show"),
|
||||
|
||||
@@ -106,10 +106,10 @@ export const ReassignDialog = ({ isOpenDialog, setIsOpenDialog, teamId, bookingI
|
||||
});
|
||||
|
||||
const roundRobinReassignMutation = trpc.viewer.teams.roundRobinReassign.useMutation({
|
||||
onSuccess: async () => {
|
||||
onSuccess: async (data) => {
|
||||
await utils.viewer.bookings.get.invalidate();
|
||||
setIsOpenDialog(false);
|
||||
showToast(t("booking_reassigned"), "success");
|
||||
showToast(t("booking_reassigned_to_host", { host: data?.reassignedTo.name }), "success");
|
||||
},
|
||||
onError: async (error) => {
|
||||
if (error.message.includes(ErrorCode.NoAvailableUsersFound)) {
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
import type { TFunction } from "next-i18next";
|
||||
import { Trans } from "next-i18next";
|
||||
import { useState } from "react";
|
||||
|
||||
import {
|
||||
SettingsToggle,
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
InputField,
|
||||
DialogClose,
|
||||
Button,
|
||||
} from "@calcom/ui";
|
||||
|
||||
interface DisableEmailsSettingProps {
|
||||
checked: boolean;
|
||||
onCheckedChange: (e: boolean) => void;
|
||||
recipient: "attendees" | "hosts";
|
||||
t: TFunction;
|
||||
}
|
||||
|
||||
export const DisableAllEmailsSetting = ({
|
||||
checked,
|
||||
onCheckedChange,
|
||||
recipient,
|
||||
t,
|
||||
}: DisableEmailsSettingProps) => {
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [confirmText, setConfirmText] = useState("");
|
||||
|
||||
const title =
|
||||
recipient === "attendees" ? t("disable_all_emails_to_attendees") : t("disable_all_emails_to_hosts");
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Dialog open={dialogOpen} onOpenChange={(e) => setDialogOpen(e)}>
|
||||
<DialogContent title={title} Icon="circle-alert">
|
||||
<p className="text-default text-sm">
|
||||
<Trans i18nKey="disable_attendees_emails_description">
|
||||
This will disable all emails to {{ recipient }}. This includes booking confirmations, requests,
|
||||
reschedules and reschedule requests, cancellation emails, and any other emails related to
|
||||
booking updates.
|
||||
<br />
|
||||
<br />
|
||||
It is your responsibility to ensure that your {{ recipient }} are aware of any bookings and
|
||||
changes to their bookings.
|
||||
</Trans>
|
||||
</p>
|
||||
<p className="text-default mb-1 mt-2 text-sm">{t("type_confirm_to_continue")}</p>
|
||||
<InputField
|
||||
value={confirmText}
|
||||
onChange={(e) => {
|
||||
setConfirmText(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<DialogFooter>
|
||||
<DialogClose />
|
||||
<Button
|
||||
disabled={confirmText !== "confirm"}
|
||||
onClick={(e) => {
|
||||
onCheckedChange(true);
|
||||
setDialogOpen(false);
|
||||
}}>
|
||||
{t("disable_email")}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
<SettingsToggle
|
||||
labelClassName="text-sm"
|
||||
toggleSwitchAtTheEnd={true}
|
||||
switchContainerClassName="border-subtle rounded-lg border py-6 px-4 sm:px-6"
|
||||
title={title}
|
||||
description={t("disable_all_emails_description")}
|
||||
checked={!!checked}
|
||||
onCheckedChange={() => {
|
||||
checked ? onCheckedChange(!checked) : setDialogOpen(true);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -8,12 +8,12 @@ type IndividualPlatformPlan = {
|
||||
// if pricing or plans change in future modify this
|
||||
export const platformPlans: IndividualPlatformPlan[] = [
|
||||
{
|
||||
plan: "Starter",
|
||||
plan: "Free",
|
||||
description:
|
||||
"Perfect for just getting started with community support and access to hosted platform APIs, Cal.com Atoms (React components) and more.",
|
||||
pricing: 99,
|
||||
"The best plan to dip your toes into scheduling infrastructure. Start immediately with 25 free meetings per month",
|
||||
pricing: 0,
|
||||
includes: [
|
||||
"Up to 100 bookings a month",
|
||||
"Up to 25 bookings a month",
|
||||
"$0.99 overage beyond",
|
||||
"Community Support",
|
||||
"Cal Atoms (React Library)",
|
||||
|
||||
@@ -37,7 +37,7 @@ export const PlatformBillingCard = ({
|
||||
</h1>
|
||||
<p className="pb-5 pt-3 text-base">{description}</p>
|
||||
<h1 className="text-3xl font-semibold">
|
||||
{pricing && (
|
||||
{pricing !== undefined && (
|
||||
<>
|
||||
US${pricing} <span className="text-sm">per month</span>
|
||||
</>
|
||||
@@ -50,7 +50,7 @@ export const PlatformBillingCard = ({
|
||||
loading={isLoading}
|
||||
onClick={handleSubscribe}
|
||||
className="flex w-[100%] items-center justify-center">
|
||||
{pricing ? "Subscribe" : "Schedule a time"}
|
||||
{pricing !== undefined ? "Subscribe" : "Schedule a time"}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -14,6 +14,7 @@ import type { ReactNode } from "react";
|
||||
import { useEffect } from "react";
|
||||
import CacheProvider from "react-inlinesvg/provider";
|
||||
|
||||
import DynamicPostHogProvider from "@calcom/features/ee/event-tracking/lib/posthog/providerDynamic";
|
||||
import { OrgBrandingProvider } from "@calcom/features/ee/organizations/context/provider";
|
||||
import DynamicHelpscoutProvider from "@calcom/features/ee/support/lib/helpscout/providerDynamic";
|
||||
import DynamicIntercomProvider from "@calcom/features/ee/support/lib/intercom/providerDynamic";
|
||||
@@ -299,7 +300,9 @@ const AppProviders = (props: PageWrapperProps) => {
|
||||
|
||||
return (
|
||||
<DynamicHelpscoutProvider>
|
||||
<DynamicIntercomProvider>{RemainingProviders}</DynamicIntercomProvider>
|
||||
<DynamicIntercomProvider>
|
||||
<DynamicPostHogProvider>{RemainingProviders}</DynamicPostHogProvider>
|
||||
</DynamicIntercomProvider>
|
||||
</DynamicHelpscoutProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -7,11 +7,13 @@ import type { SSRConfig } from "next-i18next";
|
||||
import { appWithTranslation } from "next-i18next";
|
||||
import { ThemeProvider } from "next-themes";
|
||||
import type { AppProps as NextAppProps, AppProps as NextJsAppProps } from "next/app";
|
||||
import dynamic from "next/dynamic";
|
||||
import type { ParsedUrlQuery } from "querystring";
|
||||
import type { PropsWithChildren, ReactNode } from "react";
|
||||
import { useEffect } from "react";
|
||||
import CacheProvider from "react-inlinesvg/provider";
|
||||
|
||||
import DynamicPostHogProvider from "@calcom/features/ee/event-tracking/lib/posthog/providerDynamic";
|
||||
import { OrgBrandingProvider } from "@calcom/features/ee/organizations/context/provider";
|
||||
import DynamicHelpscoutProvider from "@calcom/features/ee/support/lib/helpscout/providerDynamic";
|
||||
import DynamicIntercomProvider from "@calcom/features/ee/support/lib/intercom/providerDynamic";
|
||||
@@ -56,6 +58,13 @@ export type AppProps = Omit<
|
||||
err?: Error;
|
||||
};
|
||||
|
||||
const PostHogPageView = dynamic(
|
||||
() => import("@calcom/features/ee/event-tracking/lib/posthog/web/PostHogPageView"),
|
||||
{
|
||||
ssr: false,
|
||||
}
|
||||
);
|
||||
|
||||
type AppPropsWithChildren = AppProps & {
|
||||
children: ReactNode;
|
||||
};
|
||||
@@ -319,7 +328,12 @@ const AppProviders = (props: AppPropsWithChildren) => {
|
||||
|
||||
return (
|
||||
<DynamicHelpscoutProvider>
|
||||
<DynamicIntercomProvider>{RemainingProviders}</DynamicIntercomProvider>
|
||||
<DynamicIntercomProvider>
|
||||
<DynamicPostHogProvider>
|
||||
<PostHogPageView />
|
||||
{RemainingProviders}
|
||||
</DynamicPostHogProvider>
|
||||
</DynamicIntercomProvider>
|
||||
</DynamicHelpscoutProvider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -21,12 +21,13 @@ type HelpCardInfo = {
|
||||
// if pricing or plans change in future modify this
|
||||
export const platformPlans: IndividualPlatformPlan[] = [
|
||||
{
|
||||
plan: "Starter",
|
||||
plan: "Free",
|
||||
description:
|
||||
"Perfect for just getting started with community support and access to hosted platform APIs, Cal.com Atoms (React components) and more.",
|
||||
pricing: 99,
|
||||
"The best plan to dip your toes into scheduling infrastructure. Start immediately with 25 free meetings per month",
|
||||
pricing: 0,
|
||||
includes: [
|
||||
"Up to 100 bookings a month",
|
||||
"Up to 25 bookings a month",
|
||||
"$0.99 overage beyond",
|
||||
"Community Support",
|
||||
"Cal Atoms (React Library)",
|
||||
"Platform APIs",
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import type { Prisma } from "@prisma/client";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import type { ParsedUrlQuery } from "querystring";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getCRMContactOwnerForRRLeadSkip } from "@calcom/app-store/_utils/CRMRoundRobinSkip";
|
||||
import { ROUTING_FORM_RESPONSE_ID_QUERY_STRING } from "@calcom/app-store/routing-forms/lib/constants";
|
||||
import { enabledAppSlugs } from "@calcom/app-store/routing-forms/lib/enabledApps";
|
||||
import { zodRoutes as routesSchema } from "@calcom/app-store/routing-forms/zod";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import type { GetBookingType } from "@calcom/features/bookings/lib/get-booking";
|
||||
import { getBookingForReschedule } from "@calcom/features/bookings/lib/get-booking";
|
||||
@@ -79,6 +83,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
}
|
||||
|
||||
const eventData = team.eventTypes[0];
|
||||
const eventTypeId = eventData.id;
|
||||
|
||||
let booking: GetBookingType | null = null;
|
||||
if (rescheduleUid) {
|
||||
@@ -92,7 +97,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
return {
|
||||
props: {
|
||||
eventData: {
|
||||
eventTypeId: eventData.id,
|
||||
eventTypeId,
|
||||
entity: {
|
||||
fromRedirectOfNonOrgLink,
|
||||
considerUnpublished: isUnpublished && !fromRedirectOfNonOrgLink,
|
||||
@@ -112,21 +117,133 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
isInstantMeeting: eventData && queryIsInstantMeeting ? true : false,
|
||||
themeBasis: null,
|
||||
orgBannerUrl: team.parent?.bannerUrl ?? "",
|
||||
teamMemberEmail: await getTeamMemberEmail(eventData, email as string),
|
||||
teamMemberEmail: await handleGettingTeamMemberEmail(query, eventTypeId, eventData),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
async function getTeamMemberEmail(
|
||||
eventData: {
|
||||
id: number;
|
||||
isInstantEvent: boolean;
|
||||
schedulingType: SchedulingType | null;
|
||||
metadata: Prisma.JsonValue | null;
|
||||
length: number;
|
||||
},
|
||||
email?: string
|
||||
): Promise<string | null> {
|
||||
interface EventData {
|
||||
id: number;
|
||||
isInstantEvent: boolean;
|
||||
schedulingType: SchedulingType | null;
|
||||
metadata: Prisma.JsonValue | null;
|
||||
length: number;
|
||||
}
|
||||
|
||||
async function handleGettingTeamMemberEmail(
|
||||
query: ParsedUrlQuery,
|
||||
eventTypeId: number,
|
||||
eventData: EventData
|
||||
) {
|
||||
if (
|
||||
!query.email ||
|
||||
typeof query.email !== "string" ||
|
||||
eventData.schedulingType !== SchedulingType.ROUND_ROBIN
|
||||
)
|
||||
return null;
|
||||
|
||||
// Check if a routing form was completed and an routing form option is enabled
|
||||
if (
|
||||
ROUTING_FORM_RESPONSE_ID_QUERY_STRING in query &&
|
||||
Object.values(query).some((value) => value === "true")
|
||||
) {
|
||||
const { email, skipContactOwner } = await handleRoutingFormOption(query, eventTypeId);
|
||||
|
||||
if (skipContactOwner) return null;
|
||||
if (email) return email;
|
||||
} else {
|
||||
return await getTeamMemberEmail(eventData, query.email);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function handleRoutingFormOption(query: ParsedUrlQuery, eventTypeId: number) {
|
||||
const nullReturnValue = { email: null, skipContactOwner: false };
|
||||
|
||||
if (typeof query.email !== "string") return nullReturnValue;
|
||||
|
||||
const routingFormQuery = await prisma.app_RoutingForms_Form.findFirst({
|
||||
where: {
|
||||
responses: {
|
||||
some: {
|
||||
id: Number(query[ROUTING_FORM_RESPONSE_ID_QUERY_STRING]),
|
||||
},
|
||||
},
|
||||
},
|
||||
select: {
|
||||
routes: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!routingFormQuery || !routingFormQuery?.routes) return nullReturnValue;
|
||||
|
||||
const parsedRoutes = routesSchema.safeParse(routingFormQuery.routes);
|
||||
|
||||
if (!parsedRoutes.success || !parsedRoutes.data) return nullReturnValue;
|
||||
|
||||
// Find the route with the attributeRoutingConfig
|
||||
const route = parsedRoutes.data.find((route) => {
|
||||
if ("action" in route) {
|
||||
return route.action.eventTypeId === eventTypeId;
|
||||
}
|
||||
});
|
||||
|
||||
if (!route || !("attributeRoutingConfig" in route)) return nullReturnValue;
|
||||
|
||||
// Get attributeRoutingConfig for the form
|
||||
const attributeRoutingConfig = route.attributeRoutingConfig;
|
||||
|
||||
if (!attributeRoutingConfig) return nullReturnValue;
|
||||
|
||||
// If the skipContactOwner is enabled then don't return an team member email
|
||||
if (attributeRoutingConfig?.skipContactOwner) return { ...nullReturnValue, skipContactOwner: true };
|
||||
|
||||
// Determine if a routing form enabled app is in the query. Then pass it to the proper handler
|
||||
// Routing form apps will have the format cal.appSlug
|
||||
let enabledRoutingFormApp;
|
||||
|
||||
for (const key of Object.keys(query)) {
|
||||
const keySplit = key.split(".");
|
||||
|
||||
const appSlug = keySplit[1];
|
||||
|
||||
if (enabledAppSlugs.includes(appSlug)) {
|
||||
enabledRoutingFormApp = appSlug;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!enabledRoutingFormApp) return nullReturnValue;
|
||||
|
||||
const appBookingFormHandler = (await import("@calcom/app-store/routing-forms/appBookingFormHandler"))
|
||||
.default;
|
||||
const appHandler = appBookingFormHandler[enabledRoutingFormApp];
|
||||
|
||||
if (!appHandler) return nullReturnValue;
|
||||
|
||||
const { email: userEmail } = await appHandler(query.email, attributeRoutingConfig, eventTypeId);
|
||||
|
||||
if (!userEmail) return nullReturnValue;
|
||||
|
||||
// Determine if the user is a part of the event type
|
||||
const userQuery = await await prisma.user.findFirst({
|
||||
where: {
|
||||
email: userEmail,
|
||||
hosts: {
|
||||
some: {
|
||||
eventTypeId: eventTypeId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!userQuery) return nullReturnValue;
|
||||
|
||||
return { ...nullReturnValue, email: userEmail };
|
||||
}
|
||||
|
||||
async function getTeamMemberEmail(eventData: EventData, email: string): Promise<string | null> {
|
||||
// Pre-requisites
|
||||
if (!eventData || !email || eventData.schedulingType !== SchedulingType.ROUND_ROBIN) return null;
|
||||
const crmContactOwnerEmail = await getCRMContactOwnerForRRLeadSkip(email, eventData.metadata);
|
||||
|
||||
@@ -315,8 +315,23 @@ export default function Success(props: PageProps) {
|
||||
return t(`needs_to_be_confirmed_or_rejected${titleSuffix}`);
|
||||
}
|
||||
if (bookingInfo.user) {
|
||||
return t(`${titlePrefix}emailed_you_and_attendees${titleSuffix}`, {
|
||||
user: bookingInfo.user.name || bookingInfo.user.email,
|
||||
const isHost = bookingInfo.user.id === session?.user?.id;
|
||||
const isAttendee = bookingInfo.attendees.find((attendee) => attendee.email === session?.user?.email);
|
||||
const attendee = bookingInfo.attendees[0]?.name || bookingInfo.attendees[0]?.email || "Nameless";
|
||||
const host = bookingInfo.user.name || bookingInfo.user.email;
|
||||
if (isHost) {
|
||||
return t(`${titlePrefix}emailed_you_and_attendees${titleSuffix}`, {
|
||||
user: attendee,
|
||||
});
|
||||
}
|
||||
if (isAttendee) {
|
||||
return t(`${titlePrefix}emailed_you_and_attendees${titleSuffix}`, {
|
||||
user: host,
|
||||
});
|
||||
}
|
||||
return t(`${titlePrefix}emailed_host_and_attendee${titleSuffix}`, {
|
||||
host,
|
||||
attendee,
|
||||
});
|
||||
}
|
||||
return t(`emailed_you_and_attendees${titleSuffix}`);
|
||||
|
||||
@@ -18,8 +18,9 @@ import { InfiniteSkeletonLoader } from "@calcom/features/eventtypes/components/S
|
||||
import { getTeamsFiltersFromQuery } from "@calcom/features/filters/lib/getTeamsFiltersFromQuery";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { parseEventTypeColor } from "@calcom/lib";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import { APP_NAME, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useCopy } from "@calcom/lib/hooks/useCopy";
|
||||
import { useDebounce } from "@calcom/lib/hooks/useDebounce";
|
||||
import { useInViewObserver } from "@calcom/lib/hooks/useInViewObserver";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
@@ -80,7 +81,7 @@ interface InfiniteEventTypeListProps {
|
||||
group: InfiniteEventTypeGroup;
|
||||
readOnly: boolean;
|
||||
bookerUrl: string | null;
|
||||
pages: { nextCursor: number | undefined; eventTypes: InfiniteEventType[] }[] | undefined;
|
||||
pages: { nextCursor: number | null | undefined; eventTypes: InfiniteEventType[] }[] | undefined;
|
||||
lockedByOrg?: boolean;
|
||||
isPending?: boolean;
|
||||
debouncedSearchTerm?: string;
|
||||
@@ -257,6 +258,7 @@ export const InfiniteEventTypeList = ({
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const searchParams = useCompatSearchParams();
|
||||
const { copyToClipboard } = useCopy();
|
||||
const [parent] = useAutoAnimate<HTMLUListElement>();
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [deleteDialogTypeId, setDeleteDialogTypeId] = useState(0);
|
||||
@@ -347,9 +349,14 @@ export const InfiniteEventTypeList = ({
|
||||
group: { teamId: group?.teamId, parentId: group?.parentId },
|
||||
},
|
||||
(data) => {
|
||||
if (!data) return { pages: [], pageParams: [] };
|
||||
|
||||
return {
|
||||
pageParams: data?.pageParams ?? [],
|
||||
pages: newOrder,
|
||||
...data,
|
||||
pages: newOrder.map((page) => ({
|
||||
...page,
|
||||
nextCursor: page.nextCursor ?? undefined,
|
||||
})),
|
||||
};
|
||||
}
|
||||
);
|
||||
@@ -566,7 +573,7 @@ export const InfiniteEventTypeList = ({
|
||||
StartIcon="link"
|
||||
onClick={() => {
|
||||
showToast(t("link_copied"), "success");
|
||||
navigator.clipboard.writeText(calLink);
|
||||
copyToClipboard(calLink);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
@@ -579,7 +586,7 @@ export const InfiniteEventTypeList = ({
|
||||
StartIcon="venetian-mask"
|
||||
onClick={() => {
|
||||
showToast(t("private_link_copied"), "success");
|
||||
navigator.clipboard.writeText(placeholderHashedLink);
|
||||
copyToClipboard(placeholderHashedLink);
|
||||
setPrivateLinkCopyIndices((prev) => {
|
||||
const prevIndex = prev[type.slug] ?? 0;
|
||||
prev[type.slug] = (prevIndex + 1) % type.hashedLink.length;
|
||||
@@ -893,6 +900,7 @@ const InfiniteScrollMain = ({
|
||||
}) => {
|
||||
const searchParams = useCompatSearchParams();
|
||||
const { data } = useTypedQuery(querySchema);
|
||||
const orgBranding = useOrgBranding();
|
||||
|
||||
if (status === "error") {
|
||||
return <Alert severity="error" title="Something went wrong" message={errorMessage} />;
|
||||
@@ -911,6 +919,18 @@ const InfiniteScrollMain = ({
|
||||
const activeEventTypeGroup =
|
||||
eventTypeGroups.filter((item) => item.teamId === data.teamId) ?? eventTypeGroups[0];
|
||||
|
||||
const bookerUrl = orgBranding ? orgBranding?.fullDomain : WEBSITE_URL;
|
||||
|
||||
// If the event type group is the same as the org branding team, or the parent team, set the bookerUrl to the org branding URL
|
||||
// This is to ensure that the bookerUrl is always the same as the one in the org branding settings
|
||||
// This keeps the app working for personal event types that were not migrated to the org (rare)
|
||||
if (
|
||||
activeEventTypeGroup[0].teamId === orgBranding?.id ||
|
||||
activeEventTypeGroup[0].parentId === orgBranding?.id
|
||||
) {
|
||||
activeEventTypeGroup[0].bookerUrl = bookerUrl;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{eventTypeGroups.length >= 1 && (
|
||||
|
||||
@@ -55,11 +55,11 @@ export default function PlatformBillingUpgrade() {
|
||||
return (
|
||||
<div>
|
||||
<Shell
|
||||
heading="Platform billing"
|
||||
title="Platform billing"
|
||||
heading={t("platform_billing")}
|
||||
title={t("platform_billing")}
|
||||
hideHeadingOnMobile
|
||||
withoutMain={false}
|
||||
subtitle="Manage all things billing"
|
||||
subtitle={t("manage_billing_description")}
|
||||
isPlatformUser={true}>
|
||||
<>
|
||||
<div className="border-subtle space-y-6 rounded-lg border px-6 py-8 text-sm sm:space-y-8">
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useRouter } from "next/navigation";
|
||||
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { ErrorCode } from "@calcom/lib/errorCodes";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { PERMISSIONS_GROUPED_MAP } from "@calcom/platform-constants";
|
||||
import { showToast } from "@calcom/ui";
|
||||
|
||||
@@ -30,6 +31,7 @@ import {
|
||||
} from "../../../../../../../../packages/platform/utils/permissions";
|
||||
|
||||
export default function EditOAuthClient() {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const params = useParams<{ clientId: string }>();
|
||||
const clientId = params?.clientId || "";
|
||||
@@ -78,15 +80,15 @@ export default function EditOAuthClient() {
|
||||
if (isPlatformUser && isPaidUser) {
|
||||
return (
|
||||
<div>
|
||||
<Shell title="OAuth client updation form" isPlatformUser={true}>
|
||||
<Shell title={t("oAuth_client_updation_form")} isPlatformUser={true}>
|
||||
<div className="m-2 md:mx-14 md:mx-5">
|
||||
<div className="border-subtle mx-auto block justify-between rounded-t-lg border px-4 py-6 sm:flex sm:px-6">
|
||||
<div className="flex w-full flex-col">
|
||||
<h1 className="font-cal text-emphasis mb-1 text-xl font-semibold leading-5 tracking-wide">
|
||||
OAuth client updation form
|
||||
{t("oAuth_client_updation_form")}
|
||||
</h1>
|
||||
<p className="text-default text-sm ltr:mr-4 rtl:ml-4">
|
||||
This is the form to edit an existing OAuth client
|
||||
{t("oAuth_client_updation_form_description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+7
-3
@@ -5,6 +5,7 @@ import { useRouter } from "next/navigation";
|
||||
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { WebhookForm } from "@calcom/features/webhooks/components";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { WebhookTriggerEvents } from "@calcom/prisma/enums";
|
||||
import { showToast } from "@calcom/ui";
|
||||
|
||||
@@ -18,6 +19,7 @@ import NoPlatformPlan from "@components/settings/platform/dashboard/NoPlatformPl
|
||||
import { useGetUserAttributes } from "@components/settings/platform/hooks/useGetUserAttributes";
|
||||
|
||||
export default function EditOAuthClientWebhooks() {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const params = useParams<{ clientId: string }>();
|
||||
const clientId = params?.clientId || "";
|
||||
@@ -40,14 +42,16 @@ export default function EditOAuthClientWebhooks() {
|
||||
if (isPlatformUser && isPaidUser) {
|
||||
return (
|
||||
<div>
|
||||
<Shell title="OAuth client updation form" isPlatformUser={true}>
|
||||
<Shell title={t("webhook_update_form")} isPlatformUser={true}>
|
||||
<div className="m-2 md:mx-5">
|
||||
<div className="border-subtle mx-auto block justify-between rounded-t-lg border px-4 py-6 sm:flex sm:px-6">
|
||||
<div className="flex w-full flex-col">
|
||||
<h1 className="font-cal text-emphasis mb-1 text-xl font-semibold leading-5 tracking-wide">
|
||||
Webhook update form
|
||||
{t("webhook_update_form")}
|
||||
</h1>
|
||||
<p className="text-default text-sm ltr:mr-4 rtl:ml-4">Add a webhook to your OAuthClient.</p>
|
||||
<p className="text-default text-sm ltr:mr-4 rtl:ml-4">
|
||||
{t("webhook_update_form_description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useRouter } from "next/navigation";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { ErrorCode } from "@calcom/lib/errorCodes";
|
||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { PERMISSIONS_GROUPED_MAP } from "@calcom/platform-constants/permissions";
|
||||
import { showToast } from "@calcom/ui";
|
||||
|
||||
@@ -18,6 +19,7 @@ import { OAuthClientForm } from "@components/settings/platform/oauth-clients/oau
|
||||
export default function CreateOAuthClient() {
|
||||
const searchParams = useCompatSearchParams();
|
||||
const router = useRouter();
|
||||
const { t } = useLocale();
|
||||
const clientId = searchParams?.get("clientId") || "";
|
||||
|
||||
const { isUserLoading, isPlatformUser, isPaidUser } = useGetUserAttributes();
|
||||
@@ -62,15 +64,15 @@ export default function CreateOAuthClient() {
|
||||
if (isPlatformUser && isPaidUser) {
|
||||
return (
|
||||
<div>
|
||||
<Shell title={`OAuth client ${!!clientId ? "updation" : "creation"} form`} isPlatformUser={true}>
|
||||
<Shell title={t("oAuth_client_creation_form")} isPlatformUser={true}>
|
||||
<div className="m-2 md:mx-5">
|
||||
<div className="border-subtle mx-auto block justify-between rounded-t-lg border px-4 py-6 sm:flex sm:px-6">
|
||||
<div className="flex w-full flex-col">
|
||||
<h1 className="font-cal text-emphasis mb-1 text-xl font-semibold leading-5 tracking-wide">
|
||||
OAuth client creation form
|
||||
{t("oAuth_client_creation_form")}
|
||||
</h1>
|
||||
<p className="text-default text-sm ltr:mr-4 rtl:ml-4">
|
||||
This is the form to create a new OAuth client
|
||||
{t("oAuth_client_creation_form_description")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -624,7 +624,7 @@ export default function Signup({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="border-subtle lg:bg-subtle mx-auto mt-24 w-full max-w-2xl flex-col justify-between rounded-l-2xl pl-4 dark:bg-none lg:mt-0 lg:flex lg:max-w-full lg:border lg:py-12 lg:pl-12">
|
||||
<div className="border-subtle lg:bg-subtle mx-auto mt-24 w-full max-w-2xl flex-col justify-between rounded-l-2xl pl-4 lg:mt-0 lg:flex lg:max-w-full lg:border lg:py-12 lg:pl-12 dark:bg-none">
|
||||
{IS_CALCOM && (
|
||||
<>
|
||||
<div className="-mt-4 mb-6 mr-12 grid w-full grid-cols-3 gap-5 pr-4 sm:gap-3 lg:grid-cols-4">
|
||||
@@ -675,7 +675,7 @@ export default function Signup({
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="border-default hidden rounded-bl-2xl rounded-br-none rounded-tl-2xl border border-r-0 border-dashed bg-black/[3%] dark:bg-white/5 lg:block lg:py-[6px] lg:pl-[6px]">
|
||||
<div className="border-default hidden rounded-bl-2xl rounded-br-none rounded-tl-2xl border border-r-0 border-dashed bg-black/[3%] lg:block lg:py-[6px] lg:pl-[6px] dark:bg-white/5">
|
||||
<img className="block dark:hidden" src="/mock-event-type-list.svg" alt="Cal.com Booking Page" />
|
||||
<img
|
||||
className="hidden dark:block"
|
||||
@@ -686,7 +686,7 @@ export default function Signup({
|
||||
<div className="mr-12 mt-8 hidden h-full w-full grid-cols-3 gap-4 overflow-hidden lg:grid">
|
||||
{FEATURES.map((feature) => (
|
||||
<>
|
||||
<div className="max-w-52 mb-8 flex flex-col leading-none sm:mb-0">
|
||||
<div className="mb-8 flex max-w-52 flex-col leading-none sm:mb-0">
|
||||
<div className="text-emphasis items-center">
|
||||
<Icon name={feature.icon} className="mb-1 h-4 w-4" />
|
||||
<span className="text-sm font-medium">{t(feature.title)}</span>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "4.6.16",
|
||||
"version": "4.6.24",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
@@ -74,7 +74,7 @@
|
||||
"@upstash/redis": "^1.21.0",
|
||||
"@vercel/edge-config": "^0.1.1",
|
||||
"@vercel/edge-functions-ui": "^0.2.1",
|
||||
"@vercel/og": "^0.5.0",
|
||||
"@vercel/og": "^0.6.3",
|
||||
"accept-language-parser": "^1.5.0",
|
||||
"async": "^3.2.4",
|
||||
"bcp-47-match": "^2.0.3",
|
||||
@@ -109,6 +109,8 @@
|
||||
"nodemailer": "^6.7.8",
|
||||
"nuqs": "^1.20.0",
|
||||
"otplib": "^12.0.1",
|
||||
"posthog-js": "^1.164.1",
|
||||
"posthog-node": "^4.2.0",
|
||||
"qrcode": "^1.5.1",
|
||||
"raw-body": "^2.5.1",
|
||||
"react": "^18.2.0",
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import Head from "next/head";
|
||||
|
||||
import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button } from "@calcom/ui";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
export default function AccessDenied() {
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<div className="bg-subtle flex h-screen">
|
||||
<Head>
|
||||
<title>{`${t("access_denied")} | ${APP_NAME}`}</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<div className="rtl: bg-default m-auto rounded-md p-10 text-right ltr:text-left">
|
||||
<h1 className="font-cal text-emphasis text-6xl">403</h1>
|
||||
<h2 className="text-emphasis mt-6 text-2xl font-medium">{t("dont_have_access_this_page")}</h2>
|
||||
<p className="text-default mb-6 mt-4 max-w-2xl text-sm">
|
||||
{t("you_need_admin_or_owner_privileges_to_access")}
|
||||
</p>
|
||||
<Button href={WEBAPP_URL}>{t("go_back_home")}</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
AccessDenied.PageWrapper = PageWrapper;
|
||||
@@ -83,9 +83,12 @@ export default async function handler(req: NextApiRequest) {
|
||||
/>
|
||||
),
|
||||
ogConfig
|
||||
) as { body: Buffer };
|
||||
);
|
||||
|
||||
return new Response(img.body, { status: 200, headers: { "Content-Type": "image/png" } });
|
||||
return new Response(img.body, {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "image/png", "cache-control": "max-age=86400" },
|
||||
});
|
||||
}
|
||||
case "app": {
|
||||
const { name, description, slug } = appSchema.parse({
|
||||
@@ -94,9 +97,7 @@ export default async function handler(req: NextApiRequest) {
|
||||
slug: searchParams.get("slug"),
|
||||
imageType,
|
||||
});
|
||||
const img = new ImageResponse(<App name={name} description={description} slug={slug} />, ogConfig) as {
|
||||
body: Buffer;
|
||||
};
|
||||
const img = new ImageResponse(<App name={name} description={description} slug={slug} />, ogConfig);
|
||||
|
||||
return new Response(img.body, { status: 200, headers: { "Content-Type": "image/png" } });
|
||||
}
|
||||
@@ -108,9 +109,7 @@ export default async function handler(req: NextApiRequest) {
|
||||
imageType,
|
||||
});
|
||||
|
||||
const img = new ImageResponse(<Generic title={title} description={description} />, ogConfig) as {
|
||||
body: Buffer;
|
||||
};
|
||||
const img = new ImageResponse(<Generic title={title} description={description} />, ogConfig);
|
||||
|
||||
return new Response(img.body, { status: 200, headers: { "Content-Type": "image/png" } });
|
||||
}
|
||||
|
||||
@@ -80,9 +80,6 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
},
|
||||
});
|
||||
|
||||
// Sync Services: Close.com
|
||||
// closeComUpdateTeam(prevTeam, team);
|
||||
|
||||
// redirect to team screen
|
||||
res.redirect(302, `/settings/teams/${team.id}/onboard-members?event=team_created`);
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Meta } from "@calcom/ui";
|
||||
|
||||
import { getServerSideProps } from "@lib/settings/license-key/new/getServerSideProps";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
import CreateANewLicenseKeyForm, { LayoutWrapper } from "~/settings/license-key/new/new-view";
|
||||
|
||||
const CreateNewLicenseKeyPage = () => {
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
<>
|
||||
<Meta title={t("set_up_your_organization")} description={t("organizations_description")} />
|
||||
<CreateANewLicenseKeyForm />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
CreateNewLicenseKeyPage.getLayout = LayoutWrapper;
|
||||
CreateNewLicenseKeyPage.PageWrapper = PageWrapper;
|
||||
|
||||
export default CreateNewLicenseKeyPage;
|
||||
|
||||
export { getServerSideProps };
|
||||
@@ -1,12 +0,0 @@
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
import PlatformBillingUpgrade from "~/settings/platform/billing/billing-view";
|
||||
|
||||
const Page = new Proxy<{
|
||||
(): JSX.Element;
|
||||
PageWrapper?: typeof PageWrapper;
|
||||
}>(PlatformBillingUpgrade, {});
|
||||
|
||||
Page.PageWrapper = PageWrapper;
|
||||
|
||||
export default Page;
|
||||
@@ -1,12 +0,0 @@
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
import PlatformView from "~/settings/platform/platform-view";
|
||||
|
||||
const Page = new Proxy<{
|
||||
(): JSX.Element;
|
||||
PageWrapper?: typeof PageWrapper;
|
||||
}>(PlatformView, {});
|
||||
|
||||
Page.PageWrapper = PageWrapper;
|
||||
|
||||
export default Page;
|
||||
@@ -1,64 +0,0 @@
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Meta } from "@calcom/ui";
|
||||
import { SkeletonContainer, SkeletonText } from "@calcom/ui";
|
||||
|
||||
import { getServerSideProps } from "@lib/settings/organizations/new/getServerSideProps";
|
||||
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
import usePlatformMe from "@components/settings/platform/hooks/usePlatformMe";
|
||||
|
||||
import CreateNewOrganizationPage, { LayoutWrapper } from "~/settings/platform/new/create-new-view";
|
||||
|
||||
const SkeletonLoader = () => {
|
||||
return (
|
||||
<SkeletonContainer>
|
||||
<div className="mr-3">
|
||||
<SkeletonText className="h-4 w-28" />
|
||||
<SkeletonText className="mt-3 h-11 w-full" />
|
||||
<SkeletonText className="mt-2 h-11 w-full" />
|
||||
</div>
|
||||
</SkeletonContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const Page = () => {
|
||||
const { t } = useLocale();
|
||||
const { isFetching, data: platformMe } = usePlatformMe();
|
||||
const router = useRouter();
|
||||
|
||||
if (isFetching) {
|
||||
return <SkeletonLoader />;
|
||||
}
|
||||
|
||||
if (platformMe?.organization?.id) {
|
||||
// if user has a platform org redirect to platform dashboard
|
||||
if (platformMe?.organization?.isPlatform) {
|
||||
router.push("/settings/platform");
|
||||
// user has a regular org redirect to organization settings
|
||||
} else {
|
||||
router.push("/settings/organizations/profile");
|
||||
}
|
||||
// display loader while redirection is happening
|
||||
return <SkeletonLoader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<LicenseRequired>
|
||||
<Meta
|
||||
title={t("set_up_your_platform_organization")}
|
||||
description={t("platform_organization_description")}
|
||||
/>
|
||||
<CreateNewOrganizationPage />
|
||||
</LicenseRequired>
|
||||
);
|
||||
};
|
||||
|
||||
Page.getLayout = LayoutWrapper;
|
||||
Page.PageWrapper = PageWrapper;
|
||||
|
||||
export default Page;
|
||||
|
||||
export { getServerSideProps };
|
||||
@@ -1,12 +0,0 @@
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
import EditView from "~/settings/platform/oauth-clients/[clientId]/edit/edit-view";
|
||||
|
||||
const Page = new Proxy<{
|
||||
(): JSX.Element;
|
||||
PageWrapper?: typeof PageWrapper;
|
||||
}>(EditView, {});
|
||||
|
||||
Page.PageWrapper = PageWrapper;
|
||||
|
||||
export default Page;
|
||||
@@ -1,12 +0,0 @@
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
import EditWebhooksView from "~/settings/platform/oauth-clients/[clientId]/edit/edit-webhooks-view";
|
||||
|
||||
const Page = new Proxy<{
|
||||
(): JSX.Element;
|
||||
PageWrapper?: typeof PageWrapper;
|
||||
}>(EditWebhooksView, {});
|
||||
|
||||
Page.PageWrapper = PageWrapper;
|
||||
|
||||
export default Page;
|
||||
@@ -1,12 +0,0 @@
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
import CreateOAuthClientView from "~/settings/platform/oauth-clients/create-new-view";
|
||||
|
||||
const Page = new Proxy<{
|
||||
(): JSX.Element;
|
||||
PageWrapper?: typeof PageWrapper;
|
||||
}>(CreateOAuthClientView, {});
|
||||
|
||||
Page.PageWrapper = PageWrapper;
|
||||
|
||||
export default Page;
|
||||
@@ -1,12 +0,0 @@
|
||||
import PageWrapper from "@components/PageWrapper";
|
||||
|
||||
import PlatformPlansView from "~/settings/platform/plans/platform-plans-view";
|
||||
|
||||
const Page = new Proxy<{
|
||||
(): JSX.Element;
|
||||
PageWrapper?: typeof PageWrapper;
|
||||
}>(PlatformPlansView, {});
|
||||
|
||||
Page.PageWrapper = PageWrapper;
|
||||
|
||||
export default Page;
|
||||
@@ -94,6 +94,7 @@ const createTeamEventType = async (
|
||||
teamEventLength?: number;
|
||||
seatsPerTimeSlot?: number;
|
||||
managedEventUnlockedFields?: Record<string, boolean>;
|
||||
assignAllTeamMembers?: boolean;
|
||||
}
|
||||
) => {
|
||||
return await prisma.eventType.create({
|
||||
@@ -138,6 +139,7 @@ const createTeamEventType = async (
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
assignAllTeamMembers: scenario?.assignAllTeamMembers,
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -154,6 +156,7 @@ const createTeamAndAddUser = async (
|
||||
index,
|
||||
orgRequestedSlug,
|
||||
schedulingType,
|
||||
assignAllTeamMembersForSubTeamEvents,
|
||||
}: {
|
||||
user: { id: number; email: string; username: string | null; role?: MembershipRole };
|
||||
isUnpublished?: boolean;
|
||||
@@ -165,6 +168,7 @@ const createTeamAndAddUser = async (
|
||||
index?: number;
|
||||
orgRequestedSlug?: string;
|
||||
schedulingType?: SchedulingType;
|
||||
assignAllTeamMembersForSubTeamEvents?: boolean;
|
||||
},
|
||||
workerInfo: WorkerInfo
|
||||
) => {
|
||||
@@ -193,6 +197,7 @@ const createTeamAndAddUser = async (
|
||||
const team = await createTeamAndAddUser({ user }, workerInfo);
|
||||
await createTeamEventType(user, team, {
|
||||
schedulingType: schedulingType,
|
||||
assignAllTeamMembers: assignAllTeamMembersForSubTeamEvents,
|
||||
});
|
||||
await createTeamWorkflow(user, team);
|
||||
data.children = { connect: [{ id: team.id }] };
|
||||
@@ -282,6 +287,8 @@ export const createUsersFixture = (
|
||||
addManagedEventToTeamMates?: boolean;
|
||||
managedEventUnlockedFields?: Record<string, boolean>;
|
||||
orgRequestedSlug?: string;
|
||||
assignAllTeamMembers?: boolean;
|
||||
assignAllTeamMembersForSubTeamEvents?: boolean;
|
||||
} = {}
|
||||
) => {
|
||||
const _user = await prisma.user.create({
|
||||
@@ -537,6 +544,7 @@ export const createUsersFixture = (
|
||||
organizationId: opts?.organizationId,
|
||||
orgRequestedSlug: scenario.orgRequestedSlug,
|
||||
schedulingType: scenario.schedulingType,
|
||||
assignAllTeamMembersForSubTeamEvents: scenario.assignAllTeamMembersForSubTeamEvents,
|
||||
},
|
||||
workerInfo
|
||||
);
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 221 KiB After Width: | Height: | Size: 221 KiB |
@@ -1,6 +1,8 @@
|
||||
import { expect } from "@playwright/test";
|
||||
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import { MembershipRole, SchedulingType } from "@calcom/prisma/enums";
|
||||
|
||||
import { test } from "../lib/fixtures";
|
||||
import { getInviteLink, localize } from "../lib/testUtils";
|
||||
@@ -121,4 +123,60 @@ test.describe("Team", () => {
|
||||
).toHaveCount(1);
|
||||
});
|
||||
});
|
||||
|
||||
test("Invited member is assigned to existing managed event, after invitation is accepted", async ({
|
||||
page,
|
||||
users,
|
||||
}) => {
|
||||
const t = await localize("en");
|
||||
const teamEventSlugAndTitle = "managed-event-test";
|
||||
const teamMatesObj = [{ name: "teammate-1" }, { name: "teammate-2" }];
|
||||
const teamOwner = await users.create(
|
||||
{ name: `team-owner-${Date.now()}` },
|
||||
{
|
||||
hasTeam: true,
|
||||
teamRole: MembershipRole.ADMIN,
|
||||
teammates: teamMatesObj,
|
||||
schedulingType: SchedulingType.MANAGED,
|
||||
teamEventSlug: teamEventSlugAndTitle,
|
||||
teamEventTitle: teamEventSlugAndTitle,
|
||||
teamEventLength: 30,
|
||||
addManagedEventToTeamMates: true,
|
||||
assignAllTeamMembers: true,
|
||||
}
|
||||
);
|
||||
const invitedMember = await users.create({
|
||||
name: `invited-member-${Date.now()}`,
|
||||
email: `invited-member-${Date.now()}@example.com`,
|
||||
});
|
||||
const { team } = await teamOwner.getFirstTeamMembership();
|
||||
|
||||
await teamOwner.apiLogin();
|
||||
await page.goto(`/settings/teams/${team.id}/members`);
|
||||
await page.getByTestId("new-member-button").click();
|
||||
await page.locator('input[name="inviteUser"]').fill(invitedMember.email);
|
||||
await page.locator(`button:text("${t("send_invite")}")`).click();
|
||||
|
||||
await invitedMember.apiLogin();
|
||||
await page.goto(`/teams`);
|
||||
await page.getByTestId(`accept-invitation-${team.id}`).click();
|
||||
const response = await page.waitForResponse("/api/trpc/teams/acceptOrLeave?batch=1");
|
||||
expect(response.status()).toBe(200);
|
||||
await page.goto(`/event-types`);
|
||||
|
||||
//ensure managed event-type is created for the invited member
|
||||
await expect(page.locator(`text="${teamEventSlugAndTitle}"`)).toBeVisible();
|
||||
|
||||
//ensure the new event-type created for invited member is child of team event-type
|
||||
const parentEventType = await prisma.eventType.findFirst({
|
||||
where: {
|
||||
slug: teamEventSlugAndTitle,
|
||||
teamId: team.id,
|
||||
},
|
||||
select: {
|
||||
children: true,
|
||||
},
|
||||
});
|
||||
expect(parentEventType?.children.find((et) => et.userId === invitedMember.id)).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -161,6 +161,10 @@
|
||||
<symbol class="lucide lucide-chevron-up" viewBox="0 0 24 24" fill="inherit" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" id="chevron-up">
|
||||
<path d="m18 15-6-6-6 6" ></path>
|
||||
</symbol>
|
||||
<symbol class="lucide lucide-chevrons-down-up" viewBox="0 0 24 24" fill="inherit" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" id="chevrons-down-up">
|
||||
<path d="m7 20 5-5 5 5" ></path>
|
||||
<path d="m7 4 5 5 5-5" ></path>
|
||||
</symbol>
|
||||
<symbol class="lucide lucide-chevrons-left" viewBox="0 0 24 24" fill="inherit" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" id="chevrons-left">
|
||||
<path d="m11 17-5-5 5-5" ></path>
|
||||
<path d="m18 17-5-5 5-5" ></path>
|
||||
|
||||
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 48 KiB |
@@ -139,7 +139,8 @@
|
||||
"requested_to_reschedule_subject_attendee": "الإجراء المطلوب إعادة الجدولة: يُرجى حجز وقت جديد لمدة {{eventType}} مع {{name}}",
|
||||
"hi_user_name": "مرحبا {{name}}",
|
||||
"ics_event_title": "{{eventType}} مع {{name}}",
|
||||
"please_book_a_time_sometime_later": "عفواً، لم نتمكن من الاتصال بك هذه المرة. الرجاء جدولة مكالمة مستقبلية بدلاً من ذلك",
|
||||
"please_book_a_time_sometime_later": "يبدو أن جميع خبرائنا مشغولون حاليًا. يرجى تحديد موعد مكالمة في الوقت الذي يناسبك وسنكون جاهزين لمساعدتك.",
|
||||
"schedule_instead": "حدد موعدًا بدلاً من ذلك",
|
||||
"new_event_subject": "حدث جديد: {{attendeeName}} - {{date}} - {{eventType}}",
|
||||
"join_by_entrypoint": "الانضمام بواسطة {{entryPoint}}",
|
||||
"notes": "ملاحظات",
|
||||
@@ -198,6 +199,8 @@
|
||||
"webhook_disabled": "تعطيل الويبهوك",
|
||||
"webhook_response": "رد الويبهوك",
|
||||
"webhook_test": "اختبار ويبهوك",
|
||||
"webhook_update_form": "نموذج تحديث الويب هوك",
|
||||
"webhook_update_form_description": "أضف ويب هوك إلى عميل OAuth الخاص بك.",
|
||||
"manage_your_webhook": "إدارة الويبهوك الخاص بك",
|
||||
"webhook_created_successfully": "تم إنشاء الويبهوك بنجاح!",
|
||||
"webhook_updated_successfully": "تم تحديث الويبهوك بنجاح!",
|
||||
@@ -348,9 +351,13 @@
|
||||
"other": "آخر",
|
||||
"email_sign_in_subject": "رابط تسجيل الدخول بك لـ{{appName}}",
|
||||
"round_robin_emailed_you_and_attendees": "أنت تجتمع مع {{user}}. أرسلنا بريداً إلكترونياً مع دعوة اجتماع بكل التفاصيل إلى الجميع.",
|
||||
"round_robin_emailed_host_and_attendee": "{{host}} سيجتمع مع {{attendee}}. لقد أرسلنا بريداً إلكترونياً يحتوي على دعوة تقويم مع كافة التفاصيل للجميع.",
|
||||
"emailed_you_and_attendees": "لقد أرسلنا إليك وإلى الحضور الآخرين دعوة للتقويم عبر البريد الإلكتروني تتضمن كل التفاصيل.",
|
||||
"emailed_host_and_attendee": "لقد أرسلنا بريداً إلكترونياً يحتوي على دعوة تقويم مع كافة التفاصيل للجميع.",
|
||||
"emailed_you_and_attendees_recurring": "لقد أرسلنا إليك وإلى الحضور الآخرين دعوة تقويم عبر البريد الإلكتروني لأول هذه الأحداث المتكررة.",
|
||||
"round_robin_emailed_you_and_attendees_recurring": "أنت تجتمع مع {{user}}. أرسلنا بريداً إلكترونياً مع دعوة اجتماع بكل التفاصيل إلى الجميع لأول اجتماع من هذه الاجتماعات الدورية.",
|
||||
"emailed_host_and_attendee_recurring": "لقد أرسلنا بريداً إلكترونياً يحتوي على دعوة تقويم مع كافة التفاصيل للجميع للحدث الأول من هذه الأحداث المتكررة.",
|
||||
"round_robin_emailed_you_and_attendees_recurring": "أنت على موعد مع {{user}}. لقد أرسلنا بريداً إلكترونياً يحتوي على دعوة تقويم مع كافة التفاصيل للجميع للحدث الأول من هذه الأحداث المتكررة.",
|
||||
"round_robin_emailed_host_and_attendee_recurring": "{{host}} سيجتمع مع {{attendee}}. لقد أرسلنا بريداً إلكترونياً يحتوي على دعوة تقويم مع كافة التفاصيل للجميع للحدث الأول من هذه الأحداث المتكررة.",
|
||||
"emailed_you_and_any_other_attendees": "تم إرسال هذه المعلومات إليك وإلى الحضور الآخرين عبر البريد الإلكتروني.",
|
||||
"needs_to_be_confirmed_or_rejected": "لا يزال الحجز الخاص بك يحتاج إلى التأكيد أو الرفض.",
|
||||
"needs_to_be_confirmed_or_rejected_recurring": "لا يزال الاجتماع المتكرر الخاص بك بحاجة إلى التأكيد أو الرفض.",
|
||||
@@ -703,6 +710,9 @@
|
||||
"no_teams": "ليس لديك أي فرق حتى الآن.",
|
||||
"no_teams_description": "تسمح الفرق للآخرين بحجز الأحداث المشتركة بين زملائك في العمل.",
|
||||
"submit": "إرسال",
|
||||
"confirm_remove_attribute": "نعم، إزالة السمة",
|
||||
"remove_attribute": "إزالة السمة",
|
||||
"remove_attribute_confirmation_message": "هل أنت متأكد أنك تريد إزالة السمة '{{name}}'؟ سيتم إلغاء تعيين جميع المستخدمين المعينين لها حالياً.",
|
||||
"delete": "حذف",
|
||||
"update": "تحديث",
|
||||
"save": "حفظ",
|
||||
@@ -1630,6 +1640,7 @@
|
||||
"format": "التنسيق",
|
||||
"uppercase_for_letters": "استخدم الحروف الكبيرة لجميع الأحرف",
|
||||
"replace_whitespaces_underscores": "استبدل المساحات البيضاء بالشرطات السفلية",
|
||||
"platform_billing": "فوترة المنصة",
|
||||
"manage_billing": "إدارة الفواتير",
|
||||
"manage_billing_description": "إدارة كل ما يخص الفواتير",
|
||||
"billing_freeplan_title": "أنت حاليًا على الخطة المجانية",
|
||||
@@ -2037,6 +2048,8 @@
|
||||
"looking_for_more_insights": "هل تبحث عن المزيد من Insights؟",
|
||||
"filters": "الفلاتر",
|
||||
"add_filter": "إضافة عامل تصفية",
|
||||
"add_rule": "إضافة قاعدة",
|
||||
"add_rule_group": "إضافة مجموعة قواعد",
|
||||
"remove_filters": "إزالة جميع الفلاتر",
|
||||
"email_verified": "تم التحقق من البريد الإلكتروني",
|
||||
"select_user": "اختر المستخدم",
|
||||
@@ -2139,6 +2152,7 @@
|
||||
"set_up_your_organization": "إعداد منظمتك",
|
||||
"set_up_your_platform_organization": "إعداد منصتك",
|
||||
"organizations_description": "المنظمات هي بيئات مشتركة حيث يمكن للفرق إنشاء أنواع الأحداث المشتركة والتطبيقات ومهام سير العمل والمزيد.",
|
||||
"plans": "الخطط",
|
||||
"platform": "المنصة",
|
||||
"platform_description": "إدارة كل ما يتعلق بالمنصة.",
|
||||
"platform_organization_description": "تتيح لك منصة Cal.com دمج الجدولة بسهولة في تطبيقك باستخدام واجهات برمجة التطبيقات والعناصر.",
|
||||
@@ -2201,7 +2215,7 @@
|
||||
"add_times_to_your_email": "حدّد بعض الفترات المتاحة وأدرجها في رسالتك الإلكترونية",
|
||||
"select_time": "اختيار الوقت",
|
||||
"select_date": "اختيار التاريخ",
|
||||
"connecting_you_to_someone": "نقوم بربطك بشخص ما.",
|
||||
"connecting_you_to_someone": "نقوم بربطك بشخص من {{orgName}}.",
|
||||
"please_do_not_close_this_tab": "من فضلك لا تغلق أو تحدّث هذه الصفحة",
|
||||
"see_all_available_times": "رؤية كل الأوقات المتاحة",
|
||||
"org_team_names_example_1": "مثال، فريق التسويق",
|
||||
@@ -2276,6 +2290,10 @@
|
||||
"seat_options_doesnt_multiple_durations": "لا يدعم خيار المقعد فترات متعددة",
|
||||
"include_calendar_event": "إدراج فعاليات في التقويم",
|
||||
"oAuth": "OAuth",
|
||||
"oAuth_client_creation_form": "نموذج إنشاء عميل OAuth",
|
||||
"oAuth_client_creation_form_description": "هذا هو النموذج لإنشاء عميل OAuth جديد",
|
||||
"oAuth_client_updation_form": "نموذج تحديث عميل OAuth",
|
||||
"oAuth_client_updation_form_description": "هذا هو النموذج لتعديل عميل OAuth موجود",
|
||||
"recently_added": "تمت الإضافة مؤخراً",
|
||||
"connect_all_calendars": "قم بربط جميع التقويمات",
|
||||
"connect_all_calendars_description": "{{appName}} يقرأ التوافر من جميع تقاويمك الحالية.",
|
||||
@@ -2732,5 +2750,21 @@
|
||||
"event_end_time_in_attendee_timezone_info": "معلومات وقت انتهاء الحدث في المنطقة الزمنية للحاضر",
|
||||
"salesforce_create_event_on_contact": "إنشاء حدث على جهة الاتصال، إذا كانت موجودة. وإلا، الرجوع إلى العميل المحتمل",
|
||||
"salesforce_owner_name_to_change": "اسم المالك المراد تغييره",
|
||||
"dont_have_access_this_page": "ليس لديك صلاحية الوصول إلى هذه الصفحة",
|
||||
"you_need_admin_or_owner_privileges_to_access": "لرؤية هذه الصفحة أو التفاعل معها، يجب أن تكون لديك صلاحيات المدير أو المالك",
|
||||
"field_type": "نوع الحقل",
|
||||
"when_to_write": "متى تتم الكتابة",
|
||||
"salesforce_on_booking_write_to_record": "عند الحجز، اكتب في حقل مخصص في سجل الحاضر {{record}}",
|
||||
"on_every_booking": "عند كل حجز",
|
||||
"only_if_field_is_empty": "فقط إذا كان الحقل فارغًا",
|
||||
"booking_start_date": "تاريخ بدء الحجز",
|
||||
"booking_created_date": "تاريخ إنشاء الحجز",
|
||||
"booking_reassigned_to_host": "تم إعادة تعيين الحجز إلى {{host}}",
|
||||
"access_denied": "تم رفض الوصول",
|
||||
"salesforce_route_to_owner": "سيكون مالك جهة الاتصال هو مضيف Round Robin إذا كان متاحاً",
|
||||
"salesforce_do_not_route_to_owner": "لن يتم فرض مالك جهة الاتصال (يمكن أن يظل مضيفاً إذا تطابق مع السمات ومعايير Round Robin)",
|
||||
"salesforce_route_to_custom_lookup_field": "التوجيه إلى مستخدم يطابق حقل البحث في الحساب",
|
||||
"salesforce_option": "خيار Salesforce",
|
||||
"lookup_field_name": "اسم حقل البحث",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ أضف السلاسل الجديدة أعلاه هنا ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -139,7 +139,8 @@
|
||||
"requested_to_reschedule_subject_attendee": "Təxirə salma tələb olunur: {{eventType}} {{name}} ilə yeni vaxt təyin edin",
|
||||
"hi_user_name": "Salam {{name}}",
|
||||
"ics_event_title": "{{eventType}} {{name}} ilə",
|
||||
"please_book_a_time_sometime_later": "Bağışlayın, bu dəfə əlaqə qura bilmədik. Zəhmət olmasa, gələcək bir zəng üçün vaxt təyin edin",
|
||||
"please_book_a_time_sometime_later": "Görünür ki, hazırda bütün ekspertlərimiz məşğuldur. Zəhmət olmasa, sizə ən uyğun olan vaxtı seçin və biz sizə kömək etməyə hazır olacağıq.",
|
||||
"schedule_instead": "Əvəzində planlaşdırın",
|
||||
"new_event_subject": "Yeni tədbir: {{attendeeName}} - {{date}} - {{eventType}}",
|
||||
"join_by_entrypoint": "{{entryPoint}} ilə qoşulun",
|
||||
"notes": "Qeydlər",
|
||||
@@ -198,6 +199,8 @@
|
||||
"webhook_disabled": "Webhook Deaktivdir",
|
||||
"webhook_response": "Webhook cavabı",
|
||||
"webhook_test": "Webhook testi",
|
||||
"webhook_update_form": "Webhook yeniləmə forması",
|
||||
"webhook_update_form_description": "OAuthClient-ə webhook əlavə edin.",
|
||||
"manage_your_webhook": "Webhook-u idarə edin",
|
||||
"webhook_created_successfully": "Webhook uğurla yaradıldı!",
|
||||
"webhook_updated_successfully": "Webhook uğurla yeniləndi!",
|
||||
@@ -348,9 +351,13 @@
|
||||
"other": "Digər",
|
||||
"email_sign_in_subject": "{{appName}} üçün giriş linkiniz",
|
||||
"round_robin_emailed_you_and_attendees": "{{user}} ilə görüşürsünüz. Təfərrüatları olan təqvim dəvəti ilə hər kəsə e-poçt göndərdik.",
|
||||
"round_robin_emailed_host_and_attendee": "{{host}} {{attendee}} ilə görüşür. Təfərrüatları olan təqvim dəvəti ilə hər kəsə e-poçt göndərdik.",
|
||||
"emailed_you_and_attendees": "Təfərrüatları olan təqvim dəvəti ilə hər kəsə e-poçt göndərdik.",
|
||||
"emailed_host_and_attendee": "Təfərrüatları olan təqvim dəvəti ilə hər kəsə e-poçt göndərdik.",
|
||||
"emailed_you_and_attendees_recurring": "Bu təkrarlanan hadisələrin birincisi üçün təfərrüatları olan təqvim dəvəti ilə hər kəsə e-poçt göndərdik.",
|
||||
"round_robin_emailed_you_and_attendees_recurring": "{{user}} ilə görüşürsünüz. Bu təkrarlanan hadisələrin birincisi üçün təfərrüatları olan təqvim dəvəti ilə hər kəsə e-poçt göndərdik.",
|
||||
"emailed_host_and_attendee_recurring": "Bu təkrarlanan tədbirlərin birincisi üçün təfərrüatları olan təqvim dəvəti ilə hər kəsə e-poçt göndərdik.",
|
||||
"round_robin_emailed_you_and_attendees_recurring": "Siz {{user}} ilə görüşürsünüz. Bu təkrarlanan tədbirlərin birincisi üçün təfərrüatları olan təqvim dəvəti ilə hər kəsə e-poçt göndərdik.",
|
||||
"round_robin_emailed_host_and_attendee_recurring": "{{host}} {{attendee}} ilə görüşür. Bu təkrarlanan tədbirlərin birincisi üçün təfərrüatları olan təqvim dəvəti ilə hər kəsə e-poçt göndərdik.",
|
||||
"emailed_you_and_any_other_attendees": "Bu məlumatla hər kəsə e-poçt göndərdik.",
|
||||
"needs_to_be_confirmed_or_rejected": "Rezervasiyanız hələ də təsdiqlənməli və ya rədd edilməlidir.",
|
||||
"needs_to_be_confirmed_or_rejected_recurring": "Təkrarlanan görüşünüz hələ də təsdiqlənməli və ya rədd edilməlidir.",
|
||||
@@ -703,6 +710,9 @@
|
||||
"no_teams": "Hələ heç bir komandanız yoxdur.",
|
||||
"no_teams_description": "Komandalar həmkarlarınız arasında paylaşılan tədbirləri sifariş etməyə imkan verir.",
|
||||
"submit": "Təsdiqlə",
|
||||
"confirm_remove_attribute": "Bəli, atributu sil",
|
||||
"remove_attribute": "Atributu sil",
|
||||
"remove_attribute_confirmation_message": "Atributu '{{name}}' silmək istədiyinizə əminsinizmi? Hazırda ona təyin edilmiş bütün istifadəçilər təyin edilməmiş olacaq.",
|
||||
"delete": "Sil",
|
||||
"update": "Yenilə",
|
||||
"save": "Yadda saxla",
|
||||
@@ -1237,7 +1247,7 @@
|
||||
"number_provided": "Telefon nömrəsi təmin ediləcək",
|
||||
"before_event_trigger": "tədbir başlamazdan əvvəl",
|
||||
"event_cancelled_trigger": "tədbir ləğv edildikdə",
|
||||
"after_hosts_cal_video_no_show": "Ev sahibləri Cal video görüşünə qoşulmadıqdan sonra",
|
||||
"after_hosts_cal_video_no_show": "Təşkilatçılar Cal video görüşünə qoşulmadıqdan sonra",
|
||||
"after_guests_cal_video_no_show": "Qonaqlar Cal video görüşünə qoşulmadıqdan sonra",
|
||||
"new_event_trigger": "yeni tədbir sifariş edildikdə",
|
||||
"email_host_action": "ev sahibinə e-poçt göndərin",
|
||||
@@ -1630,6 +1640,7 @@
|
||||
"format": "Format",
|
||||
"uppercase_for_letters": "Bütün hərfləri böyük hərflərlə yaz",
|
||||
"replace_whitespaces_underscores": "Boşluqları alt xətlərlə əvəz et",
|
||||
"platform_billing": "Platforma ödənişi",
|
||||
"manage_billing": "Ödənişləri idarə et",
|
||||
"manage_billing_description": "Bütün ödəniş işlərini idarə et",
|
||||
"billing_freeplan_title": "Hazırda PULSUZ plandasınız",
|
||||
@@ -2037,6 +2048,8 @@
|
||||
"looking_for_more_insights": "Daha çox məlumat axtarırsınız?",
|
||||
"filters": "Filtrlər",
|
||||
"add_filter": "Filtr əlavə et",
|
||||
"add_rule": "Qayda əlavə et",
|
||||
"add_rule_group": "Qayda qrupu əlavə et",
|
||||
"remove_filters": "Bütün filtrləri təmizlə",
|
||||
"email_verified": "Email Təsdiqləndi",
|
||||
"select_user": "İstifadəçini seçin",
|
||||
@@ -2139,6 +2152,7 @@
|
||||
"set_up_your_organization": "Təşkilatınızı qurun",
|
||||
"set_up_your_platform_organization": "Platformanızı qurun",
|
||||
"organizations_description": "Təşkilatlar komandaların paylaşılan tədbir növləri, tətbiqlər, iş axınları və daha çoxunu yarada biləcəyi paylaşılan mühitlərdir.",
|
||||
"plans": "Planlar",
|
||||
"platform": "Platforma",
|
||||
"platform_description": "Platforma ilə bağlı hər şeyi idarə edin.",
|
||||
"platform_organization_description": "Cal.com Platforması, platforma API-ləri və atomları istifadə edərək planlaşdırmanı tətbiqinizə asanlıqla inteqrasiya etməyə imkan verir.",
|
||||
@@ -2201,7 +2215,7 @@
|
||||
"add_times_to_your_email": "Mövcud vaxtları seçin və Email-ə əlavə edin",
|
||||
"select_time": "Vaxtı Seçin",
|
||||
"select_date": "Tarixi Seçin",
|
||||
"connecting_you_to_someone": "Sizi kiməsə qoşuruq.",
|
||||
"connecting_you_to_someone": "Sizi {{orgName}}-dən birisi ilə əlaqələndiririk.",
|
||||
"please_do_not_close_this_tab": "Zəhmət olmasa, bu səhifəni bağlamayın və ya yeniləməyin",
|
||||
"see_all_available_times": "Bütün mövcud vaxtları görün",
|
||||
"org_team_names_example_1": "məs. Marketinq Komandası",
|
||||
@@ -2276,6 +2290,10 @@
|
||||
"seat_options_doesnt_multiple_durations": "Oturacaq seçimi bir neçə müddəti dəstəkləmir",
|
||||
"include_calendar_event": "Təqvim tədbirini daxil edin",
|
||||
"oAuth": "OAuth",
|
||||
"oAuth_client_creation_form": "OAuth müştəri yaratma forması",
|
||||
"oAuth_client_creation_form_description": "Bu, yeni OAuth müştərisi yaratmaq üçün formadır",
|
||||
"oAuth_client_updation_form": "OAuth müştəri yeniləmə forması",
|
||||
"oAuth_client_updation_form_description": "Bu, mövcud OAuth müştərisini redaktə etmək üçün formadır",
|
||||
"recently_added": "Son zamanlar əlavə olunmuş",
|
||||
"connect_all_calendars": "Bütün təqvimlərinizi qoşun",
|
||||
"connect_all_calendars_description": "{{appName}} mövcud təqvimlərinizdən mövcudluğu oxuyur.",
|
||||
@@ -2732,5 +2750,21 @@
|
||||
"event_end_time_in_attendee_timezone_info": "İştirakçı Saat Qurşağında Tədbirin Bitmə Vaxtı",
|
||||
"salesforce_create_event_on_contact": "Əgər mövcuddursa, kontakt üzərində hadisə yaradın. Əks halda, lidə qayıdın",
|
||||
"salesforce_owner_name_to_change": "Dəyişdiriləcək sahibin adı",
|
||||
"dont_have_access_this_page": "Bu səhifəyə girişiniz yoxdur",
|
||||
"you_need_admin_or_owner_privileges_to_access": "Bu səhifəni görmək və ya onunla qarşılıqlı əlaqədə olmaq üçün admin və ya sahib imtiyazlarına malik olmalısınız",
|
||||
"field_type": "Sahə növü",
|
||||
"when_to_write": "Nə zaman yazmaq",
|
||||
"salesforce_on_booking_write_to_record": "Rezervasiya zamanı iştirakçının {{record}} qeydinə xüsusi sahəyə yaz",
|
||||
"on_every_booking": "Hər rezervasiyada",
|
||||
"only_if_field_is_empty": "Yalnız sahə boşdursa",
|
||||
"booking_start_date": "Rezervasiyanın başlama tarixi",
|
||||
"booking_created_date": "Rezervasiyanın yaradılma tarixi",
|
||||
"booking_reassigned_to_host": "Rezervasiya {{host}}-a yenidən təyin edildi",
|
||||
"access_denied": "Giriş qadağandır",
|
||||
"salesforce_route_to_owner": "Əlaqə sahibi mövcud olduqda Round Robin ev sahibi olacaq",
|
||||
"salesforce_do_not_route_to_owner": "Əlaqə sahibi məcbur edilməyəcək (əgər atributlara və Round Robin meyarlarına uyğun gəlirsə, yenə də ev sahibi ola bilər)",
|
||||
"salesforce_route_to_custom_lookup_field": "Hesabda axtarış sahəsinə uyğun gələn istifadəçiyə yönləndir",
|
||||
"salesforce_option": "Salesforce Seçimi",
|
||||
"lookup_field_name": "Axtarış Sahəsinin Adı",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Yeni sətirləri bura əlavə edin ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -139,7 +139,8 @@
|
||||
"requested_to_reschedule_subject_attendee": "Akce vyžadovaná k přeplánování: zarezervujte si nový čas pro událost {{eventType}} s osobou {{name}}",
|
||||
"hi_user_name": "Hezký den, {{name}}",
|
||||
"ics_event_title": "{{eventType}} s {{name}}",
|
||||
"please_book_a_time_sometime_later": "Jejda, tentokrát jsme vás nemohli spojit. Prosím, naplánujte si hovor na později.",
|
||||
"please_book_a_time_sometime_later": "Zdá se, že všichni naši experti jsou momentálně zaneprázdněni. Naplánujte si prosím hovor na čas, který vám nejvíce vyhovuje, a my vám rádi pomůžeme.",
|
||||
"schedule_instead": "Naplánovat schůzku",
|
||||
"new_event_subject": "Nová událost: {{attendeeName}} - {{date}} - {{eventType}}",
|
||||
"join_by_entrypoint": "Připojit se k {{entryPoint}}",
|
||||
"notes": "Poznámky",
|
||||
@@ -198,6 +199,8 @@
|
||||
"webhook_disabled": "Webhook vypnut",
|
||||
"webhook_response": "Webhook odpověď",
|
||||
"webhook_test": "Test webhooku",
|
||||
"webhook_update_form": "Formulář pro aktualizaci webhooku",
|
||||
"webhook_update_form_description": "Přidejte webhook k vašemu OAuthClientovi.",
|
||||
"manage_your_webhook": "Spravovat webhook",
|
||||
"webhook_created_successfully": "Webhook úspěšně vytvořen!",
|
||||
"webhook_updated_successfully": "Webhook úspěšně aktualizován!",
|
||||
@@ -348,9 +351,13 @@
|
||||
"other": "Ostatní",
|
||||
"email_sign_in_subject": "Váš přihlašovací odkaz pro {{appName}}",
|
||||
"round_robin_emailed_you_and_attendees": "Setkáte se s {{user}}. Poslali jsme e-mail s pozvánkou do kalendáře a podrobnostmi všem účastníkům.",
|
||||
"round_robin_emailed_host_and_attendee": "{{host}} se setká s {{attendee}}. Všem jsme poslali e-mail s pozvánkou do kalendáře a podrobnostmi.",
|
||||
"emailed_you_and_attendees": "Poslali jsme vám a ostatním účastníkům e-mail s pozvánkou a podrobnostmi.",
|
||||
"emailed_host_and_attendee": "Všem jsme poslali e-mail s pozvánkou do kalendáře a podrobnostmi.",
|
||||
"emailed_you_and_attendees_recurring": "Na první z těchto opakovaných událostí jsme vám a ostatním účastníkům poslali e-mail s pozvánkou a podrobnostmi.",
|
||||
"round_robin_emailed_you_and_attendees_recurring": "Setkáte se s {{user}}. Poslali jsme e-mail s pozvánkou do kalendáře a podrobnostmi všem účastníkům pro první z těchto opakujících se událostí.",
|
||||
"emailed_host_and_attendee_recurring": "Všem jsme poslali e-mail s pozvánkou do kalendáře a podrobnostmi pro první z těchto opakujících se událostí.",
|
||||
"round_robin_emailed_you_and_attendees_recurring": "Setkáte se s {{user}}. Všem jsme poslali e-mail s pozvánkou do kalendáře a podrobnostmi pro první z těchto opakujících se událostí.",
|
||||
"round_robin_emailed_host_and_attendee_recurring": "{{host}} se setká s {{attendee}}. Všem jsme poslali e-mail s pozvánkou do kalendáře a podrobnostmi pro první z těchto opakujících se událostí.",
|
||||
"emailed_you_and_any_other_attendees": "Poslali jsme vám a ostatním účastníkům e-mail s těmito informacemi.",
|
||||
"needs_to_be_confirmed_or_rejected": "Vaše rezervace bude teprve schválena nebo zamítnuta.",
|
||||
"needs_to_be_confirmed_or_rejected_recurring": "Vaše opakovaná schůzka musí být ještě potvrzena nebo zamítnuta.",
|
||||
@@ -703,6 +710,9 @@
|
||||
"no_teams": "Zatím nemáte žádné týmy.",
|
||||
"no_teams_description": "Týmy ostatním umožňují rezervovat sdílené události mezi spolupracovníky.",
|
||||
"submit": "Odeslat",
|
||||
"confirm_remove_attribute": "Ano, odstranit atribut",
|
||||
"remove_attribute": "Odstranit atribut",
|
||||
"remove_attribute_confirmation_message": "Opravdu chcete odstranit atribut '{{name}}'? Všichni uživatelé, kteří jsou k němu aktuálně přiřazeni, budou od něj odpojeni.",
|
||||
"delete": "Smazat",
|
||||
"update": "Aktualizovat",
|
||||
"save": "Uložit",
|
||||
@@ -1237,8 +1247,8 @@
|
||||
"number_provided": "Bude uvedeno telefonní číslo",
|
||||
"before_event_trigger": "před zahájením události",
|
||||
"event_cancelled_trigger": "při zrušení události",
|
||||
"after_hosts_cal_video_no_show": "Po tom, co se hostitelé nepřipojí k cal video",
|
||||
"after_guests_cal_video_no_show": "Po tom, co se hosté nepřipojí k cal video",
|
||||
"after_hosts_cal_video_no_show": "Poté, co se hostitelé nepřipojili k video hovoru",
|
||||
"after_guests_cal_video_no_show": "Poté, co se hosté nepřipojili k video hovoru",
|
||||
"new_event_trigger": "při rezervaci nové události",
|
||||
"email_host_action": "odeslání e-mailu hostiteli",
|
||||
"email_attendee_action": "odeslat e-mail účastníkům",
|
||||
@@ -1630,6 +1640,7 @@
|
||||
"format": "Formát",
|
||||
"uppercase_for_letters": "Použít velká písmena pro všechna písmena",
|
||||
"replace_whitespaces_underscores": "Nahradit mezery podtržítky",
|
||||
"platform_billing": "Fakturace platformy",
|
||||
"manage_billing": "Spravovat fakturaci",
|
||||
"manage_billing_description": "Spravovat vše ohledně fakturace",
|
||||
"billing_freeplan_title": "Momentálně používáte tarif ZDARMA",
|
||||
@@ -2037,6 +2048,8 @@
|
||||
"looking_for_more_insights": "Hledáte další přehledy?",
|
||||
"filters": "Filtry",
|
||||
"add_filter": "Přidat filtr",
|
||||
"add_rule": "Přidat pravidlo",
|
||||
"add_rule_group": "Přidat skupinu pravidel",
|
||||
"remove_filters": "Vymazat všechny filtry",
|
||||
"email_verified": "E-mail ověřen",
|
||||
"select_user": "Vybrat uživatele",
|
||||
@@ -2139,6 +2152,7 @@
|
||||
"set_up_your_organization": "Nastavení organizace",
|
||||
"set_up_your_platform_organization": "Nastavte svou platformu",
|
||||
"organizations_description": "Organizace jsou sdílená prostředí, ve kterých mohou týmy vytvářet sdílené typy událostí, aplikace, pracovní postupy a mnoho dalšího.",
|
||||
"plans": "Plány",
|
||||
"platform": "Platforma",
|
||||
"platform_description": "Spravujte vše, co souvisí s platformou.",
|
||||
"platform_organization_description": "Platforma Cal.com vám umožňuje snadno integrovat plánování do vaší aplikace pomocí platformových API a atomů.",
|
||||
@@ -2201,7 +2215,7 @@
|
||||
"add_times_to_your_email": "Vyberte několik dostupných časů a vložte je do e-mailu",
|
||||
"select_time": "Vyberte čas",
|
||||
"select_date": "Vyberte datum",
|
||||
"connecting_you_to_someone": "Spojujeme vás s někým.",
|
||||
"connecting_you_to_someone": "Spojujeme vás s někým z {{orgName}}.",
|
||||
"please_do_not_close_this_tab": "Prosím, nezavírejte ani neobnovujte tuto stránku",
|
||||
"see_all_available_times": "Zobrazit všechny dostupné časy",
|
||||
"org_team_names_example_1": "Např. marketingový tým",
|
||||
@@ -2276,6 +2290,10 @@
|
||||
"seat_options_doesnt_multiple_durations": "Volba místa nepodporuje více dob trvání",
|
||||
"include_calendar_event": "Zahrnout událost kalendáře",
|
||||
"oAuth": "OAuth",
|
||||
"oAuth_client_creation_form": "Formulář pro vytvoření OAuth klienta",
|
||||
"oAuth_client_creation_form_description": "Toto je formulář pro vytvoření nového OAuth klienta",
|
||||
"oAuth_client_updation_form": "Formulář pro aktualizaci OAuth klienta",
|
||||
"oAuth_client_updation_form_description": "Toto je formulář pro úpravu existujícího OAuth klienta",
|
||||
"recently_added": "Nedávno přidáno",
|
||||
"connect_all_calendars": "Propojte všechny své kalendáře",
|
||||
"connect_all_calendars_description": "{{appName}} čte dostupnost ze všech vašich stávajících kalendářů.",
|
||||
@@ -2732,5 +2750,21 @@
|
||||
"event_end_time_in_attendee_timezone_info": "Čas konce události v časovém pásmu účastníka",
|
||||
"salesforce_create_event_on_contact": "Vytvořit událost na kontaktu, pokud existuje. Jinak použít lead",
|
||||
"salesforce_owner_name_to_change": "Jméno vlastníka ke změně",
|
||||
"dont_have_access_this_page": "K této stránce nemáte přístup",
|
||||
"you_need_admin_or_owner_privileges_to_access": "Pro zobrazení nebo interakci s touto stránkou musíte mít administrátorská nebo vlastnická práva",
|
||||
"field_type": "Typ pole",
|
||||
"when_to_write": "Kdy zapisovat",
|
||||
"salesforce_on_booking_write_to_record": "Při rezervaci zapsat do vlastního pole záznamu účastníka {{record}}",
|
||||
"on_every_booking": "Při každé rezervaci",
|
||||
"only_if_field_is_empty": "Pouze pokud je pole prázdné",
|
||||
"booking_start_date": "Datum začátku rezervace",
|
||||
"booking_created_date": "Datum vytvoření rezervace",
|
||||
"booking_reassigned_to_host": "Rezervace přeřazena hostiteli {{host}}",
|
||||
"access_denied": "Přístup odepřen",
|
||||
"salesforce_route_to_owner": "Vlastník kontaktu bude hostem Round Robin, pokud je k dispozici",
|
||||
"salesforce_do_not_route_to_owner": "Vlastník kontaktu nebude nucen (stále může být hostem, pokud odpovídá atributům a kritériím Round Robin)",
|
||||
"salesforce_route_to_custom_lookup_field": "Přiřadit uživateli, který odpovídá vyhledávacímu poli na účtu",
|
||||
"salesforce_option": "Možnost Salesforce",
|
||||
"lookup_field_name": "Název vyhledávacího pole",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Přidejte své nové řetězce nahoru ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -139,7 +139,8 @@
|
||||
"requested_to_reschedule_subject_attendee": "Handling Påkrævet Omlægning: Reservér en ny tid for {{eventType}} med {{name}}",
|
||||
"hi_user_name": "Hej {{name}}",
|
||||
"ics_event_title": "{{eventType}} med {{name}}",
|
||||
"please_book_a_time_sometime_later": "Ups, vi kunne ikke forbinde dig denne gang. Venligst planlæg et opkald på et senere tidspunkt.",
|
||||
"please_book_a_time_sometime_later": "Det ser ud til, at alle vores eksperter er optaget lige nu. Venligst planlæg et opkald på et tidspunkt, der passer dig bedst, så vi er klar til at hjælpe dig.",
|
||||
"schedule_instead": "Planlæg i stedet",
|
||||
"new_event_subject": "Ny begivenhed: {{attendeeName}} - {{date}} - {{eventType}}",
|
||||
"join_by_entrypoint": "Deltag via {{entryPoint}}",
|
||||
"notes": "Noter",
|
||||
@@ -198,6 +199,8 @@
|
||||
"webhook_disabled": "Webhook Deaktiveret",
|
||||
"webhook_response": "Webhook svar",
|
||||
"webhook_test": "Webhook test",
|
||||
"webhook_update_form": "Formular til opdatering af webhook",
|
||||
"webhook_update_form_description": "Tilføj en webhook til din OAuthClient.",
|
||||
"manage_your_webhook": "Administrer din webhook",
|
||||
"webhook_created_successfully": "Webhook blev oprettet!",
|
||||
"webhook_updated_successfully": "Webhook blev opdateret!",
|
||||
@@ -348,9 +351,13 @@
|
||||
"other": "Flere",
|
||||
"email_sign_in_subject": "Dit login link til {{appName}}",
|
||||
"round_robin_emailed_you_and_attendees": "Du mødes med {{user}}. Vi har sendt en e-mail med en kalenderinvitation med detaljerne til alle.",
|
||||
"round_robin_emailed_host_and_attendee": "{{host}} mødes med {{attendee}}. Vi har sendt en e-mail med en kalenderinvitation med detaljerne til alle.",
|
||||
"emailed_you_and_attendees": "Vi har sendt dig og de andre deltagere en kalenderinvitation med alle detaljerne.",
|
||||
"emailed_host_and_attendee": "Vi har sendt en e-mail med en kalenderinvitation med detaljerne til alle.",
|
||||
"emailed_you_and_attendees_recurring": "Vi har sendt dig og de andre deltagere en kalenderinvitation til den første af disse tilbagevendende begivenheder.",
|
||||
"emailed_host_and_attendee_recurring": "Vi har sendt en e-mail med en kalenderinvitation med detaljerne til alle for den første af disse tilbagevendende begivenheder.",
|
||||
"round_robin_emailed_you_and_attendees_recurring": "Du mødes med {{user}}. Vi har sendt en e-mail med en kalenderinvitation med detaljerne til alle for den første af disse tilbagevendende begivenheder.",
|
||||
"round_robin_emailed_host_and_attendee_recurring": "{{host}} mødes med {{attendee}}. Vi har sendt en e-mail med en kalenderinvitation med detaljerne til alle for den første af disse tilbagevendende begivenheder.",
|
||||
"emailed_you_and_any_other_attendees": "Du og eventuelle andre deltagere er blevet mailet med disse oplysninger.",
|
||||
"needs_to_be_confirmed_or_rejected": "Din booking mangler stadig at blive bekræftet eller afvist.",
|
||||
"needs_to_be_confirmed_or_rejected_recurring": "Dit tilbagevendende møde mangler stadig at blive bekræftet eller afvist.",
|
||||
@@ -703,6 +710,9 @@
|
||||
"no_teams": "Du har ikke nogen teams endnu.",
|
||||
"no_teams_description": "Teams giver andre mulighed for at booke begivenheder, der deles mellem dine kolleger.",
|
||||
"submit": "Videre",
|
||||
"confirm_remove_attribute": "Ja, fjern attribut",
|
||||
"remove_attribute": "Fjern attribut",
|
||||
"remove_attribute_confirmation_message": "Er du sikker på, at du vil fjerne attributten '{{name}}'? Alle brugere, der i øjeblikket er tildelt den, vil blive frakoblet.",
|
||||
"delete": "Slet",
|
||||
"update": "Opdatér",
|
||||
"save": "Gem",
|
||||
@@ -1237,8 +1247,8 @@
|
||||
"number_provided": "Telefonnummer vil blive angivet",
|
||||
"before_event_trigger": "før begivenheden starter",
|
||||
"event_cancelled_trigger": "når begivenheden er aflyst",
|
||||
"after_hosts_cal_video_no_show": "Efter værter ikke deltager i cal video",
|
||||
"after_guests_cal_video_no_show": "Efter gæster ikke deltager i cal video",
|
||||
"after_hosts_cal_video_no_show": "Efter værterne ikke deltog i cal-videomødet",
|
||||
"after_guests_cal_video_no_show": "Efter gæsterne ikke deltog i cal-videomødet",
|
||||
"new_event_trigger": "når ny begivenhed er booket",
|
||||
"email_host_action": "send e-mail til vært",
|
||||
"email_attendee_action": "send e-mail til deltagere",
|
||||
@@ -1630,6 +1640,7 @@
|
||||
"format": "Formatér",
|
||||
"uppercase_for_letters": "Brug store bogstaver for alle bogstaver",
|
||||
"replace_whitespaces_underscores": "Erstat blanke tegn med understregninger",
|
||||
"platform_billing": "Platformsfakturering",
|
||||
"manage_billing": "Håndtér fakturering",
|
||||
"manage_billing_description": "Håndtér alt omkring fakturering",
|
||||
"billing_freeplan_title": "Du er i øjeblikket på den GRATIS plan",
|
||||
@@ -2037,6 +2048,8 @@
|
||||
"looking_for_more_insights": "Leder du efter flere indsigter?",
|
||||
"filters": "Filtre",
|
||||
"add_filter": "Tilføj filter",
|
||||
"add_rule": "Tilføj regel",
|
||||
"add_rule_group": "Tilføj regelgruppe",
|
||||
"remove_filters": "Ryd alle filtre",
|
||||
"email_verified": "Email bekræftet",
|
||||
"select_user": "Vælg bruger",
|
||||
@@ -2139,6 +2152,7 @@
|
||||
"set_up_your_organization": "Opsæt din organisation",
|
||||
"set_up_your_platform_organization": "Opsæt din platform",
|
||||
"organizations_description": "Organisationer er delte miljøer, hvor teams kan oprette delte begivenhedstyper, apps, arbejdsgange og mere.",
|
||||
"plans": "Planer",
|
||||
"platform": "Platform",
|
||||
"platform_description": "Administrer alt relateret til platformen.",
|
||||
"platform_organization_description": "Cal.com Platform lader dig integrere planlægning problemfrit i din app ved hjælp af platformens API'er og atomer.",
|
||||
@@ -2201,7 +2215,7 @@
|
||||
"add_times_to_your_email": "Vælg nogle tilgængelige tidspunkter og indlejre dem i din e-mail",
|
||||
"select_time": "Vælg tid",
|
||||
"select_date": "Vælg dato",
|
||||
"connecting_you_to_someone": "Vi forbinder dig med en person.",
|
||||
"connecting_you_to_someone": "Vi forbinder dig med en person fra {{orgName}}.",
|
||||
"please_do_not_close_this_tab": "Luk eller opdater venligst ikke denne side",
|
||||
"see_all_available_times": "Se alle tilgængelige tidspunkter",
|
||||
"org_team_names_example_1": "f.eks. Marketingteam",
|
||||
@@ -2276,6 +2290,10 @@
|
||||
"seat_options_doesnt_multiple_durations": "Sædeoption understøtter ikke flere varigheder",
|
||||
"include_calendar_event": "Inkluder kalenderbegivenhed",
|
||||
"oAuth": "OAuth",
|
||||
"oAuth_client_creation_form": "Formular til oprettelse af OAuth-klient",
|
||||
"oAuth_client_creation_form_description": "Dette er formularen til at oprette en ny OAuth-klient",
|
||||
"oAuth_client_updation_form": "Formular til opdatering af OAuth-klient",
|
||||
"oAuth_client_updation_form_description": "Dette er formularen til at redigere en eksisterende OAuth-klient",
|
||||
"recently_added": "For nylig tilføjet",
|
||||
"connect_all_calendars": "Forbind alle dine kalendere",
|
||||
"connect_all_calendars_description": "{{appName}} læser tilgængelighed fra alle dine eksisterende kalendere.",
|
||||
@@ -2732,5 +2750,21 @@
|
||||
"event_end_time_in_attendee_timezone_info": "Begivenhedens sluttidspunkt i deltagers tidszone",
|
||||
"salesforce_create_event_on_contact": "Opret begivenhed på kontakt, hvis den eksisterer. Ellers fald tilbage til lead",
|
||||
"salesforce_owner_name_to_change": "Ejernavn der skal ændres",
|
||||
"dont_have_access_this_page": "Du har ikke adgang til denne side",
|
||||
"you_need_admin_or_owner_privileges_to_access": "For at kunne se eller interagere med denne side, skal du have administrator- eller ejerrettigheder",
|
||||
"field_type": "Felttype",
|
||||
"when_to_write": "Hvornår der skal skrives",
|
||||
"salesforce_on_booking_write_to_record": "Ved booking, skriv til et brugerdefineret felt på deltagerens {{record}}-post",
|
||||
"on_every_booking": "Ved hver booking",
|
||||
"only_if_field_is_empty": "Kun hvis feltet er tomt",
|
||||
"booking_start_date": "Bookings startdato",
|
||||
"booking_created_date": "Dato for oprettelse af booking",
|
||||
"booking_reassigned_to_host": "Booking omfordelt til {{host}}",
|
||||
"access_denied": "Adgang nægtet",
|
||||
"salesforce_route_to_owner": "Kontaktens ejer vil være Round Robin-vært, hvis tilgængelig",
|
||||
"salesforce_do_not_route_to_owner": "Kontaktens ejer vil ikke blive tvunget (kan stadig være vært, hvis det matcher attributterne og Round Robin-kriterierne)",
|
||||
"salesforce_route_to_custom_lookup_field": "Rute til en bruger, der matcher et opslagfelt på en konto",
|
||||
"salesforce_option": "Salesforce-indstilling",
|
||||
"lookup_field_name": "Opslagsfeltnavn",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Tilføj dine nye strenge ovenfor her ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -139,7 +139,8 @@
|
||||
"requested_to_reschedule_subject_attendee": "Aktion erforderlich - Neuplanung: Bitte buchen Sie eine neue Zeit für „{{eventType}}“ mit {{name}}",
|
||||
"hi_user_name": "Hallo, {{name}}",
|
||||
"ics_event_title": "{{eventType}} mit {{name}}",
|
||||
"please_book_a_time_sometime_later": "Ups, wir konnten Sie dieses Mal nicht verbinden. Bitte planen Sie stattdessen einen zukünftigen Anruf",
|
||||
"please_book_a_time_sometime_later": "Es scheint, dass alle unsere Experten momentan beschäftigt sind. Bitte vereinbaren Sie einen Termin zu einem Zeitpunkt, der Ihnen am besten passt, und wir werden bereit sein, Ihnen zu helfen.",
|
||||
"schedule_instead": "Stattdessen planen",
|
||||
"new_event_subject": "Neuer Termin: {{attendeeName}} - {{date}} - {{eventType}}",
|
||||
"join_by_entrypoint": "{{entryPoint}} beitreten",
|
||||
"notes": "Notizen",
|
||||
@@ -198,6 +199,8 @@
|
||||
"webhook_disabled": "Webhook deaktiviert",
|
||||
"webhook_response": "Webhook-Antwort",
|
||||
"webhook_test": "Webhook-Test",
|
||||
"webhook_update_form": "Webhook-Aktualisierungsformular",
|
||||
"webhook_update_form_description": "Fügen Sie Ihrem OAuth-Client einen Webhook hinzu.",
|
||||
"manage_your_webhook": "Webhook verwalten",
|
||||
"webhook_created_successfully": "Webhook erfolgreich erstellt!",
|
||||
"webhook_updated_successfully": "Webhook erfolgreich aktualisiert!",
|
||||
@@ -348,9 +351,13 @@
|
||||
"other": "Sonstige",
|
||||
"email_sign_in_subject": "Ihr Anmelde-Link für {{appName}}",
|
||||
"round_robin_emailed_you_and_attendees": "Sie treffen sich mit {{user}}. Wir haben eine E-Mail mit einer Kalendereinladung mit den Details an alle gesendet.",
|
||||
"round_robin_emailed_host_and_attendee": "{{host}} trifft sich mit {{attendee}}. Wir haben allen eine E-Mail mit einer Kalendereinladung und den Details gesendet.",
|
||||
"emailed_you_and_attendees": "Wir haben eine E-Mail mit einer Kalendereinladung mit den Details an alle gesendet.",
|
||||
"emailed_host_and_attendee": "Wir haben allen eine E-Mail mit einer Kalendereinladung und den Details gesendet.",
|
||||
"emailed_you_and_attendees_recurring": "Wir haben eine E-Mail mit einer Kalendereinladung mit den Details für das erste dieser wiederkehrenden Termine an alle gesendet.",
|
||||
"round_robin_emailed_you_and_attendees_recurring": "Sie treffen sich mit {{user}}. Wir haben eine E-Mail mit einer Kalendereinladung mit den Details für das erste dieser wiederkehrenden Termine an alle gesendet.",
|
||||
"emailed_host_and_attendee_recurring": "Wir haben allen eine E-Mail mit einer Kalendereinladung und den Details für das erste dieser wiederkehrenden Events gesendet.",
|
||||
"round_robin_emailed_you_and_attendees_recurring": "Sie treffen sich mit {{user}}. Wir haben allen eine E-Mail mit einer Kalendereinladung und den Details für das erste dieser wiederkehrenden Events gesendet.",
|
||||
"round_robin_emailed_host_and_attendee_recurring": "{{host}} trifft sich mit {{attendee}}. Wir haben allen eine E-Mail mit einer Kalendereinladung und den Details für das erste dieser wiederkehrenden Events gesendet.",
|
||||
"emailed_you_and_any_other_attendees": "Wir haben eine E-Mail mit diesen Informationen an alle gesendet.",
|
||||
"needs_to_be_confirmed_or_rejected": "Ihr Termin muss noch bestätigt oder abgelehnt werden.",
|
||||
"needs_to_be_confirmed_or_rejected_recurring": "Ihr wiederkehrender Termin muss noch bestätigt oder abgelehnt werden.",
|
||||
@@ -703,6 +710,9 @@
|
||||
"no_teams": "Sie haben noch keine Teams.",
|
||||
"no_teams_description": "Teams ermöglichen Termine zu buchen, an denen mehrere Mitarbeiter beteiligt sind.",
|
||||
"submit": "Abschicken",
|
||||
"confirm_remove_attribute": "Ja, Attribut entfernen",
|
||||
"remove_attribute": "Attribut entfernen",
|
||||
"remove_attribute_confirmation_message": "Sind Sie sicher, dass Sie das Attribut '{{name}}' entfernen möchten? Alle Benutzer, die derzeit zugewiesen sind, werden abgemeldet.",
|
||||
"delete": "Löschen",
|
||||
"update": "Aktualisieren",
|
||||
"save": "Speichern",
|
||||
@@ -1237,8 +1247,8 @@
|
||||
"number_provided": "Telefonnummer wird angegeben",
|
||||
"before_event_trigger": "vor Beginn des Termins",
|
||||
"event_cancelled_trigger": "wenn der Termin abgesagt wird",
|
||||
"after_hosts_cal_video_no_show": "Nachdem Gastgeber nicht zum Cal-Video erscheinen",
|
||||
"after_guests_cal_video_no_show": "Nachdem Gäste nicht zum Cal-Video erscheinen",
|
||||
"after_hosts_cal_video_no_show": "Nachdem die Gastgeber nicht am Cal-Video teilgenommen haben",
|
||||
"after_guests_cal_video_no_show": "Nachdem die Gäste nicht am Cal-Video teilgenommen haben",
|
||||
"new_event_trigger": "wenn ein neuer Termin gebucht wird",
|
||||
"email_host_action": "E-Mail an den Veranstalter senden",
|
||||
"email_attendee_action": "E-Mail an Teilnehmende senden",
|
||||
@@ -1630,6 +1640,7 @@
|
||||
"format": "Format",
|
||||
"uppercase_for_letters": "Großbuchstaben für alle Buchstaben verwenden",
|
||||
"replace_whitespaces_underscores": "Leerzeichen durch Unterstriche ersetzen",
|
||||
"platform_billing": "Plattform-Abrechnung",
|
||||
"manage_billing": "Abrechnung verwalten",
|
||||
"manage_billing_description": "Alles zur Abrechnung verwalten",
|
||||
"billing_freeplan_title": "Sie verwenden derzeit den KOSTENLOSEN Plan",
|
||||
@@ -2037,6 +2048,8 @@
|
||||
"looking_for_more_insights": "Möchten Sie mehr Insights?",
|
||||
"filters": "Filter",
|
||||
"add_filter": "Filter hinzufügen",
|
||||
"add_rule": "Regel hinzufügen",
|
||||
"add_rule_group": "Regelgruppe hinzufügen",
|
||||
"remove_filters": "Alle Filter löschen",
|
||||
"email_verified": "E-Mail bestätigt",
|
||||
"select_user": "Benutzer auswählen",
|
||||
@@ -2139,6 +2152,7 @@
|
||||
"set_up_your_organization": "Ihre Organization einrichten",
|
||||
"set_up_your_platform_organization": "Richten Sie Ihre Plattform ein",
|
||||
"organizations_description": "Organizations sind geteilte Umgebungen, in denen Teams gemeinsame Termintypen, Apps, Workflows und mehr erstellen können.",
|
||||
"plans": "Pläne",
|
||||
"platform": "Plattform",
|
||||
"platform_description": "Verwalten Sie alles, was mit der Plattform zusammenhängt.",
|
||||
"platform_organization_description": "Mit der Cal.com-Plattform können Sie die Terminplanung mühelos mit Hilfe von Plattform-APIs und -Atoms in Ihre App integrieren.",
|
||||
@@ -2201,7 +2215,7 @@
|
||||
"add_times_to_your_email": "Zeiten zur E-Mail hinzufügen",
|
||||
"select_time": "Zeit auswählen",
|
||||
"select_date": "Datum auswählen",
|
||||
"connecting_you_to_someone": "Wir verbinden Sie mit jemandem.",
|
||||
"connecting_you_to_someone": "Wir verbinden Sie mit jemandem von {{orgName}}.",
|
||||
"please_do_not_close_this_tab": "Bitte schließen oder aktualisieren Sie diese Seite nicht",
|
||||
"see_all_available_times": "Alle verfügbaren Zeiten ansehen",
|
||||
"org_team_names_example_1": "z.B. Marketing-Team",
|
||||
@@ -2276,6 +2290,10 @@
|
||||
"seat_options_doesnt_multiple_durations": "Platzoption unterstützt mehrere Dauern nicht",
|
||||
"include_calendar_event": "Kalenderereignis hinzufügen",
|
||||
"oAuth": "OAuth",
|
||||
"oAuth_client_creation_form": "OAuth-Client-Erstellungsformular",
|
||||
"oAuth_client_creation_form_description": "Dies ist das Formular zur Erstellung eines neuen OAuth-Clients",
|
||||
"oAuth_client_updation_form": "OAuth-Client-Aktualisierungsformular",
|
||||
"oAuth_client_updation_form_description": "Dies ist das Formular zur Bearbeitung eines bestehenden OAuth-Clients",
|
||||
"recently_added": "Kürzlich hinzugefügt",
|
||||
"connect_all_calendars": "Verbinden Sie alle Ihre Kalender",
|
||||
"connect_all_calendars_description": "{{appName}} liest die Verfügbarkeit aus allen Ihren vorhandenen Kalendern.",
|
||||
@@ -2732,5 +2750,21 @@
|
||||
"event_end_time_in_attendee_timezone_info": "Ereignis-Endzeit in der Zeitzone des Teilnehmers",
|
||||
"salesforce_create_event_on_contact": "Event für Kontakt erstellen, falls vorhanden. Andernfalls auf Lead zurückgreifen",
|
||||
"salesforce_owner_name_to_change": "Zu ändernder Besitzername",
|
||||
"dont_have_access_this_page": "Sie haben keinen Zugriff auf diese Seite",
|
||||
"you_need_admin_or_owner_privileges_to_access": "Um diese Seite anzuzeigen oder mit ihr zu interagieren, benötigen Sie Administrator- oder Eigentümerrechte",
|
||||
"field_type": "Feldtyp",
|
||||
"when_to_write": "Wann schreiben",
|
||||
"salesforce_on_booking_write_to_record": "Bei Buchung in ein benutzerdefiniertes Feld des Teilnehmer-{{record}}-Datensatzes schreiben",
|
||||
"on_every_booking": "Bei jeder Buchung",
|
||||
"only_if_field_is_empty": "Nur wenn das Feld leer ist",
|
||||
"booking_start_date": "Buchungs-Startdatum",
|
||||
"booking_created_date": "Buchungs-Erstellungsdatum",
|
||||
"booking_reassigned_to_host": "Buchung an {{host}} neu zugewiesen",
|
||||
"access_denied": "Zugriff verweigert",
|
||||
"salesforce_route_to_owner": "Der Kontaktinhaber wird der Round Robin Host sein, falls verfügbar",
|
||||
"salesforce_do_not_route_to_owner": "Der Kontaktinhaber wird nicht erzwungen (kann trotzdem Host sein, wenn die Attribute und Round Robin Kriterien übereinstimmen)",
|
||||
"salesforce_route_to_custom_lookup_field": "Weiterleitung an einen Benutzer, der mit einem Lookup-Feld eines Accounts übereinstimmt",
|
||||
"salesforce_option": "Salesforce-Option",
|
||||
"lookup_field_name": "Name des Lookup-Felds",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Fügen Sie Ihre neuen Code-Zeilen über dieser hinzu ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -139,7 +139,8 @@
|
||||
"requested_to_reschedule_subject_attendee": "Action Required Reschedule: Please book a new time for {{eventType}} with {{name}}",
|
||||
"hi_user_name": "Hi {{name}}",
|
||||
"ics_event_title": "{{eventType}} with {{name}}",
|
||||
"please_book_a_time_sometime_later": "Oops, we couldn't connect you this time. Please schedule a future call instead",
|
||||
"please_book_a_time_sometime_later": "It looks like all of our experts are busy right now. Please schedule a call at a time that works best for you and we'll be ready to assist you.",
|
||||
"schedule_instead": "Schedule instead",
|
||||
"new_event_subject": "New event: {{attendeeName}} - {{date}} - {{eventType}}",
|
||||
"join_by_entrypoint": "Join by {{entryPoint}}",
|
||||
"notes": "Notes",
|
||||
@@ -198,6 +199,8 @@
|
||||
"webhook_disabled": "Webhook Disabled",
|
||||
"webhook_response": "Webhook response",
|
||||
"webhook_test": "Webhook test",
|
||||
"webhook_update_form": "Webhook update form",
|
||||
"webhook_update_form_description": "Add a webhook to your OAuthClient.",
|
||||
"manage_your_webhook": "Manage your webhook",
|
||||
"webhook_created_successfully": "Webhook created successfully!",
|
||||
"webhook_updated_successfully": "Webhook updated successfully!",
|
||||
@@ -348,9 +351,13 @@
|
||||
"other": "Other",
|
||||
"email_sign_in_subject": "Your sign-in link for {{appName}}",
|
||||
"round_robin_emailed_you_and_attendees": "You are meeting with {{user}}. We sent an email with a calendar invitation with the details to everyone.",
|
||||
"round_robin_emailed_host_and_attendee": "{{host}} is meeting with {{attendee}}. We sent an email with a calendar invitation with the details to everyone.",
|
||||
"emailed_you_and_attendees": "We sent an email with a calendar invitation with the details to everyone.",
|
||||
"emailed_host_and_attendee": "We sent an email with a calendar invitation with the details to everyone.",
|
||||
"emailed_you_and_attendees_recurring": "We sent an email with a calendar invitation with the details to everyone for the first of these recurring events.",
|
||||
"round_robin_emailed_you_and_attendees_recurring": "You are meeting with {{user}}.We sent an email with a calendar invitation with the details to everyone for the first of these recurring events.",
|
||||
"emailed_host_and_attendee_recurring": "We sent an email with a calendar invitation with the details to everyone for the first of these recurring events.",
|
||||
"round_robin_emailed_you_and_attendees_recurring": "You are meeting with {{user}}. We sent an email with a calendar invitation with the details to everyone for the first of these recurring events.",
|
||||
"round_robin_emailed_host_and_attendee_recurring": "{{host}} is meeting with {{attendee}}. We sent an email with a calendar invitation with the details to everyone for the first of these recurring events.",
|
||||
"emailed_you_and_any_other_attendees": "We sent an email to everyone with this information.",
|
||||
"needs_to_be_confirmed_or_rejected": "Your booking still needs to be confirmed or rejected.",
|
||||
"needs_to_be_confirmed_or_rejected_recurring": "Your recurring meeting still needs to be confirmed or rejected.",
|
||||
@@ -703,6 +710,9 @@
|
||||
"no_teams": "You don't have any teams yet.",
|
||||
"no_teams_description": "Teams allow others to book events shared between your coworkers.",
|
||||
"submit": "Submit",
|
||||
"confirm_remove_attribute": "Yes, remove attribute",
|
||||
"remove_attribute": "Remove attribute",
|
||||
"remove_attribute_confirmation_message": "Are you sure you want to remove the attribute '{{name}}'? All users currently assigned to it will be unassigned.",
|
||||
"delete": "Delete",
|
||||
"update": "Update",
|
||||
"save": "Save",
|
||||
@@ -1237,8 +1247,8 @@
|
||||
"number_provided": "Phone number will be provided",
|
||||
"before_event_trigger": "before event starts",
|
||||
"event_cancelled_trigger": "when event is canceled",
|
||||
"after_hosts_cal_video_no_show": "After hosts don't join cal video",
|
||||
"after_guests_cal_video_no_show": "After guests don't join cal video",
|
||||
"after_hosts_cal_video_no_show": "After hosts didn't join cal video",
|
||||
"after_guests_cal_video_no_show": "After guests didn't join cal video",
|
||||
"new_event_trigger": "when new event is booked",
|
||||
"email_host_action": "send email to host",
|
||||
"email_attendee_action": "send email to attendees",
|
||||
@@ -1630,6 +1640,7 @@
|
||||
"format": "Format",
|
||||
"uppercase_for_letters": "Use uppercase for all letters",
|
||||
"replace_whitespaces_underscores": "Replace whitespaces with underscores",
|
||||
"platform_billing": "Platform billing",
|
||||
"manage_billing": "Manage billing",
|
||||
"manage_billing_description": "Manage all things billing",
|
||||
"billing_freeplan_title": "You're currently on the FREE plan",
|
||||
@@ -2037,6 +2048,8 @@
|
||||
"looking_for_more_insights": "Looking for more Insights?",
|
||||
"filters": "Filters",
|
||||
"add_filter": "Add filter",
|
||||
"add_rule": "Add rule",
|
||||
"add_rule_group": "Add rule group",
|
||||
"remove_filters": "Clear all filters",
|
||||
"email_verified": "Email Verified",
|
||||
"select_user": "Select User",
|
||||
@@ -2139,6 +2152,7 @@
|
||||
"set_up_your_organization": "Set up your organization",
|
||||
"set_up_your_platform_organization": "Set up your platform",
|
||||
"organizations_description": "Organizations are shared environments where teams can create shared event types, apps, workflows and more.",
|
||||
"plans": "Plans",
|
||||
"platform": "Platform",
|
||||
"platform_description": "Manage everything related to platform.",
|
||||
"platform_organization_description": "Cal.com Platform lets you integrate scheduling effortlessly into your app using platform apis and atoms.",
|
||||
@@ -2201,7 +2215,7 @@
|
||||
"add_times_to_your_email": "Select a few available times and embed them in your Email",
|
||||
"select_time": "Select Time",
|
||||
"select_date": "Select Date",
|
||||
"connecting_you_to_someone": "We are connecting you to someone.",
|
||||
"connecting_you_to_someone": "We are connecting you to someone from {{orgName}}.",
|
||||
"please_do_not_close_this_tab": "Please do not close or refresh this page",
|
||||
"see_all_available_times": "See all available times",
|
||||
"org_team_names_example_1": "e.g. Marketing Team",
|
||||
@@ -2276,6 +2290,10 @@
|
||||
"seat_options_doesnt_multiple_durations": "Seat option doesn't support multiple durations",
|
||||
"include_calendar_event": "Include calendar event",
|
||||
"oAuth": "OAuth",
|
||||
"oAuth_client_creation_form": "OAuth client creation form",
|
||||
"oAuth_client_creation_form_description": "This is the form to create a new OAuth client",
|
||||
"oAuth_client_updation_form": "OAuth client updation form",
|
||||
"oAuth_client_updation_form_description": "This is the form to edit an existing OAuth client",
|
||||
"recently_added": "Recently added",
|
||||
"connect_all_calendars": "Connect all your calendars",
|
||||
"connect_all_calendars_description": "{{appName}} reads availability from all your existing calendars.",
|
||||
@@ -2736,5 +2754,21 @@
|
||||
"event_end_time_in_attendee_timezone_info": "Event End Time In Attendee Timezone",
|
||||
"salesforce_create_event_on_contact": "Create event on contact, if it exists. Else fallback to lead",
|
||||
"salesforce_owner_name_to_change": "Owner name to change",
|
||||
"dont_have_access_this_page": "You don't have access to this page",
|
||||
"you_need_admin_or_owner_privileges_to_access": "In order to view or interact with this page, you must have admin or owner privileges",
|
||||
"field_type": "Field type",
|
||||
"when_to_write": "When to write",
|
||||
"salesforce_on_booking_write_to_record": "On booking, write to a custom field on the attendee {{record}} record",
|
||||
"on_every_booking": "On every booking",
|
||||
"only_if_field_is_empty": "Only if field is empty",
|
||||
"booking_start_date": "Booking start date",
|
||||
"booking_created_date": "Booking created date",
|
||||
"booking_reassigned_to_host": "Booking reassigned to {{host}}",
|
||||
"access_denied": "Access Denied",
|
||||
"salesforce_route_to_owner": "Contact owner will be the Round Robin host if available",
|
||||
"salesforce_do_not_route_to_owner": "Contact owner will not be forced (can still be host if it matches the attributes and Round Robin criteria)",
|
||||
"salesforce_route_to_custom_lookup_field": "Route to a user that matches a lookup field on an account",
|
||||
"salesforce_option": "Salesforce Option",
|
||||
"lookup_field_name": "Lookup Field Name",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -139,7 +139,8 @@
|
||||
"requested_to_reschedule_subject_attendee": "Acción requerida para reprogramar: Por favor, reserva un nuevo horario para {{eventType}} con {{name}}",
|
||||
"hi_user_name": "Hola {{name}}",
|
||||
"ics_event_title": "{{eventType}} con {{name}}",
|
||||
"please_book_a_time_sometime_later": "Ups, no pudimos conectarte esta vez. Por favor, programa una llamada futura en su lugar",
|
||||
"please_book_a_time_sometime_later": "Parece que todos nuestros expertos están ocupados en este momento. Por favor, agenda una llamada en el horario que más te convenga y estaremos listos para ayudarte.",
|
||||
"schedule_instead": "Agendar ahora",
|
||||
"new_event_subject": "Nuevo evento: {{attendeeName}} - {{date}} - {{eventType}}",
|
||||
"join_by_entrypoint": "Únete por {{entryPoint}}",
|
||||
"notes": "Notas",
|
||||
@@ -198,6 +199,8 @@
|
||||
"webhook_disabled": "Webhook Deshabilitado",
|
||||
"webhook_response": "Respuesta del Webhook",
|
||||
"webhook_test": "Prueba de Webhook",
|
||||
"webhook_update_form": "Formulario de actualización de webhook",
|
||||
"webhook_update_form_description": "Agrega un webhook a tu cliente OAuth.",
|
||||
"manage_your_webhook": "Gestiona tu webhook",
|
||||
"webhook_created_successfully": "¡Webhook creado con éxito!",
|
||||
"webhook_updated_successfully": "¡Webhook actualizado con éxito!",
|
||||
@@ -348,9 +351,13 @@
|
||||
"other": "Otro",
|
||||
"email_sign_in_subject": "Tu enlace de inicio de sesión para {{appName}}",
|
||||
"round_robin_emailed_you_and_attendees": "Te reunirás con {{user}}. Enviamos un correo electrónico con una invitación de calendario con los detalles a todos.",
|
||||
"round_robin_emailed_host_and_attendee": "{{host}} se reunirá con {{attendee}}. Enviamos un correo electrónico con una invitación de calendario con los detalles a todos.",
|
||||
"emailed_you_and_attendees": "Enviamos un correo electrónico con una invitación de calendario con los detalles a todos.",
|
||||
"emailed_host_and_attendee": "Enviamos un correo electrónico con una invitación de calendario con los detalles a todos.",
|
||||
"emailed_you_and_attendees_recurring": "Enviamos un correo electrónico con una invitación de calendario con los detalles a todos para el primero de estos eventos recurrentes.",
|
||||
"emailed_host_and_attendee_recurring": "Enviamos un correo electrónico con una invitación de calendario con los detalles a todos para el primero de estos eventos recurrentes.",
|
||||
"round_robin_emailed_you_and_attendees_recurring": "Te reunirás con {{user}}. Enviamos un correo electrónico con una invitación de calendario con los detalles a todos para el primero de estos eventos recurrentes.",
|
||||
"round_robin_emailed_host_and_attendee_recurring": "{{host}} se reunirá con {{attendee}}. Enviamos un correo electrónico con una invitación de calendario con los detalles a todos para el primero de estos eventos recurrentes.",
|
||||
"emailed_you_and_any_other_attendees": "Enviamos un correo electrónico a todos con esta información.",
|
||||
"needs_to_be_confirmed_or_rejected": "Tu reserva aún necesita ser confirmada o rechazada.",
|
||||
"needs_to_be_confirmed_or_rejected_recurring": "Tu reunión recurrente aún necesita ser confirmada o rechazada.",
|
||||
@@ -703,6 +710,9 @@
|
||||
"no_teams": "Aún no tienes equipos.",
|
||||
"no_teams_description": "Los equipos permiten que otros reserven eventos compartidos entre tus compañeros de trabajo.",
|
||||
"submit": "Enviar",
|
||||
"confirm_remove_attribute": "Sí, eliminar atributo",
|
||||
"remove_attribute": "Eliminar atributo",
|
||||
"remove_attribute_confirmation_message": "¿Estás seguro de que deseas eliminar el atributo '{{name}}'? Todos los usuarios asignados actualmente serán desvinculados.",
|
||||
"delete": "Eliminar",
|
||||
"update": "Actualizar",
|
||||
"save": "Guardar",
|
||||
@@ -1237,8 +1247,8 @@
|
||||
"number_provided": "Se proporcionará el número de teléfono",
|
||||
"before_event_trigger": "antes de que comience el evento",
|
||||
"event_cancelled_trigger": "cuando el evento está cancelado",
|
||||
"after_hosts_cal_video_no_show": "Después de que los anfitriones no se unan al video de Cal",
|
||||
"after_guests_cal_video_no_show": "Después de que los invitados no se unan al video de Cal",
|
||||
"after_hosts_cal_video_no_show": "Después de que los anfitriones no se unieron a la videollamada",
|
||||
"after_guests_cal_video_no_show": "Después de que los invitados no se unieron a la videollamada",
|
||||
"new_event_trigger": "cuando se reserva un nuevo evento",
|
||||
"email_host_action": "enviar correo electrónico al anfitrión",
|
||||
"email_attendee_action": "enviar correo electrónico a los asistentes",
|
||||
@@ -1630,6 +1640,7 @@
|
||||
"format": "Formato",
|
||||
"uppercase_for_letters": "Usa mayúsculas para todas las letras",
|
||||
"replace_whitespaces_underscores": "Reemplaza los espacios en blanco con guiones bajos",
|
||||
"platform_billing": "Facturación de la plataforma",
|
||||
"manage_billing": "Gestionar facturación",
|
||||
"manage_billing_description": "Gestiona todo lo relacionado con la facturación",
|
||||
"billing_freeplan_title": "Actualmente estás en el plan GRATUITO",
|
||||
@@ -2037,6 +2048,8 @@
|
||||
"looking_for_more_insights": "¿Buscas más información?",
|
||||
"filters": "Filtros",
|
||||
"add_filter": "Agregar filtro",
|
||||
"add_rule": "Agregar regla",
|
||||
"add_rule_group": "Agregar grupo de reglas",
|
||||
"remove_filters": "Borrar todos los filtros",
|
||||
"email_verified": "Correo Electrónico Verificado",
|
||||
"select_user": "Seleccionar Usuario",
|
||||
@@ -2139,6 +2152,7 @@
|
||||
"set_up_your_organization": "Configura tu organización",
|
||||
"set_up_your_platform_organization": "Configura tu plataforma",
|
||||
"organizations_description": "Las organizaciones son entornos compartidos donde los equipos pueden crear tipos de eventos, aplicaciones, flujos de trabajo y más compartidos.",
|
||||
"plans": "Planes",
|
||||
"platform": "Plataforma",
|
||||
"platform_description": "Gestiona todo lo relacionado con la plataforma.",
|
||||
"platform_organization_description": "La plataforma de Cal.com te permite integrar la programación sin esfuerzo en tu aplicación utilizando APIs y átomos de la plataforma.",
|
||||
@@ -2201,7 +2215,7 @@
|
||||
"add_times_to_your_email": "Selecciona algunos horarios disponibles e insértalos en tu correo electrónico",
|
||||
"select_time": "Seleccionar hora",
|
||||
"select_date": "Seleccionar fecha",
|
||||
"connecting_you_to_someone": "Te estamos conectando con alguien.",
|
||||
"connecting_you_to_someone": "Te estamos conectando con alguien de {{orgName}}.",
|
||||
"please_do_not_close_this_tab": "Por favor, no cierres ni actualices esta página",
|
||||
"see_all_available_times": "Ver todos los horarios disponibles",
|
||||
"org_team_names_example_1": "p. ej. Equipo de Marketing",
|
||||
@@ -2276,6 +2290,10 @@
|
||||
"seat_options_doesnt_multiple_durations": "La opción de asiento no admite múltiples duraciones",
|
||||
"include_calendar_event": "Incluir evento en el calendario",
|
||||
"oAuth": "OAuth",
|
||||
"oAuth_client_creation_form": "Formulario de creación de cliente OAuth",
|
||||
"oAuth_client_creation_form_description": "Este es el formulario para crear un nuevo cliente OAuth",
|
||||
"oAuth_client_updation_form": "Formulario de actualización de cliente OAuth",
|
||||
"oAuth_client_updation_form_description": "Este es el formulario para editar un cliente OAuth existente",
|
||||
"recently_added": "Recientemente añadido",
|
||||
"connect_all_calendars": "Conecta todos tus calendarios",
|
||||
"connect_all_calendars_description": "{{appName}} lee la disponibilidad de todos tus calendarios existentes.",
|
||||
@@ -2732,5 +2750,21 @@
|
||||
"event_end_time_in_attendee_timezone_info": "Hora de finalización del evento en la zona horaria del asistente",
|
||||
"salesforce_create_event_on_contact": "Crear evento en el contacto, si existe. De lo contrario, usar lead como alternativa",
|
||||
"salesforce_owner_name_to_change": "Nombre del propietario a cambiar",
|
||||
"dont_have_access_this_page": "No tienes acceso a esta página",
|
||||
"you_need_admin_or_owner_privileges_to_access": "Para ver o interactuar con esta página, debes tener privilegios de administrador o propietario",
|
||||
"field_type": "Tipo de campo",
|
||||
"when_to_write": "Cuándo escribir",
|
||||
"salesforce_on_booking_write_to_record": "Al reservar, escribir en un campo personalizado en el registro {{record}} del asistente",
|
||||
"on_every_booking": "En cada reserva",
|
||||
"only_if_field_is_empty": "Solo si el campo está vacío",
|
||||
"booking_start_date": "Fecha de inicio de la reserva",
|
||||
"booking_created_date": "Fecha de creación de la reserva",
|
||||
"booking_reassigned_to_host": "Reserva reasignada a {{host}}",
|
||||
"access_denied": "Acceso denegado",
|
||||
"salesforce_route_to_owner": "El propietario del contacto será el anfitrión de Round Robin si está disponible",
|
||||
"salesforce_do_not_route_to_owner": "El propietario del contacto no será forzado (aún puede ser anfitrión si coincide con los atributos y criterios de Round Robin)",
|
||||
"salesforce_route_to_custom_lookup_field": "Dirigir a un usuario que coincida con un campo de búsqueda en una cuenta",
|
||||
"salesforce_option": "Opción de Salesforce",
|
||||
"lookup_field_name": "Nombre del campo de búsqueda",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Agrega tus nuevas cadenas arriba de esta línea ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -139,7 +139,8 @@
|
||||
"requested_to_reschedule_subject_attendee": "Reprogramar acción requerida: reserva una nueva hora para {{eventType}} con {{name}}",
|
||||
"hi_user_name": "Hola {{name}}",
|
||||
"ics_event_title": "{{eventType}} con {{name}}",
|
||||
"please_book_a_time_sometime_later": "Vaya, no pudimos conectarte esta vez. Por favor, programa una llamada futura.",
|
||||
"please_book_a_time_sometime_later": "Parece que todos nuestros expertos están ocupados en este momento. Por favor, programa una llamada en el horario que mejor te convenga y estaremos listos para ayudarte.",
|
||||
"schedule_instead": "Programar ahora",
|
||||
"new_event_subject": "Nuevo Evento: {{attendeeName}} - {{date}} - {{eventType}}",
|
||||
"join_by_entrypoint": "Únase por {{entryPoint}}",
|
||||
"notes": "Notas",
|
||||
@@ -198,6 +199,8 @@
|
||||
"webhook_disabled": "Webhook Desactivado",
|
||||
"webhook_response": "Respuesta del Webhook",
|
||||
"webhook_test": "Prueba del Webhook",
|
||||
"webhook_update_form": "Formulario de actualización de webhook",
|
||||
"webhook_update_form_description": "Añade un webhook a tu cliente OAuth.",
|
||||
"manage_your_webhook": "Administra tu webhook",
|
||||
"webhook_created_successfully": "¡Webhook creado con éxito!",
|
||||
"webhook_updated_successfully": "¡Webhook actualizado con éxito!",
|
||||
@@ -348,9 +351,13 @@
|
||||
"other": "Otro",
|
||||
"email_sign_in_subject": "Su enlace de inicio de sesión para {{appName}}",
|
||||
"round_robin_emailed_you_and_attendees": "Te reunirás con {{user}}. Hemos enviado un correo electrónico con una invitación de calendario con los detalles a todos.",
|
||||
"round_robin_emailed_host_and_attendee": "{{host}} se reunirá con {{attendee}}. Hemos enviado un correo electrónico con una invitación de calendario con los detalles a todos.",
|
||||
"emailed_you_and_attendees": "Le enviamos a usted y a los demás asistentes una invitación al calendario con todos los detalles.",
|
||||
"emailed_host_and_attendee": "Hemos enviado un correo electrónico con una invitación de calendario con los detalles a todos.",
|
||||
"emailed_you_and_attendees_recurring": "Le enviamos a usted y a los demás asistentes una invitación al calendario para el primero de estos eventos recurrentes.",
|
||||
"emailed_host_and_attendee_recurring": "Hemos enviado un correo electrónico con una invitación de calendario con los detalles a todos para el primero de estos eventos recurrentes.",
|
||||
"round_robin_emailed_you_and_attendees_recurring": "Te reunirás con {{user}}. Hemos enviado un correo electrónico con una invitación de calendario con los detalles a todos para el primero de estos eventos recurrentes.",
|
||||
"round_robin_emailed_host_and_attendee_recurring": "{{host}} se reunirá con {{attendee}}. Hemos enviado un correo electrónico con una invitación de calendario con los detalles a todos para el primero de estos eventos recurrentes.",
|
||||
"emailed_you_and_any_other_attendees": "Usted y cualquier otro asistente han recibido esta información.",
|
||||
"needs_to_be_confirmed_or_rejected": "Su reserva todavía necesita ser confirmada o rechazada.",
|
||||
"needs_to_be_confirmed_or_rejected_recurring": "Su reunión recurrente todavía necesita confirmarse o rechazarse.",
|
||||
@@ -703,6 +710,9 @@
|
||||
"no_teams": "Todavía no tienes ningún equipo.",
|
||||
"no_teams_description": "Los equipos permiten que otros reserven eventos compartidos entre tus compañeros de trabajo.",
|
||||
"submit": "Enviar",
|
||||
"confirm_remove_attribute": "Sí, eliminar atributo",
|
||||
"remove_attribute": "Eliminar atributo",
|
||||
"remove_attribute_confirmation_message": "¿Estás seguro de que deseas eliminar el atributo '{{name}}'? Todos los usuarios asignados actualmente serán desvinculados.",
|
||||
"delete": "Eliminar",
|
||||
"update": "Actualizar",
|
||||
"save": "Guardar",
|
||||
@@ -1237,8 +1247,8 @@
|
||||
"number_provided": "Se proporcionará el número de teléfono",
|
||||
"before_event_trigger": "antes del inicio del evento",
|
||||
"event_cancelled_trigger": "cuando se cancela el evento",
|
||||
"after_hosts_cal_video_no_show": "Después de que los anfitriones no se unan al video de Cal",
|
||||
"after_guests_cal_video_no_show": "Después de que los invitados no se unan al video de Cal",
|
||||
"after_hosts_cal_video_no_show": "Después de que los anfitriones no se unieron al video de Cal",
|
||||
"after_guests_cal_video_no_show": "Después de que los invitados no se unieron al video de Cal",
|
||||
"new_event_trigger": "cuando se reserva un nuevo evento",
|
||||
"email_host_action": "enviar correo electrónico al anfitrión",
|
||||
"email_attendee_action": "enviar correo electrónico a los asistentes",
|
||||
@@ -1630,6 +1640,7 @@
|
||||
"format": "Formato",
|
||||
"uppercase_for_letters": "Use mayúsculas para todas las letras",
|
||||
"replace_whitespaces_underscores": "Reemplace espacios en blanco con guiones bajos",
|
||||
"platform_billing": "Facturación de la plataforma",
|
||||
"manage_billing": "Gestione la facturación",
|
||||
"manage_billing_description": "Gestione todo lo relacionado con la facturación",
|
||||
"billing_freeplan_title": "Actualmente está en el plan GRATUITO",
|
||||
@@ -2037,6 +2048,8 @@
|
||||
"looking_for_more_insights": "¿Quiere saber más sobre Insights?",
|
||||
"filters": "Filtros",
|
||||
"add_filter": "Añadir filtro",
|
||||
"add_rule": "Añadir regla",
|
||||
"add_rule_group": "Añadir grupo de reglas",
|
||||
"remove_filters": "Borrar todos los filtros",
|
||||
"email_verified": "Correo electrónico verificado",
|
||||
"select_user": "Seleccionar usuario",
|
||||
@@ -2139,6 +2152,7 @@
|
||||
"set_up_your_organization": "Configure su organización",
|
||||
"set_up_your_platform_organization": "Configura tu plataforma",
|
||||
"organizations_description": "Las organizaciones son entornos compartidos donde los equipos pueden crear tipos de eventos, aplicaciones, flujos de trabajo compartidos y más.",
|
||||
"plans": "Planes",
|
||||
"platform": "Plataforma",
|
||||
"platform_description": "Gestiona todo lo relacionado con la plataforma.",
|
||||
"platform_organization_description": "Cal.com Platform te permite integrar la programación sin esfuerzo en tu aplicación utilizando APIs y átomos de la plataforma.",
|
||||
@@ -2201,7 +2215,7 @@
|
||||
"add_times_to_your_email": "Seleccione algunos horarios disponibles e incrústelos en su correo electrónico",
|
||||
"select_time": "Seleccione la hora",
|
||||
"select_date": "Seleccione la fecha",
|
||||
"connecting_you_to_someone": "Te estamos conectando con alguien.",
|
||||
"connecting_you_to_someone": "Te estamos conectando con alguien de {{orgName}}.",
|
||||
"please_do_not_close_this_tab": "Por favor, no cierres ni actualices esta página",
|
||||
"see_all_available_times": "Ver todas las horas disponibles",
|
||||
"org_team_names_example_1": "ej. Equipo de marketing",
|
||||
@@ -2276,6 +2290,10 @@
|
||||
"seat_options_doesnt_multiple_durations": "La opción Cupo no soporta múltiples duraciones",
|
||||
"include_calendar_event": "Incluir evento del calendario",
|
||||
"oAuth": "OAuth",
|
||||
"oAuth_client_creation_form": "Formulario de creación de cliente OAuth",
|
||||
"oAuth_client_creation_form_description": "Este es el formulario para crear un nuevo cliente OAuth",
|
||||
"oAuth_client_updation_form": "Formulario de actualización de cliente OAuth",
|
||||
"oAuth_client_updation_form_description": "Este es el formulario para editar un cliente OAuth existente",
|
||||
"recently_added": "Añadido recientemente",
|
||||
"connect_all_calendars": "Conecte todos sus calendarios",
|
||||
"connect_all_calendars_description": "{{appName}} lee la disponibilidad de todos tus calendarios existentes.",
|
||||
@@ -2732,5 +2750,21 @@
|
||||
"event_end_time_in_attendee_timezone_info": "Hora de finalización del evento en la zona horaria del asistente",
|
||||
"salesforce_create_event_on_contact": "Crear evento en el contacto, si existe. De lo contrario, usar el lead",
|
||||
"salesforce_owner_name_to_change": "Nombre del propietario a cambiar",
|
||||
"dont_have_access_this_page": "No tienes acceso a esta página",
|
||||
"you_need_admin_or_owner_privileges_to_access": "Para ver o interactuar con esta página, debes tener privilegios de administrador o propietario",
|
||||
"field_type": "Tipo de campo",
|
||||
"when_to_write": "Cuándo escribir",
|
||||
"salesforce_on_booking_write_to_record": "Al reservar, escribir en un campo personalizado en el registro {{record}} del asistente",
|
||||
"on_every_booking": "En cada reserva",
|
||||
"only_if_field_is_empty": "Solo si el campo está vacío",
|
||||
"booking_start_date": "Fecha de inicio de la reserva",
|
||||
"booking_created_date": "Fecha de creación de la reserva",
|
||||
"booking_reassigned_to_host": "Reserva reasignada a {{host}}",
|
||||
"access_denied": "Acceso denegado",
|
||||
"salesforce_route_to_owner": "El propietario del contacto será el anfitrión de Round Robin si está disponible",
|
||||
"salesforce_do_not_route_to_owner": "No se forzará al propietario del contacto (aún puede ser anfitrión si coincide con los atributos y criterios de Round Robin)",
|
||||
"salesforce_route_to_custom_lookup_field": "Dirigir a un usuario que coincida con un campo de búsqueda en una cuenta",
|
||||
"salesforce_option": "Opción de Salesforce",
|
||||
"lookup_field_name": "Nombre del campo de búsqueda",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Agregue sus nuevas cadenas arriba ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user