From 679456e81945f08ecb60ee2d111c18e870a8e6c3 Mon Sep 17 00:00:00 2001 From: martmull Date: Thu, 22 Feb 2024 20:11:26 +0100 Subject: [PATCH] 46 add stripe product endpoint (#4133) * Add self billing feature flag * Add two core tables for billing * Remove useless imports * Remove graphql decorators * Rename subscriptionProduct table * WIP: Add stripe config * Add controller to get product prices * Add billing service * Remove unecessary package * Simplify stripe service * Code review returns * Use nestjs param * Rename subscription to basePlan * Rename env variable --- .../src/core/billing/billing.module.ts | 9 +++- .../src/core/billing/billing.service.ts | 43 +++++++++++++++++ .../controllers/product-price.controller.ts | 46 +++++++++++++++++++ .../src/core/billing/stripe/stripe.module.ts | 9 ++++ .../src/core/billing/stripe/stripe.service.ts | 14 ++++++ .../environment/environment.service.ts | 11 +++++ .../environment/environment.validation.ts | 13 +++++- 7 files changed, 143 insertions(+), 2 deletions(-) create mode 100644 packages/twenty-server/src/core/billing/billing.service.ts create mode 100644 packages/twenty-server/src/core/billing/controllers/product-price.controller.ts create mode 100644 packages/twenty-server/src/core/billing/stripe/stripe.module.ts create mode 100644 packages/twenty-server/src/core/billing/stripe/stripe.service.ts diff --git a/packages/twenty-server/src/core/billing/billing.module.ts b/packages/twenty-server/src/core/billing/billing.module.ts index cfeae8fb9..ed7ba9299 100644 --- a/packages/twenty-server/src/core/billing/billing.module.ts +++ b/packages/twenty-server/src/core/billing/billing.module.ts @@ -1,6 +1,13 @@ import { Module } from '@nestjs/common'; +import { ProductPriceController } from 'src/core/billing/controllers/product-price.controller'; +import { EnvironmentModule } from 'src/integrations/environment/environment.module'; +import { BillingService } from 'src/core/billing/billing.service'; +import { StripeModule } from 'src/core/billing/stripe/stripe.module'; + @Module({ - imports: [], + imports: [StripeModule], + controllers: [ProductPriceController], + providers: [EnvironmentModule, BillingService], }) export class BillingModule {} diff --git a/packages/twenty-server/src/core/billing/billing.service.ts b/packages/twenty-server/src/core/billing/billing.service.ts new file mode 100644 index 000000000..fa25e7a9e --- /dev/null +++ b/packages/twenty-server/src/core/billing/billing.service.ts @@ -0,0 +1,43 @@ +import { Injectable } from '@nestjs/common'; + +import Stripe from 'stripe'; + +import { EnvironmentService } from 'src/integrations/environment/environment.service'; + +export type PriceData = Partial< + Record +>; +export enum AvailableProduct { + BasePlan = 'base-plan', +} + +@Injectable() +export class BillingService { + constructor(private readonly environmentService: EnvironmentService) {} + + getProductStripeId(product: AvailableProduct) { + if (product === AvailableProduct.BasePlan) { + return this.environmentService.getBillingStripeBasePlanProductId(); + } + } + + formatProductPrices(prices: Stripe.Price[]) { + const result: PriceData = {}; + + prices.forEach((item) => { + const recurringInterval = item.recurring?.interval; + + if (!recurringInterval) { + return; + } + if ( + !result[recurringInterval] || + item.created > (result[recurringInterval]?.created || 0) + ) { + result[recurringInterval] = item; + } + }); + + return result; + } +} diff --git a/packages/twenty-server/src/core/billing/controllers/product-price.controller.ts b/packages/twenty-server/src/core/billing/controllers/product-price.controller.ts new file mode 100644 index 000000000..5ee8cb754 --- /dev/null +++ b/packages/twenty-server/src/core/billing/controllers/product-price.controller.ts @@ -0,0 +1,46 @@ +import { Controller, Get, Param, Res } from '@nestjs/common'; + +import { Response } from 'express'; + +import { + AvailableProduct, + BillingService, + PriceData, +} from 'src/core/billing/billing.service'; +import { StripeService } from 'src/core/billing/stripe/stripe.service'; + +@Controller('billing/product-prices') +export class ProductPriceController { + constructor( + private readonly stripeService: StripeService, + private readonly billingService: BillingService, + ) {} + + @Get(':product') + async get( + @Param() params: { product: AvailableProduct }, + @Res() res: Response, + ) { + const stripeProductId = this.billingService.getProductStripeId( + params.product, + ); + + if (!stripeProductId) { + res.status(404).send({ + error: `Product '${ + params.product + }' not found, available products are ['${Object.values( + AvailableProduct, + ).join("','")}']`, + }); + + return; + } + + const productPrices = await this.stripeService.stripe.prices.search({ + query: `product: '${stripeProductId}'`, + }); + + res.json(this.billingService.formatProductPrices(productPrices.data)); + } +} diff --git a/packages/twenty-server/src/core/billing/stripe/stripe.module.ts b/packages/twenty-server/src/core/billing/stripe/stripe.module.ts new file mode 100644 index 000000000..e5cc9df34 --- /dev/null +++ b/packages/twenty-server/src/core/billing/stripe/stripe.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; + +import { StripeService } from 'src/core/billing/stripe/stripe.service'; + +@Module({ + providers: [StripeService], + exports: [StripeService], +}) +export class StripeModule {} diff --git a/packages/twenty-server/src/core/billing/stripe/stripe.service.ts b/packages/twenty-server/src/core/billing/stripe/stripe.service.ts new file mode 100644 index 000000000..52d0e138f --- /dev/null +++ b/packages/twenty-server/src/core/billing/stripe/stripe.service.ts @@ -0,0 +1,14 @@ +import { Injectable } from '@nestjs/common'; + +import Stripe from 'stripe'; + +import { EnvironmentService } from 'src/integrations/environment/environment.service'; + +@Injectable() +export class StripeService { + public readonly stripe: Stripe; + + constructor(private readonly environmentService: EnvironmentService) { + this.stripe = new Stripe(this.environmentService.getStripeApiKey(), {}); + } +} diff --git a/packages/twenty-server/src/integrations/environment/environment.service.ts b/packages/twenty-server/src/integrations/environment/environment.service.ts index 0a372f20e..b8c4f676e 100644 --- a/packages/twenty-server/src/integrations/environment/environment.service.ts +++ b/packages/twenty-server/src/integrations/environment/environment.service.ts @@ -305,4 +305,15 @@ export class EnvironmentService { this.configService.get('MUTATION_MAXIMUM_RECORD_AFFECTED') ?? 100 ); } + + getBillingStripeBasePlanProductId(): string { + return ( + this.configService.get('BILLING_STRIPE_BASE_PLAN_PRODUCT_ID') ?? + '' + ); + } + + getStripeApiKey(): string { + return this.configService.get('STRIPE_API_KEY') ?? ''; + } } diff --git a/packages/twenty-server/src/integrations/environment/environment.validation.ts b/packages/twenty-server/src/integrations/environment/environment.validation.ts index de42aaf0f..483511727 100644 --- a/packages/twenty-server/src/integrations/environment/environment.validation.ts +++ b/packages/twenty-server/src/integrations/environment/environment.validation.ts @@ -48,6 +48,14 @@ export class EnvironmentVariables { @IsString() BILLING_URL?: string; + @IsOptional() + @IsString() + BILLING_STRIPE_BASE_PLAN_PRODUCT_ID?: string; + + @IsOptional() + @IsString() + STRIPE_API_KEY?: string; + @CastToBoolean() @IsOptional() @IsBoolean() @@ -83,21 +91,25 @@ export class EnvironmentVariables { // Json Web Token @IsString() ACCESS_TOKEN_SECRET: string; + @IsDuration() @IsOptional() ACCESS_TOKEN_EXPIRES_IN: string; @IsString() REFRESH_TOKEN_SECRET: string; + @IsDuration() @IsOptional() REFRESH_TOKEN_EXPIRES_IN: string; + @IsDuration() @IsOptional() REFRESH_TOKEN_COOL_DOWN: string; @IsString() LOGIN_TOKEN_SECRET: string; + @IsDuration() @IsOptional() LOGIN_TOKEN_EXPIRES_IN: string; @@ -205,7 +217,6 @@ export class EnvironmentVariables { @IsNumber() MUTATION_MAXIMUM_RECORD_AFFECTED: number; } - export const validate = (config: Record) => { const validatedConfig = plainToClass(EnvironmentVariables, config);