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
@@ -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
+ }