diff --git a/packages/backend/server/src/plugins/payment/config.ts b/packages/backend/server/src/plugins/payment/config.ts index 3f444b1868..0c8c2101fa 100644 --- a/packages/backend/server/src/plugins/payment/config.ts +++ b/packages/backend/server/src/plugins/payment/config.ts @@ -1,6 +1,10 @@ import type { Stripe } from 'stripe'; -import { defineStartupConfig, ModuleConfig } from '../../fundamentals/config'; +import { + defineRuntimeConfig, + defineStartupConfig, + ModuleConfig, +} from '../../fundamentals/config'; export interface PaymentStartupConfig { stripe?: { @@ -11,10 +15,20 @@ export interface PaymentStartupConfig { } & Stripe.StripeConfig; } +export interface PaymentRuntimeConfig { + showLifetimePrice: boolean; +} + declare module '../config' { interface PluginsConfig { - payment: ModuleConfig; + payment: ModuleConfig; } } defineStartupConfig('plugins.payment', {}); +defineRuntimeConfig('plugins.payment', { + showLifetimePrice: { + desc: 'Whether enable lifetime price and allow user to pay for it.', + default: false, + }, +}); diff --git a/packages/backend/server/src/plugins/payment/service.ts b/packages/backend/server/src/plugins/payment/service.ts index 4c15da5b2d..be7bc42793 100644 --- a/packages/backend/server/src/plugins/payment/service.ts +++ b/packages/backend/server/src/plugins/payment/service.ts @@ -121,8 +121,14 @@ export class SubscriptionService { }); } + const lifetimePriceEnabled = await this.config.runtime.fetch( + 'plugins.payment/showLifetimePrice' + ); + const list = await this.stripe.prices.list({ active: true, + // only list recurring prices if lifetime price is not enabled + ...(lifetimePriceEnabled ? {} : { type: 'recurring' }), }); return list.data.filter(price => { @@ -807,6 +813,16 @@ export class SubscriptionService { recurring: SubscriptionRecurring, variant?: SubscriptionPriceVariant ): Promise { + if (recurring === SubscriptionRecurring.Lifetime) { + const lifetimePriceEnabled = await this.config.runtime.fetch( + 'plugins.payment/showLifetimePrice' + ); + + if (!lifetimePriceEnabled) { + throw new ActionForbidden(); + } + } + const prices = await this.stripe.prices.list({ lookup_keys: [encodeLookupKey(plan, recurring, variant)], }); diff --git a/packages/backend/server/tests/payment/service.spec.ts b/packages/backend/server/tests/payment/service.spec.ts index 5b86e8f099..34e062fc50 100644 --- a/packages/backend/server/tests/payment/service.spec.ts +++ b/packages/backend/server/tests/payment/service.spec.ts @@ -14,7 +14,7 @@ import { FeatureManagementService, } from '../../src/core/features'; import { EventEmitter } from '../../src/fundamentals'; -import { ConfigModule } from '../../src/fundamentals/config'; +import { Config, ConfigModule } from '../../src/fundamentals/config'; import { CouponType, encodeLookupKey, @@ -969,6 +969,45 @@ const invoice: Stripe.Invoice = { }, }; +test('should not be able to checkout for lifetime recurring if not enabled', async t => { + const { service, stripe, u1 } = t.context; + + Sinon.stub(stripe.subscriptions, 'list').resolves({ data: [] } as any); + await t.throwsAsync( + () => + service.createCheckoutSession({ + user: u1, + plan: SubscriptionPlan.Pro, + recurring: SubscriptionRecurring.Lifetime, + redirectUrl: '', + idempotencyKey: '', + }), + { message: 'You are not allowed to perform this action.' } + ); +}); + +test('should be able to checkout for lifetime recurring', async t => { + const { service, stripe, u1, app } = t.context; + const config = app.get(Config); + await config.runtime.set('plugins.payment/showLifetimePrice', true); + + Sinon.stub(stripe.subscriptions, 'list').resolves({ data: [] } as any); + Sinon.stub(stripe.prices, 'list').resolves({ + data: [PRICES[PRO_LIFETIME]], + } as any); + const sessionStub = Sinon.stub(stripe.checkout.sessions, 'create'); + + await service.createCheckoutSession({ + user: u1, + plan: SubscriptionPlan.Pro, + recurring: SubscriptionRecurring.Lifetime, + redirectUrl: '', + idempotencyKey: '', + }); + + t.true(sessionStub.calledOnce); +}); + test('should be able to subscribe to lifetime recurring', async t => { // lifetime payment isn't a subscription, so we need to trigger the creation by invoice payment event const { service, stripe, db, u1, event } = t.context;