From 564faa439ad3e17f224403a04dff0973c9bbc8ff Mon Sep 17 00:00:00 2001 From: forehalo Date: Tue, 10 Dec 2024 05:31:19 +0000 Subject: [PATCH] fix(server): should auto apply ea price for users (#9082) --- .../src/plugins/payment/manager/common.ts | 6 +- .../src/plugins/payment/manager/user.ts | 61 ++++++++++++++++--- .../src/plugins/payment/manager/workspace.ts | 15 ++++- .../server/src/plugins/payment/service.ts | 45 +++----------- .../server/src/plugins/payment/webhook.ts | 4 +- .../server/tests/payment/service.spec.ts | 2 - 6 files changed, 78 insertions(+), 55 deletions(-) diff --git a/packages/backend/server/src/plugins/payment/manager/common.ts b/packages/backend/server/src/plugins/payment/manager/common.ts index c8d16e4191..fd9eb5166a 100644 --- a/packages/backend/server/src/plugins/payment/manager/common.ts +++ b/packages/backend/server/src/plugins/payment/manager/common.ts @@ -66,7 +66,7 @@ export abstract class SubscriptionManager { ): KnownStripePrice[] | Promise; abstract checkout( - price: KnownStripePrice, + lookupKey: LookupKey, params: z.infer, args: any ): Promise; @@ -206,9 +206,7 @@ export abstract class SubscriptionManager { return customer; } - protected async getPrice( - lookupKey: LookupKey - ): Promise { + async getPrice(lookupKey: LookupKey): Promise { const prices = await this.stripe.prices.list({ lookup_keys: [encodeLookupKey(lookupKey)], limit: 1, diff --git a/packages/backend/server/src/plugins/payment/manager/user.ts b/packages/backend/server/src/plugins/payment/manager/user.ts index fa1558ecad..3a1b0bc0c1 100644 --- a/packages/backend/server/src/plugins/payment/manager/user.ts +++ b/packages/backend/server/src/plugins/payment/manager/user.ts @@ -12,6 +12,7 @@ import { Config, EventEmitter, InternalServerError, + InvalidCheckoutParameters, SubscriptionAlreadyExists, SubscriptionPlanNotFound, URLHelper, @@ -21,6 +22,7 @@ import { KnownStripeInvoice, KnownStripePrice, KnownStripeSubscription, + LookupKey, retriveLookupKeyFromStripeSubscription, SubscriptionPlan, SubscriptionRecurring, @@ -88,15 +90,20 @@ export class UserSubscriptionManager extends SubscriptionManager { } async checkout( - price: KnownStripePrice, + lookupKey: LookupKey, params: z.infer, { user }: z.infer ) { - const lookupKey = price.lookupKey; + if ( + lookupKey.plan !== SubscriptionPlan.Pro && + lookupKey.plan !== SubscriptionPlan.AI + ) { + throw new InvalidCheckoutParameters(); + } + const subscription = await this.getSubscription({ - // @ts-expect-error filtered already - plan: price.lookupKey.plan, - user, + plan: lookupKey.plan, + userId: user.id, }); if ( @@ -119,12 +126,12 @@ export class UserSubscriptionManager extends SubscriptionManager { const customer = await this.getOrCreateCustomer(user.id); const strategy = await this.strategyStatus(customer); - const available = await this.isPriceAvailable(price, { - ...strategy, - onetime: true, - }); + const price = await this.autoPrice(lookupKey, strategy); - if (!available) { + if ( + !price || + !(await this.isPriceAvailable(price, { ...strategy, onetime: true })) + ) { throw new SubscriptionPlanNotFound({ plan: lookupKey.plan, recurring: lookupKey.recurring, @@ -564,6 +571,40 @@ export class UserSubscriptionManager extends SubscriptionManager { return subscription; } + private async autoPrice(lookupKey: LookupKey, strategy: PriceStrategyStatus) { + // auto select ea variant when available if not specified + let variant: SubscriptionVariant | null = lookupKey.variant; + + if (!variant) { + // make the if conditions separated, more readable + // pro early access + if ( + lookupKey.plan === SubscriptionPlan.Pro && + lookupKey.recurring === SubscriptionRecurring.Yearly && + strategy.proEarlyAccess && + !strategy.proSubscribed + ) { + variant = SubscriptionVariant.EA; + } + + // ai early access + if ( + lookupKey.plan === SubscriptionPlan.AI && + lookupKey.recurring === SubscriptionRecurring.Yearly && + strategy.aiEarlyAccess && + !strategy.aiSubscribed + ) { + variant = SubscriptionVariant.EA; + } + } + + return this.getPrice({ + plan: lookupKey.plan, + recurring: lookupKey.recurring, + variant, + }); + } + private async isPriceAvailable( price: KnownStripePrice, strategy: PriceStrategyStatus diff --git a/packages/backend/server/src/plugins/payment/manager/workspace.ts b/packages/backend/server/src/plugins/payment/manager/workspace.ts index 5f41f8e23a..e2c9b28610 100644 --- a/packages/backend/server/src/plugins/payment/manager/workspace.ts +++ b/packages/backend/server/src/plugins/payment/manager/workspace.ts @@ -9,12 +9,14 @@ import { type EventPayload, OnEvent, SubscriptionAlreadyExists, + SubscriptionPlanNotFound, URLHelper, } from '../../../fundamentals'; import { KnownStripeInvoice, KnownStripePrice, KnownStripeSubscription, + LookupKey, retriveLookupKeyFromStripeSubscription, SubscriptionPlan, SubscriptionRecurring, @@ -62,7 +64,7 @@ export class WorkspaceSubscriptionManager extends SubscriptionManager { } async checkout( - { price }: KnownStripePrice, + lookupKey: LookupKey, params: z.infer, args: z.infer ) { @@ -75,6 +77,15 @@ export class WorkspaceSubscriptionManager extends SubscriptionManager { throw new SubscriptionAlreadyExists({ plan: SubscriptionPlan.Team }); } + const price = await this.getPrice(lookupKey); + + if (!price) { + throw new SubscriptionPlanNotFound({ + plan: lookupKey.plan, + recurring: lookupKey.recurring, + }); + } + const customer = await this.getOrCreateCustomer(args.user.id); const discounts = await (async () => { @@ -102,7 +113,7 @@ export class WorkspaceSubscriptionManager extends SubscriptionManager { return this.stripe.checkout.sessions.create({ line_items: [ { - price: price.id, + price: price.price.id, quantity: count, }, ], diff --git a/packages/backend/server/src/plugins/payment/service.ts b/packages/backend/server/src/plugins/payment/service.ts index 391f75b0be..f5717d076a 100644 --- a/packages/backend/server/src/plugins/payment/service.ts +++ b/packages/backend/server/src/plugins/payment/service.ts @@ -42,11 +42,9 @@ import { ScheduleManager } from './schedule'; import { decodeLookupKey, DEFAULT_PRICES, - encodeLookupKey, KnownStripeInvoice, KnownStripePrice, KnownStripeSubscription, - LookupKey, retriveLookupKeyFromStripePrice, retriveLookupKeyFromStripeSubscription, SubscriptionPlan, @@ -129,19 +127,6 @@ export class SubscriptionService implements OnApplicationBootstrap { throw new ActionForbidden(); } - const price = await this.getPrice({ - plan, - recurring, - variant: variant ?? null, - }); - - if (!price) { - throw new SubscriptionPlanNotFound({ - plan, - recurring, - }); - } - const manager = this.select(plan); const result = CheckoutExtraArgs.safeParse(args); @@ -149,7 +134,15 @@ export class SubscriptionService implements OnApplicationBootstrap { throw new InvalidCheckoutParameters(); } - return manager.checkout(price, params, args); + return manager.checkout( + { + plan, + recurring, + variant: variant ?? null, + }, + params, + args + ); } async cancelSubscription( @@ -270,7 +263,7 @@ export class SubscriptionService implements OnApplicationBootstrap { throw new SameSubscriptionRecurring({ recurring }); } - const price = await this.getPrice({ + const price = await manager.getPrice({ plan: identity.plan, recurring, variant: null, @@ -469,24 +462,6 @@ export class SubscriptionService implements OnApplicationBootstrap { .filter(Boolean) as KnownStripePrice[]; } - private async getPrice( - lookupKey: LookupKey - ): Promise { - const prices = await this.stripe.prices.list({ - lookup_keys: [encodeLookupKey(lookupKey)], - limit: 1, - }); - - const price = prices.data[0]; - - return price - ? { - lookupKey, - price, - } - : null; - } - private async parseStripeInvoice( invoice: Stripe.Invoice ): Promise { diff --git a/packages/backend/server/src/plugins/payment/webhook.ts b/packages/backend/server/src/plugins/payment/webhook.ts index 5cfbfe35a5..6c18fe7dfb 100644 --- a/packages/backend/server/src/plugins/payment/webhook.ts +++ b/packages/backend/server/src/plugins/payment/webhook.ts @@ -26,14 +26,14 @@ export class StripeWebhook { @OnStripeEvent('invoice.updated') @OnStripeEvent('invoice.finalization_failed') @OnStripeEvent('invoice.payment_failed') - @OnStripeEvent('invoice.payment_succeeded') + @OnStripeEvent('invoice.paid') async onInvoiceUpdated( event: | Stripe.InvoiceCreatedEvent | Stripe.InvoiceUpdatedEvent | Stripe.InvoiceFinalizationFailedEvent | Stripe.InvoicePaymentFailedEvent - | Stripe.InvoicePaymentSucceededEvent + | Stripe.InvoicePaidEvent ) { const invoice = await this.stripe.invoices.retrieve(event.data.object.id); await this.service.saveStripeInvoice(invoice); diff --git a/packages/backend/server/tests/payment/service.spec.ts b/packages/backend/server/tests/payment/service.spec.ts index 2d8eee2055..6bd603548a 100644 --- a/packages/backend/server/tests/payment/service.spec.ts +++ b/packages/backend/server/tests/payment/service.spec.ts @@ -476,7 +476,6 @@ test('should get correct pro plan price for checking out', async t => { { plan: SubscriptionPlan.Pro, recurring: SubscriptionRecurring.Yearly, - variant: SubscriptionVariant.EA, successCallbackLink: '', }, { user: u1 } @@ -593,7 +592,6 @@ test('should get correct ai plan price for checking out', async t => { { plan: SubscriptionPlan.AI, recurring: SubscriptionRecurring.Yearly, - variant: SubscriptionVariant.EA, successCallbackLink: '', }, { user: u1 }