payment-kit 1.23.8 → 1.23.9

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.
@@ -43,6 +43,13 @@ export const sequelizeOptionsPoolIdle: number = process.env.SEQUELIZE_OPTIONS_PO
43
43
  export const updateDataConcurrency: number = process.env.UPDATE_DATA_CONCURRENCY
44
44
  ? +process.env.UPDATE_DATA_CONCURRENCY
45
45
  : 5; // 默认并发数为 5
46
+
47
+ // System-level maximum pending amount limit (in token format, e.g., "10")
48
+ // Default is 0 (disabled). Set PAYMENT_KIT_MAX_PENDING_AMOUNT to enable this limit.
49
+ export const systemMaxPendingAmount: number = process.env.PAYMENT_KIT_MAX_PENDING_AMOUNT
50
+ ? +process.env.PAYMENT_KIT_MAX_PENDING_AMOUNT
51
+ : 5;
52
+
46
53
  export default {
47
54
  ...env,
48
55
  };
@@ -1555,8 +1555,6 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
1555
1555
  });
1556
1556
 
1557
1557
  try {
1558
- // eslint-disable-next-line no-console
1559
- console.time('updateUserAddress');
1560
1558
  await blocklet.updateUserAddress(
1561
1559
  {
1562
1560
  did: customer.did,
@@ -1570,8 +1568,6 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
1570
1568
  },
1571
1569
  }
1572
1570
  );
1573
- // eslint-disable-next-line no-console
1574
- console.timeEnd('updateUserAddress');
1575
1571
  logger.info('updateUserAddress success', {
1576
1572
  did: customer.did,
1577
1573
  });
@@ -25,6 +25,8 @@ import { blocklet } from '../libs/auth';
25
25
  import { formatMetadata } from '../libs/util';
26
26
  import { getPriceUintAmountByCurrency } from '../libs/price';
27
27
  import { checkTokenBalance } from '../libs/payment';
28
+ import { trimDecimals } from '../libs/math-utils';
29
+ import { systemMaxPendingAmount } from '../libs/env';
28
30
 
29
31
  const router = Router();
30
32
  const auth = authenticate<CreditGrant>({ component: true, roles: ['owner', 'admin'] });
@@ -167,6 +169,10 @@ const checkAutoRechargeSchema = Joi.object({
167
169
  customer_id: Joi.string().required(),
168
170
  currency_id: Joi.string().required(),
169
171
  pending_amount: Joi.string().optional(),
172
+ max_recharge_times: Joi.number().integer().min(1).max(10).optional().default(3),
173
+ max_pending_amount: Joi.string()
174
+ .pattern(/^\d+(\.\d+)?$/)
175
+ .optional(),
170
176
  });
171
177
 
172
178
  router.get('/verify-availability', authMine, async (req, res) => {
@@ -255,6 +261,7 @@ router.get('/verify-availability', authMine, async (req, res) => {
255
261
  });
256
262
  }
257
263
 
264
+ // totalAmount: payment amount required for one auto recharge (priceAmount * quantity)
258
265
  const totalAmount = new BN(priceAmount).mul(new BN(config.quantity ?? 1));
259
266
 
260
267
  // 4. Get pending amount if not provided
@@ -267,46 +274,45 @@ router.get('/verify-availability', authMine, async (req, res) => {
267
274
  pendingAmount = pendingSummary?.[currencyId] || '0';
268
275
  }
269
276
 
270
- // 5. Check daily limit
271
277
  const pendingAmountBN = new BN(pendingAmount);
272
- const today = new Date().toISOString().split('T')[0];
273
- const isNewDay = config.last_recharge_date !== today;
274
-
275
- // Calculate required recharge times: if pendingAmount > 0, calculate needed times; otherwise check if at least one recharge is possible
276
- const requiredRechargeTimes = pendingAmountBN.gt(new BN(0))
277
- ? pendingAmountBN.add(totalAmount).sub(new BN(1)).div(totalAmount).toNumber()
278
- : 1;
279
-
280
- if (!isNewDay && config.daily_stats && config.daily_limits) {
281
- // Check attempt limit
282
- const maxAttempts = Number(config.daily_limits.max_attempts);
283
- if (maxAttempts > 0) {
284
- const remainingAttempts = maxAttempts - Number(config.daily_stats.attempt_count);
285
- if (requiredRechargeTimes > remainingAttempts) {
286
- return res.json({
287
- can_continue: false,
288
- reason: 'daily_limit_reached',
289
- detail: 'attempt_limit_exceeded',
290
- });
291
- }
278
+
279
+ // 5. Check system-level maximum pending amount limit (highest priority, cannot be bypassed)
280
+ // systemMaxPendingAmount is configured via PAYMENT_KIT_MAX_PENDING_AMOUNT (in token format)
281
+ if (systemMaxPendingAmount > 0 && pendingAmountBN.gt(new BN(0))) {
282
+ const systemMaxPendingAmountBN = fromTokenToUnit(
283
+ trimDecimals(String(systemMaxPendingAmount), currency.decimal),
284
+ currency.decimal
285
+ );
286
+ if (pendingAmountBN.gt(systemMaxPendingAmountBN)) {
287
+ return res.json({
288
+ can_continue: false,
289
+ reason: 'system_pending_limit_exceeded',
290
+ pending_amount: pendingAmount,
291
+ system_max_pending_amount: systemMaxPendingAmountBN.toString(),
292
+ detail: 'Pending amount exceeds system maximum limit configured in Payment Kit',
293
+ });
292
294
  }
295
+ }
293
296
 
294
- // Check amount limit
295
- const maxAmount = new BN(config.daily_limits.max_amount || '0');
296
- if (maxAmount.gt(new BN(0))) {
297
- const requiredTotalAmount = totalAmount.mul(new BN(requiredRechargeTimes));
298
- const remainingAmount = maxAmount.sub(new BN(config.daily_stats.total_amount || '0'));
299
- if (requiredTotalAmount.gt(remainingAmount)) {
300
- return res.json({
301
- can_continue: false,
302
- reason: 'daily_limit_reached',
303
- detail: 'amount_limit_exceeded',
304
- });
305
- }
297
+ // 6. Check caller-specified max pending amount limit (if provided)
298
+ // Note: max_pending_amount is in token format (e.g., "100"), need to convert to unit format for comparison
299
+ if (value.max_pending_amount && pendingAmountBN.gt(new BN(0))) {
300
+ const maxPendingAmountBN = fromTokenToUnit(
301
+ trimDecimals(value.max_pending_amount, currency.decimal),
302
+ currency.decimal
303
+ );
304
+ if (pendingAmountBN.gt(maxPendingAmountBN)) {
305
+ return res.json({
306
+ can_continue: false,
307
+ reason: 'pending_amount_exceeds_limit',
308
+ pending_amount: pendingAmount,
309
+ max_pending_amount: maxPendingAmountBN.toString(),
310
+ detail: 'Current pending amount exceeds the maximum allowed limit',
311
+ });
306
312
  }
307
313
  }
308
314
 
309
- // 6. Check payment account balance
315
+ // 7. Get payer
310
316
  const payer =
311
317
  config.payment_settings?.payment_method_options?.[
312
318
  config.paymentMethod.type as keyof typeof config.payment_settings.payment_method_options
@@ -319,29 +325,130 @@ router.get('/verify-availability', authMine, async (req, res) => {
319
325
  });
320
326
  }
321
327
 
322
- // Check token balance: if pendingAmount > 0, check if balance can cover pending; otherwise check if balance can cover at least one recharge
323
- const amountToCheck = pendingAmountBN.gt(new BN(0)) ? pendingAmount : totalAmount.toString();
324
- const balanceResult = await checkTokenBalance({
325
- paymentMethod: config.paymentMethod,
326
- paymentCurrency: config.rechargeCurrency,
327
- userDid: payer,
328
- amount: amountToCheck,
329
- skipUserCheck: true,
330
- });
328
+ let balanceResult: { sufficient: boolean; token?: { balance: string } } | null = null;
329
+ // 8. If user has pending amount, check if they can pay it off
330
+ if (pendingAmountBN.gt(new BN(0))) {
331
+ // Get credit amount per recharge from price metadata
332
+ const creditConfig = config.price.metadata?.credit_config;
333
+ if (!creditConfig || !creditConfig.credit_amount) {
334
+ return res.json({
335
+ can_continue: false,
336
+ reason: 'credit_config_not_found',
337
+ });
338
+ }
331
339
 
332
- if (!balanceResult.sufficient) {
333
- return res.json({
334
- can_continue: false,
335
- reason: 'insufficient_balance',
336
- payment_account_balance: balanceResult.token?.balance || '0',
337
- pending_amount: pendingAmount,
340
+ // Convert credit_amount from token to unit, then multiply by quantity
341
+ // creditAmountPerRecharge: credits amount obtained from one auto recharge
342
+ const creditAmountPerUnit = fromTokenToUnit(creditConfig.credit_amount, currency.decimal);
343
+ const creditAmountPerRecharge = new BN(creditAmountPerUnit).mul(new BN(config.quantity ?? 1));
344
+
345
+ // Calculate how many recharges are needed to pay off the pending amount
346
+ // Formula: ceil(pendingAmount / creditAmountPerRecharge)
347
+ const requiredRechargeTimesBN = pendingAmountBN
348
+ .add(creditAmountPerRecharge)
349
+ .sub(new BN(1))
350
+ .div(creditAmountPerRecharge);
351
+
352
+ // Check if required recharge times exceeds limit
353
+ const maxRechargeTimes = value.max_recharge_times;
354
+ if (requiredRechargeTimesBN.gt(new BN(maxRechargeTimes))) {
355
+ return res.json({
356
+ can_continue: false,
357
+ reason: 'too_many_recharges_required',
358
+ pending_amount: pendingAmount,
359
+ required_recharge_times: requiredRechargeTimesBN.toString(),
360
+ max_allowed_times: maxRechargeTimes,
361
+ });
362
+ }
363
+
364
+ // Calculate required payment amount to pay off pending
365
+ // requiredPaymentAmount = totalAmount (one recharge cost) * requiredRechargeTimes
366
+ const requiredPaymentAmount = totalAmount.mul(requiredRechargeTimesBN);
367
+
368
+ // Check daily limit
369
+ const today = new Date().toISOString().split('T')[0];
370
+ const isNewDay = config.last_recharge_date !== today;
371
+
372
+ if (!isNewDay && config.daily_stats && config.daily_limits) {
373
+ const { max_attempts: maxAttemptsRaw, max_amount: maxAmountRaw } = config.daily_limits;
374
+ const { attempt_count: attemptCount, total_amount: totalAmountStats } = config.daily_stats;
375
+
376
+ // Check attempt limit
377
+ const maxAttempts = Number(maxAttemptsRaw);
378
+ if (maxAttempts > 0) {
379
+ // Safe to convert to number since requiredRechargeTimesBN is already checked to be <= 3
380
+ const requiredRechargeTimes = requiredRechargeTimesBN.toNumber();
381
+ const remainingAttempts = maxAttempts - Number(attemptCount);
382
+ if (requiredRechargeTimes > remainingAttempts) {
383
+ return res.json({
384
+ can_continue: false,
385
+ reason: 'daily_limit_reached',
386
+ detail: 'attempt_limit_exceeded',
387
+ });
388
+ }
389
+ }
390
+
391
+ // Check amount limit
392
+ const maxAmount = new BN(maxAmountRaw || '0');
393
+ if (maxAmount.gt(new BN(0))) {
394
+ const remainingAmount = maxAmount.sub(new BN(totalAmountStats || '0'));
395
+ if (requiredPaymentAmount.gt(remainingAmount)) {
396
+ return res.json({
397
+ can_continue: false,
398
+ reason: 'daily_limit_reached',
399
+ detail: 'amount_limit_exceeded',
400
+ });
401
+ }
402
+ }
403
+ }
404
+
405
+ // Check payment account balance: balance must be sufficient to pay off pending AND cover at least one more recharge
406
+ // This ensures user can pay off pending amount and still have balance for continued usage
407
+ // minimumRequiredBalance = requiredPaymentAmount (to pay off pending) + totalAmount (for one more recharge)
408
+ const minimumRequiredBalance = requiredPaymentAmount.add(totalAmount);
409
+ balanceResult = await checkTokenBalance({
410
+ paymentMethod: config.paymentMethod,
411
+ paymentCurrency: config.rechargeCurrency,
412
+ userDid: payer,
413
+ amount: minimumRequiredBalance.toString(),
414
+ skipUserCheck: true,
338
415
  });
416
+
417
+ if (!balanceResult.sufficient) {
418
+ return res.json({
419
+ can_continue: false,
420
+ reason: 'insufficient_balance',
421
+ payment_account_balance: balanceResult.token?.balance || '0',
422
+ pending_amount: pendingAmount,
423
+ required_amount: minimumRequiredBalance.toString(),
424
+ detail: 'balance_must_cover_pending_plus_one_recharge',
425
+ });
426
+ }
427
+ } else {
428
+ // No pending amount: check if balance can cover at least one recharge
429
+ balanceResult = await checkTokenBalance({
430
+ paymentMethod: config.paymentMethod,
431
+ paymentCurrency: config.rechargeCurrency,
432
+ userDid: payer,
433
+ amount: totalAmount.toString(),
434
+ skipUserCheck: true,
435
+ });
436
+
437
+ if (!balanceResult.sufficient) {
438
+ return res.json({
439
+ can_continue: false,
440
+ reason: 'insufficient_balance',
441
+ payment_account_balance: balanceResult.token?.balance || '0',
442
+ pending_amount: pendingAmount,
443
+ required_amount: totalAmount.toString(),
444
+ });
445
+ }
339
446
  }
340
447
 
341
448
  return res.json({
342
449
  can_continue: true,
343
450
  payment_account_sufficient: true,
344
- payment_account_balance: balanceResult.token?.balance || '0',
451
+ payment_account_balance: balanceResult?.token?.balance || '0',
345
452
  pending_amount: pendingAmount,
346
453
  });
347
454
  } catch (err: any) {
@@ -59,12 +59,14 @@ const listSchema = createListParamSchema<{
59
59
  customer_id?: string;
60
60
  start?: number;
61
61
  end?: number;
62
+ status?: string;
62
63
  }>({
63
64
  event_name: Joi.string().empty(''),
64
65
  meter_id: Joi.string().empty(''),
65
66
  customer_id: Joi.string().empty(''),
66
67
  start: Joi.number().integer().optional(),
67
68
  end: Joi.number().integer().optional(),
69
+ status: Joi.string().empty(''),
68
70
  });
69
71
 
70
72
  const statsSchema = Joi.object({
@@ -111,6 +113,15 @@ router.get('/', authMine, async (req, res) => {
111
113
  }
112
114
  }
113
115
 
116
+ if (query.status) {
117
+ where.status = {
118
+ [Op.in]: query.status
119
+ ?.split(',')
120
+ .map((x) => x.trim())
121
+ .filter(Boolean),
122
+ };
123
+ }
124
+
114
125
  const { rows: list, count } = await MeterEvent.findAndCountAll({
115
126
  where,
116
127
  order: getOrder(query, [['created_at', query.o === 'asc' ? 'ASC' : 'DESC']]),
@@ -335,11 +346,7 @@ router.get('/pending-amount', authMine, async (req, res) => {
335
346
  }
336
347
  params.customerId = customer.id;
337
348
  }
338
- // eslint-disable-next-line no-console
339
- console.time('pending-amount: getPendingAmounts');
340
349
  const [summary] = await MeterEvent.getPendingAmounts(params);
341
- // eslint-disable-next-line no-console
342
- console.timeEnd('pending-amount: getPendingAmounts');
343
350
  return res.json(summary);
344
351
  } catch (err) {
345
352
  logger.error('Error getting meter event pending amount', err);