import { AppConfig } from "@/config/type"; import { API_VERSIONS_VALUES } from "@/lib/api-versions"; import { MembershipRoles } from "@/modules/auth/decorators/roles/membership-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 { StripeService } from "@/modules/stripe/stripe.service"; import { Body, Controller, Get, Param, Post, Req, UseGuards, Headers, HttpCode, HttpStatus, Logger, Delete, BadRequestException, } 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: "/v2/billing", version: API_VERSIONS_VALUES, }) @ApiExcludeController(true) export class BillingController { private readonly stripeWhSecret: string; private logger = new Logger("Billing Controller"); constructor( private readonly billingService: BillingService, public readonly stripeService: StripeService, private readonly configService: ConfigService ) { this.stripeWhSecret = configService.get("stripe.webhookSecret", { infer: true }) ?? ""; } @Get("/:teamId/check") @UseGuards(NextAuthGuard, OrganizationRolesGuard) @MembershipRoles(["OWNER", "ADMIN", "MEMBER"]) async checkTeamBilling( @Param("teamId") teamId: number ): Promise> { const { status, plan } = await this.billingService.getBillingData(teamId); return { status: "success", data: { valid: status === "valid", plan, }, }; } @Post("/:teamId/subscribe") @UseGuards(NextAuthGuard, OrganizationRolesGuard) @MembershipRoles(["OWNER", "ADMIN"]) async subscribeTeamToStripe( @Param("teamId") teamId: number, @Body() input: SubscribeToPlanInput ): Promise> { const customerId = await this.billingService.createTeamBilling(teamId); const url = await this.billingService.redirectToSubscribeCheckout(teamId, input.plan, customerId); return { status: "success", data: { url, }, }; } @Post("/:teamId/upgrade") @UseGuards(NextAuthGuard, OrganizationRolesGuard) @MembershipRoles(["OWNER", "ADMIN"]) async upgradeTeamBillingInStripe( @Param("teamId") teamId: number, @Body() input: SubscribeToPlanInput ): Promise> { const url = await this.billingService.updateSubscriptionForTeam(teamId, input.plan); return { status: "success", data: { url, }, }; } @Delete("/:organizationId/unsubscribe") @UseGuards(NextAuthGuard, OrganizationRolesGuard) @MembershipRoles(["OWNER", "ADMIN"]) async cancelTeamSubscriptionInStripe( @Param("organizationId") organizationId: number ): Promise { await this.billingService.cancelTeamSubscription(organizationId); return { status: "success", }; } @Post("/webhook") @HttpCode(HttpStatus.OK) async stripeWebhook( @Req() request: Request, @Headers("stripe-signature") stripeSignature: string ): Promise { try { if (!stripeSignature) { this.logger.warn("Missing stripe-signature header in webhook request"); return { status: "success", }; } if (!this.stripeWhSecret) { this.logger.error("Missing STRIPE_WEBHOOK_SECRET configuration"); return { status: "success", }; } const event = await this.billingService.stripeService .getStripe() .webhooks.constructEventAsync(request.body, stripeSignature, this.stripeWhSecret); switch (event.type) { case "checkout.session.completed": await this.billingService.handleStripeCheckoutEvents(event); break; case "customer.subscription.updated": await this.billingService.handleStripePaymentPastDue(event); break; case "customer.subscription.deleted": await this.billingService.handleStripeSubscriptionDeleted(event); break; case "invoice.created": await this.billingService.handleStripeSubscriptionForActiveManagedUsers(event); break; case "invoice.payment_failed": await this.billingService.handleStripePaymentFailed(event); break; case "invoice.payment_succeeded": await this.billingService.handleStripePaymentSuccess(event); break; default: break; } return { status: "success", }; } catch (error) { if (error instanceof Stripe.errors.StripeSignatureVerificationError) { this.logger.error("Webhook signature validation failed", error); return { status: "success", }; } throw error; } } }