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,17 +1,12 @@
1
1
  /* eslint-disable @typescript-eslint/brace-style */
2
2
  /* eslint-disable @typescript-eslint/indent */
3
- import prettyMsI18n from 'pretty-ms-i18n';
4
-
5
- import { getUserLocale } from '../../../integrations/blocklet/notification';
6
3
  import { translate } from '../../../locales';
7
- import { Customer, PaymentIntent, PaymentMethod, Refund, Subscription } from '../../../store/models';
4
+ import { PaymentIntent, PaymentMethod, Refund, Subscription } from '../../../store/models';
8
5
  import { Invoice } from '../../../store/models/invoice';
9
6
  import { PaymentCurrency } from '../../../store/models/payment-currency';
10
- import { getMainProductName } from '../../product';
11
- import { getCustomerSubscriptionPageUrl } from '../../subscription';
12
- import { formatTime, getPrettyMsI18nLocale } from '../../time';
13
- import { formatCurrencyInfo, getExplorerLink, getSubscriptionNotificationCustomActions } from '../../util';
14
- import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
7
+ import { formatTime } from '../../time';
8
+ import { formatCurrencyInfo, getExplorerLink } from '../../util';
9
+ import { BaseSubscriptionEmailTemplate, BaseEmailTemplateType } from './base';
15
10
 
16
11
  export interface SubscriptionRefundSucceededEmailTemplateOptions {
17
12
  refundId: string;
@@ -21,7 +16,6 @@ interface SubscriptionRefundSucceededEmailTemplateContext {
21
16
  locale: string;
22
17
  productName: string;
23
18
  at: string;
24
-
25
19
  chainHost: string | undefined;
26
20
  userDid: string;
27
21
  paymentInfo: string;
@@ -32,24 +26,23 @@ interface SubscriptionRefundSucceededEmailTemplateContext {
32
26
  unusedPeriodStart?: string;
33
27
  unusedPeriodEnd?: string;
34
28
  unusedDuration: string;
35
-
36
29
  viewSubscriptionLink: string;
37
30
  viewTxHashLink: string | undefined;
38
31
  refund: Refund;
39
32
  customActions: any[];
33
+ isCreditSubscription: boolean;
40
34
  }
41
35
 
42
- export class SubscriptionRefundSucceededEmailTemplate
43
- implements BaseEmailTemplate<SubscriptionRefundSucceededEmailTemplateContext>
44
- {
36
+ export class SubscriptionRefundSucceededEmailTemplate extends BaseSubscriptionEmailTemplate<SubscriptionRefundSucceededEmailTemplateContext> {
45
37
  options: SubscriptionRefundSucceededEmailTemplateOptions;
46
38
 
47
39
  constructor(options: SubscriptionRefundSucceededEmailTemplateOptions) {
40
+ super();
48
41
  this.options = options;
49
42
  }
50
43
 
51
44
  async getContext(): Promise<SubscriptionRefundSucceededEmailTemplateContext> {
52
- const refund: Refund | null = await Refund.findByPk(this.options.refundId);
45
+ const refund = await Refund.findByPk(this.options.refundId);
53
46
  if (!refund) {
54
47
  throw new Error(`Refund not found: ${this.options.refundId}`);
55
48
  }
@@ -57,69 +50,80 @@ export class SubscriptionRefundSucceededEmailTemplate
57
50
  throw new Error(`Refund not succeeded: ${this.options.refundId}`);
58
51
  }
59
52
 
60
- const customer = await Customer.findByPk(refund.customer_id);
61
- if (!customer) {
62
- throw new Error(`Customer not found: ${refund.customer_id}`);
63
- }
53
+ // 获取基础订阅数据
54
+ const basicData = await this.getSubscriptionBasicData(refund.subscription_id!);
55
+ const { userDid, locale, productName, isCreditSubscription } = basicData;
64
56
 
57
+ // 获取支付相关信息
65
58
  const paymentIntent = await PaymentIntent.findByPk(refund.payment_intent_id);
66
- const paymentCurrency = (await PaymentCurrency.findOne({
67
- where: {
68
- id: refund.currency_id,
69
- },
70
- })) as PaymentCurrency;
71
-
72
- const userDid: string = customer.did;
73
- const locale = await getUserLocale(userDid);
74
- const productName = await getMainProductName(refund.subscription_id!);
75
- const at: string = formatTime(refund.created_at);
59
+ const paymentCurrency = await PaymentCurrency.findByPk(refund.currency_id);
60
+ if (!paymentCurrency) {
61
+ throw new Error(`PaymentCurrency not found: ${refund.currency_id}`);
62
+ }
76
63
 
64
+ // 获取发票和周期信息
77
65
  let invoice = null;
78
66
  let currentPeriodStart;
79
67
  let currentPeriodEnd;
80
- let duration;
68
+ let duration = '';
81
69
  let unusedPeriodStart;
82
70
  let unusedPeriodEnd;
83
- let unusedDuration;
71
+ let unusedDuration = '';
72
+
84
73
  if (refund.type === 'refund') {
85
- invoice = (await Invoice.findByPk(refund.invoice_id)) as Invoice;
86
- currentPeriodStart = formatTime(invoice.period_start * 1000);
87
- currentPeriodEnd = formatTime(invoice.period_end * 1000);
88
- duration = prettyMsI18n(new Date(currentPeriodEnd).getTime() - new Date(currentPeriodStart).getTime(), {
89
- locale: getPrettyMsI18nLocale(locale),
90
- });
74
+ invoice = await Invoice.findByPk(refund.invoice_id);
75
+ if (invoice) {
76
+ const periodInfo = this.formatSubscriptionPeriod(invoice.period_start, invoice.period_end, locale);
77
+ currentPeriodStart = periodInfo.currentPeriodStart;
78
+ currentPeriodEnd = periodInfo.currentPeriodEnd;
79
+ duration = periodInfo.duration;
91
80
 
92
- if (refund?.metadata?.unused_period_start && refund?.metadata?.unused_period_end) {
93
- unusedPeriodStart = formatTime(refund!.metadata!.unused_period_start! * 1000);
94
- unusedPeriodEnd = formatTime(refund!.metadata!.unused_period_end! * 1000);
95
- unusedDuration = prettyMsI18n(new Date(unusedPeriodEnd).getTime() - new Date(unusedPeriodStart).getTime(), {
96
- locale: getPrettyMsI18nLocale(locale),
97
- });
81
+ // 处理未使用期间
82
+ if (refund?.metadata?.unused_period_start && refund?.metadata?.unused_period_end) {
83
+ const unusedPeriodInfo = this.formatSubscriptionPeriod(
84
+ refund.metadata.unused_period_start,
85
+ refund.metadata.unused_period_end,
86
+ locale
87
+ );
88
+ unusedPeriodStart = unusedPeriodInfo.currentPeriodStart;
89
+ unusedPeriodEnd = unusedPeriodInfo.currentPeriodEnd;
90
+ unusedDuration = unusedPeriodInfo.duration;
91
+ }
98
92
  }
99
93
  }
100
94
 
101
- const paymentMethod: PaymentMethod | null = await PaymentMethod.findByPk(
102
- refund.payment_method_id || invoice?.default_payment_method_id
103
- );
95
+ // 获取支付方式
96
+ const paymentMethod = await PaymentMethod.findByPk(refund.payment_method_id || invoice?.default_payment_method_id);
104
97
 
105
- const paymentInfo: string = formatCurrencyInfo(
106
- paymentIntent?.amount_received || '0',
107
- paymentCurrency,
108
- paymentMethod
109
- );
110
- const refundInfo: string = formatCurrencyInfo(refund.amount, paymentCurrency, paymentMethod);
98
+ // 格式化支付和退款信息
99
+ const paymentInfo = formatCurrencyInfo(paymentIntent?.amount_received || '0', paymentCurrency, paymentMethod);
100
+ const refundInfo = formatCurrencyInfo(refund.amount, paymentCurrency, paymentMethod);
111
101
 
112
- // @ts-expect-error
113
- const chainHost: string | undefined = paymentMethod?.settings?.[paymentMethod.type]?.api_host;
114
- const viewSubscriptionLink = getCustomerSubscriptionPageUrl({
115
- subscriptionId: refund.subscription_id!,
116
- locale,
117
- userDid,
118
- });
102
+ // 获取链接
103
+ let customActions: any[] = [];
104
+ let viewSubscriptionLink = '';
105
+ if (refund?.subscription_id) {
106
+ const subscription = await Subscription.findByPk(refund.subscription_id);
107
+ if (subscription) {
108
+ const links = this.generateSubscriptionLinks(
109
+ subscription,
110
+ locale,
111
+ userDid,
112
+ 'refund.succeeded',
113
+ undefined,
114
+ isCreditSubscription
115
+ );
116
+ viewSubscriptionLink = links.viewSubscriptionLink;
117
+ customActions = links.customActions;
118
+ }
119
+ }
119
120
 
121
+ // 获取交易哈希链接
122
+ // @ts-expect-error
123
+ const chainHost = paymentMethod?.settings?.[paymentMethod?.type]?.api_host;
120
124
  // @ts-expect-error
121
- const txHash: string | undefined = refund?.payment_details?.[paymentMethod.type]?.tx_hash;
122
- const viewTxHashLink: string | undefined =
125
+ const txHash = refund?.payment_details?.[paymentMethod?.type]?.tx_hash;
126
+ const viewTxHashLink =
123
127
  txHash &&
124
128
  getExplorerLink({
125
129
  type: 'tx',
@@ -127,17 +131,12 @@ export class SubscriptionRefundSucceededEmailTemplate
127
131
  chainHost,
128
132
  });
129
133
 
130
- let customActions: any[] = [];
131
- if (refund?.subscription_id) {
132
- const subscription = await Subscription.findByPk(refund.subscription_id);
133
- customActions = getSubscriptionNotificationCustomActions(subscription!, 'refund.succeeded', locale);
134
- }
134
+ const at = formatTime(refund.created_at);
135
135
 
136
136
  return {
137
137
  locale,
138
138
  productName,
139
139
  at,
140
-
141
140
  userDid,
142
141
  chainHost,
143
142
  paymentInfo,
@@ -148,20 +147,20 @@ export class SubscriptionRefundSucceededEmailTemplate
148
147
  unusedPeriodStart,
149
148
  unusedPeriodEnd,
150
149
  unusedDuration,
151
-
152
150
  viewSubscriptionLink,
153
151
  viewTxHashLink,
154
152
  refund,
155
153
  customActions,
154
+ isCreditSubscription,
156
155
  };
157
156
  }
158
157
 
159
158
  async getTemplate(): Promise<BaseEmailTemplateType> {
159
+ const context = await this.getContext();
160
160
  const {
161
161
  locale,
162
162
  productName,
163
163
  at,
164
-
165
164
  userDid,
166
165
  paymentInfo,
167
166
  refundInfo,
@@ -171,217 +170,196 @@ export class SubscriptionRefundSucceededEmailTemplate
171
170
  unusedPeriodStart,
172
171
  unusedPeriodEnd,
173
172
  unusedDuration,
174
-
175
173
  viewSubscriptionLink,
176
174
  viewTxHashLink,
177
175
  refund,
178
- } = await this.getContext();
176
+ isCreditSubscription,
177
+ } = context;
179
178
 
179
+ // 如果是质押返还类型,使用特殊模板
180
180
  if (refund.type === 'stake_return') {
181
- return {
182
- title: `${translate('notification.subscriptionStakeReturnSucceeded.title', locale, {
183
- productName,
184
- })}`,
185
- body: `${translate('notification.subscriptionStakeReturnSucceeded.body', locale, {
186
- at,
187
- productName,
188
- refundInfo,
189
- })}`,
190
- attachments: [
191
- {
192
- type: 'section',
193
- fields: [
194
- {
195
- type: 'text',
196
- data: {
197
- type: 'plain',
198
- color: '#9397A1',
199
- text: translate('notification.common.account', locale),
200
- },
201
- },
202
- {
203
- type: 'text',
204
- data: {
205
- type: 'plain',
206
- text: userDid,
207
- },
208
- },
209
- {
210
- type: 'text',
211
- data: {
212
- type: 'plain',
213
- color: '#9397A1',
214
- text: translate('notification.common.product', locale),
215
- },
216
- },
217
- {
218
- type: 'text',
219
- data: {
220
- type: 'plain',
221
- text: productName,
222
- },
223
- },
224
- {
225
- type: 'text',
226
- data: {
227
- type: 'plain',
228
- color: '#9397A1',
229
- text: translate('notification.common.returnAmount', locale),
230
- },
231
- },
232
- {
233
- type: 'text',
234
- data: {
235
- type: 'plain',
236
- text: refundInfo,
237
- },
238
- },
239
- ],
240
- },
241
- ],
242
- // @ts-ignore
243
- actions: [
244
- {
245
- name: translate('notification.common.viewSubscription', locale),
246
- title: translate('notification.common.viewSubscription', locale),
247
- link: viewSubscriptionLink,
248
- },
249
- viewTxHashLink && {
250
- name: translate('notification.common.viewTxHash', locale),
251
- title: translate('notification.common.viewTxHash', locale),
252
- link: viewTxHashLink as string,
253
- },
254
- ].filter(Boolean),
255
- };
181
+ return this.getStakeReturnTemplate(context);
256
182
  }
257
183
 
258
- const template: BaseEmailTemplateType = {
259
- title: `${translate('notification.subscriptionRefundSucceeded.title', locale, {
260
- productName,
261
- })}`,
262
- body: `${translate('notification.subscriptionRefundSucceeded.body', locale, {
263
- at,
264
- productName,
265
- refundInfo,
266
- })}`,
267
- // @ts-expect-error
268
- attachments: [
269
- {
270
- type: 'section',
271
- fields: [
272
- {
273
- type: 'text',
274
- data: {
275
- type: 'plain',
276
- color: '#9397A1',
277
- text: translate('notification.common.account', locale),
278
- },
279
- },
280
- {
281
- type: 'text',
282
- data: {
283
- type: 'plain',
284
- text: userDid,
285
- },
286
- },
287
- {
288
- type: 'text',
289
- data: {
290
- type: 'plain',
291
- color: '#9397A1',
292
- text: translate('notification.common.product', locale),
293
- },
184
+ // 构建字段
185
+ const commonFields = this.buildCommonFields(userDid, productName, locale);
186
+
187
+ // 支付和退款金额字段
188
+ const amountFields = isCreditSubscription
189
+ ? []
190
+ : [
191
+ {
192
+ type: 'text',
193
+ data: {
194
+ type: 'plain',
195
+ color: '#9397A1',
196
+ text: translate('notification.common.paymentAmount', locale),
294
197
  },
295
- {
296
- type: 'text',
297
- data: {
298
- type: 'plain',
299
- text: productName,
300
- },
198
+ },
199
+ {
200
+ type: 'text',
201
+ data: {
202
+ type: 'plain',
203
+ text: paymentInfo,
301
204
  },
302
- {
303
- type: 'text',
304
- data: {
305
- type: 'plain',
306
- color: '#9397A1',
307
- text: translate('notification.common.paymentAmount', locale),
308
- },
205
+ },
206
+ {
207
+ type: 'text',
208
+ data: {
209
+ type: 'plain',
210
+ color: '#9397A1',
211
+ text: translate('notification.common.refundAmount', locale),
309
212
  },
310
- {
311
- type: 'text',
312
- data: {
313
- type: 'plain',
314
- text: paymentInfo,
315
- },
213
+ },
214
+ {
215
+ type: 'text',
216
+ data: {
217
+ type: 'plain',
218
+ text: refundInfo,
316
219
  },
220
+ },
221
+ ];
222
+
223
+ // 有效期字段
224
+ const validityPeriodFields =
225
+ currentPeriodStart && currentPeriodEnd && !isCreditSubscription
226
+ ? [
317
227
  {
318
228
  type: 'text',
319
229
  data: {
320
230
  type: 'plain',
321
231
  color: '#9397A1',
322
- text: translate('notification.common.refundAmount', locale),
232
+ text: translate('notification.common.validityPeriod', locale),
323
233
  },
324
234
  },
325
235
  {
326
236
  type: 'text',
327
237
  data: {
328
238
  type: 'plain',
329
- text: refundInfo,
239
+ text: `${currentPeriodStart} ~ ${currentPeriodEnd}(${duration})`,
330
240
  },
331
241
  },
242
+ ]
243
+ : [];
244
+
245
+ // 退款期间字段
246
+ const refundPeriodFields =
247
+ unusedPeriodStart && unusedPeriodEnd && unusedDuration && !isCreditSubscription
248
+ ? [
332
249
  {
333
250
  type: 'text',
334
251
  data: {
335
252
  type: 'plain',
336
253
  color: '#9397A1',
337
- text: translate('notification.common.validityPeriod', locale),
254
+ text: translate('notification.common.refundPeriod', locale),
338
255
  },
339
256
  },
340
257
  {
341
258
  type: 'text',
342
259
  data: {
343
260
  type: 'plain',
344
- text: `${currentPeriodStart} ~ ${currentPeriodEnd}(${duration})`,
261
+ text: `${unusedPeriodStart} ~ ${unusedPeriodEnd}(${unusedDuration})`,
345
262
  },
346
263
  },
264
+ ]
265
+ : [];
347
266
 
348
- ...(unusedPeriodStart && unusedPeriodEnd && unusedDuration
349
- ? [
350
- {
351
- type: 'text',
352
- data: {
353
- type: 'plain',
354
- color: '#9397A1',
355
- text: translate('notification.common.refundPeriod', locale),
356
- },
357
- },
358
- {
359
- type: 'text',
360
- data: {
361
- type: 'plain',
362
- text: `${unusedPeriodStart} ~ ${unusedPeriodEnd}(${unusedDuration})`,
363
- },
364
- },
365
- ]
366
- : []),
367
- ].filter(Boolean),
368
- },
369
- ].filter(Boolean),
370
- // @ts-ignore
371
- actions: [
267
+ // 构建操作按钮
268
+ const actions = [
269
+ {
270
+ name: translate('notification.common.viewSubscription', locale),
271
+ title: translate('notification.common.viewSubscription', locale),
272
+ link: viewSubscriptionLink,
273
+ },
274
+ viewTxHashLink && {
275
+ name: translate('notification.common.viewTxHash', locale),
276
+ title: translate('notification.common.viewTxHash', locale),
277
+ link: viewTxHashLink,
278
+ },
279
+ ].filter(Boolean);
280
+
281
+ const template: BaseEmailTemplateType = {
282
+ title: translate('notification.subscriptionRefundSucceeded.title', locale, {
283
+ productName,
284
+ }),
285
+ body: translate('notification.subscriptionRefundSucceeded.body', locale, {
286
+ at,
287
+ productName,
288
+ refundInfo,
289
+ }),
290
+ // @ts-expect-error
291
+ attachments: [
372
292
  {
373
- name: translate('notification.common.viewSubscription', locale),
374
- title: translate('notification.common.viewSubscription', locale),
375
- link: viewSubscriptionLink,
376
- },
377
- viewTxHashLink && {
378
- name: translate('notification.common.viewTxHash', locale),
379
- title: translate('notification.common.viewTxHash', locale),
380
- link: viewTxHashLink as string,
293
+ type: 'section',
294
+ fields: [...commonFields, ...amountFields, ...validityPeriodFields, ...refundPeriodFields].filter(Boolean),
381
295
  },
382
296
  ].filter(Boolean),
297
+ // @ts-ignore
298
+ actions,
383
299
  };
384
300
 
385
301
  return template;
386
302
  }
303
+
304
+ private getStakeReturnTemplate(context: SubscriptionRefundSucceededEmailTemplateContext): BaseEmailTemplateType {
305
+ const { locale, productName, at, userDid, refundInfo, viewSubscriptionLink, viewTxHashLink, isCreditSubscription } =
306
+ context;
307
+
308
+ // 构建字段
309
+ const commonFields = this.buildCommonFields(userDid, productName, locale);
310
+
311
+ // 返还金额字段
312
+ const returnAmountFields = isCreditSubscription
313
+ ? []
314
+ : [
315
+ {
316
+ type: 'text',
317
+ data: {
318
+ type: 'plain',
319
+ color: '#9397A1',
320
+ text: translate('notification.common.returnAmount', locale),
321
+ },
322
+ },
323
+ {
324
+ type: 'text',
325
+ data: {
326
+ type: 'plain',
327
+ text: refundInfo,
328
+ },
329
+ },
330
+ ];
331
+
332
+ // 构建操作按钮
333
+ const actions = [
334
+ {
335
+ name: translate('notification.common.viewSubscription', locale),
336
+ title: translate('notification.common.viewSubscription', locale),
337
+ link: viewSubscriptionLink,
338
+ },
339
+ viewTxHashLink && {
340
+ name: translate('notification.common.viewTxHash', locale),
341
+ title: translate('notification.common.viewTxHash', locale),
342
+ link: viewTxHashLink,
343
+ },
344
+ ].filter(Boolean);
345
+
346
+ return {
347
+ title: translate('notification.subscriptionStakeReturnSucceeded.title', locale, {
348
+ productName,
349
+ }),
350
+ body: translate('notification.subscriptionStakeReturnSucceeded.body', locale, {
351
+ at,
352
+ productName,
353
+ refundInfo,
354
+ }),
355
+ attachments: [
356
+ {
357
+ type: 'section',
358
+ fields: [...commonFields, ...returnAmountFields].filter(Boolean),
359
+ },
360
+ ],
361
+ // @ts-ignore
362
+ actions,
363
+ };
364
+ }
387
365
  }