* refactor: platform billing controller auth guards * fixup! refactor: platform billing controller auth guards
182 lines
5.9 KiB
TypeScript
182 lines
5.9 KiB
TypeScript
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<AppConfig>
|
|
) {
|
|
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<ApiResponse<CheckPlatformBillingResponseDto>> {
|
|
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<ApiResponse<SubscribeTeamToBillingResponseDto | undefined>> {
|
|
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<ApiResponse<SubscribeTeamToBillingResponseDto | undefined>> {
|
|
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<ApiResponse> {
|
|
await this.billingService.cancelTeamSubscription(teamId);
|
|
|
|
return {
|
|
status: "success",
|
|
};
|
|
}
|
|
|
|
@Post("/webhook")
|
|
@HttpCode(HttpStatus.OK)
|
|
async stripeWebhook(
|
|
@Req() request: Request,
|
|
@Headers("stripe-signature") stripeSignature: string
|
|
): Promise<ApiResponse> {
|
|
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;
|
|
}
|
|
}
|
|
}
|