feat: v2 teams CRUD (#18019)
* feat: teams endpoints * refactor: organization teams reuse team types * refactor: move team specific functions out of organizations-teams.repository * supporting files * regenerate docs * feat: stripe service to generate team creation checkout * refactor: request payment when creating a team * fix: tests * fix env * driveby: event type name and email booking fields * finish merge main
This commit is contained in:
@@ -30,6 +30,8 @@ const loadConfig = (): AppConfig => {
|
||||
stripe: {
|
||||
apiKey: getEnv("STRIPE_API_KEY"),
|
||||
webhookSecret: getEnv("STRIPE_WEBHOOK_SECRET"),
|
||||
teamMonthlyPriceId: getEnv("STRIPE_TEAM_MONTHLY_PRICE_ID", "set-team-monthly-price-in-your-env"),
|
||||
isTeamBillingEnabled: getEnv("IS_TEAM_BILLING_ENABLED", true),
|
||||
},
|
||||
app: {
|
||||
baseUrl: getEnv("WEB_APP_URL", "https://app.cal.com"),
|
||||
|
||||
@@ -21,6 +21,8 @@ export type AppConfig = {
|
||||
stripe: {
|
||||
apiKey: string;
|
||||
webhookSecret: string;
|
||||
teamMonthlyPriceId: string;
|
||||
isTeamBillingEnabled: boolean;
|
||||
};
|
||||
app: {
|
||||
baseUrl: string;
|
||||
|
||||
@@ -9,6 +9,7 @@ import { ProviderModule } from "@/ee/provider/provider.module";
|
||||
import { SchedulesModule_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/schedules.module";
|
||||
import { SchedulesModule_2024_06_11 } from "@/ee/schedules/schedules_2024_06_11/schedules.module";
|
||||
import { SlotsModule } from "@/modules/slots/slots.module";
|
||||
import { TeamsModule } from "@/modules/teams/teams/teams.module";
|
||||
import { TeamsMembershipsModule } from "@/modules/teams/memberships/teams-memberships.module";
|
||||
import type { MiddlewareConsumer, NestModule } from "@nestjs/common";
|
||||
import { Module } from "@nestjs/common";
|
||||
@@ -27,6 +28,7 @@ import { Module } from "@nestjs/common";
|
||||
BookingsModule_2024_08_13,
|
||||
TeamsMembershipsModule,
|
||||
SlotsModule,
|
||||
TeamsModule,
|
||||
],
|
||||
})
|
||||
export class PlatformEndpointsModule implements NestModule {
|
||||
|
||||
@@ -30,12 +30,14 @@ export type Environment = {
|
||||
RATE_LIMIT_DEFAULT_BLOCK_DURATION_MS: number;
|
||||
AXIOM_DATASET: string;
|
||||
AXIOM_TOKEN: string;
|
||||
STRIPE_TEAM_MONTHLY_PRICE_ID: string;
|
||||
IS_TEAM_BILLING_ENABLED: boolean;
|
||||
};
|
||||
|
||||
export const getEnv = <K extends keyof Environment>(key: K, fallback?: Environment[K]): Environment[K] => {
|
||||
const value = process.env[key] as Environment[K] | undefined;
|
||||
|
||||
if (!value) {
|
||||
if (value === undefined) {
|
||||
if (fallback) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { credentialForCalendarServiceSelect } from "@calcom/platform-libraries";
|
||||
import { paymentDataSelect } from "@calcom/platform-libraries";
|
||||
|
||||
@Injectable()
|
||||
@@ -14,4 +15,53 @@ export class AtomsRepository {
|
||||
select: paymentDataSelect,
|
||||
});
|
||||
}
|
||||
|
||||
async getUserTeams(userId: number) {
|
||||
const userTeams = await this.dbRead.prisma.team.findMany({
|
||||
where: {
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
accepted: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
credentials: {
|
||||
select: credentialForCalendarServiceSelect,
|
||||
},
|
||||
name: true,
|
||||
logoUrl: true,
|
||||
members: {
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
select: {
|
||||
role: true,
|
||||
},
|
||||
},
|
||||
parent: {
|
||||
select: {
|
||||
id: true,
|
||||
credentials: {
|
||||
select: credentialForCalendarServiceSelect,
|
||||
},
|
||||
name: true,
|
||||
logoUrl: true,
|
||||
members: {
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
select: {
|
||||
role: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return userTeams;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@ import { EventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_
|
||||
import { AtomsRepository } from "@/modules/atoms/atoms.repository";
|
||||
import { CredentialsRepository } from "@/modules/credentials/credentials.repository";
|
||||
import { MembershipsRepository } from "@/modules/memberships/memberships.repository";
|
||||
import { OrganizationsTeamsRepository } from "@/modules/organizations/repositories/organizations-teams.repository";
|
||||
import { OrganizationsEventTypesService } from "@/modules/organizations/services/event-types/organizations-event-types.service";
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { TeamsRepository } from "@/modules/teams/teams/teams.repository";
|
||||
import { UsersService } from "@/modules/users/services/users.service";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { Injectable, NotFoundException, ForbiddenException } from "@nestjs/common";
|
||||
@@ -46,7 +46,6 @@ export class EventTypesAtomService {
|
||||
private readonly membershipsRepository: MembershipsRepository,
|
||||
private readonly credentialsRepository: CredentialsRepository,
|
||||
private readonly atomsRepository: AtomsRepository,
|
||||
private readonly organizationsTeamsRepository: OrganizationsTeamsRepository,
|
||||
private readonly usersService: UsersService,
|
||||
private readonly dbWrite: PrismaWriteService,
|
||||
private readonly dbRead: PrismaReadService,
|
||||
@@ -183,7 +182,7 @@ export class EventTypesAtomService {
|
||||
let credentials = await this.credentialsRepository.getAllUserCredentialsById(userId);
|
||||
let userTeams: TeamQuery[] = [];
|
||||
if (teamId) {
|
||||
const teamsQuery = await this.organizationsTeamsRepository.getUserTeamsById(userId);
|
||||
const teamsQuery = await this.atomsRepository.getUserTeams(userId);
|
||||
// If a team is a part of an org then include those apps
|
||||
// Don't want to iterate over these parent teams
|
||||
const filteredTeams: TeamQuery[] = [];
|
||||
|
||||
@@ -33,6 +33,16 @@ export class MembershipsRepository {
|
||||
return membership;
|
||||
}
|
||||
|
||||
async findUserMemberships(userId: number) {
|
||||
const memberships = await this.dbRead.prisma.membership.findMany({
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
});
|
||||
|
||||
return memberships;
|
||||
}
|
||||
|
||||
async findMembershipByOrgId(orgId: number, userId: number) {
|
||||
return this.findMembershipByTeamId(orgId, userId);
|
||||
}
|
||||
|
||||
@@ -1,118 +1,3 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { IsBoolean, IsOptional, IsString, IsUrl, Length } from "class-validator";
|
||||
import { CreateTeamInput } from "@/modules/teams/teams/inputs/create-team.input";
|
||||
|
||||
export class CreateOrgTeamDto {
|
||||
@IsString()
|
||||
@Length(1)
|
||||
@ApiProperty({ description: "Name of the team", example: "CalTeam", required: true })
|
||||
readonly name!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({ type: String, description: "Team slug", example: "caltel" })
|
||||
readonly slug?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
@ApiPropertyOptional({
|
||||
type: String,
|
||||
example: "https://i.cal.com/api/avatar/b0b58752-68ad-4c0d-8024-4fa382a77752.png",
|
||||
description: `URL of the teams logo image`,
|
||||
})
|
||||
readonly logoUrl?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
@ApiPropertyOptional()
|
||||
readonly calVideoLogo?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
@ApiPropertyOptional()
|
||||
readonly appLogo?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
@ApiPropertyOptional()
|
||||
readonly appIconLogo?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
readonly bio?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional({ type: Boolean, default: false })
|
||||
readonly hideBranding?: boolean = false;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional()
|
||||
readonly isPrivate?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional()
|
||||
readonly hideBookATeamMember?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
readonly metadata?: string; // Assuming metadata is a JSON string. Adjust accordingly if it's a nested object.
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
readonly theme?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
readonly brandColor?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
readonly darkBrandColor?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
@ApiPropertyOptional({
|
||||
type: String,
|
||||
example: "https://i.cal.com/api/avatar/949be534-7a88-4185-967c-c020b0c0bef3.png",
|
||||
description: `URL of the teams banner image which is shown on booker`,
|
||||
required: false,
|
||||
})
|
||||
readonly bannerUrl?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
readonly timeFormat?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({
|
||||
type: String,
|
||||
example: "America/New_York",
|
||||
description: `Timezone is used to create teams's default schedule from Monday to Friday from 9AM to 5PM. It will default to Europe/London if not passed.`,
|
||||
required: false,
|
||||
default: "Europe/London",
|
||||
})
|
||||
readonly timeZone?: string = "Europe/London";
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({
|
||||
type: String,
|
||||
example: "Monday",
|
||||
default: "Sunday",
|
||||
})
|
||||
readonly weekStart?: string = "Sunday";
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional({ type: Boolean, default: true })
|
||||
readonly autoAcceptCreator?: boolean = true;
|
||||
}
|
||||
export class CreateOrgTeamDto extends CreateTeamInput {}
|
||||
@@ -1,119 +1,3 @@
|
||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { IsBoolean, IsOptional, IsString, IsUrl, Length } from "class-validator";
|
||||
import { UpdateTeamDto } from "@/modules/teams/teams/inputs/update-team.input";
|
||||
|
||||
export class UpdateOrgTeamDto {
|
||||
@IsString()
|
||||
@Length(1)
|
||||
@ApiPropertyOptional({ description: "Name of the team", example: "CalTeam" })
|
||||
readonly name?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({ type: String, description: "Team slug", example: "caltel" })
|
||||
readonly slug?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
@ApiPropertyOptional({
|
||||
type: String,
|
||||
example: "https://i.cal.com/api/avatar/b0b58752-68ad-4c0d-8024-4fa382a77752.png",
|
||||
description: `URL of the teams logo image`,
|
||||
})
|
||||
readonly logoUrl?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
@ApiPropertyOptional()
|
||||
readonly calVideoLogo?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
@ApiPropertyOptional()
|
||||
readonly appLogo?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
@ApiPropertyOptional()
|
||||
readonly appIconLogo?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
readonly bio?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional()
|
||||
readonly hideBranding?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional()
|
||||
readonly isPrivate?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional()
|
||||
readonly hideBookATeamMember?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
readonly metadata?: string; // Assuming metadata is a JSON string. Adjust accordingly if it's a nested object.
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
readonly theme?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
readonly brandColor?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
readonly darkBrandColor?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
@ApiPropertyOptional({
|
||||
type: String,
|
||||
example: "https://i.cal.com/api/avatar/949be534-7a88-4185-967c-c020b0c0bef3.png",
|
||||
description: `URL of the teams banner image which is shown on booker`,
|
||||
})
|
||||
readonly bannerUrl?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
readonly timeFormat?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({
|
||||
type: String,
|
||||
example: "America/New_York",
|
||||
description: `Timezone is used to create teams's default schedule from Monday to Friday from 9AM to 5PM. It will default to Europe/London if not passed.`,
|
||||
})
|
||||
readonly timeZone?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({
|
||||
type: String,
|
||||
example: "Monday",
|
||||
})
|
||||
readonly weekStart?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
readonly bookingLimits?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional()
|
||||
readonly includeManagedEventsInLimits?: boolean;
|
||||
}
|
||||
export class UpdateOrgTeamDto extends UpdateTeamDto {}
|
||||
@@ -39,6 +39,7 @@ import { OrganizationsService } from "@/modules/organizations/services/organizat
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { RedisModule } from "@/modules/redis/redis.module";
|
||||
import { StripeModule } from "@/modules/stripe/stripe.module";
|
||||
import { TeamsModule } from "@/modules/teams/teams/teams.module";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
import { WebhooksService } from "@/modules/webhooks/services/webhooks.service";
|
||||
import { WebhooksRepository } from "@/modules/webhooks/webhooks.repository";
|
||||
@@ -53,6 +54,7 @@ import { Module } from "@nestjs/common";
|
||||
RedisModule,
|
||||
EmailModule,
|
||||
EventTypesModule_2024_06_14,
|
||||
TeamsModule,
|
||||
],
|
||||
providers: [
|
||||
OrganizationsRepository,
|
||||
|
||||
@@ -4,8 +4,6 @@ import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { credentialForCalendarServiceSelect } from "@calcom/platform-libraries";
|
||||
|
||||
@Injectable()
|
||||
export class OrganizationsTeamsRepository {
|
||||
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||
@@ -20,14 +18,6 @@ export class OrganizationsTeamsRepository {
|
||||
});
|
||||
}
|
||||
|
||||
async findTeamById(teamId: number) {
|
||||
return this.dbRead.prisma.team.findUnique({
|
||||
where: {
|
||||
id: teamId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findOrgTeams(organizationId: number) {
|
||||
return this.dbRead.prisma.team.findMany({
|
||||
where: {
|
||||
@@ -105,69 +95,4 @@ export class OrganizationsTeamsRepository {
|
||||
take,
|
||||
});
|
||||
}
|
||||
|
||||
async getTeamMembersIds(teamId: number) {
|
||||
const team = await this.dbRead.prisma.team.findUnique({
|
||||
where: {
|
||||
id: teamId,
|
||||
},
|
||||
include: {
|
||||
members: true,
|
||||
},
|
||||
});
|
||||
if (!team) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return team.members.map((member) => member.userId);
|
||||
}
|
||||
|
||||
async getUserTeamsById(userId: number) {
|
||||
const userTeams = await this.dbRead.prisma.team.findMany({
|
||||
where: {
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
accepted: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
credentials: {
|
||||
select: credentialForCalendarServiceSelect,
|
||||
},
|
||||
name: true,
|
||||
logoUrl: true,
|
||||
members: {
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
select: {
|
||||
role: true,
|
||||
},
|
||||
},
|
||||
parent: {
|
||||
select: {
|
||||
id: true,
|
||||
credentials: {
|
||||
select: credentialForCalendarServiceSelect,
|
||||
},
|
||||
name: true,
|
||||
logoUrl: true,
|
||||
members: {
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
select: {
|
||||
role: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return userTeams;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { InputEventTypesService_2024_06_14 } from "@/ee/event-types/event-types_2024_06_14/services/input-event-types.service";
|
||||
import { OrganizationsEventTypesRepository } from "@/modules/organizations/repositories/organizations-event-types.repository";
|
||||
import { OrganizationsTeamsRepository } from "@/modules/organizations/repositories/organizations-teams.repository";
|
||||
import { TeamsRepository } from "@/modules/teams/teams/teams.repository";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { BadRequestException, Injectable, NotFoundException } from "@nestjs/common";
|
||||
|
||||
@@ -15,7 +15,7 @@ import { SchedulingType } from "@calcom/prisma/client";
|
||||
export class InputOrganizationsEventTypesService {
|
||||
constructor(
|
||||
private readonly inputEventTypesService: InputEventTypesService_2024_06_14,
|
||||
private readonly organizationsTeamsRepository: OrganizationsTeamsRepository,
|
||||
private readonly teamsRepository: TeamsRepository,
|
||||
private readonly usersRepository: UsersRepository,
|
||||
private readonly orgEventTypesRepository: OrganizationsEventTypesRepository
|
||||
) {}
|
||||
@@ -167,7 +167,7 @@ export class InputOrganizationsEventTypesService {
|
||||
eventType: { children: { userId: number | null }[] } | null
|
||||
) {
|
||||
if (inputEventType.assignAllTeamMembers) {
|
||||
return await this.organizationsTeamsRepository.getTeamMembersIds(teamId);
|
||||
return await this.teamsRepository.getTeamMembersIds(teamId);
|
||||
}
|
||||
|
||||
// note(Lauris): when API user updates managed event type users
|
||||
@@ -195,7 +195,7 @@ export class InputOrganizationsEventTypesService {
|
||||
}
|
||||
|
||||
async getAllTeamMembers(teamId: number, schedulingType: SchedulingType | null) {
|
||||
const membersIds = await this.organizationsTeamsRepository.getTeamMembersIds(teamId);
|
||||
const membersIds = await this.teamsRepository.getTeamMembersIds(teamId);
|
||||
const isFixed = schedulingType === "COLLECTIVE" ? true : false;
|
||||
|
||||
return membersIds.map((id) => ({
|
||||
@@ -227,7 +227,7 @@ export class InputOrganizationsEventTypesService {
|
||||
|
||||
async validateHosts(teamId: number, hosts: CreateTeamEventTypeInput_2024_06_14["hosts"] | undefined) {
|
||||
if (hosts && hosts.length) {
|
||||
const membersIds = await this.organizationsTeamsRepository.getTeamMembersIds(teamId);
|
||||
const membersIds = await this.teamsRepository.getTeamMembersIds(teamId);
|
||||
const invalidHosts = hosts.filter((host) => !membersIds.includes(host.userId));
|
||||
if (invalidHosts.length) {
|
||||
throw new NotFoundException(`Invalid hosts: ${invalidHosts.join(", ")}`);
|
||||
|
||||
@@ -5,11 +5,12 @@ import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { StripeController } from "@/modules/stripe/controllers/stripe.controller";
|
||||
import { StripeService } from "@/modules/stripe/stripe.service";
|
||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
import { Module } from "@nestjs/common";
|
||||
import { ConfigModule } from "@nestjs/config";
|
||||
|
||||
@Module({
|
||||
imports: [ConfigModule, PrismaModule],
|
||||
imports: [ConfigModule, PrismaModule, UsersModule],
|
||||
exports: [StripeService],
|
||||
providers: [StripeService, AppsRepository, CredentialsRepository, TokensRepository, MembershipsRepository],
|
||||
controllers: [StripeController],
|
||||
|
||||
@@ -6,9 +6,16 @@ import { getReturnToValueFromQueryState } from "@/modules/stripe/utils/getReturn
|
||||
import { stripeInstance } from "@/modules/stripe/utils/newStripeInstance";
|
||||
import { StripeData } from "@/modules/stripe/utils/stripeDataSchemas";
|
||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
import { Injectable, NotFoundException, BadRequestException, UnauthorizedException } from "@nestjs/common";
|
||||
import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
BadRequestException,
|
||||
UnauthorizedException,
|
||||
InternalServerErrorException,
|
||||
} from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import type { Prisma, Credential } from "@prisma/client";
|
||||
import type { Prisma, Credential, User } from "@prisma/client";
|
||||
import Stripe from "stripe";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -30,6 +37,9 @@ type IntegrationOAuthCallbackState = {
|
||||
export class StripeService {
|
||||
private stripe: Stripe;
|
||||
private redirectUri = `${this.config.get("api.url")}/stripe/save`;
|
||||
private webAppUrl = this.config.get("app.baseUrl");
|
||||
private environment = this.config.get("env.type");
|
||||
private teamMonthlyPriceId = this.config.get("stripe.teamMonthlyPriceId");
|
||||
|
||||
constructor(
|
||||
configService: ConfigService<AppConfig>,
|
||||
@@ -37,7 +47,8 @@ export class StripeService {
|
||||
private readonly appsRepository: AppsRepository,
|
||||
private readonly credentialRepository: CredentialsRepository,
|
||||
private readonly tokensRepository: TokensRepository,
|
||||
private readonly membershipRepository: MembershipsRepository
|
||||
private readonly membershipRepository: MembershipsRepository,
|
||||
private readonly usersRepository: UsersRepository
|
||||
) {
|
||||
this.stripe = new Stripe(configService.get("stripe.apiKey", { infer: true }) ?? "", {
|
||||
apiVersion: "2020-08-27",
|
||||
@@ -176,4 +187,94 @@ export class StripeService {
|
||||
status: SUCCESS_STATUS,
|
||||
};
|
||||
}
|
||||
|
||||
async generateTeamCheckoutSession(pendingPaymentTeamId: number, ownerId: number) {
|
||||
const stripe = this.getStripe();
|
||||
const customer = await this.getStripeCustomerIdFromUserId(ownerId);
|
||||
|
||||
if (!customer) {
|
||||
throw new BadRequestException("Failed to create a customer on Stripe.");
|
||||
}
|
||||
|
||||
const session = await stripe.checkout.sessions.create({
|
||||
customer,
|
||||
mode: "subscription",
|
||||
allow_promotion_codes: true,
|
||||
success_url: `${this.webAppUrl}/api/teams/api/create?session_id={CHECKOUT_SESSION_ID}`,
|
||||
cancel_url: `${this.webAppUrl}/settings/my-account/profile`,
|
||||
line_items: [
|
||||
{
|
||||
/** We only need to set the base price and we can upsell it directly on Stripe's checkout */
|
||||
price: this.teamMonthlyPriceId,
|
||||
/**Initially it will be just the team owner */
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
customer_update: {
|
||||
address: "auto",
|
||||
},
|
||||
// Disabled when testing locally as usually developer doesn't setup Tax in Stripe Test mode
|
||||
automatic_tax: {
|
||||
enabled: this.environment === "production",
|
||||
},
|
||||
metadata: {
|
||||
pendingPaymentTeamId,
|
||||
ownerId,
|
||||
dubCustomerId: ownerId, // pass the userId during checkout creation for sales conversion tracking: https://d.to/conversions/stripe
|
||||
},
|
||||
});
|
||||
|
||||
if (!session.url) {
|
||||
throw new InternalServerErrorException({
|
||||
message: "Failed generating a Stripe checkout session URL.",
|
||||
});
|
||||
}
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
async getStripeCustomerIdFromUserId(userId: number) {
|
||||
const user = await this.usersRepository.findById(userId);
|
||||
|
||||
if (!user?.email) return null;
|
||||
const customerId = await this.getStripeCustomerId(user);
|
||||
if (!customerId) {
|
||||
return this.createStripeCustomerId(user);
|
||||
}
|
||||
|
||||
return customerId;
|
||||
}
|
||||
|
||||
async getStripeCustomerId(user: Pick<User, "email" | "name" | "metadata">) {
|
||||
if (user?.metadata && typeof user.metadata === "object" && "stripeCustomerId" in user.metadata) {
|
||||
return (user?.metadata as Prisma.JsonObject).stripeCustomerId as string;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async createStripeCustomerId(user: Pick<User, "email" | "name" | "metadata">) {
|
||||
let customerId: string;
|
||||
|
||||
const stripe = this.getStripe();
|
||||
try {
|
||||
const customersResponse = await stripe.customers.list({
|
||||
email: user.email,
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
customerId = customersResponse.data[0].id;
|
||||
} catch (error) {
|
||||
const customer = await stripe.customers.create({ email: user.email });
|
||||
customerId = customer.id;
|
||||
}
|
||||
|
||||
await this.usersRepository.updateByEmail(user.email, {
|
||||
metadata: {
|
||||
...(user.metadata as Prisma.JsonObject),
|
||||
stripeCustomerId: customerId,
|
||||
},
|
||||
});
|
||||
|
||||
return customerId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,18 @@ export class TeamsMembershipsRepository {
|
||||
include: { user: { select: { username: true, email: true, avatarUrl: true, name: true } } },
|
||||
});
|
||||
}
|
||||
|
||||
async findTeamMembershipsByNameAndUser(teamName: string, userId: number) {
|
||||
return this.dbRead.prisma.membership.findFirst({
|
||||
where: {
|
||||
team: {
|
||||
name: teamName,
|
||||
},
|
||||
userId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async deleteTeamMembershipById(teamId: number, membershipId: number) {
|
||||
return this.dbWrite.prisma.membership.delete({
|
||||
where: {
|
||||
|
||||
@@ -0,0 +1,230 @@
|
||||
import { bootstrap } from "@/app";
|
||||
import { AppModule } from "@/app.module";
|
||||
import { StripeService } from "@/modules/stripe/stripe.service";
|
||||
import { CreateTeamInput } from "@/modules/teams/teams/inputs/create-team.input";
|
||||
import { UpdateTeamDto } from "@/modules/teams/teams/inputs/update-team.input";
|
||||
import { CreateTeamOutput } from "@/modules/teams/teams/outputs/teams/create-team.output";
|
||||
import { GetTeamOutput } from "@/modules/teams/teams/outputs/teams/get-team.output";
|
||||
import { GetTeamsOutput } from "@/modules/teams/teams/outputs/teams/get-teams.output";
|
||||
import { TeamsModule } from "@/modules/teams/teams/teams.module";
|
||||
import { INestApplication } from "@nestjs/common";
|
||||
import { NestExpressApplication } from "@nestjs/platform-express";
|
||||
import { Test } from "@nestjs/testing";
|
||||
import { User } from "next-auth";
|
||||
import Stripe from "stripe";
|
||||
import * as request from "supertest";
|
||||
import { ApiKeysRepositoryFixture } from "test/fixtures/repository/api-keys.repository.fixture";
|
||||
import { MembershipRepositoryFixture } from "test/fixtures/repository/membership.repository.fixture";
|
||||
import { TeamRepositoryFixture } from "test/fixtures/repository/team.repository.fixture";
|
||||
import { UserRepositoryFixture } from "test/fixtures/repository/users.repository.fixture";
|
||||
import { randomNumber } from "test/utils/randomNumber";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { TeamOutputDto } from "@calcom/platform-types";
|
||||
|
||||
describe("Teams endpoint", () => {
|
||||
let app: INestApplication;
|
||||
let userRepositoryFixture: UserRepositoryFixture;
|
||||
let teamRepositoryFixture: TeamRepositoryFixture;
|
||||
let apiKeysRepositoryFixture: ApiKeysRepositoryFixture;
|
||||
let membershipRepositoryFixture: MembershipRepositoryFixture;
|
||||
|
||||
const aliceEmail = `alice-${randomNumber()}@api.com`;
|
||||
let alice: User;
|
||||
let aliceApiKey: string;
|
||||
|
||||
const bobEmail = `bob-${randomNumber()}@api.com`;
|
||||
let bob: User;
|
||||
let bobApiKey: string;
|
||||
|
||||
let team1: TeamOutputDto;
|
||||
let team2: TeamOutputDto;
|
||||
|
||||
beforeAll(async () => {
|
||||
const moduleRef = await Test.createTestingModule({
|
||||
imports: [AppModule, TeamsModule],
|
||||
}).compile();
|
||||
|
||||
jest.spyOn(StripeService.prototype, "getStripe").mockImplementation(() => ({} as unknown as Stripe));
|
||||
|
||||
userRepositoryFixture = new UserRepositoryFixture(moduleRef);
|
||||
teamRepositoryFixture = new TeamRepositoryFixture(moduleRef);
|
||||
apiKeysRepositoryFixture = new ApiKeysRepositoryFixture(moduleRef);
|
||||
membershipRepositoryFixture = new MembershipRepositoryFixture(moduleRef);
|
||||
|
||||
alice = await userRepositoryFixture.create({
|
||||
email: aliceEmail,
|
||||
});
|
||||
|
||||
bob = await userRepositoryFixture.create({
|
||||
email: bobEmail,
|
||||
});
|
||||
|
||||
const { keyString } = await apiKeysRepositoryFixture.createApiKey(alice.id, null);
|
||||
aliceApiKey = keyString;
|
||||
|
||||
const { keyString: bobKeyString } = await apiKeysRepositoryFixture.createApiKey(bob.id, null);
|
||||
bobApiKey = bobKeyString;
|
||||
|
||||
app = moduleRef.createNestApplication();
|
||||
bootstrap(app as NestExpressApplication);
|
||||
|
||||
await app.init();
|
||||
});
|
||||
|
||||
describe("User has membership in created team", () => {
|
||||
it("should create a team", async () => {
|
||||
const body: CreateTeamInput = {
|
||||
name: "Team dog",
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/v2/teams")
|
||||
.send(body)
|
||||
.set({ Authorization: `Bearer cal_test_${aliceApiKey}` })
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: CreateTeamOutput = response.body;
|
||||
const responseData = responseBody.data as TeamOutputDto;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseData).toBeDefined();
|
||||
expect(responseData.id).toBeDefined();
|
||||
expect(responseData.name).toEqual(body.name);
|
||||
team1 = responseData;
|
||||
});
|
||||
});
|
||||
|
||||
it("should create a team", async () => {
|
||||
const body: CreateTeamInput = {
|
||||
name: "Team cats",
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.post("/v2/teams")
|
||||
.send(body)
|
||||
.set({ Authorization: `Bearer cal_test_${aliceApiKey}` })
|
||||
.expect(201)
|
||||
.then(async (response) => {
|
||||
const responseBody: CreateTeamOutput = response.body;
|
||||
const responseData = responseBody.data as TeamOutputDto;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseData).toBeDefined();
|
||||
expect(responseData.id).toBeDefined();
|
||||
expect(responseData.name).toEqual(body.name);
|
||||
team2 = responseData;
|
||||
});
|
||||
});
|
||||
|
||||
it("should get a team", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/teams/${team1.id}`)
|
||||
.set({ Authorization: `Bearer cal_test_${aliceApiKey}` })
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetTeamOutput = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.id).toBeDefined();
|
||||
expect(responseBody.data.name).toEqual(team1.name);
|
||||
});
|
||||
});
|
||||
|
||||
it("should get teams", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/teams`)
|
||||
.set({ Authorization: `Bearer cal_test_${aliceApiKey}` })
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetTeamsOutput = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.length).toEqual(2);
|
||||
expect(responseBody.data[0].id).toBeDefined();
|
||||
expect(responseBody.data[0].name).toEqual(team1.name);
|
||||
expect(responseBody.data[1].id).toBeDefined();
|
||||
expect(responseBody.data[1].name).toEqual(team2.name);
|
||||
});
|
||||
});
|
||||
|
||||
it("should update a team", async () => {
|
||||
const body: UpdateTeamDto = {
|
||||
name: "Team dogs shepherds",
|
||||
};
|
||||
|
||||
return request(app.getHttpServer())
|
||||
.patch(`/v2/teams/${team1.id}`)
|
||||
.send(body)
|
||||
.set({ Authorization: `Bearer cal_test_${aliceApiKey}` })
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetTeamOutput = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.id).toBeDefined();
|
||||
expect(responseBody.data.name).toEqual(body.name);
|
||||
team1 = responseBody.data;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("User does not have membership in created team", () => {
|
||||
it("should not be able to get a team", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.get(`/v2/teams/${team2.id}`)
|
||||
.set({ Authorization: `Bearer cal_test_${bobApiKey}` })
|
||||
.expect(403);
|
||||
});
|
||||
});
|
||||
|
||||
describe("User does not have sufficient membership in created team", () => {
|
||||
it("should not be able to delete a team", async () => {
|
||||
await membershipRepositoryFixture.create({
|
||||
role: "MEMBER",
|
||||
user: { connect: { id: bob.id } },
|
||||
team: { connect: { id: team2.id } },
|
||||
accepted: true,
|
||||
});
|
||||
return request(app.getHttpServer())
|
||||
.delete(`/v2/teams/${team2.id}`)
|
||||
.set({ Authorization: `Bearer cal_test_${bobApiKey}` })
|
||||
.expect(403);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Delete teams", () => {
|
||||
it("should delete team", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.delete(`/v2/teams/${team1.id}`)
|
||||
.set({ Authorization: `Bearer cal_test_${aliceApiKey}` })
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetTeamOutput = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.id).toBeDefined();
|
||||
expect(responseBody.data.name).toEqual(team1.name);
|
||||
});
|
||||
});
|
||||
|
||||
it("should delete team", async () => {
|
||||
return request(app.getHttpServer())
|
||||
.delete(`/v2/teams/${team2.id}`)
|
||||
.set({ Authorization: `Bearer cal_test_${aliceApiKey}` })
|
||||
.expect(200)
|
||||
.then(async (response) => {
|
||||
const responseBody: GetTeamOutput = response.body;
|
||||
expect(responseBody.status).toEqual(SUCCESS_STATUS);
|
||||
expect(responseBody.data).toBeDefined();
|
||||
expect(responseBody.data.id).toBeDefined();
|
||||
expect(responseBody.data.name).toEqual(team2.name);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await userRepositoryFixture.deleteByEmail(aliceEmail);
|
||||
await teamRepositoryFixture.delete(team1.id);
|
||||
await teamRepositoryFixture.delete(team2.id);
|
||||
await app.close();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,105 @@
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { RolesGuard } from "@/modules/auth/guards/roles/roles.guard";
|
||||
import { UpdateOrgTeamDto } from "@/modules/organizations/inputs/update-organization-team.input";
|
||||
import { OrgTeamOutputResponseDto } from "@/modules/organizations/outputs/organization-team.output";
|
||||
import { CreateTeamInput } from "@/modules/teams/teams/inputs/create-team.input";
|
||||
import { CreateTeamOutput } from "@/modules/teams/teams/outputs/teams/create-team.output";
|
||||
import { GetTeamOutput } from "@/modules/teams/teams/outputs/teams/get-team.output";
|
||||
import { GetTeamsOutput } from "@/modules/teams/teams/outputs/teams/get-teams.output";
|
||||
import { UpdateTeamOutput } from "@/modules/teams/teams/outputs/teams/update-team.output";
|
||||
import { TeamsService } from "@/modules/teams/teams/services/teams.service";
|
||||
import { TeamsRepository } from "@/modules/teams/teams/teams.repository";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import { Controller, UseGuards, Get, Param, ParseIntPipe, Delete, Patch, Post, Body } from "@nestjs/common";
|
||||
import { ApiOperation, ApiTags as DocsTags } from "@nestjs/swagger";
|
||||
import { plainToClass } from "class-transformer";
|
||||
|
||||
import { SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { TeamOutputDto } from "@calcom/platform-types";
|
||||
|
||||
@Controller({
|
||||
path: "/v2/teams",
|
||||
version: API_VERSIONS_VALUES,
|
||||
})
|
||||
@UseGuards(ApiAuthGuard)
|
||||
@DocsTags("Teams")
|
||||
export class TeamsController {
|
||||
constructor(private teamsService: TeamsService, private teamsRepository: TeamsRepository) {}
|
||||
|
||||
@Post()
|
||||
@ApiOperation({ summary: "Create a team" })
|
||||
async createTeam(
|
||||
@Body() body: CreateTeamInput,
|
||||
@GetUser() user: UserWithProfile
|
||||
): Promise<CreateTeamOutput> {
|
||||
const team = await this.teamsService.createTeam(body, user.id);
|
||||
|
||||
if ("paymentLink" in team) {
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: {
|
||||
pendingTeam: plainToClass(TeamOutputDto, team.pendingTeam, { strategy: "excludeAll" }),
|
||||
paymentLink: team.paymentLink,
|
||||
message: team.message,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToClass(TeamOutputDto, team, { strategy: "excludeAll" }),
|
||||
};
|
||||
}
|
||||
|
||||
@Get("/:teamId")
|
||||
@ApiOperation({ summary: "Get a team" })
|
||||
@UseGuards(RolesGuard)
|
||||
@Roles("TEAM_MEMBER")
|
||||
async getTeam(@Param("teamId", ParseIntPipe) teamId: number): Promise<GetTeamOutput> {
|
||||
const team = await this.teamsRepository.getById(teamId);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToClass(TeamOutputDto, team, { strategy: "excludeAll" }),
|
||||
};
|
||||
}
|
||||
|
||||
@Get("/")
|
||||
@ApiOperation({ summary: "Get teams" })
|
||||
async getTeams(@GetUser("id") userId: number): Promise<GetTeamsOutput> {
|
||||
const teams = await this.teamsService.getUserTeams(userId);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: teams.map((team) => plainToClass(TeamOutputDto, team, { strategy: "excludeAll" })),
|
||||
};
|
||||
}
|
||||
|
||||
@Patch("/:teamId")
|
||||
@ApiOperation({ summary: "Update a team" })
|
||||
@UseGuards(RolesGuard)
|
||||
@Roles("TEAM_OWNER")
|
||||
async updateTeam(
|
||||
@Param("teamId", ParseIntPipe) teamId: number,
|
||||
@Body() body: UpdateOrgTeamDto
|
||||
): Promise<UpdateTeamOutput> {
|
||||
const team = await this.teamsService.updateTeam(teamId, body);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToClass(TeamOutputDto, team, { strategy: "excludeAll" }),
|
||||
};
|
||||
}
|
||||
|
||||
@UseGuards(RolesGuard)
|
||||
@Delete("/:teamId")
|
||||
@ApiOperation({ summary: "Delete a team" })
|
||||
@Roles("TEAM_OWNER")
|
||||
async deleteTeam(@Param("teamId", ParseIntPipe) teamId: number): Promise<OrgTeamOutputResponseDto> {
|
||||
const team = await this.teamsRepository.delete(teamId);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: plainToClass(TeamOutputDto, team, { strategy: "excludeAll" }),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { IsBoolean, IsOptional, IsString, IsUrl, Length } from "class-validator";
|
||||
|
||||
export class CreateTeamInput {
|
||||
@IsString()
|
||||
@Length(1)
|
||||
@ApiProperty({ description: "Name of the team", example: "CalTeam", required: true })
|
||||
readonly name!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({ type: String, description: "Team slug", example: "caltel" })
|
||||
readonly slug?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
@ApiPropertyOptional({
|
||||
type: String,
|
||||
example: "https://i.cal.com/api/avatar/b0b58752-68ad-4c0d-8024-4fa382a77752.png",
|
||||
description: `URL of the teams logo image`,
|
||||
})
|
||||
readonly logoUrl?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
@ApiPropertyOptional()
|
||||
readonly calVideoLogo?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
@ApiPropertyOptional()
|
||||
readonly appLogo?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
@ApiPropertyOptional()
|
||||
readonly appIconLogo?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
readonly bio?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional({ type: Boolean, default: false })
|
||||
readonly hideBranding?: boolean = false;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional()
|
||||
readonly isPrivate?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional()
|
||||
readonly hideBookATeamMember?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
readonly metadata?: string; // Assuming metadata is a JSON string. Adjust accordingly if it's a nested object.
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
readonly theme?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
readonly brandColor?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
readonly darkBrandColor?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
@ApiPropertyOptional({
|
||||
type: String,
|
||||
example: "https://i.cal.com/api/avatar/949be534-7a88-4185-967c-c020b0c0bef3.png",
|
||||
description: `URL of the teams banner image which is shown on booker`,
|
||||
required: false,
|
||||
})
|
||||
readonly bannerUrl?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
readonly timeFormat?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({
|
||||
type: String,
|
||||
example: "America/New_York",
|
||||
description: `Timezone is used to create teams's default schedule from Monday to Friday from 9AM to 5PM. It will default to Europe/London if not passed.`,
|
||||
required: false,
|
||||
default: "Europe/London",
|
||||
})
|
||||
readonly timeZone?: string = "Europe/London";
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({
|
||||
type: String,
|
||||
example: "Monday",
|
||||
default: "Sunday",
|
||||
})
|
||||
readonly weekStart?: string = "Sunday";
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional({ type: Boolean, default: true })
|
||||
readonly autoAcceptCreator?: boolean = true;
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { IsBoolean, IsOptional, IsString, IsUrl, Length } from "class-validator";
|
||||
|
||||
export class UpdateTeamDto {
|
||||
@IsString()
|
||||
@Length(1)
|
||||
@ApiPropertyOptional({ description: "Name of the team", example: "CalTeam" })
|
||||
readonly name?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({ type: String, description: "Team slug", example: "caltel" })
|
||||
readonly slug?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
@ApiPropertyOptional({
|
||||
type: String,
|
||||
example: "https://i.cal.com/api/avatar/b0b58752-68ad-4c0d-8024-4fa382a77752.png",
|
||||
description: `URL of the teams logo image`,
|
||||
})
|
||||
readonly logoUrl?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
@ApiPropertyOptional()
|
||||
readonly calVideoLogo?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
@ApiPropertyOptional()
|
||||
readonly appLogo?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
@ApiPropertyOptional()
|
||||
readonly appIconLogo?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
readonly bio?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional()
|
||||
readonly hideBranding?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional()
|
||||
readonly isPrivate?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional()
|
||||
readonly hideBookATeamMember?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
readonly metadata?: string; // Assuming metadata is a JSON string. Adjust accordingly if it's a nested object.
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
readonly theme?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
readonly brandColor?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
readonly darkBrandColor?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
@ApiPropertyOptional({
|
||||
type: String,
|
||||
example: "https://i.cal.com/api/avatar/949be534-7a88-4185-967c-c020b0c0bef3.png",
|
||||
description: `URL of the teams banner image which is shown on booker`,
|
||||
})
|
||||
readonly bannerUrl?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
readonly timeFormat?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({
|
||||
type: String,
|
||||
example: "America/New_York",
|
||||
description: `Timezone is used to create teams's default schedule from Monday to Friday from 9AM to 5PM. It will default to Europe/London if not passed.`,
|
||||
})
|
||||
readonly timeZone?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({
|
||||
type: String,
|
||||
example: "Monday",
|
||||
})
|
||||
readonly weekStart?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
readonly bookingLimits?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional()
|
||||
readonly includeManagedEventsInLimits?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { ApiProperty, getSchemaPath } from "@nestjs/swagger";
|
||||
import { Expose } from "class-transformer";
|
||||
import { IsEnum, ValidateNested, IsString, IsUrl } from "class-validator";
|
||||
|
||||
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { TeamOutputDto } from "@calcom/platform-types";
|
||||
|
||||
class Output {
|
||||
@Expose()
|
||||
@IsString()
|
||||
message!: string;
|
||||
|
||||
@Expose()
|
||||
@IsUrl()
|
||||
paymentLink!: string;
|
||||
|
||||
@Expose()
|
||||
@ValidateNested()
|
||||
pendingTeam!: TeamOutputDto;
|
||||
}
|
||||
|
||||
export class CreateTeamOutput {
|
||||
@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(Output) }, { $ref: getSchemaPath(TeamOutputDto) }],
|
||||
description: "Either an Output object or a TeamOutputDto.",
|
||||
})
|
||||
@Expose()
|
||||
@ValidateNested()
|
||||
data!: Output | TeamOutputDto;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { MembershipRole } from "@prisma/client";
|
||||
import { Expose, Type } from "class-transformer";
|
||||
import { IsEnum, IsString, ValidateNested } from "class-validator";
|
||||
|
||||
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { TeamOutputDto } from "@calcom/platform-types";
|
||||
|
||||
export class GetTeamOutput {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@Expose()
|
||||
@ValidateNested()
|
||||
@Type(() => TeamOutputDto)
|
||||
data!: TeamOutputDto;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { MembershipRole } from "@prisma/client";
|
||||
import { Expose, Type } from "class-transformer";
|
||||
import { IsEnum, IsString, ValidateNested } from "class-validator";
|
||||
|
||||
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { TeamOutputDto } from "@calcom/platform-types";
|
||||
|
||||
export class GetTeamsOutput {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@Expose()
|
||||
@ValidateNested()
|
||||
@Type(() => TeamOutputDto)
|
||||
data!: TeamOutputDto[];
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { MembershipRole } from "@prisma/client";
|
||||
import { Expose, Type } from "class-transformer";
|
||||
import { IsEnum, IsString, ValidateNested } from "class-validator";
|
||||
|
||||
import { ERROR_STATUS, SUCCESS_STATUS } from "@calcom/platform-constants";
|
||||
import { TeamOutputDto } from "@calcom/platform-types";
|
||||
|
||||
export class UpdateTeamOutput {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@Expose()
|
||||
@ValidateNested()
|
||||
@Type(() => TeamOutputDto)
|
||||
data!: TeamOutputDto;
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { StripeService } from "@/modules/stripe/stripe.service";
|
||||
import { TeamsMembershipsRepository } from "@/modules/teams/memberships/teams-memberships.repository";
|
||||
import { CreateTeamInput } from "@/modules/teams/teams/inputs/create-team.input";
|
||||
import { UpdateTeamDto } from "@/modules/teams/teams/inputs/update-team.input";
|
||||
import { TeamsRepository } from "@/modules/teams/teams/teams.repository";
|
||||
import { BadRequestException, Injectable, InternalServerErrorException } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
|
||||
@Injectable()
|
||||
export class TeamsService {
|
||||
private isTeamBillingEnabled = this.configService.get("stripe.isTeamBillingEnabled");
|
||||
|
||||
constructor(
|
||||
private readonly teamsRepository: TeamsRepository,
|
||||
private readonly teamsMembershipsRepository: TeamsMembershipsRepository,
|
||||
private readonly stripeService: StripeService,
|
||||
private readonly configService: ConfigService
|
||||
) {}
|
||||
|
||||
async createTeam(input: CreateTeamInput, ownerId: number) {
|
||||
const { autoAcceptCreator, ...teamData } = input;
|
||||
|
||||
const existingTeam = await this.teamsMembershipsRepository.findTeamMembershipsByNameAndUser(
|
||||
input.name,
|
||||
ownerId
|
||||
);
|
||||
if (existingTeam) {
|
||||
throw new BadRequestException({
|
||||
message: `You already have created a team with name=${input.name}`,
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.isTeamBillingEnabled) {
|
||||
const team = await this.teamsRepository.create(teamData);
|
||||
await this.teamsMembershipsRepository.createTeamMembership(team.id, {
|
||||
userId: ownerId,
|
||||
role: "OWNER",
|
||||
accepted: !!autoAcceptCreator,
|
||||
});
|
||||
return team;
|
||||
}
|
||||
|
||||
const pendingTeam = await this.teamsRepository.create({ ...teamData, pendingPayment: true });
|
||||
|
||||
const checkoutSession = await this.stripeService.generateTeamCheckoutSession(pendingTeam.id, ownerId);
|
||||
|
||||
if (!checkoutSession.url) {
|
||||
await this.teamsRepository.delete(pendingTeam.id);
|
||||
throw new InternalServerErrorException({
|
||||
message: `Failed generating team Stripe checkout session URL which is why team creation was cancelled. Please contact support.`,
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
message:
|
||||
"Your team will be created once we receive your payment. Please complete the payment using the payment link.",
|
||||
paymentLink: checkoutSession.url,
|
||||
pendingTeam,
|
||||
};
|
||||
}
|
||||
|
||||
async getUserTeams(userId: number) {
|
||||
const teams = await this.teamsRepository.getTeamsUserIsMemberOf(userId);
|
||||
return teams;
|
||||
}
|
||||
|
||||
async updateTeam(teamId: number, data: UpdateTeamDto) {
|
||||
const team = await this.teamsRepository.update(teamId, data);
|
||||
return team;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { RedisModule } from "@/modules/redis/redis.module";
|
||||
import { StripeModule } from "@/modules/stripe/stripe.module";
|
||||
import { TeamsMembershipsService } from "@/modules/teams/memberships/services/teams-memberships.service";
|
||||
import { TeamsMembershipsRepository } from "@/modules/teams/memberships/teams-memberships.repository";
|
||||
import { TeamsController } from "@/modules/teams/teams/controllers/teams.controller";
|
||||
import { TeamsService } from "@/modules/teams/teams/services/teams.service";
|
||||
import { TeamsRepository } from "@/modules/teams/teams/teams.repository";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, MembershipsModule, RedisModule, StripeModule],
|
||||
providers: [TeamsRepository, TeamsService, TeamsMembershipsRepository, TeamsMembershipsService],
|
||||
controllers: [TeamsController],
|
||||
exports: [TeamsRepository],
|
||||
})
|
||||
export class TeamsModule {}
|
||||
@@ -0,0 +1,73 @@
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
import { Prisma } from "@calcom/prisma/client";
|
||||
|
||||
@Injectable()
|
||||
export class TeamsRepository {
|
||||
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||
|
||||
async create(team: Prisma.TeamCreateInput) {
|
||||
return this.dbWrite.prisma.team.create({
|
||||
data: team,
|
||||
});
|
||||
}
|
||||
|
||||
async getById(teamId: number) {
|
||||
return this.dbRead.prisma.team.findUnique({
|
||||
where: { id: teamId },
|
||||
});
|
||||
}
|
||||
|
||||
async getByIds(teamIds: number[]) {
|
||||
return this.dbRead.prisma.team.findMany({
|
||||
where: {
|
||||
id: {
|
||||
in: teamIds,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getTeamMembersIds(teamId: number) {
|
||||
const team = await this.dbRead.prisma.team.findUnique({
|
||||
where: {
|
||||
id: teamId,
|
||||
},
|
||||
include: {
|
||||
members: true,
|
||||
},
|
||||
});
|
||||
if (!team) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return team.members.map((member) => member.userId);
|
||||
}
|
||||
|
||||
async getTeamsUserIsMemberOf(userId: number) {
|
||||
return this.dbRead.prisma.team.findMany({
|
||||
where: {
|
||||
members: {
|
||||
some: {
|
||||
userId,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async update(teamId: number, team: Prisma.TeamUpdateInput) {
|
||||
return this.dbWrite.prisma.team.update({
|
||||
where: { id: teamId },
|
||||
data: team,
|
||||
});
|
||||
}
|
||||
|
||||
async delete(teamId: number) {
|
||||
return this.dbWrite.prisma.team.delete({
|
||||
where: { id: teamId },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { CreateManagedUserInput } from "@/modules/users/inputs/create-managed-user.input";
|
||||
import { UpdateManagedUserInput } from "@/modules/users/inputs/update-managed-user.input";
|
||||
import { Injectable, NotFoundException } from "@nestjs/common";
|
||||
import type { Profile, User, Team } from "@prisma/client";
|
||||
import type { Profile, User, Team, Prisma } from "@prisma/client";
|
||||
|
||||
export type UserWithProfile = User & {
|
||||
movedToProfile?: (Profile & { organization: Pick<Team, "isPlatform" | "id" | "slug" | "name"> }) | null;
|
||||
@@ -184,6 +184,13 @@ export class UsersRepository {
|
||||
});
|
||||
}
|
||||
|
||||
async updateByEmail(email: string, updateData: Prisma.UserUpdateInput) {
|
||||
return this.dbWrite.prisma.user.update({
|
||||
where: { email },
|
||||
data: updateData,
|
||||
});
|
||||
}
|
||||
|
||||
async updateUsername(userId: number, newUsername: string) {
|
||||
return this.dbWrite.prisma.user.update({
|
||||
where: { id: userId },
|
||||
|
||||
@@ -5403,6 +5403,157 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/teams": {
|
||||
"post": {
|
||||
"operationId": "TeamsController_createTeam",
|
||||
"summary": "Create a team",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreateTeamInput"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreateTeamOutput"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Teams"
|
||||
]
|
||||
},
|
||||
"get": {
|
||||
"operationId": "TeamsController_getTeams",
|
||||
"summary": "Get teams",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/GetTeamsOutput"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Teams"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/teams/{teamId}": {
|
||||
"get": {
|
||||
"operationId": "TeamsController_getTeam",
|
||||
"summary": "Get a team",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "teamId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/GetTeamOutput"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Teams"
|
||||
]
|
||||
},
|
||||
"patch": {
|
||||
"operationId": "TeamsController_updateTeam",
|
||||
"summary": "Update a team",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "teamId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UpdateOrgTeamDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UpdateTeamOutput"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Teams"
|
||||
]
|
||||
},
|
||||
"delete": {
|
||||
"operationId": "TeamsController_deleteTeam",
|
||||
"summary": "Delete a team",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "teamId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/OrgTeamOutputResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"Teams"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/v2/teams/{teamId}/memberships": {
|
||||
"post": {
|
||||
"operationId": "TeamsMembershipsController_createTeamMembership",
|
||||
@@ -7263,6 +7414,56 @@
|
||||
"type"
|
||||
]
|
||||
},
|
||||
"NameFieldInput_2024_06_14": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"example": "name",
|
||||
"description": "only allowed value for type is `name`"
|
||||
},
|
||||
"label": {
|
||||
"type": "string"
|
||||
},
|
||||
"placeholder": {
|
||||
"type": "string"
|
||||
},
|
||||
"disableOnPrefill": {
|
||||
"type": "boolean",
|
||||
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if URL contains query parameter `&name=bob`, the name field will be prefilled with this value and disabled."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"label",
|
||||
"placeholder"
|
||||
]
|
||||
},
|
||||
"EmailFieldInput_2024_06_14": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"example": "email",
|
||||
"description": "only allowed value for type is `email`"
|
||||
},
|
||||
"label": {
|
||||
"type": "string"
|
||||
},
|
||||
"placeholder": {
|
||||
"type": "string"
|
||||
},
|
||||
"disableOnPrefill": {
|
||||
"type": "boolean",
|
||||
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if URL contains query parameter `&email=bob@gmail.com`, the email field will be prefilled with this value and disabled."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"type",
|
||||
"label",
|
||||
"placeholder"
|
||||
]
|
||||
},
|
||||
"BookerLayouts_2024_06_14": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -7392,6 +7593,12 @@
|
||||
"description": "Custom fields that can be added to the booking form when the event is booked by someone. By default booking form has name and email field.",
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/NameFieldInput_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EmailFieldInput_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/PhoneFieldInput_2024_06_14"
|
||||
},
|
||||
@@ -9117,6 +9324,12 @@
|
||||
"description": "Custom fields that can be added to the booking form when the event is booked by someone. By default booking form has name and email field.",
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/NameFieldInput_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EmailFieldInput_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/PhoneFieldInput_2024_06_14"
|
||||
},
|
||||
@@ -10688,6 +10901,12 @@
|
||||
"description": "Custom fields that can be added to the booking form when the event is booked by someone. By default booking form has name and email field.",
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/NameFieldInput_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EmailFieldInput_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/PhoneFieldInput_2024_06_14"
|
||||
},
|
||||
@@ -11507,6 +11726,12 @@
|
||||
"description": "Custom fields that can be added to the booking form when the event is booked by someone. By default booking form has name and email field.",
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/NameFieldInput_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EmailFieldInput_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/PhoneFieldInput_2024_06_14"
|
||||
},
|
||||
@@ -12654,6 +12879,255 @@
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"CreateTeamInput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "Name of the team",
|
||||
"example": "CalTeam"
|
||||
},
|
||||
"slug": {
|
||||
"type": "string",
|
||||
"description": "Team slug",
|
||||
"example": "caltel"
|
||||
},
|
||||
"logoUrl": {
|
||||
"type": "string",
|
||||
"example": "https://i.cal.com/api/avatar/b0b58752-68ad-4c0d-8024-4fa382a77752.png",
|
||||
"description": "URL of the teams logo image"
|
||||
},
|
||||
"calVideoLogo": {
|
||||
"type": "string"
|
||||
},
|
||||
"appLogo": {
|
||||
"type": "string"
|
||||
},
|
||||
"appIconLogo": {
|
||||
"type": "string"
|
||||
},
|
||||
"bio": {
|
||||
"type": "string"
|
||||
},
|
||||
"hideBranding": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"isPrivate": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"hideBookATeamMember": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "string"
|
||||
},
|
||||
"theme": {
|
||||
"type": "string"
|
||||
},
|
||||
"brandColor": {
|
||||
"type": "string"
|
||||
},
|
||||
"darkBrandColor": {
|
||||
"type": "string"
|
||||
},
|
||||
"bannerUrl": {
|
||||
"type": "string",
|
||||
"example": "https://i.cal.com/api/avatar/949be534-7a88-4185-967c-c020b0c0bef3.png",
|
||||
"description": "URL of the teams banner image which is shown on booker"
|
||||
},
|
||||
"timeFormat": {
|
||||
"type": "number"
|
||||
},
|
||||
"timeZone": {
|
||||
"type": "string",
|
||||
"default": "Europe/London",
|
||||
"example": "America/New_York",
|
||||
"description": "Timezone is used to create teams's default schedule from Monday to Friday from 9AM to 5PM. It will default to Europe/London if not passed."
|
||||
},
|
||||
"weekStart": {
|
||||
"type": "string",
|
||||
"default": "Sunday",
|
||||
"example": "Monday"
|
||||
},
|
||||
"autoAcceptCreator": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"name"
|
||||
]
|
||||
},
|
||||
"CreateTeamOutput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "success",
|
||||
"enum": [
|
||||
"success",
|
||||
"error"
|
||||
]
|
||||
},
|
||||
"data": {
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/Output"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/TeamOutputDto"
|
||||
}
|
||||
],
|
||||
"description": "Either an Output object or a TeamOutputDto."
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"TeamOutputDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number"
|
||||
},
|
||||
"parentId": {
|
||||
"type": "number"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"slug": {
|
||||
"type": "string"
|
||||
},
|
||||
"logoUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"calVideoLogo": {
|
||||
"type": "string"
|
||||
},
|
||||
"appLogo": {
|
||||
"type": "string"
|
||||
},
|
||||
"appIconLogo": {
|
||||
"type": "string"
|
||||
},
|
||||
"bio": {
|
||||
"type": "string"
|
||||
},
|
||||
"hideBranding": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"isOrganization": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"isPrivate": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"hideBookATeamMember": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"metadata": {
|
||||
"type": "string"
|
||||
},
|
||||
"theme": {
|
||||
"type": "string"
|
||||
},
|
||||
"brandColor": {
|
||||
"type": "string"
|
||||
},
|
||||
"darkBrandColor": {
|
||||
"type": "string"
|
||||
},
|
||||
"bannerUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"timeFormat": {
|
||||
"type": "number"
|
||||
},
|
||||
"timeZone": {
|
||||
"type": "string",
|
||||
"default": "Europe/London"
|
||||
},
|
||||
"weekStart": {
|
||||
"type": "string",
|
||||
"default": "Sunday"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"id",
|
||||
"name",
|
||||
"isOrganization"
|
||||
]
|
||||
},
|
||||
"GetTeamOutput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "success",
|
||||
"enum": [
|
||||
"success",
|
||||
"error"
|
||||
]
|
||||
},
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/TeamOutputDto"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"GetTeamsOutput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "success",
|
||||
"enum": [
|
||||
"success",
|
||||
"error"
|
||||
]
|
||||
},
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/TeamOutputDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"UpdateTeamOutput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "success",
|
||||
"enum": [
|
||||
"success",
|
||||
"error"
|
||||
]
|
||||
},
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/TeamOutputDto"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"status",
|
||||
"data"
|
||||
]
|
||||
},
|
||||
"AuthUrlData": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -21,7 +21,7 @@ export class TeamRepositoryFixture {
|
||||
}
|
||||
|
||||
async delete(teamId: Team["id"]) {
|
||||
return this.prismaWriteClient.team.delete({ where: { id: teamId } });
|
||||
return this.prismaWriteClient.team.deleteMany({ where: { id: teamId } });
|
||||
}
|
||||
|
||||
async getPlatformOrgTeams(organizationId: number, oAuthClientId: string) {
|
||||
|
||||
@@ -20,6 +20,7 @@ const env: Partial<Omit<Environment, "NODE_ENV">> = {
|
||||
// note(Lauris): setting high limit so that e2e tests themselves are not rate limited
|
||||
RATE_LIMIT_DEFAULT_LIMIT: 10000,
|
||||
RATE_LIMIT_DEFAULT_BLOCK_DURATION_MS: 60000,
|
||||
IS_TEAM_BILLING_ENABLED: false,
|
||||
};
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
|
||||
@@ -5130,6 +5130,147 @@
|
||||
"tags": ["Stripe"]
|
||||
}
|
||||
},
|
||||
"/v2/teams": {
|
||||
"post": {
|
||||
"operationId": "TeamsController_createTeam",
|
||||
"summary": "Create a team",
|
||||
"parameters": [],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreateTeamInput"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"201": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/CreateTeamOutput"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": ["Teams"]
|
||||
},
|
||||
"get": {
|
||||
"operationId": "TeamsController_getTeams",
|
||||
"summary": "Get teams",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/GetTeamsOutput"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": ["Teams"]
|
||||
}
|
||||
},
|
||||
"/v2/teams/{teamId}": {
|
||||
"get": {
|
||||
"operationId": "TeamsController_getTeam",
|
||||
"summary": "Get a team",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "teamId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/GetTeamOutput"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": ["Teams"]
|
||||
},
|
||||
"patch": {
|
||||
"operationId": "TeamsController_updateTeam",
|
||||
"summary": "Update a team",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "teamId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
],
|
||||
"requestBody": {
|
||||
"required": true,
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UpdateOrgTeamDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/UpdateTeamOutput"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": ["Teams"]
|
||||
},
|
||||
"delete": {
|
||||
"operationId": "TeamsController_deleteTeam",
|
||||
"summary": "Delete a team",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "teamId",
|
||||
"required": true,
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/OrgTeamOutputResponseDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": ["Teams"]
|
||||
}
|
||||
},
|
||||
"/v2/teams/{teamId}/memberships": {
|
||||
"post": {
|
||||
"operationId": "TeamsMembershipsController_createTeamMembership",
|
||||
@@ -6728,6 +6869,48 @@
|
||||
},
|
||||
"required": ["type"]
|
||||
},
|
||||
"NameFieldInput_2024_06_14": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"example": "name",
|
||||
"description": "only allowed value for type is `name`"
|
||||
},
|
||||
"label": {
|
||||
"type": "string"
|
||||
},
|
||||
"placeholder": {
|
||||
"type": "string"
|
||||
},
|
||||
"disableOnPrefill": {
|
||||
"type": "boolean",
|
||||
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if URL contains query parameter `&name=bob`, the name field will be prefilled with this value and disabled."
|
||||
}
|
||||
},
|
||||
"required": ["type", "label", "placeholder"]
|
||||
},
|
||||
"EmailFieldInput_2024_06_14": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"example": "email",
|
||||
"description": "only allowed value for type is `email`"
|
||||
},
|
||||
"label": {
|
||||
"type": "string"
|
||||
},
|
||||
"placeholder": {
|
||||
"type": "string"
|
||||
},
|
||||
"disableOnPrefill": {
|
||||
"type": "boolean",
|
||||
"description": "Disable this booking field if the URL contains query parameter with key equal to the slug and prefill it with the provided value. For example, if URL contains query parameter `&email=bob@gmail.com`, the email field will be prefilled with this value and disabled."
|
||||
}
|
||||
},
|
||||
"required": ["type", "label", "placeholder"]
|
||||
},
|
||||
"BookerLayouts_2024_06_14": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -6836,6 +7019,12 @@
|
||||
"description": "Custom fields that can be added to the booking form when the event is booked by someone. By default booking form has name and email field.",
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/NameFieldInput_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EmailFieldInput_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/PhoneFieldInput_2024_06_14"
|
||||
},
|
||||
@@ -8393,6 +8582,12 @@
|
||||
"description": "Custom fields that can be added to the booking form when the event is booked by someone. By default booking form has name and email field.",
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/NameFieldInput_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EmailFieldInput_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/PhoneFieldInput_2024_06_14"
|
||||
},
|
||||
@@ -9730,6 +9925,12 @@
|
||||
"description": "Custom fields that can be added to the booking form when the event is booked by someone. By default booking form has name and email field.",
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/NameFieldInput_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EmailFieldInput_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/PhoneFieldInput_2024_06_14"
|
||||
},
|
||||
@@ -10483,6 +10684,12 @@
|
||||
"description": "Custom fields that can be added to the booking form when the event is booked by someone. By default booking form has name and email field.",
|
||||
"items": {
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/NameFieldInput_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/EmailFieldInput_2024_06_14"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/PhoneFieldInput_2024_06_14"
|
||||
},
|
||||
@@ -11421,6 +11628,225 @@
|
||||
},
|
||||
"required": ["status", "data"]
|
||||
},
|
||||
"CreateTeamInput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"description": "Name of the team",
|
||||
"example": "CalTeam"
|
||||
},
|
||||
"slug": {
|
||||
"type": "string",
|
||||
"description": "Team slug",
|
||||
"example": "caltel"
|
||||
},
|
||||
"logoUrl": {
|
||||
"type": "string",
|
||||
"example": "https://i.cal.com/api/avatar/b0b58752-68ad-4c0d-8024-4fa382a77752.png",
|
||||
"description": "URL of the teams logo image"
|
||||
},
|
||||
"calVideoLogo": {
|
||||
"type": "string"
|
||||
},
|
||||
"appLogo": {
|
||||
"type": "string"
|
||||
},
|
||||
"appIconLogo": {
|
||||
"type": "string"
|
||||
},
|
||||
"bio": {
|
||||
"type": "string"
|
||||
},
|
||||
"hideBranding": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"isPrivate": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"hideBookATeamMember": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"metadata": {
|
||||
"type": "string"
|
||||
},
|
||||
"theme": {
|
||||
"type": "string"
|
||||
},
|
||||
"brandColor": {
|
||||
"type": "string"
|
||||
},
|
||||
"darkBrandColor": {
|
||||
"type": "string"
|
||||
},
|
||||
"bannerUrl": {
|
||||
"type": "string",
|
||||
"example": "https://i.cal.com/api/avatar/949be534-7a88-4185-967c-c020b0c0bef3.png",
|
||||
"description": "URL of the teams banner image which is shown on booker"
|
||||
},
|
||||
"timeFormat": {
|
||||
"type": "number"
|
||||
},
|
||||
"timeZone": {
|
||||
"type": "string",
|
||||
"default": "Europe/London",
|
||||
"example": "America/New_York",
|
||||
"description": "Timezone is used to create teams's default schedule from Monday to Friday from 9AM to 5PM. It will default to Europe/London if not passed."
|
||||
},
|
||||
"weekStart": {
|
||||
"type": "string",
|
||||
"default": "Sunday",
|
||||
"example": "Monday"
|
||||
},
|
||||
"autoAcceptCreator": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
}
|
||||
},
|
||||
"required": ["name"]
|
||||
},
|
||||
"CreateTeamOutput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "success",
|
||||
"enum": ["success", "error"]
|
||||
},
|
||||
"data": {
|
||||
"oneOf": [
|
||||
{
|
||||
"$ref": "#/components/schemas/Output"
|
||||
},
|
||||
{
|
||||
"$ref": "#/components/schemas/TeamOutputDto"
|
||||
}
|
||||
],
|
||||
"description": "Either an Output object or a TeamOutputDto."
|
||||
}
|
||||
},
|
||||
"required": ["status", "data"]
|
||||
},
|
||||
"TeamOutputDto": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "number"
|
||||
},
|
||||
"parentId": {
|
||||
"type": "number"
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"slug": {
|
||||
"type": "string"
|
||||
},
|
||||
"logoUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"calVideoLogo": {
|
||||
"type": "string"
|
||||
},
|
||||
"appLogo": {
|
||||
"type": "string"
|
||||
},
|
||||
"appIconLogo": {
|
||||
"type": "string"
|
||||
},
|
||||
"bio": {
|
||||
"type": "string"
|
||||
},
|
||||
"hideBranding": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"isOrganization": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"isPrivate": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"hideBookATeamMember": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"metadata": {
|
||||
"type": "string"
|
||||
},
|
||||
"theme": {
|
||||
"type": "string"
|
||||
},
|
||||
"brandColor": {
|
||||
"type": "string"
|
||||
},
|
||||
"darkBrandColor": {
|
||||
"type": "string"
|
||||
},
|
||||
"bannerUrl": {
|
||||
"type": "string"
|
||||
},
|
||||
"timeFormat": {
|
||||
"type": "number"
|
||||
},
|
||||
"timeZone": {
|
||||
"type": "string",
|
||||
"default": "Europe/London"
|
||||
},
|
||||
"weekStart": {
|
||||
"type": "string",
|
||||
"default": "Sunday"
|
||||
}
|
||||
},
|
||||
"required": ["id", "name", "isOrganization"]
|
||||
},
|
||||
"GetTeamOutput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "success",
|
||||
"enum": ["success", "error"]
|
||||
},
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/TeamOutputDto"
|
||||
}
|
||||
},
|
||||
"required": ["status", "data"]
|
||||
},
|
||||
"GetTeamsOutput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "success",
|
||||
"enum": ["success", "error"]
|
||||
},
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/TeamOutputDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["status", "data"]
|
||||
},
|
||||
"UpdateTeamOutput": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"status": {
|
||||
"type": "string",
|
||||
"example": "success",
|
||||
"enum": ["success", "error"]
|
||||
},
|
||||
"data": {
|
||||
"$ref": "#/components/schemas/TeamOutputDto"
|
||||
}
|
||||
},
|
||||
"required": ["status", "data"]
|
||||
},
|
||||
"AuthUrlData": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
+7
-1
@@ -26,8 +26,10 @@ import {
|
||||
AddressFieldInput_2024_06_14,
|
||||
BooleanFieldInput_2024_06_14,
|
||||
CheckboxGroupFieldInput_2024_06_14,
|
||||
EmailFieldInput_2024_06_14,
|
||||
MultiEmailFieldInput_2024_06_14,
|
||||
MultiSelectFieldInput_2024_06_14,
|
||||
NameFieldInput_2024_06_14,
|
||||
NumberFieldInput_2024_06_14,
|
||||
PhoneFieldInput_2024_06_14,
|
||||
RadioGroupFieldInput_2024_06_14,
|
||||
@@ -103,7 +105,9 @@ export const CREATE_EVENT_SLUG_EXAMPLE = "learn-the-secrets-of-masterchief";
|
||||
Seats_2024_06_14,
|
||||
InputAttendeeAddressLocation_2024_06_14,
|
||||
InputAttendeePhoneLocation_2024_06_14,
|
||||
InputAttendeeDefinedLocation_2024_06_14
|
||||
InputAttendeeDefinedLocation_2024_06_14,
|
||||
NameFieldInput_2024_06_14,
|
||||
EmailFieldInput_2024_06_14
|
||||
)
|
||||
export class CreateEventTypeInput_2024_06_14 {
|
||||
@IsInt()
|
||||
@@ -162,6 +166,8 @@ export class CreateEventTypeInput_2024_06_14 {
|
||||
description:
|
||||
"Custom fields that can be added to the booking form when the event is booked by someone. By default booking form has name and email field.",
|
||||
oneOf: [
|
||||
{ $ref: getSchemaPath(NameFieldInput_2024_06_14) },
|
||||
{ $ref: getSchemaPath(EmailFieldInput_2024_06_14) },
|
||||
{ $ref: getSchemaPath(PhoneFieldInput_2024_06_14) },
|
||||
{ $ref: getSchemaPath(AddressFieldInput_2024_06_14) },
|
||||
{ $ref: getSchemaPath(TextFieldInput_2024_06_14) },
|
||||
|
||||
+7
-1
@@ -24,8 +24,10 @@ import {
|
||||
AddressFieldInput_2024_06_14,
|
||||
BooleanFieldInput_2024_06_14,
|
||||
CheckboxGroupFieldInput_2024_06_14,
|
||||
EmailFieldInput_2024_06_14,
|
||||
MultiEmailFieldInput_2024_06_14,
|
||||
MultiSelectFieldInput_2024_06_14,
|
||||
NameFieldInput_2024_06_14,
|
||||
NumberFieldInput_2024_06_14,
|
||||
PhoneFieldInput_2024_06_14,
|
||||
RadioGroupFieldInput_2024_06_14,
|
||||
@@ -101,7 +103,9 @@ import { Seats_2024_06_14 } from "./seats.input";
|
||||
Seats_2024_06_14,
|
||||
InputAttendeeAddressLocation_2024_06_14,
|
||||
InputAttendeePhoneLocation_2024_06_14,
|
||||
InputAttendeeDefinedLocation_2024_06_14
|
||||
InputAttendeeDefinedLocation_2024_06_14,
|
||||
NameFieldInput_2024_06_14,
|
||||
EmailFieldInput_2024_06_14
|
||||
)
|
||||
export class UpdateEventTypeInput_2024_06_14 {
|
||||
@IsOptional()
|
||||
@@ -163,6 +167,8 @@ export class UpdateEventTypeInput_2024_06_14 {
|
||||
description:
|
||||
"Custom fields that can be added to the booking form when the event is booked by someone. By default booking form has name and email field.",
|
||||
oneOf: [
|
||||
{ $ref: getSchemaPath(NameFieldInput_2024_06_14) },
|
||||
{ $ref: getSchemaPath(EmailFieldInput_2024_06_14) },
|
||||
{ $ref: getSchemaPath(PhoneFieldInput_2024_06_14) },
|
||||
{ $ref: getSchemaPath(AddressFieldInput_2024_06_14) },
|
||||
{ $ref: getSchemaPath(TextFieldInput_2024_06_14) },
|
||||
|
||||
@@ -8,3 +8,4 @@ export * from "./billing";
|
||||
export * from "./schedules";
|
||||
export * from "./event-types";
|
||||
export * from "./organizations";
|
||||
export * from "./teams";
|
||||
|
||||
@@ -1,129 +1,13 @@
|
||||
import { ApiProperty as DocsProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Expose } from "class-transformer";
|
||||
import { IsBoolean, IsInt, IsOptional, IsString, IsUrl, Length } from "class-validator";
|
||||
import { IsInt, IsOptional } from "class-validator";
|
||||
|
||||
export class OrgTeamOutputDto {
|
||||
@IsInt()
|
||||
@Expose()
|
||||
@DocsProperty()
|
||||
readonly id!: number;
|
||||
import { TeamOutputDto } from "../../../teams/outputs";
|
||||
|
||||
export class OrgTeamOutputDto extends TeamOutputDto {
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
@Expose()
|
||||
@ApiPropertyOptional()
|
||||
readonly parentId?: number;
|
||||
|
||||
@IsString()
|
||||
@Length(1)
|
||||
@Expose()
|
||||
@DocsProperty()
|
||||
readonly name!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Expose()
|
||||
@ApiPropertyOptional()
|
||||
readonly slug?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
@Expose()
|
||||
@ApiPropertyOptional()
|
||||
readonly logoUrl?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
@Expose()
|
||||
@ApiPropertyOptional()
|
||||
readonly calVideoLogo?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
@Expose()
|
||||
@ApiPropertyOptional()
|
||||
readonly appLogo?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
@Expose()
|
||||
@ApiPropertyOptional()
|
||||
readonly appIconLogo?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Expose()
|
||||
@ApiPropertyOptional()
|
||||
readonly bio?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Expose()
|
||||
@ApiPropertyOptional()
|
||||
readonly hideBranding?: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@Expose()
|
||||
@DocsProperty()
|
||||
readonly isOrganization!: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Expose()
|
||||
@ApiPropertyOptional()
|
||||
readonly isPrivate?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Expose()
|
||||
@ApiPropertyOptional()
|
||||
readonly hideBookATeamMember?: boolean = false;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Expose()
|
||||
@ApiPropertyOptional()
|
||||
readonly metadata?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Expose()
|
||||
@ApiPropertyOptional()
|
||||
readonly theme?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Expose()
|
||||
@ApiPropertyOptional()
|
||||
readonly brandColor?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Expose()
|
||||
@ApiPropertyOptional()
|
||||
readonly darkBrandColor?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
@Expose()
|
||||
@ApiPropertyOptional()
|
||||
readonly bannerUrl?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Expose()
|
||||
@ApiPropertyOptional()
|
||||
readonly timeFormat?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Expose()
|
||||
@ApiPropertyOptional({ type: String, default: "Europe/London" })
|
||||
readonly timeZone?: string = "Europe/London";
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Expose()
|
||||
@ApiPropertyOptional({ type: String, default: "Sunday" })
|
||||
readonly weekStart?: string = "Sunday";
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./outputs";
|
||||
@@ -0,0 +1 @@
|
||||
export * from "./team.output";
|
||||
@@ -0,0 +1,129 @@
|
||||
import { ApiProperty as DocsProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Expose } from "class-transformer";
|
||||
import { IsBoolean, IsInt, IsOptional, IsString, IsUrl, Length } from "class-validator";
|
||||
|
||||
export class TeamOutputDto {
|
||||
@IsInt()
|
||||
@Expose()
|
||||
@DocsProperty()
|
||||
readonly id!: number;
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
@Expose()
|
||||
@ApiPropertyOptional()
|
||||
readonly parentId?: number;
|
||||
|
||||
@IsString()
|
||||
@Length(1)
|
||||
@Expose()
|
||||
@DocsProperty()
|
||||
readonly name!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Expose()
|
||||
@ApiPropertyOptional()
|
||||
readonly slug?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
@Expose()
|
||||
@ApiPropertyOptional()
|
||||
readonly logoUrl?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
@Expose()
|
||||
@ApiPropertyOptional()
|
||||
readonly calVideoLogo?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
@Expose()
|
||||
@ApiPropertyOptional()
|
||||
readonly appLogo?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
@Expose()
|
||||
@ApiPropertyOptional()
|
||||
readonly appIconLogo?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Expose()
|
||||
@ApiPropertyOptional()
|
||||
readonly bio?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Expose()
|
||||
@ApiPropertyOptional()
|
||||
readonly hideBranding?: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@Expose()
|
||||
@DocsProperty()
|
||||
readonly isOrganization!: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Expose()
|
||||
@ApiPropertyOptional()
|
||||
readonly isPrivate?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Expose()
|
||||
@ApiPropertyOptional()
|
||||
readonly hideBookATeamMember?: boolean = false;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Expose()
|
||||
@ApiPropertyOptional()
|
||||
readonly metadata?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Expose()
|
||||
@ApiPropertyOptional()
|
||||
readonly theme?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Expose()
|
||||
@ApiPropertyOptional()
|
||||
readonly brandColor?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Expose()
|
||||
@ApiPropertyOptional()
|
||||
readonly darkBrandColor?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
@Expose()
|
||||
@ApiPropertyOptional()
|
||||
readonly bannerUrl?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Expose()
|
||||
@ApiPropertyOptional()
|
||||
readonly timeFormat?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Expose()
|
||||
@ApiPropertyOptional({ type: String, default: "Europe/London" })
|
||||
readonly timeZone?: string = "Europe/London";
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Expose()
|
||||
@ApiPropertyOptional({ type: String, default: "Sunday" })
|
||||
readonly weekStart?: string = "Sunday";
|
||||
}
|
||||
Reference in New Issue
Block a user