feat: api v2 org ooo crud (#18499)
* feat: api v2 org ooo crud * chore: add guard isUserOOO on ooo controller * chore: add checks for ooo * fixup! chore: add checks for ooo * added e2e tests * fixup! added e2e tests
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
import { UserOOORepository } from "@/modules/ooo/repositories/ooo.repository";
|
||||
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from "@nestjs/common";
|
||||
import { Request } from "express";
|
||||
|
||||
@Injectable()
|
||||
export class IsUserOOO implements CanActivate {
|
||||
constructor(private oooRepo: UserOOORepository) {}
|
||||
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest<Request>();
|
||||
const oooId: string = request.params.oooId;
|
||||
const userId: string = request.params.userId;
|
||||
|
||||
if (!userId) {
|
||||
throw new ForbiddenException("No user id found in request params.");
|
||||
}
|
||||
|
||||
if (!oooId) {
|
||||
throw new ForbiddenException("No ooo entry id found in request params.");
|
||||
}
|
||||
|
||||
const ooo = await this.oooRepo.getUserOOOByIdAndUserId(Number(oooId), Number(userId));
|
||||
|
||||
if (ooo) {
|
||||
return true;
|
||||
}
|
||||
|
||||
throw new ForbiddenException("This OOO entry does not belong to this user.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { BadRequestException } from "@nestjs/common";
|
||||
import { ApiProperty, ApiPropertyOptional, PartialType } from "@nestjs/swagger";
|
||||
import { Transform } from "class-transformer";
|
||||
import { IsDate, IsInt, IsOptional, IsString, IsEnum, isDate } from "class-validator";
|
||||
|
||||
export enum OutOfOfficeReason {
|
||||
UNSPECIFIED = "unspecified",
|
||||
VACATION = "vacation",
|
||||
TRAVEL = "travel",
|
||||
SICK_LEAVE = "sick",
|
||||
PUBLIC_HOLIDAY = "public_holiday",
|
||||
}
|
||||
|
||||
export type OutOfOfficeReasonType = `${OutOfOfficeReason}`;
|
||||
|
||||
const isDateString = (dateString: string) => {
|
||||
try {
|
||||
const isoDateRegex = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(\.\d{3})?Z$/;
|
||||
return isoDateRegex.test(dateString);
|
||||
} catch {
|
||||
throw new BadRequestException("Invalid Date.");
|
||||
}
|
||||
};
|
||||
|
||||
export class CreateOutOfOfficeEntryDto {
|
||||
@Transform(({ value }: { value: string }) => {
|
||||
if (isDateString(value)) {
|
||||
const date = new Date(value);
|
||||
date.setUTCHours(0, 0, 0, 0);
|
||||
return date;
|
||||
}
|
||||
throw new BadRequestException("Invalid Date.");
|
||||
})
|
||||
@IsDate()
|
||||
@ApiProperty({
|
||||
description: "The start date and time of the out of office period in ISO 8601 format in UTC timezone.",
|
||||
example: "2023-05-01T00:00:00.000Z",
|
||||
})
|
||||
start!: Date;
|
||||
|
||||
@Transform(({ value }: { value: string }) => {
|
||||
if (isDateString(value)) {
|
||||
const date = new Date(value);
|
||||
date.setUTCHours(23, 59, 59, 999);
|
||||
return date;
|
||||
}
|
||||
throw new BadRequestException("Invalid Date.");
|
||||
})
|
||||
@IsDate()
|
||||
@ApiProperty({
|
||||
description: "The end date and time of the out of office period in ISO 8601 format in UTC timezone.",
|
||||
example: "2023-05-10T23:59:59.999Z",
|
||||
})
|
||||
end!: Date;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
description: "Optional notes for the out of office entry.",
|
||||
example: "Vacation in Hawaii",
|
||||
})
|
||||
notes?: string;
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
description: "The ID of the user covering for the out of office period, if applicable.",
|
||||
example: 2,
|
||||
})
|
||||
toUserId?: number;
|
||||
|
||||
@IsEnum(OutOfOfficeReason)
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
description: "the reason for the out of office entry, if applicable",
|
||||
example: "vacation",
|
||||
})
|
||||
reason?: OutOfOfficeReasonType;
|
||||
}
|
||||
|
||||
export class UpdateOutOfOfficeEntryDto extends PartialType(CreateOutOfOfficeEntryDto) {}
|
||||
@@ -0,0 +1,99 @@
|
||||
import { OutOfOfficeReason } from "@/modules/ooo/inputs/ooo.input";
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Expose, Type } from "class-transformer";
|
||||
import { IsInt, IsEnum, ValidateNested, IsString, IsDateString, IsOptional } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
export class UserOooOutputDto {
|
||||
@IsInt()
|
||||
@Expose()
|
||||
@ApiProperty({
|
||||
description: "The ID of the user.",
|
||||
example: 2,
|
||||
})
|
||||
readonly userId!: number;
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
description: "The ID of the user covering for the out of office period, if applicable.",
|
||||
example: 2,
|
||||
})
|
||||
@Expose()
|
||||
readonly toUserId?: number;
|
||||
|
||||
@IsInt()
|
||||
@Expose()
|
||||
@ApiProperty({
|
||||
description: "The ID of the ooo entry.",
|
||||
example: 2,
|
||||
})
|
||||
readonly id!: number;
|
||||
|
||||
@IsString()
|
||||
@Expose()
|
||||
@ApiProperty({
|
||||
description: "The UUID of the ooo entry.",
|
||||
example: 2,
|
||||
})
|
||||
readonly uuid!: string;
|
||||
|
||||
@IsDateString()
|
||||
@ApiProperty({
|
||||
description: "The start date and time of the out of office period in ISO 8601 format in UTC timezone.",
|
||||
example: "2023-05-01T00:00:00.000Z",
|
||||
})
|
||||
@Expose()
|
||||
start!: Date;
|
||||
|
||||
@IsDateString()
|
||||
@ApiProperty({
|
||||
description: "The end date and time of the out of office period in ISO 8601 format in UTC timezone.",
|
||||
example: "2023-05-10T23:59:59.999Z",
|
||||
})
|
||||
@Expose()
|
||||
end!: Date;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
description: "Optional notes for the out of office entry.",
|
||||
example: "Vacation in Hawaii",
|
||||
})
|
||||
@Expose()
|
||||
notes?: string;
|
||||
|
||||
@IsEnum(OutOfOfficeReason)
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({
|
||||
description: "the reason for the out of office entry, if applicable",
|
||||
example: "vacation",
|
||||
})
|
||||
@Expose()
|
||||
reason?: OutOfOfficeReason;
|
||||
}
|
||||
|
||||
export class UserOooOutputResponseDto {
|
||||
@Expose()
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@Expose()
|
||||
@ValidateNested()
|
||||
@Type(() => UserOooOutputDto)
|
||||
data!: UserOooOutputDto;
|
||||
}
|
||||
|
||||
export class UserOoosOutputResponseDto {
|
||||
@Expose()
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@Expose()
|
||||
@ValidateNested()
|
||||
@Type(() => UserOooOutputDto)
|
||||
data!: UserOooOutputDto[];
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
|
||||
import { Prisma } from "@calcom/prisma/client";
|
||||
|
||||
import { PrismaWriteService } from "../../prisma/prisma-write.service";
|
||||
|
||||
type OOOInputData = Omit<Prisma.OutOfOfficeEntryCreateInput, "user" | "toUser" | "reason" | "uuid"> & {
|
||||
toUserId?: number;
|
||||
userId: number;
|
||||
reasonId?: number;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class UserOOORepository {
|
||||
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||
|
||||
async createUserOOO(data: OOOInputData) {
|
||||
const uuid = uuidv4();
|
||||
return this.dbWrite.prisma.outOfOfficeEntry.create({
|
||||
data: { ...data, uuid },
|
||||
include: { reason: true },
|
||||
});
|
||||
}
|
||||
|
||||
async updateUserOOO(oooId: number, data: Partial<OOOInputData>) {
|
||||
return this.dbWrite.prisma.outOfOfficeEntry.update({
|
||||
where: { id: oooId },
|
||||
data,
|
||||
include: { reason: true },
|
||||
});
|
||||
}
|
||||
|
||||
async getUserOOOById(oooId: number) {
|
||||
return this.dbRead.prisma.outOfOfficeEntry.findFirst({
|
||||
where: { id: oooId },
|
||||
include: { reason: true },
|
||||
});
|
||||
}
|
||||
|
||||
async getUserOOOByIdAndUserId(oooId: number, userId: number) {
|
||||
return this.dbRead.prisma.outOfOfficeEntry.findFirst({
|
||||
where: { id: oooId, userId },
|
||||
include: { reason: true },
|
||||
});
|
||||
}
|
||||
|
||||
async getUserOOOPaginated(userId: number, skip: number, take: number) {
|
||||
return this.dbRead.prisma.outOfOfficeEntry.findMany({
|
||||
where: { userId },
|
||||
skip,
|
||||
take,
|
||||
include: { reason: true },
|
||||
});
|
||||
}
|
||||
|
||||
async deleteUserOOO(oooId: number) {
|
||||
return this.dbWrite.prisma.outOfOfficeEntry.delete({
|
||||
where: { id: oooId },
|
||||
include: { reason: true },
|
||||
});
|
||||
}
|
||||
|
||||
async findExistingOooRedirect(userId: number, start: Date, end: Date, toUserId?: number) {
|
||||
const existingOutOfOfficeEntry = await this.dbRead.prisma.outOfOfficeEntry.findFirst({
|
||||
select: {
|
||||
userId: true,
|
||||
toUserId: true,
|
||||
},
|
||||
where: {
|
||||
...(toUserId && { userId: toUserId }),
|
||||
toUserId: userId,
|
||||
// Check for time overlap or collision
|
||||
OR: [
|
||||
// Outside of range
|
||||
{
|
||||
AND: [{ start: { lte: end } }, { end: { gte: start } }],
|
||||
},
|
||||
// Inside of range
|
||||
{
|
||||
AND: [{ start: { gte: start } }, { end: { lte: end } }],
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
return existingOutOfOfficeEntry;
|
||||
}
|
||||
|
||||
async getOooByUserIdAndTime(userId: number, start: Date, end: Date) {
|
||||
return await this.dbRead.prisma.outOfOfficeEntry.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
start,
|
||||
end,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
import {
|
||||
CreateOutOfOfficeEntryDto,
|
||||
UpdateOutOfOfficeEntryDto,
|
||||
OutOfOfficeReason,
|
||||
} from "@/modules/ooo/inputs/ooo.input";
|
||||
import { UserOOORepository } from "@/modules/ooo/repositories/ooo.repository";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { BadRequestException, ConflictException, Injectable } from "@nestjs/common";
|
||||
|
||||
import { OutOfOfficeEntry } from "@calcom/prisma/client";
|
||||
|
||||
const OOO_REASON_ID_TO_REASON = {
|
||||
1: OutOfOfficeReason["UNSPECIFIED"],
|
||||
2: OutOfOfficeReason["VACATION"],
|
||||
3: OutOfOfficeReason["TRAVEL"],
|
||||
4: OutOfOfficeReason["SICK_LEAVE"],
|
||||
5: OutOfOfficeReason["PUBLIC_HOLIDAY"],
|
||||
};
|
||||
|
||||
const OOO_REASON_TO_REASON_ID = {
|
||||
[OutOfOfficeReason["UNSPECIFIED"]]: 1,
|
||||
[OutOfOfficeReason["VACATION"]]: 2,
|
||||
[OutOfOfficeReason["TRAVEL"]]: 3,
|
||||
[OutOfOfficeReason["SICK_LEAVE"]]: 4,
|
||||
[OutOfOfficeReason["PUBLIC_HOLIDAY"]]: 5,
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class UserOOOService {
|
||||
constructor(
|
||||
private readonly oooRepository: UserOOORepository,
|
||||
private readonly usersRepository: UsersRepository
|
||||
) {}
|
||||
|
||||
formatOooReason(ooo: OutOfOfficeEntry) {
|
||||
return {
|
||||
...ooo,
|
||||
reason: ooo.reasonId
|
||||
? OOO_REASON_ID_TO_REASON[ooo.reasonId as keyof typeof OOO_REASON_ID_TO_REASON]
|
||||
: OOO_REASON_ID_TO_REASON[1],
|
||||
};
|
||||
}
|
||||
|
||||
isStartBeforeEnd(start?: Date, end?: Date) {
|
||||
if ((end && !start) || (start && !end)) {
|
||||
throw new BadRequestException("Please specify both ooo start and end time.");
|
||||
}
|
||||
|
||||
if (start && end) {
|
||||
if (start.getTime() > end.getTime()) {
|
||||
throw new BadRequestException("Start date must be before end date.");
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async checkUserEligibleForRedirect(userId: number, toUserId?: number) {
|
||||
if (toUserId) {
|
||||
const user = await this.usersRepository.findUserOOORedirectEligible(userId, toUserId);
|
||||
if (!user) {
|
||||
throw new BadRequestException("Cannot redirect to this user.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async checkExistingOooRedirect(userId: number, start?: Date, end?: Date, toUserId?: number) {
|
||||
if (start && end) {
|
||||
const existingOooRedirect = await this.oooRepository.findExistingOooRedirect(
|
||||
userId,
|
||||
start,
|
||||
end,
|
||||
toUserId
|
||||
);
|
||||
|
||||
if (existingOooRedirect) {
|
||||
throw new BadRequestException("Booking redirect infinite not allowed.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async checkDuplicateOOOEntry(userId: number, start?: Date, end?: Date) {
|
||||
if (start && end) {
|
||||
const duplicateEntry = await this.oooRepository.getOooByUserIdAndTime(userId, start, end);
|
||||
|
||||
if (duplicateEntry) {
|
||||
throw new ConflictException("Ooo entry already exists.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checkRedirectToSelf(userId: number, toUserId?: number) {
|
||||
if (toUserId && toUserId === userId) {
|
||||
throw new BadRequestException("Cannot redirect to self.");
|
||||
}
|
||||
}
|
||||
|
||||
async checkIsValidOOO(userId: number, ooo: CreateOutOfOfficeEntryDto | UpdateOutOfOfficeEntryDto) {
|
||||
this.isStartBeforeEnd(ooo.start, ooo.end);
|
||||
await this.checkExistingOooRedirect(userId, ooo.start, ooo.end, ooo.toUserId);
|
||||
await this.checkDuplicateOOOEntry(userId, ooo.start, ooo.end);
|
||||
await this.checkRedirectToSelf(userId, ooo.toUserId);
|
||||
await this.checkUserEligibleForRedirect(userId, ooo.toUserId);
|
||||
}
|
||||
|
||||
async createUserOOO(userId: number, body: CreateOutOfOfficeEntryDto) {
|
||||
await this.checkIsValidOOO(userId, body);
|
||||
const { reason, ...rest } = body;
|
||||
const ooo = await this.oooRepository.createUserOOO({
|
||||
...rest,
|
||||
userId,
|
||||
reasonId: OOO_REASON_TO_REASON_ID[reason ?? OutOfOfficeReason["UNSPECIFIED"]],
|
||||
});
|
||||
return this.formatOooReason(ooo);
|
||||
}
|
||||
|
||||
async updateUserOOO(userId: number, oooId: number, body: UpdateOutOfOfficeEntryDto) {
|
||||
await this.checkIsValidOOO(userId, body);
|
||||
const { reason, ...rest } = body;
|
||||
const data = reason
|
||||
? { ...rest, reasonId: OOO_REASON_TO_REASON_ID[reason ?? OutOfOfficeReason["UNSPECIFIED"]] }
|
||||
: rest;
|
||||
const ooo = await this.oooRepository.updateUserOOO(oooId, data);
|
||||
return this.formatOooReason(ooo);
|
||||
}
|
||||
|
||||
async deleteUserOOO(oooId: number) {
|
||||
const ooo = await this.oooRepository.deleteUserOOO(oooId);
|
||||
return this.formatOooReason(ooo);
|
||||
}
|
||||
|
||||
async getUserOOOPaginated(userId: number, skip: number, take: number) {
|
||||
const ooos = await this.oooRepository.getUserOOOPaginated(userId, skip, take);
|
||||
return ooos.map((ooo) => this.formatOooReason(ooo));
|
||||
}
|
||||
}
|
||||
+114
@@ -0,0 +1,114 @@
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { PlatformPlan } from "@/modules/auth/decorators/billing/platform-plan.decorator";
|
||||
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { PlatformPlanGuard } from "@/modules/auth/guards/billing/platform-plan.guard";
|
||||
import { IsAdminAPIEnabledGuard } from "@/modules/auth/guards/organizations/is-admin-api-enabled.guard";
|
||||
import { IsOrgGuard } from "@/modules/auth/guards/organizations/is-org.guard";
|
||||
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||
import { IsUserInOrg } from "@/modules/auth/guards/users/is-user-in-org.guard";
|
||||
import { IsUserOOO } from "@/modules/ooo/guards/is-user-ooo";
|
||||
import { CreateOutOfOfficeEntryDto, UpdateOutOfOfficeEntryDto } from "@/modules/ooo/inputs/ooo.input";
|
||||
import {
|
||||
UserOooOutputDto,
|
||||
UserOooOutputResponseDto,
|
||||
UserOoosOutputResponseDto,
|
||||
} from "@/modules/ooo/outputs/ooo.output";
|
||||
import { UserOOOService } from "@/modules/ooo/services/ooo.service";
|
||||
import {
|
||||
Controller,
|
||||
UseGuards,
|
||||
Get,
|
||||
Post,
|
||||
Patch,
|
||||
Delete,
|
||||
Param,
|
||||
ParseIntPipe,
|
||||
Body,
|
||||
UseInterceptors,
|
||||
Query,
|
||||
} from "@nestjs/common";
|
||||
import { ClassSerializerInterceptor } from "@nestjs/common";
|
||||
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
import { plainToInstance } from "class-transformer";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { SkipTakePagination } from "@calcom/platform-types";
|
||||
|
||||
@Controller({
|
||||
path: "/v2/organizations/:orgId/users/:userId/ooo",
|
||||
version: API_VERSIONS_VALUES,
|
||||
})
|
||||
@UseInterceptors(ClassSerializerInterceptor)
|
||||
@UseGuards(ApiAuthGuard, IsOrgGuard, RolesGuard, PlatformPlanGuard, IsAdminAPIEnabledGuard)
|
||||
@UseGuards(IsOrgGuard)
|
||||
@DocsTags("Orgs / Users / OOO")
|
||||
export class OrganizationsUsersOOOController {
|
||||
constructor(private readonly userOOOService: UserOOOService) {}
|
||||
|
||||
@Get()
|
||||
@Roles("ORG_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@UseGuards(IsUserInOrg)
|
||||
@ApiOperation({ summary: "Get all ooo entries of a user" })
|
||||
async getOrganizationUserOOO(
|
||||
@Param("userId", ParseIntPipe) userId: number,
|
||||
@Query() query: SkipTakePagination
|
||||
): Promise<UserOoosOutputResponseDto> {
|
||||
const ooos = await this.userOOOService.getUserOOOPaginated(userId, query.skip ?? 0, query.take ?? 250);
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: ooos.map((ooo) => plainToInstance(UserOooOutputDto, ooo, { strategy: "excludeAll" })),
|
||||
};
|
||||
}
|
||||
|
||||
@Post()
|
||||
@Roles("ORG_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@UseGuards(IsUserInOrg)
|
||||
@ApiOperation({ summary: "Create an ooo entry for user" })
|
||||
async createOrganizationUserOOO(
|
||||
@Param("userId", ParseIntPipe) userId: number,
|
||||
@Body() input: CreateOutOfOfficeEntryDto
|
||||
): Promise<UserOooOutputResponseDto> {
|
||||
const ooo = await this.userOOOService.createUserOOO(userId, input);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToInstance(UserOooOutputDto, ooo, { strategy: "excludeAll" }),
|
||||
};
|
||||
}
|
||||
|
||||
@Patch("/:oooId")
|
||||
@Roles("ORG_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@UseGuards(IsUserInOrg, IsUserOOO)
|
||||
@ApiOperation({ summary: "Update ooo entry of a user" })
|
||||
async updateOrganizationUserOOO(
|
||||
@Param("userId", ParseIntPipe) userId: number,
|
||||
@Param("oooId", ParseIntPipe) oooId: number,
|
||||
|
||||
@Body() input: UpdateOutOfOfficeEntryDto
|
||||
): Promise<UserOooOutputResponseDto> {
|
||||
const ooo = await this.userOOOService.updateUserOOO(userId, oooId, input);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToInstance(UserOooOutputDto, ooo, { strategy: "excludeAll" }),
|
||||
};
|
||||
}
|
||||
|
||||
@Delete("/:oooId")
|
||||
@Roles("ORG_ADMIN")
|
||||
@PlatformPlan("ESSENTIALS")
|
||||
@UseGuards(IsUserInOrg, IsUserOOO)
|
||||
@ApiOperation({ summary: "Delete ooo entry of a user" })
|
||||
async deleteOrganizationUserOOO(
|
||||
@Param("oooId", ParseIntPipe) oooId: number
|
||||
): Promise<UserOooOutputResponseDto> {
|
||||
const ooo = await this.userOOOService.deleteUserOOO(oooId);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToInstance(UserOooOutputDto, ooo, { strategy: "excludeAll" }),
|
||||
};
|
||||
}
|
||||
}
|
||||
+421
@@ -0,0 +1,421 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { UserOooOutputDto } from "@/modules/ooo/outputs/ooo.output";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.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 { 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";
|
||||
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 { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { Team } from "@calcom/prisma/client";
|
||||
|
||||
describe("Organizations User OOO Endpoints", () => {
|
||||
describe("User Authentication - User is Org Admin", () => {
|
||||
let app: INestApplication;
|
||||
let oooCreatedViaApiId: number;
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let organizationsRepositoryFixture: OrganizationRepositoryFixture;
|
||||
let teamsRepositoryFixture: TeamRepositoryFixture;
|
||||
let membershipsRepositoryFixture: MembershipRepositoryFixture;
|
||||
let profileRepositoryFixture: ProfileRepositoryFixture;
|
||||
|
||||
let org: Team;
|
||||
let team: Team;
|
||||
let falseTestOrg: Team;
|
||||
let falseTestTeam: Team;
|
||||
|
||||
const userEmail = "org-admin-ooo-controller-e222e@api.com";
|
||||
let userAdmin: User;
|
||||
|
||||
const teammate1Email = "teammate111ooo@team.com";
|
||||
const teammate2Email = "teammate221ooo@team.com";
|
||||
const falseTestUserEmail = "false-user-ooo@false-team.com";
|
||||
let teammate1: User;
|
||||
let teammate2: User;
|
||||
let falseTestUser: User;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await withApiAuth(
|
||||
userEmail,
|
||||
Test.createTestingModule({
|
||||
imports: [AppModule, PrismaModule, UsersModule, TokensModule],
|
||||
})
|
||||
).compile();
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
organizationsRepositoryFixture = new OrganizationRepositoryFixture(moduleRef);
|
||||
teamsRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
membershipsRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||
profileRepositoryFixture = new ProfileRepositoryFixture(moduleRef);
|
||||
|
||||
userAdmin = await userRepositoryFixture.create({
|
||||
email: userEmail,
|
||||
username: userEmail,
|
||||
role: "ADMIN",
|
||||
});
|
||||
|
||||
teammate1 = await userRepositoryFixture.create({
|
||||
email: teammate1Email,
|
||||
username: teammate1Email,
|
||||
});
|
||||
|
||||
teammate2 = await userRepositoryFixture.create({
|
||||
email: teammate2Email,
|
||||
username: teammate2Email,
|
||||
});
|
||||
|
||||
falseTestUser = await userRepositoryFixture.create({
|
||||
email: falseTestUserEmail,
|
||||
username: falseTestUserEmail,
|
||||
});
|
||||
|
||||
org = await organizationsRepositoryFixture.create({
|
||||
name: "Test Organization ooo",
|
||||
isOrganization: true,
|
||||
});
|
||||
|
||||
falseTestOrg = await organizationsRepositoryFixture.create({
|
||||
name: "False test org ooo",
|
||||
isOrganization: true,
|
||||
});
|
||||
|
||||
team = await teamsRepositoryFixture.create({
|
||||
name: "Test org team ooo",
|
||||
isOrganization: false,
|
||||
parent: { connect: { id: org.id } },
|
||||
});
|
||||
|
||||
falseTestTeam = await teamsRepositoryFixture.create({
|
||||
name: "Outside org team ooo",
|
||||
isOrganization: false,
|
||||
parent: { connect: { id: falseTestOrg.id } },
|
||||
});
|
||||
|
||||
await profileRepositoryFixture.create({
|
||||
uid: `usr-${userAdmin.id}`,
|
||||
username: userEmail,
|
||||
organization: {
|
||||
connect: {
|
||||
id: org.id,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
connect: {
|
||||
id: userAdmin.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await profileRepositoryFixture.create({
|
||||
uid: `usr-${teammate1.id}`,
|
||||
username: teammate1Email,
|
||||
organization: {
|
||||
connect: {
|
||||
id: org.id,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
connect: {
|
||||
id: teammate1.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "ADMIN",
|
||||
user: { connect: { id: userAdmin.id } },
|
||||
team: { connect: { id: org.id } },
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "MEMBER",
|
||||
user: { connect: { id: teammate1.id } },
|
||||
team: { connect: { id: team.id } },
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "MEMBER",
|
||||
user: { connect: { id: teammate2.id } },
|
||||
team: { connect: { id: team.id } },
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
await membershipsRepositoryFixture.create({
|
||||
role: "MEMBER",
|
||||
user: { connect: { id: falseTestUser.id } },
|
||||
team: { connect: { id: falseTestTeam.id } },
|
||||
accepted: true,
|
||||
});
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
it("should be defined", () => {
|
||||
expect(userRepositoryFixture).toBeDefined();
|
||||
expect(organizationsRepositoryFixture).toBeDefined();
|
||||
expect(userAdmin).toBeDefined();
|
||||
expect(org).toBeDefined();
|
||||
});
|
||||
|
||||
it("should create a ooo entry without redirect", async () => {
|
||||
const body = {
|
||||
start: "2025-05-01T01:00:00.000Z",
|
||||
end: "2025-05-10T13:59:59.999Z",
|
||||
notes: "ooo numero uno",
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo`)
|
||||
.send(body)
|
||||
.expect(201)
|
||||
.then((response) => {
|
||||
const responseBody = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
|
||||
const data = responseBody.data as UserOooOutputDto;
|
||||
expect(data.reason).toEqual("unspecified");
|
||||
expect(data.userId).toEqual(teammate1.id);
|
||||
expect(data.start).toEqual("2025-05-01T00:00:00.000Z");
|
||||
expect(data.end).toEqual("2025-05-10T23:59:59.999Z");
|
||||
oooCreatedViaApiId = data.id;
|
||||
});
|
||||
});
|
||||
|
||||
it("should create a ooo entry with redirect", async () => {
|
||||
const body = {
|
||||
start: "2025-08-01T01:00:00.000Z",
|
||||
end: "2025-10-10T13:59:59.999Z",
|
||||
notes: "ooo numero dos with redirect",
|
||||
toUserId: teammate2.id,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo`)
|
||||
.send(body)
|
||||
.expect(201)
|
||||
.then((response) => {
|
||||
const responseBody = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
const data = responseBody.data as UserOooOutputDto;
|
||||
expect(data.reason).toEqual("unspecified");
|
||||
expect(data.userId).toEqual(teammate1.id);
|
||||
expect(data.start).toEqual("2025-08-01T00:00:00.000Z");
|
||||
expect(data.end).toEqual("2025-10-10T23:59:59.999Z");
|
||||
expect(data.toUserId).toEqual(teammate2.id);
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail to create a ooo entry with start after end", async () => {
|
||||
const body = {
|
||||
start: "2025-07-01T00:00:00.000Z",
|
||||
end: "2025-05-10T23:59:59.999Z",
|
||||
notes: "ooo numero uno duplicate",
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo`)
|
||||
.send(body)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it("should fail to create a duplicate ooo entry", async () => {
|
||||
const body = {
|
||||
start: "2025-05-01T00:00:00.000Z",
|
||||
end: "2025-05-10T23:59:59.999Z",
|
||||
notes: "ooo numero uno duplicate",
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo`)
|
||||
.send(body)
|
||||
.expect(409);
|
||||
});
|
||||
|
||||
it("should fail to create an ooo entry that redirects to self", async () => {
|
||||
const body = {
|
||||
start: "2025-05-02T00:00:00.000Z",
|
||||
end: "2025-05-03T23:59:59.999Z",
|
||||
notes: "ooo infinite redirect",
|
||||
toUserId: teammate1.id,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo`)
|
||||
.send(body)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it("should fail to create an ooo entry that redirects to member outside of org", async () => {
|
||||
const body = {
|
||||
start: "2025-05-02T00:00:00.000Z",
|
||||
end: "2025-05-03T23:59:59.999Z",
|
||||
notes: "ooo invalid redirect",
|
||||
toUserId: falseTestUser.id,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo`)
|
||||
.send(body)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it("should update a ooo entry without redirect", async () => {
|
||||
const body = {
|
||||
start: "2025-06-01T01:00:00.000Z",
|
||||
end: "2025-06-10T13:59:59.999Z",
|
||||
notes: "ooo numero uno",
|
||||
reason: "vacation",
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.patch(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo/${oooCreatedViaApiId}`)
|
||||
.send(body)
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
const responseBody = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
|
||||
const data = responseBody.data as UserOooOutputDto;
|
||||
expect(data.reason).toEqual("vacation");
|
||||
expect(data.userId).toEqual(teammate1.id);
|
||||
expect(data.start).toEqual("2025-06-01T00:00:00.000Z");
|
||||
expect(data.end).toEqual("2025-06-10T23:59:59.999Z");
|
||||
});
|
||||
});
|
||||
|
||||
it("should fail to update a ooo entry with redirect outside of org", async () => {
|
||||
const body = {
|
||||
toUserId: falseTestUser.id,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.patch(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo/${oooCreatedViaApiId}`)
|
||||
.send(body)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it("should fail to update a ooo entry with start after end ", async () => {
|
||||
const body = {
|
||||
start: "2025-07-01T00:00:00.000Z",
|
||||
end: "2025-05-10T23:59:59.999Z",
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.patch(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo/${oooCreatedViaApiId}`)
|
||||
.send(body)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it("should fail to update a ooo entry with redirect to self ", async () => {
|
||||
const body = {
|
||||
toUserId: teammate1.id,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.patch(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo/${oooCreatedViaApiId}`)
|
||||
.send(body)
|
||||
.expect(400);
|
||||
});
|
||||
|
||||
it("should fail to update a ooo entry with duplicate time", async () => {
|
||||
const body = {
|
||||
start: "2025-06-01T01:00:00.000Z",
|
||||
end: "2025-06-10T13:59:59.999Z",
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.patch(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo/${oooCreatedViaApiId}`)
|
||||
.send(body)
|
||||
.expect(409);
|
||||
});
|
||||
|
||||
it("should update a ooo entry without redirect", async () => {
|
||||
const body = {
|
||||
toUserId: teammate2.id,
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.patch(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo/${oooCreatedViaApiId}`)
|
||||
.send(body)
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
const responseBody = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
|
||||
const data = responseBody.data as UserOooOutputDto;
|
||||
expect(data.reason).toEqual("vacation");
|
||||
expect(data.toUserId).toEqual(teammate2.id);
|
||||
expect(data.userId).toEqual(teammate1.id);
|
||||
expect(data.start).toEqual("2025-06-01T00:00:00.000Z");
|
||||
expect(data.end).toEqual("2025-06-10T23:59:59.999Z");
|
||||
});
|
||||
});
|
||||
|
||||
it("should get 2 ooo entries", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo`)
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
const responseBody = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
|
||||
const data = responseBody.data as UserOooOutputDto[];
|
||||
expect(data.length).toEqual(2);
|
||||
const oooUno = data.find((ooo) => ooo.id === oooCreatedViaApiId);
|
||||
expect(oooUno).toBeDefined();
|
||||
if (oooUno) {
|
||||
expect(oooUno.reason).toEqual("vacation");
|
||||
expect(oooUno.toUserId).toEqual(teammate2.id);
|
||||
expect(oooUno.userId).toEqual(teammate1.id);
|
||||
expect(oooUno.start).toEqual("2025-06-01T00:00:00.000Z");
|
||||
expect(oooUno.end).toEqual("2025-06-10T23:59:59.999Z");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
it("should delete ooo entry", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.delete(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo/${oooCreatedViaApiId}`)
|
||||
.expect(200);
|
||||
});
|
||||
|
||||
it("user should have 1 ooo entries", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/organizations/${org.id}/users/${teammate1.id}/ooo`)
|
||||
.expect(200)
|
||||
.then((response) => {
|
||||
const responseBody = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
const data = responseBody.data as UserOooOutputDto[];
|
||||
expect(data.length).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await userRepositoryFixture.deleteByEmail(userAdmin.email);
|
||||
await userRepositoryFixture.deleteByEmail(teammate1.email);
|
||||
await userRepositoryFixture.deleteByEmail(teammate2.email);
|
||||
await userRepositoryFixture.deleteByEmail(falseTestUser.email);
|
||||
await teamsRepositoryFixture.delete(team.id);
|
||||
await teamsRepositoryFixture.delete(falseTestTeam.id);
|
||||
await organizationsRepositoryFixture.delete(org.id);
|
||||
await organizationsRepositoryFixture.delete(falseTestOrg.id);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -3,6 +3,8 @@ import { SchedulesModule_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/
|
||||
import { EmailModule } from "@/modules/email/email.module";
|
||||
import { EmailService } from "@/modules/email/email.service";
|
||||
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||
import { UserOOORepository } from "@/modules/ooo/repositories/ooo.repository";
|
||||
import { UserOOOService } from "@/modules/ooo/services/ooo.service";
|
||||
import { OrganizationsOptionsAttributesController } from "@/modules/organizations/controllers/attributes/organizations-attributes-options.controller";
|
||||
import { OrganizationsAttributesController } from "@/modules/organizations/controllers/attributes/organizations-attributes.controller";
|
||||
import { OrganizationsEventTypesController } from "@/modules/organizations/controllers/event-types/organizations-event-types.controller";
|
||||
@@ -12,6 +14,7 @@ import { OrganizationsSchedulesController } from "@/modules/organizations/contro
|
||||
import { OrganizationsTeamsMembershipsController } from "@/modules/organizations/controllers/teams/memberships/organizations-teams-memberships.controller";
|
||||
import { OrganizationsTeamsController } from "@/modules/organizations/controllers/teams/organizations-teams.controller";
|
||||
import { OrganizationsTeamsSchedulesController } from "@/modules/organizations/controllers/teams/schedules/organizations-teams-schedules.controller";
|
||||
import { OrganizationsUsersOOOController } from "@/modules/organizations/controllers/users/ooo/organizations-users-ooo-controller";
|
||||
import { OrganizationsUsersController } from "@/modules/organizations/controllers/users/organizations-users.controller";
|
||||
import { OrganizationsWebhooksController } from "@/modules/organizations/controllers/webhooks/organizations-webhooks.controller";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
|
||||
@@ -86,6 +89,8 @@ import { Module } from "@nestjs/common";
|
||||
WebhooksRepository,
|
||||
WebhooksService,
|
||||
OutputTeamEventTypesResponsePipe,
|
||||
UserOOOService,
|
||||
UserOOORepository,
|
||||
],
|
||||
exports: [
|
||||
OrganizationsService,
|
||||
@@ -118,6 +123,7 @@ import { Module } from "@nestjs/common";
|
||||
OrganizationsOptionsAttributesController,
|
||||
OrganizationsWebhooksController,
|
||||
OrganizationsTeamsSchedulesController,
|
||||
OrganizationsUsersOOOController,
|
||||
],
|
||||
})
|
||||
export class OrganizationsModule {}
|
||||
|
||||
@@ -265,4 +265,27 @@ export class UsersRepository {
|
||||
where: { id: userId },
|
||||
});
|
||||
}
|
||||
|
||||
async findUserOOORedirectEligible(userId: number, toTeamUserId: number) {
|
||||
return await this.dbRead.prisma.user.findUnique({
|
||||
where: {
|
||||
id: toTeamUserId,
|
||||
teams: {
|
||||
some: {
|
||||
team: {
|
||||
members: {
|
||||
some: {
|
||||
userId: userId,
|
||||
accepted: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2684,6 +2684,146 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/organizations/{orgId}/users/{userId}/ooo": {
|
||||
"get": {
|
||||
"operationId": "OrganizationsUsersOOOController_getOrganizationUserOOO",
|
||||
"summary": "Get all ooo entries of a user",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "userId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "take",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "The number of items to return",
|
||||
"example": 10,
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "skip",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "The number of items to skip",
|
||||
"example": 0,
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Orgs / Users / OOO"
|
||||
]
|
||||
},
|
||||
"post": {
|
||||
"operationId": "OrganizationsUsersOOOController_createOrganizationUserOOO",
|
||||
"summary": "Create an ooo entry for user",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "userId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreateOutOfOfficeEntryDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Orgs / Users / OOO"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/organizations/{orgId}/users/{userId}/ooo/{oooId}": {
|
||||
"patch": {
|
||||
"operationId": "OrganizationsUsersOOOController_updateOrganizationUserOOO",
|
||||
"summary": "Update ooo entry of a user",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "userId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "oooId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UpdateOutOfOfficeEntryDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Orgs / Users / OOO"
|
||||
]
|
||||
},
|
||||
"delete": {
|
||||
"operationId": "OrganizationsUsersOOOController_deleteOrganizationUserOOO",
|
||||
"summary": "Delete ooo entry of a user",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "oooId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Orgs / Users / OOO"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/organizations/{orgId}/webhooks": {
|
||||
"get": {
|
||||
"operationId": "OrganizationsWebhooksController_getAllOrganizationWebhooks",
|
||||
@@ -13454,6 +13594,74 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"CreateOutOfOfficeEntryDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"start": {
|
||||
"format": "date-time",
|
||||
"type": "string",
|
||||
"description": "The start date and time of the out of office period in ISO 8601 format in UTC timezone.",
|
||||
"example": "2023-05-01T00:00:00.000Z"
|
||||
},
|
||||
"end": {
|
||||
"format": "date-time",
|
||||
"type": "string",
|
||||
"description": "The end date and time of the out of office period in ISO 8601 format in UTC timezone.",
|
||||
"example": "2023-05-10T23:59:59.999Z"
|
||||
},
|
||||
"notes": {
|
||||
"type": "string",
|
||||
"description": "Optional notes for the out of office entry.",
|
||||
"example": "Vacation in Hawaii"
|
||||
},
|
||||
"toUserId": {
|
||||
"type": "number",
|
||||
"description": "The ID of the user covering for the out of office period, if applicable.",
|
||||
"example": 2
|
||||
},
|
||||
"reason": {
|
||||
"type": "object",
|
||||
"description": "the reason for the out of office entry, if applicable",
|
||||
"example": "vacation"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"start",
|
||||
"end"
|
||||
]
|
||||
},
|
||||
"UpdateOutOfOfficeEntryDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"start": {
|
||||
"format": "date-time",
|
||||
"type": "string",
|
||||
"description": "The start date and time of the out of office period in ISO 8601 format in UTC timezone.",
|
||||
"example": "2023-05-01T00:00:00.000Z"
|
||||
},
|
||||
"end": {
|
||||
"format": "date-time",
|
||||
"type": "string",
|
||||
"description": "The end date and time of the out of office period in ISO 8601 format in UTC timezone.",
|
||||
"example": "2023-05-10T23:59:59.999Z"
|
||||
},
|
||||
"notes": {
|
||||
"type": "string",
|
||||
"description": "Optional notes for the out of office entry.",
|
||||
"example": "Vacation in Hawaii"
|
||||
},
|
||||
"toUserId": {
|
||||
"type": "number",
|
||||
"description": "The ID of the user covering for the out of office period, if applicable.",
|
||||
"example": 2
|
||||
},
|
||||
"reason": {
|
||||
"type": "object",
|
||||
"description": "the reason for the out of office entry, if applicable",
|
||||
"example": "vacation"
|
||||
}
|
||||
}
|
||||
},
|
||||
"StripConnectOutputDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -2549,6 +2549,138 @@
|
||||
"tags": ["Orgs / Users"]
|
||||
}
|
||||
},
|
||||
"/v2/organizations/{orgId}/users/{userId}/ooo": {
|
||||
"get": {
|
||||
"operationId": "OrganizationsUsersOOOController_getOrganizationUserOOO",
|
||||
"summary": "Get all ooo entries of a user",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "userId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "take",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "The number of items to return",
|
||||
"example": 10,
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "skip",
|
||||
"required": false,
|
||||
"in": "query",
|
||||
"description": "The number of items to skip",
|
||||
"example": 0,
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"tags": ["Orgs / Users / OOO"]
|
||||
},
|
||||
"post": {
|
||||
"operationId": "OrganizationsUsersOOOController_createOrganizationUserOOO",
|
||||
"summary": "Create an ooo entry for user",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "userId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreateOutOfOfficeEntryDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"tags": ["Orgs / Users / OOO"]
|
||||
}
|
||||
},
|
||||
"/v2/organizations/{orgId}/users/{userId}/ooo/{oooId}": {
|
||||
"patch": {
|
||||
"operationId": "OrganizationsUsersOOOController_updateOrganizationUserOOO",
|
||||
"summary": "Update ooo entry of a user",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "userId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "oooId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UpdateOutOfOfficeEntryDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"tags": ["Orgs / Users / OOO"]
|
||||
},
|
||||
"delete": {
|
||||
"operationId": "OrganizationsUsersOOOController_deleteOrganizationUserOOO",
|
||||
"summary": "Delete ooo entry of a user",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "oooId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": ""
|
||||
}
|
||||
},
|
||||
"tags": ["Orgs / Users / OOO"]
|
||||
}
|
||||
},
|
||||
"/v2/organizations/{orgId}/webhooks": {
|
||||
"get": {
|
||||
"operationId": "OrganizationsWebhooksController_getAllOrganizationWebhooks",
|
||||
@@ -12157,6 +12289,71 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"CreateOutOfOfficeEntryDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"start": {
|
||||
"format": "date-time",
|
||||
"type": "string",
|
||||
"description": "The start date and time of the out of office period in ISO 8601 format in UTC timezone.",
|
||||
"example": "2023-05-01T00:00:00.000Z"
|
||||
},
|
||||
"end": {
|
||||
"format": "date-time",
|
||||
"type": "string",
|
||||
"description": "The end date and time of the out of office period in ISO 8601 format in UTC timezone.",
|
||||
"example": "2023-05-10T23:59:59.999Z"
|
||||
},
|
||||
"notes": {
|
||||
"type": "string",
|
||||
"description": "Optional notes for the out of office entry.",
|
||||
"example": "Vacation in Hawaii"
|
||||
},
|
||||
"toUserId": {
|
||||
"type": "number",
|
||||
"description": "The ID of the user covering for the out of office period, if applicable.",
|
||||
"example": 2
|
||||
},
|
||||
"reason": {
|
||||
"type": "object",
|
||||
"description": "the reason for the out of office entry, if applicable",
|
||||
"example": "vacation"
|
||||
}
|
||||
},
|
||||
"required": ["start", "end"]
|
||||
},
|
||||
"UpdateOutOfOfficeEntryDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"start": {
|
||||
"format": "date-time",
|
||||
"type": "string",
|
||||
"description": "The start date and time of the out of office period in ISO 8601 format in UTC timezone.",
|
||||
"example": "2023-05-01T00:00:00.000Z"
|
||||
},
|
||||
"end": {
|
||||
"format": "date-time",
|
||||
"type": "string",
|
||||
"description": "The end date and time of the out of office period in ISO 8601 format in UTC timezone.",
|
||||
"example": "2023-05-10T23:59:59.999Z"
|
||||
},
|
||||
"notes": {
|
||||
"type": "string",
|
||||
"description": "Optional notes for the out of office entry.",
|
||||
"example": "Vacation in Hawaii"
|
||||
},
|
||||
"toUserId": {
|
||||
"type": "number",
|
||||
"description": "The ID of the user covering for the out of office period, if applicable.",
|
||||
"example": 2
|
||||
},
|
||||
"reason": {
|
||||
"type": "object",
|
||||
"description": "the reason for the out of office entry, if applicable",
|
||||
"example": "vacation"
|
||||
}
|
||||
}
|
||||
},
|
||||
"StripConnectOutputDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
Reference in New Issue
Block a user