feat: Billing Service (#14756)
* chore: Billing Service * chore: others * chore: don't type * chore: typo * chore: use proper wh secret * chore: updates * chore: lockfile * chore: docs and comment out * chore: updates and include plan * chore: config * chore: bring back alias * chore: update lockfile * chore: refactor * chore: default none plan in db * chore: read from env vars * chore: feedback * chore: remove json config * chore: proper type
This commit is contained in:
@@ -10,4 +10,13 @@ JWT_SECRET=
|
||||
SENTRY_DSN=
|
||||
|
||||
# KEEP THIS EMPTY, DISABLE SENTRY CLIENT INSIDE OF LIBRARIES USED BY APIv2
|
||||
NEXT_PUBLIC_SENTRY_DSN=
|
||||
NEXT_PUBLIC_SENTRY_DSN=
|
||||
|
||||
# Stripe Billing
|
||||
STRIPE_PRICE_ID_STARTER=
|
||||
STRIPE_PRICE_ID_ESSENTIALS=
|
||||
STRIPE_PRICE_ID_ENTERPRISE=
|
||||
STRIPE_API_KEY=
|
||||
STRIPE_WEBHOOK_SECRET=
|
||||
|
||||
WEB_APP_URL=http://localhost:3000/
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"dev:build": "yarn workspace @calcom/platform-constants build && yarn workspace @calcom/platform-utils build && yarn workspace @calcom/platform-types build && yarn workspace @calcom/platform-libraries build",
|
||||
"dev": "yarn dev:build && docker-compose up -d && yarn copy-swagger-module && yarn start --watch",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:prod": "node ./dist/apps/api/v2/src/main.js",
|
||||
"start:prod": "SKIP_DOCS_GENERATION=1 node ./dist/apps/api/v2/src/main.js",
|
||||
"test": "yarn dev:build && jest",
|
||||
"test:watch": "yarn dev:build && jest --watch",
|
||||
"test:cov": "yarn dev:build && jest --coverage",
|
||||
@@ -40,6 +40,7 @@
|
||||
"@nestjs/throttler": "^5.1.2",
|
||||
"@sentry/node": "^7.86.0",
|
||||
"@sentry/tracing": "^7.86.0",
|
||||
"body-parser": "^1.20.2",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.0",
|
||||
"cookie-parser": "^1.4.6",
|
||||
@@ -56,6 +57,7 @@
|
||||
"passport-jwt": "^4.0.1",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rxjs": "^7.8.1",
|
||||
"stripe": "^15.3.0",
|
||||
"uuid": "^8.3.2",
|
||||
"winston": "^3.11.0",
|
||||
"zod": "^3.22.4"
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import { AppLoggerMiddleware } from "@/app.logger.middleware";
|
||||
import { RewriterMiddleware } from "@/app.rewrites.middleware";
|
||||
import appConfig from "@/config/app";
|
||||
import { AppLoggerMiddleware } from "@/middleware/app.logger.middleware";
|
||||
import { RewriterMiddleware } from "@/middleware/app.rewrites.middleware";
|
||||
import { JsonBodyMiddleware } from "@/middleware/body/json.body.middleware";
|
||||
import { RawBodyMiddleware } from "@/middleware/body/raw.body.middleware";
|
||||
import { AuthModule } from "@/modules/auth/auth.module";
|
||||
import { EndpointsModule } from "@/modules/endpoints.module";
|
||||
import { JwtModule } from "@/modules/jwt/jwt.module";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { RedisModule } from "@/modules/redis/redis.module";
|
||||
import { RedisService } from "@/modules/redis/redis.service";
|
||||
import { MiddlewareConsumer, Module, NestModule } from "@nestjs/common";
|
||||
import { MiddlewareConsumer, Module, NestModule, RequestMethod } from "@nestjs/common";
|
||||
import { ConfigModule } from "@nestjs/config";
|
||||
import { RouterModule } from "@nestjs/core";
|
||||
import { ThrottlerModule, seconds } from "@nestjs/throttler";
|
||||
import { seconds, ThrottlerModule } from "@nestjs/throttler";
|
||||
import { ThrottlerStorageRedisService } from "nestjs-throttler-storage-redis";
|
||||
|
||||
import { AppController } from "./app.controller";
|
||||
@@ -53,7 +55,17 @@ import { AppController } from "./app.controller";
|
||||
})
|
||||
export class AppModule implements NestModule {
|
||||
configure(consumer: MiddlewareConsumer): void {
|
||||
consumer.apply(AppLoggerMiddleware).forRoutes("*");
|
||||
consumer.apply(RewriterMiddleware).forRoutes("/");
|
||||
consumer
|
||||
.apply(RawBodyMiddleware)
|
||||
.forRoutes({
|
||||
path: "/api/v2/billing/webhook",
|
||||
method: RequestMethod.POST,
|
||||
})
|
||||
.apply(JsonBodyMiddleware)
|
||||
.forRoutes("*")
|
||||
.apply(AppLoggerMiddleware)
|
||||
.forRoutes("*")
|
||||
.apply(RewriterMiddleware)
|
||||
.forRoutes("/");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,13 @@ const loadConfig = (): AppConfig => {
|
||||
next: {
|
||||
authSecret: getEnv("NEXTAUTH_SECRET"),
|
||||
},
|
||||
stripe: {
|
||||
apiKey: getEnv("STRIPE_API_KEY"),
|
||||
webhookSecret: getEnv("STRIPE_WEBHOOK_SECRET"),
|
||||
},
|
||||
app: {
|
||||
baseUrl: getEnv("WEB_APP_URL", "https://app.cal.com"),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -15,4 +15,11 @@ export type AppConfig = {
|
||||
next: {
|
||||
authSecret: string;
|
||||
};
|
||||
stripe: {
|
||||
apiKey: string;
|
||||
webhookSecret: string;
|
||||
};
|
||||
app: {
|
||||
baseUrl: string;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { BookingsController } from "@/ee/bookings/controllers/bookings.controller";
|
||||
import { BillingModule } from "@/modules/billing/billing.module";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
@@ -8,7 +9,7 @@ import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, RedisModule, TokensModule],
|
||||
imports: [PrismaModule, RedisModule, TokensModule, BillingModule],
|
||||
providers: [TokensRepository, OAuthFlowService, OAuthClientRepository],
|
||||
controllers: [BookingsController],
|
||||
})
|
||||
|
||||
@@ -6,6 +6,7 @@ import { GetUser } from "@/modules/auth/decorators/get-user/get-user.decorator";
|
||||
import { Permissions } from "@/modules/auth/decorators/permissions/permissions.decorator";
|
||||
import { AccessTokenGuard } from "@/modules/auth/guards/access-token/access-token.guard";
|
||||
import { PermissionsGuard } from "@/modules/auth/guards/permissions/permissions.guard";
|
||||
import { BillingService } from "@/modules/billing/services/billing.service";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.service";
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
@@ -77,12 +78,13 @@ const DEFAULT_PLATFORM_PARAMS = {
|
||||
@UseGuards(PermissionsGuard)
|
||||
@DocsTags("Bookings")
|
||||
export class BookingsController {
|
||||
private readonly logger = new Logger("ee bookings controller");
|
||||
private readonly logger = new Logger("BookingsController");
|
||||
|
||||
constructor(
|
||||
private readonly oAuthFlowService: OAuthFlowService,
|
||||
private readonly prismaReadService: PrismaReadService,
|
||||
private readonly oAuthClientRepository: OAuthClientRepository
|
||||
private readonly oAuthClientRepository: OAuthClientRepository,
|
||||
private readonly billingService: BillingService
|
||||
) {}
|
||||
|
||||
@Get("/")
|
||||
@@ -153,12 +155,14 @@ export class BookingsController {
|
||||
const booking = await handleNewBooking(
|
||||
await this.createNextApiBookingRequest(req, oAuthClientId, locationUrl)
|
||||
);
|
||||
|
||||
void (await this.billingService.increaseUsageByClientId(oAuthClientId!));
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: booking,
|
||||
};
|
||||
} catch (err) {
|
||||
handleBookingErrors(err);
|
||||
this.handleBookingErrors(err);
|
||||
}
|
||||
throw new InternalServerErrorException("Could not create booking.");
|
||||
}
|
||||
@@ -179,7 +183,7 @@ export class BookingsController {
|
||||
status: SUCCESS_STATUS,
|
||||
};
|
||||
} catch (err) {
|
||||
handleBookingErrors(err);
|
||||
this.handleBookingErrors(err);
|
||||
}
|
||||
} else {
|
||||
throw new NotFoundException("Booking ID is required.");
|
||||
@@ -198,12 +202,15 @@ export class BookingsController {
|
||||
const createdBookings: BookingResponse[] = await handleNewRecurringBooking(
|
||||
await this.createNextApiBookingRequest(req, oAuthClientId)
|
||||
);
|
||||
|
||||
void (await this.billingService.increaseUsageByClientId(oAuthClientId!));
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: createdBookings,
|
||||
};
|
||||
} catch (err) {
|
||||
handleBookingErrors(err, "recurring");
|
||||
this.handleBookingErrors(err, "recurring");
|
||||
}
|
||||
throw new InternalServerErrorException("Could not create recurring booking.");
|
||||
}
|
||||
@@ -220,17 +227,20 @@ export class BookingsController {
|
||||
const instantMeeting = await handleInstantMeeting(
|
||||
await this.createNextApiBookingRequest(req, oAuthClientId)
|
||||
);
|
||||
|
||||
void (await this.billingService.increaseUsageByClientId(oAuthClientId!));
|
||||
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
data: instantMeeting,
|
||||
};
|
||||
} catch (err) {
|
||||
handleBookingErrors(err, "instant");
|
||||
this.handleBookingErrors(err, "instant");
|
||||
}
|
||||
throw new InternalServerErrorException("Could not create instant booking.");
|
||||
}
|
||||
|
||||
async getOwnerId(req: Request): Promise<number | undefined> {
|
||||
private async getOwnerId(req: Request): Promise<number | undefined> {
|
||||
try {
|
||||
const accessToken = req.get("Authorization")?.replace("Bearer ", "");
|
||||
if (accessToken) {
|
||||
@@ -241,7 +251,7 @@ export class BookingsController {
|
||||
}
|
||||
}
|
||||
|
||||
async getOAuthClientsParams(req: BookingRequest, clientId: string): Promise<OAuthRequestParams> {
|
||||
private async getOAuthClientsParams(clientId: string): Promise<OAuthRequestParams> {
|
||||
const res = DEFAULT_PLATFORM_PARAMS;
|
||||
try {
|
||||
const client = await this.oAuthClientRepository.getOAuthClient(clientId);
|
||||
@@ -260,32 +270,32 @@ export class BookingsController {
|
||||
}
|
||||
}
|
||||
|
||||
async createNextApiBookingRequest(
|
||||
private async createNextApiBookingRequest(
|
||||
req: BookingRequest,
|
||||
oAuthClientId?: string,
|
||||
platformBookingLocation?: string
|
||||
): Promise<NextApiRequest & { userId?: number } & OAuthRequestParams> {
|
||||
const userId = (await this.getOwnerId(req)) ?? -1;
|
||||
const oAuthParams = oAuthClientId
|
||||
? await this.getOAuthClientsParams(req, oAuthClientId)
|
||||
? await this.getOAuthClientsParams(oAuthClientId)
|
||||
: DEFAULT_PLATFORM_PARAMS;
|
||||
Object.assign(req, { userId, ...oAuthParams, platformBookingLocation });
|
||||
req.body = { ...req.body, noEmail: !oAuthParams.arePlatformEmailsEnabled };
|
||||
return req as unknown as NextApiRequest & { userId?: number } & OAuthRequestParams;
|
||||
}
|
||||
}
|
||||
|
||||
function handleBookingErrors(err: Error | HttpError | unknown, type?: "recurring" | `instant`): void {
|
||||
const errMsg = `Error while creating ${type ? type + " " : ""}booking.`;
|
||||
if (err instanceof HttpError) {
|
||||
const httpError = err as HttpError;
|
||||
throw new HttpException(httpError?.message ?? errMsg, httpError?.statusCode ?? 500);
|
||||
private handleBookingErrors(err: Error | HttpError | unknown, type?: "recurring" | `instant`): void {
|
||||
const errMsg = `Error while creating ${type ? type + " " : ""}booking.`;
|
||||
if (err instanceof HttpError) {
|
||||
const httpError = err as HttpError;
|
||||
throw new HttpException(httpError?.message ?? errMsg, httpError?.statusCode ?? 500);
|
||||
}
|
||||
|
||||
if (err instanceof Error) {
|
||||
const error = err as Error;
|
||||
throw new InternalServerErrorException(error?.message ?? errMsg);
|
||||
}
|
||||
|
||||
throw new InternalServerErrorException(errMsg);
|
||||
}
|
||||
|
||||
if (err instanceof Error) {
|
||||
const error = err as Error;
|
||||
throw new InternalServerErrorException(error?.message ?? errMsg);
|
||||
}
|
||||
|
||||
throw new InternalServerErrorException(errMsg);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,9 @@ export type Environment = {
|
||||
SENTRY_DSN: string;
|
||||
LOG_LEVEL: keyof typeof logLevels;
|
||||
REDIS_URL: string;
|
||||
STRIPE_API_KEY: string;
|
||||
STRIPE_WEBHOOK_SECRET: string;
|
||||
WEB_APP_URL: string;
|
||||
};
|
||||
|
||||
export const getEnv = <K extends keyof Environment>(key: K, fallback?: Environment[K]): Environment[K] => {
|
||||
|
||||
@@ -16,6 +16,7 @@ import { loggerConfig } from "./lib/logger";
|
||||
const run = async () => {
|
||||
const app = await NestFactory.create<NestExpressApplication>(AppModule, {
|
||||
logger: WinstonModule.createLogger(loggerConfig()),
|
||||
bodyParser: false,
|
||||
});
|
||||
|
||||
const logger = new Logger("App");
|
||||
@@ -23,7 +24,9 @@ const run = async () => {
|
||||
try {
|
||||
bootstrap(app);
|
||||
const port = app.get(ConfigService<AppConfig, true>).get("api.port", { infer: true });
|
||||
generateSwagger(app);
|
||||
if (process.env.SKIP_DOCS_GENERATION !== "1") {
|
||||
void generateSwagger(app);
|
||||
}
|
||||
await app.listen(port);
|
||||
logger.log(`Application started on port: ${port}`);
|
||||
} catch (error) {
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Injectable, NestMiddleware } from "@nestjs/common";
|
||||
import * as bodyParser from "body-parser";
|
||||
import type { Request, Response } from "express";
|
||||
|
||||
@Injectable()
|
||||
export class JsonBodyMiddleware implements NestMiddleware {
|
||||
use(req: Request, res: Response, next: () => any) {
|
||||
bodyParser.json()(req, res, next);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Injectable, NestMiddleware } from "@nestjs/common";
|
||||
import * as bodyParser from "body-parser";
|
||||
import type { Request, Response } from "express";
|
||||
|
||||
@Injectable()
|
||||
export class RawBodyMiddleware implements NestMiddleware {
|
||||
use(req: Request, res: Response, next: () => any) {
|
||||
bodyParser.raw({ type: "*/*" })(req, res, next);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { BillingRepository } from "@/modules/billing/billing.repository";
|
||||
import { BillingController } from "@/modules/billing/controllers/billing.controller";
|
||||
import { BillingConfigService } from "@/modules/billing/services/billing.config.service";
|
||||
import { BillingService } from "@/modules/billing/services/billing.service";
|
||||
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
||||
import { OrganizationsModule } from "@/modules/organizations/organizations.module";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { StripeModule } from "@/modules/stripe/stripe.module";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule, StripeModule, MembershipsModule, OrganizationsModule],
|
||||
providers: [BillingConfigService, BillingService, BillingRepository],
|
||||
exports: [BillingService, BillingRepository],
|
||||
controllers: [BillingController],
|
||||
})
|
||||
export class BillingModule {}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { PlatformPlan } from "@/modules/billing/types";
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
@Injectable()
|
||||
export class BillingRepository {
|
||||
constructor(private readonly dbRead: PrismaReadService, private readonly dbWrite: PrismaWriteService) {}
|
||||
|
||||
getBillingForTeam = (teamId: number) =>
|
||||
this.dbRead.prisma.platformBilling.findUnique({
|
||||
where: {
|
||||
id: teamId,
|
||||
},
|
||||
});
|
||||
|
||||
async updateTeamBilling(
|
||||
teamId: number,
|
||||
billingStart: number,
|
||||
billingEnd: number,
|
||||
plan: PlatformPlan,
|
||||
subscription?: string
|
||||
) {
|
||||
return this.dbWrite.prisma.platformBilling.update({
|
||||
where: {
|
||||
id: teamId,
|
||||
},
|
||||
data: {
|
||||
billingCycleStart: billingStart,
|
||||
billingCycleEnd: billingEnd,
|
||||
subscriptionId: subscription,
|
||||
plan: plan.toString(),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import { AppConfig } from "@/config/type";
|
||||
import { Roles } from "@/modules/auth/decorators/roles/roles.decorator";
|
||||
import { NextAuthGuard } from "@/modules/auth/guards/next-auth/next-auth.guard";
|
||||
import { OrganizationRolesGuard } from "@/modules/auth/guards/organization-roles/organization-roles.guard";
|
||||
import { SubscribeToPlanInput } from "@/modules/billing/controllers/inputs/subscribe-to-plan.input";
|
||||
import { CheckPlatformBillingResponseDto } from "@/modules/billing/controllers/outputs/CheckPlatformBillingResponse.dto";
|
||||
import { SubscribeTeamToBillingResponseDto } from "@/modules/billing/controllers/outputs/SubscribeTeamToBillingResponse.dto";
|
||||
import { BillingService } from "@/modules/billing/services/billing.service";
|
||||
import { PlatformPlan } from "@/modules/billing/types";
|
||||
import {
|
||||
BadRequestException,
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
Param,
|
||||
Post,
|
||||
Req,
|
||||
UseGuards,
|
||||
Headers,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Logger,
|
||||
} from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { ApiExcludeController } from "@nestjs/swagger";
|
||||
import { Request } from "express";
|
||||
import { Stripe } from "stripe";
|
||||
|
||||
import { ApiResponse } from "@calcom/platform-types";
|
||||
|
||||
@Controller({
|
||||
path: "/billing",
|
||||
version: "2",
|
||||
})
|
||||
@ApiExcludeController(true)
|
||||
export class BillingController {
|
||||
private readonly stripeWhSecret: string;
|
||||
private logger = new Logger("Billing Controller");
|
||||
|
||||
constructor(
|
||||
private readonly billingService: BillingService,
|
||||
private readonly configService: ConfigService<AppConfig>
|
||||
) {
|
||||
this.stripeWhSecret = configService.get("stripe.webhookSecret", { infer: true }) ?? "";
|
||||
}
|
||||
|
||||
@Get("/:teamId/check")
|
||||
@UseGuards(NextAuthGuard, OrganizationRolesGuard)
|
||||
@Roles(["OWNER", "ADMIN"])
|
||||
async checkTeamBilling(
|
||||
@Param("teamId") teamId: number
|
||||
): Promise<ApiResponse<CheckPlatformBillingResponseDto>> {
|
||||
const { status, plan } = await this.billingService.getBillingData(teamId);
|
||||
|
||||
return {
|
||||
status: "success",
|
||||
data: {
|
||||
valid: status === "valid",
|
||||
plan,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@Post("/:teamId/subscribe")
|
||||
@UseGuards(NextAuthGuard, OrganizationRolesGuard)
|
||||
@Roles(["OWNER", "ADMIN"])
|
||||
async subscribeTeamToStripe(
|
||||
@Param("teamId") teamId: number,
|
||||
@Body() input: SubscribeToPlanInput
|
||||
): Promise<ApiResponse<SubscribeTeamToBillingResponseDto | undefined>> {
|
||||
const { status } = await this.billingService.getBillingData(teamId);
|
||||
|
||||
if (status === "valid") {
|
||||
throw new BadRequestException("This team is already subscribed to a plan.");
|
||||
}
|
||||
|
||||
const { action, url } = await this.billingService.createSubscriptionForTeam(teamId, input.plan);
|
||||
if (action === "redirect") {
|
||||
return {
|
||||
status: "success",
|
||||
data: {
|
||||
action: "redirect",
|
||||
url,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
status: "success",
|
||||
};
|
||||
}
|
||||
|
||||
@Post("/webhook")
|
||||
@HttpCode(HttpStatus.OK)
|
||||
async stripeWebhook(
|
||||
@Req() request: Request,
|
||||
@Headers("stripe-signature") stripeSignature: string
|
||||
): Promise<ApiResponse> {
|
||||
const event = await this.billingService.stripeService.stripe.webhooks.constructEventAsync(
|
||||
request.body,
|
||||
stripeSignature,
|
||||
this.stripeWhSecret
|
||||
);
|
||||
|
||||
if (event.type === "customer.subscription.created" || event.type === "customer.subscription.updated") {
|
||||
const subscription = event.data.object as Stripe.Subscription;
|
||||
const teamId = Number.parseInt(subscription.metadata.teamId);
|
||||
const plan = subscription.metadata.plan;
|
||||
if (!plan || !teamId) {
|
||||
throw new Error("Invalid webhook received.");
|
||||
}
|
||||
|
||||
await this.billingService.setSubscriptionForTeam(
|
||||
teamId,
|
||||
subscription,
|
||||
PlatformPlan[plan as keyof typeof PlatformPlan]
|
||||
);
|
||||
|
||||
return {
|
||||
status: "success",
|
||||
};
|
||||
}
|
||||
|
||||
throw new BadRequestException(`Unhandled event type ${event.type}`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { PlatformPlan } from "@/modules/billing/types";
|
||||
import { IsEnum } from "class-validator";
|
||||
|
||||
export class SubscribeToPlanInput {
|
||||
@IsEnum(PlatformPlan)
|
||||
plan!: PlatformPlan;
|
||||
}
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
export class CheckPlatformBillingResponseDto {
|
||||
valid!: boolean;
|
||||
|
||||
plan?: string;
|
||||
}
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
export class SubscribeTeamToBillingResponseDto {
|
||||
url?: string;
|
||||
action?: "redirect";
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { PlatformPlan } from "@/modules/billing/types";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
@Injectable()
|
||||
export class BillingConfigService {
|
||||
private readonly config: Map<PlatformPlan, string>;
|
||||
|
||||
constructor() {
|
||||
this.config = new Map<PlatformPlan, string>();
|
||||
|
||||
const planKeys = Object.keys(PlatformPlan).filter((key) => isNaN(Number(key)));
|
||||
for (const key of planKeys) {
|
||||
this.config.set(
|
||||
PlatformPlan[key as keyof typeof PlatformPlan],
|
||||
process.env[`STRIPE_PRICE_ID_${key}`] ?? ""
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
get(plan: PlatformPlan): string | undefined {
|
||||
return this.config.get(plan);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import { AppConfig } from "@/config/type";
|
||||
import { BillingRepository } from "@/modules/billing/billing.repository";
|
||||
import { BillingConfigService } from "@/modules/billing/services/billing.config.service";
|
||||
import { PlatformPlan } from "@/modules/billing/types";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
|
||||
import { StripeService } from "@/modules/stripe/stripe.service";
|
||||
import { Injectable, InternalServerErrorException, Logger } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { DateTime } from "luxon";
|
||||
import Stripe from "stripe";
|
||||
|
||||
@Injectable()
|
||||
export class BillingService {
|
||||
private logger = new Logger("BillingService");
|
||||
private readonly webAppUrl: string;
|
||||
|
||||
constructor(
|
||||
private readonly teamsRepository: OrganizationsRepository,
|
||||
public readonly stripeService: StripeService,
|
||||
private readonly billingRepository: BillingRepository,
|
||||
private readonly configService: ConfigService<AppConfig>,
|
||||
private readonly billingConfigService: BillingConfigService
|
||||
) {
|
||||
this.webAppUrl = configService.get("app.baseUrl", { infer: true }) ?? "https://app.cal.com";
|
||||
}
|
||||
|
||||
async getBillingData(teamId: number) {
|
||||
const teamWithBilling = await this.teamsRepository.findByIdIncludeBilling(teamId);
|
||||
if (teamWithBilling?.platformBilling) {
|
||||
if (!teamWithBilling?.platformBilling.subscriptionId) {
|
||||
return { team: teamWithBilling, status: "no_subscription", plan: "none" };
|
||||
}
|
||||
|
||||
return { team: teamWithBilling, status: "valid", plan: teamWithBilling.platformBilling.plan };
|
||||
} else {
|
||||
return { team: teamWithBilling, status: "no_billing", plan: "none" };
|
||||
}
|
||||
}
|
||||
|
||||
async createSubscriptionForTeam(teamId: number, plan: PlatformPlan) {
|
||||
const teamWithBilling = await this.teamsRepository.findByIdIncludeBilling(teamId);
|
||||
let brandNewBilling = false;
|
||||
|
||||
let customerId = teamWithBilling?.platformBilling?.customerId;
|
||||
|
||||
if (!teamWithBilling?.platformBilling) {
|
||||
brandNewBilling = true;
|
||||
customerId = await this.teamsRepository.createNewBillingRelation(teamId);
|
||||
|
||||
this.logger.log("Team had no Stripe Customer ID, created one for them.", {
|
||||
id: teamId,
|
||||
stripeId: customerId,
|
||||
});
|
||||
}
|
||||
|
||||
if (brandNewBilling || !teamWithBilling?.platformBilling?.subscriptionId) {
|
||||
const { url } = await this.stripeService.stripe.checkout.sessions.create({
|
||||
customer: customerId,
|
||||
line_items: [
|
||||
{
|
||||
price: this.billingConfigService.get(plan),
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
success_url: `${this.webAppUrl}/settings/platform/oauth-clients`,
|
||||
cancel_url: `${this.webAppUrl}/settings/platform/oauth-clients`,
|
||||
mode: "subscription",
|
||||
metadata: {
|
||||
teamId: teamId.toString(),
|
||||
plan: plan.toString(),
|
||||
},
|
||||
subscription_data: {
|
||||
metadata: {
|
||||
teamId: teamId.toString(),
|
||||
plan: plan.toString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!url) throw new InternalServerErrorException("Failed to create Stripe session.");
|
||||
|
||||
return { action: "redirect", url };
|
||||
}
|
||||
|
||||
return { action: "none" };
|
||||
}
|
||||
|
||||
async setSubscriptionForTeam(teamId: number, subscription: Stripe.Subscription, plan: PlatformPlan) {
|
||||
const billingCycleStart = DateTime.now().get("day");
|
||||
const billingCycleEnd = DateTime.now().plus({ month: 1 }).get("day");
|
||||
|
||||
return this.billingRepository.updateTeamBilling(
|
||||
teamId,
|
||||
billingCycleStart,
|
||||
billingCycleEnd,
|
||||
plan,
|
||||
subscription.id
|
||||
);
|
||||
}
|
||||
|
||||
async increaseUsageForTeam(teamId: number) {
|
||||
// TODO - if we support multiple subscription items per team, we may need to track which plan they're
|
||||
// subscribed to so we can do one less query.
|
||||
const billingSubscription = await this.billingRepository.getBillingForTeam(teamId);
|
||||
if (!billingSubscription || !billingSubscription?.subscriptionId) {
|
||||
throw new Error(`Failed to increase usage for team ${teamId}`);
|
||||
}
|
||||
|
||||
const stripeSubscription = await this.stripeService.stripe.subscriptions.retrieve(
|
||||
billingSubscription.subscriptionId
|
||||
);
|
||||
const items = stripeSubscription.items.data[0]; // first (and only) subscription item.
|
||||
await this.stripeService.stripe.subscriptionItems.createUsageRecord(items.id, {
|
||||
action: "increment",
|
||||
quantity: 1,
|
||||
timestamp: "now",
|
||||
});
|
||||
}
|
||||
|
||||
async increaseUsageByClientId(clientId: string) {
|
||||
const team = await this.teamsRepository.findTeamIdFromClientId(clientId);
|
||||
if (!team.id) return Promise.resolve(); // noop resolution.
|
||||
|
||||
return this.increaseUsageForTeam(team?.id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
export enum PlatformPlan {
|
||||
STARTER = "STARTER",
|
||||
ESSENTIALS = "ESSENTIALS",
|
||||
ENTERPRISE = "ENTERPRISE",
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
import { PlatformEndpointsModule } from "@/ee/platform-endpoints-module";
|
||||
import { BillingModule } from "@/modules/billing/billing.module";
|
||||
import { OAuthClientModule } from "@/modules/oauth-clients/oauth-client.module";
|
||||
import { TimezoneModule } from "@/modules/timezones/timezones.module";
|
||||
import type { MiddlewareConsumer, NestModule } from "@nestjs/common";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({
|
||||
imports: [OAuthClientModule, PlatformEndpointsModule, TimezoneModule],
|
||||
imports: [OAuthClientModule, BillingModule, PlatformEndpointsModule, TimezoneModule],
|
||||
})
|
||||
export class EndpointsModule implements NestModule {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
|
||||
+12
-1
@@ -8,6 +8,7 @@ import { GetOAuthClientResponseDto } from "@/modules/oauth-clients/controllers/o
|
||||
import { GetOAuthClientsResponseDto } from "@/modules/oauth-clients/controllers/oauth-clients/responses/GetOAuthClientsResponse.dto";
|
||||
import { UpdateOAuthClientInput } from "@/modules/oauth-clients/inputs/update-oauth-client.input";
|
||||
import { OAuthClientRepository } from "@/modules/oauth-clients/oauth-client.repository";
|
||||
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
|
||||
import { UserWithProfile } from "@/modules/users/users.repository";
|
||||
import {
|
||||
Body,
|
||||
@@ -22,6 +23,7 @@ import {
|
||||
Logger,
|
||||
UseGuards,
|
||||
NotFoundException,
|
||||
BadRequestException,
|
||||
} from "@nestjs/common";
|
||||
import {
|
||||
ApiTags as DocsTags,
|
||||
@@ -47,7 +49,10 @@ Second, make sure that the logged in user has organizationId set to pass the Org
|
||||
export class OAuthClientsController {
|
||||
private readonly logger = new Logger("OAuthClientController");
|
||||
|
||||
constructor(private readonly oauthClientRepository: OAuthClientRepository) {}
|
||||
constructor(
|
||||
private readonly oauthClientRepository: OAuthClientRepository,
|
||||
private readonly teamsRepository: OrganizationsRepository
|
||||
) {}
|
||||
|
||||
@Post("/")
|
||||
@HttpCode(HttpStatus.CREATED)
|
||||
@@ -65,6 +70,12 @@ export class OAuthClientsController {
|
||||
this.logger.log(
|
||||
`For organisation ${organizationId} creating OAuth Client with data: ${JSON.stringify(body)}`
|
||||
);
|
||||
|
||||
const organization = await this.teamsRepository.findByIdIncludeBilling(organizationId);
|
||||
if (!organization?.platformBilling || !organization?.platformBilling?.subscriptionId) {
|
||||
throw new BadRequestException("Team is not subscribed, cannot create an OAuth Client.");
|
||||
}
|
||||
|
||||
const { id, secret } = await this.oauthClientRepository.createOAuthClient(organizationId, body);
|
||||
return {
|
||||
status: SUCCESS_STATUS,
|
||||
|
||||
+7
-1
@@ -2,7 +2,7 @@ import { ApiProperty } from "@nestjs/swagger";
|
||||
import { Type } from "class-transformer";
|
||||
import { IsIn, ValidateNested, IsNotEmptyObject, IsString } from "class-validator";
|
||||
|
||||
import { SUCCESS_STATUS, ERROR_STATUS } from "@calcom/platform-constants";
|
||||
import { SUCCESS_STATUS, ERROR_STATUS, REDIRECT_STATUS } from "@calcom/platform-constants";
|
||||
|
||||
class DataDto {
|
||||
@ApiProperty({
|
||||
@@ -34,3 +34,9 @@ export class CreateOAuthClientResponseDto {
|
||||
@Type(() => DataDto)
|
||||
data!: DataDto;
|
||||
}
|
||||
|
||||
export class CreateOauthClientRedirect {
|
||||
status!: typeof REDIRECT_STATUS;
|
||||
|
||||
url!: string;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { EventTypesModule } from "@/ee/event-types/event-types.module";
|
||||
import { SchedulesModule } from "@/ee/schedules/schedules.module";
|
||||
import { AuthModule } from "@/modules/auth/auth.module";
|
||||
import { BillingModule } from "@/modules/billing/billing.module";
|
||||
import { MembershipsModule } from "@/modules/memberships/memberships.module";
|
||||
import { OAuthClientUsersController } from "@/modules/oauth-clients/controllers/oauth-client-users/oauth-client-users.controller";
|
||||
import { OAuthClientsController } from "@/modules/oauth-clients/controllers/oauth-clients/oauth-clients.controller";
|
||||
@@ -12,6 +13,7 @@ import { OAuthFlowService } from "@/modules/oauth-clients/services/oauth-flow.se
|
||||
import { OrganizationsModule } from "@/modules/organizations/organizations.module";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { RedisModule } from "@/modules/redis/redis.module";
|
||||
import { StripeModule } from "@/modules/stripe/stripe.module";
|
||||
import { TokensModule } from "@/modules/tokens/tokens.module";
|
||||
import { TokensRepository } from "@/modules/tokens/tokens.repository";
|
||||
import { UsersModule } from "@/modules/users/users.module";
|
||||
@@ -28,6 +30,8 @@ import { Global, Module } from "@nestjs/common";
|
||||
MembershipsModule,
|
||||
EventTypesModule,
|
||||
OrganizationsModule,
|
||||
StripeModule,
|
||||
BillingModule,
|
||||
SchedulesModule,
|
||||
],
|
||||
providers: [
|
||||
|
||||
@@ -59,7 +59,7 @@ export class OAuthClientRepository {
|
||||
}
|
||||
|
||||
async getOAuthClientWithRefreshSecret(clientId: string, clientSecret: string, refreshToken: string) {
|
||||
return await this.dbRead.prisma.platformOAuthClient.findFirst({
|
||||
return this.dbRead.prisma.platformOAuthClient.findFirst({
|
||||
where: {
|
||||
id: clientId,
|
||||
secret: clientSecret,
|
||||
|
||||
@@ -7,8 +7,7 @@ import { UsersRepository } from "@/modules/users/users.repository";
|
||||
import { BadRequestException, Injectable } from "@nestjs/common";
|
||||
import { User } from "@prisma/client";
|
||||
|
||||
import { createNewUsersConnectToOrgIfExists } from "@calcom/platform-libraries";
|
||||
import { slugify } from "@calcom/platform-libraries";
|
||||
import { createNewUsersConnectToOrgIfExists, slugify } from "@calcom/platform-libraries";
|
||||
|
||||
@Injectable()
|
||||
export class OAuthClientUsersService {
|
||||
@@ -100,8 +99,6 @@ export class OAuthClientUsersService {
|
||||
|
||||
getOAuthUserEmail(oAuthClientId: string, userEmail: string) {
|
||||
const [username, emailDomain] = userEmail.split("@");
|
||||
const email = `${username}+${oAuthClientId}@${emailDomain}`;
|
||||
|
||||
return email;
|
||||
return `${username}+${oAuthClientId}@${emailDomain}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { OrganizationsRepository } from "@/modules/organizations/organizations.repository";
|
||||
import { OrganizationsService } from "@/modules/organizations/services/organizations.service";
|
||||
import { PrismaModule } from "@/modules/prisma/prisma.module";
|
||||
import { StripeModule } from "@/modules/stripe/stripe.module";
|
||||
import { Module } from "@nestjs/common";
|
||||
|
||||
@Module({
|
||||
imports: [PrismaModule],
|
||||
imports: [PrismaModule, StripeModule],
|
||||
providers: [OrganizationsRepository, OrganizationsService],
|
||||
exports: [OrganizationsService],
|
||||
exports: [OrganizationsService, OrganizationsRepository],
|
||||
})
|
||||
export class OrganizationsModule {}
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import { PlatformPlan } from "@/modules/billing/types";
|
||||
import { PrismaReadService } from "@/modules/prisma/prisma-read.service";
|
||||
import { PrismaWriteService } from "@/modules/prisma/prisma-write.service";
|
||||
import { StripeService } from "@/modules/stripe/stripe.service";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
|
||||
@Injectable()
|
||||
export class OrganizationsRepository {
|
||||
constructor(private readonly dbRead: PrismaReadService) {}
|
||||
constructor(
|
||||
private readonly dbRead: PrismaReadService,
|
||||
private readonly dbWrite: PrismaWriteService,
|
||||
private readonly stripeService: StripeService
|
||||
) {}
|
||||
|
||||
async findById(organizationId: number) {
|
||||
return this.dbRead.prisma.team.findUnique({
|
||||
@@ -12,4 +19,54 @@ export class OrganizationsRepository {
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async findByIdIncludeBilling(orgId: number) {
|
||||
return this.dbRead.prisma.team.findUnique({
|
||||
where: {
|
||||
id: orgId,
|
||||
},
|
||||
include: {
|
||||
platformBilling: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async createNewBillingRelation(orgId: number, plan?: PlatformPlan) {
|
||||
const { id } = await this.stripeService.stripe.customers.create({
|
||||
metadata: {
|
||||
createdBy: "oauth_client_no_csid", // mark in case this is needed in the future.
|
||||
},
|
||||
});
|
||||
|
||||
await this.dbWrite.prisma.team.update({
|
||||
where: {
|
||||
id: orgId,
|
||||
},
|
||||
data: {
|
||||
platformBilling: {
|
||||
create: {
|
||||
customerId: id,
|
||||
plan: plan ? plan.toString() : "none",
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
async findTeamIdFromClientId(clientId: string) {
|
||||
return this.dbRead.prisma.team.findFirstOrThrow({
|
||||
where: {
|
||||
platformOAuthClient: {
|
||||
some: {
|
||||
id: clientId,
|
||||
},
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { StripeService } from "@/modules/stripe/stripe.service";
|
||||
import { Module } from "@nestjs/common";
|
||||
import { ConfigModule } from "@nestjs/config";
|
||||
|
||||
@Module({
|
||||
imports: [ConfigModule],
|
||||
exports: [StripeService],
|
||||
providers: [StripeService],
|
||||
})
|
||||
export class StripeModule {}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { AppConfig } from "@/config/type";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import Stripe from "stripe";
|
||||
|
||||
@Injectable()
|
||||
export class StripeService {
|
||||
public stripe: Stripe;
|
||||
|
||||
constructor(configService: ConfigService<AppConfig>) {
|
||||
this.stripe = new Stripe(configService.get("stripe.apiKey", { infer: true }) ?? "", {
|
||||
apiVersion: "2020-08-27",
|
||||
});
|
||||
}
|
||||
}
|
||||
+1
-3
@@ -19,7 +19,7 @@ export const useCreateOAuthClient = (
|
||||
},
|
||||
}
|
||||
) => {
|
||||
const mutation = useMutation<
|
||||
return useMutation<
|
||||
ApiResponse<{ clientId: string; clientSecret: string }>,
|
||||
unknown,
|
||||
CreateOAuthClientInput
|
||||
@@ -42,8 +42,6 @@ export const useCreateOAuthClient = (
|
||||
onError?.();
|
||||
},
|
||||
});
|
||||
|
||||
return mutation;
|
||||
};
|
||||
|
||||
export const useUpdateOAuthClient = (
|
||||
|
||||
@@ -9,6 +9,7 @@ export const V2_ENDPOINTS = {
|
||||
|
||||
export const SUCCESS_STATUS = "success";
|
||||
export const ERROR_STATUS = "error";
|
||||
export const REDIRECT_STATUS = "redirect";
|
||||
// Client Errors (4xx)
|
||||
export const BAD_REQUEST = "BAD_REQUEST";
|
||||
export const UNAUTHORIZED = "UNAUTHORIZED";
|
||||
|
||||
@@ -2,7 +2,12 @@ import { Transform } from "class-transformer";
|
||||
import { IsNumber, Min, Max, IsOptional } from "class-validator";
|
||||
import type { Response as BaseResponse } from "express";
|
||||
|
||||
import type { ERROR_STATUS, SUCCESS_STATUS, API_ERROR_CODES } from "@calcom/platform-constants";
|
||||
import type {
|
||||
ERROR_STATUS,
|
||||
SUCCESS_STATUS,
|
||||
API_ERROR_CODES,
|
||||
REDIRECT_STATUS,
|
||||
} from "@calcom/platform-constants";
|
||||
|
||||
export type ApiSuccessResponse<T> = { status: typeof SUCCESS_STATUS; data: T };
|
||||
export type ApiSuccessResponseWithoutData = { status: typeof SUCCESS_STATUS };
|
||||
@@ -23,6 +28,7 @@ export type ApiErrorResponse = {
|
||||
};
|
||||
|
||||
export type ApiRedirectResponseType = {
|
||||
status: typeof REDIRECT_STATUS;
|
||||
url: string;
|
||||
};
|
||||
|
||||
@@ -32,6 +38,8 @@ export type ApiResponse<T = undefined> = T extends undefined
|
||||
? ApiSuccessResponseWithoutData | ApiErrorResponse
|
||||
: ApiSuccessResponse<T> | ApiErrorResponse;
|
||||
|
||||
export type ApiResponseMaybeRedirect<T = undefined> = ApiResponse<T> | ApiRedirectResponseType;
|
||||
|
||||
export class Pagination {
|
||||
@Transform(({ value }: { value: string }) => value && parseInt(value))
|
||||
@IsNumber()
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "PlatformBilling" (
|
||||
"id" INTEGER NOT NULL,
|
||||
"customerId" TEXT NOT NULL,
|
||||
"plan" TEXT NOT NULL DEFAULT 'none',
|
||||
"subscriptionId" TEXT,
|
||||
"billingCycleStart" INTEGER,
|
||||
"billingCycleEnd" INTEGER,
|
||||
"overdue" BOOLEAN DEFAULT false,
|
||||
|
||||
CONSTRAINT "PlatformBilling_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "PlatformBilling_id_key" ON "PlatformBilling"("id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "PlatformBilling_customerId_key" ON "PlatformBilling"("customerId");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "PlatformBilling" ADD CONSTRAINT "PlatformBilling_id_fkey" FOREIGN KEY ("id") REFERENCES "Team"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -400,6 +400,7 @@ model Team {
|
||||
isPlatform Boolean @default(false)
|
||||
platformOAuthClient PlatformOAuthClient[]
|
||||
smsLockState SMSLockState @default(UNLOCKED)
|
||||
platformBilling PlatformBilling?
|
||||
|
||||
@@unique([slug, parentId])
|
||||
@@index([parentId])
|
||||
@@ -1357,3 +1358,17 @@ enum SMSLockState {
|
||||
UNLOCKED
|
||||
REVIEW_NEEDED
|
||||
}
|
||||
|
||||
model PlatformBilling {
|
||||
id Int @id @unique // team id
|
||||
|
||||
customerId String @unique
|
||||
subscriptionId String?
|
||||
plan String
|
||||
|
||||
billingCycleStart Int?
|
||||
billingCycleEnd Int?
|
||||
overdue Boolean? @default(false)
|
||||
|
||||
team Team @relation(fields: [id], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
@@ -290,25 +290,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@auth/core@npm:^0.1.4":
|
||||
version: 0.1.4
|
||||
resolution: "@auth/core@npm:0.1.4"
|
||||
dependencies:
|
||||
"@panva/hkdf": 1.0.2
|
||||
cookie: 0.5.0
|
||||
jose: 4.11.1
|
||||
oauth4webapi: 2.0.5
|
||||
preact: 10.11.3
|
||||
preact-render-to-string: 5.2.3
|
||||
peerDependencies:
|
||||
nodemailer: 6.8.0
|
||||
peerDependenciesMeta:
|
||||
nodemailer:
|
||||
optional: true
|
||||
checksum: 64854404ea1883e0deb5535b34bed95cd43fc85094aeaf4f15a79e14045020eb944f844defe857edfc8528a0a024be89cbb2a3069dedef0e9217a74ca6c3eb79
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@aw-web-design/x-default-browser@npm:1.4.126":
|
||||
version: 1.4.126
|
||||
resolution: "@aw-web-design/x-default-browser@npm:1.4.126"
|
||||
@@ -3891,7 +3872,7 @@ __metadata:
|
||||
"@nestjs/schematics": ^10.0.0
|
||||
"@nestjs/swagger": ^7.3.0
|
||||
"@nestjs/testing": ^10.0.0
|
||||
"@nestjs/throttler": ^5.1.1
|
||||
"@nestjs/throttler": ^5.1.2
|
||||
"@sentry/node": ^7.86.0
|
||||
"@sentry/tracing": ^7.86.0
|
||||
"@types/cookie-parser": ^1.4.6
|
||||
@@ -3902,6 +3883,7 @@ __metadata:
|
||||
"@types/node": ^20.3.1
|
||||
"@types/passport-jwt": ^3.0.13
|
||||
"@types/supertest": ^2.0.12
|
||||
body-parser: ^1.20.2
|
||||
class-transformer: ^0.5.1
|
||||
class-validator: ^0.14.0
|
||||
cookie-parser: ^1.4.6
|
||||
@@ -3921,6 +3903,7 @@ __metadata:
|
||||
reflect-metadata: ^0.1.13
|
||||
rxjs: ^7.8.1
|
||||
source-map-support: ^0.5.21
|
||||
stripe: ^15.3.0
|
||||
supertest: ^6.3.3
|
||||
ts-jest: ^29.1.1
|
||||
ts-loader: ^9.4.3
|
||||
@@ -4055,38 +4038,12 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@calcom/auth@workspace:apps/auth":
|
||||
"@calcom/autocheckin@workspace:packages/app-store/autocheckin":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@calcom/auth@workspace:apps/auth"
|
||||
resolution: "@calcom/autocheckin@workspace:packages/app-store/autocheckin"
|
||||
dependencies:
|
||||
"@auth/core": ^0.1.4
|
||||
"@calcom/app-store": "*"
|
||||
"@calcom/app-store-cli": "*"
|
||||
"@calcom/config": "*"
|
||||
"@calcom/core": "*"
|
||||
"@calcom/dayjs": "*"
|
||||
"@calcom/embed-core": "workspace:*"
|
||||
"@calcom/embed-react": "workspace:*"
|
||||
"@calcom/embed-snippet": "workspace:*"
|
||||
"@calcom/features": "*"
|
||||
"@calcom/lib": "*"
|
||||
"@calcom/prisma": "*"
|
||||
"@calcom/trpc": "*"
|
||||
"@calcom/tsconfig": "*"
|
||||
"@calcom/types": "*"
|
||||
"@calcom/ui": "*"
|
||||
"@types/node": 16.9.1
|
||||
"@types/react": 18.0.26
|
||||
"@types/react-dom": ^18.0.9
|
||||
eslint: ^8.34.0
|
||||
eslint-config-next: ^13.2.1
|
||||
next: ^13.5.4
|
||||
next-auth: ^4.22.1
|
||||
postcss: ^8.4.18
|
||||
react: ^18.2.0
|
||||
react-dom: ^18.2.0
|
||||
tailwindcss: ^3.3.3
|
||||
typescript: ^4.9.4
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
@@ -4152,6 +4109,15 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@calcom/clic@workspace:packages/app-store/clic":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@calcom/clic@workspace:packages/app-store/clic"
|
||||
dependencies:
|
||||
"@calcom/lib": "*"
|
||||
"@calcom/types": "*"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@calcom/closecom@workspace:packages/app-store/closecom":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@calcom/closecom@workspace:packages/app-store/closecom"
|
||||
@@ -5195,12 +5161,14 @@ __metadata:
|
||||
class-variance-authority: ^0.4.0
|
||||
cmdk: ^0.2.0
|
||||
cmk: ^0.1.1
|
||||
date-fns: ^3.6.0
|
||||
downshift: ^6.1.9
|
||||
lucide-react: ^0.364.0
|
||||
next: ^13.5.4
|
||||
next-seo: ^6.0.0
|
||||
react: ^18.2.0
|
||||
react-colorful: ^5.6.0
|
||||
react-day-picker: ^8.10.1
|
||||
react-feather: ^2.0.10
|
||||
react-hook-form: ^7.43.3
|
||||
react-select: ^5.7.0
|
||||
@@ -9488,7 +9456,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@nestjs/throttler@npm:^5.1.1":
|
||||
"@nestjs/throttler@npm:^5.1.2":
|
||||
version: 5.1.2
|
||||
resolution: "@nestjs/throttler@npm:5.1.2"
|
||||
peerDependencies:
|
||||
@@ -10374,13 +10342,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@panva/hkdf@npm:1.0.2":
|
||||
version: 1.0.2
|
||||
resolution: "@panva/hkdf@npm:1.0.2"
|
||||
checksum: 75183b4d5ea816ef516dcea70985c610683579a9e2ac540c2d59b9a3ed27eedaff830a43a1c43c1683556a457c92ac66e09109ee995ab173090e4042c4c4bb03
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@panva/hkdf@npm:^1.0.2":
|
||||
version: 1.0.4
|
||||
resolution: "@panva/hkdf@npm:1.0.4"
|
||||
@@ -19705,7 +19666,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"body-parser@npm:1.20.2":
|
||||
"body-parser@npm:1.20.2, body-parser@npm:^1.20.2":
|
||||
version: 1.20.2
|
||||
resolution: "body-parser@npm:1.20.2"
|
||||
dependencies:
|
||||
@@ -22766,6 +22727,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"date-fns@npm:^3.6.0":
|
||||
version: 3.6.0
|
||||
resolution: "date-fns@npm:3.6.0"
|
||||
checksum: 0daa1e9a436cf99f9f2ae9232b55e11f3dd46132bee10987164f3eebd29f245b2e066d7d7db40782627411ecf18551d8f4c9fcdf2226e48bb66545407d448ab7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"datocms-listen@npm:^0.1.9":
|
||||
version: 0.1.15
|
||||
resolution: "datocms-listen@npm:0.1.15"
|
||||
@@ -31146,13 +31114,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jose@npm:4.11.1":
|
||||
version: 4.11.1
|
||||
resolution: "jose@npm:4.11.1"
|
||||
checksum: cd15cba258d0fd20f6168631ce2e94fda8442df80e43c1033c523915cecdf390a1cc8efe0eab0c2d65935ca973d791c668aea80724d2aa9c2879d4e70f3081d7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jose@npm:5.2.1":
|
||||
version: 5.2.1
|
||||
resolution: "jose@npm:5.2.1"
|
||||
@@ -36152,13 +36113,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"oauth4webapi@npm:2.0.5":
|
||||
version: 2.0.5
|
||||
resolution: "oauth4webapi@npm:2.0.5"
|
||||
checksum: 32d0cb7b1cca42d51dfb88075ca2d69fe33172a807e8ea50e317d17cab3bc80588ab8ebcb7eb4600c371a70af4674595b4b341daf6f3a655f1efa1ab715bb6c9
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"oauth@npm:^0.9.15":
|
||||
version: 0.9.15
|
||||
resolution: "oauth@npm:0.9.15"
|
||||
@@ -38333,17 +38287,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"preact-render-to-string@npm:5.2.3":
|
||||
version: 5.2.3
|
||||
resolution: "preact-render-to-string@npm:5.2.3"
|
||||
dependencies:
|
||||
pretty-format: ^3.8.0
|
||||
peerDependencies:
|
||||
preact: ">=10"
|
||||
checksum: 6e46288d8956adde35b9fe3a21aecd9dea29751b40f0f155dea62f3896f27cb8614d457b32f48d33909d2da81135afcca6c55077520feacd7d15164d1371fb44
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"preact-render-to-string@npm:^5.1.19":
|
||||
version: 5.2.6
|
||||
resolution: "preact-render-to-string@npm:5.2.6"
|
||||
@@ -38355,7 +38298,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"preact@npm:10.11.3, preact@npm:^10.6.3":
|
||||
"preact@npm:^10.6.3":
|
||||
version: 10.11.3
|
||||
resolution: "preact@npm:10.11.3"
|
||||
checksum: 9387115aa0581e8226309e6456e9856f17dfc0e3d3e63f774de80f3d462a882ba7c60914c05942cb51d51e23e120dcfe904b8d392d46f29ad15802941fe7a367
|
||||
@@ -39393,6 +39336,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-day-picker@npm:^8.10.1":
|
||||
version: 8.10.1
|
||||
resolution: "react-day-picker@npm:8.10.1"
|
||||
peerDependencies:
|
||||
date-fns: ^2.28.0 || ^3.0.0
|
||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||
checksum: daf7fb5aad1e355df9149e954fafc2cd40ceb90de799db4f36ef86d24729aaecfdf022fa9e63f24d093d81dc2beb60f65ae796741dff0f7305a2af80b333fb4c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"react-debounce-input@npm:=3.2.4":
|
||||
version: 3.2.4
|
||||
resolution: "react-debounce-input@npm:3.2.4"
|
||||
@@ -43652,6 +43605,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"stripe@npm:^15.3.0":
|
||||
version: 15.4.0
|
||||
resolution: "stripe@npm:15.4.0"
|
||||
dependencies:
|
||||
"@types/node": ">=8.1.0"
|
||||
qs: ^6.11.0
|
||||
checksum: 0302b740ef76084b8cc0fbec14f80c864101a0b04b1d85ecd120d921f3d665a512230a905be1388f777d9677850ffcf066c8cce37935e2f8f3d0d3d5d29d7a4b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"stripe@npm:^9.16.0":
|
||||
version: 9.16.0
|
||||
resolution: "stripe@npm:9.16.0"
|
||||
@@ -48180,4 +48143,4 @@ __metadata:
|
||||
resolution: "zwitch@npm:2.0.4"
|
||||
checksum: f22ec5fc2d5f02c423c93d35cdfa83573a3a3bd98c66b927c368ea4d0e7252a500df2a90a6b45522be536a96a73404393c958e945fdba95e6832c200791702b6
|
||||
languageName: node
|
||||
linkType: hard
|
||||
linkType: hard
|
||||
|
||||
Reference in New Issue
Block a user