42 add billing portal endpoint (#4315)

* Add create billing portal session endpoint

* Rename checkout to checkoutSession

* Code review returns
This commit is contained in:
martmull
2024-03-05 15:28:45 +01:00
committed by GitHub
parent 1f00af286b
commit 28a093d495
9 changed files with 118 additions and 49 deletions

View File

@ -11,8 +11,9 @@ import { ProductPricesEntity } from 'src/core/billing/dto/product-prices.entity'
import { JwtAuthGuard } from 'src/guards/jwt.auth.guard';
import { AuthUser } from 'src/decorators/auth/auth-user.decorator';
import { User } from 'src/core/user/user.entity';
import { CheckoutInput } from 'src/core/billing/dto/checkout.input';
import { CheckoutEntity } from 'src/core/billing/dto/checkout.entity';
import { CheckoutSessionInput } from 'src/core/billing/dto/checkout-session.input';
import { SessionEntity } from 'src/core/billing/dto/session.entity';
import { BillingSessionInput } from 'src/core/billing/dto/billing-session.input';
@Resolver()
export class BillingResolver {
@ -38,11 +39,25 @@ export class BillingResolver {
};
}
@Mutation(() => CheckoutEntity)
@Query(() => SessionEntity)
@UseGuards(JwtAuthGuard)
async checkout(
async billingPortalSession(
@AuthUser() user: User,
@Args() { recurringInterval, successUrlPath }: CheckoutInput,
@Args() { returnUrlPath }: BillingSessionInput,
) {
return {
url: await this.billingService.computeBillingPortalSessionURL(
user.defaultWorkspaceId,
returnUrlPath,
),
};
}
@Mutation(() => SessionEntity)
@UseGuards(JwtAuthGuard)
async checkoutSession(
@AuthUser() user: User,
@Args() { recurringInterval, successUrlPath }: CheckoutSessionInput,
) {
const stripeProductId = this.billingService.getProductStripeId(
AvailableProduct.BasePlan,
@ -66,7 +81,7 @@ export class BillingResolver {
);
return {
url: await this.billingService.checkout(
url: await this.billingService.computeCheckoutSessionURL(
user,
stripePriceId,
successUrlPath,

View File

@ -11,6 +11,7 @@ import { BillingSubscriptionItem } from 'src/core/billing/entities/billing-subsc
import { Workspace } from 'src/core/workspace/workspace.entity';
import { ProductPriceEntity } from 'src/core/billing/dto/product-price.entity';
import { User } from 'src/core/user/user.entity';
import { assert } from 'src/utils/assert';
export enum AvailableProduct {
BasePlan = 'base-plan',
@ -101,18 +102,45 @@ export class BillingService {
return billingSubscriptionItem;
}
async checkout(user: User, priceId: string, successUrlPath?: string) {
async computeBillingPortalSessionURL(
workspaceId: string,
returnUrlPath?: string,
) {
const billingSubscription =
await this.billingSubscriptionRepository.findOneOrFail({
where: { workspaceId },
});
const session = await this.stripeService.createBillingPortalSession(
billingSubscription.stripeCustomerId,
returnUrlPath,
);
assert(session.url, 'Error: missing billingPortal.session.url');
return session.url;
}
async computeCheckoutSessionURL(
user: User,
priceId: string,
successUrlPath?: string,
): Promise<string> {
const frontBaseUrl = this.environmentService.getFrontBaseUrl();
const successUrl = successUrlPath
? frontBaseUrl + successUrlPath
: frontBaseUrl;
return await this.stripeService.createCheckoutSession(
const session = await this.stripeService.createCheckoutSession(
user,
priceId,
successUrl,
frontBaseUrl,
);
assert(session.url, 'Error: missing checkout.session.url');
return session.url;
}
async deleteSubscription(workspaceId: string) {

View File

@ -0,0 +1,11 @@
import { ArgsType, Field } from '@nestjs/graphql';
import { IsOptional, IsString } from 'class-validator';
@ArgsType()
export class BillingSessionInput {
@Field(() => String, { nullable: true })
@IsString()
@IsOptional()
returnUrlPath?: string;
}

View File

@ -4,7 +4,7 @@ import { IsNotEmpty, IsOptional, IsString } from 'class-validator';
import Stripe from 'stripe';
@ArgsType()
export class CheckoutInput {
export class CheckoutSessionInput {
@Field(() => String)
@IsString()
@IsNotEmpty()

View File

@ -1,7 +1,7 @@
import { Field, ObjectType } from '@nestjs/graphql';
@ObjectType()
export class CheckoutEntity {
export class SessionEntity {
@Field(() => String)
url: string;
}

View File

@ -4,7 +4,6 @@ import Stripe from 'stripe';
import { EnvironmentService } from 'src/integrations/environment/environment.service';
import { User } from 'src/core/user/user.entity';
import { assert } from 'src/utils/assert';
@Injectable()
export class StripeService {
@ -43,13 +42,23 @@ export class StripeService {
await this.stripe.subscriptions.cancel(stripeSubscriptionId);
}
async createBillingPortalSession(
stripeCustomerId: string,
returnUrlPath?: string,
): Promise<Stripe.BillingPortal.Session> {
return await this.stripe.billingPortal.sessions.create({
customer: stripeCustomerId,
return_url: returnUrlPath ?? this.environmentService.getFrontBaseUrl(),
});
}
async createCheckoutSession(
user: User,
priceId: string,
successUrl?: string,
cancelUrl?: string,
) {
const session = await this.stripe.checkout.sessions.create({
): Promise<Stripe.Checkout.Session> {
return await this.stripe.checkout.sessions.create({
line_items: [
{
price: priceId,
@ -70,11 +79,5 @@ export class StripeService {
success_url: successUrl,
cancel_url: cancelUrl,
});
assert(session.url, 'Error: missing checkout.session.url');
this.logger.log(`Stripe Checkout Session Url Redirection: ${session.url}`);
return session.url;
}
}