payment-kit 1.22.32 → 1.23.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.
- package/api/src/index.ts +4 -0
- package/api/src/integrations/arcblock/token.ts +599 -0
- package/api/src/libs/credit-grant.ts +7 -6
- package/api/src/libs/util.ts +34 -0
- package/api/src/queues/credit-consume.ts +29 -4
- package/api/src/queues/credit-grant.ts +245 -50
- package/api/src/queues/credit-reconciliation.ts +253 -0
- package/api/src/queues/refund.ts +263 -30
- package/api/src/queues/token-transfer.ts +331 -0
- package/api/src/routes/checkout-sessions.ts +94 -29
- package/api/src/routes/credit-grants.ts +35 -9
- package/api/src/routes/credit-tokens.ts +38 -0
- package/api/src/routes/credit-transactions.ts +20 -3
- package/api/src/routes/index.ts +2 -0
- package/api/src/routes/meter-events.ts +4 -0
- package/api/src/routes/meters.ts +32 -10
- package/api/src/routes/payment-currencies.ts +103 -0
- package/api/src/routes/payment-links.ts +3 -1
- package/api/src/routes/products.ts +2 -2
- package/api/src/routes/settings.ts +4 -3
- package/api/src/store/migrations/20251120-add-token-config-to-currencies.ts +20 -0
- package/api/src/store/migrations/20251204-add-chain-fields.ts +74 -0
- package/api/src/store/migrations/20251211-optimize-slow-queries.ts +33 -0
- package/api/src/store/models/credit-grant.ts +47 -9
- package/api/src/store/models/credit-transaction.ts +18 -1
- package/api/src/store/models/index.ts +2 -1
- package/api/src/store/models/payment-currency.ts +31 -4
- package/api/src/store/models/refund.ts +12 -2
- package/api/src/store/models/types.ts +48 -0
- package/api/src/store/sequelize.ts +1 -0
- package/api/third.d.ts +2 -0
- package/blocklet.yml +1 -1
- package/package.json +7 -6
- package/src/app.tsx +10 -0
- package/src/components/customer/credit-overview.tsx +19 -3
- package/src/components/meter/form.tsx +191 -18
- package/src/components/price/form.tsx +49 -37
- package/src/locales/en.tsx +25 -1
- package/src/locales/zh.tsx +27 -1
- package/src/pages/admin/billing/meters/create.tsx +42 -13
- package/src/pages/admin/billing/meters/detail.tsx +56 -5
- package/src/pages/admin/customers/customers/credit-grant/detail.tsx +13 -0
- package/src/pages/admin/customers/customers/credit-transaction/detail.tsx +324 -0
- package/src/pages/admin/customers/index.tsx +5 -0
- package/src/pages/customer/credit-grant/detail.tsx +14 -1
- package/src/pages/customer/credit-transaction/detail.tsx +289 -0
- package/src/pages/customer/invoice/detail.tsx +1 -1
- package/src/pages/customer/recharge/subscription.tsx +1 -1
- package/src/pages/customer/subscription/detail.tsx +1 -1
package/api/src/routes/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ import autoRechargeConfigs from './auto-recharge-configs';
|
|
|
5
5
|
import checkoutSessions from './checkout-sessions';
|
|
6
6
|
import coupons from './coupons';
|
|
7
7
|
import creditGrants from './credit-grants';
|
|
8
|
+
import creditTokens from './credit-tokens';
|
|
8
9
|
import creditTransactions from './credit-transactions';
|
|
9
10
|
import customers from './customers';
|
|
10
11
|
import donations from './donations';
|
|
@@ -61,6 +62,7 @@ router.use('/auto-recharge-configs', autoRechargeConfigs);
|
|
|
61
62
|
router.use('/checkout-sessions', checkoutSessions);
|
|
62
63
|
router.use('/coupons', coupons);
|
|
63
64
|
router.use('/credit-grants', creditGrants);
|
|
65
|
+
router.use('/credit-tokens', creditTokens);
|
|
64
66
|
router.use('/credit-transactions', creditTransactions);
|
|
65
67
|
router.use('/customers', customers);
|
|
66
68
|
router.use('/donations', donations);
|
|
@@ -335,7 +335,11 @@ router.get('/pending-amount', authMine, async (req, res) => {
|
|
|
335
335
|
}
|
|
336
336
|
params.customerId = customer.id;
|
|
337
337
|
}
|
|
338
|
+
// eslint-disable-next-line no-console
|
|
339
|
+
console.time('pending-amount: getPendingAmounts');
|
|
338
340
|
const [summary] = await MeterEvent.getPendingAmounts(params);
|
|
341
|
+
// eslint-disable-next-line no-console
|
|
342
|
+
console.timeEnd('pending-amount: getPendingAmounts');
|
|
339
343
|
return res.json(summary);
|
|
340
344
|
} catch (err) {
|
|
341
345
|
logger.error('Error getting meter event pending amount', err);
|
package/api/src/routes/meters.ts
CHANGED
|
@@ -18,9 +18,13 @@ const meterSchema = Joi.object({
|
|
|
18
18
|
aggregation_method: Joi.string().valid('sum', 'count', 'last').default('sum'),
|
|
19
19
|
unit: Joi.string().max(32).required(),
|
|
20
20
|
currency_id: Joi.string().max(40).optional(),
|
|
21
|
+
decimal: Joi.number().integer().min(2).max(18).default(10),
|
|
21
22
|
description: Joi.string().max(255).allow('').optional(),
|
|
22
23
|
metadata: MetadataSchema,
|
|
23
24
|
component_did: Joi.string().max(40).optional(),
|
|
25
|
+
token: Joi.object({
|
|
26
|
+
tokenFactoryAddress: Joi.string().required(),
|
|
27
|
+
}).optional(),
|
|
24
28
|
}).unknown(true);
|
|
25
29
|
|
|
26
30
|
const updateMeterSchema = Joi.object({
|
|
@@ -78,6 +82,32 @@ router.post('/', auth, async (req, res) => {
|
|
|
78
82
|
return res.status(400).json({ error: 'Aggregation method is not supported' });
|
|
79
83
|
}
|
|
80
84
|
|
|
85
|
+
const needArcblockMethod = req.body.token?.tokenFactoryAddress || !req.body.currency_id;
|
|
86
|
+
const arcblockMethod = needArcblockMethod
|
|
87
|
+
? await PaymentMethod.findOne({ where: { livemode: !!req.livemode, type: 'arcblock' } })
|
|
88
|
+
: null;
|
|
89
|
+
if (needArcblockMethod && !arcblockMethod) {
|
|
90
|
+
throw new Error('ArcBlock payment method not found');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let tokenConfig: Record<string, any> | undefined;
|
|
94
|
+
if (req.body.token?.tokenFactoryAddress) {
|
|
95
|
+
const client = arcblockMethod!.getOcapClient();
|
|
96
|
+
const { state: tokenFactoryState } = await client.getTokenFactoryState({
|
|
97
|
+
address: req.body.token.tokenFactoryAddress,
|
|
98
|
+
});
|
|
99
|
+
if (!tokenFactoryState) {
|
|
100
|
+
return res.status(400).json({ error: 'Token factory not found on chain' });
|
|
101
|
+
}
|
|
102
|
+
tokenConfig = {
|
|
103
|
+
address: tokenFactoryState.token.address,
|
|
104
|
+
symbol: tokenFactoryState.token.symbol,
|
|
105
|
+
name: tokenFactoryState.token.name,
|
|
106
|
+
decimal: tokenFactoryState.token.decimal,
|
|
107
|
+
token_factory_address: tokenFactoryState.address,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
81
111
|
const meterData = {
|
|
82
112
|
...pick(req.body, ['name', 'event_name', 'aggregation_method', 'unit', 'currency_id', 'description', 'metadata']),
|
|
83
113
|
livemode: !!req.livemode,
|
|
@@ -87,17 +117,9 @@ router.post('/', auth, async (req, res) => {
|
|
|
87
117
|
};
|
|
88
118
|
|
|
89
119
|
if (!meterData.currency_id) {
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
livemode: !!req.livemode,
|
|
93
|
-
type: 'arcblock',
|
|
94
|
-
},
|
|
120
|
+
const paymentCurrency = await PaymentCurrency.createForMeter(meterData, arcblockMethod!.id, tokenConfig, {
|
|
121
|
+
decimal: req.body.decimal,
|
|
95
122
|
});
|
|
96
|
-
if (!paymentMethod) {
|
|
97
|
-
return res.status(400).json({ error: 'Payment method not found' });
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const paymentCurrency = await PaymentCurrency.createForMeter(meterData, paymentMethod.id);
|
|
101
123
|
meterData.currency_id = paymentCurrency.id;
|
|
102
124
|
}
|
|
103
125
|
|
|
@@ -351,6 +351,109 @@ router.put('/:id', auth, async (req, res) => {
|
|
|
351
351
|
return res.json(updatedCurrency);
|
|
352
352
|
});
|
|
353
353
|
|
|
354
|
+
const tokenConfigSchema = Joi.object({
|
|
355
|
+
token_factory_address: Joi.string().required(),
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
router.put('/:id/token-config', auth, async (req, res) => {
|
|
359
|
+
try {
|
|
360
|
+
const { id } = req.params;
|
|
361
|
+
|
|
362
|
+
const { error, value } = tokenConfigSchema.validate(req.body);
|
|
363
|
+
if (error) {
|
|
364
|
+
return res.status(400).json({ error: error.message });
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const currency = await PaymentCurrency.findByPk(id);
|
|
368
|
+
if (!currency) {
|
|
369
|
+
return res.status(404).json({ error: 'Payment currency not found' });
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (currency.type !== 'credit') {
|
|
373
|
+
return res.status(400).json({ error: 'Only credit currencies can have token_config' });
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (currency.token_config) {
|
|
377
|
+
return res.status(400).json({ error: 'Token config already exists. Cannot be updated once set.' });
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const paymentMethod = await PaymentMethod.findOne({
|
|
381
|
+
where: {
|
|
382
|
+
livemode: currency.livemode,
|
|
383
|
+
type: 'arcblock',
|
|
384
|
+
},
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
if (!paymentMethod) {
|
|
388
|
+
return res.status(400).json({ error: 'ArcBlock payment method not found' });
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const client = paymentMethod.getOcapClient();
|
|
392
|
+
const { state: tokenFactoryState } = await client.getTokenFactoryState({
|
|
393
|
+
address: value.token_factory_address,
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
if (!tokenFactoryState) {
|
|
397
|
+
return res.status(400).json({ error: 'Token factory not found on chain' });
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const tokenConfig = {
|
|
401
|
+
address: tokenFactoryState.token.address,
|
|
402
|
+
symbol: tokenFactoryState.token.symbol,
|
|
403
|
+
name: tokenFactoryState.token.name,
|
|
404
|
+
decimal: tokenFactoryState.token.decimal,
|
|
405
|
+
token_factory_address: tokenFactoryState.address,
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
// Only update token_config, keep the original decimal to avoid breaking existing credit grants
|
|
409
|
+
await currency.update({
|
|
410
|
+
token_config: tokenConfig,
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
logger.info('Payment currency token_config updated', {
|
|
414
|
+
currencyId: id,
|
|
415
|
+
tokenConfig,
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
return res.json(currency.toJSON());
|
|
419
|
+
} catch (err) {
|
|
420
|
+
logger.error('update payment currency token_config failed', { error: err?.message, id: req.params.id });
|
|
421
|
+
return res.status(400).json({ error: err?.message });
|
|
422
|
+
}
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
router.delete('/:id/token-config', auth, async (req, res) => {
|
|
426
|
+
try {
|
|
427
|
+
const { id } = req.params;
|
|
428
|
+
|
|
429
|
+
const currency = await PaymentCurrency.findByPk(id);
|
|
430
|
+
if (!currency) {
|
|
431
|
+
return res.status(404).json({ error: 'Payment currency not found' });
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (currency.type !== 'credit') {
|
|
435
|
+
return res.status(400).json({ error: 'Only credit currencies can have token_config' });
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (!currency.token_config) {
|
|
439
|
+
return res.status(400).json({ error: 'Token config does not exist' });
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
await currency.update({
|
|
443
|
+
token_config: null,
|
|
444
|
+
});
|
|
445
|
+
|
|
446
|
+
logger.info('Payment currency token_config removed', {
|
|
447
|
+
currencyId: id,
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
return res.json(currency.toJSON());
|
|
451
|
+
} catch (err) {
|
|
452
|
+
logger.error('delete payment currency token_config failed', { error: err?.message, id: req.params.id });
|
|
453
|
+
return res.status(400).json({ error: err?.message });
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
|
|
354
457
|
router.delete('/:id', auth, async (req, res) => {
|
|
355
458
|
const { id } = req.params;
|
|
356
459
|
|
|
@@ -260,10 +260,12 @@ router.get('/', auth, async (req, res) => {
|
|
|
260
260
|
const priceIds: string[] = list.reduce((acc: string[], x) => acc.concat(x.line_items.map((i) => i.price_id)), []);
|
|
261
261
|
const prices = await Price.findAll({ where: { id: priceIds }, include: [{ model: Product, as: 'product' }] });
|
|
262
262
|
|
|
263
|
+
const priceMap = new Map(prices.map((p) => [p.id, p]));
|
|
264
|
+
|
|
263
265
|
list.forEach((x) => {
|
|
264
266
|
x.line_items.forEach((i) => {
|
|
265
267
|
// @ts-ignore
|
|
266
|
-
i.price =
|
|
268
|
+
i.price = priceMap.get(i.price_id);
|
|
267
269
|
});
|
|
268
270
|
});
|
|
269
271
|
|
|
@@ -37,7 +37,7 @@ const ProductAndPriceSchema = Joi.object({
|
|
|
37
37
|
description: Joi.string().max(250).empty('').optional(),
|
|
38
38
|
images: Joi.any().optional(),
|
|
39
39
|
metadata: MetadataSchema,
|
|
40
|
-
tax_code: Joi.string().max(30).empty('').optional(),
|
|
40
|
+
tax_code: Joi.string().max(30).allow(null).empty('').optional(),
|
|
41
41
|
statement_descriptor: Joi.string()
|
|
42
42
|
.max(22)
|
|
43
43
|
.pattern(/^(?=.*[A-Za-z])[^\u4e00-\u9fa5<>"’\\]*$/)
|
|
@@ -48,7 +48,7 @@ const ProductAndPriceSchema = Joi.object({
|
|
|
48
48
|
.allow(null, '')
|
|
49
49
|
.empty('')
|
|
50
50
|
.optional(),
|
|
51
|
-
unit_label: Joi.string().max(12).empty('').optional(),
|
|
51
|
+
unit_label: Joi.string().max(12).allow(null).empty('').optional(),
|
|
52
52
|
nft_factory: Joi.string().max(40).allow(null).empty('').optional(),
|
|
53
53
|
features: Joi.array()
|
|
54
54
|
.items(Joi.object({ name: Joi.string().max(64).empty('').optional() }).unknown(true))
|
|
@@ -31,9 +31,10 @@ router.get('/', async (req, res) => {
|
|
|
31
31
|
});
|
|
32
32
|
|
|
33
33
|
res.json({
|
|
34
|
-
paymentMethods: methods.map((x) =>
|
|
35
|
-
pick(x, ['id', 'name', 'type', 'logo', 'payment_currencies', 'default_currency_id', 'maximum_precision'])
|
|
36
|
-
|
|
34
|
+
paymentMethods: methods.map((x) => ({
|
|
35
|
+
...pick(x, ['id', 'name', 'type', 'logo', 'payment_currencies', 'default_currency_id', 'maximum_precision']),
|
|
36
|
+
api_host: x.settings?.arcblock?.api_host,
|
|
37
|
+
})),
|
|
37
38
|
baseCurrency: await PaymentCurrency.findOne({
|
|
38
39
|
where: { is_base_currency: true, livemode: req.livemode },
|
|
39
40
|
attributes,
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { DataTypes } from 'sequelize';
|
|
2
|
+
import { safeApplyColumnChanges, type Migration } from '../migrate';
|
|
3
|
+
|
|
4
|
+
export const up: Migration = async ({ context }) => {
|
|
5
|
+
await safeApplyColumnChanges(context, {
|
|
6
|
+
payment_currencies: [
|
|
7
|
+
{
|
|
8
|
+
name: 'token_config',
|
|
9
|
+
field: {
|
|
10
|
+
type: DataTypes.JSON,
|
|
11
|
+
allowNull: true,
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
],
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const down: Migration = async ({ context }) => {
|
|
19
|
+
await context.removeColumn('payment_currencies', 'token_config');
|
|
20
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { DataTypes } from 'sequelize';
|
|
2
|
+
import { createIndexIfNotExists, safeApplyColumnChanges, type Migration } from '../migrate';
|
|
3
|
+
|
|
4
|
+
export const up: Migration = async ({ context }) => {
|
|
5
|
+
// Add new columns to credit_transactions table
|
|
6
|
+
await safeApplyColumnChanges(context, {
|
|
7
|
+
credit_transactions: [
|
|
8
|
+
{
|
|
9
|
+
name: 'transfer_status',
|
|
10
|
+
field: {
|
|
11
|
+
type: DataTypes.ENUM('pending', 'completed', 'failed'),
|
|
12
|
+
allowNull: true,
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
name: 'transfer_hash',
|
|
17
|
+
field: {
|
|
18
|
+
type: DataTypes.STRING(255),
|
|
19
|
+
allowNull: true,
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Add new columns to credit_grants table for on-chain operations
|
|
26
|
+
await safeApplyColumnChanges(context, {
|
|
27
|
+
credit_grants: [
|
|
28
|
+
{
|
|
29
|
+
name: 'chain_status',
|
|
30
|
+
field: {
|
|
31
|
+
type: DataTypes.ENUM(
|
|
32
|
+
'mint_pending',
|
|
33
|
+
'mint_completed',
|
|
34
|
+
'mint_failed',
|
|
35
|
+
'burn_pending',
|
|
36
|
+
'burn_completed',
|
|
37
|
+
'burn_failed'
|
|
38
|
+
),
|
|
39
|
+
allowNull: true,
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'chain_detail',
|
|
44
|
+
field: {
|
|
45
|
+
type: DataTypes.JSON,
|
|
46
|
+
allowNull: true,
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Add indexes for efficient querying
|
|
53
|
+
await createIndexIfNotExists(
|
|
54
|
+
context,
|
|
55
|
+
'credit_transactions',
|
|
56
|
+
['transfer_status'],
|
|
57
|
+
'idx_credit_transactions_transfer_status'
|
|
58
|
+
);
|
|
59
|
+
await createIndexIfNotExists(context, 'credit_grants', ['chain_status'], 'idx_credit_grants_chain_status');
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const down: Migration = async ({ context }) => {
|
|
63
|
+
// Remove indexes
|
|
64
|
+
await context.removeIndex('credit_transactions', 'idx_credit_transactions_transfer_status');
|
|
65
|
+
await context.removeIndex('credit_grants', 'idx_credit_grants_chain_status');
|
|
66
|
+
|
|
67
|
+
// Remove columns from credit_transactions
|
|
68
|
+
await context.removeColumn('credit_transactions', 'transfer_hash');
|
|
69
|
+
await context.removeColumn('credit_transactions', 'transfer_status');
|
|
70
|
+
|
|
71
|
+
// Remove columns from credit_grants
|
|
72
|
+
await context.removeColumn('credit_grants', 'chain_detail');
|
|
73
|
+
await context.removeColumn('credit_grants', 'chain_status');
|
|
74
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
import { createIndexIfNotExists, type Migration } from '../migrate';
|
|
3
|
+
|
|
4
|
+
export const up: Migration = async ({ context }) => {
|
|
5
|
+
await createIndexIfNotExists(
|
|
6
|
+
context,
|
|
7
|
+
'meter_events',
|
|
8
|
+
['livemode', 'event_name', 'timestamp'],
|
|
9
|
+
'idx_meter_events_livemode_event_timestamp'
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
await createIndexIfNotExists(
|
|
13
|
+
context,
|
|
14
|
+
'payment_currencies',
|
|
15
|
+
['is_base_currency', 'livemode'],
|
|
16
|
+
'idx_payment_currencies_base_livemode'
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
await createIndexIfNotExists(context, 'jobs', ['queue', 'id'], 'idx_jobs_queue_id');
|
|
20
|
+
await createIndexIfNotExists(
|
|
21
|
+
context,
|
|
22
|
+
'jobs',
|
|
23
|
+
['queue', 'cancelled', 'will_run_at', 'delay'],
|
|
24
|
+
'idx_jobs_queue_cancelled_run_at_delay'
|
|
25
|
+
);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const down: Migration = async ({ context }) => {
|
|
29
|
+
await context.removeIndex('meter_events', 'idx_meter_events_livemode_event_timestamp');
|
|
30
|
+
await context.removeIndex('payment_currencies', 'idx_payment_currencies_base_livemode');
|
|
31
|
+
await context.removeIndex('jobs', 'idx_jobs_queue_id');
|
|
32
|
+
await context.removeIndex('jobs', 'idx_jobs_queue_cancelled_run_at_delay');
|
|
33
|
+
};
|
|
@@ -6,9 +6,10 @@ import type { LiteralUnion } from 'type-fest';
|
|
|
6
6
|
import { createEvent } from '../../libs/audit';
|
|
7
7
|
import { createIdGenerator } from '../../libs/util';
|
|
8
8
|
import dayjs from '../../libs/dayjs';
|
|
9
|
-
import { CreditGrantApplicabilityConfig } from './types';
|
|
9
|
+
import { CreditGrantApplicabilityConfig, CreditGrantChainDetail, CreditGrantChainStatus } from './types';
|
|
10
10
|
import logger from '../../libs/logger';
|
|
11
11
|
import { PaymentCurrency, TPaymentCurrency } from './payment-currency';
|
|
12
|
+
import { Meter, TMeter } from './meter';
|
|
12
13
|
|
|
13
14
|
const CREDIT_GRANT_STATUS_EVENTS = {
|
|
14
15
|
depleted: 'depleted',
|
|
@@ -18,6 +19,7 @@ const CREDIT_GRANT_STATUS_EVENTS = {
|
|
|
18
19
|
|
|
19
20
|
type CreditGrantSummary = {
|
|
20
21
|
paymentCurrency: TPaymentCurrency;
|
|
22
|
+
meter?: TMeter;
|
|
21
23
|
totalAmount: string;
|
|
22
24
|
remainingAmount: string;
|
|
23
25
|
grantCount: number;
|
|
@@ -62,6 +64,10 @@ export class CreditGrant extends Model<InferAttributes<CreditGrant>, InferCreati
|
|
|
62
64
|
declare status: LiteralUnion<'pending' | 'granted' | 'depleted' | 'expired' | 'voided', string>;
|
|
63
65
|
declare remaining_amount: string; // 剩余金额
|
|
64
66
|
|
|
67
|
+
// On-chain credit token operation fields
|
|
68
|
+
declare chain_status?: CreditGrantChainStatus;
|
|
69
|
+
declare chain_detail?: CreditGrantChainDetail;
|
|
70
|
+
|
|
65
71
|
// 审计字段
|
|
66
72
|
declare created_by?: string;
|
|
67
73
|
declare updated_by?: string;
|
|
@@ -148,6 +154,21 @@ export class CreditGrant extends Model<InferAttributes<CreditGrant>, InferCreati
|
|
|
148
154
|
type: DataTypes.STRING(32),
|
|
149
155
|
allowNull: false,
|
|
150
156
|
},
|
|
157
|
+
chain_status: {
|
|
158
|
+
type: DataTypes.ENUM(
|
|
159
|
+
'mint_pending',
|
|
160
|
+
'mint_completed',
|
|
161
|
+
'mint_failed',
|
|
162
|
+
'burn_pending',
|
|
163
|
+
'burn_completed',
|
|
164
|
+
'burn_failed'
|
|
165
|
+
),
|
|
166
|
+
allowNull: true,
|
|
167
|
+
},
|
|
168
|
+
chain_detail: {
|
|
169
|
+
type: DataTypes.JSON,
|
|
170
|
+
allowNull: true,
|
|
171
|
+
},
|
|
151
172
|
created_by: {
|
|
152
173
|
type: DataTypes.STRING(40),
|
|
153
174
|
allowNull: true,
|
|
@@ -267,6 +288,16 @@ export class CreditGrant extends Model<InferAttributes<CreditGrant>, InferCreati
|
|
|
267
288
|
return true;
|
|
268
289
|
}
|
|
269
290
|
|
|
291
|
+
// check if Credit Grant has on-chain token (mint completed)
|
|
292
|
+
public hasOnchainToken(): boolean {
|
|
293
|
+
return CreditGrant.hasOnchainToken(this);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// Static version that works with plain objects
|
|
297
|
+
public static hasOnchainToken(grant: { chain_status?: string | null }): boolean {
|
|
298
|
+
return grant.chain_status === 'mint_completed';
|
|
299
|
+
}
|
|
300
|
+
|
|
270
301
|
public static initialize(sequelize: any) {
|
|
271
302
|
this.init(this.GENESIS_ATTRIBUTES, {
|
|
272
303
|
sequelize,
|
|
@@ -282,9 +313,6 @@ export class CreditGrant extends Model<InferAttributes<CreditGrant>, InferCreati
|
|
|
282
313
|
hooks: {
|
|
283
314
|
afterCreate: (model: CreditGrant, options) => {
|
|
284
315
|
createEvent('CreditGrant', 'customer.credit_grant.created', model, options).catch(console.error);
|
|
285
|
-
if (!model.effective_at || model.effective_at <= Math.floor(Date.now() / 1000)) {
|
|
286
|
-
createEvent('CreditGrant', 'customer.credit_grant.granted', model, options).catch(console.error);
|
|
287
|
-
}
|
|
288
316
|
},
|
|
289
317
|
afterUpdate: (model: CreditGrant, options) => {
|
|
290
318
|
createEvent('CreditGrant', 'customer.credit_grant.updated', model, options).catch(console.error);
|
|
@@ -404,10 +432,7 @@ export class CreditGrant extends Model<InferAttributes<CreditGrant>, InferCreati
|
|
|
404
432
|
currencyId?: string[] | string;
|
|
405
433
|
priceIds?: string[];
|
|
406
434
|
}): Promise<Record<string, CreditGrantSummary>> {
|
|
407
|
-
const summary: Record<
|
|
408
|
-
string,
|
|
409
|
-
{ paymentCurrency: TPaymentCurrency; totalAmount: string; remainingAmount: string; grantCount: number }
|
|
410
|
-
> = {};
|
|
435
|
+
const summary: Record<string, CreditGrantSummary> = {};
|
|
411
436
|
|
|
412
437
|
let targetCurrencyIds: string[] = [];
|
|
413
438
|
if (searchCurrencyId) {
|
|
@@ -436,6 +461,16 @@ export class CreditGrant extends Model<InferAttributes<CreditGrant>, InferCreati
|
|
|
436
461
|
});
|
|
437
462
|
const currencyMap = new Map(currencies.map((c) => [c.id, c]));
|
|
438
463
|
|
|
464
|
+
// Query meters for credit currencies
|
|
465
|
+
const meters = await Meter.findAll({
|
|
466
|
+
where: {
|
|
467
|
+
currency_id: {
|
|
468
|
+
[Op.in]: Array.from(new Set(targetCurrencyIds)),
|
|
469
|
+
},
|
|
470
|
+
},
|
|
471
|
+
});
|
|
472
|
+
const meterMap = new Map(meters.map((m) => [m.currency_id, m]));
|
|
473
|
+
|
|
439
474
|
await Promise.all(
|
|
440
475
|
targetCurrencyIds.map(async (currencyId: string) => {
|
|
441
476
|
const paymentCurrency = currencyMap.get(currencyId);
|
|
@@ -446,6 +481,7 @@ export class CreditGrant extends Model<InferAttributes<CreditGrant>, InferCreati
|
|
|
446
481
|
return null;
|
|
447
482
|
}
|
|
448
483
|
|
|
484
|
+
const meter = meterMap.get(currencyId);
|
|
449
485
|
const availableGrants = await this.getAvailableCreditsForCustomer(customerId, currencyId, priceIds);
|
|
450
486
|
|
|
451
487
|
if (availableGrants.length > 0) {
|
|
@@ -459,8 +495,9 @@ export class CreditGrant extends Model<InferAttributes<CreditGrant>, InferCreati
|
|
|
459
495
|
grantCount += 1;
|
|
460
496
|
});
|
|
461
497
|
|
|
462
|
-
const result = {
|
|
498
|
+
const result: CreditGrantSummary = {
|
|
463
499
|
paymentCurrency,
|
|
500
|
+
meter,
|
|
464
501
|
totalAmount,
|
|
465
502
|
remainingAmount,
|
|
466
503
|
grantCount,
|
|
@@ -471,6 +508,7 @@ export class CreditGrant extends Model<InferAttributes<CreditGrant>, InferCreati
|
|
|
471
508
|
}
|
|
472
509
|
summary[currencyId] = {
|
|
473
510
|
paymentCurrency,
|
|
511
|
+
meter,
|
|
474
512
|
totalAmount: '0',
|
|
475
513
|
remainingAmount: '0',
|
|
476
514
|
grantCount: 0,
|
|
@@ -29,6 +29,10 @@ export class CreditTransaction extends Model<
|
|
|
29
29
|
declare description?: string;
|
|
30
30
|
declare metadata?: Record<string, any>;
|
|
31
31
|
|
|
32
|
+
// Token transfer fields
|
|
33
|
+
declare transfer_status?: 'pending' | 'completed' | 'failed' | null;
|
|
34
|
+
declare transfer_hash?: string;
|
|
35
|
+
|
|
32
36
|
declare created_at: CreationOptional<Date>;
|
|
33
37
|
declare updated_at: CreationOptional<Date>;
|
|
34
38
|
|
|
@@ -88,6 +92,14 @@ export class CreditTransaction extends Model<
|
|
|
88
92
|
type: DataTypes.JSON,
|
|
89
93
|
allowNull: true,
|
|
90
94
|
},
|
|
95
|
+
transfer_status: {
|
|
96
|
+
type: DataTypes.ENUM('pending', 'completed', 'failed'),
|
|
97
|
+
allowNull: true,
|
|
98
|
+
},
|
|
99
|
+
transfer_hash: {
|
|
100
|
+
type: DataTypes.STRING(255),
|
|
101
|
+
allowNull: true,
|
|
102
|
+
},
|
|
91
103
|
created_at: {
|
|
92
104
|
type: DataTypes.DATE,
|
|
93
105
|
defaultValue: DataTypes.NOW,
|
|
@@ -107,7 +119,12 @@ export class CreditTransaction extends Model<
|
|
|
107
119
|
tableName: 'credit_transactions',
|
|
108
120
|
createdAt: 'created_at',
|
|
109
121
|
updatedAt: 'updated_at',
|
|
110
|
-
indexes: [
|
|
122
|
+
indexes: [
|
|
123
|
+
{ fields: ['customer_id'] },
|
|
124
|
+
{ fields: ['credit_grant_id'] },
|
|
125
|
+
{ fields: ['source'] },
|
|
126
|
+
{ fields: ['transfer_status'] },
|
|
127
|
+
],
|
|
111
128
|
hooks: {
|
|
112
129
|
afterCreate: (model: CreditTransaction, options) =>
|
|
113
130
|
createEvent('CreditTransaction', 'customer.credit_transaction.created', model, options).catch(console.error),
|
|
@@ -305,9 +305,10 @@ export type TCreditTransactionExpanded = TCreditTransaction & {
|
|
|
305
305
|
customer: TCustomer;
|
|
306
306
|
paymentCurrency: TPaymentCurrency;
|
|
307
307
|
paymentMethod?: TPaymentMethod;
|
|
308
|
-
creditGrant: TCreditGrant;
|
|
308
|
+
creditGrant: TCreditGrant & { paymentCurrency?: TPaymentCurrency };
|
|
309
309
|
meter: TMeter;
|
|
310
310
|
subscription: TSubscription;
|
|
311
|
+
meterEvent?: TMeterEvent;
|
|
311
312
|
};
|
|
312
313
|
|
|
313
314
|
export type TMeterEventExpanded = TMeterEvent & {
|
|
@@ -46,6 +46,7 @@ export class PaymentCurrency extends Model<InferAttributes<PaymentCurrency>, Inf
|
|
|
46
46
|
declare vault_config?: VaultConfig;
|
|
47
47
|
declare type: LiteralUnion<'standard' | 'credit', string>;
|
|
48
48
|
declare recharge_config?: RechargeConfig;
|
|
49
|
+
declare token_config?: Record<string, any> | null;
|
|
49
50
|
|
|
50
51
|
public static readonly GENESIS_ATTRIBUTES = {
|
|
51
52
|
id: {
|
|
@@ -144,6 +145,10 @@ export class PaymentCurrency extends Model<InferAttributes<PaymentCurrency>, Inf
|
|
|
144
145
|
type: DataTypes.JSON,
|
|
145
146
|
allowNull: true,
|
|
146
147
|
},
|
|
148
|
+
token_config: {
|
|
149
|
+
type: DataTypes.JSON,
|
|
150
|
+
allowNull: true,
|
|
151
|
+
},
|
|
147
152
|
},
|
|
148
153
|
{
|
|
149
154
|
sequelize,
|
|
@@ -210,7 +215,12 @@ export class PaymentCurrency extends Model<InferAttributes<PaymentCurrency>, Inf
|
|
|
210
215
|
);
|
|
211
216
|
return count > 0;
|
|
212
217
|
}
|
|
213
|
-
public static async createForMeter(
|
|
218
|
+
public static async createForMeter(
|
|
219
|
+
meter: any,
|
|
220
|
+
paymentMethodId: string,
|
|
221
|
+
tokenConfig?: Record<string, any>,
|
|
222
|
+
options?: { decimal?: number }
|
|
223
|
+
) {
|
|
214
224
|
const existingCurrency = await this.findOne({
|
|
215
225
|
where: {
|
|
216
226
|
type: 'credit',
|
|
@@ -222,6 +232,8 @@ export class PaymentCurrency extends Model<InferAttributes<PaymentCurrency>, Inf
|
|
|
222
232
|
if (existingCurrency) {
|
|
223
233
|
return existingCurrency;
|
|
224
234
|
}
|
|
235
|
+
|
|
236
|
+
const decimal = options?.decimal ?? 10;
|
|
225
237
|
const currency = await this.create({
|
|
226
238
|
type: 'credit',
|
|
227
239
|
payment_method_id: paymentMethodId,
|
|
@@ -229,8 +241,8 @@ export class PaymentCurrency extends Model<InferAttributes<PaymentCurrency>, Inf
|
|
|
229
241
|
description: `Credit for ${meter.unit}`,
|
|
230
242
|
symbol: meter.unit,
|
|
231
243
|
logo: getUrl('/methods/arcblock.png'), // 默认credit图标
|
|
232
|
-
decimal
|
|
233
|
-
maximum_precision:
|
|
244
|
+
decimal,
|
|
245
|
+
maximum_precision: decimal,
|
|
234
246
|
minimum_payment_amount: '1',
|
|
235
247
|
maximum_payment_amount: '100000000000',
|
|
236
248
|
active: true,
|
|
@@ -242,13 +254,28 @@ export class PaymentCurrency extends Model<InferAttributes<PaymentCurrency>, Inf
|
|
|
242
254
|
meter_event_name: meter.event_name,
|
|
243
255
|
created_by_meter: true,
|
|
244
256
|
},
|
|
257
|
+
token_config: tokenConfig,
|
|
245
258
|
});
|
|
246
259
|
|
|
247
260
|
return currency;
|
|
248
261
|
}
|
|
249
262
|
|
|
250
263
|
public isCredit(): boolean {
|
|
251
|
-
return this
|
|
264
|
+
return PaymentCurrency.isCredit(this);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
public isOnChainCredit(): boolean {
|
|
268
|
+
return PaymentCurrency.isOnChainCredit(this);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Static version that works with plain objects
|
|
272
|
+
public static isCredit(currency: { type?: string | null }): boolean {
|
|
273
|
+
return currency.type === 'credit';
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Static version that works with plain objects
|
|
277
|
+
public static isOnChainCredit(currency: { type?: string | null; token_config?: any }): boolean {
|
|
278
|
+
return PaymentCurrency.isCredit(currency) && !!currency.token_config;
|
|
252
279
|
}
|
|
253
280
|
}
|
|
254
281
|
|
|
@@ -32,7 +32,11 @@ export class Refund extends Model<InferAttributes<Refund>, InferCreationAttribut
|
|
|
32
32
|
|
|
33
33
|
declare metadata?: Record<string, any>;
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
// processing: refund transfer succeeded, waiting for credit burn (for onchain credit only)
|
|
36
|
+
declare status: LiteralUnion<
|
|
37
|
+
'pending' | 'requires_action' | 'processing' | 'failed' | 'canceled' | 'succeeded',
|
|
38
|
+
string
|
|
39
|
+
>;
|
|
36
40
|
declare reason?: LiteralUnion<
|
|
37
41
|
'duplicate' | 'requested_by_customer' | 'requested_by_admin' | 'fraudulent' | 'expired_uncaptured_charge',
|
|
38
42
|
string
|
|
@@ -199,7 +203,13 @@ export class Refund extends Model<InferAttributes<Refund>, InferCreationAttribut
|
|
|
199
203
|
afterCreate: (model: Refund, options) =>
|
|
200
204
|
createEvent('Refund', 'refund.created', model, options).catch(console.error),
|
|
201
205
|
afterUpdate: (model: Refund, options) =>
|
|
202
|
-
createStatusEvent(
|
|
206
|
+
createStatusEvent(
|
|
207
|
+
'Refund',
|
|
208
|
+
'refund',
|
|
209
|
+
{ canceled: 'canceled', processing: 'processing', succeeded: 'succeeded' },
|
|
210
|
+
model,
|
|
211
|
+
options
|
|
212
|
+
),
|
|
203
213
|
afterDestroy: (model: Refund, options) =>
|
|
204
214
|
createEvent('Refund', 'refund.deleted', model, options).catch(console.error),
|
|
205
215
|
},
|