Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a0706d9b51 | ||
|
|
2eb385af48 | ||
|
|
a0f986f949 | ||
|
|
b0094bad2d | ||
|
|
c732c06835 | ||
|
|
c1334475c5 | ||
|
|
b35dc43011 | ||
|
|
c7dc3ecc49 | ||
|
|
a613c975d3 | ||
|
|
5fad7c9478 | ||
|
|
318470d866 | ||
|
|
a0ce42e375 | ||
|
|
5293a5cc54 | ||
|
|
42aefc9032 | ||
|
|
14acf4e9bc | ||
|
|
80815bd8a5 | ||
|
|
9ed8face03 | ||
|
|
18a107d081 | ||
|
|
04d7537e2f | ||
|
|
423b17a819 |
Vendored
+8
@@ -0,0 +1,8 @@
|
||||
undecided:
|
||||
- calcom-monorepo
|
||||
- "@calcom/app-store-cli"
|
||||
- "@calcom/platform-constants"
|
||||
- "@calcom/platform-enums"
|
||||
- "@calcom/platform-types"
|
||||
- "@calcom/platform-utils"
|
||||
- "@calcom/prisma"
|
||||
@@ -29,7 +29,7 @@
|
||||
"@axiomhq/winston": "^1.2.0",
|
||||
"@calcom/platform-constants": "*",
|
||||
"@calcom/platform-enums": "*",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.72",
|
||||
"@calcom/platform-libraries": "npm:@calcom/platform-libraries@0.0.73",
|
||||
"@calcom/platform-libraries-0.0.2": "npm:@calcom/platform-libraries@0.0.2",
|
||||
"@calcom/platform-types": "*",
|
||||
"@calcom/platform-utils": "*",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Transform, Type } from "class-transformer";
|
||||
import {
|
||||
IsBoolean,
|
||||
@@ -13,53 +14,66 @@ import {
|
||||
|
||||
class Location {
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
optionValue!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
value!: string;
|
||||
}
|
||||
|
||||
class Response {
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
name!: string;
|
||||
|
||||
@IsEmail()
|
||||
@ApiProperty()
|
||||
email!: string;
|
||||
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
@ApiProperty({ type: [String] })
|
||||
guests!: string[];
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => Location)
|
||||
@ApiPropertyOptional({ type: Location })
|
||||
location?: Location;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
notes?: string;
|
||||
}
|
||||
|
||||
export class CreateBookingInput_2024_04_15 {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
end?: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
start!: string;
|
||||
|
||||
@IsNumber()
|
||||
@ApiProperty()
|
||||
eventTypeId!: number;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
eventTypeSlug?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
rescheduleUid?: string;
|
||||
|
||||
@IsTimeZone()
|
||||
@ApiProperty()
|
||||
timeZone!: string;
|
||||
|
||||
@Transform(({ value }: { value: string | string[] }) => {
|
||||
@@ -68,38 +82,48 @@ export class CreateBookingInput_2024_04_15 {
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
@ApiPropertyOptional({ type: [String] })
|
||||
user?: string[];
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
language!: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
bookingUid?: string;
|
||||
|
||||
@IsObject()
|
||||
@ApiProperty({ type: Object })
|
||||
metadata!: Record<string, string>;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
hasHashedBookingLink?: boolean;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
hashedLink!: string | null;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
seatReferenceUid?: string;
|
||||
|
||||
@Type(() => Response)
|
||||
@ApiProperty({ type: Response })
|
||||
responses!: Response;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
orgSlug?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
locationUrl?: string;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { CreateBookingInput_2024_04_15 } from "@/ee/bookings/2024-04-15/inputs/create-booking.input";
|
||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { IsBoolean, IsString, IsNumber, IsOptional } from "class-validator";
|
||||
|
||||
import type { AppsStatus } from "@calcom/platform-libraries";
|
||||
@@ -6,23 +7,29 @@ import type { AppsStatus } from "@calcom/platform-libraries";
|
||||
export class CreateRecurringBookingInput_2024_04_15 extends CreateBookingInput_2024_04_15 {
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
noEmail?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@ApiPropertyOptional()
|
||||
recurringCount?: number;
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: [Object] })
|
||||
appsStatus?: AppsStatus[] | undefined;
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: [Object] })
|
||||
allRecurringDates?: Record<string, string>[];
|
||||
|
||||
@IsOptional()
|
||||
@IsNumber()
|
||||
@ApiPropertyOptional()
|
||||
currentRecurringIndex?: number;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
recurringEventId?: string;
|
||||
}
|
||||
|
||||
@@ -1,22 +1,27 @@
|
||||
import { ApiPropertyOptional, ApiProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsOptional, IsArray, IsEmail, IsBoolean, ValidateNested } from "class-validator";
|
||||
|
||||
class Attendee {
|
||||
@IsEmail()
|
||||
@ApiProperty()
|
||||
email!: string;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty()
|
||||
noShow!: boolean;
|
||||
}
|
||||
|
||||
export class MarkNoShowInput_2024_04_15 {
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
noShowHost?: boolean;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => Attendee)
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: [Attendee] })
|
||||
attendees?: Attendee[];
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import {
|
||||
IsString,
|
||||
@@ -17,145 +17,185 @@ import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
class Metadata {
|
||||
@IsUrl()
|
||||
@ApiProperty()
|
||||
videoCallUrl!: string;
|
||||
}
|
||||
|
||||
class Location {
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
optionValue!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
value!: string;
|
||||
}
|
||||
|
||||
class Response {
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
name!: string;
|
||||
|
||||
@IsEmail()
|
||||
@ApiProperty()
|
||||
email!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
notes!: string;
|
||||
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
@ApiProperty({ type: [String] })
|
||||
guests!: string[];
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => Location)
|
||||
@ApiProperty({ type: Location })
|
||||
location!: Location;
|
||||
}
|
||||
|
||||
class User {
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
id!: number;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
name!: string | null;
|
||||
|
||||
@IsEmail()
|
||||
@ApiProperty()
|
||||
email!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
username!: string | null;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
timeZone!: string;
|
||||
}
|
||||
|
||||
class Attendee {
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
name!: string;
|
||||
|
||||
@IsEmail()
|
||||
@ApiProperty()
|
||||
email!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
timeZone!: string;
|
||||
}
|
||||
|
||||
class EventType {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
eventName!: string | null;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
slug!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
timeZone!: string | null;
|
||||
}
|
||||
|
||||
class GetBookingData_2024_04_15 {
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
title!: string;
|
||||
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
id!: number;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
uid!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
description!: string | null;
|
||||
|
||||
@IsObject()
|
||||
@ApiProperty({ type: Object })
|
||||
customInputs!: any;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
smsReminderNumber!: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
recurringEventId!: string | null;
|
||||
|
||||
@IsDateString()
|
||||
@ApiProperty()
|
||||
startTime!: Date;
|
||||
|
||||
@IsDateString()
|
||||
@ApiProperty()
|
||||
endTime!: Date;
|
||||
|
||||
@IsUrl()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
location!: string | null;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
status!: string;
|
||||
|
||||
@Type(() => Metadata)
|
||||
@ApiProperty({ type: Metadata })
|
||||
metadata!: Metadata | any;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
cancellationReason!: string | null;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => Response)
|
||||
@ApiProperty({ type: Response })
|
||||
responses!: Response | any;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
rejectionReason!: string | null;
|
||||
|
||||
@IsString()
|
||||
@IsEmail()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
userPrimaryEmail!: string | null;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => User)
|
||||
@ApiProperty({ type: User, nullable: true })
|
||||
user!: User | null;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => Attendee)
|
||||
@IsArray()
|
||||
@ApiProperty({ type: [Attendee] })
|
||||
attendees!: Attendee[];
|
||||
|
||||
@IsInt()
|
||||
@ApiProperty({ type: Number, nullable: true })
|
||||
eventTypeId!: number | null;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => EventType)
|
||||
@ApiProperty({ type: EventType, nullable: true })
|
||||
eventType!: EventType | null;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import {
|
||||
IsString,
|
||||
@@ -26,179 +26,230 @@ enum Status {
|
||||
|
||||
class Attendee {
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
id!: number;
|
||||
|
||||
@IsEmail()
|
||||
@ApiProperty()
|
||||
email!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
name!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
timeZone!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
locale!: string | null;
|
||||
|
||||
@IsInt()
|
||||
@ApiProperty({ type: Number, nullable: true })
|
||||
bookingId!: number | null;
|
||||
}
|
||||
|
||||
class EventType {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
slug?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@ApiPropertyOptional()
|
||||
id?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
eventName?: string | null;
|
||||
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
price!: number;
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
recurringEvent?: any;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
currency!: string;
|
||||
|
||||
@IsObject()
|
||||
@ApiProperty()
|
||||
metadata!: any;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
seatsShowAttendees?: boolean | undefined | null;
|
||||
@ApiPropertyOptional({ type: Boolean, nullable: true })
|
||||
seatsShowAttendees?: boolean | null;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
seatsShowAvailabilityCount?: boolean | undefined | null;
|
||||
@ApiPropertyOptional({ type: Boolean, nullable: true })
|
||||
seatsShowAvailabilityCount?: boolean | null;
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ nullable: true })
|
||||
team?: any | null;
|
||||
}
|
||||
|
||||
class Reference {
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
id!: number;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
type!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
uid!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
meetingId?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
thirdPartyRecurringEventId?: string | null;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
meetingPassword!: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
meetingUrl?: string | null;
|
||||
|
||||
@IsInt()
|
||||
@ApiProperty({ type: Number, nullable: true })
|
||||
bookingId!: number | null;
|
||||
|
||||
@IsEmail()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
externalCalendarId!: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
deleted?: any;
|
||||
|
||||
@IsInt()
|
||||
@ApiProperty({ type: Number, nullable: true })
|
||||
credentialId!: number | null;
|
||||
}
|
||||
|
||||
class User {
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
id!: number;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
name!: string | null;
|
||||
|
||||
@IsEmail()
|
||||
@ApiProperty()
|
||||
email!: string;
|
||||
}
|
||||
|
||||
class GetBookingsDataEntry {
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
id!: number;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
title!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEmail()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
userPrimaryEmail?: string | null;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
description!: string | null;
|
||||
|
||||
@IsObject()
|
||||
@ApiProperty({ type: Object })
|
||||
customInputs!: object | any;
|
||||
|
||||
@IsDateString()
|
||||
@ApiProperty()
|
||||
startTime!: string;
|
||||
|
||||
@IsDateString()
|
||||
@ApiProperty()
|
||||
endTime!: string;
|
||||
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => Attendee)
|
||||
@IsArray()
|
||||
@ApiProperty({ type: [Attendee] })
|
||||
attendees!: Attendee[];
|
||||
|
||||
@ApiProperty()
|
||||
metadata!: any;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
uid!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
recurringEventId!: string | null;
|
||||
|
||||
@IsUrl()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
location!: string | null;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => EventType)
|
||||
@ApiProperty({ type: EventType })
|
||||
eventType!: EventType;
|
||||
|
||||
@IsEnum(Status)
|
||||
status!: "CANCELLED" | "REJECTED" | "ACCEPTED" | "PENDING" | "AWAITING_HOST";
|
||||
@ApiProperty({ enum: Status, type: String })
|
||||
status!: Status;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty({ type: Boolean })
|
||||
paid!: boolean;
|
||||
|
||||
@IsArray()
|
||||
@ApiProperty()
|
||||
payment!: any[];
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => Reference)
|
||||
@IsArray()
|
||||
@ApiProperty({ type: [Reference] })
|
||||
references!: Reference[];
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty({ type: Boolean })
|
||||
isRecorded!: boolean;
|
||||
|
||||
@IsArray()
|
||||
@ApiProperty()
|
||||
seatsReferences!: any[];
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => User)
|
||||
@ApiProperty({ type: User })
|
||||
user!: User | null;
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
rescheduled?: any;
|
||||
}
|
||||
|
||||
@@ -206,17 +257,20 @@ class GetBookingsData_2024_04_15 {
|
||||
@ValidateNested()
|
||||
@Type(() => GetBookingsDataEntry)
|
||||
@IsArray()
|
||||
@ApiProperty({ type: [GetBookingsDataEntry] })
|
||||
bookings!: GetBookingsDataEntry[];
|
||||
|
||||
@IsArray()
|
||||
@ApiProperty()
|
||||
recurringInfo!: any[];
|
||||
|
||||
@IsInt()
|
||||
@ApiProperty({ type: Number, nullable: true })
|
||||
nextCursor!: number | null;
|
||||
}
|
||||
|
||||
export class GetBookingsOutput_2024_04_15 {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS], type: String })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsString, IsEnum, IsOptional, ValidateNested, IsArray, IsEmail, IsBoolean } from "class-validator";
|
||||
|
||||
@@ -6,24 +6,29 @@ import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
class Attendee {
|
||||
@IsEmail()
|
||||
@ApiProperty()
|
||||
email!: string;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty()
|
||||
noShow!: boolean;
|
||||
}
|
||||
|
||||
class HandleMarkNoShowData_2024_04_15 {
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
message!: string;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
noShowHost?: boolean;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => Attendee)
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: [Attendee] })
|
||||
attendees?: Attendee[];
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import {
|
||||
ArrayNotEmpty,
|
||||
IsBoolean,
|
||||
@@ -50,11 +50,10 @@ export class CreateIcsFeedInputDto {
|
||||
urls!: string[];
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty({
|
||||
@ApiPropertyOptional({
|
||||
example: false,
|
||||
description: "Whether to allowing writing to the calendar or not",
|
||||
type: "boolean",
|
||||
required: false,
|
||||
default: true,
|
||||
})
|
||||
@IsOptional()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsArray, IsDate, IsEnum, IsOptional, IsString, ValidateNested } from "class-validator";
|
||||
|
||||
@@ -6,13 +6,16 @@ import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
export class BusyTimesOutput {
|
||||
@IsDate()
|
||||
@ApiProperty({ type: Date })
|
||||
start!: Date;
|
||||
|
||||
@IsDate()
|
||||
@ApiProperty({ type: Date })
|
||||
end!: Date;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({ type: String, nullable: true })
|
||||
source?: string | null;
|
||||
}
|
||||
|
||||
@@ -24,5 +27,6 @@ export class GetBusyTimesOutput {
|
||||
@ValidateNested()
|
||||
@Type(() => BusyTimesOutput)
|
||||
@IsArray()
|
||||
@ApiProperty({ type: [BusyTimesOutput] })
|
||||
data!: BusyTimesOutput[];
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import {
|
||||
IsArray,
|
||||
@@ -18,195 +18,246 @@ import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
class Integration {
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
@ApiPropertyOptional({ type: Object, nullable: true })
|
||||
appData?: object | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
dirName?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
__template?: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
name!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
description!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional()
|
||||
installed?: boolean;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
type!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
title?: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
variant!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
category?: string;
|
||||
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
@ApiProperty({ type: [String] })
|
||||
categories!: string[];
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
logo!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
publisher!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
slug!: string;
|
||||
|
||||
@IsUrl()
|
||||
@ApiProperty()
|
||||
url!: string;
|
||||
|
||||
@IsEmail()
|
||||
@ApiProperty()
|
||||
email!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsObject()
|
||||
@ApiProperty({ type: Object, nullable: true })
|
||||
locationOption!: object | null;
|
||||
}
|
||||
|
||||
class Primary {
|
||||
@IsEmail()
|
||||
@ApiProperty()
|
||||
externalId!: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
integration?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEmail()
|
||||
@ApiPropertyOptional()
|
||||
name?: string;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty({ type: Boolean, nullable: true })
|
||||
primary!: boolean | null;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty()
|
||||
readOnly!: boolean;
|
||||
|
||||
@IsEmail()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
email?: string;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty()
|
||||
isSelected!: boolean;
|
||||
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
credentialId!: number;
|
||||
}
|
||||
|
||||
export class Calendar {
|
||||
@IsEmail()
|
||||
@ApiProperty()
|
||||
externalId!: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
integration?: string;
|
||||
|
||||
@IsEmail()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
name?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional({ type: Boolean, nullable: true })
|
||||
primary?: boolean | null;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty()
|
||||
readOnly!: boolean;
|
||||
|
||||
@IsEmail()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
email?: string;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty()
|
||||
isSelected!: boolean;
|
||||
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
credentialId!: number;
|
||||
}
|
||||
|
||||
export class ConnectedCalendar {
|
||||
@ValidateNested()
|
||||
@IsObject()
|
||||
@ApiProperty({ type: Integration })
|
||||
integration!: Integration;
|
||||
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
credentialId!: number;
|
||||
|
||||
@ValidateNested()
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: Primary })
|
||||
primary?: Primary;
|
||||
|
||||
@ValidateNested({ each: true })
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: [Calendar] })
|
||||
calendars?: Calendar[];
|
||||
}
|
||||
|
||||
class DestinationCalendar {
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
id!: number;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
integration!: string;
|
||||
|
||||
@IsEmail()
|
||||
@ApiProperty()
|
||||
externalId!: string;
|
||||
|
||||
@IsEmail()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
primaryEmail!: string | null;
|
||||
|
||||
@IsInt()
|
||||
@ApiProperty({ nullable: true })
|
||||
userId!: number | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@ApiProperty({ type: Number, nullable: true })
|
||||
eventTypeId!: number | null;
|
||||
|
||||
@IsInt()
|
||||
@ApiProperty({ type: Number, nullable: true })
|
||||
credentialId!: number | null;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: String, nullable: true })
|
||||
name?: string | null;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
primary?: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
readOnly?: boolean;
|
||||
|
||||
@IsEmail()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
email?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
integrationTitle?: string;
|
||||
}
|
||||
|
||||
export class ConnectedCalendarsData {
|
||||
@ValidateNested({ each: true })
|
||||
@IsArray()
|
||||
@ApiProperty({ type: [ConnectedCalendar] })
|
||||
connectedCalendars!: ConnectedCalendar[];
|
||||
|
||||
@ValidateNested()
|
||||
@IsObject()
|
||||
@ApiProperty({ type: DestinationCalendar })
|
||||
destinationCalendar!: DestinationCalendar;
|
||||
}
|
||||
|
||||
export class ConnectedCalendarsOutput {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
@@ -214,5 +265,6 @@ export class ConnectedCalendarsOutput {
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => ConnectedCalendarsData)
|
||||
@ApiProperty({ type: ConnectedCalendarsData })
|
||||
data!: ConnectedCalendarsData;
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ export class GoogleCalendarService implements OAuthCalendarApp {
|
||||
) {
|
||||
// User chose not to authorize your app or didn't authorize your app
|
||||
// redirect directly without oauth code
|
||||
if (!code) {
|
||||
if (!code || code === "undefined") {
|
||||
return { url: redir || origin };
|
||||
}
|
||||
|
||||
|
||||
@@ -151,7 +151,7 @@ export class OutlookService implements OAuthCalendarApp {
|
||||
redir?: string
|
||||
) {
|
||||
// if code is not defined, user denied to authorize office 365 app, just redirect straight away
|
||||
if (!code) {
|
||||
if (!code || code === "undefined") {
|
||||
return { url: redir || origin };
|
||||
}
|
||||
|
||||
|
||||
+18
-7
@@ -1,5 +1,5 @@
|
||||
import { EventTypeLocation_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/event-type-location.input";
|
||||
import { ApiProperty as DocsProperty, ApiHideProperty } from "@nestjs/swagger";
|
||||
import { ApiProperty as DocsProperty, ApiPropertyOptional, ApiHideProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import {
|
||||
IsString,
|
||||
@@ -36,7 +36,10 @@ export class CreateEventTypeInput_2024_04_15 {
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@DocsProperty({ example: CREATE_EVENT_DESCRIPTION_EXAMPLE })
|
||||
@ApiPropertyOptional({
|
||||
type: String,
|
||||
example: CREATE_EVENT_DESCRIPTION_EXAMPLE,
|
||||
})
|
||||
description?: string;
|
||||
|
||||
@IsOptional()
|
||||
@@ -48,30 +51,38 @@ export class CreateEventTypeInput_2024_04_15 {
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => EventTypeLocation_2024_04_15)
|
||||
@IsArray()
|
||||
@ApiPropertyOptional({
|
||||
type: [EventTypeLocation_2024_04_15],
|
||||
})
|
||||
locations?: EventTypeLocation_2024_04_15[];
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional()
|
||||
disableGuests?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
slotInterval?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
minimumBookingNotice?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
beforeEventBuffer?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
afterEventBuffer?: number;
|
||||
|
||||
// @ApiHideProperty()
|
||||
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty as DocsProperty, ApiHideProperty } from "@nestjs/swagger";
|
||||
import { ApiProperty, ApiHideProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { IsString, IsNumber, IsBoolean, IsOptional, IsUrl } from "class-validator";
|
||||
|
||||
// note(Lauris): We will gradually expose more properties if any customer needs them.
|
||||
@@ -6,7 +6,7 @@ import { IsString, IsNumber, IsBoolean, IsOptional, IsUrl } from "class-validato
|
||||
|
||||
export class EventTypeLocation_2024_04_15 {
|
||||
@IsString()
|
||||
@DocsProperty({ example: "link" })
|
||||
@ApiProperty({ example: "link" })
|
||||
type!: string;
|
||||
|
||||
@IsOptional()
|
||||
@@ -16,7 +16,7 @@ export class EventTypeLocation_2024_04_15 {
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
@DocsProperty({ example: "https://masterchief.com/argentina/flan/video/9129412" })
|
||||
@ApiPropertyOptional({ example: "https://masterchief.com/argentina/flan/video/9129412" })
|
||||
link?: string;
|
||||
|
||||
@IsOptional()
|
||||
|
||||
+4
-4
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Transform } from "class-transformer";
|
||||
import { IsBoolean, IsOptional, IsString } from "class-validator";
|
||||
|
||||
@@ -6,11 +6,11 @@ export class GetPublicEventTypeQueryParams_2024_04_15 {
|
||||
@Transform(({ value }: { value: string }) => value === "true")
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@ApiProperty({ required: false })
|
||||
@ApiPropertyOptional()
|
||||
isTeamEvent?: boolean;
|
||||
|
||||
@ApiProperty({ required: false })
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({ type: String, nullable: true })
|
||||
org?: string | null;
|
||||
}
|
||||
|
||||
+66
@@ -2,6 +2,7 @@ import { Editable } from "@/ee/event-types/event-types_2024_04_15/inputs/enums/e
|
||||
import { BaseField } from "@/ee/event-types/event-types_2024_04_15/inputs/enums/field-type";
|
||||
import { Frequency } from "@/ee/event-types/event-types_2024_04_15/inputs/enums/frequency";
|
||||
import { EventTypeLocation_2024_04_15 } from "@/ee/event-types/event-types_2024_04_15/inputs/event-type-location.input";
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import {
|
||||
IsString,
|
||||
@@ -21,206 +22,259 @@ import {
|
||||
|
||||
class Option {
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
value!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
label!: string;
|
||||
}
|
||||
|
||||
class Source {
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
id!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
type!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
label!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
editUrl?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional()
|
||||
fieldRequired?: boolean;
|
||||
}
|
||||
|
||||
class View {
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
id!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
label!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
description?: string;
|
||||
}
|
||||
|
||||
class OptionsInput {
|
||||
@IsString()
|
||||
@ApiProperty({
|
||||
description: 'Type of the field, can be one of "address", "text", or "phone".',
|
||||
enum: ["address", "text", "phone"],
|
||||
example: "text",
|
||||
})
|
||||
type!: "address" | "text" | "phone";
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional()
|
||||
required?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
class VariantField {
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
type!: BaseField;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
name!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
label?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
labelAsSafeHtml?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
placeholder?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional()
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
class Variant {
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => VariantField)
|
||||
@ApiProperty({ type: [VariantField] })
|
||||
fields!: VariantField[];
|
||||
}
|
||||
|
||||
class VariantsConfig {
|
||||
@ApiProperty({ type: Object })
|
||||
variants!: Record<string, Variant>;
|
||||
}
|
||||
|
||||
export class BookingField_2024_04_15 {
|
||||
@IsEnum(BaseField)
|
||||
@ApiProperty({ enum: BaseField })
|
||||
type!: BaseField;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
name!: string;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => Option)
|
||||
@ApiPropertyOptional({ type: [Option] })
|
||||
options?: Option[];
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
label?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
labelAsSafeHtml?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
defaultLabel?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
placeholder?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional()
|
||||
required?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
getOptionsAt?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => OptionsInput)
|
||||
@ApiPropertyOptional()
|
||||
optionsInputs?: Record<string, OptionsInput>;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
variant?: string;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => VariantsConfig)
|
||||
@ApiPropertyOptional({ type: VariantsConfig })
|
||||
variantsConfig?: VariantsConfig;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => View)
|
||||
@ApiPropertyOptional({ type: [View] })
|
||||
views?: View[];
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional()
|
||||
hideWhenJustOneOption?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional()
|
||||
hidden?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(Editable)
|
||||
@ApiPropertyOptional()
|
||||
editable?: Editable;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => Source)
|
||||
@ApiPropertyOptional({ type: [Source] })
|
||||
sources?: Source[];
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional()
|
||||
disableOnPrefill?: boolean;
|
||||
}
|
||||
|
||||
export class RecurringEvent_2024_04_15 {
|
||||
@IsDate()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: Date })
|
||||
dtstart?: Date;
|
||||
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
interval!: number;
|
||||
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
count!: number;
|
||||
|
||||
@IsEnum(Frequency)
|
||||
@ApiProperty({ enum: Frequency })
|
||||
freq!: Frequency;
|
||||
|
||||
@IsDate()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: Date })
|
||||
until?: Date;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
tzid?: string;
|
||||
}
|
||||
|
||||
export class IntervalLimits_2024_04_15 {
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
PER_DAY?: number;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
PER_WEEK?: number;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
PER_MONTH?: number;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
PER_YEAR?: number;
|
||||
}
|
||||
|
||||
@@ -228,27 +282,33 @@ export class UpdateEventTypeInput_2024_04_15 {
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
length?: number;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
slug?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
title?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
description?: string;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
hidden?: boolean;
|
||||
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => EventTypeLocation_2024_04_15)
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: [EventTypeLocation_2024_04_15] })
|
||||
locations?: EventTypeLocation_2024_04_15[];
|
||||
|
||||
// @IsInt()
|
||||
@@ -283,6 +343,7 @@ export class UpdateEventTypeInput_2024_04_15 {
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => BookingField_2024_04_15)
|
||||
@ApiPropertyOptional({ type: [BookingField_2024_04_15] })
|
||||
bookingFields?: BookingField_2024_04_15[];
|
||||
|
||||
// @IsString()
|
||||
@@ -328,6 +389,7 @@ export class UpdateEventTypeInput_2024_04_15 {
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
disableGuests?: boolean;
|
||||
|
||||
// @IsBoolean()
|
||||
@@ -337,16 +399,19 @@ export class UpdateEventTypeInput_2024_04_15 {
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
minimumBookingNotice?: number;
|
||||
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
beforeEventBuffer?: number;
|
||||
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
afterEventBuffer?: number;
|
||||
|
||||
// @IsInt()
|
||||
@@ -384,6 +449,7 @@ export class UpdateEventTypeInput_2024_04_15 {
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
slotInterval?: number;
|
||||
|
||||
// @IsString()
|
||||
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
IntervalLimits_2024_04_15,
|
||||
RecurringEvent_2024_04_15,
|
||||
} from "@/ee/event-types/event-types_2024_04_15/inputs/update-event-type.input";
|
||||
import { ApiProperty as DocsProperty, ApiHideProperty } from "@nestjs/swagger";
|
||||
import { ApiHideProperty, ApiProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import {
|
||||
IsArray,
|
||||
@@ -29,23 +29,23 @@ import {
|
||||
|
||||
export class EventTypeOutput {
|
||||
@IsInt()
|
||||
@DocsProperty({ example: 1 })
|
||||
@ApiProperty({ example: 1 })
|
||||
id!: number;
|
||||
|
||||
@IsInt()
|
||||
@DocsProperty({ example: CREATE_EVENT_LENGTH_EXAMPLE })
|
||||
@ApiProperty({ example: CREATE_EVENT_LENGTH_EXAMPLE })
|
||||
length!: number;
|
||||
|
||||
@IsString()
|
||||
@DocsProperty({ example: CREATE_EVENT_SLUG_EXAMPLE })
|
||||
@ApiProperty({ example: CREATE_EVENT_SLUG_EXAMPLE })
|
||||
slug!: string;
|
||||
|
||||
@IsString()
|
||||
@DocsProperty({ example: CREATE_EVENT_TITLE_EXAMPLE })
|
||||
@ApiProperty({ example: CREATE_EVENT_TITLE_EXAMPLE })
|
||||
title!: string;
|
||||
|
||||
@IsString()
|
||||
@DocsProperty({ example: CREATE_EVENT_DESCRIPTION_EXAMPLE })
|
||||
@ApiProperty({ example: CREATE_EVENT_DESCRIPTION_EXAMPLE, nullable: true })
|
||||
description!: string | null;
|
||||
|
||||
@IsBoolean()
|
||||
@@ -55,6 +55,7 @@ export class EventTypeOutput {
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => EventTypeLocation_2024_04_15)
|
||||
@IsArray()
|
||||
@ApiProperty({ type: [EventTypeLocation_2024_04_15], nullable: true })
|
||||
locations!: EventTypeLocation_2024_04_15[] | null;
|
||||
|
||||
@IsInt()
|
||||
|
||||
+94
-3
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import {
|
||||
IsBoolean,
|
||||
@@ -17,330 +17,421 @@ import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
class Location {
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
type!: string;
|
||||
}
|
||||
|
||||
class Source {
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
id!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
type!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
label!: string;
|
||||
}
|
||||
|
||||
class OptionInput {
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
type!: string;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
required?: boolean;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
class BookingField {
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
name!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
type!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
defaultLabel?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
label?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
placeholder?: string;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
required?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
getOptionsAt?: string;
|
||||
|
||||
@IsObject()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
optionsInputs?: { [key: string]: OptionInput };
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
hideWhenJustOneOption?: boolean;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
editable?: string;
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => Source)
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: [Source] })
|
||||
sources?: Source[];
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional()
|
||||
disableOnPrefill?: boolean;
|
||||
}
|
||||
|
||||
class Organization {
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
id!: number;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: String, nullable: true })
|
||||
slug?: string | null;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
name!: string;
|
||||
|
||||
@IsOptional()
|
||||
metadata!: Record<string, any>;
|
||||
@ApiPropertyOptional({ type: Object })
|
||||
metadata?: Record<string, any>;
|
||||
}
|
||||
|
||||
class Profile {
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
username!: string | null;
|
||||
|
||||
@IsInt()
|
||||
@ApiProperty({ type: Number, nullable: true })
|
||||
id!: number | null;
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
userId?: number;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
uid?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
name?: string;
|
||||
|
||||
@IsInt()
|
||||
@ApiProperty({ type: Number, nullable: true })
|
||||
organizationId!: number | null;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => Organization)
|
||||
@ApiPropertyOptional({ type: Organization, nullable: true })
|
||||
organization?: Organization | null;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
upId!: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
image?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
brandColor?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
darkBrandColor?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
theme?: string;
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
bookerLayouts?: any;
|
||||
}
|
||||
|
||||
class Owner {
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
id!: number;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: String, nullable: true })
|
||||
avatarUrl?: string | null;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
username!: string | null;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
name!: string | null;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
weekStart!: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: String, nullable: true })
|
||||
brandColor?: string | null;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: String, nullable: true })
|
||||
darkBrandColor?: string | null;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: String, nullable: true })
|
||||
theme?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
metadata!: any;
|
||||
@ApiPropertyOptional()
|
||||
metadata?: any;
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: Number, nullable: true })
|
||||
defaultScheduleId?: number | null;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
nonProfileUsername!: string | null;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => Profile)
|
||||
@ApiProperty({ type: Profile })
|
||||
profile!: Profile;
|
||||
}
|
||||
|
||||
class User {
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
username!: string | null;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
name!: string | null;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
weekStart!: string;
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
organizationId?: number;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: String, nullable: true })
|
||||
avatarUrl?: string | null;
|
||||
|
||||
@ValidateNested()
|
||||
@ApiProperty({ type: Profile })
|
||||
profile!: Profile;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
bookerUrl!: string;
|
||||
}
|
||||
|
||||
class Schedule {
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
id!: number;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty({ type: String, nullable: true })
|
||||
timeZone!: string | null;
|
||||
}
|
||||
|
||||
class PublicEventTypeOutput {
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
id!: number;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
title!: string;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
description!: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: String, nullable: true })
|
||||
eventName?: string | null;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
slug!: string;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty()
|
||||
isInstantEvent!: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
aiPhoneCallConfig?: any;
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
schedulingType?: any;
|
||||
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
length!: number;
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => Location)
|
||||
@ApiProperty({ type: [Location] })
|
||||
locations!: Location[];
|
||||
|
||||
@IsArray()
|
||||
@ApiProperty({ type: [Object] })
|
||||
customInputs!: any[];
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty()
|
||||
disableGuests!: boolean;
|
||||
|
||||
@IsObject()
|
||||
@ApiProperty({ type: Object, nullable: true })
|
||||
metadata!: object | null;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty()
|
||||
lockTimeZoneToggleOnBookingPage!: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty()
|
||||
requiresConfirmation!: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty()
|
||||
requiresBookerEmailVerification!: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
recurringEvent?: any;
|
||||
|
||||
@IsNumber()
|
||||
@ApiProperty()
|
||||
price!: number;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
currency!: string;
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: Number, nullable: true })
|
||||
seatsPerTimeSlot?: number | null;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty({ type: Boolean, nullable: true })
|
||||
seatsShowAvailabilityCount!: boolean | null;
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => BookingField)
|
||||
@ApiProperty({ type: [BookingField] })
|
||||
bookingFields!: BookingField[];
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
team?: any;
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
@ApiPropertyOptional({ type: String, nullable: true })
|
||||
successRedirectUrl?: string | null;
|
||||
|
||||
@IsArray()
|
||||
@ApiProperty()
|
||||
workflows!: any[];
|
||||
|
||||
@IsArray()
|
||||
@ApiProperty()
|
||||
hosts!: any[];
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => Owner)
|
||||
@ApiProperty({ type: Owner, nullable: true })
|
||||
owner!: Owner | null;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => Schedule)
|
||||
@ApiProperty({ type: Schedule, nullable: true })
|
||||
schedule!: Schedule | null;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty()
|
||||
hidden!: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty()
|
||||
assignAllTeamMembers!: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
bookerLayouts?: any;
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => User)
|
||||
@ApiProperty({ type: [User] })
|
||||
users!: User[];
|
||||
|
||||
@IsObject()
|
||||
@ApiProperty({ type: Object })
|
||||
entity!: object;
|
||||
|
||||
@IsBoolean()
|
||||
|
||||
+7
-7
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty as DocsProperty } from "@nestjs/swagger";
|
||||
import { ApiProperty as DocsProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Transform } from "class-transformer";
|
||||
import { IsString, IsBoolean, IsOptional, IsEnum, Matches } from "class-validator";
|
||||
|
||||
@@ -38,34 +38,34 @@ export class CreatePhoneCallInput {
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@DocsProperty({ description: "Scheduler name" })
|
||||
@ApiPropertyOptional({ description: "Scheduler name" })
|
||||
schedulerName?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Transform(({ value }) => (value ? value : undefined))
|
||||
@DocsProperty({ description: "Guest name" })
|
||||
@ApiPropertyOptional({ description: "Guest name" })
|
||||
guestName?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Transform(({ value }) => (value ? value : undefined))
|
||||
@DocsProperty({ description: "Guest email" })
|
||||
@ApiPropertyOptional({ description: "Guest email" })
|
||||
guestEmail?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Transform(({ value }) => (value ? value : undefined))
|
||||
@DocsProperty({ description: "Guest company" })
|
||||
@ApiPropertyOptional({ description: "Guest company" })
|
||||
guestCompany?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@DocsProperty({ description: "Begin message" })
|
||||
@ApiPropertyOptional({ description: "Begin message" })
|
||||
beginMessage?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@DocsProperty({ description: "General prompt" })
|
||||
@ApiPropertyOptional({ description: "General prompt" })
|
||||
generalPrompt?: string;
|
||||
}
|
||||
|
||||
@@ -1,39 +1,51 @@
|
||||
import { ApiProperty as DocsProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsInt, IsEmail, IsOptional, IsString, ValidateNested } from "class-validator";
|
||||
import { IsString, IsOptional, IsInt, IsEmail, ValidateNested } from "class-validator";
|
||||
|
||||
export class MeOrgOutput {
|
||||
@DocsProperty()
|
||||
isPlatform!: boolean;
|
||||
|
||||
@DocsProperty()
|
||||
id!: number;
|
||||
}
|
||||
|
||||
export class MeOutput {
|
||||
@IsInt()
|
||||
@DocsProperty()
|
||||
id!: number;
|
||||
|
||||
@IsString()
|
||||
@DocsProperty()
|
||||
username!: string;
|
||||
|
||||
@IsEmail()
|
||||
@DocsProperty()
|
||||
email!: string;
|
||||
|
||||
@IsInt()
|
||||
@DocsProperty()
|
||||
timeFormat!: number;
|
||||
|
||||
@IsInt()
|
||||
@IsOptional()
|
||||
@DocsProperty({ type: Number, nullable: true })
|
||||
defaultScheduleId!: number | null;
|
||||
|
||||
@IsString()
|
||||
@DocsProperty()
|
||||
weekStart!: string;
|
||||
|
||||
@IsString()
|
||||
@DocsProperty()
|
||||
timeZone!: string;
|
||||
|
||||
@IsInt()
|
||||
@DocsProperty({ type: Number, nullable: true })
|
||||
organizationId!: number | null;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested()
|
||||
@Type(() => MeOrgOutput)
|
||||
@ApiPropertyOptional({ type: MeOrgOutput })
|
||||
organization?: MeOrgOutput;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
import { CreateAvailabilityInput_2024_04_15 } from "@/ee/schedules/schedules_2024_04_15/inputs/create-availability.input";
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsArray, IsBoolean, IsTimeZone, IsOptional, IsString, ValidateNested } from "class-validator";
|
||||
|
||||
export class CreateScheduleInput_2024_04_15 {
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
name!: string;
|
||||
|
||||
@IsTimeZone()
|
||||
@ApiProperty()
|
||||
timeZone!: string;
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => CreateAvailabilityInput_2024_04_15)
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: [CreateAvailabilityInput_2024_04_15] })
|
||||
availabilities?: CreateAvailabilityInput_2024_04_15[];
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty()
|
||||
isDefault!: boolean;
|
||||
}
|
||||
|
||||
+25
-3
@@ -1,95 +1,117 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsBoolean, IsInt, IsOptional, IsString, ValidateNested, IsArray } from "class-validator";
|
||||
|
||||
class EventTypeModel_2024_04_15 {
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
id!: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({ type: String, nullable: true })
|
||||
eventName?: string | null;
|
||||
}
|
||||
|
||||
class AvailabilityModel_2024_04_15 {
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
id!: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@ApiPropertyOptional({ type: Number, nullable: true })
|
||||
userId?: number | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@ApiPropertyOptional({ type: Number, nullable: true })
|
||||
scheduleId?: number | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@ApiPropertyOptional({ type: Number, nullable: true })
|
||||
eventTypeId?: number | null;
|
||||
|
||||
@IsArray()
|
||||
@IsInt({ each: true })
|
||||
@ApiProperty({ type: [Number] })
|
||||
days!: number[];
|
||||
|
||||
@IsOptional()
|
||||
@Type(() => Date)
|
||||
@IsString() // Assuming date is serialized/deserialized appropriately
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
startTime?: Date;
|
||||
|
||||
@IsOptional()
|
||||
@Type(() => Date)
|
||||
@IsString() // Assuming date is serialized/deserialized appropriately
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
endTime?: Date;
|
||||
|
||||
@IsOptional()
|
||||
@Type(() => Date)
|
||||
@IsString() // Assuming date is serialized/deserialized appropriately
|
||||
@IsString()
|
||||
@ApiPropertyOptional({ type: String, nullable: true })
|
||||
date?: Date | null;
|
||||
}
|
||||
|
||||
class ScheduleModel_2024_04_15 {
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
id!: number;
|
||||
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
userId!: number;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
name!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({ type: String, nullable: true })
|
||||
timeZone?: string | null;
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => EventTypeModel_2024_04_15)
|
||||
@IsArray()
|
||||
@ApiPropertyOptional({ type: [EventTypeModel_2024_04_15] })
|
||||
eventType?: EventTypeModel_2024_04_15[];
|
||||
|
||||
@IsOptional()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => AvailabilityModel_2024_04_15)
|
||||
@IsArray()
|
||||
@ApiPropertyOptional({ type: [AvailabilityModel_2024_04_15] })
|
||||
availability?: AvailabilityModel_2024_04_15[];
|
||||
}
|
||||
|
||||
export class UpdatedScheduleOutput_2024_04_15 {
|
||||
@ValidateNested()
|
||||
@Type(() => ScheduleModel_2024_04_15)
|
||||
@ApiProperty({ type: ScheduleModel_2024_04_15 })
|
||||
schedule!: ScheduleModel_2024_04_15;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty()
|
||||
isDefault!: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
timeZone?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@ApiPropertyOptional({ type: Number, nullable: true })
|
||||
prevDefaultId?: number | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@ApiPropertyOptional({ type: Number, nullable: true })
|
||||
currentDefaultId?: number | null;
|
||||
}
|
||||
|
||||
@@ -1,105 +1,132 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsDate, IsOptional, IsArray, IsBoolean, IsInt, IsString, ValidateNested } from "class-validator";
|
||||
|
||||
class AvailabilityModel {
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
id!: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@ApiPropertyOptional({ type: Number, nullable: true })
|
||||
userId?: number | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@ApiPropertyOptional({ type: Number, nullable: true })
|
||||
eventTypeId?: number | null;
|
||||
|
||||
@IsArray()
|
||||
@IsInt({ each: true })
|
||||
@ApiProperty({ type: [Number] })
|
||||
days!: number[];
|
||||
|
||||
@IsDate()
|
||||
@Type(() => Date)
|
||||
@ApiProperty({ type: Date })
|
||||
startTime!: Date;
|
||||
|
||||
@IsDate()
|
||||
@Type(() => Date)
|
||||
@ApiProperty({ type: Date })
|
||||
endTime!: Date;
|
||||
|
||||
@IsOptional()
|
||||
@IsDate()
|
||||
@Type(() => Date)
|
||||
@ApiPropertyOptional({ type: Date, nullable: true })
|
||||
date?: Date | null;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@ApiPropertyOptional({ type: Number, nullable: true })
|
||||
scheduleId?: number | null;
|
||||
}
|
||||
|
||||
class WorkingHours {
|
||||
@IsArray()
|
||||
@IsInt({ each: true })
|
||||
@ApiProperty({ type: [Number] })
|
||||
days!: number[];
|
||||
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
startTime!: number;
|
||||
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
endTime!: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@ApiPropertyOptional({ type: Number, nullable: true })
|
||||
userId?: number | null;
|
||||
}
|
||||
|
||||
class TimeRange {
|
||||
@IsOptional()
|
||||
@IsInt()
|
||||
@ApiPropertyOptional({ type: Number, nullable: true })
|
||||
userId?: number | null;
|
||||
|
||||
@IsDate()
|
||||
@ApiProperty({ type: Date })
|
||||
start!: Date;
|
||||
|
||||
@IsDate()
|
||||
@ApiProperty({ type: Date })
|
||||
end!: Date;
|
||||
}
|
||||
|
||||
export class ScheduleOutput {
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
id!: number;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
name!: string;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty()
|
||||
isManaged!: boolean;
|
||||
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => WorkingHours)
|
||||
@ApiProperty({ type: [WorkingHours] })
|
||||
workingHours!: WorkingHours[];
|
||||
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => AvailabilityModel)
|
||||
@IsArray()
|
||||
@ApiProperty({ type: [AvailabilityModel] })
|
||||
schedule!: AvailabilityModel[];
|
||||
|
||||
@ApiProperty({ type: [[TimeRange]] })
|
||||
availability!: TimeRange[][];
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
timeZone!: string;
|
||||
|
||||
@ValidateNested({ each: true })
|
||||
@IsArray()
|
||||
@ApiPropertyOptional({ type: [Object] })
|
||||
// note(Lauris) it should be
|
||||
// dateOverrides!: { ranges: TimeRange[] }[];
|
||||
// but docs aren't generating correctly it results in array of strings
|
||||
dateOverrides!: unknown[];
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty()
|
||||
isDefault!: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty()
|
||||
isLastSchedule!: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty()
|
||||
readOnly!: boolean;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -9,7 +9,13 @@ import {
|
||||
} from "@prisma/client/runtime/library";
|
||||
import { Request } from "express";
|
||||
|
||||
import { ERROR_STATUS, INTERNAL_SERVER_ERROR } from "@calcom/platform-constants";
|
||||
import {
|
||||
BAD_REQUEST,
|
||||
CONFLICT,
|
||||
ERROR_STATUS,
|
||||
INTERNAL_SERVER_ERROR,
|
||||
NOT_FOUND,
|
||||
} from "@calcom/platform-constants";
|
||||
import { Response } from "@calcom/platform-types";
|
||||
|
||||
type PrismaError =
|
||||
@@ -43,11 +49,37 @@ export class PrismaExceptionFilter implements ExceptionFilter {
|
||||
method: request.method,
|
||||
requestId,
|
||||
});
|
||||
response.status(HttpStatus.INTERNAL_SERVER_ERROR).json({
|
||||
|
||||
let message = "There was an error, please try again later.";
|
||||
let statusCode = HttpStatus.INTERNAL_SERVER_ERROR;
|
||||
let errorCode = INTERNAL_SERVER_ERROR;
|
||||
if (error instanceof PrismaClientKnownRequestError) {
|
||||
switch (error.code) {
|
||||
case "P2002": // Unique constraint failed
|
||||
errorCode = CONFLICT;
|
||||
statusCode = HttpStatus.CONFLICT;
|
||||
message = "Invalid Input: Trying to create a record that already exists.";
|
||||
break;
|
||||
case "P2025": // Record not found
|
||||
errorCode = NOT_FOUND;
|
||||
statusCode = HttpStatus.NOT_FOUND;
|
||||
message = "Invalid Query: The requested record was not found.";
|
||||
break;
|
||||
case "P2003": // Foreign key constraint failed
|
||||
errorCode = BAD_REQUEST;
|
||||
statusCode = HttpStatus.BAD_REQUEST;
|
||||
message = "Invalid input: The referenced data does not exist.";
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
response.status(statusCode).json({
|
||||
status: ERROR_STATUS,
|
||||
timestamp: new Date().toISOString(),
|
||||
path: request.url,
|
||||
error: { code: INTERNAL_SERVER_ERROR, message: "There was an error, please try again later." },
|
||||
error: { code: errorCode, message: message },
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { isOriginAllowed } from "@/lib/is-origin-allowed/is-origin-allowed";
|
||||
|
||||
describe("isOriginAllowed", () => {
|
||||
describe("is allowed", () => {
|
||||
it("should return true for exact match without wildcard", () => {
|
||||
const allowedOrigins = ["https://app.cal.com/callback"];
|
||||
const origin = "https://app.cal.com/callback";
|
||||
expect(isOriginAllowed(origin, allowedOrigins)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return true for wildcard domain match", () => {
|
||||
const allowedOrigins = ["*.multiscreen.d1test.biz"];
|
||||
const origin = "https://sub.multiscreen.d1test.biz";
|
||||
expect(isOriginAllowed(origin, allowedOrigins)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return true for wildcard pattern matching any domain", () => {
|
||||
const allowedOrigins = ["*/callback"];
|
||||
const origin = "https://another.com/callback";
|
||||
expect(isOriginAllowed(origin, allowedOrigins)).toBe(true);
|
||||
});
|
||||
|
||||
it("should handle a wildcard only pattern", () => {
|
||||
const allowedOrigins = ["*"];
|
||||
const origin = "https://any.domain.com/anypath";
|
||||
expect(isOriginAllowed(origin, allowedOrigins)).toBe(true);
|
||||
});
|
||||
|
||||
it("should handle multiple allowed URIs where only one matches", () => {
|
||||
const allowedOrigins = [
|
||||
"https://app.cal.com/other",
|
||||
"*.notthisone.com",
|
||||
"https://app.cal.com/callback",
|
||||
];
|
||||
const origin = "https://app.cal.com/callback";
|
||||
expect(isOriginAllowed(origin, allowedOrigins)).toBe(true);
|
||||
});
|
||||
|
||||
it("should handle patterns with multiple wildcards correctly", () => {
|
||||
const allowedOrigins = ["https://*.mydomain.com/*"];
|
||||
const origin = "https://sub.mydomain.com/path/to/resource";
|
||||
expect(isOriginAllowed(origin, allowedOrigins)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("is not allowed", () => {
|
||||
it("should return false if no allowed patterns match", () => {
|
||||
const allowedOrigins = ["https://app.cal.com/callback", "*.multiscreen.d1test.biz", "*/callback"];
|
||||
const origin = "https://unknown.com";
|
||||
expect(isOriginAllowed(origin, allowedOrigins)).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false if wildcard pattern doesn't match the given origin", () => {
|
||||
const allowedOrigins = ["*.example.com"];
|
||||
const origin = "https://notexample.org";
|
||||
expect(isOriginAllowed(origin, allowedOrigins)).toBe(false);
|
||||
});
|
||||
|
||||
it("should handle empty allowedUris array correctly", () => {
|
||||
const allowedOrigins: string[] = [];
|
||||
const origin = "https://app.cal.com/callback";
|
||||
expect(isOriginAllowed(origin, allowedOrigins)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
export function isOriginAllowed(origin: string, allowedOrigins: string[]): boolean {
|
||||
return allowedOrigins.some((allowedOrigin) => {
|
||||
if (allowedOrigin.includes("*")) {
|
||||
return wildcardToRegex(allowedOrigin).test(origin);
|
||||
}
|
||||
return origin === allowedOrigin;
|
||||
});
|
||||
}
|
||||
|
||||
function wildcardToRegex(pattern: string): RegExp {
|
||||
const escaped = escapeRegex(pattern);
|
||||
const regexPattern = "^" + escaped.replace(/\\\*/g, ".*") + "$";
|
||||
return new RegExp(regexPattern);
|
||||
}
|
||||
|
||||
function escapeRegex(str: string): string {
|
||||
return str.replace(/[.+*?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Transform, Type } from "class-transformer";
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Transform } from "class-transformer";
|
||||
import { ArrayNotEmpty, IsArray, IsInt, IsNumber, IsOptional } from "class-validator";
|
||||
|
||||
export class EventTypesAppInput {
|
||||
@Transform(({ value }: { value: string }) => value && parseInt(value))
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
teamId?: number;
|
||||
}
|
||||
|
||||
@@ -12,5 +14,6 @@ export class BulkUpdateEventTypeToDefaultLocationDto {
|
||||
@IsArray()
|
||||
@ArrayNotEmpty()
|
||||
@IsInt({ each: true })
|
||||
@ApiProperty({ type: [Number] })
|
||||
eventTypeIds!: number[];
|
||||
}
|
||||
|
||||
@@ -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[] = [];
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { hashAPIKey, isApiKey, stripApiKey } from "@/lib/api-key";
|
||||
import { AuthMethods } from "@/lib/enums/auth-methods";
|
||||
import { isOriginAllowed } from "@/lib/is-origin-allowed/is-origin-allowed";
|
||||
import { BaseStrategy } from "@/lib/passport/strategies/types";
|
||||
import { ApiKeyRepository } from "@/modules/api-key/api-key-repository";
|
||||
import { DeploymentsService } from "@/modules/deployments/deployments.service";
|
||||
@@ -167,7 +168,7 @@ export class ApiAuthStrategy extends PassportStrategy(BaseStrategy, "api-auth")
|
||||
throw new UnauthorizedException("OAuth client not found given the access token");
|
||||
}
|
||||
|
||||
if (origin && !client.redirectUris.some((uri) => uri.startsWith(origin))) {
|
||||
if (origin && !isOriginAllowed(origin, client.redirectUris)) {
|
||||
throw new UnauthorizedException(
|
||||
`Invalid request origin - please open https://app.cal.com/settings/platform and add the origin '${origin}' to the 'Redirect uris' of your OAuth client.`
|
||||
);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Expose, Type } from "class-transformer";
|
||||
import { IsString, ValidateNested, IsEnum, IsNumber, IsOptional, IsBoolean } from "class-validator";
|
||||
|
||||
@@ -20,7 +20,11 @@ export class ConferencingAppsOutputDto {
|
||||
@IsNumber()
|
||||
userId!: number;
|
||||
|
||||
@ApiProperty({ example: true, description: "Whether if the connection is working or not." })
|
||||
@ApiPropertyOptional({
|
||||
example: true,
|
||||
description: "Whether if the connection is working or not.",
|
||||
nullable: true,
|
||||
})
|
||||
@Expose()
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@@ -28,24 +32,26 @@ export class ConferencingAppsOutputDto {
|
||||
}
|
||||
|
||||
export class ConferencingAppsOutputResponseDto {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@ApiProperty({ type: String, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@Expose()
|
||||
@ValidateNested()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => ConferencingAppsOutputDto)
|
||||
@ApiProperty({ type: [ConferencingAppsOutputDto] })
|
||||
data!: ConferencingAppsOutputDto[];
|
||||
}
|
||||
|
||||
export class ConferencingAppOutputResponseDto {
|
||||
@ApiProperty({ example: SUCCESS_STATUS, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@ApiProperty({ type: String, enum: [SUCCESS_STATUS, ERROR_STATUS] })
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@Expose()
|
||||
@ValidateNested()
|
||||
@Type(() => ConferencingAppsOutputDto)
|
||||
@ApiProperty({ type: ConferencingAppsOutputDto })
|
||||
data!: ConferencingAppsOutputDto;
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
+7
-7
@@ -10,10 +10,10 @@ export class ManagedUserOutput {
|
||||
@ApiProperty({ example: "alice+cluo37fwd0001khkzqqynkpj3@example.com" })
|
||||
email!: string;
|
||||
|
||||
@ApiProperty({ example: "alice" })
|
||||
@ApiProperty({ example: "alice", nullable: true })
|
||||
username!: string | null;
|
||||
|
||||
@ApiProperty({ example: "alice" })
|
||||
@ApiProperty({ example: "alice", nullable: true })
|
||||
name!: string | null;
|
||||
|
||||
@ApiProperty({ example: "America/New_York" })
|
||||
@@ -22,19 +22,19 @@ export class ManagedUserOutput {
|
||||
@ApiProperty({ example: "Sunday" })
|
||||
weekStart!: string;
|
||||
|
||||
@ApiProperty({ example: "2024-04-01T00:00:00.000Z", type: "string" })
|
||||
@ApiProperty({ type: String, example: "2024-04-01T00:00:00.000Z" })
|
||||
@Transform(({ value }) => value.toISOString())
|
||||
createdDate!: Date;
|
||||
|
||||
@ApiProperty({ example: 12, nullable: true })
|
||||
@ApiProperty({ type: Number, example: 12, nullable: true })
|
||||
timeFormat!: number | null;
|
||||
|
||||
@ApiProperty({ example: null })
|
||||
@ApiProperty({ type: Number, example: null, nullable: true })
|
||||
defaultScheduleId!: number | null;
|
||||
|
||||
@IsEnum(Locales)
|
||||
@IsOptional()
|
||||
@ApiProperty({ example: Locales.EN, enum: Locales })
|
||||
@ApiPropertyOptional({ type: String, example: Locales.EN, enum: Locales })
|
||||
locale?: Locales;
|
||||
|
||||
@IsUrl()
|
||||
@@ -43,7 +43,7 @@ export class ManagedUserOutput {
|
||||
type: String,
|
||||
example: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png",
|
||||
description: `URL of the user's avatar image`,
|
||||
required: false,
|
||||
nullable: true,
|
||||
})
|
||||
avatarUrl?: string | null;
|
||||
}
|
||||
|
||||
+3
-5
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import {
|
||||
IsArray,
|
||||
@@ -30,7 +30,7 @@ export class PlatformOAuthClientDto {
|
||||
@IsNumber()
|
||||
permissions!: number;
|
||||
|
||||
@ApiProperty({ example: "https://example.com/logo.png", required: false })
|
||||
@ApiPropertyOptional({ example: "https://example.com/logo.png" })
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
logo!: string | null;
|
||||
@@ -54,9 +54,7 @@ export class GetOAuthClientResponseDto {
|
||||
@IsEnum([SUCCESS_STATUS, ERROR_STATUS])
|
||||
status!: typeof SUCCESS_STATUS | typeof ERROR_STATUS;
|
||||
|
||||
@ApiProperty({
|
||||
type: PlatformOAuthClientDto,
|
||||
})
|
||||
@ApiProperty({ type: PlatformOAuthClientDto })
|
||||
@IsNotEmptyObject()
|
||||
@ValidateNested()
|
||||
@Type(() => PlatformOAuthClientDto)
|
||||
|
||||
+2
-1
@@ -1,5 +1,6 @@
|
||||
import { getEnv } from "@/env";
|
||||
import { API_VERSIONS_VALUES } from "@/lib/api-versions";
|
||||
import { isOriginAllowed } from "@/lib/is-origin-allowed/is-origin-allowed";
|
||||
import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-auth.guard";
|
||||
import { NextAuthGuard } from "@/modules/auth/guards/next-auth/next-auth.guard";
|
||||
@@ -62,7 +63,7 @@ export class OAuthFlowController {
|
||||
throw new BadRequestException(`OAuth client with ID '${clientId}' not found`);
|
||||
}
|
||||
|
||||
if (!oauthClient?.redirectUris.includes(body.redirectUri)) {
|
||||
if (!isOriginAllowed(body.redirectUri, oauthClient.redirectUris)) {
|
||||
throw new BadRequestException("Invalid 'redirect_uri' value.");
|
||||
}
|
||||
|
||||
|
||||
@@ -1,32 +1,40 @@
|
||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { IsArray, IsBoolean, IsOptional, IsString } from "class-validator";
|
||||
|
||||
export class UpdateOAuthClientInput {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
logo?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
name?: string;
|
||||
|
||||
@IsArray()
|
||||
@IsOptional()
|
||||
@IsString({ each: true })
|
||||
redirectUris?: string[] = [];
|
||||
@ApiPropertyOptional({ type: [String] })
|
||||
redirectUris?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
bookingRedirectUri?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
bookingCancelRedirectUri?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional()
|
||||
bookingRescheduleRedirectUri?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional()
|
||||
areEmailsEnabled?: boolean;
|
||||
}
|
||||
|
||||
+4
@@ -1,17 +1,21 @@
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { IsNotEmpty, IsOptional, IsString } from "class-validator";
|
||||
|
||||
export class AssignOrganizationAttributeOptionToUserInput {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@ApiPropertyOptional()
|
||||
readonly value?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@ApiPropertyOptional()
|
||||
readonly attributeOptionId?: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
readonly attributeId!: string;
|
||||
}
|
||||
|
||||
+7
-1
@@ -1,4 +1,5 @@
|
||||
import { CreateOrganizationAttributeOptionInput } from "@/modules/organizations/inputs/attributes/options/create-organization-attribute-option.input";
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { AttributeType } from "@prisma/client";
|
||||
import { Type } from "class-transformer";
|
||||
import {
|
||||
@@ -14,22 +15,27 @@ import {
|
||||
export class CreateOrganizationAttributeInput {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@ApiProperty()
|
||||
readonly name!: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@ApiProperty()
|
||||
readonly slug!: string;
|
||||
|
||||
@IsEnum(AttributeType)
|
||||
@IsNotEmpty()
|
||||
@ApiProperty({ enum: AttributeType })
|
||||
readonly type!: AttributeType;
|
||||
|
||||
@IsArray()
|
||||
@ValidateNested()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => CreateOrganizationAttributeOptionInput)
|
||||
@ApiProperty({ type: [CreateOrganizationAttributeOptionInput] })
|
||||
readonly options!: CreateOrganizationAttributeOptionInput[];
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
readonly enabled?: boolean;
|
||||
}
|
||||
|
||||
+4
-1
@@ -1,11 +1,14 @@
|
||||
import { IsNotEmpty, IsOptional, IsString } from "class-validator";
|
||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { IsString, IsOptional } from "class-validator";
|
||||
|
||||
export class UpdateOrganizationAttributeOptionInput {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
readonly value?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
readonly slug?: string;
|
||||
}
|
||||
|
||||
+5
@@ -1,20 +1,25 @@
|
||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { AttributeType } from "@prisma/client";
|
||||
import { IsBoolean, IsEnum, IsOptional, IsString } from "class-validator";
|
||||
|
||||
export class UpdateOrganizationAttributeInput {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
readonly name?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
readonly slug?: string;
|
||||
|
||||
@IsEnum(AttributeType)
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ enum: AttributeType })
|
||||
readonly type?: AttributeType;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
readonly enabled?: boolean;
|
||||
}
|
||||
|
||||
+4
-1
@@ -1,13 +1,15 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { MembershipRole } from "@prisma/client";
|
||||
import { IsBoolean, IsOptional, IsEnum, IsInt } from "class-validator";
|
||||
|
||||
export class CreateOrgMembershipDto {
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
readonly userId!: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional({ type: Boolean, default: false })
|
||||
readonly accepted?: boolean = false;
|
||||
|
||||
@IsEnum(MembershipRole)
|
||||
@@ -16,5 +18,6 @@ export class CreateOrgMembershipDto {
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional({ type: Boolean, default: false })
|
||||
readonly disableImpersonation?: boolean = false;
|
||||
}
|
||||
|
||||
+4
-1
@@ -1,12 +1,14 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { MembershipRole } from "@prisma/client";
|
||||
import { IsBoolean, IsOptional, IsEnum, IsInt } from "class-validator";
|
||||
|
||||
export class CreateOrgTeamMembershipDto {
|
||||
@IsInt()
|
||||
@ApiProperty()
|
||||
readonly userId!: number;
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: Boolean, default: false })
|
||||
@IsBoolean()
|
||||
readonly accepted?: boolean = false;
|
||||
|
||||
@@ -15,6 +17,7 @@ export class CreateOrgTeamMembershipDto {
|
||||
readonly role: MembershipRole = MembershipRole.MEMBER;
|
||||
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional({ type: Boolean, default: false })
|
||||
@IsBoolean()
|
||||
readonly disableImpersonation?: boolean = false;
|
||||
}
|
||||
|
||||
@@ -1,107 +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", required: false })
|
||||
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`,
|
||||
required: false,
|
||||
})
|
||||
readonly logoUrl?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
readonly calVideoLogo?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
readonly appLogo?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
readonly appIconLogo?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
readonly bio?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
readonly hideBranding?: boolean = false;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
readonly isPrivate?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
readonly hideBookATeamMember?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
readonly metadata?: string; // Assuming metadata is a JSON string. Adjust accordingly if it's a nested object.
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
readonly theme?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
readonly brandColor?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
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()
|
||||
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",
|
||||
required: false,
|
||||
default: "Sunday",
|
||||
})
|
||||
readonly weekStart?: string = "Sunday";
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
readonly autoAcceptCreator?: boolean = true;
|
||||
}
|
||||
export class CreateOrgTeamDto extends CreateTeamInput {}
|
||||
@@ -1,17 +1,21 @@
|
||||
import { CreateUserInput } from "@/modules/users/inputs/create-user.input";
|
||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { MembershipRole } from "@prisma/client";
|
||||
import { IsString, IsOptional, IsBoolean, IsEnum } from "class-validator";
|
||||
|
||||
export class CreateOrganizationUserInput extends CreateUserInput {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({ type: String, default: "en" })
|
||||
locale = "en";
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(MembershipRole)
|
||||
@ApiPropertyOptional({ enum: MembershipRole, default: MembershipRole.MEMBER })
|
||||
organizationRole: MembershipRole = MembershipRole.MEMBER;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional({ type: Boolean, default: true })
|
||||
autoAccept = true;
|
||||
}
|
||||
|
||||
+8
-5
@@ -1,17 +1,20 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { MembershipRole } from "@prisma/client";
|
||||
import { IsBoolean, IsOptional, IsEnum } from "class-validator";
|
||||
|
||||
export class UpdateOrgMembershipDto {
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
readonly accepted?: boolean = false;
|
||||
@ApiPropertyOptional()
|
||||
readonly accepted?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(MembershipRole)
|
||||
@ApiProperty({ enum: ["MEMBER", "OWNER", "ADMIN"] })
|
||||
readonly role?: MembershipRole = MembershipRole.MEMBER;
|
||||
@ApiPropertyOptional({ enum: ["MEMBER", "OWNER", "ADMIN"] })
|
||||
readonly role?: MembershipRole;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
readonly disableImpersonation?: boolean = false;
|
||||
@ApiPropertyOptional()
|
||||
readonly disableImpersonation?: boolean;
|
||||
}
|
||||
|
||||
+8
-5
@@ -1,17 +1,20 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { MembershipRole } from "@prisma/client";
|
||||
import { IsBoolean, IsOptional, IsEnum } from "class-validator";
|
||||
|
||||
export class UpdateOrgTeamMembershipDto {
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
readonly accepted?: boolean = false;
|
||||
@ApiPropertyOptional()
|
||||
readonly accepted?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(MembershipRole)
|
||||
@ApiProperty({ enum: ["MEMBER", "OWNER", "ADMIN"] })
|
||||
readonly role?: MembershipRole = MembershipRole.MEMBER;
|
||||
@ApiPropertyOptional({ enum: ["MEMBER", "OWNER", "ADMIN"] })
|
||||
readonly role?: MembershipRole;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
readonly disableImpersonation?: boolean = false;
|
||||
@ApiPropertyOptional()
|
||||
readonly disableImpersonation?: boolean;
|
||||
}
|
||||
|
||||
@@ -1,111 +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", required: false })
|
||||
readonly name?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiPropertyOptional({ type: String, description: "Team slug", example: "caltel", required: false })
|
||||
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`,
|
||||
required: false,
|
||||
})
|
||||
readonly logoUrl?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
readonly calVideoLogo?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
readonly appLogo?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsUrl()
|
||||
readonly appIconLogo?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
readonly bio?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
readonly hideBranding?: boolean = false;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
readonly isPrivate?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
readonly hideBookATeamMember?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
readonly metadata?: string; // Assuming metadata is a JSON string. Adjust accordingly if it's a nested object.
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
readonly theme?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
readonly brandColor?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
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()
|
||||
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",
|
||||
required: false,
|
||||
default: "Sunday",
|
||||
})
|
||||
readonly weekStart?: string = "Sunday";
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
readonly bookingLimits?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
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
-4
@@ -2,21 +2,21 @@ import { Attribute } from "@/modules/organizations/outputs/attributes/attribute.
|
||||
import { BaseOutputDTO } from "@/modules/organizations/outputs/attributes/base.output";
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Expose, Type } from "class-transformer";
|
||||
import { IsBoolean, IsInt, IsString, IsEnum, ValidateNested, IsOptional } from "class-validator";
|
||||
|
||||
import { AttributeType } from "@calcom/prisma/client";
|
||||
import { IsOptional, ValidateNested } from "class-validator";
|
||||
|
||||
export class GetSingleAttributeOutput extends BaseOutputDTO {
|
||||
@Expose()
|
||||
@ValidateNested()
|
||||
@IsOptional()
|
||||
@Type(() => Attribute)
|
||||
@ApiProperty({ type: Attribute, nullable: true })
|
||||
data!: Attribute | null;
|
||||
}
|
||||
|
||||
export class GetOrganizationAttributesOutput extends BaseOutputDTO {
|
||||
@Expose()
|
||||
@ValidateNested()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => Attribute)
|
||||
@ApiProperty({ type: [Attribute] })
|
||||
data!: Attribute[];
|
||||
}
|
||||
|
||||
+6
-1
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { MembershipRole } from "@prisma/client";
|
||||
import { Expose } from "class-transformer";
|
||||
import { IsBoolean, IsInt, IsOptional, IsString } from "class-validator";
|
||||
@@ -6,18 +6,22 @@ import { IsBoolean, IsInt, IsOptional, IsString } from "class-validator";
|
||||
export class OrgMembershipOutputDto {
|
||||
@IsInt()
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
readonly id!: number;
|
||||
|
||||
@IsInt()
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
readonly userId!: number;
|
||||
|
||||
@IsInt()
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
readonly teamId!: number;
|
||||
|
||||
@IsBoolean()
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
readonly accepted!: boolean;
|
||||
|
||||
@IsString()
|
||||
@@ -28,5 +32,6 @@ export class OrgMembershipOutputDto {
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Expose()
|
||||
@ApiPropertyOptional()
|
||||
readonly disableImpersonation?: boolean;
|
||||
}
|
||||
|
||||
+13
-1
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { MembershipRole } from "@prisma/client";
|
||||
import { Expose, Type } from "class-transformer";
|
||||
import { IsArray, IsBoolean, IsEnum, IsInt, IsOptional, IsString, ValidateNested } from "class-validator";
|
||||
@@ -9,38 +9,46 @@ export class MembershipUserOutputDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Expose()
|
||||
@ApiPropertyOptional()
|
||||
readonly avatarUrl?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Expose()
|
||||
@ApiPropertyOptional()
|
||||
readonly username?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Expose()
|
||||
@ApiPropertyOptional()
|
||||
readonly name?: string;
|
||||
|
||||
@IsBoolean()
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
readonly email!: string;
|
||||
}
|
||||
|
||||
export class OrgTeamMembershipOutputDto {
|
||||
@IsInt()
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
readonly id!: number;
|
||||
|
||||
@IsInt()
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
readonly userId!: number;
|
||||
|
||||
@IsInt()
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
readonly teamId!: number;
|
||||
|
||||
@IsBoolean()
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
readonly accepted!: boolean;
|
||||
|
||||
@IsString()
|
||||
@@ -51,11 +59,13 @@ export class OrgTeamMembershipOutputDto {
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Expose()
|
||||
@ApiPropertyOptional()
|
||||
readonly disableImpersonation?: boolean;
|
||||
|
||||
@ValidateNested()
|
||||
@Type(() => MembershipUserOutputDto)
|
||||
@Expose()
|
||||
@ApiProperty({ type: MembershipUserOutputDto })
|
||||
user!: MembershipUserOutputDto;
|
||||
}
|
||||
|
||||
@@ -68,6 +78,7 @@ export class OrgTeamMembershipsOutputResponseDto {
|
||||
@ValidateNested()
|
||||
@Type(() => OrgTeamMembershipOutputDto)
|
||||
@IsArray()
|
||||
@ApiProperty({ type: [OrgTeamMembershipOutputDto] })
|
||||
data!: OrgTeamMembershipOutputDto[];
|
||||
}
|
||||
|
||||
@@ -79,5 +90,6 @@ export class OrgTeamMembershipOutputResponseDto {
|
||||
@Expose()
|
||||
@ValidateNested()
|
||||
@Type(() => OrgTeamMembershipOutputDto)
|
||||
@ApiProperty({ type: OrgTeamMembershipOutputDto })
|
||||
data!: OrgTeamMembershipOutputDto;
|
||||
}
|
||||
|
||||
+24
-2
@@ -29,7 +29,18 @@ export class OrganizationsEventTypesRepository {
|
||||
schedule: true,
|
||||
hosts: true,
|
||||
destinationCalendar: true,
|
||||
team: { select: { bannerUrl: true } },
|
||||
team: {
|
||||
select: {
|
||||
bannerUrl: true,
|
||||
name: true,
|
||||
logoUrl: true,
|
||||
slug: true,
|
||||
weekStart: true,
|
||||
brandColor: true,
|
||||
darkBrandColor: true,
|
||||
theme: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -44,7 +55,18 @@ export class OrganizationsEventTypesRepository {
|
||||
schedule: true,
|
||||
hosts: true,
|
||||
destinationCalendar: true,
|
||||
team: { select: { bannerUrl: true } },
|
||||
team: {
|
||||
select: {
|
||||
bannerUrl: true,
|
||||
name: true,
|
||||
logoUrl: true,
|
||||
slug: true,
|
||||
weekStart: true,
|
||||
brandColor: true,
|
||||
darkBrandColor: true,
|
||||
theme: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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(", ")}`);
|
||||
|
||||
@@ -12,7 +12,10 @@ type EventTypeRelations = {
|
||||
schedule: Schedule | null;
|
||||
hosts: Host[];
|
||||
destinationCalendar?: DestinationCalendar | null;
|
||||
team?: Pick<Team, "bannerUrl"> | null;
|
||||
team?: Pick<
|
||||
Team,
|
||||
"bannerUrl" | "name" | "logoUrl" | "slug" | "weekStart" | "brandColor" | "darkBrandColor" | "theme"
|
||||
> | null;
|
||||
};
|
||||
export type DatabaseTeamEventType = EventType & EventTypeRelations;
|
||||
|
||||
@@ -98,8 +101,18 @@ export class OutputOrganizationsEventTypesService {
|
||||
ownerId: userId,
|
||||
parentEventTypeId: parentId,
|
||||
schedulingType: databaseEventType.schedulingType,
|
||||
bannerUrl: databaseEventType?.team?.bannerUrl,
|
||||
assignAllTeamMembers: teamId ? assignAllTeamMembers : undefined,
|
||||
team: {
|
||||
id: teamId,
|
||||
name: databaseEventType?.team?.name,
|
||||
slug: databaseEventType?.team?.slug,
|
||||
bannerUrl: databaseEventType?.team?.bannerUrl,
|
||||
logoUrl: databaseEventType?.team?.logoUrl,
|
||||
weekStart: databaseEventType?.team?.weekStart,
|
||||
brandColor: databaseEventType?.team?.brandColor,
|
||||
darkBrandColor: databaseEventType?.team?.darkBrandColor,
|
||||
theme: databaseEventType?.team?.theme,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,24 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { MembershipRole } from "@prisma/client";
|
||||
import { IsBoolean, IsOptional, IsEnum, IsInt } from "class-validator";
|
||||
|
||||
export class CreateTeamMembershipInput {
|
||||
@IsInt()
|
||||
@ApiProperty({ type: Number })
|
||||
readonly userId!: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional({ type: Boolean, default: false })
|
||||
readonly accepted?: boolean = false;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(MembershipRole)
|
||||
@ApiProperty({ enum: ["MEMBER", "OWNER", "ADMIN"] })
|
||||
@ApiPropertyOptional({ enum: ["MEMBER", "OWNER", "ADMIN"], default: "MEMBER" })
|
||||
readonly role: MembershipRole = MembershipRole.MEMBER;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@ApiPropertyOptional({ type: Boolean, default: false })
|
||||
readonly disableImpersonation?: boolean = false;
|
||||
}
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { MembershipRole } from "@prisma/client";
|
||||
import { IsBoolean, IsOptional, IsEnum } from "class-validator";
|
||||
|
||||
export class UpdateTeamMembershipInput {
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
readonly accepted?: boolean = false;
|
||||
@ApiPropertyOptional({ type: Boolean })
|
||||
readonly accepted?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(MembershipRole)
|
||||
@ApiProperty({ enum: ["MEMBER", "OWNER", "ADMIN"] })
|
||||
readonly role?: MembershipRole = MembershipRole.MEMBER;
|
||||
@ApiPropertyOptional({ enum: ["MEMBER", "OWNER", "ADMIN"] })
|
||||
readonly role?: MembershipRole;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
readonly disableImpersonation?: boolean = false;
|
||||
@ApiPropertyOptional({ type: Boolean })
|
||||
readonly disableImpersonation?: boolean;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { MembershipRole } from "@prisma/client";
|
||||
import { Expose } from "class-transformer";
|
||||
import { IsBoolean, IsInt, IsOptional, IsString } from "class-validator";
|
||||
@@ -6,18 +6,22 @@ import { IsBoolean, IsInt, IsOptional, IsString } from "class-validator";
|
||||
export class TeamMembershipOutput {
|
||||
@IsInt()
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
readonly id!: number;
|
||||
|
||||
@IsInt()
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
readonly userId!: number;
|
||||
|
||||
@IsInt()
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
readonly teamId!: number;
|
||||
|
||||
@IsBoolean()
|
||||
@Expose()
|
||||
@ApiProperty()
|
||||
readonly accepted!: boolean;
|
||||
|
||||
@IsString()
|
||||
@@ -28,5 +32,6 @@ export class TeamMembershipOutput {
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Expose()
|
||||
@ApiPropertyOptional()
|
||||
readonly disableImpersonation?: boolean;
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -16,12 +16,12 @@ export class CreateManagedUserInput {
|
||||
|
||||
@IsOptional()
|
||||
@IsIn([12, 24], { message: "timeFormat must be a number either 12 or 24" })
|
||||
@ApiProperty({ example: 12, enum: [12, 24], description: "Must be a number 12 or 24" })
|
||||
@ApiPropertyOptional({ example: 12, enum: [12, 24], description: "Must be a number 12 or 24" })
|
||||
timeFormat?: TimeFormat;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@ApiProperty({
|
||||
@ApiPropertyOptional({
|
||||
example: "Monday",
|
||||
enum: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"],
|
||||
})
|
||||
@@ -30,7 +30,7 @@ export class CreateManagedUserInput {
|
||||
@IsTimeZone()
|
||||
@IsOptional()
|
||||
@CapitalizeTimeZone()
|
||||
@ApiProperty({
|
||||
@ApiPropertyOptional({
|
||||
example: "America/New_York",
|
||||
description: `Timezone is used to create user's default schedule from Monday to Friday from 9AM to 5PM. If it is not passed then user does not have
|
||||
a default schedule and it must be created manually via the /schedules endpoint. Until the schedule is created, the user can't access availability atom to set his / her availability nor booked.
|
||||
@@ -49,7 +49,6 @@ export class CreateManagedUserInput {
|
||||
type: String,
|
||||
example: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png",
|
||||
description: `URL of the user's avatar image`,
|
||||
required: false,
|
||||
})
|
||||
avatarUrl?: string;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty } from "@nestjs/swagger";
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { Transform } from "class-transformer";
|
||||
import { IsOptional, Validate } from "class-validator";
|
||||
|
||||
@@ -12,7 +12,8 @@ export class GetUsersInput extends SkipTakePagination {
|
||||
@Transform(({ value }: { value: string | string[] }) => {
|
||||
return typeof value === "string" ? [value] : value;
|
||||
})
|
||||
@ApiProperty({
|
||||
@ApiPropertyOptional({
|
||||
type: [String],
|
||||
description: "The email address or an array of email addresses to filter by",
|
||||
})
|
||||
emails?: string[];
|
||||
|
||||
@@ -1,31 +1,34 @@
|
||||
import { Locales } from "@/lib/enums/locales";
|
||||
import { CapitalizeTimeZone } from "@/lib/inputs/capitalize-timezone";
|
||||
import { TimeFormat, WeekDay } from "@/modules/users/inputs/create-managed-user.input";
|
||||
import { ApiProperty, ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { ApiPropertyOptional } from "@nestjs/swagger";
|
||||
import { IsEnum, IsIn, IsNumber, IsOptional, IsString, IsTimeZone, IsUrl } from "class-validator";
|
||||
|
||||
export class UpdateManagedUserInput {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
email?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
name?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsIn([12, 24])
|
||||
@ApiProperty({ example: 12, enum: [12, 24], description: "Must be 12 or 24" })
|
||||
@ApiPropertyOptional({ example: 12, enum: [12, 24], description: "Must be 12 or 24" })
|
||||
timeFormat?: TimeFormat;
|
||||
|
||||
@IsNumber()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
defaultScheduleId?: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@IsIn(["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"])
|
||||
@ApiProperty({
|
||||
@ApiPropertyOptional({
|
||||
example: "Monday",
|
||||
enum: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"],
|
||||
})
|
||||
@@ -34,11 +37,12 @@ export class UpdateManagedUserInput {
|
||||
@IsTimeZone()
|
||||
@IsOptional()
|
||||
@CapitalizeTimeZone()
|
||||
@ApiPropertyOptional()
|
||||
timeZone?: string;
|
||||
|
||||
@IsEnum(Locales)
|
||||
@IsOptional()
|
||||
@ApiProperty({ example: Locales.EN, enum: Locales })
|
||||
@ApiPropertyOptional({ example: Locales.EN, enum: Locales })
|
||||
locale?: Locales;
|
||||
|
||||
@IsUrl()
|
||||
@@ -47,7 +51,6 @@ export class UpdateManagedUserInput {
|
||||
type: String,
|
||||
example: "https://cal.com/api/avatar/2b735186-b01b-46d3-87da-019b8f61776b.png",
|
||||
description: `URL of the user's avatar image`,
|
||||
required: false,
|
||||
})
|
||||
avatarUrl?: string;
|
||||
}
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ApiProperty, PartialType } from "@nestjs/swagger";
|
||||
import { ApiProperty, ApiPropertyOptional, PartialType } from "@nestjs/swagger";
|
||||
import { WebhookTriggerEvents } from "@prisma/client";
|
||||
import { IsArray, IsBoolean, IsEnum, IsOptional, IsString } from "class-validator";
|
||||
|
||||
@@ -19,9 +19,11 @@ export class CreateWebhookInputDto {
|
||||
payloadTemplate?: string;
|
||||
|
||||
@IsBoolean()
|
||||
@ApiProperty()
|
||||
active!: boolean;
|
||||
|
||||
@IsString()
|
||||
@ApiProperty()
|
||||
subscriberUrl!: string;
|
||||
|
||||
@IsArray()
|
||||
@@ -43,6 +45,7 @@ export class CreateWebhookInputDto {
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@ApiPropertyOptional()
|
||||
secret?: string;
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { getTranslate, _generateMetadata } from "app/_utils";
|
||||
import { Suspense } from "react";
|
||||
|
||||
import SettingsHeader from "@calcom/features/settings/appDir/SettingsHeader";
|
||||
import { NewWebhookView } from "@calcom/features/webhooks/pages/webhook-new-view";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import { SkeletonContainer, SkeletonText } from "@calcom/ui";
|
||||
|
||||
export const generateMetadata = async () =>
|
||||
await _generateMetadata(
|
||||
@@ -10,6 +12,17 @@ export const generateMetadata = async () =>
|
||||
(t) => t("add_webhook_description", { appName: APP_NAME })
|
||||
);
|
||||
|
||||
const SkeletonLoader = () => {
|
||||
return (
|
||||
<SkeletonContainer>
|
||||
<div className="divide-subtle border-subtle space-y-6 rounded-b-lg border border-t-0 px-6 py-4">
|
||||
<SkeletonText className="h-8 w-full" />
|
||||
<SkeletonText className="h-8 w-full" />
|
||||
</div>
|
||||
</SkeletonContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const Page = async () => {
|
||||
const t = await getTranslate();
|
||||
|
||||
@@ -19,7 +32,9 @@ const Page = async () => {
|
||||
description={t("add_webhook_description", { appName: APP_NAME })}
|
||||
borderInShellHeader={true}
|
||||
backButton>
|
||||
<NewWebhookView />
|
||||
<Suspense fallback={<SkeletonLoader />}>
|
||||
<NewWebhookView />
|
||||
</Suspense>
|
||||
</SettingsHeader>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import { Schedule } from "@calcom/features/schedules";
|
||||
import { DEFAULT_SCHEDULE } from "@calcom/lib/availability";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { TRPCClientErrorLike } from "@calcom/trpc/react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||
import { Button, Form, Icon } from "@calcom/ui";
|
||||
|
||||
interface ISetupAvailabilityProps {
|
||||
nextStep: () => void;
|
||||
defaultScheduleId?: number | null;
|
||||
}
|
||||
|
||||
const SetupAvailability = (props: ISetupAvailabilityProps) => {
|
||||
const { defaultScheduleId } = props;
|
||||
|
||||
const { t } = useLocale();
|
||||
const { nextStep } = props;
|
||||
|
||||
const scheduleId = defaultScheduleId === null ? undefined : defaultScheduleId;
|
||||
const queryAvailability = trpc.viewer.availability.schedule.get.useQuery(
|
||||
{ scheduleId: defaultScheduleId ?? undefined },
|
||||
{
|
||||
enabled: !!scheduleId,
|
||||
}
|
||||
);
|
||||
|
||||
const availabilityForm = useForm({
|
||||
defaultValues: {
|
||||
schedule: queryAvailability?.data?.availability || DEFAULT_SCHEDULE,
|
||||
},
|
||||
});
|
||||
|
||||
const mutationOptions = {
|
||||
onError: (error: TRPCClientErrorLike<AppRouter>) => {
|
||||
throw new Error(error.message);
|
||||
},
|
||||
onSuccess: () => {
|
||||
nextStep();
|
||||
},
|
||||
};
|
||||
const createSchedule = trpc.viewer.availability.schedule.create.useMutation(mutationOptions);
|
||||
const updateSchedule = trpc.viewer.availability.schedule.update.useMutation(mutationOptions);
|
||||
return (
|
||||
<Form
|
||||
form={availabilityForm}
|
||||
handleSubmit={async (values) => {
|
||||
try {
|
||||
if (defaultScheduleId) {
|
||||
await updateSchedule.mutate({
|
||||
scheduleId: defaultScheduleId,
|
||||
name: t("default_schedule_name"),
|
||||
...values,
|
||||
});
|
||||
} else {
|
||||
await createSchedule.mutate({
|
||||
name: t("default_schedule_name"),
|
||||
...values,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
// setError(error);
|
||||
// @TODO: log error
|
||||
}
|
||||
}
|
||||
}}>
|
||||
<div className="bg-default dark:text-inverted text-emphasis border-subtle w-full rounded-md border dark:bg-opacity-5">
|
||||
<Schedule control={availabilityForm.control} name="schedule" weekStart={1} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Button
|
||||
data-testid="save-availability"
|
||||
type="submit"
|
||||
className="mt-2 w-full justify-center p-2 text-sm sm:mt-8"
|
||||
loading={availabilityForm.formState.isSubmitting}
|
||||
disabled={availabilityForm.formState.isSubmitting}>
|
||||
{t("next_step_text")} <Icon name="arrow-right" className="ml-2 h-4 w-4 self-center" />
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
);
|
||||
};
|
||||
|
||||
export { SetupAvailability };
|
||||
@@ -86,6 +86,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
length: true,
|
||||
hidden: true,
|
||||
hosts: {
|
||||
take: 3,
|
||||
select: {
|
||||
user: {
|
||||
select: {
|
||||
@@ -135,6 +136,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
where: { id: eventTypeId },
|
||||
select: {
|
||||
users: {
|
||||
take: 1,
|
||||
select: {
|
||||
username: true,
|
||||
name: true,
|
||||
@@ -143,15 +145,14 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
},
|
||||
});
|
||||
|
||||
users =
|
||||
data.length > 0
|
||||
? [
|
||||
{
|
||||
username: data[0].username ?? "",
|
||||
name: data[0].name ?? "",
|
||||
},
|
||||
]
|
||||
: [];
|
||||
if (data.length > 0) {
|
||||
users = [
|
||||
{
|
||||
username: data[0].username ?? "",
|
||||
name: data[0].name ?? "",
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
const orgSlug = isValidOrgDomain ? currentOrgDomain : null;
|
||||
|
||||
@@ -4,32 +4,30 @@ import { signOut } from "next-auth/react";
|
||||
import type { TFunction } from "next-i18next";
|
||||
import Head from "next/head";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { Suspense, useEffect } from "react";
|
||||
import { Suspense } from "react";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
import { z } from "zod";
|
||||
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { DEFAULT_SCHEDULE } from "@calcom/lib/availability";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { useParamsWithFallback } from "@calcom/lib/hooks/useParamsWithFallback";
|
||||
import { IdentityProvider } from "@calcom/prisma/enums";
|
||||
import { trpc } from "@calcom/trpc";
|
||||
import type { TRPCClientErrorLike } from "@calcom/trpc/react";
|
||||
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||
import { Button, showToast, StepCard, Steps } from "@calcom/ui";
|
||||
import { Button, StepCard, Steps } from "@calcom/ui";
|
||||
import { Icon } from "@calcom/ui";
|
||||
|
||||
import type { getServerSideProps } from "@lib/getting-started/[[...step]]/getServerSideProps";
|
||||
|
||||
import { ConnectedCalendars } from "@components/getting-started/steps-views/ConnectCalendars";
|
||||
import { ConnectedVideoStep } from "@components/getting-started/steps-views/ConnectedVideoStep";
|
||||
import { SetupAvailability } from "@components/getting-started/steps-views/SetupAvailability";
|
||||
import UserProfile from "@components/getting-started/steps-views/UserProfile";
|
||||
import { UserSettings } from "@components/getting-started/steps-views/UserSettings";
|
||||
|
||||
const INITIAL_STEP = "user-settings";
|
||||
const BASE_STEPS = ["user-settings", "user-profile"] as const;
|
||||
const BASE_STEPS = ["user-settings", "setup-availability", "user-profile"] as const;
|
||||
const EXTRA_STEPS = ["connected-calendar", "connected-video"] as const;
|
||||
type StepType = (typeof BASE_STEPS)[number] | (typeof EXTRA_STEPS)[number];
|
||||
|
||||
@@ -43,6 +41,13 @@ const getStepsAndHeadersForUser = (identityProvider: IdentityProvider, t: TFunct
|
||||
title: t("welcome_to_cal_header", { appName: APP_NAME }),
|
||||
subtitle: [t("we_just_need_basic_info"), t("edit_form_later_subtitle")],
|
||||
},
|
||||
{
|
||||
title: t("set_availability"),
|
||||
subtitle: [
|
||||
t("set_availability_getting_started_subtitle_1"),
|
||||
t("set_availability_getting_started_subtitle_2"),
|
||||
],
|
||||
},
|
||||
{
|
||||
title: t("nearly_there"),
|
||||
subtitle: [t("nearly_there_instructions")],
|
||||
@@ -81,7 +86,9 @@ const getStepsAndHeadersForUser = (identityProvider: IdentityProvider, t: TFunct
|
||||
|
||||
const stepRouteSchema = z.object({
|
||||
step: z
|
||||
.array(z.enum(["user-settings", "user-profile", "connected-calendar", "connected-video"]))
|
||||
.array(
|
||||
z.enum(["user-settings", "setup-availability", "user-profile", "connected-calendar", "connected-video"])
|
||||
)
|
||||
.default([INITIAL_STEP]),
|
||||
from: z.string().optional(),
|
||||
});
|
||||
@@ -90,15 +97,10 @@ export type PageProps = inferSSRProps<typeof getServerSideProps>;
|
||||
const OnboardingPage = (props: PageProps) => {
|
||||
const pathname = usePathname();
|
||||
const params = useParamsWithFallback();
|
||||
|
||||
const router = useRouter();
|
||||
const { t } = useLocale();
|
||||
const [user] = trpc.viewer.me.useSuspenseQuery();
|
||||
const scheduleMutationOptions = {
|
||||
onError: (error: TRPCClientErrorLike<AppRouter>) => {
|
||||
showToast(error.message, "error");
|
||||
},
|
||||
};
|
||||
const createSchedule = trpc.viewer.availability.schedule.create.useMutation(scheduleMutationOptions);
|
||||
const { t } = useLocale();
|
||||
|
||||
const result = stepRouteSchema.safeParse({
|
||||
...params,
|
||||
@@ -135,20 +137,6 @@ const OnboardingPage = (props: PageProps) => {
|
||||
router.push(`/getting-started/${stepTransform(newStep)}`);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!user || user.completedOnboarding) {
|
||||
return;
|
||||
}
|
||||
const createDefaultSchedule = async () => {
|
||||
await createSchedule.mutateAsync({
|
||||
name: t("default_schedule_name"),
|
||||
...DEFAULT_SCHEDULE,
|
||||
});
|
||||
};
|
||||
|
||||
createDefaultSchedule();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
@@ -190,6 +178,9 @@ const OnboardingPage = (props: PageProps) => {
|
||||
|
||||
{currentStep === "connected-video" && <ConnectedVideoStep nextStep={goToNextStep} />}
|
||||
|
||||
{currentStep === "setup-availability" && (
|
||||
<SetupAvailability nextStep={goToNextStep} defaultScheduleId={user.defaultScheduleId} />
|
||||
)}
|
||||
{currentStep === "user-profile" && <UserProfile />}
|
||||
</Suspense>
|
||||
</StepCard>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "4.8.1",
|
||||
"version": "4.8.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { wrapApiHandlerWithSentry } from "@sentry/nextjs";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
@@ -21,4 +22,4 @@ async function handler(req: NextApiRequest & { userId?: number }, res: NextApiRe
|
||||
return booking;
|
||||
}
|
||||
|
||||
export default defaultResponder(handler);
|
||||
export default defaultResponder(wrapApiHandlerWithSentry(handler, "/api/book/event"));
|
||||
|
||||
@@ -158,13 +158,14 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
take: 10,
|
||||
});
|
||||
|
||||
const eventTypeIds = bookingsFromSelected
|
||||
.filter((booking) => typeof booking.eventTypeId === "number")
|
||||
.map((booking) => booking.eventTypeId);
|
||||
const eventTypeIds = bookingsFromSelected.reduce((acc: number[], booking) => {
|
||||
if (typeof booking.eventTypeId !== "number") return acc;
|
||||
return [...acc, booking.eventTypeId];
|
||||
}, []);
|
||||
|
||||
const eventTypeWhereConditional: Prisma.EventTypeWhereInput = {
|
||||
id: {
|
||||
in: eventTypeIds as number[],
|
||||
in: eventTypeIds,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ test.describe("Onboarding", () => {
|
||||
await page.locator("button[type=submit]").click();
|
||||
|
||||
if (identityProvider === IdentityProvider.GOOGLE) {
|
||||
await expect(page).toHaveURL(/.*user-profile/);
|
||||
await expect(page).toHaveURL(/.*setup-availability/);
|
||||
} else {
|
||||
await expect(page).toHaveURL(/.*connected-calendar/);
|
||||
}
|
||||
@@ -66,11 +66,20 @@ test.describe("Onboarding", () => {
|
||||
// tests skip button, we don't want to test entire flow.
|
||||
await page.locator("button[data-testid=skip-step]").click();
|
||||
|
||||
await expect(page).toHaveURL(/.*user-profile/);
|
||||
await expect(page).toHaveURL(/.*setup-availability/);
|
||||
});
|
||||
}
|
||||
|
||||
await test.step("step 4 - User Profile", async () => {
|
||||
await test.step("step 4 - Setup Availability", async () => {
|
||||
const isDisabled = await page.locator("button[data-testid=save-availability]").isDisabled();
|
||||
await expect(isDisabled).toBe(false);
|
||||
// same here, skip this step.
|
||||
await page.locator("button[data-testid=save-availability]").click();
|
||||
|
||||
await expect(page).toHaveURL(/.*user-profile/);
|
||||
});
|
||||
|
||||
await test.step("step 5- User Profile", async () => {
|
||||
await page.locator("button[type=submit]").click();
|
||||
|
||||
// should redirect to /event-types after onboarding
|
||||
|
||||
@@ -2651,6 +2651,8 @@
|
||||
"go_back_and_assign": "Go back and Assign",
|
||||
"leave_without_assigning": "Leave without Assigning",
|
||||
"signing_up_terms": "By proceeding, you agree to our <0>Terms</0> and <1>Privacy Policy</1>.",
|
||||
"proceeding_agreement": "By proceeding, you agree to our",
|
||||
"terms": "Terms",
|
||||
"always_show_x_days": "Always {{x}} days available",
|
||||
"unable_to_subscribe_to_the_platform": "An error occurred while trying to subscribe to the platform plan, please try again later",
|
||||
"updating_oauth_client_error": "An error occurred while updating the OAuth client, please try again later",
|
||||
|
||||
@@ -47,6 +47,7 @@ logger.settings.minLevel = 1;
|
||||
const log = logger.getSubLogger({ prefix: ["[bookingScenario]"] });
|
||||
|
||||
type InputWebhook = {
|
||||
id?: string;
|
||||
appId: string | null;
|
||||
userId?: number | null;
|
||||
teamId?: number | null;
|
||||
@@ -54,6 +55,8 @@ type InputWebhook = {
|
||||
active: boolean;
|
||||
eventTriggers: WebhookTriggerEvents[];
|
||||
subscriberUrl: string;
|
||||
time?: number | null;
|
||||
timeUnit?: TimeUnit | null;
|
||||
};
|
||||
|
||||
type InputWorkflow = {
|
||||
|
||||
+754
-169
File diff suppressed because it is too large
Load Diff
Binary file not shown.
@@ -10,6 +10,7 @@ import { MeetLocationType } from "@calcom/app-store/locations";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { CalendarCache } from "@calcom/features/calendar-cache/calendar-cache";
|
||||
import type { FreeBusyArgs } from "@calcom/features/calendar-cache/calendar-cache.repository.interface";
|
||||
import { getTimeMax, getTimeMin } from "@calcom/features/calendar-cache/lib/datesForCache";
|
||||
import { getLocation, getRichDescription } from "@calcom/lib/CalEventParser";
|
||||
import type CalendarService from "@calcom/lib/CalendarService";
|
||||
import {
|
||||
@@ -510,16 +511,22 @@ export default class GoogleCalendarService implements Calendar {
|
||||
return apiResponse.json;
|
||||
}
|
||||
|
||||
async getCacheOrFetchAvailability(args: FreeBusyArgs): Promise<EventBusyDate[] | null> {
|
||||
const { timeMin, timeMax, items } = args;
|
||||
let freeBusyResult: calendar_v3.Schema$FreeBusyResponse = {};
|
||||
async getFreeBusyResult(
|
||||
args: FreeBusyArgs,
|
||||
shouldServeCache?: boolean
|
||||
): Promise<calendar_v3.Schema$FreeBusyResponse> {
|
||||
if (shouldServeCache === false) return await this.fetchAvailability(args);
|
||||
const calendarCache = await CalendarCache.init(null);
|
||||
const cached = await calendarCache.getCachedAvailability(this.credential.id, args);
|
||||
if (cached) {
|
||||
freeBusyResult = cached.value as unknown as calendar_v3.Schema$FreeBusyResponse;
|
||||
} else {
|
||||
freeBusyResult = await this.fetchAvailability({ timeMin, timeMax, items });
|
||||
}
|
||||
if (cached) return cached.value as unknown as calendar_v3.Schema$FreeBusyResponse;
|
||||
return await this.fetchAvailability(args);
|
||||
}
|
||||
|
||||
async getCacheOrFetchAvailability(
|
||||
args: FreeBusyArgs,
|
||||
shouldServeCache?: boolean
|
||||
): Promise<EventBusyDate[] | null> {
|
||||
const freeBusyResult = await this.getFreeBusyResult(args, shouldServeCache);
|
||||
if (!freeBusyResult.calendars) return null;
|
||||
|
||||
const result = Object.values(freeBusyResult.calendars).reduce((c, i) => {
|
||||
@@ -537,7 +544,8 @@ export default class GoogleCalendarService implements Calendar {
|
||||
async getAvailability(
|
||||
dateFrom: string,
|
||||
dateTo: string,
|
||||
selectedCalendars: IntegrationCalendar[]
|
||||
selectedCalendars: IntegrationCalendar[],
|
||||
shouldServeCache?: boolean
|
||||
): Promise<EventBusyDate[]> {
|
||||
this.log.debug("Getting availability", safeStringify({ dateFrom, dateTo, selectedCalendars }));
|
||||
const calendar = await this.authedCalendar();
|
||||
@@ -563,11 +571,14 @@ export default class GoogleCalendarService implements Calendar {
|
||||
|
||||
// /freebusy from google api only allows a date range of 90 days
|
||||
if (diff <= 90) {
|
||||
const freeBusyData = await this.getCacheOrFetchAvailability({
|
||||
timeMin: dateFrom,
|
||||
timeMax: dateTo,
|
||||
items: calsIds.map((id) => ({ id })),
|
||||
});
|
||||
const freeBusyData = await this.getCacheOrFetchAvailability(
|
||||
{
|
||||
timeMin: dateFrom,
|
||||
timeMax: dateTo,
|
||||
items: calsIds.map((id) => ({ id })),
|
||||
},
|
||||
shouldServeCache
|
||||
);
|
||||
if (!freeBusyData) throw new Error("No response from google calendar");
|
||||
|
||||
return freeBusyData;
|
||||
@@ -583,11 +594,14 @@ export default class GoogleCalendarService implements Calendar {
|
||||
if (endDate.isAfter(originalEndDate)) endDate = originalEndDate;
|
||||
|
||||
busyData.push(
|
||||
...((await this.getCacheOrFetchAvailability({
|
||||
timeMin: startDate.format(),
|
||||
timeMax: endDate.format(),
|
||||
items: calsIds.map((id) => ({ id })),
|
||||
})) || [])
|
||||
...((await this.getCacheOrFetchAvailability(
|
||||
{
|
||||
timeMin: startDate.format(),
|
||||
timeMax: endDate.format(),
|
||||
items: calsIds.map((id) => ({ id })),
|
||||
},
|
||||
shouldServeCache
|
||||
)) || [])
|
||||
);
|
||||
|
||||
startDate = endDate.add(1, "minutes");
|
||||
@@ -672,12 +686,7 @@ export default class GoogleCalendarService implements Calendar {
|
||||
}
|
||||
async unwatchCalendar({ calendarId }: { calendarId: string }) {
|
||||
const credentialId = this.credential.id;
|
||||
const sc = await prisma.selectedCalendar.findFirst({
|
||||
where: {
|
||||
credentialId,
|
||||
externalId: calendarId,
|
||||
},
|
||||
});
|
||||
const sc = await SelectedCalendarRepository.findByExternalId(credentialId, calendarId);
|
||||
// Delete the calendar cache to force a fresh cache
|
||||
await prisma.calendarCache.deleteMany({ where: { credentialId } });
|
||||
const calendar = await this.authedCalendar();
|
||||
@@ -699,6 +708,12 @@ export default class GoogleCalendarService implements Calendar {
|
||||
googleChannelResourceUri: null,
|
||||
googleChannelExpiration: null,
|
||||
});
|
||||
// Populate the cache back for the remaining calendars, if any
|
||||
const remainingCalendars =
|
||||
sc?.credential?.selectedCalendars.filter((sc) => sc.externalId !== calendarId) || [];
|
||||
if (remainingCalendars.length > 0) {
|
||||
await this.fetchAvailabilityAndSetCache(remainingCalendars);
|
||||
}
|
||||
}
|
||||
|
||||
async setAvailabilityInCache(args: FreeBusyArgs, data: calendar_v3.Schema$FreeBusyResponse): Promise<void> {
|
||||
@@ -707,12 +722,11 @@ export default class GoogleCalendarService implements Calendar {
|
||||
}
|
||||
|
||||
async fetchAvailabilityAndSetCache(selectedCalendars: IntegrationCalendar[]) {
|
||||
const date = new Date();
|
||||
const parsedArgs = {
|
||||
/** Expand the start date to the start of the month */
|
||||
timeMin: new Date(date.getFullYear(), date.getMonth(), 1, 0, 0, 0, 0).toISOString(),
|
||||
timeMin: getTimeMax(),
|
||||
/** Expand the end date to the end of the month */
|
||||
timeMax: new Date(date.getFullYear(), date.getMonth() + 1, 0, 0, 0, 0, 0).toISOString(),
|
||||
timeMax: getTimeMin(),
|
||||
items: selectedCalendars.map((sc) => ({ id: sc.externalId })),
|
||||
};
|
||||
const data = await this.fetchAvailability(parsedArgs);
|
||||
|
||||
@@ -0,0 +1,265 @@
|
||||
import { setupAndTeardown } from "@calcom/web/test/utils/bookingScenario/setupAndTeardown";
|
||||
|
||||
import { describe, expect, it, beforeEach, vi } from "vitest";
|
||||
|
||||
import type { CredentialPayload } from "@calcom/types/Credential";
|
||||
|
||||
import SalesforceCRMService from "./CrmService";
|
||||
import { SalesforceRecordEnum } from "./enums";
|
||||
|
||||
describe("SalesforceCRMService", () => {
|
||||
let service: SalesforceCRMService;
|
||||
let mockConnection: { query: any };
|
||||
|
||||
setupAndTeardown();
|
||||
|
||||
beforeEach(() => {
|
||||
mockConnection = {
|
||||
query: vi.fn(),
|
||||
};
|
||||
|
||||
const mockCredential: CredentialPayload = {
|
||||
id: 1,
|
||||
type: "salesforce_crm",
|
||||
key: {
|
||||
access_token: "mock_token",
|
||||
refresh_token: "mock_refresh",
|
||||
instance_url: "https://mock.salesforce.com",
|
||||
},
|
||||
userId: 1,
|
||||
appId: "salesforce",
|
||||
teamId: null,
|
||||
invalid: false,
|
||||
user: {
|
||||
email: "test-user@example.com",
|
||||
},
|
||||
};
|
||||
|
||||
service = new SalesforceCRMService(mockCredential, {}, true);
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
service.conn = Promise.resolve(mockConnection);
|
||||
});
|
||||
|
||||
describe("getContacts", () => {
|
||||
it("should return empty array when no records found", async () => {
|
||||
// Setup mock response
|
||||
mockConnection.query.mockResolvedValueOnce({ records: [] });
|
||||
|
||||
const result = await service.getContacts({ emails: "test@example.com" });
|
||||
|
||||
expect(result).toEqual([]);
|
||||
expect(mockConnection.query).toHaveBeenCalledWith(
|
||||
"SELECT Id, Email, OwnerId FROM Contact WHERE Email IN ('test@example.com')"
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle single email string input", async () => {
|
||||
// Setup mock response with descriptive name
|
||||
const contactQueryResponse = {
|
||||
records: [
|
||||
{
|
||||
Id: "001",
|
||||
Email: "test@example.com",
|
||||
OwnerId: "owner001",
|
||||
attributes: { type: "Contact" },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Setup query spy
|
||||
const querySpy = vi.spyOn(mockConnection, "query");
|
||||
querySpy.mockResolvedValueOnce(contactQueryResponse);
|
||||
|
||||
const result = await service.getContacts({ emails: "test@example.com" });
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
id: "001",
|
||||
email: "test@example.com",
|
||||
recordType: "Contact",
|
||||
},
|
||||
]);
|
||||
|
||||
// Verify the exact query made
|
||||
expect(querySpy).toHaveBeenCalledWith(
|
||||
"SELECT Id, Email, OwnerId FROM Contact WHERE Email IN ('test@example.com')"
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle array of emails input", async () => {
|
||||
const contactsQueryResponse = {
|
||||
records: [
|
||||
{
|
||||
Id: "001",
|
||||
Email: "test1@example.com",
|
||||
OwnerId: "owner001",
|
||||
attributes: { type: "Contact" },
|
||||
},
|
||||
{
|
||||
Id: "002",
|
||||
Email: "test2@example.com",
|
||||
OwnerId: "owner002",
|
||||
attributes: { type: "Contact" },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const querySpy = vi.spyOn(mockConnection, "query");
|
||||
querySpy.mockResolvedValueOnce(contactsQueryResponse);
|
||||
|
||||
const result = await service.getContacts({
|
||||
emails: ["test1@example.com", "test2@example.com"],
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
id: "001",
|
||||
email: "test1@example.com",
|
||||
recordType: "Contact",
|
||||
},
|
||||
{
|
||||
id: "002",
|
||||
email: "test2@example.com",
|
||||
recordType: "Contact",
|
||||
},
|
||||
]);
|
||||
|
||||
expect(querySpy).toHaveBeenCalledWith(
|
||||
"SELECT Id, Email, OwnerId FROM Contact WHERE Email IN ('test1@example.com','test2@example.com')"
|
||||
);
|
||||
});
|
||||
|
||||
it("should include owner information when includeOwner is true", async () => {
|
||||
const contactQueryResponse = {
|
||||
records: [
|
||||
{
|
||||
Id: "001",
|
||||
Email: "test@example.com",
|
||||
OwnerId: "owner001",
|
||||
attributes: { type: "Contact" },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const ownerQueryResponse = {
|
||||
records: [
|
||||
{
|
||||
Id: "owner001",
|
||||
Email: "owner@example.com",
|
||||
Name: "Test Owner",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
// Setup query spy with specific responses
|
||||
const querySpy = vi.spyOn(mockConnection, "query");
|
||||
querySpy
|
||||
.mockResolvedValueOnce(contactQueryResponse) // Contact query
|
||||
.mockResolvedValueOnce(ownerQueryResponse); // Owner query
|
||||
|
||||
const result = await service.getContacts({
|
||||
emails: "test@example.com",
|
||||
includeOwner: true,
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
id: "001",
|
||||
email: "test@example.com",
|
||||
ownerId: "owner001",
|
||||
ownerEmail: "owner@example.com",
|
||||
recordType: "Contact",
|
||||
},
|
||||
]);
|
||||
|
||||
expect(querySpy).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
"SELECT Id, Email, OwnerId FROM Contact WHERE Email IN ('test@example.com')"
|
||||
);
|
||||
expect(querySpy).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
expect.stringContaining("SELECT Id, Email, Name FROM User WHERE Id = 'owner001'")
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle account record type with round robin skip", async () => {
|
||||
service = new SalesforceCRMService(
|
||||
{} as CredentialPayload,
|
||||
{
|
||||
createEventOn: SalesforceRecordEnum.ACCOUNT,
|
||||
roundRobinSkipCheckRecordOn: SalesforceRecordEnum.ACCOUNT,
|
||||
},
|
||||
true
|
||||
);
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
service.conn = Promise.resolve(mockConnection);
|
||||
|
||||
const initialContactQueryResponse = {
|
||||
records: [
|
||||
{
|
||||
Id: "001",
|
||||
Email: "test@example.com",
|
||||
AccountId: "acc001",
|
||||
OwnerId: "owner001",
|
||||
attributes: { type: "Contact" },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const accountQueryResponse = {
|
||||
records: [
|
||||
{
|
||||
Id: "acc001",
|
||||
OwnerId: "owner001",
|
||||
Email: "test@example.com",
|
||||
attributes: { type: "Account" },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const ownerQueryResponse = {
|
||||
records: [
|
||||
{
|
||||
Id: "owner001",
|
||||
Email: "owner@example.com",
|
||||
Name: "Test Owner",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const querySpy = vi.spyOn(mockConnection, "query");
|
||||
querySpy
|
||||
.mockResolvedValueOnce(initialContactQueryResponse)
|
||||
.mockResolvedValueOnce(accountQueryResponse)
|
||||
.mockResolvedValueOnce(ownerQueryResponse);
|
||||
|
||||
const result = await service.getContacts({
|
||||
emails: "test@example.com",
|
||||
forRoundRobinSkip: true,
|
||||
includeOwner: true,
|
||||
});
|
||||
|
||||
expect(result).toEqual([
|
||||
{
|
||||
id: "acc001",
|
||||
email: "test@example.com",
|
||||
ownerId: "owner001",
|
||||
ownerEmail: "owner@example.com",
|
||||
recordType: "Account",
|
||||
},
|
||||
]);
|
||||
|
||||
expect(querySpy).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
"SELECT Id, Email, OwnerId, AccountId FROM Contact WHERE Email = 'test@example.com' AND AccountId != null"
|
||||
);
|
||||
expect(querySpy).toHaveBeenNthCalledWith(2, "SELECT Id, OwnerId FROM Account WHERE Id = 'acc001'");
|
||||
expect(querySpy).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
expect.stringContaining("SELECT Id, Email, Name FROM User WHERE Id = 'owner001'")
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -7,6 +7,7 @@ import type { FormResponse } from "@calcom/app-store/routing-forms/types/types";
|
||||
import { getLocation } from "@calcom/lib/CalEventParser";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
import type { CalendarEvent, CalEventResponses } from "@calcom/types/Calendar";
|
||||
import type { CredentialPayload } from "@calcom/types/Credential";
|
||||
@@ -62,16 +63,18 @@ const salesforceTokenSchema = z.object({
|
||||
|
||||
export default class SalesforceCRMService implements CRM {
|
||||
private integrationName = "";
|
||||
private conn: Promise<jsforce.Connection>;
|
||||
private conn!: Promise<jsforce.Connection>;
|
||||
private log: typeof logger;
|
||||
private calWarnings: string[] = [];
|
||||
private appOptions: any;
|
||||
private doNotCreateEvent = false;
|
||||
private fallbackToContact = false;
|
||||
|
||||
constructor(credential: CredentialPayload, appOptions: any) {
|
||||
constructor(credential: CredentialPayload, appOptions: any, testMode = false) {
|
||||
this.integrationName = "salesforce_other_calendar";
|
||||
this.conn = this.getClient(credential).then((c) => c);
|
||||
if (!testMode) {
|
||||
this.conn = this.getClient(credential).then((c) => c);
|
||||
}
|
||||
this.log = logger.getSubLogger({ prefix: [`[[lib] ${this.integrationName}`] });
|
||||
this.appOptions = appOptions;
|
||||
}
|
||||
@@ -323,108 +326,196 @@ export default class SalesforceCRMService implements CRM {
|
||||
includeOwner?: boolean;
|
||||
forRoundRobinSkip?: boolean;
|
||||
}) {
|
||||
const conn = await this.conn;
|
||||
const emailArray = Array.isArray(emails) ? emails : [emails];
|
||||
const appOptions = this.getAppOptions();
|
||||
const recordToSearch =
|
||||
(forRoundRobinSkip ? appOptions?.roundRobinSkipCheckRecordOn : appOptions?.createEventOn) ??
|
||||
SalesforceRecordEnum.CONTACT;
|
||||
let soql: string;
|
||||
let accountOwnerId = "";
|
||||
if (recordToSearch === SalesforceRecordEnum.ACCOUNT) {
|
||||
// For an account let's assume that the first email is the one we should be querying against
|
||||
const attendeeEmail = emailArray[0];
|
||||
soql = `SELECT Id, Email, OwnerId, AccountId FROM Contact WHERE Email = '${attendeeEmail}' AND AccountId != null`;
|
||||
const log = logger.getSubLogger({ prefix: [`[getContacts]:${emails}`] });
|
||||
try {
|
||||
const conn = await this.conn;
|
||||
const emailArray = Array.isArray(emails) ? emails : [emails];
|
||||
|
||||
// If this is for a round robin skip then we need to return the account record
|
||||
if (forRoundRobinSkip) {
|
||||
const results = await conn.query(soql);
|
||||
if (results.records.length) {
|
||||
const contact = results.records[0] as { AccountId: string };
|
||||
if (contact) {
|
||||
soql = `SELECT Id, OwnerId FROM Account WHERE Id = '${contact.AccountId}'`;
|
||||
}
|
||||
} else {
|
||||
// If we can't find the exact contact, then we need to search for an account where the contacts share the same email domain
|
||||
const accountId = await this.getAccountIdBasedOnEmailDomainOfContacts(attendeeEmail);
|
||||
if (accountId) {
|
||||
soql = `SELECT Id, OwnerId FROM Account WHERE Id = '${accountId}'`;
|
||||
}
|
||||
}
|
||||
}
|
||||
// If creating events on contacts or leads
|
||||
} else {
|
||||
soql = `SELECT Id, Email, OwnerId FROM ${recordToSearch} WHERE Email IN ('${emailArray.join("','")}')`;
|
||||
}
|
||||
const results = await conn.query(soql);
|
||||
const appOptions = this.getAppOptions();
|
||||
const recordToSearch =
|
||||
(forRoundRobinSkip ? appOptions?.roundRobinSkipCheckRecordOn : appOptions?.createEventOn) ??
|
||||
SalesforceRecordEnum.CONTACT;
|
||||
|
||||
// If we're checking against the contact, the ownerId should take precedence
|
||||
if (recordToSearch === SalesforceRecordEnum.ACCOUNT && results.records.length) {
|
||||
const account = results.records[0] as ContactRecord;
|
||||
accountOwnerId = account.OwnerId;
|
||||
}
|
||||
let soql: string;
|
||||
let accountOwnerId = "";
|
||||
|
||||
let records: ContactRecord[] = [];
|
||||
|
||||
// If falling back to contacts, check for the contact before returning the leads or empty array
|
||||
if (
|
||||
appOptions.createEventOn === SalesforceRecordEnum.LEAD &&
|
||||
appOptions.createEventOnLeadCheckForContact
|
||||
) {
|
||||
// Get any matching contacts
|
||||
const contactSearch = await conn.query(
|
||||
`SELECT Id, Email, OwnerId FROM ${SalesforceRecordEnum.CONTACT} WHERE Email IN ('${emailArray.join(
|
||||
"','"
|
||||
)}')`
|
||||
log.info(
|
||||
"Getting contacts for emails",
|
||||
safeStringify({
|
||||
emailArray,
|
||||
includeOwner,
|
||||
forRoundRobinSkip,
|
||||
recordToSearch,
|
||||
appOptions,
|
||||
})
|
||||
);
|
||||
|
||||
if (contactSearch && contactSearch.records.length > 0) {
|
||||
records = contactSearch.records as ContactRecord[];
|
||||
this.setFallbackToContact(true);
|
||||
// Handle Account record type
|
||||
if (recordToSearch === SalesforceRecordEnum.ACCOUNT) {
|
||||
// For an account let's assume that the first email is the one we should be querying against
|
||||
const attendeeEmail = emailArray[0];
|
||||
log.info("Searching account for email", safeStringify({ attendeeEmail }));
|
||||
|
||||
soql = `SELECT Id, Email, OwnerId, AccountId FROM Contact WHERE Email = '${attendeeEmail}' AND AccountId != null`;
|
||||
|
||||
// If this is for a round robin skip then we need to return the account record
|
||||
if (forRoundRobinSkip) {
|
||||
const results = await conn.query(soql);
|
||||
log.info("Account contact search results", safeStringify({ resultCount: results.records.length }));
|
||||
|
||||
if (results.records?.length) {
|
||||
const contact = results.records[0] as { AccountId?: string };
|
||||
if (contact?.AccountId) {
|
||||
soql = `SELECT Id, OwnerId FROM Account WHERE Id = '${contact.AccountId}'`;
|
||||
}
|
||||
} else {
|
||||
// If we can't find the exact contact, then we need to search for an account where the contacts share the same email domain
|
||||
const accountId = await this.getAccountIdBasedOnEmailDomainOfContacts(attendeeEmail);
|
||||
if (accountId) {
|
||||
soql = `SELECT Id, OwnerId FROM Account WHERE Id = '${accountId}'`;
|
||||
}
|
||||
}
|
||||
}
|
||||
// If creating events on contacts or leads
|
||||
} else {
|
||||
// Handle Contact/Lead record types
|
||||
soql = `SELECT Id, Email, OwnerId FROM ${recordToSearch} WHERE Email IN ('${emailArray.join(
|
||||
"','"
|
||||
)}')`;
|
||||
}
|
||||
} else if (!results || !results.records.length) {
|
||||
|
||||
const results = await conn.query(soql);
|
||||
log.info("Query results", safeStringify({ recordCount: results.records?.length }));
|
||||
|
||||
// If we're checking against the contact, the ownerId should take precedence
|
||||
if (recordToSearch === SalesforceRecordEnum.ACCOUNT && results.records?.length) {
|
||||
const account = results.records[0] as ContactRecord;
|
||||
if (account?.OwnerId) {
|
||||
accountOwnerId = account.OwnerId;
|
||||
}
|
||||
}
|
||||
|
||||
let records: ContactRecord[] = [];
|
||||
|
||||
// If falling back to contacts, check for the contact before returning the leads or empty array
|
||||
if (
|
||||
appOptions.createEventOn === SalesforceRecordEnum.LEAD &&
|
||||
appOptions.createEventOnLeadCheckForContact
|
||||
) {
|
||||
// Get any matching contacts
|
||||
const contactSearch = await conn.query(
|
||||
`SELECT Id, Email, OwnerId FROM ${SalesforceRecordEnum.CONTACT} WHERE Email IN ('${emailArray.join(
|
||||
"','"
|
||||
)}')`
|
||||
);
|
||||
|
||||
if (contactSearch?.records?.length > 0) {
|
||||
records = contactSearch.records as ContactRecord[];
|
||||
this.setFallbackToContact(true);
|
||||
log.info(
|
||||
"Found matching contacts, falling back to contact",
|
||||
safeStringify({
|
||||
contactCount: records.length,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!records.length && results?.records?.length) {
|
||||
records = results.records as ContactRecord[];
|
||||
}
|
||||
|
||||
if (!records.length) {
|
||||
log.info("No records found");
|
||||
return [];
|
||||
}
|
||||
|
||||
// Handle owner information
|
||||
if (includeOwner || forRoundRobinSkip) {
|
||||
const ownerIds: Set<string> = new Set();
|
||||
|
||||
if (accountOwnerId) {
|
||||
ownerIds.add(accountOwnerId);
|
||||
} else {
|
||||
records.forEach((record) => {
|
||||
if (record?.OwnerId) {
|
||||
ownerIds.add(record.OwnerId);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (ownerIds.size === 0) {
|
||||
log.warn("No owner IDs found for records");
|
||||
return [];
|
||||
}
|
||||
|
||||
const ownersQuery = await Promise.all(
|
||||
Array.from(ownerIds).map(async (ownerId) => {
|
||||
const result = await this.getSalesforceUserFromUserId(ownerId);
|
||||
return result;
|
||||
})
|
||||
);
|
||||
|
||||
// Filter out any undefined results and ensure records exist
|
||||
const validOwnersQuery = ownersQuery.filter((query): query is jsforce.QueryResult<ContactRecord> => {
|
||||
const firstRecord = query?.records?.[0];
|
||||
if (!firstRecord) return false;
|
||||
|
||||
return (
|
||||
typeof firstRecord === "object" && "Email" in firstRecord && typeof firstRecord.Email === "string"
|
||||
);
|
||||
});
|
||||
|
||||
if (validOwnersQuery.length === 0) {
|
||||
log.warn("No valid owner records found");
|
||||
return [];
|
||||
}
|
||||
|
||||
return records
|
||||
.map((record) => {
|
||||
if (!record?.Id || !record?.OwnerId) {
|
||||
log.warn("Invalid record data", { record });
|
||||
return null;
|
||||
}
|
||||
|
||||
const ownerEmail = accountOwnerId
|
||||
? validOwnersQuery[0]?.records[0]?.Email
|
||||
: validOwnersQuery.find((user) => user.records[0]?.Id === record.OwnerId)?.records[0]?.Email;
|
||||
|
||||
if (!ownerEmail) {
|
||||
log.warn("Could not find owner email", { recordId: record.Id, ownerId: record.OwnerId });
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: record.Id,
|
||||
email: record.Email,
|
||||
ownerId: record.OwnerId,
|
||||
ownerEmail,
|
||||
recordType: accountOwnerId ? SalesforceRecordEnum.ACCOUNT : record.attributes?.type,
|
||||
};
|
||||
})
|
||||
.filter((record): record is NonNullable<typeof record> => record !== null);
|
||||
}
|
||||
|
||||
return records
|
||||
.map((record) => {
|
||||
if (!record?.Id || !record?.Email || !record?.attributes?.type) {
|
||||
log.warn("Invalid record data for basic mapping", { record });
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
id: record.Id,
|
||||
email: record.Email,
|
||||
recordType: record.attributes.type,
|
||||
};
|
||||
})
|
||||
.filter((record): record is NonNullable<typeof record> => record !== null);
|
||||
} catch (error) {
|
||||
log.error("Error in getContacts", safeStringify({ error }));
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!records.length) records = results.records as ContactRecord[];
|
||||
|
||||
if (includeOwner || forRoundRobinSkip) {
|
||||
const ownerIds: Set<string> = new Set();
|
||||
if (accountOwnerId) {
|
||||
ownerIds.add(accountOwnerId);
|
||||
} else {
|
||||
records.forEach((record) => {
|
||||
ownerIds.add(record.OwnerId);
|
||||
});
|
||||
}
|
||||
|
||||
const ownersQuery = (await Promise.all(
|
||||
Array.from(ownerIds).map(async (ownerId) => {
|
||||
return await this.getSalesforceUserFromUserId(ownerId);
|
||||
})
|
||||
)) as { records: ContactRecord[] }[];
|
||||
const contactsWithOwners = records.map((record) => {
|
||||
const ownerEmail = accountOwnerId
|
||||
? ownersQuery[0].records[0].Email
|
||||
: ownersQuery.find((user) => user.records[0]?.Id === record.OwnerId)?.records[0].Email;
|
||||
return {
|
||||
id: record.Id,
|
||||
email: record.Email,
|
||||
ownerId: record.OwnerId,
|
||||
ownerEmail,
|
||||
recordType: accountOwnerId ? SalesforceRecordEnum.ACCOUNT : record.attributes.type,
|
||||
};
|
||||
});
|
||||
return contactsWithOwners;
|
||||
}
|
||||
|
||||
return records
|
||||
? records.map((record) => ({
|
||||
id: record.Id,
|
||||
email: record.Email,
|
||||
recordType: record.attributes.type,
|
||||
}))
|
||||
: [];
|
||||
}
|
||||
|
||||
async createContacts(
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user