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.
package/api/src/libs/env.ts
CHANGED
|
@@ -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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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
|
|
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);
|