import { AppConfig } from "@/config/type"; import { API_VERSIONS_VALUES } from "@/lib/api-versions"; import { ApiAuthGuardOnlyAllow } from "@/modules/auth/decorators/api-auth-guard-only-allow.decorator"; import { MembershipRoles } from "@/modules/auth/decorators/roles/membership-roles.decorator"; import { ApiAuthGuard } from "@/modules/auth/guards/api-auth/api-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 { IsUserInBillingOrg } from "@/modules/billing/guards/is-user-in-billing-org"; import { IBillingService } from "@/modules/billing/interfaces/billing-service.interface"; import { StripeService } from "@/modules/stripe/stripe.service"; import { Body, Controller, Get, Param, Post, Req, UseGuards, Headers, HttpCode, HttpStatus, Inject, Logger, Delete, ParseIntPipe, } 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( @Inject("IBillingService") private readonly billingService: IBillingService, public readonly stripeService: StripeService, private readonly configService: ConfigService ) { this.stripeWhSecret = configService.get("stripe.webhookSecret", { infer: true }) ?? ""; } @Get("/:teamId/check") @UseGuards(ApiAuthGuard, OrganizationRolesGuard, IsUserInBillingOrg) @MembershipRoles(["OWNER", "ADMIN", "MEMBER"]) @ApiAuthGuardOnlyAllow(["NEXT_AUTH"]) async checkTeamBilling( @Param("teamId", ParseIntPipe) teamId: number ): Promise> { const { status, plan } = await this.billingService.getBillingData(teamId); return { status: "success", data: { valid: status === "valid", plan, }, }; } @Post("/:teamId/subscribe") @UseGuards(ApiAuthGuard, OrganizationRolesGuard, IsUserInBillingOrg) @MembershipRoles(["OWNER", "ADMIN"]) @ApiAuthGuardOnlyAllow(["NEXT_AUTH"]) 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(ApiAuthGuard, OrganizationRolesGuard, IsUserInBillingOrg) @MembershipRoles(["OWNER", "ADMIN"]) @ApiAuthGuardOnlyAllow(["NEXT_AUTH"]) 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("/:teamId/unsubscribe") @UseGuards(ApiAuthGuard, OrganizationRolesGuard, IsUserInBillingOrg) @MembershipRoles(["OWNER", "ADMIN"]) @ApiAuthGuardOnlyAllow(["NEXT_AUTH"]) async cancelTeamSubscriptionInStripe(@Param("teamId") teamId: number): Promise { await this.billingService.cancelTeamSubscription(teamId); 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; } } }