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
@@ -0,0 +1,149 @@
1
+ /* eslint-disable @typescript-eslint/brace-style */
2
+ /* eslint-disable @typescript-eslint/indent */
3
+ import { fromUnitToToken } from '@ocap/util';
4
+ import { getUserLocale } from '../../../integrations/blocklet/notification';
5
+ import { translate } from '../../../locales';
6
+ import { CreditGrant, Customer, PaymentCurrency } from '../../../store/models';
7
+ import { formatTime } from '../../time';
8
+ import { getCustomerIndexUrl } from '../../util';
9
+ import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
10
+
11
+ export interface CustomerCreditGrantGrantedEmailTemplateOptions {
12
+ creditGrantId: string;
13
+ }
14
+
15
+ interface CustomerCreditGrantGrantedEmailTemplateContext {
16
+ locale: string;
17
+ userDid: string;
18
+ currencySymbol: string;
19
+ grantedAmount: string;
20
+ expiresAt?: string;
21
+ neverExpires: boolean;
22
+ at: string;
23
+ }
24
+
25
+ export class CustomerCreditGrantGrantedEmailTemplate
26
+ implements BaseEmailTemplate<CustomerCreditGrantGrantedEmailTemplateContext>
27
+ {
28
+ options: CustomerCreditGrantGrantedEmailTemplateOptions;
29
+
30
+ constructor(options: CustomerCreditGrantGrantedEmailTemplateOptions) {
31
+ this.options = options;
32
+ }
33
+
34
+ async getContext(): Promise<CustomerCreditGrantGrantedEmailTemplateContext> {
35
+ const creditGrant = await CreditGrant.findByPk(this.options.creditGrantId);
36
+ if (!creditGrant) {
37
+ throw new Error(`CreditGrant not found: ${this.options.creditGrantId}`);
38
+ }
39
+
40
+ const customer = await Customer.findByPk(creditGrant.customer_id);
41
+ if (!customer) {
42
+ throw new Error(`Customer not found: ${creditGrant.customer_id}`);
43
+ }
44
+
45
+ const paymentCurrency = await PaymentCurrency.findByPk(creditGrant.currency_id);
46
+ if (!paymentCurrency) {
47
+ throw new Error(`PaymentCurrency not found: ${creditGrant.currency_id}`);
48
+ }
49
+
50
+ const userDid = customer.did;
51
+ const locale = await getUserLocale(userDid);
52
+ const currencySymbol = paymentCurrency.symbol;
53
+ const at = formatTime(Date.now());
54
+
55
+ const neverExpires = !creditGrant.expires_at;
56
+ const expiresAt = creditGrant.expires_at ? formatTime(creditGrant.expires_at * 1000) : undefined;
57
+
58
+ return {
59
+ locale,
60
+ userDid,
61
+ currencySymbol,
62
+ grantedAmount: `${fromUnitToToken(creditGrant.amount.toString(), paymentCurrency.decimal)} ${currencySymbol}`,
63
+ expiresAt,
64
+ neverExpires,
65
+ at,
66
+ };
67
+ }
68
+
69
+ async getTemplate(): Promise<BaseEmailTemplateType> {
70
+ const context = await this.getContext();
71
+ const { locale, userDid, grantedAmount, expiresAt, neverExpires, at } = context;
72
+
73
+ // 构建字段
74
+ const fields = [
75
+ {
76
+ type: 'text',
77
+ data: {
78
+ type: 'plain',
79
+ color: '#9397A1',
80
+ text: translate('notification.common.account', locale),
81
+ },
82
+ },
83
+ {
84
+ type: 'text',
85
+ data: {
86
+ type: 'plain',
87
+ text: userDid,
88
+ },
89
+ },
90
+ {
91
+ type: 'text',
92
+ data: {
93
+ type: 'plain',
94
+ color: '#9397A1',
95
+ text: translate('notification.creditGrantGranted.grantedCredit', locale),
96
+ },
97
+ },
98
+ {
99
+ type: 'text',
100
+ data: {
101
+ type: 'plain',
102
+ text: `${grantedAmount}`,
103
+ },
104
+ },
105
+ {
106
+ type: 'text',
107
+ data: {
108
+ type: 'plain',
109
+ color: '#9397A1',
110
+ text: translate('notification.creditGrantGranted.validUntil', locale),
111
+ },
112
+ },
113
+ {
114
+ type: 'text',
115
+ data: {
116
+ type: 'plain',
117
+ text: neverExpires
118
+ ? translate('notification.creditGrantGranted.neverExpires', locale)
119
+ : (expiresAt as string),
120
+ },
121
+ },
122
+ ];
123
+
124
+ const template: BaseEmailTemplateType = {
125
+ title: translate('notification.creditGrantGranted.title', locale),
126
+ body: neverExpires
127
+ ? translate('notification.creditGrantGranted.bodyNoExpire', locale, { grantedAmount, at })
128
+ : translate('notification.creditGrantGranted.body', locale, { grantedAmount, at, expiresAt }),
129
+ attachments: [
130
+ {
131
+ type: 'section',
132
+ fields,
133
+ },
134
+ ],
135
+ actions: [
136
+ {
137
+ name: translate('notification.common.viewCreditGrant', locale),
138
+ title: translate('notification.common.viewCreditGrant', locale),
139
+ link: getCustomerIndexUrl({
140
+ locale,
141
+ userDid,
142
+ }),
143
+ },
144
+ ], // 可以根据需要添加操作按钮
145
+ };
146
+
147
+ return template;
148
+ }
149
+ }
@@ -0,0 +1,151 @@
1
+ /* eslint-disable @typescript-eslint/brace-style */
2
+ /* eslint-disable @typescript-eslint/indent */
3
+ import { BN, fromUnitToToken } from '@ocap/util';
4
+ import { getUserLocale } from '../../../integrations/blocklet/notification';
5
+ import { translate } from '../../../locales';
6
+ import { CreditGrant, Customer, PaymentCurrency } from '../../../store/models';
7
+ import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
8
+ import { getCustomerIndexUrl } from '../../util';
9
+
10
+ export interface CustomerCreditGrantLowBalanceEmailTemplateOptions {
11
+ creditGrantId: string;
12
+ }
13
+
14
+ interface CustomerCreditGrantLowBalanceEmailTemplateContext {
15
+ locale: string;
16
+ userDid: string;
17
+ currencySymbol: string;
18
+ availableAmount: string;
19
+ totalGrantedAmount: string;
20
+ lowBalancePercentage: string;
21
+ }
22
+
23
+ export class CustomerCreditGrantLowBalanceEmailTemplate
24
+ implements BaseEmailTemplate<CustomerCreditGrantLowBalanceEmailTemplateContext>
25
+ {
26
+ options: CustomerCreditGrantLowBalanceEmailTemplateOptions;
27
+
28
+ constructor(options: CustomerCreditGrantLowBalanceEmailTemplateOptions) {
29
+ this.options = options;
30
+ }
31
+
32
+ async getContext(): Promise<CustomerCreditGrantLowBalanceEmailTemplateContext> {
33
+ const creditGrant = await CreditGrant.findByPk(this.options.creditGrantId);
34
+ if (!creditGrant) {
35
+ throw new Error(`CreditGrant not found: ${this.options.creditGrantId}`);
36
+ }
37
+
38
+ const customer = await Customer.findByPk(creditGrant.customer_id);
39
+ if (!customer) {
40
+ throw new Error(`Customer not found: ${creditGrant.customer_id}`);
41
+ }
42
+
43
+ const paymentCurrency = await PaymentCurrency.findByPk(creditGrant.currency_id);
44
+ if (!paymentCurrency) {
45
+ throw new Error(`PaymentCurrency not found: ${creditGrant.currency_id}`);
46
+ }
47
+
48
+ const userDid = customer.did;
49
+ const locale = await getUserLocale(userDid);
50
+ const currencySymbol = paymentCurrency.symbol;
51
+
52
+ // 计算百分比
53
+ const available = new BN(creditGrant.remaining_amount);
54
+ const total = new BN(creditGrant.amount);
55
+ const percentage = total.gt(0) ? available.mul(new BN(100)).div(total).toString() : '0';
56
+
57
+ return {
58
+ locale,
59
+ userDid,
60
+ currencySymbol,
61
+ availableAmount: `${fromUnitToToken(available.toString(), paymentCurrency.decimal)} ${currencySymbol}`,
62
+ totalGrantedAmount: `${fromUnitToToken(total.toString(), paymentCurrency.decimal)} ${currencySymbol}`,
63
+ lowBalancePercentage: `${percentage}%`,
64
+ };
65
+ }
66
+
67
+ async getTemplate(): Promise<BaseEmailTemplateType> {
68
+ const context = await this.getContext();
69
+ const { locale, userDid, availableAmount, totalGrantedAmount, lowBalancePercentage } = context;
70
+
71
+ // 构建字段
72
+ const fields = [
73
+ {
74
+ type: 'text',
75
+ data: {
76
+ type: 'plain',
77
+ color: '#9397A1',
78
+ text: translate('notification.common.account', locale),
79
+ },
80
+ },
81
+ {
82
+ type: 'text',
83
+ data: {
84
+ type: 'plain',
85
+ text: userDid,
86
+ },
87
+ },
88
+ {
89
+ type: 'text',
90
+ data: {
91
+ type: 'plain',
92
+ color: '#9397A1',
93
+ text: translate('notification.creditInsufficient.availableCredit', locale),
94
+ },
95
+ },
96
+ {
97
+ type: 'text',
98
+ data: {
99
+ type: 'plain',
100
+ color: '#FF6600', // 橙色警告
101
+ text: `${availableAmount} (${lowBalancePercentage})`,
102
+ },
103
+ },
104
+ {
105
+ type: 'text',
106
+ data: {
107
+ type: 'plain',
108
+ color: '#9397A1',
109
+ text: translate('notification.creditGrantLowBalance.totalGrantedCredit', locale),
110
+ },
111
+ },
112
+ {
113
+ type: 'text',
114
+ data: {
115
+ type: 'plain',
116
+ text: `${totalGrantedAmount}`,
117
+ },
118
+ },
119
+ ];
120
+
121
+ // 构建操作按钮
122
+ const actions = [
123
+ {
124
+ name: translate('notification.common.viewCreditGrant', locale),
125
+ title: translate('notification.common.viewCreditGrant', locale),
126
+ link: getCustomerIndexUrl({
127
+ locale,
128
+ userDid,
129
+ }),
130
+ },
131
+ ];
132
+
133
+ const template: BaseEmailTemplateType = {
134
+ title: translate('notification.creditGrantLowBalance.title', locale),
135
+ body: translate('notification.creditGrantLowBalance.body', locale, {
136
+ availableAmount,
137
+ totalGrantedAmount,
138
+ }),
139
+ attachments: [
140
+ {
141
+ type: 'section',
142
+ fields,
143
+ },
144
+ ],
145
+ // @ts-ignore
146
+ actions,
147
+ };
148
+
149
+ return template;
150
+ }
151
+ }
@@ -0,0 +1,254 @@
1
+ /* eslint-disable @typescript-eslint/brace-style */
2
+ /* eslint-disable @typescript-eslint/indent */
3
+ import { BN, fromUnitToToken } from '@ocap/util';
4
+ import { getUserLocale } from '../../../integrations/blocklet/notification';
5
+ import { translate } from '../../../locales';
6
+ import { Customer, PaymentCurrency, Subscription } from '../../../store/models';
7
+ import { getMainProductName } from '../../product';
8
+ import { getCustomerSubscriptionPageUrl } from '../../subscription';
9
+ import { formatTime } from '../../time';
10
+ import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
11
+
12
+ export interface CustomerCreditInsufficientEmailTemplateOptions {
13
+ customerId: string;
14
+ currencyId: string;
15
+ meterEventName: string;
16
+ requiredAmount: string;
17
+ availableAmount: string;
18
+ pendingAmount: string;
19
+ subscriptionId?: string;
20
+ }
21
+
22
+ interface CustomerCreditInsufficientEmailTemplateContext {
23
+ locale: string;
24
+ userDid: string;
25
+ currencySymbol: string;
26
+ meterEventName: string;
27
+ requiredAmount: string;
28
+ availableAmount: string;
29
+ isExhausted: boolean;
30
+ productName?: string;
31
+ viewSubscriptionLink?: string;
32
+ at: string;
33
+ }
34
+
35
+ export class CustomerCreditInsufficientEmailTemplate
36
+ implements BaseEmailTemplate<CustomerCreditInsufficientEmailTemplateContext>
37
+ {
38
+ options: CustomerCreditInsufficientEmailTemplateOptions;
39
+
40
+ constructor(options: CustomerCreditInsufficientEmailTemplateOptions) {
41
+ this.options = options;
42
+ }
43
+
44
+ async getContext(): Promise<CustomerCreditInsufficientEmailTemplateContext> {
45
+ const customer = await Customer.findByPk(this.options.customerId);
46
+ if (!customer) {
47
+ throw new Error(`Customer not found: ${this.options.customerId}`);
48
+ }
49
+
50
+ const paymentCurrency = await PaymentCurrency.findByPk(this.options.currencyId);
51
+ if (!paymentCurrency) {
52
+ throw new Error(`PaymentCurrency not found: ${this.options.currencyId}`);
53
+ }
54
+
55
+ const userDid = customer.did;
56
+ const locale = await getUserLocale(userDid);
57
+ const currencySymbol = paymentCurrency.symbol;
58
+ const at = formatTime(Date.now());
59
+
60
+ // 检查是否完全耗尽(可用额度为0或负数)
61
+ const isExhausted = new BN(this.options.availableAmount).lte(0);
62
+
63
+ // 如果有订阅ID,获取订阅信息
64
+ let productName: string | undefined;
65
+ let viewSubscriptionLink: string | undefined;
66
+
67
+ if (this.options.subscriptionId) {
68
+ const subscription = await Subscription.findByPk(this.options.subscriptionId);
69
+ if (subscription) {
70
+ productName = await getMainProductName(subscription.id);
71
+ viewSubscriptionLink = getCustomerSubscriptionPageUrl({
72
+ subscriptionId: subscription.id,
73
+ locale,
74
+ userDid,
75
+ });
76
+ }
77
+ }
78
+
79
+ return {
80
+ locale,
81
+ userDid,
82
+ currencySymbol,
83
+ meterEventName: this.options.meterEventName,
84
+ requiredAmount: `${fromUnitToToken(this.options.requiredAmount, paymentCurrency.decimal)} ${currencySymbol}`,
85
+ availableAmount: `${fromUnitToToken(this.options.availableAmount, paymentCurrency.decimal)} ${currencySymbol}`,
86
+ isExhausted,
87
+ productName,
88
+ viewSubscriptionLink,
89
+ at,
90
+ };
91
+ }
92
+
93
+ async getTemplate(): Promise<BaseEmailTemplateType> {
94
+ const context = await this.getContext();
95
+ const {
96
+ locale,
97
+ userDid,
98
+ meterEventName,
99
+ requiredAmount,
100
+ availableAmount,
101
+ isExhausted,
102
+ productName,
103
+ viewSubscriptionLink,
104
+ } = context;
105
+
106
+ // 构建基础字段
107
+ const baseFields = [
108
+ {
109
+ type: 'text',
110
+ data: {
111
+ type: 'plain',
112
+ color: '#9397A1',
113
+ text: translate('notification.common.account', locale),
114
+ },
115
+ },
116
+ {
117
+ type: 'text',
118
+ data: {
119
+ type: 'plain',
120
+ text: userDid,
121
+ },
122
+ },
123
+ {
124
+ type: 'text',
125
+ data: {
126
+ type: 'plain',
127
+ color: '#9397A1',
128
+ text: locale === 'zh' ? '服务项目' : 'Service',
129
+ },
130
+ },
131
+ {
132
+ type: 'text',
133
+ data: {
134
+ type: 'plain',
135
+ text: meterEventName,
136
+ },
137
+ },
138
+ ];
139
+
140
+ // 产品信息字段(如果有订阅)
141
+ const productFields = productName
142
+ ? [
143
+ {
144
+ type: 'text',
145
+ data: {
146
+ type: 'plain',
147
+ color: '#9397A1',
148
+ text: translate('notification.common.product', locale),
149
+ },
150
+ },
151
+ {
152
+ type: 'text',
153
+ data: {
154
+ type: 'plain',
155
+ text: productName,
156
+ },
157
+ },
158
+ ]
159
+ : [];
160
+
161
+ // 额度信息字段
162
+ const creditFields = [
163
+ {
164
+ type: 'text',
165
+ data: {
166
+ type: 'plain',
167
+ color: '#9397A1',
168
+ text: translate('notification.creditInsufficient.availableCredit', locale),
169
+ },
170
+ },
171
+ {
172
+ type: 'text',
173
+ data: {
174
+ type: 'plain',
175
+ color: isExhausted ? '#FF0000' : '#333333',
176
+ text: `${availableAmount}`,
177
+ },
178
+ },
179
+ ];
180
+
181
+ // 所需额度字段(仅在未完全耗尽时显示)
182
+ const requiredCreditFields = !isExhausted
183
+ ? [
184
+ {
185
+ type: 'text',
186
+ data: {
187
+ type: 'plain',
188
+ color: '#9397A1',
189
+ text: translate('notification.creditInsufficient.requiredCredit', locale),
190
+ },
191
+ },
192
+ {
193
+ type: 'text',
194
+ data: {
195
+ type: 'plain',
196
+ text: `${requiredAmount}`,
197
+ },
198
+ },
199
+ ]
200
+ : [];
201
+
202
+ // 构建操作按钮
203
+ const actions = [
204
+ viewSubscriptionLink && {
205
+ name: translate('notification.common.viewSubscription', locale),
206
+ title: translate('notification.common.viewSubscription', locale),
207
+ link: viewSubscriptionLink,
208
+ },
209
+ ].filter(Boolean);
210
+
211
+ // 构建标题和正文
212
+ let title: string;
213
+ let body: string;
214
+
215
+ if (isExhausted) {
216
+ title = translate('notification.creditInsufficient.exhaustedTitle', locale);
217
+ if (productName) {
218
+ body = translate('notification.creditInsufficient.exhaustedBodyWithSubscription', locale, {
219
+ subscriptionName: productName,
220
+ });
221
+ } else {
222
+ body = translate('notification.creditInsufficient.exhaustedBodyWithoutSubscription', locale);
223
+ }
224
+ } else {
225
+ title = translate('notification.creditInsufficient.title', locale);
226
+ if (productName) {
227
+ body = translate('notification.creditInsufficient.bodyWithSubscription', locale, {
228
+ availableAmount,
229
+
230
+ subscriptionName: productName,
231
+ });
232
+ } else {
233
+ body = translate('notification.creditInsufficient.bodyWithoutSubscription', locale, {
234
+ availableAmount,
235
+ });
236
+ }
237
+ }
238
+
239
+ const template: BaseEmailTemplateType = {
240
+ title,
241
+ body,
242
+ attachments: [
243
+ {
244
+ type: 'section',
245
+ fields: [...baseFields, ...productFields, ...creditFields, ...requiredCreditFields].filter(Boolean),
246
+ },
247
+ ],
248
+ // @ts-ignore
249
+ actions,
250
+ };
251
+
252
+ return template;
253
+ }
254
+ }