feat(server): allow pass coupon to checkout session (#5749)

This commit is contained in:
liuyi
2024-01-31 21:34:22 +08:00
committed by 李华桥
parent 1a643cc70c
commit 79a8be7799
3 changed files with 131 additions and 9 deletions
@@ -3,6 +3,7 @@ import {
Args, Args,
Context, Context,
Field, Field,
InputType,
Int, Int,
Mutation, Mutation,
ObjectType, ObjectType,
@@ -125,6 +126,31 @@ class UserInvoiceType implements Partial<UserInvoice> {
updatedAt!: Date; updatedAt!: Date;
} }
@InputType()
class CreateCheckoutSessionInput {
@Field(() => SubscriptionRecurring, {
nullable: true,
defaultValue: SubscriptionRecurring.Yearly,
})
recurring!: SubscriptionRecurring;
@Field(() => SubscriptionPlan, {
nullable: true,
defaultValue: SubscriptionPlan.Pro,
})
plan!: SubscriptionPlan;
@Field(() => String, { nullable: true })
coupon!: string | null;
@Field(() => String, { nullable: true })
successCallbackLink!: string | null;
// @FIXME(forehalo): we should put this field in the header instead of as a explicity args
@Field(() => String)
idempotencyKey!: string;
}
@Auth() @Auth()
@Resolver(() => UserSubscriptionType) @Resolver(() => UserSubscriptionType)
export class SubscriptionResolver { export class SubscriptionResolver {
@@ -182,7 +208,11 @@ export class SubscriptionResolver {
}); });
} }
/**
* @deprecated
*/
@Mutation(() => String, { @Mutation(() => String, {
deprecationReason: 'use `createCheckoutSession` instead',
description: 'Create a subscription checkout link of stripe', description: 'Create a subscription checkout link of stripe',
}) })
async checkout( async checkout(
@@ -193,6 +223,7 @@ export class SubscriptionResolver {
) { ) {
const session = await this.service.createCheckoutSession({ const session = await this.service.createCheckoutSession({
user, user,
plan: SubscriptionPlan.Pro,
recurring, recurring,
redirectUrl: `${this.config.baseUrl}/upgrade-success`, redirectUrl: `${this.config.baseUrl}/upgrade-success`,
idempotencyKey, idempotencyKey,
@@ -210,6 +241,36 @@ export class SubscriptionResolver {
return session.url; return session.url;
} }
@Mutation(() => String, {
description: 'Create a subscription checkout link of stripe',
})
async createCheckoutSession(
@CurrentUser() user: User,
@Args({ name: 'input', type: () => CreateCheckoutSessionInput })
input: CreateCheckoutSessionInput
) {
const session = await this.service.createCheckoutSession({
user,
plan: input.plan,
recurring: input.recurring,
promotionCode: input.coupon,
redirectUrl:
input.successCallbackLink ?? `${this.config.baseUrl}/upgrade-success`,
idempotencyKey: input.idempotencyKey,
});
if (!session.url) {
throw new GraphQLError('Failed to create checkout session', {
extensions: {
status: HttpStatus[HttpStatus.BAD_GATEWAY],
code: HttpStatus.BAD_GATEWAY,
},
});
}
return session.url;
}
@Mutation(() => String, { @Mutation(() => String, {
description: 'Create a stripe customer portal to manage payment methods', description: 'Create a stripe customer portal to manage payment methods',
}) })
@@ -69,13 +69,15 @@ export class SubscriptionService {
async createCheckoutSession({ async createCheckoutSession({
user, user,
recurring, recurring,
plan,
promotionCode,
redirectUrl, redirectUrl,
idempotencyKey, idempotencyKey,
plan = SubscriptionPlan.Pro,
}: { }: {
user: User; user: User;
plan?: SubscriptionPlan;
recurring: SubscriptionRecurring; recurring: SubscriptionRecurring;
plan: SubscriptionPlan;
promotionCode?: string | null;
redirectUrl: string; redirectUrl: string;
idempotencyKey: string; idempotencyKey: string;
}) { }) {
@@ -95,7 +97,28 @@ export class SubscriptionService {
`${idempotencyKey}-getOrCreateCustomer`, `${idempotencyKey}-getOrCreateCustomer`,
user user
); );
const coupon = await this.getAvailableCoupon(user, CouponType.EarlyAccess);
let discount: { coupon?: string; promotion_code?: string } | undefined;
if (promotionCode) {
const code = await this.getAvailablePromotionCode(
promotionCode,
customer.stripeCustomerId
);
if (code) {
discount ??= {};
discount.promotion_code = code;
}
} else {
const coupon = await this.getAvailableCoupon(
user,
CouponType.EarlyAccess
);
if (coupon) {
discount ??= {};
discount.coupon = coupon;
}
}
return await this.stripe.checkout.sessions.create( return await this.stripe.checkout.sessions.create(
{ {
@@ -108,13 +131,11 @@ export class SubscriptionService {
tax_id_collection: { tax_id_collection: {
enabled: true, enabled: true,
}, },
...(coupon ...(discount
? { ? {
discounts: [{ coupon }], discounts: [discount],
} }
: { : { allow_promotion_codes: true }),
allow_promotion_codes: true,
}),
mode: 'subscription', mode: 'subscription',
success_url: redirectUrl, success_url: redirectUrl,
customer: customer.stripeCustomerId, customer: customer.stripeCustomerId,
@@ -643,4 +664,33 @@ export class SubscriptionService {
return null; return null;
} }
private async getAvailablePromotionCode(
userFacingPromotionCode: string,
customer?: string
) {
const list = await this.stripe.promotionCodes.list({
code: userFacingPromotionCode,
active: true,
limit: 1,
});
const code = list.data[0];
if (!code) {
return null;
}
let available = false;
if (code.customer) {
available =
typeof code.customer === 'string'
? code.customer === customer
: code.customer.id === customer;
} else {
available = true;
}
return available ? code.id : null;
}
} }
+12 -1
View File
@@ -2,6 +2,14 @@
# THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY) # THIS FILE WAS AUTOMATICALLY GENERATED (DO NOT MODIFY)
# ------------------------------------------------------ # ------------------------------------------------------
input CreateCheckoutSessionInput {
coupon: String
idempotencyKey: String!
plan: SubscriptionPlan = Pro
recurring: SubscriptionRecurring = Yearly
successCallbackLink: String
}
""" """
A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date-time format. A date-time string at UTC, such as 2019-12-03T09:54:33Z, compliant with the date-time format.
""" """
@@ -107,7 +115,10 @@ type Mutation {
changePassword(newPassword: String!, token: String!): UserType! changePassword(newPassword: String!, token: String!): UserType!
"""Create a subscription checkout link of stripe""" """Create a subscription checkout link of stripe"""
checkout(idempotencyKey: String!, recurring: SubscriptionRecurring!): String! checkout(idempotencyKey: String!, recurring: SubscriptionRecurring!): String! @deprecated(reason: "use `createCheckoutSession` instead")
"""Create a subscription checkout link of stripe"""
createCheckoutSession(input: CreateCheckoutSessionInput!): String!
"""Create a stripe customer portal to manage payment methods""" """Create a stripe customer portal to manage payment methods"""
createCustomerPortal: String! createCustomerPortal: String!