mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-14 00:26:51 +08:00
refactor(server): config system (#11081)
This commit is contained in:
@@ -1,10 +1,6 @@
|
||||
import type { Stripe } from 'stripe';
|
||||
|
||||
import {
|
||||
defineRuntimeConfig,
|
||||
defineStartupConfig,
|
||||
ModuleConfig,
|
||||
} from '../../base/config';
|
||||
import { defineModuleConfig } from '../../base';
|
||||
|
||||
export interface PaymentStartupConfig {
|
||||
stripe?: {
|
||||
@@ -19,16 +15,40 @@ export interface PaymentRuntimeConfig {
|
||||
showLifetimePrice: boolean;
|
||||
}
|
||||
|
||||
declare module '../config' {
|
||||
interface PluginsConfig {
|
||||
payment: ModuleConfig<PaymentStartupConfig, PaymentRuntimeConfig>;
|
||||
declare global {
|
||||
interface AppConfigSchema {
|
||||
payment: {
|
||||
enabled: boolean;
|
||||
showLifetimePrice: boolean;
|
||||
apiKey: string;
|
||||
webhookKey: string;
|
||||
stripe: ConfigItem<{} & Stripe.StripeConfig>;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
defineStartupConfig('plugins.payment', {});
|
||||
defineRuntimeConfig('plugins.payment', {
|
||||
defineModuleConfig('payment', {
|
||||
enabled: {
|
||||
desc: 'Whether enable payment plugin',
|
||||
default: false,
|
||||
},
|
||||
showLifetimePrice: {
|
||||
desc: 'Whether enable lifetime price and allow user to pay for it.',
|
||||
default: true,
|
||||
},
|
||||
apiKey: {
|
||||
desc: 'Stripe API key to enable payment service.',
|
||||
default: '',
|
||||
env: 'STRIPE_API_KEY',
|
||||
},
|
||||
webhookKey: {
|
||||
desc: 'Stripe webhook key to enable payment service.',
|
||||
default: '',
|
||||
env: 'STRIPE_WEBHOOK_KEY',
|
||||
},
|
||||
stripe: {
|
||||
desc: 'Stripe API keys',
|
||||
default: {},
|
||||
link: 'https://docs.stripe.com/api',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1,37 +1,32 @@
|
||||
import assert from 'node:assert';
|
||||
|
||||
import type { RawBodyRequest } from '@nestjs/common';
|
||||
import { Controller, Logger, Post, Req } from '@nestjs/common';
|
||||
import type { Request } from 'express';
|
||||
import Stripe from 'stripe';
|
||||
|
||||
import { Config, EventBus, InternalServerError } from '../../base';
|
||||
import { Public } from '../../core/auth';
|
||||
import { StripeFactory } from './stripe';
|
||||
|
||||
@Controller('/api/stripe')
|
||||
export class StripeWebhookController {
|
||||
private readonly webhookKey: string;
|
||||
private readonly logger = new Logger(StripeWebhookController.name);
|
||||
|
||||
constructor(
|
||||
config: Config,
|
||||
private readonly stripe: Stripe,
|
||||
private readonly config: Config,
|
||||
private readonly stripeProvider: StripeFactory,
|
||||
private readonly event: EventBus
|
||||
) {
|
||||
assert(config.plugins.payment.stripe);
|
||||
this.webhookKey = config.plugins.payment.stripe.keys.webhookKey;
|
||||
}
|
||||
) {}
|
||||
|
||||
@Public()
|
||||
@Post('/webhook')
|
||||
async handleWebhook(@Req() req: RawBodyRequest<Request>) {
|
||||
const webhookKey = this.config.payment.webhookKey;
|
||||
// Retrieve the event by verifying the signature using the raw body and secret.
|
||||
const signature = req.headers['stripe-signature'];
|
||||
try {
|
||||
const event = this.stripe.webhooks.constructEvent(
|
||||
const event = this.stripeProvider.stripe.webhooks.constructEvent(
|
||||
req.rawBody ?? '',
|
||||
signature ?? '',
|
||||
this.webhookKey
|
||||
webhookKey
|
||||
);
|
||||
|
||||
this.logger.debug(
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import './config';
|
||||
|
||||
import { ServerFeature } from '../../core/config';
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { ServerConfigModule } from '../../core';
|
||||
import { FeatureModule } from '../../core/features';
|
||||
import { MailModule } from '../../core/mail';
|
||||
import { PermissionModule } from '../../core/permission';
|
||||
import { QuotaModule } from '../../core/quota';
|
||||
import { UserModule } from '../../core/user';
|
||||
import { WorkspaceModule } from '../../core/workspaces';
|
||||
import { Plugin } from '../registry';
|
||||
import { StripeWebhookController } from './controller';
|
||||
import { SubscriptionCronJobs } from './cron';
|
||||
import { LicenseController } from './license/controller';
|
||||
@@ -23,11 +24,10 @@ import {
|
||||
WorkspaceSubscriptionResolver,
|
||||
} from './resolver';
|
||||
import { SubscriptionService } from './service';
|
||||
import { StripeProvider } from './stripe';
|
||||
import { StripeFactory, StripeProvider } from './stripe';
|
||||
import { StripeWebhook } from './webhook';
|
||||
|
||||
@Plugin({
|
||||
name: 'payment',
|
||||
@Module({
|
||||
imports: [
|
||||
FeatureModule,
|
||||
QuotaModule,
|
||||
@@ -35,8 +35,10 @@ import { StripeWebhook } from './webhook';
|
||||
PermissionModule,
|
||||
WorkspaceModule,
|
||||
MailModule,
|
||||
ServerConfigModule,
|
||||
],
|
||||
providers: [
|
||||
StripeFactory,
|
||||
StripeProvider,
|
||||
SubscriptionService,
|
||||
SubscriptionResolver,
|
||||
@@ -50,11 +52,5 @@ import { StripeWebhook } from './webhook';
|
||||
QuotaOverride,
|
||||
],
|
||||
controllers: [StripeWebhookController, LicenseController],
|
||||
requires: [
|
||||
'plugins.payment.stripe.keys.APIKey',
|
||||
'plugins.payment.stripe.keys.webhookKey',
|
||||
],
|
||||
contributesTo: ServerFeature.Payment,
|
||||
if: config => config.flavor.graphql,
|
||||
})
|
||||
export class PaymentModule {}
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
import { Public } from '../../../core/auth';
|
||||
import { SelfhostTeamSubscriptionManager } from '../manager/selfhost';
|
||||
import { SubscriptionService } from '../service';
|
||||
import { StripeFactory } from '../stripe';
|
||||
import {
|
||||
SubscriptionPlan,
|
||||
SubscriptionRecurring,
|
||||
@@ -53,7 +54,7 @@ export class LicenseController {
|
||||
private readonly mutex: Mutex,
|
||||
private readonly subscription: SubscriptionService,
|
||||
private readonly manager: SelfhostTeamSubscriptionManager,
|
||||
private readonly stripe: Stripe
|
||||
private readonly stripeProvider: StripeFactory
|
||||
) {}
|
||||
|
||||
@Post('/:license/activate')
|
||||
@@ -238,18 +239,20 @@ export class LicenseController {
|
||||
throw new LicenseNotFound();
|
||||
}
|
||||
|
||||
const subscriptionData = await this.stripe.subscriptions.retrieve(
|
||||
subscription.stripeSubscriptionId,
|
||||
{
|
||||
expand: ['customer'],
|
||||
}
|
||||
);
|
||||
const subscriptionData =
|
||||
await this.stripeProvider.stripe.subscriptions.retrieve(
|
||||
subscription.stripeSubscriptionId,
|
||||
{
|
||||
expand: ['customer'],
|
||||
}
|
||||
);
|
||||
|
||||
const customer = subscriptionData.customer as Stripe.Customer;
|
||||
try {
|
||||
const portal = await this.stripe.billingPortal.sessions.create({
|
||||
customer: customer.id,
|
||||
});
|
||||
const portal =
|
||||
await this.stripeProvider.stripe.billingPortal.sessions.create({
|
||||
customer: customer.id,
|
||||
});
|
||||
|
||||
return { url: portal.url };
|
||||
} catch (e) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { z } from 'zod';
|
||||
|
||||
import { UserNotFound } from '../../../base';
|
||||
import { ScheduleManager } from '../schedule';
|
||||
import { StripeFactory } from '../stripe';
|
||||
import {
|
||||
encodeLookupKey,
|
||||
KnownStripeInvoice,
|
||||
@@ -55,12 +56,16 @@ export const CheckoutParams = z.object({
|
||||
});
|
||||
|
||||
export abstract class SubscriptionManager {
|
||||
protected readonly scheduleManager = new ScheduleManager(this.stripe);
|
||||
protected readonly scheduleManager = new ScheduleManager(this.stripeProvider);
|
||||
constructor(
|
||||
protected readonly stripe: Stripe,
|
||||
protected readonly stripeProvider: StripeFactory,
|
||||
protected readonly db: PrismaClient
|
||||
) {}
|
||||
|
||||
get stripe() {
|
||||
return this.stripeProvider.stripe;
|
||||
}
|
||||
|
||||
abstract filterPrices(
|
||||
prices: KnownStripePrice[],
|
||||
customer?: UserStripeCustomer
|
||||
|
||||
@@ -3,11 +3,11 @@ import { randomUUID } from 'node:crypto';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PrismaClient, UserStripeCustomer } from '@prisma/client';
|
||||
import { pick } from 'lodash-es';
|
||||
import Stripe from 'stripe';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { SubscriptionPlanNotFound, URLHelper } from '../../../base';
|
||||
import { Mailer } from '../../../core/mail';
|
||||
import { StripeFactory } from '../stripe';
|
||||
import {
|
||||
KnownStripeInvoice,
|
||||
KnownStripePrice,
|
||||
@@ -43,12 +43,12 @@ export const SelfhostTeamSubscriptionIdentity = z.object({
|
||||
@Injectable()
|
||||
export class SelfhostTeamSubscriptionManager extends SubscriptionManager {
|
||||
constructor(
|
||||
stripe: Stripe,
|
||||
stripeProvider: StripeFactory,
|
||||
db: PrismaClient,
|
||||
private readonly url: URLHelper,
|
||||
private readonly mailer: Mailer
|
||||
) {
|
||||
super(stripe, db);
|
||||
super(stripeProvider, db);
|
||||
}
|
||||
|
||||
filterPrices(
|
||||
|
||||
@@ -5,17 +5,18 @@ import Stripe from 'stripe';
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
Config,
|
||||
EventBus,
|
||||
InternalServerError,
|
||||
InvalidCheckoutParameters,
|
||||
Mutex,
|
||||
Runtime,
|
||||
SubscriptionAlreadyExists,
|
||||
SubscriptionPlanNotFound,
|
||||
TooManyRequest,
|
||||
URLHelper,
|
||||
} from '../../../base';
|
||||
import { EarlyAccessType, FeatureService } from '../../../core/features';
|
||||
import { StripeFactory } from '../stripe';
|
||||
import {
|
||||
CouponType,
|
||||
KnownStripeInvoice,
|
||||
@@ -53,15 +54,15 @@ export const UserSubscriptionCheckoutArgs = z.object({
|
||||
@Injectable()
|
||||
export class UserSubscriptionManager extends SubscriptionManager {
|
||||
constructor(
|
||||
stripe: Stripe,
|
||||
stripeProvider: StripeFactory,
|
||||
db: PrismaClient,
|
||||
private readonly runtime: Runtime,
|
||||
private readonly config: Config,
|
||||
private readonly feature: FeatureService,
|
||||
private readonly event: EventBus,
|
||||
private readonly url: URLHelper,
|
||||
private readonly mutex: Mutex
|
||||
) {
|
||||
super(stripe, db);
|
||||
super(stripeProvider, db);
|
||||
}
|
||||
|
||||
async filterPrices(
|
||||
@@ -588,7 +589,7 @@ export class UserSubscriptionManager extends SubscriptionManager {
|
||||
{ proEarlyAccess, proSubscribed, onetime }: PriceStrategyStatus
|
||||
) {
|
||||
if (lookupKey.recurring === SubscriptionRecurring.Lifetime) {
|
||||
return this.runtime.fetch('plugins.payment/showLifetimePrice');
|
||||
return this.config.payment.showLifetimePrice;
|
||||
}
|
||||
|
||||
if (lookupKey.variant === SubscriptionVariant.Onetime) {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { PrismaClient, UserStripeCustomer } from '@prisma/client';
|
||||
import { omit, pick } from 'lodash-es';
|
||||
import Stripe from 'stripe';
|
||||
import { z } from 'zod';
|
||||
|
||||
import {
|
||||
@@ -12,6 +11,7 @@ import {
|
||||
URLHelper,
|
||||
} from '../../../base';
|
||||
import { Models } from '../../../models';
|
||||
import { StripeFactory } from '../stripe';
|
||||
import {
|
||||
KnownStripeInvoice,
|
||||
KnownStripePrice,
|
||||
@@ -46,13 +46,13 @@ export const WorkspaceSubscriptionCheckoutArgs = z.object({
|
||||
@Injectable()
|
||||
export class WorkspaceSubscriptionManager extends SubscriptionManager {
|
||||
constructor(
|
||||
stripe: Stripe,
|
||||
stripeProvider: StripeFactory,
|
||||
db: PrismaClient,
|
||||
private readonly url: URLHelper,
|
||||
private readonly event: EventBus,
|
||||
private readonly models: Models
|
||||
) {
|
||||
super(stripe, db);
|
||||
super(stripeProvider, db);
|
||||
}
|
||||
|
||||
filterPrices(
|
||||
|
||||
@@ -1,15 +1,24 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import Stripe from 'stripe';
|
||||
|
||||
import { StripeFactory } from './stripe';
|
||||
|
||||
@Injectable()
|
||||
export class ScheduleManager {
|
||||
private _schedule: Stripe.SubscriptionSchedule | null = null;
|
||||
private readonly logger = new Logger(ScheduleManager.name);
|
||||
|
||||
constructor(private readonly stripe: Stripe) {}
|
||||
constructor(private readonly stripeProvider: StripeFactory) {}
|
||||
|
||||
static create(stripe: Stripe, schedule?: Stripe.SubscriptionSchedule) {
|
||||
const manager = new ScheduleManager(stripe);
|
||||
get stripe() {
|
||||
return this.stripeProvider.stripe;
|
||||
}
|
||||
|
||||
static create(
|
||||
stripeProvider: StripeFactory,
|
||||
schedule?: Stripe.SubscriptionSchedule
|
||||
) {
|
||||
const manager = new ScheduleManager(stripeProvider);
|
||||
if (schedule) {
|
||||
manager._schedule = schedule;
|
||||
}
|
||||
@@ -56,9 +65,9 @@ export class ScheduleManager {
|
||||
return undefined;
|
||||
});
|
||||
|
||||
return ScheduleManager.create(this.stripe, s);
|
||||
return ScheduleManager.create(this.stripeProvider, s);
|
||||
} else {
|
||||
return ScheduleManager.create(this.stripe, schedule);
|
||||
return ScheduleManager.create(this.stripeProvider, schedule);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common';
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import type { User, UserStripeCustomer } from '@prisma/client';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import Stripe from 'stripe';
|
||||
@@ -7,14 +7,12 @@ import { z } from 'zod';
|
||||
import {
|
||||
ActionForbidden,
|
||||
CantUpdateOnetimePaymentSubscription,
|
||||
Config,
|
||||
CustomerPortalCreateFailed,
|
||||
InternalServerError,
|
||||
InvalidCheckoutParameters,
|
||||
InvalidLicenseSessionId,
|
||||
InvalidSubscriptionParameters,
|
||||
LicenseRevealed,
|
||||
Mutex,
|
||||
OnEvent,
|
||||
SameSubscriptionRecurring,
|
||||
SubscriptionExpired,
|
||||
@@ -46,9 +44,8 @@ import {
|
||||
SelfhostTeamSubscriptionManager,
|
||||
} from './manager/selfhost';
|
||||
import { ScheduleManager } from './schedule';
|
||||
import { StripeFactory } from './stripe';
|
||||
import {
|
||||
decodeLookupKey,
|
||||
DEFAULT_PRICES,
|
||||
KnownStripeInvoice,
|
||||
KnownStripePrice,
|
||||
KnownStripeSubscription,
|
||||
@@ -57,7 +54,6 @@ import {
|
||||
SubscriptionPlan,
|
||||
SubscriptionRecurring,
|
||||
SubscriptionStatus,
|
||||
SubscriptionVariant,
|
||||
} from './types';
|
||||
|
||||
export const CheckoutExtraArgs = z.union([
|
||||
@@ -75,24 +71,22 @@ export const SubscriptionIdentity = z.union([
|
||||
export { CheckoutParams };
|
||||
|
||||
@Injectable()
|
||||
export class SubscriptionService implements OnApplicationBootstrap {
|
||||
export class SubscriptionService {
|
||||
private readonly logger = new Logger(SubscriptionService.name);
|
||||
private readonly scheduleManager = new ScheduleManager(this.stripe);
|
||||
private readonly scheduleManager = new ScheduleManager(this.stripeProvider);
|
||||
|
||||
constructor(
|
||||
private readonly config: Config,
|
||||
private readonly stripe: Stripe,
|
||||
private readonly stripeProvider: StripeFactory,
|
||||
private readonly db: PrismaClient,
|
||||
private readonly feature: FeatureService,
|
||||
private readonly models: Models,
|
||||
private readonly userManager: UserSubscriptionManager,
|
||||
private readonly workspaceManager: WorkspaceSubscriptionManager,
|
||||
private readonly selfhostManager: SelfhostTeamSubscriptionManager,
|
||||
private readonly mutex: Mutex
|
||||
private readonly selfhostManager: SelfhostTeamSubscriptionManager
|
||||
) {}
|
||||
|
||||
async onApplicationBootstrap() {
|
||||
await this.initStripeProducts();
|
||||
get stripe() {
|
||||
return this.stripeProvider.stripe;
|
||||
}
|
||||
|
||||
private select(plan: SubscriptionPlan): SubscriptionManager {
|
||||
@@ -132,8 +126,8 @@ export class SubscriptionService implements OnApplicationBootstrap {
|
||||
const { plan, recurring, variant } = params;
|
||||
|
||||
if (
|
||||
this.config.deploy &&
|
||||
this.config.affine.canary &&
|
||||
env.namespaces.canary &&
|
||||
env.prod &&
|
||||
args.user &&
|
||||
!this.feature.isStaff(args.user.email)
|
||||
) {
|
||||
@@ -683,72 +677,4 @@ export class SubscriptionService implements OnApplicationBootstrap {
|
||||
throw new InvalidSubscriptionParameters();
|
||||
}
|
||||
}
|
||||
|
||||
private async initStripeProducts() {
|
||||
// only init stripe products in dev mode or canary deployment
|
||||
if (
|
||||
(this.config.deploy && !this.config.affine.canary) ||
|
||||
!this.config.node.dev
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
await using lock = await this.mutex.acquire('init stripe prices');
|
||||
|
||||
if (!lock) {
|
||||
return;
|
||||
}
|
||||
|
||||
const keys = new Set<string>();
|
||||
try {
|
||||
await this.stripe.prices
|
||||
.list({
|
||||
active: true,
|
||||
limit: 100,
|
||||
})
|
||||
.autoPagingEach(item => {
|
||||
if (item.lookup_key) {
|
||||
keys.add(item.lookup_key);
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
this.logger.warn('Failed to list stripe prices, skip auto init.');
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [key, setting] of DEFAULT_PRICES) {
|
||||
if (keys.has(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const lookupKey = decodeLookupKey(key);
|
||||
|
||||
try {
|
||||
await this.stripe.prices.create({
|
||||
product_data: {
|
||||
name: setting.product,
|
||||
},
|
||||
billing_scheme: 'per_unit',
|
||||
unit_amount: setting.price,
|
||||
currency: 'usd',
|
||||
lookup_key: key,
|
||||
tax_behavior: 'inclusive',
|
||||
recurring:
|
||||
lookupKey.recurring === SubscriptionRecurring.Lifetime ||
|
||||
lookupKey.variant === SubscriptionVariant.Onetime
|
||||
? undefined
|
||||
: {
|
||||
interval:
|
||||
lookupKey.recurring === SubscriptionRecurring.Monthly
|
||||
? 'month'
|
||||
: 'year',
|
||||
interval_count: 1,
|
||||
usage_type: 'licensed',
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
this.logger.error('Failed to create stripe price.', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,130 @@
|
||||
import assert from 'node:assert';
|
||||
|
||||
import { FactoryProvider } from '@nestjs/common';
|
||||
import { omit } from 'lodash-es';
|
||||
import { FactoryProvider, Injectable, Logger } from '@nestjs/common';
|
||||
import Stripe from 'stripe';
|
||||
|
||||
import { Config } from '../../base';
|
||||
import { Config, Mutex, OnEvent } from '../../base';
|
||||
import { ServerFeature, ServerService } from '../../core';
|
||||
import {
|
||||
decodeLookupKey,
|
||||
DEFAULT_PRICES,
|
||||
SubscriptionRecurring,
|
||||
SubscriptionVariant,
|
||||
} from './types';
|
||||
|
||||
@Injectable()
|
||||
export class StripeFactory {
|
||||
#stripe!: Stripe;
|
||||
readonly #logger = new Logger(StripeFactory.name);
|
||||
|
||||
constructor(
|
||||
private readonly config: Config,
|
||||
private readonly mutex: Mutex,
|
||||
private readonly server: ServerService
|
||||
) {}
|
||||
|
||||
get stripe() {
|
||||
return this.#stripe;
|
||||
}
|
||||
|
||||
@OnEvent('config.init')
|
||||
async onConfigInit() {
|
||||
this.setup();
|
||||
await this.initStripeProducts();
|
||||
}
|
||||
|
||||
@OnEvent('config.changed')
|
||||
async onConfigChanged(event: Events['config.changed']) {
|
||||
if ('payment' in event.updates) {
|
||||
this.setup();
|
||||
}
|
||||
}
|
||||
|
||||
setup() {
|
||||
// TODO@(@forehalo): use per-requests api key injection
|
||||
this.#stripe = new Stripe(
|
||||
this.config.payment.apiKey ||
|
||||
// NOTE(@forehalo):
|
||||
// we always fake a key if not set because `new Stripe` will complain if it's empty string
|
||||
// this will make code cleaner than providing `Stripe` instance as optional one.
|
||||
'stripe-api-key',
|
||||
this.config.payment.stripe
|
||||
);
|
||||
if (this.config.payment.enabled) {
|
||||
this.server.enableFeature(ServerFeature.Payment);
|
||||
} else {
|
||||
this.server.disableFeature(ServerFeature.Payment);
|
||||
}
|
||||
}
|
||||
|
||||
private async initStripeProducts() {
|
||||
// only init stripe products in dev mode or canary deployment
|
||||
if (!this.config.payment.enabled && !env.namespaces.canary) {
|
||||
return;
|
||||
}
|
||||
|
||||
await using lock = await this.mutex.acquire('init stripe prices');
|
||||
|
||||
if (!lock) {
|
||||
return;
|
||||
}
|
||||
|
||||
const keys = new Set<string>();
|
||||
try {
|
||||
await this.stripe.prices
|
||||
.list({
|
||||
active: true,
|
||||
limit: 100,
|
||||
})
|
||||
.autoPagingEach(item => {
|
||||
if (item.lookup_key) {
|
||||
keys.add(item.lookup_key);
|
||||
}
|
||||
});
|
||||
} catch {
|
||||
this.#logger.warn('Failed to list stripe prices, skip auto init.');
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [key, setting] of DEFAULT_PRICES) {
|
||||
if (keys.has(key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const lookupKey = decodeLookupKey(key);
|
||||
|
||||
try {
|
||||
await this.stripe.prices.create({
|
||||
product_data: {
|
||||
name: setting.product,
|
||||
},
|
||||
billing_scheme: 'per_unit',
|
||||
unit_amount: setting.price,
|
||||
currency: 'usd',
|
||||
lookup_key: key,
|
||||
tax_behavior: 'inclusive',
|
||||
recurring:
|
||||
lookupKey.recurring === SubscriptionRecurring.Lifetime ||
|
||||
lookupKey.variant === SubscriptionVariant.Onetime
|
||||
? undefined
|
||||
: {
|
||||
interval:
|
||||
lookupKey.recurring === SubscriptionRecurring.Monthly
|
||||
? 'month'
|
||||
: 'year',
|
||||
interval_count: 1,
|
||||
usage_type: 'licensed',
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
this.#logger.error('Failed to create stripe price.', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const StripeProvider: FactoryProvider = {
|
||||
provide: Stripe,
|
||||
useFactory: (config: Config) => {
|
||||
const stripeConfig = config.plugins.payment.stripe;
|
||||
assert(stripeConfig, 'Stripe configuration is missing');
|
||||
|
||||
return new Stripe(stripeConfig.keys.APIKey, omit(stripeConfig, 'keys'));
|
||||
useFactory: (provider: StripeFactory) => {
|
||||
return provider.stripe;
|
||||
},
|
||||
inject: [Config],
|
||||
inject: [StripeFactory],
|
||||
};
|
||||
|
||||
@@ -3,6 +3,7 @@ import Stripe from 'stripe';
|
||||
|
||||
import { OnEvent } from '../../base';
|
||||
import { SubscriptionService } from './service';
|
||||
import { StripeFactory } from './stripe';
|
||||
|
||||
/**
|
||||
* Stripe webhook events sent in random order, and may be even sent more than once.
|
||||
@@ -14,9 +15,13 @@ import { SubscriptionService } from './service';
|
||||
export class StripeWebhook {
|
||||
constructor(
|
||||
private readonly service: SubscriptionService,
|
||||
private readonly stripe: Stripe
|
||||
private readonly stripeProvider: StripeFactory
|
||||
) {}
|
||||
|
||||
get stripe() {
|
||||
return this.stripeProvider.stripe;
|
||||
}
|
||||
|
||||
@OnEvent('stripe.invoice.created')
|
||||
@OnEvent('stripe.invoice.updated')
|
||||
@OnEvent('stripe.invoice.finalization_failed')
|
||||
|
||||
Reference in New Issue
Block a user