payment-kit 1.19.0 → 1.19.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/api/src/crons/index.ts +8 -0
  2. package/api/src/index.ts +4 -0
  3. package/api/src/libs/credit-grant.ts +146 -0
  4. package/api/src/libs/env.ts +1 -0
  5. package/api/src/libs/invoice.ts +4 -3
  6. package/api/src/libs/notification/template/base.ts +388 -2
  7. package/api/src/libs/notification/template/customer-credit-grant-granted.ts +149 -0
  8. package/api/src/libs/notification/template/customer-credit-grant-low-balance.ts +151 -0
  9. package/api/src/libs/notification/template/customer-credit-insufficient.ts +254 -0
  10. package/api/src/libs/notification/template/subscription-canceled.ts +193 -202
  11. package/api/src/libs/notification/template/subscription-refund-succeeded.ts +215 -237
  12. package/api/src/libs/notification/template/subscription-renewed.ts +130 -200
  13. package/api/src/libs/notification/template/subscription-succeeded.ts +100 -202
  14. package/api/src/libs/notification/template/subscription-trial-start.ts +142 -188
  15. package/api/src/libs/notification/template/subscription-trial-will-end.ts +146 -174
  16. package/api/src/libs/notification/template/subscription-upgraded.ts +96 -192
  17. package/api/src/libs/notification/template/subscription-will-canceled.ts +94 -135
  18. package/api/src/libs/notification/template/subscription-will-renew.ts +220 -245
  19. package/api/src/libs/payment.ts +69 -0
  20. package/api/src/libs/queue/index.ts +3 -2
  21. package/api/src/libs/session.ts +8 -0
  22. package/api/src/libs/subscription.ts +74 -3
  23. package/api/src/libs/ws.ts +23 -1
  24. package/api/src/locales/en.ts +33 -0
  25. package/api/src/locales/zh.ts +31 -0
  26. package/api/src/queues/credit-consume.ts +715 -0
  27. package/api/src/queues/credit-grant.ts +572 -0
  28. package/api/src/queues/notification.ts +173 -128
  29. package/api/src/queues/payment.ts +210 -122
  30. package/api/src/queues/subscription.ts +179 -0
  31. package/api/src/routes/checkout-sessions.ts +157 -9
  32. package/api/src/routes/connect/shared.ts +3 -2
  33. package/api/src/routes/credit-grants.ts +241 -0
  34. package/api/src/routes/credit-transactions.ts +208 -0
  35. package/api/src/routes/index.ts +8 -0
  36. package/api/src/routes/meter-events.ts +347 -0
  37. package/api/src/routes/meters.ts +219 -0
  38. package/api/src/routes/payment-currencies.ts +14 -2
  39. package/api/src/routes/payment-links.ts +1 -1
  40. package/api/src/routes/payment-methods.ts +14 -2
  41. package/api/src/routes/prices.ts +43 -0
  42. package/api/src/routes/pricing-table.ts +13 -7
  43. package/api/src/routes/products.ts +63 -4
  44. package/api/src/routes/settings.ts +1 -1
  45. package/api/src/routes/subscriptions.ts +4 -0
  46. package/api/src/store/migrations/20250610-billing-credit.ts +43 -0
  47. package/api/src/store/models/credit-grant.ts +486 -0
  48. package/api/src/store/models/credit-transaction.ts +268 -0
  49. package/api/src/store/models/customer.ts +8 -0
  50. package/api/src/store/models/index.ts +52 -1
  51. package/api/src/store/models/meter-event.ts +423 -0
  52. package/api/src/store/models/meter.ts +176 -0
  53. package/api/src/store/models/payment-currency.ts +66 -14
  54. package/api/src/store/models/price.ts +6 -0
  55. package/api/src/store/models/product.ts +2 -2
  56. package/api/src/store/models/subscription.ts +24 -0
  57. package/api/src/store/models/types.ts +28 -2
  58. package/api/tests/libs/subscription.spec.ts +53 -0
  59. package/blocklet.yml +9 -1
  60. package/package.json +4 -4
  61. package/scripts/sdk.js +233 -1
  62. package/src/app.tsx +10 -0
  63. package/src/components/collapse.tsx +11 -1
  64. package/src/components/customer/credit-grant-item-list.tsx +99 -0
  65. package/src/components/customer/credit-overview.tsx +233 -0
  66. package/src/components/customer/form.tsx +5 -2
  67. package/src/components/invoice/list.tsx +19 -1
  68. package/src/components/metadata/form.tsx +286 -90
  69. package/src/components/meter/actions.tsx +101 -0
  70. package/src/components/meter/add-usage-dialog.tsx +239 -0
  71. package/src/components/meter/events-list.tsx +657 -0
  72. package/src/components/meter/form.tsx +245 -0
  73. package/src/components/meter/products.tsx +264 -0
  74. package/src/components/meter/usage-guide.tsx +174 -0
  75. package/src/components/payment-currency/form.tsx +2 -0
  76. package/src/components/payment-intent/list.tsx +19 -1
  77. package/src/components/payment-link/preview.tsx +1 -1
  78. package/src/components/payment-link/product-select.tsx +52 -12
  79. package/src/components/payment-method/arcblock.tsx +2 -0
  80. package/src/components/payment-method/base.tsx +2 -0
  81. package/src/components/payment-method/bitcoin.tsx +2 -0
  82. package/src/components/payment-method/ethereum.tsx +2 -0
  83. package/src/components/payment-method/stripe.tsx +2 -0
  84. package/src/components/payouts/list.tsx +19 -1
  85. package/src/components/price/currency-select.tsx +51 -31
  86. package/src/components/price/form.tsx +881 -407
  87. package/src/components/pricing-table/preview.tsx +1 -1
  88. package/src/components/product/add-price.tsx +9 -7
  89. package/src/components/product/create.tsx +7 -4
  90. package/src/components/product/edit-price.tsx +21 -12
  91. package/src/components/product/features.tsx +17 -7
  92. package/src/components/product/form.tsx +104 -89
  93. package/src/components/refund/list.tsx +19 -1
  94. package/src/components/section/header.tsx +5 -18
  95. package/src/components/subscription/items/index.tsx +1 -1
  96. package/src/components/subscription/metrics.tsx +37 -5
  97. package/src/components/subscription/portal/actions.tsx +2 -1
  98. package/src/contexts/products.tsx +26 -9
  99. package/src/hooks/subscription.ts +34 -0
  100. package/src/libs/meter-utils.ts +196 -0
  101. package/src/libs/util.ts +4 -0
  102. package/src/locales/en.tsx +385 -4
  103. package/src/locales/zh.tsx +364 -0
  104. package/src/pages/admin/billing/index.tsx +61 -33
  105. package/src/pages/admin/billing/invoices/detail.tsx +1 -1
  106. package/src/pages/admin/billing/meters/create.tsx +60 -0
  107. package/src/pages/admin/billing/meters/detail.tsx +435 -0
  108. package/src/pages/admin/billing/meters/index.tsx +210 -0
  109. package/src/pages/admin/billing/meters/meter-event.tsx +346 -0
  110. package/src/pages/admin/billing/subscriptions/detail.tsx +47 -14
  111. package/src/pages/admin/customers/customers/credit-grant/detail.tsx +391 -0
  112. package/src/pages/admin/customers/customers/detail.tsx +22 -10
  113. package/src/pages/admin/customers/index.tsx +5 -0
  114. package/src/pages/admin/developers/events/detail.tsx +1 -1
  115. package/src/pages/admin/developers/index.tsx +1 -1
  116. package/src/pages/admin/payments/intents/detail.tsx +1 -1
  117. package/src/pages/admin/payments/payouts/detail.tsx +1 -1
  118. package/src/pages/admin/payments/refunds/detail.tsx +1 -1
  119. package/src/pages/admin/products/index.tsx +3 -2
  120. package/src/pages/admin/products/links/detail.tsx +1 -1
  121. package/src/pages/admin/products/prices/actions.tsx +16 -4
  122. package/src/pages/admin/products/prices/detail.tsx +30 -3
  123. package/src/pages/admin/products/prices/list.tsx +8 -1
  124. package/src/pages/admin/products/pricing-tables/detail.tsx +1 -1
  125. package/src/pages/admin/products/products/create.tsx +233 -57
  126. package/src/pages/admin/products/products/detail.tsx +2 -1
  127. package/src/pages/admin/settings/payment-methods/index.tsx +3 -0
  128. package/src/pages/customer/credit-grant/detail.tsx +308 -0
  129. package/src/pages/customer/index.tsx +35 -2
  130. package/src/pages/customer/recharge/account.tsx +5 -5
  131. package/src/pages/customer/subscription/change-payment.tsx +4 -2
  132. package/src/pages/customer/subscription/detail.tsx +48 -14
  133. package/src/pages/customer/subscription/embed.tsx +1 -1
@@ -1,28 +1,14 @@
1
1
  /* eslint-disable @typescript-eslint/brace-style */
2
2
  /* eslint-disable @typescript-eslint/indent */
3
- import { fromUnitToToken, toDid } from '@ocap/util';
3
+ import { toDid } from '@ocap/util';
4
4
  import pWaitFor from 'p-wait-for';
5
- import prettyMsI18n from 'pretty-ms-i18n';
6
-
7
5
  import isEmpty from 'lodash/isEmpty';
8
- import { getUserLocale } from '../../../integrations/blocklet/notification';
6
+
9
7
  import { translate } from '../../../locales';
10
- import {
11
- CheckoutSession,
12
- Customer,
13
- NFTMintChainType,
14
- Invoice,
15
- NftMintItem,
16
- PaymentMethod,
17
- Subscription,
18
- } from '../../../store/models';
19
- import { PaymentCurrency } from '../../../store/models/payment-currency';
20
- import { getCustomerInvoicePageUrl, getOneTimeProductInfo } from '../../invoice';
21
- import { getMainProductName } from '../../product';
22
- import { getCustomerSubscriptionPageUrl } from '../../subscription';
23
- import { formatTime, getPrettyMsI18nLocale } from '../../time';
24
- import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
25
- import { getSubscriptionNotificationCustomActions } from '../../util';
8
+ import { CheckoutSession, NFTMintChainType, NftMintItem } from '../../../store/models';
9
+ import { getOneTimeProductInfo } from '../../invoice';
10
+ import { formatTime } from '../../time';
11
+ import { BaseSubscriptionEmailTemplate, BaseEmailTemplateType } from './base';
26
12
 
27
13
  export interface SubscriptionTrialStartEmailTemplateOptions {
28
14
  subscriptionId: string;
@@ -51,31 +37,27 @@ interface SubscriptionTrialStartEmailTemplateContext {
51
37
  viewInvoiceLink: string;
52
38
  oneTimeProductInfo?: Array<OneTimeProductInfo>;
53
39
  customActions: any[];
40
+ isCreditSubscription: boolean;
54
41
  }
55
42
 
56
- export class SubscriptionTrialStartEmailTemplate
57
- implements BaseEmailTemplate<SubscriptionTrialStartEmailTemplateContext>
58
- {
43
+ export class SubscriptionTrialStartEmailTemplate extends BaseSubscriptionEmailTemplate<SubscriptionTrialStartEmailTemplateContext> {
59
44
  options: SubscriptionTrialStartEmailTemplateOptions;
60
45
 
61
46
  constructor(options: SubscriptionTrialStartEmailTemplateOptions) {
47
+ super();
62
48
  this.options = options;
63
49
  }
64
50
 
65
51
  async getContext(): Promise<SubscriptionTrialStartEmailTemplateContext> {
66
- const subscription: Subscription | null = await Subscription.findByPk(this.options.subscriptionId);
67
- if (!subscription) {
68
- throw new Error(`Subscription not found: ${this.options.subscriptionId}`);
69
- }
52
+ // 获取基础订阅数据
53
+ const basicData = await this.getSubscriptionBasicData(this.options.subscriptionId);
54
+ const { subscription, userDid, locale, productName, paymentCurrency, isCreditSubscription } = basicData;
55
+
70
56
  if (subscription.status !== 'trialing') {
71
57
  throw new Error(`Subscription not trialing: ${this.options.subscriptionId}`);
72
58
  }
73
59
 
74
- const customer = await Customer.findByPk(subscription.customer_id);
75
- if (!customer) {
76
- throw new Error(`Customer not found: ${subscription.customer_id}`);
77
- }
78
-
60
+ // 等待 NFT 铸造完成
79
61
  await pWaitFor(
80
62
  async () => {
81
63
  const checkoutSession = await CheckoutSession.findBySubscriptionId(subscription.id);
@@ -84,57 +66,39 @@ export class SubscriptionTrialStartEmailTemplate
84
66
  { timeout: 1000 * 10, interval: 1000 }
85
67
  );
86
68
 
87
- const invoice: Invoice = (await Invoice.findByPk(subscription.latest_invoice_id)) as Invoice;
88
- if (!invoice) {
89
- throw new Error(`Invoice not found in subscription: ${subscription.id}`);
90
- }
91
-
92
- const paymentCurrency = (await PaymentCurrency.findOne({
93
- where: {
94
- id: subscription.currency_id,
95
- },
96
- })) as PaymentCurrency;
97
-
69
+ // 获取 NFT 和支付信息
70
+ const paymentInfoResult = await this.getPaymentInfo(
71
+ subscription,
72
+ paymentCurrency,
73
+ isCreditSubscription,
74
+ subscription.latest_invoice_id
75
+ );
98
76
  const oneTimeProductInfo = await getOneTimeProductInfo(subscription.latest_invoice_id as string, paymentCurrency);
99
77
 
100
78
  const checkoutSession = await CheckoutSession.findBySubscriptionId(subscription.id);
101
-
102
- const userDid: string = customer.did;
103
- const locale = await getUserLocale(userDid);
104
- const productName = await getMainProductName(subscription.id);
105
- const subscriptionTrialEnd: string = formatTime((subscription.trial_end as number) * 1000);
106
-
107
- const hasNft: boolean = checkoutSession?.nft_mint_status === 'minted';
108
- const nftMintItem: NftMintItem | undefined = hasNft
79
+ const hasNft = checkoutSession?.nft_mint_status === 'minted';
80
+ const nftMintItem = hasNft
109
81
  ? checkoutSession?.nft_mint_details?.[checkoutSession?.nft_mint_details?.type as NFTMintChainType]
110
82
  : undefined;
111
- const paymentMethod: PaymentMethod | null = await PaymentMethod.findByPk(invoice.default_payment_method_id);
112
- const chainHost: string | undefined =
113
- paymentMethod?.settings?.[checkoutSession?.nft_mint_details?.type as NFTMintChainType]?.api_host;
114
- const paymentInfo: string = `${fromUnitToToken('0', paymentCurrency.decimal)} ${paymentCurrency.symbol}`;
115
- const currentPeriodStart: string = formatTime((subscription.trial_start as number) * 1000);
116
- const currentPeriodEnd: string = formatTime((subscription.trial_end as number) * 1000);
117
- const duration: string = prettyMsI18n(
118
- new Date(currentPeriodEnd).getTime() - new Date(currentPeriodStart).getTime(),
119
- {
120
- locale: getPrettyMsI18nLocale(locale),
121
- }
122
- );
83
+ const chainHost =
84
+ paymentInfoResult.paymentMethod?.settings?.[checkoutSession?.nft_mint_details?.type as NFTMintChainType]
85
+ ?.api_host;
123
86
 
124
- const viewSubscriptionLink = getCustomerSubscriptionPageUrl({
125
- subscriptionId: subscription.id,
87
+ // 获取链接
88
+ const links = this.generateSubscriptionLinks(
89
+ subscription,
126
90
  locale,
127
91
  userDid,
128
- });
129
- const viewInvoiceLink = getCustomerInvoicePageUrl({
130
- invoiceId: invoice.id,
131
- userDid,
132
- locale,
133
- });
134
-
135
- const customActions = getSubscriptionNotificationCustomActions(
136
- subscription,
137
92
  'customer.subscription.trial_start',
93
+ subscription.latest_invoice_id,
94
+ isCreditSubscription
95
+ );
96
+
97
+ // 计算时间信息
98
+ const subscriptionTrialEnd = formatTime((subscription.trial_end as number) * 1000);
99
+ const periodInfo = this.formatSubscriptionPeriod(
100
+ subscription.trial_start as number,
101
+ subscription.trial_end as number,
138
102
  locale
139
103
  );
140
104
 
@@ -142,23 +106,22 @@ export class SubscriptionTrialStartEmailTemplate
142
106
  locale,
143
107
  productName,
144
108
  subscriptionTrialEnd,
145
-
146
109
  nftMintItem,
147
110
  chainHost,
148
111
  userDid,
149
- paymentInfo,
150
- currentPeriodStart,
151
- currentPeriodEnd,
152
- duration,
153
-
154
- viewSubscriptionLink,
155
- viewInvoiceLink,
112
+ paymentInfo: '0',
113
+ currentPeriodStart: periodInfo.currentPeriodStart,
114
+ currentPeriodEnd: periodInfo.currentPeriodEnd,
115
+ duration: periodInfo.duration,
116
+ viewSubscriptionLink: links.viewSubscriptionLink,
117
+ viewInvoiceLink: links.viewInvoiceLink!,
156
118
  oneTimeProductInfo,
157
- customActions,
119
+ customActions: links.customActions,
120
+ isCreditSubscription,
158
121
  };
159
122
  }
160
123
 
161
- getOneTimeProductTemplate(oneTimeProductInfo: Array<OneTimeProductInfo>, locale: string = 'en') {
124
+ private getOneTimeProductTemplate(oneTimeProductInfo: Array<OneTimeProductInfo>, locale: string = 'en') {
162
125
  return [
163
126
  {
164
127
  type: 'divider',
@@ -226,134 +189,125 @@ export class SubscriptionTrialStartEmailTemplate
226
189
  }
227
190
 
228
191
  async getTemplate(): Promise<BaseEmailTemplateType> {
192
+ const context = await this.getContext();
229
193
  const {
230
194
  locale,
231
195
  productName,
232
196
  subscriptionTrialEnd,
233
-
234
197
  nftMintItem,
235
198
  chainHost,
236
199
  userDid,
237
200
  currentPeriodStart,
238
201
  currentPeriodEnd,
239
202
  duration,
240
-
241
203
  viewSubscriptionLink,
242
204
  viewInvoiceLink,
243
205
  oneTimeProductInfo,
244
206
  customActions,
245
- } = await this.getContext();
207
+ isCreditSubscription,
208
+ } = context;
246
209
 
247
210
  const hasOneTimeProduct = !isEmpty(oneTimeProductInfo);
248
- const template: BaseEmailTemplateType = {
249
- title: `${translate('notification.subscriptionTrialStart.title', locale, {
250
- productName,
251
- })}`,
252
- body: `${translate('notification.subscriptionTrialStart.body', locale, {
253
- subscriptionTrialEnd,
254
- productName,
255
- trialDuration: duration,
256
- })}`,
257
- // @ts-expect-error
258
- attachments: [
259
- nftMintItem &&
260
- chainHost && {
261
- type: 'asset',
211
+
212
+ // 构建字段
213
+ const commonFields = this.buildCommonFields(userDid, productName, locale);
214
+
215
+ // 试用期字段
216
+ const trialPeriodFields = isCreditSubscription
217
+ ? []
218
+ : [
219
+ {
220
+ type: 'text',
262
221
  data: {
263
- chainHost,
264
- did: nftMintItem.address,
222
+ type: 'plain',
223
+ color: '#9397A1',
224
+ text: translate('notification.common.trialPeriod', locale),
265
225
  },
266
226
  },
267
- hasOneTimeProduct && {
268
- type: 'text',
269
- data: {
270
- type: 'plain',
271
- text: translate('notification.common.trialProduct', locale),
272
- },
273
- },
274
- {
275
- type: 'section',
276
- fields: [
277
- {
278
- type: 'text',
279
- data: {
280
- type: 'plain',
281
- color: '#9397A1',
282
- text: translate('notification.common.account', locale),
283
- },
284
- },
285
- {
286
- type: 'text',
287
- data: {
288
- type: 'plain',
289
- text: userDid,
290
- },
291
- },
292
- {
293
- type: 'text',
294
- data: {
295
- type: 'plain',
296
- color: '#9397A1',
297
- text: translate('notification.common.product', locale),
298
- },
299
- },
300
- {
301
- type: 'text',
302
- data: {
303
- type: 'plain',
304
- text: productName,
305
- },
306
- },
307
- {
308
- type: 'text',
309
- data: {
310
- type: 'plain',
311
- color: '#9397A1',
312
- text: translate('notification.common.trialPeriod', locale),
313
- },
314
- },
315
- {
316
- type: 'text',
317
- data: {
318
- type: 'plain',
319
- text: `${currentPeriodStart} ~ ${currentPeriodEnd}(${duration})`,
320
- },
227
+ {
228
+ type: 'text',
229
+ data: {
230
+ type: 'plain',
231
+ text: `${currentPeriodStart} ~ ${currentPeriodEnd}(${duration})`,
321
232
  },
322
- nftMintItem && {
323
- type: 'text',
324
- data: {
325
- type: 'plain',
326
- color: '#9397A1',
327
- text: translate('notification.common.nftAddress', locale),
328
- },
233
+ },
234
+ ];
235
+
236
+ // NFT 地址字段
237
+ const nftFields = nftMintItem
238
+ ? [
239
+ {
240
+ type: 'text',
241
+ data: {
242
+ type: 'plain',
243
+ color: '#9397A1',
244
+ text: translate('notification.common.nftAddress', locale),
329
245
  },
330
- nftMintItem && {
331
- type: 'text',
332
- data: {
333
- type: 'plain',
334
- text: toDid(nftMintItem.address),
335
- },
246
+ },
247
+ {
248
+ type: 'text',
249
+ data: {
250
+ type: 'plain',
251
+ text: toDid(nftMintItem.address),
336
252
  },
337
- ].filter(Boolean),
338
- },
339
- ...(hasOneTimeProduct
340
- ? this.getOneTimeProductTemplate(oneTimeProductInfo as Array<OneTimeProductInfo>, locale)
341
- : []),
342
- ].filter(Boolean),
343
- // @ts-ignore
344
- actions: [
345
- {
346
- name: translate('notification.common.viewSubscription', locale),
347
- title: translate('notification.common.viewSubscription', locale),
348
- link: viewSubscriptionLink,
253
+ },
254
+ ]
255
+ : [];
256
+
257
+ // 构建附件
258
+ const attachments = [
259
+ nftMintItem &&
260
+ chainHost && {
261
+ type: 'asset',
262
+ data: {
263
+ chainHost,
264
+ did: nftMintItem.address,
265
+ },
349
266
  },
350
- {
351
- name: translate('notification.common.viewInvoice', locale),
352
- title: translate('notification.common.viewInvoice', locale),
353
- link: viewInvoiceLink,
267
+ hasOneTimeProduct && {
268
+ type: 'text',
269
+ data: {
270
+ type: 'plain',
271
+ text: translate('notification.common.trialProduct', locale),
354
272
  },
355
- ...customActions,
356
- ].filter(Boolean),
273
+ },
274
+ {
275
+ type: 'section',
276
+ fields: [...commonFields, ...trialPeriodFields, ...nftFields].filter(Boolean),
277
+ },
278
+ ...(hasOneTimeProduct
279
+ ? this.getOneTimeProductTemplate(oneTimeProductInfo as Array<OneTimeProductInfo>, locale)
280
+ : []),
281
+ ].filter(Boolean);
282
+
283
+ // 构建操作按钮
284
+ const actions = [
285
+ {
286
+ name: translate('notification.common.viewSubscription', locale),
287
+ title: translate('notification.common.viewSubscription', locale),
288
+ link: viewSubscriptionLink,
289
+ },
290
+ {
291
+ name: translate('notification.common.viewInvoice', locale),
292
+ title: translate('notification.common.viewInvoice', locale),
293
+ link: viewInvoiceLink,
294
+ },
295
+ ...customActions,
296
+ ].filter(Boolean);
297
+
298
+ const template: BaseEmailTemplateType = {
299
+ title: translate('notification.subscriptionTrialStart.title', locale, {
300
+ productName,
301
+ }),
302
+ body: translate('notification.subscriptionTrialStart.body', locale, {
303
+ subscriptionTrialEnd,
304
+ productName,
305
+ trialDuration: duration,
306
+ }),
307
+ // @ts-expect-error
308
+ attachments,
309
+ // @ts-ignore
310
+ actions,
357
311
  };
358
312
 
359
313
  return template;