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.
Files changed (51) hide show
  1. package/api/src/index.ts +3 -1
  2. package/api/src/integrations/ethereum/tx.ts +11 -0
  3. package/api/src/integrations/stripe/handlers/invoice.ts +26 -6
  4. package/api/src/integrations/stripe/handlers/setup-intent.ts +34 -2
  5. package/api/src/integrations/stripe/resource.ts +185 -1
  6. package/api/src/libs/invoice.ts +2 -1
  7. package/api/src/libs/session.ts +6 -1
  8. package/api/src/queues/auto-recharge.ts +343 -0
  9. package/api/src/queues/credit-consume.ts +15 -1
  10. package/api/src/queues/credit-grant.ts +15 -0
  11. package/api/src/queues/payment.ts +14 -1
  12. package/api/src/queues/space.ts +1 -0
  13. package/api/src/routes/auto-recharge-configs.ts +454 -0
  14. package/api/src/routes/connect/auto-recharge-auth.ts +182 -0
  15. package/api/src/routes/connect/recharge-account.ts +72 -10
  16. package/api/src/routes/connect/setup.ts +5 -3
  17. package/api/src/routes/connect/shared.ts +45 -4
  18. package/api/src/routes/customers.ts +10 -6
  19. package/api/src/routes/index.ts +2 -0
  20. package/api/src/routes/invoices.ts +10 -1
  21. package/api/src/routes/meters.ts +1 -1
  22. package/api/src/routes/payment-currencies.ts +129 -0
  23. package/api/src/store/migrate.ts +20 -0
  24. package/api/src/store/migrations/20250821-auto-recharge-config.ts +38 -0
  25. package/api/src/store/models/auto-recharge-config.ts +225 -0
  26. package/api/src/store/models/credit-grant.ts +1 -1
  27. package/api/src/store/models/customer.ts +1 -0
  28. package/api/src/store/models/index.ts +3 -0
  29. package/api/src/store/models/invoice.ts +2 -1
  30. package/api/src/store/models/payment-currency.ts +10 -2
  31. package/api/src/store/models/types.ts +11 -0
  32. package/blocklet.yml +1 -1
  33. package/package.json +17 -17
  34. package/src/components/customer/credit-overview.tsx +103 -18
  35. package/src/components/customer/overdraft-protection.tsx +5 -5
  36. package/src/components/info-metric.tsx +11 -2
  37. package/src/components/invoice/recharge.tsx +8 -2
  38. package/src/components/metadata/form.tsx +29 -27
  39. package/src/components/meter/form.tsx +1 -2
  40. package/src/components/price/form.tsx +39 -26
  41. package/src/components/product/form.tsx +1 -2
  42. package/src/locales/en.tsx +15 -0
  43. package/src/locales/zh.tsx +14 -0
  44. package/src/pages/admin/billing/meters/detail.tsx +18 -0
  45. package/src/pages/admin/customers/customers/credit-grant/detail.tsx +10 -0
  46. package/src/pages/admin/products/prices/actions.tsx +42 -2
  47. package/src/pages/admin/products/products/create.tsx +1 -2
  48. package/src/pages/admin/settings/vault-config/edit-form.tsx +8 -8
  49. package/src/pages/customer/credit-grant/detail.tsx +9 -1
  50. package/src/pages/customer/recharge/account.tsx +14 -7
  51. 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(extraParams.customerDid, extraParams.currencyId);
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(customerDid, currencyId);
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: customerDid, tokens, assets: [] }],
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 ${customerDid}`,
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(customerDid, currencyId);
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: customerDid,
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', { receiverAddress: customerDid, error: err });
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
- await ensureSetupIntent(checkoutSessionId);
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
- const customer = await Customer.findByPkOrDid(req.user.did as string);
349
- if (!customer) {
350
- return res.status(404).json({ error: 'Customer not found' });
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 (paymentMethod.type !== 'arcblock') {
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.id}` });
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);
@@ -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
- where.customer_id = query.customer_id;
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: {
@@ -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;
@@ -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
+ };