payment-kit 1.19.18 → 1.19.19
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 +3 -1
- package/api/src/integrations/ethereum/tx.ts +11 -0
- package/api/src/integrations/stripe/handlers/invoice.ts +26 -6
- package/api/src/integrations/stripe/handlers/setup-intent.ts +34 -2
- package/api/src/integrations/stripe/resource.ts +185 -1
- package/api/src/libs/invoice.ts +2 -1
- package/api/src/libs/session.ts +6 -1
- package/api/src/queues/auto-recharge.ts +343 -0
- package/api/src/queues/credit-consume.ts +15 -1
- package/api/src/queues/credit-grant.ts +15 -0
- package/api/src/queues/payment.ts +14 -1
- package/api/src/queues/space.ts +1 -0
- package/api/src/routes/auto-recharge-configs.ts +454 -0
- package/api/src/routes/connect/auto-recharge-auth.ts +182 -0
- package/api/src/routes/connect/recharge-account.ts +72 -10
- package/api/src/routes/connect/setup.ts +5 -3
- package/api/src/routes/connect/shared.ts +45 -4
- package/api/src/routes/customers.ts +10 -6
- package/api/src/routes/index.ts +2 -0
- package/api/src/routes/invoices.ts +10 -1
- package/api/src/routes/meters.ts +1 -1
- package/api/src/routes/payment-currencies.ts +129 -0
- package/api/src/store/migrate.ts +20 -0
- package/api/src/store/migrations/20250821-auto-recharge-config.ts +38 -0
- package/api/src/store/models/auto-recharge-config.ts +225 -0
- package/api/src/store/models/credit-grant.ts +1 -1
- package/api/src/store/models/customer.ts +1 -0
- package/api/src/store/models/index.ts +3 -0
- package/api/src/store/models/invoice.ts +2 -1
- package/api/src/store/models/payment-currency.ts +10 -2
- package/api/src/store/models/types.ts +11 -0
- package/blocklet.yml +1 -1
- package/package.json +17 -17
- package/src/components/customer/credit-overview.tsx +103 -18
- package/src/components/customer/overdraft-protection.tsx +5 -5
- package/src/components/info-metric.tsx +11 -2
- package/src/components/invoice/recharge.tsx +8 -2
- package/src/components/metadata/form.tsx +29 -27
- package/src/components/meter/form.tsx +1 -2
- package/src/components/price/form.tsx +39 -26
- package/src/components/product/form.tsx +1 -2
- package/src/locales/en.tsx +15 -0
- package/src/locales/zh.tsx +14 -0
- package/src/pages/admin/billing/meters/detail.tsx +18 -0
- package/src/pages/admin/customers/customers/credit-grant/detail.tsx +10 -0
- package/src/pages/admin/products/prices/actions.tsx +42 -2
- package/src/pages/admin/products/products/create.tsx +1 -2
- package/src/pages/admin/settings/vault-config/edit-form.tsx +8 -8
- package/src/pages/customer/credit-grant/detail.tsx +9 -1
- package/src/pages/customer/recharge/account.tsx +14 -7
- package/src/pages/customer/recharge/subscription.tsx +4 -4
|
@@ -1,35 +1,48 @@
|
|
|
1
1
|
import type { Transaction } from '@ocap/client';
|
|
2
2
|
import { fromAddress } from '@ocap/wallet';
|
|
3
|
-
import { fromTokenToUnit } from '@ocap/util';
|
|
3
|
+
import { fromTokenToUnit, toBase58 } from '@ocap/util';
|
|
4
|
+
import { encodeTransferItx } from '../../integrations/ethereum/token';
|
|
5
|
+
import { executeEvmTransaction, waitForEvmTxConfirm } from '../../integrations/ethereum/tx';
|
|
4
6
|
import type { CallbackArgs } from '../../libs/auth';
|
|
5
7
|
import { getGasPayerExtra } from '../../libs/payment';
|
|
6
8
|
import { getTxMetadata } from '../../libs/util';
|
|
7
9
|
import { ensureAccountRecharge, getAuthPrincipalClaim } from './shared';
|
|
8
10
|
import logger from '../../libs/logger';
|
|
9
11
|
import { ensureRechargeInvoice, retryUncollectibleInvoices } from '../../libs/invoice';
|
|
12
|
+
import { EVMChainType } from '../../store/models';
|
|
13
|
+
import { EVM_CHAIN_TYPES } from '../../libs/constants';
|
|
10
14
|
|
|
11
15
|
export default {
|
|
12
16
|
action: 'recharge-account',
|
|
13
17
|
authPrincipal: false,
|
|
14
18
|
claims: {
|
|
15
19
|
authPrincipal: async ({ extraParams }: CallbackArgs) => {
|
|
16
|
-
const { paymentMethod } = await ensureAccountRecharge(
|
|
20
|
+
const { paymentMethod } = await ensureAccountRecharge(
|
|
21
|
+
extraParams.customerDid,
|
|
22
|
+
extraParams.currencyId,
|
|
23
|
+
extraParams.rechargeAddress
|
|
24
|
+
);
|
|
17
25
|
return getAuthPrincipalClaim(paymentMethod, 'recharge-account');
|
|
18
26
|
},
|
|
19
27
|
},
|
|
20
28
|
onConnect: async ({ userDid, userPk, extraParams }: CallbackArgs) => {
|
|
21
|
-
const { customerDid, currencyId } = extraParams;
|
|
29
|
+
const { customerDid, currencyId, rechargeAddress } = extraParams;
|
|
22
30
|
let { amount } = extraParams;
|
|
23
|
-
const { paymentMethod, paymentCurrency, customer } = await ensureAccountRecharge(
|
|
31
|
+
const { paymentMethod, paymentCurrency, customer, receiverAddress } = await ensureAccountRecharge(
|
|
32
|
+
customerDid,
|
|
33
|
+
currencyId,
|
|
34
|
+
rechargeAddress
|
|
35
|
+
);
|
|
24
36
|
amount = fromTokenToUnit(amount, paymentCurrency.decimal).toString();
|
|
25
37
|
if (paymentMethod.type === 'arcblock') {
|
|
26
38
|
const tokens = [{ address: paymentCurrency.contract as string, value: amount }];
|
|
27
39
|
// @ts-ignore
|
|
28
40
|
const itx: TransferV3Tx = {
|
|
29
|
-
outputs: [{ owner:
|
|
41
|
+
outputs: [{ owner: receiverAddress, tokens, assets: [] }],
|
|
30
42
|
data: getTxMetadata({
|
|
31
43
|
rechargeCustomerId: customer.id,
|
|
32
44
|
customerDid,
|
|
45
|
+
receiverAddress,
|
|
33
46
|
currencyId: paymentCurrency.id,
|
|
34
47
|
amount,
|
|
35
48
|
action: 'recharge-account',
|
|
@@ -39,7 +52,7 @@ export default {
|
|
|
39
52
|
const claims: { [key: string]: object } = {
|
|
40
53
|
prepareTx: {
|
|
41
54
|
type: 'TransferV3Tx',
|
|
42
|
-
description: `Recharge account for ${
|
|
55
|
+
description: `Recharge account for ${receiverAddress}`,
|
|
43
56
|
partialTx: { from: userDid, pk: userPk, itx },
|
|
44
57
|
requirement: { tokens },
|
|
45
58
|
chainInfo: {
|
|
@@ -52,11 +65,32 @@ export default {
|
|
|
52
65
|
return claims;
|
|
53
66
|
}
|
|
54
67
|
|
|
68
|
+
if (EVM_CHAIN_TYPES.includes(paymentMethod.type)) {
|
|
69
|
+
return {
|
|
70
|
+
signature: {
|
|
71
|
+
type: 'eth:transaction',
|
|
72
|
+
data: toBase58(
|
|
73
|
+
Buffer.from(
|
|
74
|
+
JSON.stringify({
|
|
75
|
+
network: paymentMethod.settings[paymentMethod.type as EVMChainType]?.chain_id,
|
|
76
|
+
tx: encodeTransferItx(receiverAddress, amount, paymentCurrency.contract as string),
|
|
77
|
+
}),
|
|
78
|
+
'utf-8'
|
|
79
|
+
)
|
|
80
|
+
),
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
55
85
|
throw new Error(`Payment method ${paymentMethod.type} not supported`);
|
|
56
86
|
},
|
|
57
87
|
onAuth: async ({ request, userDid, claims, extraParams }: CallbackArgs) => {
|
|
58
|
-
const { customerDid, currencyId } = extraParams;
|
|
59
|
-
const { paymentMethod, paymentCurrency, customer } = await ensureAccountRecharge(
|
|
88
|
+
const { customerDid, currencyId, rechargeAddress } = extraParams;
|
|
89
|
+
const { paymentMethod, paymentCurrency, customer, receiverAddress } = await ensureAccountRecharge(
|
|
90
|
+
customerDid,
|
|
91
|
+
currencyId,
|
|
92
|
+
rechargeAddress
|
|
93
|
+
);
|
|
60
94
|
let { amount } = extraParams;
|
|
61
95
|
amount = fromTokenToUnit(amount, paymentCurrency.decimal).toString();
|
|
62
96
|
|
|
@@ -69,7 +103,7 @@ export default {
|
|
|
69
103
|
metadata: {
|
|
70
104
|
payment_details: {
|
|
71
105
|
[paymentMethod.type]: paymentDetails,
|
|
72
|
-
receiverAddress
|
|
106
|
+
receiverAddress,
|
|
73
107
|
},
|
|
74
108
|
},
|
|
75
109
|
livemode: paymentCurrency?.livemode,
|
|
@@ -126,7 +160,35 @@ export default {
|
|
|
126
160
|
return { hash: txHash };
|
|
127
161
|
} catch (err) {
|
|
128
162
|
console.error(err);
|
|
129
|
-
logger.error('Failed to finalize recharge on arcblock', {
|
|
163
|
+
logger.error('Failed to finalize recharge on arcblock', {
|
|
164
|
+
receiverAddress,
|
|
165
|
+
customerId: customer.id,
|
|
166
|
+
error: err,
|
|
167
|
+
});
|
|
168
|
+
throw err;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (EVM_CHAIN_TYPES.includes(paymentMethod.type)) {
|
|
173
|
+
try {
|
|
174
|
+
const paymentDetails = await executeEvmTransaction('transfer', userDid, claims, paymentMethod);
|
|
175
|
+
waitForEvmTxConfirm(
|
|
176
|
+
paymentMethod.getEvmClient(),
|
|
177
|
+
Number(paymentDetails.block_height),
|
|
178
|
+
paymentMethod.confirmation.block
|
|
179
|
+
)
|
|
180
|
+
.then(async () => {
|
|
181
|
+
await afterTxExecution(paymentDetails);
|
|
182
|
+
})
|
|
183
|
+
.catch(console.error);
|
|
184
|
+
|
|
185
|
+
return { hash: paymentDetails.tx_hash };
|
|
186
|
+
} catch (err) {
|
|
187
|
+
console.error(err);
|
|
188
|
+
logger.error(`Failed to finalize recharge on ${paymentMethod.type}`, {
|
|
189
|
+
receiverAddress,
|
|
190
|
+
error: err,
|
|
191
|
+
});
|
|
130
192
|
throw err;
|
|
131
193
|
}
|
|
132
194
|
}
|
|
@@ -26,15 +26,17 @@ export default {
|
|
|
26
26
|
persistentDynamicClaims: true,
|
|
27
27
|
claims: {
|
|
28
28
|
authPrincipal: async ({ extraParams }: CallbackArgs) => {
|
|
29
|
-
const { paymentMethod } = await ensureSetupIntent(extraParams.checkoutSessionId);
|
|
29
|
+
const { paymentMethod } = await ensureSetupIntent(extraParams.checkoutSessionId, true);
|
|
30
30
|
return getAuthPrincipalClaim(paymentMethod, 'subscribe');
|
|
31
31
|
},
|
|
32
32
|
},
|
|
33
33
|
onConnect: async (args: CallbackArgs) => {
|
|
34
34
|
const { userDid, userPk, extraParams } = args;
|
|
35
35
|
const { checkoutSessionId } = extraParams;
|
|
36
|
-
const { paymentMethod, paymentCurrency, checkoutSession, subscription } =
|
|
37
|
-
|
|
36
|
+
const { paymentMethod, paymentCurrency, checkoutSession, subscription } = await ensureSetupIntent(
|
|
37
|
+
checkoutSessionId,
|
|
38
|
+
true
|
|
39
|
+
);
|
|
38
40
|
if (!subscription) {
|
|
39
41
|
throw new Error('Subscription for checkoutSession not found');
|
|
40
42
|
}
|
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
import { getCustomerStakeAddress, OCAP_PAYMENT_TX_TYPE } from '../../libs/util';
|
|
31
31
|
|
|
32
32
|
import { invoiceQueue } from '../../queues/invoice';
|
|
33
|
-
import { type TLineItemExpanded } from '../../store/models';
|
|
33
|
+
import { AutoRechargeConfig, type TLineItemExpanded } from '../../store/models';
|
|
34
34
|
import { CheckoutSession } from '../../store/models/checkout-session';
|
|
35
35
|
import { Customer } from '../../store/models/customer';
|
|
36
36
|
import { Invoice, TInvoice } from '../../store/models/invoice';
|
|
@@ -212,7 +212,7 @@ export async function ensurePaymentIntent(
|
|
|
212
212
|
};
|
|
213
213
|
}
|
|
214
214
|
|
|
215
|
-
export async function ensureSetupIntent(checkoutSessionId: string) {
|
|
215
|
+
export async function ensureSetupIntent(checkoutSessionId: string, skipInvoice?: boolean) {
|
|
216
216
|
const checkoutSession = await ensureCheckoutSession(checkoutSessionId);
|
|
217
217
|
|
|
218
218
|
if (!checkoutSession.setup_intent_id) {
|
|
@@ -264,7 +264,7 @@ export async function ensureSetupIntent(checkoutSessionId: string) {
|
|
|
264
264
|
checkoutSession.line_items = await Price.expand(checkoutSession.line_items);
|
|
265
265
|
|
|
266
266
|
let invoice;
|
|
267
|
-
if (subscription) {
|
|
267
|
+
if (subscription && !skipInvoice) {
|
|
268
268
|
if (checkoutSession.invoice_id) {
|
|
269
269
|
invoice = await Invoice.findByPk(checkoutSession.invoice_id);
|
|
270
270
|
} else {
|
|
@@ -610,7 +610,7 @@ export async function ensureSubscriptionRecharge(subscriptionId: string) {
|
|
|
610
610
|
};
|
|
611
611
|
}
|
|
612
612
|
|
|
613
|
-
export async function ensureAccountRecharge(customerId: string, currencyId: string) {
|
|
613
|
+
export async function ensureAccountRecharge(customerId: string, currencyId: string, rechargeAddress?: string) {
|
|
614
614
|
const customer = await Customer.findByPkOrDid(customerId);
|
|
615
615
|
if (!customer) {
|
|
616
616
|
throw new Error(`Customer ${customerId} not found`);
|
|
@@ -623,10 +623,14 @@ export async function ensureAccountRecharge(customerId: string, currencyId: stri
|
|
|
623
623
|
if (!paymentMethod) {
|
|
624
624
|
throw new Error(`Payment method ${paymentCurrency.payment_method_id} not found`);
|
|
625
625
|
}
|
|
626
|
+
|
|
627
|
+
const receiverAddress = rechargeAddress || customer.did;
|
|
628
|
+
|
|
626
629
|
return {
|
|
627
630
|
paymentCurrency: paymentCurrency as PaymentCurrency,
|
|
628
631
|
paymentMethod: paymentMethod as PaymentMethod,
|
|
629
632
|
customer,
|
|
633
|
+
receiverAddress,
|
|
630
634
|
};
|
|
631
635
|
}
|
|
632
636
|
export function getAuthPrincipalClaim(method: PaymentMethod, action: string) {
|
|
@@ -685,6 +689,7 @@ export async function getDelegationTxClaim({
|
|
|
685
689
|
trialing: boolean;
|
|
686
690
|
billingThreshold?: number;
|
|
687
691
|
requiredStake?: boolean;
|
|
692
|
+
requiredAmount?: string;
|
|
688
693
|
}) {
|
|
689
694
|
const amount = getFastCheckoutAmount(items, mode, paymentCurrency.id);
|
|
690
695
|
const address = toDelegateAddress(userDid, wallet.address);
|
|
@@ -1416,6 +1421,42 @@ export async function updateStripeSubscriptionAfterChangePayment(setupIntent: Se
|
|
|
1416
1421
|
}
|
|
1417
1422
|
}
|
|
1418
1423
|
|
|
1424
|
+
|
|
1425
|
+
export async function ensureAutoRechargeAuthorization(
|
|
1426
|
+
autoRechargeConfigId: string
|
|
1427
|
+
): Promise<{ autoRechargeConfig: AutoRechargeConfig; paymentMethod: PaymentMethod; paymentCurrency: PaymentCurrency }> {
|
|
1428
|
+
const autoRechargeConfig = await AutoRechargeConfig.findByPk(autoRechargeConfigId);
|
|
1429
|
+
if (!autoRechargeConfig) {
|
|
1430
|
+
throw new Error(`Auto recharge config ${autoRechargeConfigId} not found`);
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
const price = await Price.findByPk(autoRechargeConfig.price_id);
|
|
1434
|
+
if (!price) {
|
|
1435
|
+
throw new Error(`Price ${autoRechargeConfig.price_id} not found`);
|
|
1436
|
+
}
|
|
1437
|
+
const paymentCurrency = await PaymentCurrency.findByPk(autoRechargeConfig.recharge_currency_id);
|
|
1438
|
+
if (!paymentCurrency) {
|
|
1439
|
+
throw new Error(`Payment currency ${autoRechargeConfig.recharge_currency_id} not found`);
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
if (paymentCurrency.payment_method_id !== autoRechargeConfig.payment_method_id) {
|
|
1443
|
+
await autoRechargeConfig.update({
|
|
1444
|
+
payment_method_id: paymentCurrency.payment_method_id,
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
const paymentMethod = await PaymentMethod.findByPk(paymentCurrency.payment_method_id);
|
|
1449
|
+
if (!paymentMethod) {
|
|
1450
|
+
throw new Error(`Payment method ${paymentCurrency.payment_method_id} not found`);
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
return {
|
|
1454
|
+
autoRechargeConfig,
|
|
1455
|
+
paymentMethod,
|
|
1456
|
+
paymentCurrency,
|
|
1457
|
+
};
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1419
1460
|
export async function returnStakeForCanceledSubscription(subscriptionId: string) {
|
|
1420
1461
|
if (!subscriptionId) {
|
|
1421
1462
|
return;
|
|
@@ -345,9 +345,14 @@ router.get('/payer-token', sessionMiddleware({ accessKey: true }), async (req, r
|
|
|
345
345
|
return res.status(400).json({ error: 'Currency ID is required' });
|
|
346
346
|
}
|
|
347
347
|
try {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
348
|
+
let paymentAddress = req.query.payerAddress as string;
|
|
349
|
+
let customer: Customer | null = null;
|
|
350
|
+
if (!paymentAddress) {
|
|
351
|
+
customer = await Customer.findByPkOrDid(req.user.did as string);
|
|
352
|
+
if (!customer) {
|
|
353
|
+
return res.status(404).json({ error: 'Customer not found' });
|
|
354
|
+
}
|
|
355
|
+
paymentAddress = customer.did;
|
|
351
356
|
}
|
|
352
357
|
|
|
353
358
|
const paymentCurrency = await PaymentCurrency.findByPk(req.query.currencyId as string);
|
|
@@ -360,13 +365,12 @@ router.get('/payer-token', sessionMiddleware({ accessKey: true }), async (req, r
|
|
|
360
365
|
return res.status(404).json({ error: 'Payment method not found' });
|
|
361
366
|
}
|
|
362
367
|
|
|
363
|
-
if (
|
|
368
|
+
if (!['arcblock', 'ethereum', 'base'].includes(paymentMethod.type)) {
|
|
364
369
|
return res.status(400).json({ error: `Payment method not supported: ${paymentMethod.type}` });
|
|
365
370
|
}
|
|
366
371
|
|
|
367
|
-
const paymentAddress = customer.did;
|
|
368
372
|
if (!paymentAddress) {
|
|
369
|
-
return res.status(400).json({ error: `Payment address not found for customer: ${customer
|
|
373
|
+
return res.status(400).json({ error: `Payment address not found for customer: ${customer?.id}` });
|
|
370
374
|
}
|
|
371
375
|
|
|
372
376
|
const token = await getTokenByAddress(paymentAddress, paymentMethod, paymentCurrency);
|
package/api/src/routes/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
2
|
|
|
3
3
|
import { PaymentCurrency } from '../store/models/payment-currency';
|
|
4
|
+
import autoRechargeConfigs from './auto-recharge-configs';
|
|
4
5
|
import checkoutSessions from './checkout-sessions';
|
|
5
6
|
import creditGrants from './credit-grants';
|
|
6
7
|
import creditTransactions from './credit-transactions';
|
|
@@ -51,6 +52,7 @@ router.use(async (req, _, next) => {
|
|
|
51
52
|
next();
|
|
52
53
|
});
|
|
53
54
|
|
|
55
|
+
router.use('/auto-recharge-configs', autoRechargeConfigs);
|
|
54
56
|
router.use('/checkout-sessions', checkoutSessions);
|
|
55
57
|
router.use('/credit-grants', creditGrants);
|
|
56
58
|
router.use('/credit-transactions', creditTransactions);
|
|
@@ -379,10 +379,12 @@ const rechargeSchema = createListParamSchema<{
|
|
|
379
379
|
status?: string;
|
|
380
380
|
customer_id?: string;
|
|
381
381
|
currency_id?: string;
|
|
382
|
+
recharge_address?: string;
|
|
382
383
|
}>({
|
|
383
384
|
status: Joi.string().empty(''),
|
|
384
385
|
customer_id: Joi.string().empty(''),
|
|
385
386
|
currency_id: Joi.string().empty(''),
|
|
387
|
+
recharge_address: Joi.string().empty(''),
|
|
386
388
|
});
|
|
387
389
|
|
|
388
390
|
router.get('/recharge', authMine, async (req, res) => {
|
|
@@ -392,12 +394,19 @@ router.get('/recharge', authMine, async (req, res) => {
|
|
|
392
394
|
});
|
|
393
395
|
const where = getWhereFromKvQuery(query.q);
|
|
394
396
|
if (query.customer_id) {
|
|
395
|
-
|
|
397
|
+
const customer = await Customer.findByPkOrDid(query.customer_id);
|
|
398
|
+
if (!customer) {
|
|
399
|
+
return res.status(404).json({ error: 'Customer not found' });
|
|
400
|
+
}
|
|
401
|
+
where.customer_id = customer.id;
|
|
396
402
|
}
|
|
397
403
|
if (query.currency_id) {
|
|
398
404
|
where.currency_id = query.currency_id;
|
|
399
405
|
}
|
|
400
406
|
|
|
407
|
+
if (query.recharge_address) {
|
|
408
|
+
where['metadata.payment_details.receiverAddress'] = query.recharge_address;
|
|
409
|
+
}
|
|
401
410
|
try {
|
|
402
411
|
const { rows: invoices, count } = await Invoice.findAndCountAll({
|
|
403
412
|
where: {
|
package/api/src/routes/meters.ts
CHANGED
|
@@ -121,7 +121,7 @@ router.get('/:id', auth, async (req, res) => {
|
|
|
121
121
|
where: {
|
|
122
122
|
[Op.or]: [{ id: req.params.id }, { event_name: req.params.id }],
|
|
123
123
|
},
|
|
124
|
-
include: [{ model: PaymentCurrency, as: 'paymentCurrency' }],
|
|
124
|
+
include: [{ model: PaymentCurrency.scope('withRechargeConfig'), as: 'paymentCurrency' }],
|
|
125
125
|
});
|
|
126
126
|
|
|
127
127
|
if (!meter) {
|
|
@@ -4,20 +4,25 @@ 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
|
+
import sessionMiddleware from '@blocklet/sdk/lib/middlewares/session';
|
|
7
9
|
import { fetchErc20Meta } from '../integrations/ethereum/token';
|
|
8
10
|
import logger from '../libs/logger';
|
|
9
11
|
import { authenticate } from '../libs/security';
|
|
10
12
|
import { PaymentCurrency, TPaymentCurrency } from '../store/models/payment-currency';
|
|
11
13
|
import { PaymentMethod } from '../store/models/payment-method';
|
|
14
|
+
import { Price, Product } from '../store/models';
|
|
12
15
|
import { EVM_CHAIN_TYPES } from '../libs/constants';
|
|
13
16
|
import { ethWallet, getVaultAddress, wallet } from '../libs/auth';
|
|
14
17
|
import { resolveAddressChainTypes } from '../libs/util';
|
|
15
18
|
import { depositVaultQueue } from '../queues/payment';
|
|
16
19
|
import { checkDepositVaultAmount } from '../libs/payment';
|
|
17
20
|
import { getTokenSummaryByDid } from '../integrations/arcblock/stake';
|
|
21
|
+
import { createPaymentLink } from './payment-links';
|
|
18
22
|
|
|
19
23
|
const router = Router();
|
|
20
24
|
|
|
25
|
+
const user = sessionMiddleware({ accessKey: true });
|
|
21
26
|
const auth = authenticate<PaymentCurrency>({ component: true, roles: ['owner', 'admin'] });
|
|
22
27
|
const authOwner = authenticate<PaymentCurrency>({ component: true, roles: ['owner'] });
|
|
23
28
|
const paymentCurrencyCreateSchema = Joi.object({
|
|
@@ -370,4 +375,128 @@ router.delete('/:id', auth, async (req, res) => {
|
|
|
370
375
|
}
|
|
371
376
|
});
|
|
372
377
|
|
|
378
|
+
router.get('/:id/recharge-config', user, async (req, res) => {
|
|
379
|
+
try {
|
|
380
|
+
const { id } = req.params;
|
|
381
|
+
|
|
382
|
+
const currency = await PaymentCurrency.scope('withRechargeConfig').findByPk(id);
|
|
383
|
+
|
|
384
|
+
if (!currency) {
|
|
385
|
+
return res.status(404).json({ error: 'Credit currency not found' });
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (!currency.recharge_config) {
|
|
389
|
+
return res.json({
|
|
390
|
+
currency_id: id,
|
|
391
|
+
recharge_config: null,
|
|
392
|
+
message: 'No recharge config available for this currency',
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
let basePrice: (Price & { product: Product }) | null = null;
|
|
397
|
+
if (currency.recharge_config.base_price_id) {
|
|
398
|
+
basePrice = (await Price.findByPk(currency.recharge_config.base_price_id, {
|
|
399
|
+
include: [{ model: Product, as: 'product' }],
|
|
400
|
+
})) as Price & { product: Product };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const rechargeConfig = currency.recharge_config;
|
|
404
|
+
let paymentUrl = rechargeConfig.checkout_url;
|
|
405
|
+
if (!paymentUrl && rechargeConfig.payment_link_id) {
|
|
406
|
+
paymentUrl = getUrl(`/checkout/pay/${rechargeConfig.payment_link_id}`);
|
|
407
|
+
}
|
|
408
|
+
if (!paymentUrl && rechargeConfig.base_price_id) {
|
|
409
|
+
const paymentLink = await createPaymentLink({
|
|
410
|
+
livemode: currency.livemode,
|
|
411
|
+
currency_id: basePrice?.currency_id,
|
|
412
|
+
name: basePrice?.product?.name || `${currency.name} Recharge`,
|
|
413
|
+
submit_type: 'pay',
|
|
414
|
+
line_items: [
|
|
415
|
+
{
|
|
416
|
+
price_id: rechargeConfig.base_price_id,
|
|
417
|
+
quantity: 1,
|
|
418
|
+
adjustable_quantity: {
|
|
419
|
+
enabled: true,
|
|
420
|
+
minimum: 1,
|
|
421
|
+
maximum: 100000000,
|
|
422
|
+
},
|
|
423
|
+
},
|
|
424
|
+
],
|
|
425
|
+
});
|
|
426
|
+
await currency.update({ recharge_config: { ...rechargeConfig, payment_link_id: paymentLink.id } });
|
|
427
|
+
paymentUrl = getUrl(`/checkout/pay/${paymentLink.id}`);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return res.json({
|
|
431
|
+
currency_id: id,
|
|
432
|
+
currency_info: pick(currency, ['id', 'name', 'symbol', 'decimal', 'type']),
|
|
433
|
+
recharge_config: {
|
|
434
|
+
...currency.recharge_config,
|
|
435
|
+
basePrice,
|
|
436
|
+
payment_url: paymentUrl,
|
|
437
|
+
},
|
|
438
|
+
});
|
|
439
|
+
} catch (error: any) {
|
|
440
|
+
logger.error('Failed to get currency recharge config', {
|
|
441
|
+
currencyId: req.params.id,
|
|
442
|
+
error: error.message,
|
|
443
|
+
});
|
|
444
|
+
return res.status(500).json({ error: 'Internal server error' });
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
const rechargeConfigSchema = Joi.object({
|
|
449
|
+
base_price_id: Joi.string().required(),
|
|
450
|
+
payment_link_id: Joi.string().optional(),
|
|
451
|
+
checkout_url: Joi.string().optional(),
|
|
452
|
+
settings: Joi.object({
|
|
453
|
+
min_recharge_amount: Joi.number().min(0).optional(),
|
|
454
|
+
max_recharge_amount: Joi.number().min(0).optional(),
|
|
455
|
+
}).optional(),
|
|
456
|
+
}).unknown(true);
|
|
457
|
+
|
|
458
|
+
router.put('/:id/recharge-config', auth, async (req, res) => {
|
|
459
|
+
const { id } = req.params;
|
|
460
|
+
const { error, value: rechargeConfig } = rechargeConfigSchema.validate(
|
|
461
|
+
pick(req.body, ['base_price_id', 'payment_link_id', 'checkout_url', 'settings'])
|
|
462
|
+
);
|
|
463
|
+
if (error) {
|
|
464
|
+
return res.status(400).json({ error: error.message });
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const currency = await PaymentCurrency.findByPk(id);
|
|
468
|
+
|
|
469
|
+
if (!currency) {
|
|
470
|
+
return res.status(404).json({ error: 'Credit currency not found' });
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
if (rechargeConfig.base_price_id) {
|
|
474
|
+
const basePrice = (await Price.findOne({
|
|
475
|
+
where: {
|
|
476
|
+
id: rechargeConfig.base_price_id,
|
|
477
|
+
active: true,
|
|
478
|
+
},
|
|
479
|
+
include: [{ model: Product, as: 'product' }],
|
|
480
|
+
})) as Price & { product: Product };
|
|
481
|
+
|
|
482
|
+
if (!basePrice) {
|
|
483
|
+
return res.status(404).json({ error: 'Base price not found or inactive' });
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
await currency.update({ recharge_config: rechargeConfig });
|
|
488
|
+
|
|
489
|
+
logger.info('Currency recharge config updated', {
|
|
490
|
+
currencyId: id,
|
|
491
|
+
basePriceId: rechargeConfig.base_price_id,
|
|
492
|
+
tiersCount: rechargeConfig.price_tiers?.length || 0,
|
|
493
|
+
});
|
|
494
|
+
|
|
495
|
+
return res.json({
|
|
496
|
+
currency_id: id,
|
|
497
|
+
recharge_config: rechargeConfig,
|
|
498
|
+
message: 'Recharge config updated successfully',
|
|
499
|
+
});
|
|
500
|
+
});
|
|
501
|
+
|
|
373
502
|
export default router;
|
package/api/src/store/migrate.ts
CHANGED
|
@@ -46,4 +46,24 @@ export async function safeApplyColumnChanges(context: QueryInterface, changes: C
|
|
|
46
46
|
}
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
const indexExists = async (table: string, indexName: string, queryInterface: QueryInterface) => {
|
|
50
|
+
const indexes = await queryInterface.showIndex(table);
|
|
51
|
+
return indexes && Array.isArray(indexes) && indexes.some((index: { name: string }) => index.name === indexName);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export const createIndexIfNotExists = async (
|
|
55
|
+
queryInterface: QueryInterface,
|
|
56
|
+
table: string,
|
|
57
|
+
columns: string[],
|
|
58
|
+
indexName: string,
|
|
59
|
+
extra?: any
|
|
60
|
+
) => {
|
|
61
|
+
if (await indexExists(table, indexName, queryInterface)) {
|
|
62
|
+
/* eslint-disable no-console */
|
|
63
|
+
console.log(`Index ${indexName} already exists on ${table}, skipping...`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
await queryInterface.addIndex(table, columns, { name: indexName, ...(extra || {}) });
|
|
67
|
+
};
|
|
68
|
+
|
|
49
69
|
export type Migration = typeof umzug._types.migration;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { DataTypes } from 'sequelize';
|
|
2
|
+
import { AutoRechargeConfig } from '../models/auto-recharge-config';
|
|
3
|
+
import { createIndexIfNotExists, Migration, safeApplyColumnChanges } from '../migrate';
|
|
4
|
+
|
|
5
|
+
export const up: Migration = async ({ context }) => {
|
|
6
|
+
await context.createTable('auto_recharge_configs', AutoRechargeConfig.GENESIS_ATTRIBUTES);
|
|
7
|
+
|
|
8
|
+
await createIndexIfNotExists(
|
|
9
|
+
context,
|
|
10
|
+
'auto_recharge_configs',
|
|
11
|
+
['customer_id'],
|
|
12
|
+
'idx_auto_recharge_configs_customer_id'
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
await createIndexIfNotExists(
|
|
16
|
+
context,
|
|
17
|
+
'auto_recharge_configs',
|
|
18
|
+
['currency_id'],
|
|
19
|
+
'idx_auto_recharge_configs_currency_id'
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
await safeApplyColumnChanges(context, {
|
|
23
|
+
payment_currencies: [
|
|
24
|
+
{
|
|
25
|
+
name: 'recharge_config',
|
|
26
|
+
field: {
|
|
27
|
+
type: DataTypes.JSON,
|
|
28
|
+
allowNull: true,
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
});
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const down: Migration = async ({ context }) => {
|
|
36
|
+
await context.dropTable('auto_recharge_configs');
|
|
37
|
+
await context.removeColumn('payment_currencies', 'recharge_config');
|
|
38
|
+
};
|