payment-kit 1.22.24 → 1.22.25

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.
@@ -1,6 +1,9 @@
1
1
  import { fromTokenToUnit, fromUnitToToken } from '@ocap/util';
2
- import { PaymentCurrency } from '../store/models';
2
+ import { getUrl } from '@blocklet/sdk';
3
+ import { PaymentCurrency, Price, Product, RechargeConfig } from '../store/models';
3
4
  import { trimDecimals } from './math-utils';
5
+ import { createPaymentLink } from '../routes/payment-links';
6
+ import logger from './logger';
4
7
 
5
8
  export async function formatCurrencyToken(amount: string, currencyId: string) {
6
9
  if (!amount) {
@@ -29,3 +32,71 @@ export async function formatCurrencyUnit(amount: string, currencyId: string) {
29
32
  }
30
33
  return fromTokenToUnit(amount || '0', currency.decimal).toString();
31
34
  }
35
+
36
+ export async function getRechargePaymentUrl(
37
+ currency: PaymentCurrency & { recharge_config?: RechargeConfig }
38
+ ): Promise<string | null> {
39
+ try {
40
+ if (!currency || !currency.recharge_config) {
41
+ return null;
42
+ }
43
+
44
+ const rechargeConfig = currency.recharge_config;
45
+ const checkoutUrl = rechargeConfig.checkout_url;
46
+
47
+ if (checkoutUrl) {
48
+ return checkoutUrl;
49
+ }
50
+
51
+ if (rechargeConfig.payment_link_id) {
52
+ return getUrl(`/checkout/pay/${rechargeConfig.payment_link_id}`);
53
+ }
54
+
55
+ if (rechargeConfig.base_price_id) {
56
+ const basePrice = (await Price.findByPk(rechargeConfig.base_price_id, {
57
+ include: [{ model: Product, as: 'product' }],
58
+ })) as (Price & { product: Product }) | null;
59
+
60
+ if (!basePrice) {
61
+ logger.warn('Base price not found for recharge config', {
62
+ currencyId: currency.id,
63
+ basePriceId: rechargeConfig.base_price_id,
64
+ });
65
+ return null;
66
+ }
67
+
68
+ const paymentLink = await createPaymentLink({
69
+ livemode: currency.livemode,
70
+ currency_id: basePrice.currency_id,
71
+ name: basePrice.product?.name || `${currency.name} Recharge`,
72
+ submit_type: 'pay',
73
+ allow_promotion_codes: true,
74
+ line_items: [
75
+ {
76
+ price_id: rechargeConfig.base_price_id,
77
+ quantity: 1,
78
+ adjustable_quantity: {
79
+ enabled: true,
80
+ minimum: 1,
81
+ maximum: 100000000,
82
+ },
83
+ },
84
+ ],
85
+ });
86
+
87
+ await currency.update({
88
+ recharge_config: { ...rechargeConfig, payment_link_id: paymentLink.id },
89
+ });
90
+
91
+ return getUrl(`/checkout/pay/${paymentLink.id}`);
92
+ }
93
+
94
+ return null;
95
+ } catch (error: any) {
96
+ logger.error('Failed to get recharge payment URL', {
97
+ currencyId: currency.id,
98
+ error: error.message,
99
+ });
100
+ return null;
101
+ }
102
+ }
@@ -1,6 +1,7 @@
1
1
  /* eslint-disable @typescript-eslint/brace-style */
2
2
  /* eslint-disable @typescript-eslint/indent */
3
3
  import { BN, fromUnitToToken } from '@ocap/util';
4
+ import { withQuery } from 'ufo';
4
5
  import { getUserLocale } from '../../../integrations/blocklet/notification';
5
6
  import { translate } from '../../../locales';
6
7
  import { Customer, PaymentCurrency, Subscription } from '../../../store/models';
@@ -8,7 +9,8 @@ import { getMainProductName } from '../../product';
8
9
  import { getCustomerSubscriptionPageUrl } from '../../subscription';
9
10
  import { formatTime } from '../../time';
10
11
  import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
11
- import { formatNumber, getCustomerIndexUrl } from '../../util';
12
+ import { formatNumber, getConnectQueryParam, getCustomerIndexUrl } from '../../util';
13
+ import { getRechargePaymentUrl } from '../../currency';
12
14
 
13
15
  export interface CustomerCreditInsufficientEmailTemplateOptions {
14
16
  customerId: string;
@@ -32,6 +34,7 @@ interface CustomerCreditInsufficientEmailTemplateContext {
32
34
  isExhausted: boolean;
33
35
  productName?: string;
34
36
  viewSubscriptionLink?: string;
37
+ rechargeUrl: string | null;
35
38
  at: string;
36
39
  }
37
40
 
@@ -50,7 +53,7 @@ export class CustomerCreditInsufficientEmailTemplate
50
53
  throw new Error(`Customer not found: ${this.options.customerId}`);
51
54
  }
52
55
 
53
- const paymentCurrency = await PaymentCurrency.findByPk(this.options.currencyId);
56
+ const paymentCurrency = await PaymentCurrency.scope('withRechargeConfig').findByPk(this.options.currencyId);
54
57
  if (!paymentCurrency) {
55
58
  throw new Error(`PaymentCurrency not found: ${this.options.currencyId}`);
56
59
  }
@@ -79,6 +82,11 @@ export class CustomerCreditInsufficientEmailTemplate
79
82
  }
80
83
  }
81
84
 
85
+ let rechargeUrl: string | null = await getRechargePaymentUrl(paymentCurrency);
86
+ if (rechargeUrl) {
87
+ rechargeUrl = withQuery(rechargeUrl, { ...getConnectQueryParam({ userDid }) });
88
+ }
89
+
82
90
  return {
83
91
  locale,
84
92
  userDid,
@@ -90,6 +98,7 @@ export class CustomerCreditInsufficientEmailTemplate
90
98
  isExhausted,
91
99
  productName,
92
100
  viewSubscriptionLink,
101
+ rechargeUrl,
93
102
  at,
94
103
  };
95
104
  }
@@ -105,6 +114,7 @@ export class CustomerCreditInsufficientEmailTemplate
105
114
  isExhausted,
106
115
  productName,
107
116
  viewSubscriptionLink,
117
+ rechargeUrl,
108
118
  } = context;
109
119
 
110
120
  // 构建基础字段
@@ -204,19 +214,26 @@ export class CustomerCreditInsufficientEmailTemplate
204
214
  : [];
205
215
 
206
216
  // 构建操作按钮
217
+ const customerIndexUrl = getCustomerIndexUrl({
218
+ locale,
219
+ userDid,
220
+ });
221
+
207
222
  const actions = [
223
+ rechargeUrl && {
224
+ name: translate('notification.common.reloadCredits', locale),
225
+ title: translate('notification.common.reloadCredits', locale),
226
+ link: rechargeUrl,
227
+ },
208
228
  viewSubscriptionLink && {
209
229
  name: translate('notification.common.viewSubscription', locale),
210
230
  title: translate('notification.common.viewSubscription', locale),
211
231
  link: viewSubscriptionLink,
212
232
  },
213
233
  {
214
- name: translate('notification.common.viewCreditGrant', locale),
215
- title: translate('notification.common.viewCreditGrant', locale),
216
- link: getCustomerIndexUrl({
217
- locale,
218
- userDid,
219
- }),
234
+ name: translate('notification.common.manageCredit', locale),
235
+ title: translate('notification.common.manageCredit', locale),
236
+ link: customerIndexUrl,
220
237
  },
221
238
  ].filter(Boolean);
222
239
 
@@ -1,11 +1,13 @@
1
1
  /* eslint-disable @typescript-eslint/brace-style */
2
2
  /* eslint-disable @typescript-eslint/indent */
3
3
  import { fromUnitToToken } from '@ocap/util';
4
+ import { withQuery } from 'ufo';
4
5
  import { getUserLocale } from '../../../integrations/blocklet/notification';
5
6
  import { translate } from '../../../locales';
6
7
  import { Customer, PaymentCurrency } from '../../../store/models';
7
8
  import type { BaseEmailTemplate, BaseEmailTemplateType } from './base';
8
- import { formatNumber, getCustomerIndexUrl } from '../../util';
9
+ import { formatNumber, getConnectQueryParam, getCustomerIndexUrl } from '../../util';
10
+ import { getRechargePaymentUrl } from '../../currency';
9
11
 
10
12
  export interface CustomerCreditLowBalanceEmailTemplateOptions {
11
13
  customerId: string;
@@ -20,9 +22,10 @@ interface CustomerCreditLowBalanceEmailTemplateContext {
20
22
  userDid: string;
21
23
  currencySymbol: string;
22
24
  availableAmount: string; // formatted with symbol
23
- totalAmount: string; // formatted with symbol
24
- lowBalancePercentage: string; // with %
25
+ lowBalancePercentage: string; // with % or "less than 1%"
25
26
  currencyName: string;
27
+ isCritical: boolean; // true if percentage < 1%
28
+ rechargeUrl: string | null;
26
29
  }
27
30
  export class CustomerCreditLowBalanceEmailTemplate
28
31
  implements BaseEmailTemplate<CustomerCreditLowBalanceEmailTemplateContext>
@@ -34,14 +37,14 @@ export class CustomerCreditLowBalanceEmailTemplate
34
37
  }
35
38
 
36
39
  async getContext(): Promise<CustomerCreditLowBalanceEmailTemplateContext> {
37
- const { customerId, currencyId, availableAmount, totalAmount, percentage } = this.options;
40
+ const { customerId, currencyId, availableAmount, percentage } = this.options;
38
41
 
39
42
  const customer = await Customer.findByPk(customerId);
40
43
  if (!customer) {
41
44
  throw new Error(`Customer not found: ${customerId}`);
42
45
  }
43
46
 
44
- const paymentCurrency = await PaymentCurrency.findByPk(currencyId);
47
+ const paymentCurrency = await PaymentCurrency.scope('withRechargeConfig').findByPk(currencyId);
45
48
  if (!paymentCurrency) {
46
49
  throw new Error(`PaymentCurrency not found: ${currencyId}`);
47
50
  }
@@ -51,23 +54,41 @@ export class CustomerCreditLowBalanceEmailTemplate
51
54
  const currencySymbol = paymentCurrency.symbol;
52
55
 
53
56
  const available = formatNumber(fromUnitToToken(availableAmount, paymentCurrency.decimal));
54
- const total = formatNumber(fromUnitToToken(totalAmount, paymentCurrency.decimal));
57
+ const percentageNum = parseFloat(percentage);
58
+ const isCritical = percentageNum < 1;
59
+ const lowBalancePercentage = isCritical
60
+ ? translate('notification.creditLowBalance.lessThanOnePercent', locale)
61
+ : `${percentage}%`;
62
+
63
+ let rechargeUrl: string | null = await getRechargePaymentUrl(paymentCurrency);
64
+ if (rechargeUrl) {
65
+ rechargeUrl = withQuery(rechargeUrl, { ...getConnectQueryParam({ userDid }) });
66
+ }
55
67
 
56
68
  return {
57
69
  locale,
58
70
  userDid,
59
71
  currencySymbol,
60
72
  availableAmount: `${available}`,
61
- totalAmount: `${total}`,
62
- lowBalancePercentage: `${percentage}%`,
73
+ lowBalancePercentage,
63
74
  currencyName: paymentCurrency.name,
75
+ isCritical,
76
+ rechargeUrl,
64
77
  };
65
78
  }
66
79
 
67
80
  async getTemplate(): Promise<BaseEmailTemplateType> {
68
81
  const context = await this.getContext();
69
- const { locale, userDid, availableAmount, totalAmount, lowBalancePercentage, currencyName, currencySymbol } =
70
- context;
82
+ const {
83
+ locale,
84
+ userDid,
85
+ availableAmount,
86
+ lowBalancePercentage,
87
+ currencyName,
88
+ currencySymbol,
89
+ isCritical,
90
+ rechargeUrl,
91
+ } = context;
71
92
 
72
93
  const fields = [
73
94
  {
@@ -90,15 +111,15 @@ export class CustomerCreditLowBalanceEmailTemplate
90
111
  data: {
91
112
  type: 'plain',
92
113
  color: '#9397A1',
93
- text: translate('notification.creditInsufficient.availableCredit', locale),
114
+ text: translate('notification.creditLowBalance.remainingBalance', locale),
94
115
  },
95
116
  },
96
117
  {
97
118
  type: 'text',
98
119
  data: {
99
120
  type: 'plain',
100
- color: '#FF6600',
101
- text: `${availableAmount} ${currencySymbol} (${lowBalancePercentage})`,
121
+ color: isCritical ? '#FF0000' : '#FF6600',
122
+ text: `${availableAmount} ${currencySymbol}`,
102
123
  },
103
124
  },
104
125
  {
@@ -106,38 +127,42 @@ export class CustomerCreditLowBalanceEmailTemplate
106
127
  data: {
107
128
  type: 'plain',
108
129
  color: '#9397A1',
109
- text: translate('notification.creditLowBalance.totalAmount', locale),
130
+ text: translate('notification.creditLowBalance.status', locale),
110
131
  },
111
132
  },
112
133
  {
113
134
  type: 'text',
114
135
  data: {
115
136
  type: 'plain',
116
- text: `${totalAmount} ${currencySymbol}`,
137
+ color: isCritical ? '#FF0000' : '#FF6600',
138
+ text: lowBalancePercentage,
117
139
  },
118
140
  },
119
141
  ];
120
142
 
143
+ const customerIndexUrl = getCustomerIndexUrl({
144
+ locale,
145
+ userDid,
146
+ });
147
+
121
148
  const actions = [
149
+ rechargeUrl && {
150
+ name: translate('notification.common.reloadCredits', locale),
151
+ title: translate('notification.common.reloadCredits', locale),
152
+ link: rechargeUrl,
153
+ },
122
154
  {
123
- name: translate('notification.common.viewCreditGrant', locale),
124
- title: translate('notification.common.viewCreditGrant', locale),
125
- link: getCustomerIndexUrl({
126
- locale,
127
- userDid,
128
- }),
155
+ name: translate('notification.common.manageCredit', locale),
156
+ title: translate('notification.common.manageCredit', locale),
157
+ link: customerIndexUrl,
129
158
  },
130
- ];
159
+ ].filter(Boolean);
131
160
 
132
161
  const template: BaseEmailTemplateType = {
133
- title: translate('notification.creditLowBalance.title', locale, {
134
- lowBalancePercentage,
135
- currency: currencyName,
136
- }),
162
+ title: translate('notification.creditLowBalance.title', locale),
137
163
  body: translate('notification.creditLowBalance.body', locale, {
138
164
  currency: currencyName,
139
165
  availableAmount,
140
- totalAmount,
141
166
  lowBalancePercentage,
142
167
  }),
143
168
  attachments: [
@@ -49,6 +49,8 @@ export default flat({
49
49
  shouldPayAmount: 'Should pay amount',
50
50
  billedAmount: 'Billed amount',
51
51
  viewCreditGrant: 'View Credit Balance',
52
+ manageCredit: 'Manage Credits',
53
+ reloadCredits: 'Reload Credits',
52
54
  invoiceNumber: 'Invoice Number',
53
55
  payer: 'Payer',
54
56
  },
@@ -253,18 +255,17 @@ export default flat({
253
255
  creditInsufficient: {
254
256
  title: 'Insufficient Credit',
255
257
  bodyWithSubscription:
256
- 'Your available credit is only {availableAmount}, which is not enough to cover your subscription to {subscriptionName}. Please top up to avoid service interruption.',
258
+ 'Your available credit ({availableAmount}) is not enough to cover your subscription to {subscriptionName}. To ensure uninterrupted service, please reload your account.',
257
259
  bodyWithoutSubscription:
258
- 'Your available credit is only {availableAmount}, which is not enough to continue using the service. Please top up to avoid restrictions.',
259
- exhaustedTitle: 'Credit Exhausted – Please Top Up',
260
+ 'Your available credit ({availableAmount}) is not enough to continue using the service. To ensure uninterrupted service, please reload your account.',
261
+ exhaustedTitle: 'Credit Exhausted – Please Reload',
260
262
  exhaustedBodyWithSubscription:
261
- 'Your credit is fully exhausted and can no longer cover your subscription to {subscriptionName}. Please top up to avoid service interruption.',
263
+ 'Your credit is fully exhausted and can no longer cover your subscription to {subscriptionName}. To ensure uninterrupted service, please reload your account.',
262
264
  exhaustedBodyWithoutSubscription:
263
- 'Your credit is fully exhausted (remaining balance: 0). Please top up to ensure uninterrupted service.',
265
+ 'Your credit is fully exhausted. To ensure uninterrupted service, please reload your account.',
264
266
  meterEventName: 'Service',
265
267
  availableCredit: 'Available Credit Amount',
266
268
  requiredCredit: 'Required Credit Amount',
267
- topUpNow: 'Top Up Now',
268
269
  },
269
270
 
270
271
  creditGrantGranted: {
@@ -277,9 +278,11 @@ export default flat({
277
278
  },
278
279
 
279
280
  creditLowBalance: {
280
- title: 'Your {currency} is below {lowBalancePercentage}',
281
- body: 'Your {currency} available balance is below {lowBalancePercentage} of the total. Please top up to avoid service interruption.',
282
- totalAmount: 'Total Credit Amount',
281
+ title: 'Your credit balance is low',
282
+ body: 'Your {currency} balance has dropped to critical levels ({lowBalancePercentage}). To ensure uninterrupted service, please reload your account.',
283
+ remainingBalance: 'Remaining Balance',
284
+ status: 'Status',
285
+ lessThanOnePercent: 'less than 1%',
283
286
  },
284
287
  },
285
288
  });
@@ -49,6 +49,8 @@ export default flat({
49
49
  shouldPayAmount: '应收金额',
50
50
  billedAmount: '实缴金额',
51
51
  viewCreditGrant: '查看额度',
52
+ reloadCredits: '立即充值',
53
+ manageCredit: '管理额度',
52
54
  invoiceNumber: '账单编号',
53
55
  payer: '付款方',
54
56
  },
@@ -243,16 +245,15 @@ export default flat({
243
245
  creditInsufficient: {
244
246
  title: '额度不足,服务可能受限',
245
247
  bodyWithSubscription:
246
- '您的信用额度仅剩 {availableAmount},不足以支付您订阅的 {subscriptionName}。请及时充值以避免服务中断。',
247
- bodyWithoutSubscription: '您的信用额度仅剩 {availableAmount},不足以继续使用服务。请及时充值以避免服务受限。',
248
+ '您的信用额度仅剩 {availableAmount},不足以支付您订阅的 {subscriptionName}。为避免服务中断,请及时充值。',
249
+ bodyWithoutSubscription: '您的信用额度仅剩 {availableAmount},不足以继续使用服务。为避免服务受限,请及时充值。',
248
250
  exhaustedTitle: '额度已用尽,请尽快充值',
249
251
  exhaustedBodyWithSubscription:
250
- '您的信用额度已用尽,无法继续支付您订阅的 {subscriptionName}。为确保服务不中断,请及时充值。',
251
- exhaustedBodyWithoutSubscription: '您的信用额度已用尽(剩余额度为 0)。为确保服务正常使用,请及时充值。',
252
+ '您的信用额度已用尽,无法继续支付您订阅的 {subscriptionName}。为避免服务中断,请及时充值。',
253
+ exhaustedBodyWithoutSubscription: '您的信用额度已用尽。为确保服务正常使用,请及时充值。',
252
254
  meterEventName: '服务项目',
253
255
  availableCredit: '剩余额度',
254
256
  requiredCredit: '所需额度',
255
- topUpNow: '立即充值',
256
257
  },
257
258
 
258
259
  creditGrantGranted: {
@@ -265,9 +266,11 @@ export default flat({
265
266
  },
266
267
 
267
268
  creditLowBalance: {
268
- title: '您的{currency} 已低于 {lowBalancePercentage}',
269
- body: '您的 {currency} 总可用额度已低于 {lowBalancePercentage},请及时充值以避免服务受限。',
270
- totalAmount: '总额度',
269
+ title: '您的信用余额偏低',
270
+ body: '您的 {currency} 余额已降至临界水平({lowBalancePercentage})。为避免服务中断,请及时充值。',
271
+ remainingBalance: '剩余余额',
272
+ status: '状态',
273
+ lessThanOnePercent: '低于 1%',
271
274
  },
272
275
  },
273
276
  });
@@ -1,17 +1,29 @@
1
1
  import { Router } from 'express';
2
2
  import Joi from 'joi';
3
- import { fromTokenToUnit } from '@ocap/util';
3
+ import { BN, fromTokenToUnit } from '@ocap/util';
4
4
 
5
5
  import { literal, OrderItem } from 'sequelize';
6
6
  import pick from 'lodash/pick';
7
7
  import { createListParamSchema, getOrder, getWhereFromKvQuery, MetadataSchema } from '../libs/api';
8
8
  import logger from '../libs/logger';
9
9
  import { authenticate } from '../libs/security';
10
- import { CreditGrant, Customer, PaymentCurrency, Price, Subscription } from '../store/models';
10
+ import {
11
+ AutoRechargeConfig,
12
+ CreditGrant,
13
+ Customer,
14
+ MeterEvent,
15
+ PaymentCurrency,
16
+ PaymentMethod,
17
+ Price,
18
+ Product,
19
+ Subscription,
20
+ } from '../store/models';
11
21
  import { createCreditGrant } from '../libs/credit-grant';
12
22
  import { getMeterPriceIdsFromSubscription } from '../libs/subscription';
13
23
  import { blocklet } from '../libs/auth';
14
24
  import { formatMetadata } from '../libs/util';
25
+ import { getPriceUintAmountByCurrency } from '../libs/price';
26
+ import { checkTokenBalance } from '../libs/payment';
15
27
 
16
28
  const router = Router();
17
29
  const auth = authenticate<CreditGrant>({ component: true, roles: ['owner', 'admin'] });
@@ -150,6 +162,197 @@ router.get('/summary', authMine, async (req, res) => {
150
162
  }
151
163
  });
152
164
 
165
+ const checkAutoRechargeSchema = Joi.object({
166
+ customer_id: Joi.string().required(),
167
+ currency_id: Joi.string().required(),
168
+ pending_amount: Joi.string().optional(),
169
+ });
170
+
171
+ router.get('/verify-availability', authMine, async (req, res) => {
172
+ try {
173
+ const { error, value } = checkAutoRechargeSchema.validate(req.query, { stripUnknown: true });
174
+ if (error) {
175
+ return res.status(400).json({ error: error.message });
176
+ }
177
+
178
+ const { customer_id: customerId, currency_id: currencyId } = value;
179
+ let pendingAmount = value.pending_amount;
180
+
181
+ const customer = await Customer.findByPkOrDid(customerId);
182
+ if (!customer) {
183
+ return res.status(404).json({ error: `Customer ${customerId} not found` });
184
+ }
185
+
186
+ const currency = await PaymentCurrency.findByPk(currencyId);
187
+ if (!currency) {
188
+ return res.status(404).json({ error: `PaymentCurrency ${currencyId} not found` });
189
+ }
190
+
191
+ const config = (await AutoRechargeConfig.findOne({
192
+ where: {
193
+ customer_id: customer.id,
194
+ currency_id: currencyId,
195
+ enabled: true,
196
+ },
197
+ include: [
198
+ { model: PaymentCurrency, as: 'rechargeCurrency', required: false },
199
+ { model: Price, as: 'price', include: [{ model: Product, as: 'product' }] },
200
+ { model: PaymentMethod, as: 'paymentMethod', required: false },
201
+ ],
202
+ })) as
203
+ | (AutoRechargeConfig & {
204
+ rechargeCurrency?: PaymentCurrency;
205
+ price?: Price & { product?: Product };
206
+ paymentMethod?: PaymentMethod;
207
+ })
208
+ | null;
209
+
210
+ if (!config) {
211
+ return res.json({
212
+ can_continue: false,
213
+ has_auto_recharge: false,
214
+ reason: 'auto_recharge_config_not_found',
215
+ });
216
+ }
217
+
218
+ // 1. Check config completeness
219
+ if (!config.rechargeCurrency) {
220
+ return res.json({
221
+ can_continue: false,
222
+ reason: 'recharge_currency_not_found',
223
+ });
224
+ }
225
+
226
+ if (!config.price) {
227
+ return res.json({
228
+ can_continue: false,
229
+ reason: 'price_not_found',
230
+ });
231
+ }
232
+
233
+ if (!config.paymentMethod) {
234
+ return res.json({
235
+ can_continue: false,
236
+ reason: 'payment_method_not_found',
237
+ });
238
+ }
239
+
240
+ // 2. Check if stripe (balance check not supported)
241
+ if (config.paymentMethod.type === 'stripe') {
242
+ return res.json({
243
+ can_continue: false,
244
+ reason: 'balance_check_not_supported',
245
+ });
246
+ }
247
+
248
+ // 3. Check price amount
249
+ const priceAmount = await getPriceUintAmountByCurrency(config.price, config.rechargeCurrency.id);
250
+ if (!priceAmount) {
251
+ return res.json({
252
+ can_continue: false,
253
+ reason: 'invalid_price_amount',
254
+ });
255
+ }
256
+
257
+ const totalAmount = new BN(priceAmount).mul(new BN(config.quantity ?? 1));
258
+
259
+ // 4. Get pending amount if not provided
260
+ if (!pendingAmount) {
261
+ const [pendingSummary] = await MeterEvent.getPendingAmounts({
262
+ customerId: customer.id,
263
+ currencyId,
264
+ livemode: req.livemode,
265
+ });
266
+ pendingAmount = pendingSummary?.[currencyId] || '0';
267
+ }
268
+
269
+ // 5. Check daily limit
270
+ const pendingAmountBN = new BN(pendingAmount);
271
+ const today = new Date().toISOString().split('T')[0];
272
+ const isNewDay = config.last_recharge_date !== today;
273
+
274
+ // Calculate required recharge times: if pendingAmount > 0, calculate needed times; otherwise check if at least one recharge is possible
275
+ const requiredRechargeTimes = pendingAmountBN.gt(new BN(0))
276
+ ? pendingAmountBN.add(totalAmount).sub(new BN(1)).div(totalAmount).toNumber()
277
+ : 1;
278
+
279
+ if (!isNewDay && config.daily_stats && config.daily_limits) {
280
+ // Check attempt limit
281
+ const maxAttempts = Number(config.daily_limits.max_attempts);
282
+ if (maxAttempts > 0) {
283
+ const remainingAttempts = maxAttempts - Number(config.daily_stats.attempt_count);
284
+ if (requiredRechargeTimes > remainingAttempts) {
285
+ return res.json({
286
+ can_continue: false,
287
+ reason: 'daily_limit_reached',
288
+ detail: 'attempt_limit_exceeded',
289
+ });
290
+ }
291
+ }
292
+
293
+ // Check amount limit
294
+ const maxAmount = new BN(config.daily_limits.max_amount || '0');
295
+ if (maxAmount.gt(new BN(0))) {
296
+ const requiredTotalAmount = totalAmount.mul(new BN(requiredRechargeTimes));
297
+ const remainingAmount = maxAmount.sub(new BN(config.daily_stats.total_amount || '0'));
298
+ if (requiredTotalAmount.gt(remainingAmount)) {
299
+ return res.json({
300
+ can_continue: false,
301
+ reason: 'daily_limit_reached',
302
+ detail: 'amount_limit_exceeded',
303
+ });
304
+ }
305
+ }
306
+ }
307
+
308
+ // 6. Check payment account balance
309
+ const payer =
310
+ config.payment_settings?.payment_method_options?.[
311
+ config.paymentMethod.type as keyof typeof config.payment_settings.payment_method_options
312
+ ]?.payer || customer.did;
313
+
314
+ if (!payer) {
315
+ return res.json({
316
+ can_continue: false,
317
+ reason: 'payer_not_found',
318
+ });
319
+ }
320
+
321
+ // Check token balance: if pendingAmount > 0, check if balance can cover pending; otherwise check if balance can cover at least one recharge
322
+ const amountToCheck = pendingAmountBN.gt(new BN(0)) ? pendingAmount : totalAmount.toString();
323
+ const balanceResult = await checkTokenBalance({
324
+ paymentMethod: config.paymentMethod,
325
+ paymentCurrency: config.rechargeCurrency,
326
+ userDid: payer,
327
+ amount: amountToCheck,
328
+ skipUserCheck: true,
329
+ });
330
+
331
+ if (!balanceResult.sufficient) {
332
+ return res.json({
333
+ can_continue: false,
334
+ reason: 'insufficient_balance',
335
+ payment_account_balance: balanceResult.token?.balance || '0',
336
+ pending_amount: pendingAmount,
337
+ });
338
+ }
339
+
340
+ return res.json({
341
+ can_continue: true,
342
+ payment_account_sufficient: true,
343
+ payment_account_balance: balanceResult.token?.balance || '0',
344
+ pending_amount: pendingAmount,
345
+ });
346
+ } catch (err: any) {
347
+ logger.error('check auto recharge failed', {
348
+ error: err.message,
349
+ customerId: req.query.customer_id,
350
+ currencyId: req.query.currency_id,
351
+ });
352
+ return res.status(400).json({ error: err.message });
353
+ }
354
+ });
355
+
153
356
  router.get('/:id', authPortal, async (req, res) => {
154
357
  const creditGrant = await CreditGrant.findByPk(req.params.id, {
155
358
  include: [
@@ -4,7 +4,6 @@ import { InferAttributes, Op, WhereOptions } from 'sequelize';
4
4
 
5
5
  import Joi from 'joi';
6
6
  import pick from 'lodash/pick';
7
- import { getUrl } from '@blocklet/sdk';
8
7
  import { sessionMiddleware } from '@blocklet/sdk/lib/middlewares/session';
9
8
  import { fetchErc20Meta } from '../integrations/ethereum/token';
10
9
  import logger from '../libs/logger';
@@ -18,8 +17,8 @@ import { resolveAddressChainTypes } from '../libs/util';
18
17
  import { depositVaultQueue } from '../queues/payment';
19
18
  import { checkDepositVaultAmount } from '../libs/payment';
20
19
  import { getTokenSummaryByDid } from '../integrations/arcblock/stake';
21
- import { createPaymentLink } from './payment-links';
22
20
  import { MetadataSchema } from '../libs/api';
21
+ import { getRechargePaymentUrl } from '../libs/currency';
23
22
 
24
23
  const router = Router();
25
24
 
@@ -394,6 +393,8 @@ router.get('/:id/recharge-config', user, async (req, res) => {
394
393
  });
395
394
  }
396
395
 
396
+ const paymentUrl = await getRechargePaymentUrl(currency);
397
+
397
398
  let basePrice: (Price & { product: Product }) | null = null;
398
399
  if (currency.recharge_config.base_price_id) {
399
400
  basePrice = (await Price.findByPk(currency.recharge_config.base_price_id, {
@@ -401,34 +402,6 @@ router.get('/:id/recharge-config', user, async (req, res) => {
401
402
  })) as Price & { product: Product };
402
403
  }
403
404
 
404
- const rechargeConfig = currency.recharge_config;
405
- let paymentUrl = rechargeConfig.checkout_url;
406
- if (!paymentUrl && rechargeConfig.payment_link_id) {
407
- paymentUrl = getUrl(`/checkout/pay/${rechargeConfig.payment_link_id}`);
408
- }
409
- if (!paymentUrl && rechargeConfig.base_price_id) {
410
- const paymentLink = await createPaymentLink({
411
- livemode: currency.livemode,
412
- currency_id: basePrice?.currency_id,
413
- name: basePrice?.product?.name || `${currency.name} Recharge`,
414
- submit_type: 'pay',
415
- allow_promotion_codes: true,
416
- line_items: [
417
- {
418
- price_id: rechargeConfig.base_price_id,
419
- quantity: 1,
420
- adjustable_quantity: {
421
- enabled: true,
422
- minimum: 1,
423
- maximum: 100000000,
424
- },
425
- },
426
- ],
427
- });
428
- await currency.update({ recharge_config: { ...rechargeConfig, payment_link_id: paymentLink.id } });
429
- paymentUrl = getUrl(`/checkout/pay/${paymentLink.id}`);
430
- }
431
-
432
405
  return res.json({
433
406
  currency_id: id,
434
407
  currency_info: pick(currency, ['id', 'name', 'symbol', 'decimal', 'type']),
package/blocklet.yml CHANGED
@@ -14,7 +14,7 @@ repository:
14
14
  type: git
15
15
  url: git+https://github.com/blocklet/payment-kit.git
16
16
  specVersion: 1.2.8
17
- version: 1.22.24
17
+ version: 1.22.25
18
18
  logo: logo.png
19
19
  files:
20
20
  - dist
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payment-kit",
3
- "version": "1.22.24",
3
+ "version": "1.22.25",
4
4
  "scripts": {
5
5
  "dev": "blocklet dev --open",
6
6
  "lint": "tsc --noEmit && eslint src api/src --ext .mjs,.js,.jsx,.ts,.tsx",
@@ -44,26 +44,26 @@
44
44
  ]
45
45
  },
46
46
  "dependencies": {
47
- "@abtnode/cron": "^1.17.3-beta-20251126-121502-d0926972",
47
+ "@abtnode/cron": "^1.17.3",
48
48
  "@arcblock/did": "^1.27.12",
49
- "@arcblock/did-connect-react": "^3.2.10",
49
+ "@arcblock/did-connect-react": "^3.2.11",
50
50
  "@arcblock/did-connect-storage-nedb": "^1.8.0",
51
51
  "@arcblock/did-util": "^1.27.12",
52
52
  "@arcblock/jwt": "^1.27.12",
53
- "@arcblock/react-hooks": "^3.2.10",
54
- "@arcblock/ux": "^3.2.10",
53
+ "@arcblock/react-hooks": "^3.2.11",
54
+ "@arcblock/ux": "^3.2.11",
55
55
  "@arcblock/validator": "^1.27.12",
56
56
  "@blocklet/did-space-js": "^1.2.6",
57
57
  "@blocklet/error": "^0.3.3",
58
- "@blocklet/js-sdk": "^1.17.3-beta-20251126-121502-d0926972",
59
- "@blocklet/logger": "^1.17.3-beta-20251126-121502-d0926972",
60
- "@blocklet/payment-broker-client": "1.22.24",
61
- "@blocklet/payment-react": "1.22.24",
62
- "@blocklet/payment-vendor": "1.22.24",
63
- "@blocklet/sdk": "^1.17.3-beta-20251126-121502-d0926972",
64
- "@blocklet/ui-react": "^3.2.10",
65
- "@blocklet/uploader": "^0.3.12",
66
- "@blocklet/xss": "^0.3.10",
58
+ "@blocklet/js-sdk": "^1.17.3",
59
+ "@blocklet/logger": "^1.17.3",
60
+ "@blocklet/payment-broker-client": "1.22.25",
61
+ "@blocklet/payment-react": "1.22.25",
62
+ "@blocklet/payment-vendor": "1.22.25",
63
+ "@blocklet/sdk": "^1.17.3",
64
+ "@blocklet/ui-react": "^3.2.11",
65
+ "@blocklet/uploader": "^0.3.13",
66
+ "@blocklet/xss": "^0.3.11",
67
67
  "@mui/icons-material": "^7.1.2",
68
68
  "@mui/lab": "7.0.0-beta.14",
69
69
  "@mui/material": "^7.1.2",
@@ -127,9 +127,9 @@
127
127
  "web3": "^4.16.0"
128
128
  },
129
129
  "devDependencies": {
130
- "@abtnode/types": "^1.17.3-beta-20251126-121502-d0926972",
130
+ "@abtnode/types": "^1.17.3",
131
131
  "@arcblock/eslint-config-ts": "^0.3.3",
132
- "@blocklet/payment-types": "1.22.24",
132
+ "@blocklet/payment-types": "1.22.25",
133
133
  "@types/cookie-parser": "^1.4.9",
134
134
  "@types/cors": "^2.8.19",
135
135
  "@types/debug": "^4.1.12",
@@ -176,5 +176,5 @@
176
176
  "parser": "typescript"
177
177
  }
178
178
  },
179
- "gitHead": "d24ce471f0bdbf4a0ccaaa5317d4c68b4ddfd3bf"
179
+ "gitHead": "59a2dd22163f544db386b492276c8a1da6e3ebe2"
180
180
  }