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
This commit is contained in:
martmull
2024-02-22 20:11:26 +01:00
committed by GitHub
parent ce7be4c48e
commit 679456e819
7 changed files with 143 additions and 2 deletions

View File

@ -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 {}

View File

@ -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<Stripe.Price.Recurring.Interval, Stripe.Price>
>;
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;
}
}

View File

@ -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<PriceData | { error: string }>,
) {
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));
}
}

View File

@ -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 {}

View File

@ -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(), {});
}
}

View File

@ -305,4 +305,15 @@ export class EnvironmentService {
this.configService.get<number>('MUTATION_MAXIMUM_RECORD_AFFECTED') ?? 100
);
}
getBillingStripeBasePlanProductId(): string {
return (
this.configService.get<string>('BILLING_STRIPE_BASE_PLAN_PRODUCT_ID') ??
''
);
}
getStripeApiKey(): string {
return this.configService.get<string>('STRIPE_API_KEY') ?? '';
}
}

View File

@ -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<string, unknown>) => {
const validatedConfig = plainToClass(EnvironmentVariables, config);