payment-kit 1.18.56 → 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 (214) hide show
  1. package/.eslintrc.js +6 -0
  2. package/api/src/crons/index.ts +8 -0
  3. package/api/src/index.ts +4 -0
  4. package/api/src/libs/credit-grant.ts +146 -0
  5. package/api/src/libs/env.ts +1 -0
  6. package/api/src/libs/invoice.ts +4 -3
  7. package/api/src/libs/notification/template/base.ts +388 -2
  8. package/api/src/libs/notification/template/customer-credit-grant-granted.ts +149 -0
  9. package/api/src/libs/notification/template/customer-credit-grant-low-balance.ts +151 -0
  10. package/api/src/libs/notification/template/customer-credit-insufficient.ts +254 -0
  11. package/api/src/libs/notification/template/subscription-canceled.ts +193 -202
  12. package/api/src/libs/notification/template/subscription-refund-succeeded.ts +215 -237
  13. package/api/src/libs/notification/template/subscription-renewed.ts +130 -200
  14. package/api/src/libs/notification/template/subscription-succeeded.ts +100 -202
  15. package/api/src/libs/notification/template/subscription-trial-start.ts +142 -188
  16. package/api/src/libs/notification/template/subscription-trial-will-end.ts +146 -174
  17. package/api/src/libs/notification/template/subscription-upgraded.ts +96 -192
  18. package/api/src/libs/notification/template/subscription-will-canceled.ts +94 -135
  19. package/api/src/libs/notification/template/subscription-will-renew.ts +220 -245
  20. package/api/src/libs/payment.ts +69 -0
  21. package/api/src/libs/queue/index.ts +3 -2
  22. package/api/src/libs/session.ts +8 -0
  23. package/api/src/libs/subscription.ts +74 -3
  24. package/api/src/libs/ws.ts +23 -1
  25. package/api/src/locales/en.ts +33 -0
  26. package/api/src/locales/zh.ts +31 -0
  27. package/api/src/queues/credit-consume.ts +715 -0
  28. package/api/src/queues/credit-grant.ts +572 -0
  29. package/api/src/queues/notification.ts +173 -128
  30. package/api/src/queues/payment.ts +210 -122
  31. package/api/src/queues/subscription.ts +179 -0
  32. package/api/src/routes/checkout-sessions.ts +157 -9
  33. package/api/src/routes/connect/shared.ts +3 -2
  34. package/api/src/routes/credit-grants.ts +241 -0
  35. package/api/src/routes/credit-transactions.ts +208 -0
  36. package/api/src/routes/index.ts +8 -0
  37. package/api/src/routes/meter-events.ts +347 -0
  38. package/api/src/routes/meters.ts +219 -0
  39. package/api/src/routes/payment-currencies.ts +14 -2
  40. package/api/src/routes/payment-links.ts +1 -1
  41. package/api/src/routes/payment-methods.ts +14 -2
  42. package/api/src/routes/prices.ts +43 -0
  43. package/api/src/routes/pricing-table.ts +13 -7
  44. package/api/src/routes/products.ts +63 -4
  45. package/api/src/routes/settings.ts +1 -1
  46. package/api/src/routes/subscriptions.ts +4 -0
  47. package/api/src/store/migrations/20250610-billing-credit.ts +43 -0
  48. package/api/src/store/models/credit-grant.ts +486 -0
  49. package/api/src/store/models/credit-transaction.ts +268 -0
  50. package/api/src/store/models/customer.ts +8 -0
  51. package/api/src/store/models/index.ts +52 -1
  52. package/api/src/store/models/meter-event.ts +423 -0
  53. package/api/src/store/models/meter.ts +176 -0
  54. package/api/src/store/models/payment-currency.ts +66 -14
  55. package/api/src/store/models/price.ts +6 -0
  56. package/api/src/store/models/product.ts +2 -2
  57. package/api/src/store/models/subscription.ts +24 -0
  58. package/api/src/store/models/types.ts +28 -2
  59. package/api/tests/libs/subscription.spec.ts +53 -0
  60. package/blocklet.yml +9 -1
  61. package/package.json +57 -58
  62. package/scripts/sdk.js +233 -1
  63. package/src/app.tsx +10 -0
  64. package/src/components/actions.tsx +22 -9
  65. package/src/components/balance-list.tsx +40 -12
  66. package/src/components/collapse.tsx +33 -15
  67. package/src/components/copyable.tsx +8 -7
  68. package/src/components/currency.tsx +15 -7
  69. package/src/components/customer/actions.tsx +1 -5
  70. package/src/components/customer/credit-grant-item-list.tsx +99 -0
  71. package/src/components/customer/credit-overview.tsx +233 -0
  72. package/src/components/customer/form.tsx +7 -2
  73. package/src/components/customer/link.tsx +4 -12
  74. package/src/components/customer/notification-preference.tsx +18 -9
  75. package/src/components/customer/overdraft-protection.tsx +112 -41
  76. package/src/components/drawer-form.tsx +42 -18
  77. package/src/components/error.tsx +1 -5
  78. package/src/components/event/list.tsx +9 -10
  79. package/src/components/filter-toolbar.tsx +20 -19
  80. package/src/components/info-card.tsx +32 -18
  81. package/src/components/info-metric.tsx +16 -6
  82. package/src/components/info-row-group.tsx +1 -7
  83. package/src/components/info-row.tsx +30 -24
  84. package/src/components/invoice/action.tsx +1 -7
  85. package/src/components/invoice/list.tsx +34 -26
  86. package/src/components/invoice/recharge.tsx +5 -7
  87. package/src/components/invoice/table.tsx +17 -12
  88. package/src/components/layout/user.tsx +1 -1
  89. package/src/components/metadata/form.tsx +290 -94
  90. package/src/components/metadata/list.tsx +11 -3
  91. package/src/components/meter/actions.tsx +101 -0
  92. package/src/components/meter/add-usage-dialog.tsx +239 -0
  93. package/src/components/meter/events-list.tsx +657 -0
  94. package/src/components/meter/form.tsx +245 -0
  95. package/src/components/meter/products.tsx +264 -0
  96. package/src/components/meter/usage-guide.tsx +174 -0
  97. package/src/components/passport/actions.tsx +9 -4
  98. package/src/components/payment-currency/add.tsx +16 -3
  99. package/src/components/payment-currency/form.tsx +14 -6
  100. package/src/components/payment-intent/actions.tsx +24 -16
  101. package/src/components/payment-intent/list.tsx +30 -9
  102. package/src/components/payment-link/actions.tsx +1 -5
  103. package/src/components/payment-link/after-pay.tsx +4 -2
  104. package/src/components/payment-link/before-pay.tsx +14 -4
  105. package/src/components/payment-link/item.tsx +27 -6
  106. package/src/components/payment-link/preview.tsx +9 -9
  107. package/src/components/payment-link/product-select.tsx +69 -15
  108. package/src/components/payment-method/arcblock.tsx +8 -1
  109. package/src/components/payment-method/base.tsx +8 -1
  110. package/src/components/payment-method/bitcoin.tsx +8 -1
  111. package/src/components/payment-method/ethereum.tsx +8 -1
  112. package/src/components/payment-method/evm-rpc-input.tsx +11 -7
  113. package/src/components/payment-method/form.tsx +2 -7
  114. package/src/components/payment-method/stripe.tsx +2 -0
  115. package/src/components/payouts/actions.tsx +1 -5
  116. package/src/components/payouts/list.tsx +30 -10
  117. package/src/components/payouts/portal/list.tsx +11 -9
  118. package/src/components/price/currency-select.tsx +63 -32
  119. package/src/components/price/form.tsx +895 -370
  120. package/src/components/price/upsell-select.tsx +10 -2
  121. package/src/components/price/upsell.tsx +7 -2
  122. package/src/components/pricing-table/actions.tsx +1 -5
  123. package/src/components/pricing-table/customer-settings.tsx +5 -1
  124. package/src/components/pricing-table/payment-settings.tsx +14 -4
  125. package/src/components/pricing-table/preview.tsx +9 -9
  126. package/src/components/pricing-table/price-item.tsx +6 -1
  127. package/src/components/pricing-table/product-item.tsx +6 -1
  128. package/src/components/pricing-table/product-settings.tsx +17 -4
  129. package/src/components/product/actions.tsx +1 -5
  130. package/src/components/product/add-price.tsx +9 -7
  131. package/src/components/product/create.tsx +8 -9
  132. package/src/components/product/cross-sell-select.tsx +5 -1
  133. package/src/components/product/cross-sell.tsx +7 -2
  134. package/src/components/product/edit-price.tsx +21 -12
  135. package/src/components/product/features.tsx +26 -6
  136. package/src/components/product/form.tsx +115 -72
  137. package/src/components/progress-bar.tsx +1 -1
  138. package/src/components/refund/actions.tsx +1 -7
  139. package/src/components/refund/list.tsx +31 -18
  140. package/src/components/section/header.tsx +12 -14
  141. package/src/components/subscription/actions/cancel.tsx +22 -5
  142. package/src/components/subscription/actions/index.tsx +9 -10
  143. package/src/components/subscription/actions/pause.tsx +32 -6
  144. package/src/components/subscription/actions/slash-stake.tsx +5 -3
  145. package/src/components/subscription/description.tsx +12 -8
  146. package/src/components/subscription/items/index.tsx +31 -16
  147. package/src/components/subscription/items/usage-records.tsx +19 -5
  148. package/src/components/subscription/list.tsx +5 -7
  149. package/src/components/subscription/metrics.tsx +62 -15
  150. package/src/components/subscription/portal/actions.tsx +78 -71
  151. package/src/components/subscription/portal/cancel.tsx +10 -3
  152. package/src/components/subscription/portal/list.tsx +48 -26
  153. package/src/components/uploader.tsx +5 -13
  154. package/src/components/webhook/attempts.tsx +51 -16
  155. package/src/components/webhook/request-info.tsx +8 -6
  156. package/src/contexts/products.tsx +27 -10
  157. package/src/hooks/subscription.ts +34 -0
  158. package/src/libs/meter-utils.ts +196 -0
  159. package/src/libs/util.ts +4 -0
  160. package/src/locales/en.tsx +385 -4
  161. package/src/locales/zh.tsx +364 -0
  162. package/src/pages/admin/billing/index.tsx +61 -33
  163. package/src/pages/admin/billing/invoices/detail.tsx +49 -13
  164. package/src/pages/admin/billing/meters/create.tsx +60 -0
  165. package/src/pages/admin/billing/meters/detail.tsx +435 -0
  166. package/src/pages/admin/billing/meters/index.tsx +210 -0
  167. package/src/pages/admin/billing/meters/meter-event.tsx +346 -0
  168. package/src/pages/admin/billing/subscriptions/detail.tsx +90 -25
  169. package/src/pages/admin/customers/customers/credit-grant/detail.tsx +391 -0
  170. package/src/pages/admin/customers/customers/detail.tsx +67 -14
  171. package/src/pages/admin/customers/customers/index.tsx +6 -1
  172. package/src/pages/admin/customers/index.tsx +5 -0
  173. package/src/pages/admin/developers/events/detail.tsx +37 -11
  174. package/src/pages/admin/developers/index.tsx +1 -1
  175. package/src/pages/admin/developers/webhooks/detail.tsx +41 -11
  176. package/src/pages/admin/index.tsx +15 -2
  177. package/src/pages/admin/overview.tsx +107 -19
  178. package/src/pages/admin/payments/intents/detail.tsx +58 -14
  179. package/src/pages/admin/payments/payouts/detail.tsx +63 -15
  180. package/src/pages/admin/payments/refunds/detail.tsx +58 -14
  181. package/src/pages/admin/products/index.tsx +11 -4
  182. package/src/pages/admin/products/links/create.tsx +22 -4
  183. package/src/pages/admin/products/links/detail.tsx +43 -14
  184. package/src/pages/admin/products/passports/index.tsx +23 -4
  185. package/src/pages/admin/products/prices/actions.tsx +16 -9
  186. package/src/pages/admin/products/prices/detail.tsx +73 -14
  187. package/src/pages/admin/products/prices/list.tsx +15 -3
  188. package/src/pages/admin/products/pricing-tables/create.tsx +45 -12
  189. package/src/pages/admin/products/pricing-tables/detail.tsx +45 -14
  190. package/src/pages/admin/products/products/create.tsx +233 -54
  191. package/src/pages/admin/products/products/detail.tsx +74 -18
  192. package/src/pages/admin/settings/index.tsx +8 -1
  193. package/src/pages/admin/settings/payment-methods/index.tsx +87 -19
  194. package/src/pages/admin/settings/vault-config/edit-form.tsx +42 -28
  195. package/src/pages/admin/settings/vault-config/index.tsx +57 -10
  196. package/src/pages/customer/credit-grant/detail.tsx +308 -0
  197. package/src/pages/customer/index.tsx +76 -17
  198. package/src/pages/customer/invoice/detail.tsx +63 -14
  199. package/src/pages/customer/invoice/past-due.tsx +11 -3
  200. package/src/pages/customer/payout/detail.tsx +56 -13
  201. package/src/pages/customer/recharge/account.tsx +78 -18
  202. package/src/pages/customer/recharge/subscription.tsx +86 -25
  203. package/src/pages/customer/refund/list.tsx +60 -24
  204. package/src/pages/customer/subscription/change-payment.tsx +17 -6
  205. package/src/pages/customer/subscription/change-plan.tsx +34 -7
  206. package/src/pages/customer/subscription/detail.tsx +134 -34
  207. package/src/pages/customer/subscription/embed.tsx +25 -5
  208. package/src/pages/home.tsx +26 -4
  209. package/src/pages/integrations/donations/edit-form.tsx +25 -9
  210. package/src/pages/integrations/donations/index.tsx +26 -9
  211. package/src/pages/integrations/donations/preview.tsx +59 -15
  212. package/src/pages/integrations/index.tsx +10 -1
  213. package/src/pages/integrations/overview.tsx +78 -17
  214. package/vite.config.ts +60 -30
@@ -1,33 +1,18 @@
1
1
  /* eslint-disable @typescript-eslint/brace-style */
2
2
  /* eslint-disable @typescript-eslint/indent */
3
3
  import type { ManipulateType } from 'dayjs';
4
- import prettyMsI18n from 'pretty-ms-i18n';
5
4
  import type { LiteralUnion } from 'type-fest';
6
5
 
7
6
  import { fromUnitToToken } from '@ocap/util';
8
7
  import dayjs from '../../dayjs';
9
8
  import { getTokenByAddress } from '../../../integrations/arcblock/stake';
10
- import { getUserLocale } from '../../../integrations/blocklet/notification';
11
9
  import { translate } from '../../../locales';
12
- import {
13
- Customer,
14
- Invoice,
15
- PaymentMethod,
16
- Price,
17
- Subscription,
18
- SubscriptionItem,
19
- PaymentCurrency,
20
- } from '../../../store/models';
10
+ import { Invoice, Price, SubscriptionItem } from '../../../store/models';
21
11
  import type { PaymentDetail } from '../../payment';
22
- import { getMainProductName } from '../../product';
23
- import {
24
- getCustomerSubscriptionPageUrl,
25
- getSubscriptionPaymentAddress,
26
- getPaymentAmountForCycleSubscription,
27
- } from '../../subscription';
28
- import { formatTime, getPrettyMsI18nLocale, getSimplifyDuration } from '../../time';
29
- import { formatCurrencyInfo, getCustomerRechargeLink, getSubscriptionNotificationCustomActions } from '../../util';
30
- import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
12
+ import { getSubscriptionPaymentAddress, getPaymentAmountForCycleSubscription } from '../../subscription';
13
+ import { formatTime, getSimplifyDuration } from '../../time';
14
+ import { formatCurrencyInfo, getCustomerRechargeLink } from '../../util';
15
+ import { BaseSubscriptionEmailTemplate, BaseEmailTemplateType } from './base';
31
16
 
32
17
  export interface SubscriptionWillRenewEmailTemplateOptions {
33
18
  subscriptionId: string;
@@ -43,110 +28,109 @@ interface SubscriptionWillRenewEmailTemplateContext {
43
28
  willRenewDuration: string;
44
29
  paymentDetail: PaymentDetail;
45
30
  paidType: string;
46
-
47
31
  userDid: string;
48
32
  paymentInfo: string;
49
33
  currentPeriodStart: string;
50
34
  currentPeriodEnd: string;
51
35
  duration: string;
52
-
53
36
  viewSubscriptionLink: string;
54
37
  addFundsLink: string;
55
- paymentMethod: PaymentMethod | null;
56
38
  customActions: any[];
39
+ isCreditSubscription: boolean;
40
+ isStripe: boolean;
57
41
  }
58
42
 
59
- export class SubscriptionWillRenewEmailTemplate
60
- implements BaseEmailTemplate<SubscriptionWillRenewEmailTemplateContext>
61
- {
43
+ export class SubscriptionWillRenewEmailTemplate extends BaseSubscriptionEmailTemplate<SubscriptionWillRenewEmailTemplateContext> {
62
44
  options: SubscriptionWillRenewEmailTemplateOptions;
63
45
 
64
46
  constructor(options: SubscriptionWillRenewEmailTemplateOptions) {
47
+ super();
65
48
  this.options = options;
66
49
  }
67
50
 
68
51
  async getContext(): Promise<SubscriptionWillRenewEmailTemplateContext> {
69
- const subscription: Subscription | null = await Subscription.findByPk(this.options.subscriptionId);
70
- if (!subscription) {
71
- throw new Error(`Subscription not found: ${this.options.subscriptionId}`);
72
- }
52
+ // 获取基础订阅数据
53
+ const basicData = await this.getSubscriptionBasicData(this.options.subscriptionId);
54
+ const { subscription, userDid, locale, productName, paymentCurrency, isCreditSubscription } = basicData;
55
+
73
56
  if (subscription.isActive() === false) {
74
57
  throw new Error(`Subscription not active: ${this.options.subscriptionId}`);
75
58
  }
76
59
  if (subscription.isScheduledToCancel()) {
77
60
  throw new Error(`Subscription is scheduled to cancel: ${this.options.subscriptionId}`);
78
61
  }
79
- if (await this.subscriptionIsRenewed(subscription)) {
62
+ if (this.subscriptionIsRenewed(subscription)) {
80
63
  throw new Error(`The subscription(${this.options.subscriptionId}) has been renewed`);
81
64
  }
82
65
 
83
- const customer = await Customer.findByPk(subscription.customer_id);
84
- if (!customer) {
85
- throw new Error(`Customer not found: ${subscription.customer_id}`);
86
- }
66
+ const invoice = await Invoice.findByPk(subscription.latest_invoice_id);
87
67
 
88
- const invoice = (await Invoice.findByPk(subscription.latest_invoice_id)) as Invoice;
89
- const paymentCurrency = (await PaymentCurrency.findOne({
90
- where: {
91
- id: subscription.currency_id,
92
- },
93
- })) as PaymentCurrency;
68
+ // 获取支付信息
69
+ const paymentInfoResult = await this.getPaymentInfo(
70
+ subscription,
71
+ paymentCurrency,
72
+ isCreditSubscription,
73
+ invoice?.id
74
+ );
94
75
 
95
- const userDid = customer.did;
96
- const locale = await getUserLocale(userDid);
97
- const productName = await getMainProductName(subscription.id);
98
- const at: string = formatTime(invoice.period_end * 1000);
99
- const willRenewDuration = getSimplifyDuration((invoice.period_end - dayjs().unix()) * 1000, locale);
76
+ // 获取链接
77
+ const links = this.generateSubscriptionLinks(
78
+ subscription,
79
+ locale,
80
+ userDid,
81
+ 'customer.subscription.will_renew',
82
+ invoice?.id,
83
+ isCreditSubscription
84
+ );
85
+
86
+ // 计算时间和支付详情 - 使用invoice的period_end,如果不存在则使用subscription的current_period_end
87
+ const periodEnd = invoice?.period_end || subscription.current_period_end;
88
+ const at = formatTime(periodEnd * 1000);
89
+ const willRenewDuration = getSimplifyDuration((periodEnd - dayjs().unix()) * 1000, locale);
100
90
 
101
91
  const paymentAmount = await getPaymentAmountForCycleSubscription(subscription, paymentCurrency);
102
92
  const paymentDetail = { price: paymentAmount, balance: 0, symbol: paymentCurrency.symbol, balanceFormatted: '0' };
103
93
 
104
- const paymentMethod = await PaymentMethod.findByPk(paymentCurrency.payment_method_id);
105
- if (!paymentMethod) {
106
- throw new Error(`Payment method not found: ${paymentCurrency.payment_method_id}`);
94
+ // 获取余额信息(非 Stripe 支付方式)
95
+ const isStripe = paymentInfoResult.paymentMethod?.type === 'stripe';
96
+ if (!isStripe && !isCreditSubscription) {
97
+ const paymentAddress = getSubscriptionPaymentAddress(subscription, paymentInfoResult.paymentMethod!.type);
98
+ const balance = await getTokenByAddress(paymentAddress, paymentInfoResult.paymentMethod!, paymentCurrency);
99
+ paymentDetail.balanceFormatted = fromUnitToToken(balance, paymentCurrency.decimal);
100
+ paymentDetail.balance = +paymentDetail.balanceFormatted;
107
101
  }
108
102
 
109
- const paymentAddress = getSubscriptionPaymentAddress(subscription, paymentMethod.type);
110
- const balance = await getTokenByAddress(paymentAddress, paymentMethod, paymentCurrency);
111
- paymentDetail.balanceFormatted = fromUnitToToken(balance, paymentCurrency.decimal);
112
- paymentDetail.balance = +paymentDetail.balanceFormatted;
113
-
114
- const { isPrePaid, interval } = await this.getPaymentCategory({
115
- subscriptionId: subscription.id,
116
- });
117
- const paidType: string = isPrePaid
103
+ // 获取支付类型和周期信息
104
+ const { isPrePaid, interval } = await this.getPaymentCategory(subscription.id);
105
+ const paidType = isPrePaid
118
106
  ? translate('notification.common.prepaid', locale)
119
107
  : translate('notification.common.postpaid', locale);
120
- const paymentInfo: string = formatCurrencyInfo(paymentDetail?.price || '0', paymentCurrency, paymentMethod, true);
121
- const currentPeriodStart: string = isPrePaid
122
- ? formatTime(invoice.period_end * 1000)
123
- : formatTime(invoice.period_start * 1000);
124
- const currentPeriodEnd: string = isPrePaid
125
- ? formatTime(dayjs(invoice.period_end * 1000).add(1, interval as ManipulateType))
126
- : formatTime(invoice.period_end * 1000);
127
- const duration: string = prettyMsI18n(
128
- new Date(currentPeriodEnd).getTime() - new Date(currentPeriodStart).getTime(),
129
- {
130
- locale: getPrettyMsI18nLocale(locale),
131
- }
108
+
109
+ const paymentInfo = formatCurrencyInfo(
110
+ paymentDetail?.price || '0',
111
+ paymentCurrency,
112
+ paymentInfoResult.paymentMethod,
113
+ true
132
114
  );
133
115
 
134
- const viewSubscriptionLink = getCustomerSubscriptionPageUrl({
135
- subscriptionId: subscription.id,
136
- locale,
137
- userDid,
138
- });
139
- const addFundsLink: string = getCustomerRechargeLink({
116
+ // 计算周期时间 - 使用安全的回退机制
117
+ const periodStart = invoice?.period_start || subscription.current_period_start;
118
+ const periodInfo = this.formatSubscriptionPeriod(
119
+ isPrePaid ? periodEnd : periodStart,
120
+ isPrePaid
121
+ ? dayjs(periodEnd * 1000)
122
+ .add(1, interval as ManipulateType)
123
+ .unix()
124
+ : periodEnd,
125
+ locale
126
+ );
127
+
128
+ const addFundsLink = getCustomerRechargeLink({
140
129
  locale,
141
130
  userDid,
142
131
  subscriptionId: subscription.id,
143
132
  });
144
133
 
145
- const customActions = getSubscriptionNotificationCustomActions(
146
- subscription,
147
- 'customer.subscription.will_renew',
148
- locale
149
- );
150
134
  return {
151
135
  locale,
152
136
  productName,
@@ -154,62 +138,49 @@ export class SubscriptionWillRenewEmailTemplate
154
138
  willRenewDuration,
155
139
  paymentDetail,
156
140
  paidType,
157
-
158
141
  userDid,
159
142
  paymentInfo,
160
- currentPeriodStart,
161
- currentPeriodEnd,
162
- duration,
163
-
164
- viewSubscriptionLink,
143
+ currentPeriodStart: periodInfo.currentPeriodStart,
144
+ currentPeriodEnd: periodInfo.currentPeriodEnd,
145
+ duration: periodInfo.duration,
146
+ viewSubscriptionLink: links.viewSubscriptionLink,
165
147
  addFundsLink,
166
- paymentMethod,
167
- customActions,
148
+ customActions: links.customActions,
149
+ isCreditSubscription,
150
+ isStripe,
168
151
  };
169
152
  }
170
- async getPaymentCategory({ subscriptionId }: { subscriptionId: string }): Promise<{
153
+
154
+ private async getPaymentCategory(subscriptionId: string): Promise<{
171
155
  isPrePaid: boolean;
172
156
  interval: LiteralUnion<'hour' | 'day' | 'week' | 'month' | 'year', string> | undefined;
173
157
  }> {
174
158
  const subscriptionItems = await SubscriptionItem.findAll({
175
- where: {
176
- subscription_id: subscriptionId,
177
- },
159
+ where: { subscription_id: subscriptionId },
178
160
  });
179
161
 
180
162
  const lineItemExpanded = await Price.expand(subscriptionItems);
181
163
  const metered = lineItemExpanded.find((lineItem) => lineItem.price.recurring?.usage_type === 'metered');
182
164
  const recurringItems = lineItemExpanded.filter((lineItem) => lineItem.price.type === 'recurring');
183
165
 
184
- const isPrePaid = !metered;
185
- const interval = recurringItems?.[0]?.price.recurring?.interval;
186
-
187
166
  return {
188
- isPrePaid,
189
- interval,
167
+ isPrePaid: !metered,
168
+ interval: recurringItems?.[0]?.price.recurring?.interval,
190
169
  };
191
170
  }
192
171
 
193
- /**
194
- * @see https://github.com/blocklet/payment-kit/issues/307
195
- * @description 该订阅已经在指定期限内续费过了吗?
196
- * @param {Subscription} subscription
197
- * @return {*} {Promise<void>}
198
- * @memberof SubscriptionWillRenewEmailTemplate
199
- */
200
- // eslint-disable-next-line require-await
201
- async subscriptionIsRenewed(subscription: Subscription): Promise<boolean> {
202
- // @note: 为了最精确的验证,这里的期限 this.options.willRenewValue 还要 + 1,如果没有订阅没有续费,那么 expectedCurrentPeriodEnd 一定是大于 currentPeriodEnd
203
- const expectedCurrentPeriodEnd: number = dayjs()
172
+ private subscriptionIsRenewed(subscription: any): boolean {
173
+ const expectedCurrentPeriodEnd = dayjs()
204
174
  .add(this.options.willRenewValue + 1, this.options.willRenewUnit)
205
175
  .toDate()
206
176
  .getTime();
207
- const currentPeriodEnd: number = subscription.current_period_end * 1000;
177
+ const currentPeriodEnd = subscription.current_period_end * 1000;
208
178
 
209
179
  return currentPeriodEnd > expectedCurrentPeriodEnd;
210
180
  }
211
181
 
212
182
  async getTemplate(): Promise<BaseEmailTemplateType | null> {
183
+ const context = await this.getContext();
213
184
  const {
214
185
  locale,
215
186
  productName,
@@ -217,195 +188,199 @@ export class SubscriptionWillRenewEmailTemplate
217
188
  willRenewDuration,
218
189
  paymentDetail,
219
190
  paidType,
220
-
221
191
  userDid,
222
192
  paymentInfo,
223
193
  currentPeriodStart,
224
194
  currentPeriodEnd,
225
195
  duration,
226
-
227
196
  viewSubscriptionLink,
228
197
  addFundsLink,
229
- paymentMethod,
230
198
  customActions,
231
- } = await this.getContext();
199
+ isCreditSubscription,
200
+ isStripe,
201
+ } = context;
232
202
 
233
203
  // 如果当前时间大于预计扣费时间,那么不发送通知
234
204
  if (dayjs().utc().isAfter(dayjs.utc(at))) {
235
205
  return null;
236
206
  }
237
- const canPay: boolean = paymentDetail.balance >= paymentDetail.price;
238
- if (canPay) {
239
- // 当余额足够支付并且本封邮件不是必须发送时,可以不发送邮件
207
+
208
+ const canPay = paymentDetail.balance >= paymentDetail.price;
209
+ if (canPay && !this.options.required) {
240
210
  return null;
241
211
  }
212
+
242
213
  if (!paymentDetail.price && paymentDetail.symbol !== 'USD') {
243
- // 如果预估的价格是 0 并且货币不是 USD,那么直接不发送
244
214
  return null;
245
215
  }
246
- const isStripe = paymentMethod?.type === 'stripe';
247
- const template: BaseEmailTemplateType = {
248
- title: `${translate('notification.subscriptionWillRenew.title', locale, {
249
- productName,
250
- willRenewDuration,
251
- })}`,
252
- body:
253
- canPay || isStripe
254
- ? `${translate('notification.subscriptionWillRenew.body', locale, {
255
- at,
256
- productName,
257
- willRenewDuration,
258
- balance: `${paymentDetail.balanceFormatted} ${paymentDetail.symbol}`,
259
- })}`
260
- : `${translate('notification.subscriptionWillRenew.unableToPayBody', locale, {
261
- at,
262
- productName,
263
- willRenewDuration,
264
- })}`,
265
- // @ts-expect-error
266
- attachments: [
267
- {
268
- type: 'section',
269
- fields: [
270
- {
271
- type: 'text',
272
- data: {
273
- type: 'plain',
274
- color: '#9397A1',
275
- text: translate('notification.common.account', locale),
276
- },
216
+
217
+ // 构建字段
218
+ const commonFields = this.buildCommonFields(userDid, productName, locale);
219
+
220
+ // 续费金额字段
221
+ const renewAmountFields = isCreditSubscription
222
+ ? []
223
+ : [
224
+ {
225
+ type: 'text',
226
+ data: {
227
+ type: 'plain',
228
+ color: '#9397A1',
229
+ text: translate('notification.subscriptionWillRenew.renewAmount', locale),
277
230
  },
278
- {
279
- type: 'text',
280
- data: {
281
- type: 'plain',
282
- text: userDid,
283
- },
231
+ },
232
+ {
233
+ type: 'text',
234
+ data: {
235
+ type: 'plain',
236
+ text: paymentInfo,
284
237
  },
238
+ },
239
+ ];
240
+
241
+ // 当前余额字段(非 Stripe 时显示)
242
+ const balanceFields =
243
+ !isStripe && !isCreditSubscription
244
+ ? [
285
245
  {
286
246
  type: 'text',
287
247
  data: {
288
248
  type: 'plain',
289
249
  color: '#9397A1',
290
- text: translate('notification.common.product', locale),
250
+ text: translate('notification.common.currentBalance', locale),
291
251
  },
292
252
  },
293
253
  {
294
254
  type: 'text',
295
255
  data: {
296
256
  type: 'plain',
297
- text: productName,
257
+ ...(!canPay && { color: '#FF0000' }),
258
+ text: `${paymentDetail.balanceFormatted} ${paymentDetail.symbol}`,
298
259
  },
299
260
  },
261
+ ]
262
+ : [];
263
+
264
+ // 余额不足提醒字段
265
+ const insufficientBalanceFields =
266
+ !canPay && !isStripe && !isCreditSubscription
267
+ ? [
300
268
  {
301
269
  type: 'text',
302
270
  data: {
303
271
  type: 'plain',
304
272
  color: '#9397A1',
305
- text: translate('notification.subscriptionWillRenew.renewAmount', locale),
273
+ text: translate('notification.common.balanceReminder', locale),
306
274
  },
307
275
  },
308
276
  {
309
277
  type: 'text',
310
278
  data: {
311
279
  type: 'plain',
312
- text: paymentInfo,
280
+ color: '#FF0000',
281
+ text: translate('notification.subscriptionWillRenew.unableToPayReason', locale, {
282
+ balance: `${paymentDetail.balance} ${paymentDetail.symbol}`,
283
+ price: `${paymentDetail.price} ${paymentDetail.symbol}`,
284
+ }),
313
285
  },
314
286
  },
315
- ...(!isStripe
316
- ? [
317
- {
318
- type: 'text',
319
- data: {
320
- type: 'plain',
321
- color: '#9397A1',
322
- text: translate('notification.common.currentBalance', locale),
323
- },
324
- },
325
- {
326
- type: 'text',
327
- data: {
328
- type: 'plain',
329
- ...(!canPay && {
330
- color: '#FF0000',
331
- }),
332
- text: `${paymentDetail.balanceFormatted} ${paymentDetail.symbol}`,
333
- },
334
- },
335
- ]
336
- : []),
337
- ...(!canPay && !isStripe
338
- ? [
339
- {
340
- type: 'text',
341
- data: {
342
- type: 'plain',
343
- color: '#9397A1',
344
- text: translate('notification.common.balanceReminder', locale),
345
- },
346
- },
347
- {
348
- type: 'text',
349
- data: {
350
- type: 'plain',
351
- color: '#FF0000',
352
- text: translate('notification.subscriptionWillRenew.unableToPayReason', locale, {
353
- balance: `${paymentDetail.balance} ${paymentDetail.symbol}`,
354
- price: `${paymentDetail.price} ${paymentDetail.symbol}`,
355
- }),
356
- },
357
- },
358
- ]
359
- : []),
360
- {
361
- type: 'text',
362
- data: {
363
- type: 'plain',
364
- color: '#9397A1',
365
- text: translate('notification.common.paidType', locale),
366
- },
287
+ ]
288
+ : [];
289
+
290
+ // 支付类型字段
291
+ const paidTypeFields = isCreditSubscription
292
+ ? []
293
+ : [
294
+ {
295
+ type: 'text',
296
+ data: {
297
+ type: 'plain',
298
+ color: '#9397A1',
299
+ text: translate('notification.common.paidType', locale),
367
300
  },
368
- {
369
- type: 'text',
370
- data: {
371
- type: 'plain',
372
- text: `${paidType}`,
373
- },
301
+ },
302
+ {
303
+ type: 'text',
304
+ data: {
305
+ type: 'plain',
306
+ text: paidType,
374
307
  },
375
- {
376
- type: 'text',
377
- data: {
378
- type: 'plain',
379
- color: '#9397A1',
380
- text: translate('notification.common.paymentPeriod', locale),
381
- },
308
+ },
309
+ ];
310
+
311
+ // 支付周期字段
312
+ const periodFields = isCreditSubscription
313
+ ? []
314
+ : [
315
+ {
316
+ type: 'text',
317
+ data: {
318
+ type: 'plain',
319
+ color: '#9397A1',
320
+ text: translate('notification.common.paymentPeriod', locale),
382
321
  },
383
- {
384
- type: 'text',
385
- data: {
386
- type: 'plain',
387
- text: `${currentPeriodStart} ~ ${currentPeriodEnd}(${duration})`,
388
- },
322
+ },
323
+ {
324
+ type: 'text',
325
+ data: {
326
+ type: 'plain',
327
+ text: `${currentPeriodStart} ~ ${currentPeriodEnd}(${duration})`,
389
328
  },
390
- ].filter(Boolean),
391
- },
392
- ].filter(Boolean),
393
- // @ts-ignore
394
- actions: [
395
- !canPay &&
396
- !isStripe &&
397
- addFundsLink && {
398
- name: translate('notification.common.addFunds', locale),
399
- title: translate('notification.common.addFunds', locale),
400
- link: addFundsLink,
401
329
  },
330
+ ];
331
+
332
+ // 构建操作按钮
333
+ const actions = [
334
+ !canPay &&
335
+ !isStripe &&
336
+ !isCreditSubscription &&
337
+ addFundsLink && {
338
+ name: translate('notification.common.addFunds', locale),
339
+ title: translate('notification.common.addFunds', locale),
340
+ link: addFundsLink,
341
+ },
342
+ {
343
+ name: translate('notification.common.viewSubscription', locale),
344
+ title: translate('notification.common.viewSubscription', locale),
345
+ link: viewSubscriptionLink,
346
+ },
347
+ ...customActions,
348
+ ].filter(Boolean);
349
+
350
+ const template: BaseEmailTemplateType = {
351
+ title: translate('notification.subscriptionWillRenew.title', locale, {
352
+ productName,
353
+ willRenewDuration,
354
+ }),
355
+ body:
356
+ canPay || isStripe
357
+ ? translate('notification.subscriptionWillRenew.body', locale, {
358
+ at,
359
+ productName,
360
+ willRenewDuration,
361
+ balance: `${paymentDetail.balanceFormatted} ${paymentDetail.symbol}`,
362
+ })
363
+ : translate('notification.subscriptionWillRenew.unableToPayBody', locale, {
364
+ at,
365
+ productName,
366
+ willRenewDuration,
367
+ }),
368
+ // @ts-expect-error
369
+ attachments: [
402
370
  {
403
- name: translate('notification.common.viewSubscription', locale),
404
- title: translate('notification.common.viewSubscription', locale),
405
- link: viewSubscriptionLink,
371
+ type: 'section',
372
+ fields: [
373
+ ...commonFields,
374
+ ...renewAmountFields,
375
+ ...balanceFields,
376
+ ...insufficientBalanceFields,
377
+ ...paidTypeFields,
378
+ ...periodFields,
379
+ ].filter(Boolean),
406
380
  },
407
- ...customActions,
408
381
  ].filter(Boolean),
382
+ // @ts-ignore
383
+ actions,
409
384
  };
410
385
 
411
386
  return template;