payment-kit 1.24.4 → 1.25.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 (116) hide show
  1. package/api/src/index.ts +3 -0
  2. package/api/src/libs/credit-utils.ts +21 -0
  3. package/api/src/libs/discount/discount.ts +13 -0
  4. package/api/src/libs/env.ts +5 -0
  5. package/api/src/libs/error.ts +14 -0
  6. package/api/src/libs/exchange-rate/coingecko-provider.ts +193 -0
  7. package/api/src/libs/exchange-rate/coinmarketcap-provider.ts +180 -0
  8. package/api/src/libs/exchange-rate/index.ts +5 -0
  9. package/api/src/libs/exchange-rate/service.ts +583 -0
  10. package/api/src/libs/exchange-rate/token-address-mapping.ts +84 -0
  11. package/api/src/libs/exchange-rate/token-addresses.json +2147 -0
  12. package/api/src/libs/exchange-rate/token-data-provider.ts +142 -0
  13. package/api/src/libs/exchange-rate/types.ts +114 -0
  14. package/api/src/libs/exchange-rate/validator.ts +319 -0
  15. package/api/src/libs/invoice-quote.ts +158 -0
  16. package/api/src/libs/invoice.ts +143 -7
  17. package/api/src/libs/math-utils.ts +46 -0
  18. package/api/src/libs/notification/template/billing-discrepancy.ts +3 -4
  19. package/api/src/libs/notification/template/customer-auto-recharge-failed.ts +174 -79
  20. package/api/src/libs/notification/template/customer-credit-grant-granted.ts +2 -3
  21. package/api/src/libs/notification/template/customer-credit-insufficient.ts +3 -3
  22. package/api/src/libs/notification/template/customer-credit-low-balance.ts +3 -3
  23. package/api/src/libs/notification/template/customer-revenue-succeeded.ts +2 -3
  24. package/api/src/libs/notification/template/customer-reward-succeeded.ts +9 -4
  25. package/api/src/libs/notification/template/exchange-rate-alert.ts +202 -0
  26. package/api/src/libs/notification/template/subscription-slippage-exceeded.ts +203 -0
  27. package/api/src/libs/notification/template/subscription-slippage-warning.ts +212 -0
  28. package/api/src/libs/notification/template/subscription-will-canceled.ts +2 -2
  29. package/api/src/libs/notification/template/subscription-will-renew.ts +22 -8
  30. package/api/src/libs/payment.ts +3 -1
  31. package/api/src/libs/price.ts +4 -1
  32. package/api/src/libs/queue/index.ts +8 -0
  33. package/api/src/libs/quote-service.ts +1132 -0
  34. package/api/src/libs/quote-validation.ts +388 -0
  35. package/api/src/libs/session.ts +686 -39
  36. package/api/src/libs/slippage.ts +135 -0
  37. package/api/src/libs/subscription.ts +185 -15
  38. package/api/src/libs/util.ts +64 -3
  39. package/api/src/locales/en.ts +50 -0
  40. package/api/src/locales/zh.ts +48 -0
  41. package/api/src/queues/auto-recharge.ts +295 -21
  42. package/api/src/queues/exchange-rate-health.ts +242 -0
  43. package/api/src/queues/invoice.ts +48 -1
  44. package/api/src/queues/notification.ts +167 -1
  45. package/api/src/queues/payment.ts +177 -7
  46. package/api/src/queues/refund.ts +41 -9
  47. package/api/src/queues/subscription.ts +436 -6
  48. package/api/src/routes/auto-recharge-configs.ts +71 -6
  49. package/api/src/routes/checkout-sessions.ts +1730 -81
  50. package/api/src/routes/connect/auto-recharge-auth.ts +2 -0
  51. package/api/src/routes/connect/change-payer.ts +2 -0
  52. package/api/src/routes/connect/change-payment.ts +61 -8
  53. package/api/src/routes/connect/change-plan.ts +161 -17
  54. package/api/src/routes/connect/collect.ts +9 -6
  55. package/api/src/routes/connect/delegation.ts +1 -0
  56. package/api/src/routes/connect/pay.ts +157 -0
  57. package/api/src/routes/connect/setup.ts +32 -10
  58. package/api/src/routes/connect/shared.ts +159 -13
  59. package/api/src/routes/connect/subscribe.ts +32 -9
  60. package/api/src/routes/credit-grants.ts +99 -0
  61. package/api/src/routes/exchange-rate-providers.ts +248 -0
  62. package/api/src/routes/exchange-rates.ts +87 -0
  63. package/api/src/routes/index.ts +4 -0
  64. package/api/src/routes/invoices.ts +280 -2
  65. package/api/src/routes/payment-links.ts +13 -0
  66. package/api/src/routes/prices.ts +84 -2
  67. package/api/src/routes/subscriptions.ts +526 -15
  68. package/api/src/store/migrations/20251220-dynamic-pricing.ts +245 -0
  69. package/api/src/store/migrations/20251223-exchange-rate-provider-type.ts +28 -0
  70. package/api/src/store/migrations/20260110-add-quote-locked-at.ts +23 -0
  71. package/api/src/store/migrations/20260112-add-checkout-session-slippage-percent.ts +22 -0
  72. package/api/src/store/migrations/20260113-add-price-quote-slippage-fields.ts +45 -0
  73. package/api/src/store/migrations/20260116-subscription-slippage.ts +21 -0
  74. package/api/src/store/migrations/20260120-auto-recharge-slippage.ts +21 -0
  75. package/api/src/store/models/auto-recharge-config.ts +12 -0
  76. package/api/src/store/models/checkout-session.ts +7 -0
  77. package/api/src/store/models/exchange-rate-provider.ts +225 -0
  78. package/api/src/store/models/index.ts +6 -0
  79. package/api/src/store/models/payment-intent.ts +6 -0
  80. package/api/src/store/models/price-quote.ts +284 -0
  81. package/api/src/store/models/price.ts +53 -5
  82. package/api/src/store/models/subscription.ts +11 -0
  83. package/api/src/store/models/types.ts +61 -1
  84. package/api/tests/libs/change-payment-plan.spec.ts +282 -0
  85. package/api/tests/libs/exchange-rate-service.spec.ts +341 -0
  86. package/api/tests/libs/quote-service.spec.ts +199 -0
  87. package/api/tests/libs/session.spec.ts +464 -0
  88. package/api/tests/libs/slippage.spec.ts +109 -0
  89. package/api/tests/libs/token-data-provider.spec.ts +267 -0
  90. package/api/tests/models/exchange-rate-provider.spec.ts +121 -0
  91. package/api/tests/models/price-dynamic.spec.ts +100 -0
  92. package/api/tests/models/price-quote.spec.ts +112 -0
  93. package/api/tests/routes/exchange-rate-providers.spec.ts +215 -0
  94. package/api/tests/routes/subscription-slippage.spec.ts +254 -0
  95. package/blocklet.yml +1 -1
  96. package/package.json +7 -6
  97. package/src/components/customer/credit-overview.tsx +14 -0
  98. package/src/components/discount/discount-info.tsx +8 -2
  99. package/src/components/invoice/list.tsx +146 -16
  100. package/src/components/invoice/table.tsx +276 -71
  101. package/src/components/invoice-pdf/template.tsx +3 -7
  102. package/src/components/metadata/form.tsx +6 -8
  103. package/src/components/price/form.tsx +519 -149
  104. package/src/components/promotion/active-redemptions.tsx +5 -3
  105. package/src/components/quote/info.tsx +234 -0
  106. package/src/hooks/subscription.ts +132 -2
  107. package/src/locales/en.tsx +145 -0
  108. package/src/locales/zh.tsx +143 -1
  109. package/src/pages/admin/billing/invoices/detail.tsx +41 -4
  110. package/src/pages/admin/products/exchange-rate-providers/edit-dialog.tsx +354 -0
  111. package/src/pages/admin/products/exchange-rate-providers/index.tsx +363 -0
  112. package/src/pages/admin/products/index.tsx +12 -1
  113. package/src/pages/customer/invoice/detail.tsx +36 -12
  114. package/src/pages/customer/subscription/change-payment.tsx +65 -3
  115. package/src/pages/customer/subscription/change-plan.tsx +207 -38
  116. package/src/pages/customer/subscription/detail.tsx +599 -419
@@ -0,0 +1,203 @@
1
+ /* eslint-disable @typescript-eslint/brace-style */
2
+ import { getUserLocale } from '../../../integrations/blocklet/notification';
3
+ import { translate } from '../../../locales';
4
+ import { Customer, Invoice, Subscription } from '../../../store/models';
5
+ import { getCustomerInvoicePageUrl } from '../../invoice';
6
+ import { getMainProductName } from '../../product';
7
+ import { getCustomerSubscriptionPageUrl } from '../../subscription';
8
+ import { getSubscriptionNotificationCustomActions } from '../../util';
9
+ import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
10
+
11
+ export interface SubscriptionSlippageExceededEmailTemplateOptions {
12
+ subscriptionId: string;
13
+ invoiceId: string;
14
+ paymentIntentId: string;
15
+ currentRate: string;
16
+ minAcceptableRate: string;
17
+ }
18
+
19
+ interface SubscriptionSlippageExceededEmailTemplateContext {
20
+ locale: string;
21
+ productName: string;
22
+ userDid: string;
23
+ currentRate: string;
24
+ minAcceptableRate: string;
25
+ viewSubscriptionLink: string;
26
+ viewInvoiceLink: string;
27
+ customActions: any[];
28
+ }
29
+
30
+ export class SubscriptionSlippageExceededEmailTemplate
31
+ implements BaseEmailTemplate<SubscriptionSlippageExceededEmailTemplateContext>
32
+ {
33
+ options: SubscriptionSlippageExceededEmailTemplateOptions;
34
+
35
+ constructor(options: SubscriptionSlippageExceededEmailTemplateOptions) {
36
+ this.options = options;
37
+ }
38
+
39
+ async getContext(): Promise<SubscriptionSlippageExceededEmailTemplateContext> {
40
+ const subscription = await Subscription.findByPk(this.options.subscriptionId);
41
+ if (!subscription) {
42
+ throw new Error(`Subscription not found: ${this.options.subscriptionId}`);
43
+ }
44
+
45
+ const customer = await Customer.findByPk(subscription.customer_id);
46
+ if (!customer) {
47
+ throw new Error(`Customer not found: ${subscription.customer_id}`);
48
+ }
49
+
50
+ const invoice = await Invoice.findByPk(this.options.invoiceId);
51
+ if (!invoice) {
52
+ throw new Error(`Invoice not found: ${this.options.invoiceId}`);
53
+ }
54
+
55
+ const userDid = customer.did;
56
+ const locale = await getUserLocale(userDid);
57
+ const productName = await getMainProductName(subscription.id);
58
+
59
+ const viewSubscriptionLink = getCustomerSubscriptionPageUrl({
60
+ subscriptionId: subscription.id,
61
+ locale,
62
+ userDid,
63
+ });
64
+
65
+ const viewInvoiceLink = getCustomerInvoicePageUrl({
66
+ invoiceId: invoice.id,
67
+ userDid,
68
+ locale,
69
+ action: 'pay',
70
+ });
71
+
72
+ const customActions = getSubscriptionNotificationCustomActions(
73
+ subscription,
74
+ 'subscription.slippage_exceeded',
75
+ locale
76
+ );
77
+
78
+ return {
79
+ locale,
80
+ productName,
81
+ userDid,
82
+ currentRate: this.options.currentRate,
83
+ minAcceptableRate: this.options.minAcceptableRate,
84
+ viewSubscriptionLink,
85
+ viewInvoiceLink,
86
+ customActions,
87
+ };
88
+ }
89
+
90
+ async getTemplate(): Promise<BaseEmailTemplateType> {
91
+ const {
92
+ locale,
93
+ productName,
94
+ userDid,
95
+ currentRate,
96
+ minAcceptableRate,
97
+ viewSubscriptionLink,
98
+ viewInvoiceLink,
99
+ customActions,
100
+ } = await this.getContext();
101
+
102
+ const template: BaseEmailTemplateType = {
103
+ title: translate('notification.subscriptionSlippageExceeded.title', locale, {
104
+ productName,
105
+ }),
106
+ body: translate('notification.subscriptionSlippageExceeded.body', locale, {
107
+ productName,
108
+ currentRate,
109
+ minAcceptableRate,
110
+ }),
111
+ // @ts-ignore
112
+ attachments: [
113
+ {
114
+ type: 'section',
115
+ fields: [
116
+ {
117
+ type: 'text',
118
+ data: {
119
+ type: 'plain',
120
+ color: '#9397A1',
121
+ text: translate('notification.common.account', locale),
122
+ },
123
+ },
124
+ {
125
+ type: 'text',
126
+ data: {
127
+ type: 'plain',
128
+ text: userDid,
129
+ },
130
+ },
131
+ {
132
+ type: 'text',
133
+ data: {
134
+ type: 'plain',
135
+ color: '#9397A1',
136
+ text: translate('notification.common.product', locale),
137
+ },
138
+ },
139
+ {
140
+ type: 'text',
141
+ data: {
142
+ type: 'plain',
143
+ text: productName,
144
+ },
145
+ },
146
+ {
147
+ type: 'text',
148
+ data: {
149
+ type: 'plain',
150
+ color: '#9397A1',
151
+ text: translate('notification.subscriptionSlippageExceeded.currentRate', locale),
152
+ },
153
+ },
154
+ {
155
+ type: 'text',
156
+ data: {
157
+ type: 'plain',
158
+ color: '#FF0000',
159
+ text: currentRate,
160
+ },
161
+ },
162
+ {
163
+ type: 'text',
164
+ data: {
165
+ type: 'plain',
166
+ color: '#9397A1',
167
+ text: translate('notification.subscriptionSlippageExceeded.minAcceptableRate', locale),
168
+ },
169
+ },
170
+ {
171
+ type: 'text',
172
+ data: {
173
+ type: 'plain',
174
+ text: minAcceptableRate,
175
+ },
176
+ },
177
+ ],
178
+ },
179
+ ],
180
+ // @ts-ignore
181
+ actions: [
182
+ {
183
+ name: translate('notification.subscriptionSlippageExceeded.payNow', locale),
184
+ title: translate('notification.subscriptionSlippageExceeded.payNow', locale),
185
+ link: viewInvoiceLink,
186
+ },
187
+ {
188
+ name: translate('notification.common.viewSubscription', locale),
189
+ title: translate('notification.common.viewSubscription', locale),
190
+ link: viewSubscriptionLink,
191
+ },
192
+ {
193
+ name: translate('notification.subscriptionSlippageExceeded.adjustSlippage', locale),
194
+ title: translate('notification.subscriptionSlippageExceeded.adjustSlippage', locale),
195
+ link: viewSubscriptionLink,
196
+ },
197
+ ...customActions,
198
+ ].filter(Boolean),
199
+ };
200
+
201
+ return template;
202
+ }
203
+ }
@@ -0,0 +1,212 @@
1
+ /* eslint-disable @typescript-eslint/brace-style */
2
+ import prettyMsI18n from 'pretty-ms-i18n';
3
+
4
+ import { getUserLocale } from '../../../integrations/blocklet/notification';
5
+ import { translate } from '../../../locales';
6
+ import { Customer, Subscription } from '../../../store/models';
7
+ import { getMainProductName } from '../../product';
8
+ import { getCustomerSubscriptionPageUrl } from '../../subscription';
9
+ import { formatTime, getPrettyMsI18nLocale } from '../../time';
10
+ import { getSubscriptionNotificationCustomActions } from '../../util';
11
+ import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
12
+
13
+ export interface SubscriptionSlippageWarningEmailTemplateOptions {
14
+ subscriptionId: string;
15
+ currentRate: string;
16
+ minAcceptableRate: string;
17
+ renewalTime: number; // Unix timestamp in seconds
18
+ }
19
+
20
+ interface SubscriptionSlippageWarningEmailTemplateContext {
21
+ locale: string;
22
+ productName: string;
23
+ userDid: string;
24
+ currentRate: string;
25
+ minAcceptableRate: string;
26
+ renewalTime: string;
27
+ timeUntilRenewal: string;
28
+ viewSubscriptionLink: string;
29
+ customActions: any[];
30
+ }
31
+
32
+ export class SubscriptionSlippageWarningEmailTemplate
33
+ implements BaseEmailTemplate<SubscriptionSlippageWarningEmailTemplateContext>
34
+ {
35
+ options: SubscriptionSlippageWarningEmailTemplateOptions;
36
+
37
+ constructor(options: SubscriptionSlippageWarningEmailTemplateOptions) {
38
+ this.options = options;
39
+ }
40
+
41
+ async getContext(): Promise<SubscriptionSlippageWarningEmailTemplateContext> {
42
+ const subscription = await Subscription.findByPk(this.options.subscriptionId);
43
+ if (!subscription) {
44
+ throw new Error(`Subscription not found: ${this.options.subscriptionId}`);
45
+ }
46
+
47
+ const customer = await Customer.findByPk(subscription.customer_id);
48
+ if (!customer) {
49
+ throw new Error(`Customer not found: ${subscription.customer_id}`);
50
+ }
51
+
52
+ const userDid = customer.did;
53
+ const locale = await getUserLocale(userDid);
54
+ const productName = await getMainProductName(subscription.id);
55
+
56
+ const renewalTimeMs = this.options.renewalTime * 1000;
57
+ const renewalTime = formatTime(renewalTimeMs);
58
+ const timeUntilRenewal = prettyMsI18n(renewalTimeMs - Date.now(), {
59
+ locale: getPrettyMsI18nLocale(locale),
60
+ });
61
+
62
+ const viewSubscriptionLink = getCustomerSubscriptionPageUrl({
63
+ subscriptionId: subscription.id,
64
+ locale,
65
+ userDid,
66
+ });
67
+
68
+ const customActions = getSubscriptionNotificationCustomActions(
69
+ subscription,
70
+ 'subscription.slippage_warning',
71
+ locale
72
+ );
73
+
74
+ return {
75
+ locale,
76
+ productName,
77
+ userDid,
78
+ currentRate: this.options.currentRate,
79
+ minAcceptableRate: this.options.minAcceptableRate,
80
+ renewalTime,
81
+ timeUntilRenewal,
82
+ viewSubscriptionLink,
83
+ customActions,
84
+ };
85
+ }
86
+
87
+ async getTemplate(): Promise<BaseEmailTemplateType> {
88
+ const {
89
+ locale,
90
+ productName,
91
+ userDid,
92
+ currentRate,
93
+ minAcceptableRate,
94
+ renewalTime,
95
+ timeUntilRenewal,
96
+ viewSubscriptionLink,
97
+ customActions,
98
+ } = await this.getContext();
99
+
100
+ const template: BaseEmailTemplateType = {
101
+ title: translate('notification.subscriptionSlippageWarning.title', locale, {
102
+ productName,
103
+ }),
104
+ body: translate('notification.subscriptionSlippageWarning.body', locale, {
105
+ productName,
106
+ currentRate,
107
+ minAcceptableRate,
108
+ timeUntilRenewal,
109
+ }),
110
+ // @ts-ignore
111
+ attachments: [
112
+ {
113
+ type: 'section',
114
+ fields: [
115
+ {
116
+ type: 'text',
117
+ data: {
118
+ type: 'plain',
119
+ color: '#9397A1',
120
+ text: translate('notification.common.account', locale),
121
+ },
122
+ },
123
+ {
124
+ type: 'text',
125
+ data: {
126
+ type: 'plain',
127
+ text: userDid,
128
+ },
129
+ },
130
+ {
131
+ type: 'text',
132
+ data: {
133
+ type: 'plain',
134
+ color: '#9397A1',
135
+ text: translate('notification.common.product', locale),
136
+ },
137
+ },
138
+ {
139
+ type: 'text',
140
+ data: {
141
+ type: 'plain',
142
+ text: productName,
143
+ },
144
+ },
145
+ {
146
+ type: 'text',
147
+ data: {
148
+ type: 'plain',
149
+ color: '#9397A1',
150
+ text: translate('notification.subscriptionSlippageWarning.renewalTime', locale),
151
+ },
152
+ },
153
+ {
154
+ type: 'text',
155
+ data: {
156
+ type: 'plain',
157
+ text: renewalTime,
158
+ },
159
+ },
160
+ {
161
+ type: 'text',
162
+ data: {
163
+ type: 'plain',
164
+ color: '#9397A1',
165
+ text: translate('notification.subscriptionSlippageWarning.currentRate', locale),
166
+ },
167
+ },
168
+ {
169
+ type: 'text',
170
+ data: {
171
+ type: 'plain',
172
+ color: '#FF6600',
173
+ text: currentRate,
174
+ },
175
+ },
176
+ {
177
+ type: 'text',
178
+ data: {
179
+ type: 'plain',
180
+ color: '#9397A1',
181
+ text: translate('notification.subscriptionSlippageWarning.minAcceptableRate', locale),
182
+ },
183
+ },
184
+ {
185
+ type: 'text',
186
+ data: {
187
+ type: 'plain',
188
+ text: minAcceptableRate,
189
+ },
190
+ },
191
+ ],
192
+ },
193
+ ],
194
+ // @ts-ignore
195
+ actions: [
196
+ {
197
+ name: translate('notification.common.viewSubscription', locale),
198
+ title: translate('notification.common.viewSubscription', locale),
199
+ link: viewSubscriptionLink,
200
+ },
201
+ {
202
+ name: translate('notification.subscriptionSlippageWarning.adjustSlippage', locale),
203
+ title: translate('notification.subscriptionSlippageWarning.adjustSlippage', locale),
204
+ link: viewSubscriptionLink,
205
+ },
206
+ ...customActions,
207
+ ].filter(Boolean),
208
+ };
209
+
210
+ return template;
211
+ }
212
+ }
@@ -1,12 +1,12 @@
1
1
  /* eslint-disable @typescript-eslint/brace-style */
2
2
  /* eslint-disable @typescript-eslint/indent */
3
- import { fromUnitToToken } from '@ocap/util';
4
3
  import type { ManipulateType } from 'dayjs';
5
4
 
6
5
  import dayjs from '../../dayjs';
7
6
  import { translate } from '../../../locales';
8
7
  import { Invoice, PaymentCurrency, PaymentMethod } from '../../../store/models';
9
8
  import { formatTime, getSimplifyDuration } from '../../time';
9
+ import { formatTokenAmount } from '../../util';
10
10
  import { BaseSubscriptionEmailTemplate, BaseEmailTemplateType } from './base';
11
11
 
12
12
  export interface SubscriptionWillCanceledEmailTemplateOptions {
@@ -90,7 +90,7 @@ export class SubscriptionWillCanceledEmailTemplate extends BaseSubscriptionEmail
90
90
  const willCancelDuration = getSimplifyDuration((cancelAt - now) * 1000, locale);
91
91
 
92
92
  // @ts-ignore
93
- const paymentInfo = `${fromUnitToToken(+invoice.total, invoice?.paymentCurrency?.decimal)} ${invoice?.paymentCurrency?.symbol}${invoice?.paymentMethod ? `(${invoice?.paymentMethod.name})` : ''}`;
93
+ const paymentInfo = `${formatTokenAmount(invoice.total, invoice?.paymentCurrency?.decimal)} ${invoice?.paymentCurrency?.symbol}${invoice?.paymentMethod ? `(${invoice?.paymentMethod.name})` : ''}`;
94
94
 
95
95
  let body = translate('notification.subscriptWillCanceled.body', locale, {
96
96
  productName,
@@ -3,16 +3,15 @@
3
3
  import type { ManipulateType } from 'dayjs';
4
4
  import type { LiteralUnion } from 'type-fest';
5
5
 
6
- import { fromUnitToToken } from '@ocap/util';
7
6
  import dayjs from '../../dayjs';
8
7
  import { getTokenByAddress } from '../../../integrations/arcblock/stake';
9
8
  import { translate } from '../../../locales';
10
9
  import { Invoice, Price, SubscriptionItem } from '../../../store/models';
11
10
  import type { PaymentDetail } from '../../payment';
12
- import { getSubscriptionPaymentAddress, getPaymentAmountForCycleSubscription } from '../../subscription';
11
+ import { getSubscriptionPaymentAddress, getEstimatedPaymentAmountForCycleSubscription } from '../../subscription';
13
12
 
14
13
  import { formatTime, getSimplifyDuration } from '../../time';
15
- import { formatCurrencyInfo, getCustomerRechargeLink } from '../../util';
14
+ import { formatCurrencyInfo, formatTokenAmount, getCustomerRechargeLink } from '../../util';
16
15
  import { BaseSubscriptionEmailTemplate, BaseEmailTemplateType } from './base';
17
16
 
18
17
  export interface SubscriptionWillRenewEmailTemplateOptions {
@@ -91,7 +90,8 @@ export class SubscriptionWillRenewEmailTemplate extends BaseSubscriptionEmailTem
91
90
  const at = formatTime(periodEnd * 1000);
92
91
  const willRenewDuration = getSimplifyDuration((periodEnd - dayjs().unix()) * 1000, locale);
93
92
 
94
- const paymentAmount = await getPaymentAmountForCycleSubscription(subscription, paymentCurrency);
93
+ const paymentAmountResult = await getEstimatedPaymentAmountForCycleSubscription(subscription, paymentCurrency);
94
+ const paymentAmount = paymentAmountResult.amount;
95
95
  const paymentDetail = { price: paymentAmount, balance: 0, symbol: paymentCurrency.symbol, balanceFormatted: '0' };
96
96
 
97
97
  // 获取余额信息(非 Stripe 支付方式)
@@ -100,7 +100,7 @@ export class SubscriptionWillRenewEmailTemplate extends BaseSubscriptionEmailTem
100
100
  const paymentAddress = getSubscriptionPaymentAddress(subscription, paymentInfoResult.paymentMethod!.type);
101
101
  const balance = await getTokenByAddress(paymentAddress, paymentInfoResult.paymentMethod!, paymentCurrency);
102
102
 
103
- paymentDetail.balanceFormatted = fromUnitToToken(balance || '0', paymentCurrency.decimal);
103
+ paymentDetail.balanceFormatted = formatTokenAmount(balance || '0', paymentCurrency.decimal);
104
104
  paymentDetail.balance = +paymentDetail.balanceFormatted;
105
105
  }
106
106
 
@@ -120,9 +120,23 @@ export class SubscriptionWillRenewEmailTemplate extends BaseSubscriptionEmailTem
120
120
  true
121
121
  );
122
122
 
123
- const paymentInfo = isMetered
124
- ? translate('notification.subscriptionWillRenew.estimatedAmountNote', locale, { amount: basePaymentInfo })
125
- : basePaymentInfo;
123
+ // Determine payment info message based on estimation type
124
+ let paymentInfo: string;
125
+ if (paymentAmountResult.estimatedByRate && isMetered) {
126
+ paymentInfo = translate('notification.subscriptionWillRenew.estimatedAmountNoteRateAndUsage', locale, {
127
+ amount: basePaymentInfo,
128
+ });
129
+ } else if (paymentAmountResult.estimatedByRate) {
130
+ paymentInfo = translate('notification.subscriptionWillRenew.estimatedAmountNoteRate', locale, {
131
+ amount: basePaymentInfo,
132
+ });
133
+ } else if (isMetered) {
134
+ paymentInfo = translate('notification.subscriptionWillRenew.estimatedAmountNote', locale, {
135
+ amount: basePaymentInfo,
136
+ });
137
+ } else {
138
+ paymentInfo = basePaymentInfo;
139
+ }
126
140
 
127
141
  // 计算周期时间 - 使用安全的回退机制
128
142
  const periodStart = invoice?.period_start || subscription.current_period_start;
@@ -33,7 +33,7 @@ import {
33
33
  } from './util';
34
34
  import { CHARGE_SUPPORTED_CHAIN_TYPES, EVM_CHAIN_TYPES } from './constants';
35
35
  import { getTokenByAddress } from '../integrations/arcblock/stake';
36
- import { isCreditMetered } from './session';
36
+ import { isCreditMetered } from './credit-utils';
37
37
 
38
38
  export interface SufficientForPaymentResult {
39
39
  sufficient: boolean;
@@ -255,10 +255,12 @@ export async function isDelegationSufficientForPayment(args: {
255
255
  const address = toDelegateAddress(delegator, wallet.address);
256
256
  const { state } = await client.getDelegateState({ address });
257
257
  if (!state) {
258
+ logger.error('isDelegationSufficientForPayment: no delegation state', { address, delegator });
258
259
  return { sufficient: false, reason: 'NO_DELEGATION' };
259
260
  }
260
261
 
261
262
  if (!state.ops || state.ops?.length === 0) {
263
+ logger.error('isDelegationSufficientForPayment: no delegation ops', { address, delegator });
262
264
  return { sufficient: false, reason: 'NO_DELEGATION' };
263
265
  }
264
266
 
@@ -4,7 +4,10 @@ import type { PriceCurrency } from '../store/models/types';
4
4
  /**
5
5
  * Get price currency options for a given price
6
6
  */
7
- export function getPriceCurrencyOptions(price: Price | TPriceExpanded): PriceCurrency[] {
7
+ export function getPriceCurrencyOptions(price: Price | TPriceExpanded | null | undefined): PriceCurrency[] {
8
+ if (!price) {
9
+ return [];
10
+ }
8
11
  if (Array.isArray(price.currency_options)) {
9
12
  return price.currency_options;
10
13
  }
@@ -180,6 +180,14 @@ export default function createQueue<T = any>({ name, onJob, options = defaults }
180
180
  return;
181
181
  }
182
182
 
183
+ // Check if error is marked as non-retryable
184
+ if (err && err.nonRetryable === true) {
185
+ logger.info('fail job due to non-retryable error', { id: jobId, job, errorCode: err.code });
186
+ await clearJob(jobId);
187
+ emit('failed', { id: jobId, job, error: err });
188
+ return;
189
+ }
190
+
183
191
  // @ts-ignore
184
192
  if (doc.retry_count >= maxRetries) {
185
193
  logger.info('fail job on max retry exceed', { id: jobId, job });