payment-kit 1.17.4 → 1.17.5

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 (58) hide show
  1. package/api/src/crons/currency.ts +1 -1
  2. package/api/src/integrations/arcblock/stake.ts +4 -3
  3. package/api/src/libs/constants.ts +3 -0
  4. package/api/src/libs/invoice.ts +6 -5
  5. package/api/src/libs/notification/template/subscription-renew-failed.ts +4 -3
  6. package/api/src/libs/notification/template/subscription-renewed.ts +4 -3
  7. package/api/src/libs/notification/template/subscription-succeeded.ts +4 -3
  8. package/api/src/libs/notification/template/subscription-trial-start.ts +11 -3
  9. package/api/src/libs/notification/template/subscription-upgraded.ts +2 -1
  10. package/api/src/libs/payment.ts +5 -4
  11. package/api/src/libs/product.ts +24 -1
  12. package/api/src/queues/payment.ts +7 -5
  13. package/api/src/queues/refund.ts +8 -6
  14. package/api/src/routes/connect/change-payment.ts +3 -2
  15. package/api/src/routes/connect/change-plan.ts +3 -2
  16. package/api/src/routes/connect/collect-batch.ts +5 -4
  17. package/api/src/routes/connect/collect.ts +6 -5
  18. package/api/src/routes/connect/pay.ts +9 -4
  19. package/api/src/routes/connect/recharge.ts +9 -4
  20. package/api/src/routes/connect/setup.ts +3 -2
  21. package/api/src/routes/connect/shared.ts +25 -7
  22. package/api/src/routes/connect/subscribe.ts +3 -2
  23. package/api/src/routes/payment-currencies.ts +11 -10
  24. package/api/src/routes/payment-methods.ts +35 -19
  25. package/api/src/routes/payment-stats.ts +9 -3
  26. package/api/src/routes/prices.ts +19 -1
  27. package/api/src/routes/products.ts +60 -28
  28. package/api/src/routes/subscriptions.ts +4 -3
  29. package/api/src/store/models/payment-method.ts +11 -8
  30. package/api/src/store/models/types.ts +27 -1
  31. package/blocklet.yml +1 -1
  32. package/package.json +19 -19
  33. package/public/methods/base.png +0 -0
  34. package/src/components/payment-method/base.tsx +79 -0
  35. package/src/components/payment-method/form.tsx +3 -0
  36. package/src/components/price/upsell-select.tsx +1 -0
  37. package/src/components/subscription/metrics.tsx +1 -1
  38. package/src/components/subscription/portal/actions.tsx +1 -1
  39. package/src/libs/util.ts +1 -1
  40. package/src/locales/en.tsx +25 -0
  41. package/src/locales/zh.tsx +24 -0
  42. package/src/pages/admin/billing/invoices/detail.tsx +1 -1
  43. package/src/pages/admin/customers/customers/detail.tsx +2 -2
  44. package/src/pages/admin/overview.tsx +15 -2
  45. package/src/pages/admin/payments/intents/detail.tsx +1 -1
  46. package/src/pages/admin/payments/payouts/detail.tsx +1 -1
  47. package/src/pages/admin/payments/refunds/detail.tsx +1 -1
  48. package/src/pages/admin/products/links/detail.tsx +1 -0
  49. package/src/pages/admin/products/prices/actions.tsx +2 -1
  50. package/src/pages/admin/products/prices/detail.tsx +1 -0
  51. package/src/pages/admin/products/products/detail.tsx +1 -0
  52. package/src/pages/admin/settings/payment-methods/create.tsx +7 -0
  53. package/src/pages/admin/settings/payment-methods/index.tsx +99 -11
  54. package/src/pages/customer/index.tsx +1 -1
  55. package/src/pages/customer/invoice/detail.tsx +1 -1
  56. package/src/pages/customer/recharge.tsx +1 -1
  57. package/src/pages/customer/refund/list.tsx +7 -3
  58. package/src/pages/customer/subscription/change-payment.tsx +1 -1
@@ -9,6 +9,8 @@ import { getTxMetadata } from '../../libs/util';
9
9
  import { ensureSubscriptionRecharge, getAuthPrincipalClaim } from './shared';
10
10
  import logger from '../../libs/logger';
11
11
  import { ensureRechargeInvoice } from '../../libs/invoice';
12
+ import { EVMChainType } from '../../store/models';
13
+ import { EVM_CHAIN_TYPES } from '../../libs/constants';
12
14
 
13
15
  export default {
14
16
  action: 'recharge',
@@ -54,14 +56,14 @@ export default {
54
56
  return claims;
55
57
  }
56
58
 
57
- if (paymentMethod.type === 'ethereum') {
59
+ if (EVM_CHAIN_TYPES.includes(paymentMethod.type)) {
58
60
  return {
59
61
  signature: {
60
62
  type: 'eth:transaction',
61
63
  data: toBase58(
62
64
  Buffer.from(
63
65
  JSON.stringify({
64
- network: paymentMethod.settings?.ethereum?.chain_id,
66
+ network: paymentMethod.settings[paymentMethod.type as EVMChainType]?.chain_id,
65
67
  tx: encodeTransferItx(receiverAddress, amount, paymentCurrency.contract as string),
66
68
  }),
67
69
  'utf-8'
@@ -137,7 +139,7 @@ export default {
137
139
  }
138
140
  }
139
141
 
140
- if (paymentMethod.type === 'ethereum') {
142
+ if (EVM_CHAIN_TYPES.includes(paymentMethod.type)) {
141
143
  try {
142
144
  const paymentDetails = await executeEvmTransaction('transfer', userDid, claims, paymentMethod);
143
145
  waitForEvmTxConfirm(
@@ -159,7 +161,10 @@ export default {
159
161
  return { hash: paymentDetails.tx_hash };
160
162
  } catch (err) {
161
163
  console.error(err);
162
- logger.error('Failed to finalize recharge on ethereum', { receiverAddress, error: err });
164
+ logger.error(`Failed to finalize recharge on ${paymentMethod.type}`, {
165
+ receiverAddress,
166
+ error: err,
167
+ });
163
168
  throw err;
164
169
  }
165
170
  }
@@ -17,6 +17,7 @@ import {
17
17
  getStakeTxClaim,
18
18
  } from './shared';
19
19
  import { ensureStakeInvoice } from '../../libs/invoice';
20
+ import { EVM_CHAIN_TYPES } from '../../libs/constants';
20
21
 
21
22
  export default {
22
23
  action: 'setup',
@@ -94,7 +95,7 @@ export default {
94
95
  return claims;
95
96
  }
96
97
 
97
- if (paymentMethod.type === 'ethereum') {
98
+ if (EVM_CHAIN_TYPES.includes(paymentMethod.type)) {
98
99
  if (!paymentCurrency.contract) {
99
100
  throw new Error(`Payment currency ${paymentMethod.type}:${paymentCurrency.id} does not support setup`);
100
101
  }
@@ -213,7 +214,7 @@ export default {
213
214
  }
214
215
  }
215
216
 
216
- if (paymentMethod.type === 'ethereum') {
217
+ if (EVM_CHAIN_TYPES.includes(paymentMethod.type)) {
217
218
  await prepareTxExecution();
218
219
  broadcastEvmTransaction(checkoutSessionId, 'pending', claims);
219
220
  const paymentDetails = await executeEvmTransaction('approve', userDid, claims, paymentMethod);
@@ -35,6 +35,7 @@ import { Price } from '../../store/models/price';
35
35
  import { SetupIntent } from '../../store/models/setup-intent';
36
36
  import { Subscription } from '../../store/models/subscription';
37
37
  import { ensureInvoiceAndItems } from '../../libs/invoice';
38
+ import { CHARGE_SUPPORTED_CHAIN_TYPES, EVM_CHAIN_TYPES } from '../../libs/constants';
38
39
 
39
40
  type Result = {
40
41
  checkoutSession: CheckoutSession;
@@ -124,7 +125,7 @@ export async function ensurePaymentIntent(checkoutSessionId: string, userDid?: s
124
125
  throw new Error('Payment currency not found');
125
126
  }
126
127
 
127
- if (['arcblock', 'ethereum'].includes(paymentMethod.type) === false) {
128
+ if (CHARGE_SUPPORTED_CHAIN_TYPES.includes(paymentMethod.type) === false) {
128
129
  throw new Error(`Payment method ${paymentMethod.type} should not be here`);
129
130
  }
130
131
 
@@ -195,7 +196,7 @@ export async function ensureSetupIntent(checkoutSessionId: string, userDid?: str
195
196
  throw new Error('Payment currency not found for checkoutSession');
196
197
  }
197
198
 
198
- if (['arcblock', 'ethereum'].includes(paymentMethod.type) === false) {
199
+ if (CHARGE_SUPPORTED_CHAIN_TYPES.includes(paymentMethod.type) === false) {
199
200
  throw new Error(`Payment method ${paymentMethod.type} should not be here`);
200
201
  }
201
202
 
@@ -266,7 +267,7 @@ export async function ensureSubscriptionDelegation(subscriptionId: string) {
266
267
  if (!subscription) {
267
268
  throw new Error('Subscription not found');
268
269
  }
269
- if (!['arcblock', 'ethereum'].includes(subscription.paymentMethod?.type)) {
270
+ if (!CHARGE_SUPPORTED_CHAIN_TYPES.includes(subscription.paymentMethod?.type)) {
270
271
  throw new Error(`Payment method ${subscription.paymentMethod?.type} not supported for delegation`);
271
272
  }
272
273
  subscription.items = await expandSubscriptionItems(subscription.id);
@@ -497,6 +498,14 @@ export function getAuthPrincipalClaim(method: PaymentMethod, action: string) {
497
498
  host: method.settings?.ethereum?.api_host as string,
498
499
  };
499
500
  }
501
+ if (method.type === 'base') {
502
+ chainInfo = {
503
+ type: 'ethereum',
504
+ // @ts-ignore
505
+ id: method.settings?.base?.chain_id as number,
506
+ host: method.settings?.base?.api_host as string,
507
+ };
508
+ }
500
509
 
501
510
  return {
502
511
  description: `Select account to ${action}`,
@@ -570,16 +579,25 @@ export async function getDelegationTxClaim({
570
579
  };
571
580
  }
572
581
 
573
- if (paymentMethod.type === 'ethereum' && paymentCurrency.contract) {
582
+ if (EVM_CHAIN_TYPES.includes(paymentMethod.type) && paymentCurrency.contract) {
574
583
  const requirement = tokenRequirements.find((x) => x.address === paymentCurrency.contract);
575
584
  const limit = tokenLimits.find((x) => x.address === paymentCurrency.contract);
585
+ // logger.info('getDelegationTxClaim base', {
586
+ // address: ethWallet.address,
587
+ // amount: mode === 'setup' ? requirement!.value : limit!.totalAllowance,
588
+ // contract: paymentCurrency.contract,
589
+ // mode,
590
+ // requirement,
591
+ // limit,
592
+ // });
576
593
  return {
577
594
  type: 'eth:transaction',
578
595
  description: 'Sign the approval to continue',
579
596
  data: toBase58(
580
597
  Buffer.from(
581
598
  JSON.stringify({
582
- network: paymentMethod.settings?.ethereum?.chain_id,
599
+ // @ts-ignore
600
+ network: paymentMethod.settings?.[paymentMethod.type]?.chain_id,
583
601
  tx: await encodeApproveItx(
584
602
  paymentMethod.getEvmClient(),
585
603
  ethWallet.address,
@@ -847,7 +865,7 @@ export async function ensureSubscription(subscriptionId: string): Promise<Result
847
865
  throw new Error(`Payment currency not found for subscription ${subscriptionId}`);
848
866
  }
849
867
 
850
- if (['arcblock', 'ethereum'].includes(paymentMethod.type) === false) {
868
+ if (CHARGE_SUPPORTED_CHAIN_TYPES.includes(paymentMethod.type) === false) {
851
869
  throw new Error(`Payment method ${paymentMethod.type} should not be here`);
852
870
  }
853
871
 
@@ -900,7 +918,7 @@ export async function ensureChangePaymentContext(subscriptionId: string) {
900
918
  throw new Error(`Payment currency not found for SetupIntent ${setupIntent.id}`);
901
919
  }
902
920
 
903
- if (['arcblock', 'ethereum'].includes(paymentMethod.type) === false) {
921
+ if (CHARGE_SUPPORTED_CHAIN_TYPES.includes(paymentMethod.type) === false) {
904
922
  throw new Error(`Payment method ${paymentMethod.type} should not be here`);
905
923
  }
906
924
 
@@ -18,6 +18,7 @@ import {
18
18
  getStakeTxClaim,
19
19
  } from './shared';
20
20
  import { ensureStakeInvoice } from '../../libs/invoice';
21
+ import { EVM_CHAIN_TYPES } from '../../libs/constants';
21
22
 
22
23
  export default {
23
24
  action: 'subscription',
@@ -94,7 +95,7 @@ export default {
94
95
  return claims;
95
96
  }
96
97
 
97
- if (paymentMethod.type === 'ethereum') {
98
+ if (EVM_CHAIN_TYPES.includes(paymentMethod.type)) {
98
99
  if (!paymentCurrency.contract) {
99
100
  throw new Error(`Payment currency ${paymentMethod.type}:${paymentCurrency.id} does not support subscription`);
100
101
  }
@@ -194,7 +195,7 @@ export default {
194
195
  return { hash: paymentDetails.tx_hash };
195
196
  }
196
197
 
197
- if (paymentMethod.type === 'ethereum') {
198
+ if (EVM_CHAIN_TYPES.includes(paymentMethod.type)) {
198
199
  await prepareTxExecution();
199
200
  const { invoice } = await ensureInvoiceForCheckout({ checkoutSession, customer, subscription });
200
201
  broadcastEvmTransaction(checkoutSessionId, 'pending', claims);
@@ -7,6 +7,7 @@ import logger from '../libs/logger';
7
7
  import { authenticate } from '../libs/security';
8
8
  import { PaymentCurrency, TPaymentCurrency } from '../store/models/payment-currency';
9
9
  import { PaymentMethod } from '../store/models/payment-method';
10
+ import { EVM_CHAIN_TYPES } from '../libs/constants';
10
11
 
11
12
  const router = Router();
12
13
 
@@ -24,6 +25,11 @@ router.post('/', auth, async (req, res) => {
24
25
  if (!raw.description) {
25
26
  return res.status(400).json({ error: 'payment currency description is required' });
26
27
  }
28
+ const method = await PaymentMethod.findByPk(raw.payment_method_id);
29
+ if (!method) {
30
+ return res.status(400).json({ error: 'payment method not found' });
31
+ }
32
+ raw.logo = raw.logo || method.logo;
27
33
  if (!raw.logo) {
28
34
  return res.status(400).json({ error: 'payment currency logo is required' });
29
35
  }
@@ -35,22 +41,17 @@ router.post('/', auth, async (req, res) => {
35
41
  return res.status(400).json({ error: 'payment currency with same contract already exist' });
36
42
  }
37
43
 
38
- const method = await PaymentMethod.findByPk(raw.payment_method_id);
39
- if (!method) {
40
- return res.status(400).json({ error: 'payment method not found' });
41
- }
42
-
43
44
  if (method.type === 'stripe') {
44
45
  return res.status(400).json({ error: 'Adding method for stripe not supported' });
45
46
  }
46
47
 
47
- if (method.type === 'ethereum') {
48
+ if (EVM_CHAIN_TYPES.includes(method.type)) {
48
49
  try {
49
50
  const client = method.getEvmClient();
50
51
  const info = await fetchErc20Meta(client, raw.contract);
51
- logger.info('ethereum erc20 info fetched', { raw, info });
52
+ logger.info(`${method.type} erc20 info fetched`, { raw, info });
52
53
  if (!info.symbol || !info.decimal) {
53
- return res.status(400).json({ error: 'ethereum token not found' });
54
+ return res.status(400).json({ error: `${method.type} token not found` });
54
55
  }
55
56
 
56
57
  const currency = await PaymentCurrency.create({
@@ -76,7 +77,7 @@ router.post('/', auth, async (req, res) => {
76
77
  return res.json(currency);
77
78
  } catch (err) {
78
79
  console.error(err);
79
- return res.status(400).json({ error: 'ethereum currency contract verify failed' });
80
+ return res.status(400).json({ error: `${method.type} currency contract verify failed` });
80
81
  }
81
82
  }
82
83
 
@@ -112,7 +113,7 @@ router.post('/', auth, async (req, res) => {
112
113
  return res.json(currency);
113
114
  } catch (err) {
114
115
  console.error(err);
115
- return res.status(400).json({ error: 'ethereum currency contract verify failed' });
116
+ return res.status(400).json({ error: `${method.type} currency contract verify failed` });
116
117
  }
117
118
  }
118
119
 
@@ -11,7 +11,9 @@ import logger from '../libs/logger';
11
11
  import { authenticate } from '../libs/security';
12
12
  import { PaymentCurrency } from '../store/models/payment-currency';
13
13
  import { PaymentMethod, TPaymentMethod } from '../store/models/payment-method';
14
- import type { PaymentMethodSettings } from '../store/models/types';
14
+ import type { EVMChainType, PaymentMethodSettings } from '../store/models/types';
15
+ import { ethWallet, wallet } from '../libs/auth';
16
+ import { EVM_CHAIN_TYPES } from '../libs/constants';
15
17
 
16
18
  const router = Router();
17
19
 
@@ -90,36 +92,41 @@ router.post('/', auth, async (req, res) => {
90
92
  return res.json({ ...method.toJSON(), payment_currencies: [currency.toJSON()] });
91
93
  }
92
94
 
93
- if (raw.type === 'ethereum') {
94
- if (!raw.settings.ethereum?.api_host) {
95
- return res.status(400).json({ error: 'ethereum api_host is required' });
95
+ if (EVM_CHAIN_TYPES.includes(raw.type as string)) {
96
+ const paymentType = raw.type as EVMChainType;
97
+ if (!raw.settings[paymentType]?.api_host) {
98
+ return res.status(400).json({ error: `${paymentType} api_host is required` });
96
99
  }
97
- if (!raw.settings.ethereum?.explorer_host) {
98
- return res.status(400).json({ error: 'ethereum explorer_host is required' });
100
+ if (!raw.settings[paymentType]?.explorer_host) {
101
+ return res.status(400).json({ error: `${paymentType} explorer_host is required` });
99
102
  }
100
- if (!raw.settings.ethereum?.native_symbol) {
101
- return res.status(400).json({ error: 'ethereum native_symbol is required' });
103
+ if (!raw.settings[paymentType]?.native_symbol) {
104
+ return res.status(400).json({ error: `${paymentType} native_symbol is required` });
102
105
  }
103
106
 
104
107
  try {
105
- const provider = new ethers.JsonRpcProvider(raw.settings.ethereum.api_host);
108
+ const provider = new ethers.JsonRpcProvider(raw.settings[paymentType]?.api_host);
106
109
  const [network, blockNumber] = await Promise.all([provider.getNetwork(), provider.getBlockNumber()]);
107
- raw.settings.ethereum.chain_id = network.chainId.toString();
108
- logger.info('ethereum api endpoint verified', { settings: raw.settings.ethereum, network, blockNumber });
110
+ raw.settings[paymentType]!.chain_id = network.chainId.toString();
111
+ logger.info(`${paymentType} api endpoint verified`, {
112
+ settings: raw.settings[paymentType],
113
+ network,
114
+ blockNumber,
115
+ });
109
116
  } catch (err) {
110
117
  logger.error('verify ethereum api endpoint failed', err);
111
118
  return res.status(400).json({ error: err.message });
112
119
  }
113
120
 
114
121
  const exist = await PaymentMethod.findOne({
115
- where: { type: 'ethereum', 'settings.ethereum.chain_id': raw.settings.ethereum?.chain_id },
122
+ where: { type: paymentType, [`settings.${paymentType}.chain_id`]: raw.settings[paymentType]?.chain_id },
116
123
  });
117
124
  if (exist) {
118
- return res.status(400).json({ error: 'ethereum payment method already exist' });
125
+ return res.status(400).json({ error: `${paymentType} payment method already exist` });
119
126
  }
120
127
 
121
- raw.settings = pick(PaymentMethod.encryptSettings(raw.settings), ['ethereum']) as PaymentMethodSettings;
122
- raw.logo = raw.logo || getUrl('/methods/ethereum.png');
128
+ raw.settings = pick(PaymentMethod.encryptSettings(raw.settings), [paymentType]) as PaymentMethodSettings;
129
+ raw.logo = raw.logo || getUrl(`/methods/${paymentType}.png`);
123
130
  raw.features = {
124
131
  recurring: true,
125
132
  refund: true,
@@ -127,13 +134,13 @@ router.post('/', auth, async (req, res) => {
127
134
  };
128
135
  raw.confirmation = {
129
136
  type: 'block',
130
- block: raw.settings.ethereum?.confirmation || 1,
137
+ block: raw.settings[paymentType]?.confirmation || 1,
131
138
  };
132
139
 
133
140
  const method = await PaymentMethod.create(raw as TPaymentMethod);
134
141
 
135
142
  // create currency for native currency
136
- const symbol = raw.settings.ethereum?.native_symbol as string;
143
+ const symbol = raw.settings[paymentType]?.native_symbol as string;
137
144
  const currency = await PaymentCurrency.create({
138
145
  livemode: method.livemode,
139
146
  active: method.active,
@@ -177,8 +184,11 @@ router.get('/', auth, async (req, res) => {
177
184
  order: [['created_at', 'ASC']],
178
185
  include: [{ model: PaymentCurrency, as: 'payment_currencies', order: [['created_at', 'ASC']] }],
179
186
  });
180
-
181
- res.json(list);
187
+ if (query.addresses === 'true') {
188
+ res.json({ list, addresses: { arcblock: wallet.address, ethereum: ethWallet.address } });
189
+ } else {
190
+ res.json(list);
191
+ }
182
192
  });
183
193
 
184
194
  router.get('/types', auth, (_, res) => {
@@ -201,6 +211,12 @@ router.get('/types', auth, (_, res) => {
201
211
  description: 'Pay with ethereum compatible chains',
202
212
  logo: getUrl('/methods/ethereum.png'),
203
213
  },
214
+ {
215
+ type: 'base',
216
+ name: 'Base',
217
+ description: 'Pay with base compatible chains',
218
+ logo: getUrl('/methods/base.png'),
219
+ },
204
220
  ]);
205
221
  });
206
222
 
@@ -10,6 +10,7 @@ import { ethWallet, wallet } from '../libs/auth';
10
10
  import dayjs from '../libs/dayjs';
11
11
  import { authenticate } from '../libs/security';
12
12
  import {
13
+ EVMChainType,
13
14
  Invoice,
14
15
  PaymentCurrency,
15
16
  PaymentIntent,
@@ -19,6 +20,7 @@ import {
19
20
  Refund,
20
21
  Subscription,
21
22
  } from '../store/models';
23
+ import { EVM_CHAIN_TYPES } from '../libs/constants';
22
24
 
23
25
  const router = Router();
24
26
  const auth = authenticate<PaymentStat>({ component: true, roles: ['owner', 'admin'] });
@@ -98,8 +100,12 @@ async function getCurrencyLinks(livemode: boolean) {
98
100
  'tokens'
99
101
  );
100
102
  }
101
- if (item.payment_method?.type === 'ethereum') {
102
- acc[item.id] = joinURL(item.payment_method.settings.ethereum?.explorer_host, 'address', ethWallet.address);
103
+ if (EVM_CHAIN_TYPES.includes(item.payment_method?.type as string)) {
104
+ acc[item.id] = joinURL(
105
+ item.payment_method.settings[item.payment_method.type as EVMChainType]?.explorer_host,
106
+ 'address',
107
+ ethWallet.address
108
+ );
103
109
  }
104
110
  return acc;
105
111
  }, {} as any);
@@ -110,7 +116,7 @@ router.get('/summary', auth, async (req, res) => {
110
116
  try {
111
117
  const [arcblock, ethereum, links] = await Promise.all([
112
118
  getTokenSummaryByDid(wallet.address, !!req.livemode, 'arcblock'),
113
- getTokenSummaryByDid(ethWallet.address, !!req.livemode, 'ethereum'),
119
+ getTokenSummaryByDid(ethWallet.address, !!req.livemode, EVM_CHAIN_TYPES),
114
120
  getCurrencyLinks(!!req.livemode),
115
121
  ]);
116
122
  res.json({
@@ -11,6 +11,7 @@ import { canUpsell } from '../libs/session';
11
11
  import { PaymentCurrency } from '../store/models/payment-currency';
12
12
  import { Price } from '../store/models/price';
13
13
  import { Product } from '../store/models/product';
14
+ import { checkCurrencySupportRecurring } from '../libs/product';
14
15
 
15
16
  const router = Router();
16
17
 
@@ -162,7 +163,14 @@ export async function createPrice(payload: any) {
162
163
 
163
164
  raw.currency_options = Price.formatCurrencies(raw.currency_options, currencies);
164
165
  raw.unit_amount = fromTokenToUnit(raw.unit_amount, currency.decimal).toString();
165
-
166
+ const isRecurring = payload.type === 'recurring';
167
+ const { notSupportCurrencies, validate } = await checkCurrencySupportRecurring(
168
+ (raw.currency_options || [])?.map((x) => x.currency_id).filter(Boolean),
169
+ isRecurring
170
+ );
171
+ if (!validate) {
172
+ throw new Error(`currency ${notSupportCurrencies.map((x) => x.name).join(', ')} does not support recurring`);
173
+ }
166
174
  const price = await Price.insert(raw);
167
175
  return getExpandedPrice(price.id as string);
168
176
  }
@@ -349,6 +357,16 @@ router.put('/:id', auth, async (req, res) => {
349
357
  });
350
358
  }
351
359
  }
360
+ const isRecurring = updates.type === 'recurring';
361
+ const { notSupportCurrencies, validate } = await checkCurrencySupportRecurring(
362
+ (updates.currency_options || [])?.map((x) => x.currency_id).filter(Boolean),
363
+ isRecurring
364
+ );
365
+ if (!validate) {
366
+ return res
367
+ .status(400)
368
+ .json({ error: `currency ${notSupportCurrencies.map((x) => x.name).join(', ')} does not support recurring` });
369
+ }
352
370
 
353
371
  try {
354
372
  await doc.update(Price.formatBeforeSave(updates));
@@ -13,6 +13,7 @@ import { PaymentCurrency } from '../store/models/payment-currency';
13
13
  import { Price } from '../store/models/price';
14
14
  import { Product } from '../store/models/product';
15
15
  import type { CustomUnitAmount } from '../store/models/types';
16
+ import { checkCurrencySupportRecurring } from '../libs/product';
16
17
 
17
18
  const router = Router();
18
19
 
@@ -57,6 +58,7 @@ const ProductAndPriceSchema = Joi.object({
57
58
  }).unknown(true);
58
59
 
59
60
  export async function createProductAndPrices(payload: any) {
61
+ // 1. 准备 product 数据
60
62
  const raw: Partial<Product> = pick(payload, [
61
63
  'name',
62
64
  'type',
@@ -75,66 +77,96 @@ export async function createProductAndPrices(payload: any) {
75
77
  raw.created_via = payload.via || 'api';
76
78
  raw.metadata = formatMetadata(raw.metadata);
77
79
 
78
- const product = await Product.create(raw as Product);
80
+ // 2. 验证并准备 prices 数据
81
+ let pricesRaw: (Price & { model: 'string' })[] = [];
79
82
  if (Array.isArray(payload.prices) && payload.prices.length) {
80
83
  if (payload.prices.some((x: any) => !x.unit_amount && !x.custom_unit_amount)) {
81
84
  throw new Error('unit_amount or custom_unit_amount is required for price');
82
85
  }
83
86
 
84
87
  const currencies = await PaymentCurrency.findAll({ where: { active: true } });
85
- const pricesRaw = payload.prices.map((price: Price & { model: 'string' }) => {
86
- price.product_id = product.id;
87
- price.active = product.active;
88
- price.livemode = product.livemode;
89
- price.currency_id = price.currency_id || payload.currency_id;
88
+ pricesRaw = payload.prices.map((price: Price & { model: 'string' }) => {
89
+ const newPrice = { ...price };
90
+ newPrice.active = !!raw.active;
91
+ newPrice.livemode = !!raw.livemode;
92
+ newPrice.currency_id = price.currency_id || payload.currency_id;
90
93
 
91
- const currency = currencies.find((x) => x.id === price.currency_id);
94
+ const currency = currencies.find((x) => x.id === newPrice.currency_id);
92
95
  if (!currency) {
93
- throw new Error(`currency ${price.currency_id} used in price not found or inactive`);
96
+ throw new Error(`currency ${newPrice.currency_id} used in price not found or inactive`);
94
97
  }
95
- if (price.custom_unit_amount) {
98
+
99
+ if (newPrice.custom_unit_amount) {
96
100
  // @ts-ignore
97
101
  ['preset', 'maximum', 'minimum'].forEach((key: keyof CustomUnitAmount) => {
98
- if (price.custom_unit_amount?.[key]) {
99
- price.custom_unit_amount[key] = fromTokenToUnit(
100
- price.custom_unit_amount[key] as string,
102
+ if (newPrice.custom_unit_amount?.[key]) {
103
+ newPrice.custom_unit_amount[key] = fromTokenToUnit(
104
+ newPrice.custom_unit_amount[key] as string,
101
105
  currency.decimal
102
106
  ).toString();
103
107
  }
104
108
  });
105
- if (Array.isArray(price.custom_unit_amount.presets)) {
106
- price.custom_unit_amount.presets = price.custom_unit_amount.presets.map((x) =>
109
+ if (Array.isArray(newPrice.custom_unit_amount.presets)) {
110
+ newPrice.custom_unit_amount.presets = newPrice.custom_unit_amount.presets.map((x) =>
107
111
  fromTokenToUnit(x, currency.decimal).toString()
108
112
  );
109
113
  }
110
114
  } else {
111
- if (!price.unit_amount) {
115
+ if (!newPrice.unit_amount) {
112
116
  throw new Error('price.unit_amount is required');
113
117
  }
114
- price.unit_amount = fromTokenToUnit(price.unit_amount, currency.decimal).toString();
118
+ newPrice.unit_amount = fromTokenToUnit(newPrice.unit_amount, currency.decimal).toString();
115
119
  }
116
120
 
117
- if (Array.isArray(price.currency_options)) {
118
- price.currency_options = Price.formatCurrencies(price.currency_options, currencies);
121
+ if (Array.isArray(newPrice.currency_options)) {
122
+ newPrice.currency_options = Price.formatCurrencies(newPrice.currency_options, currencies);
119
123
  } else {
120
- price.currency_options = [];
124
+ newPrice.currency_options = [];
121
125
  }
122
- if (price.currency_options.some((x) => x.currency_id === price.currency_id) === false) {
123
- price.currency_options.unshift({
124
- currency_id: price.currency_id,
125
- unit_amount: price.unit_amount,
126
+
127
+ if (newPrice.currency_options.some((x) => x.currency_id === newPrice.currency_id) === false) {
128
+ newPrice.currency_options.unshift({
129
+ currency_id: newPrice.currency_id,
130
+ unit_amount: newPrice.unit_amount,
126
131
  tiers: null,
127
- custom_unit_amount: price.custom_unit_amount,
132
+ custom_unit_amount: newPrice.custom_unit_amount,
128
133
  });
129
134
  }
130
135
 
131
- return price;
136
+ return newPrice;
132
137
  });
133
138
 
134
- const prices = await Promise.all(pricesRaw.map((x: Price & { model: 'string' }) => Price.insert(x)));
139
+ // 3. 验证货币是否支持 recurring
140
+ const validationResults = await Promise.all(
141
+ pricesRaw.map((x) => {
142
+ const isRecurring = x.type === 'recurring';
143
+ const currencyIds = (x.currency_options || []).map((c) => c.currency_id).filter(Boolean);
144
+ return checkCurrencySupportRecurring(currencyIds, isRecurring);
145
+ })
146
+ );
147
+
148
+ const invalidCurrencies = validationResults.filter((result) => !result.validate);
149
+ if (invalidCurrencies.length > 0) {
150
+ throw new Error(
151
+ `Currency ${invalidCurrencies.flatMap((result) => result.notSupportCurrencies.map((c) => c?.name)).join(', ')} does not support recurring`
152
+ );
153
+ }
154
+ }
155
+
156
+ // 4. 所有验证通过后,创建 product
157
+ const product = await Product.create(raw as Product);
158
+
159
+ // 5. 创建 prices
160
+ if (pricesRaw.length > 0) {
161
+ const prices = await Promise.all(
162
+ pricesRaw.map((price) => {
163
+ price.product_id = product.id;
164
+ return Price.insert(price);
165
+ })
166
+ );
135
167
 
136
- // update default price id
137
- product.default_price_id = prices[0].id;
168
+ // 6. 更新 default price
169
+ product.default_price_id = prices?.[0]?.id;
138
170
  await product.save();
139
171
 
140
172
  return { ...product.toJSON(), prices: prices.map((x) => x.toJSON()) };
@@ -64,6 +64,7 @@ import { createUsageRecordQueryFn } from './usage-records';
64
64
  import { SubscriptionWillCanceledSchedule } from '../crons/subscription-will-canceled';
65
65
  import { getTokenByAddress } from '../integrations/arcblock/stake';
66
66
  import { ensureOverdraftProtectionPrice } from '../libs/overdraft-protection';
67
+ import { CHARGE_SUPPORTED_CHAIN_TYPES } from '../libs/constants';
67
68
 
68
69
  const router = Router();
69
70
  const auth = authenticate<Subscription>({ component: true, roles: ['owner', 'admin'] });
@@ -1766,7 +1767,7 @@ router.get('/:id/cycle-amount', authPortal, async (req, res) => {
1766
1767
 
1767
1768
  if (req.query?.overdraftProtection) {
1768
1769
  const { price } = await ensureOverdraftProtectionPrice(subscription.livemode);
1769
- const invoicePrice = price.currency_options.find((x: any) => x.currency_id === subscription?.currency_id);
1770
+ const invoicePrice = (price?.currency_options || []).find((x: any) => x.currency_id === subscription?.currency_id);
1770
1771
  const gas = invoicePrice?.unit_amount;
1771
1772
  return res.json({
1772
1773
  amount: new BN(maxAmount).add(new BN(gas)).toString(),
@@ -1856,7 +1857,7 @@ router.get('/:id/payer-token', authMine, async (req, res) => {
1856
1857
 
1857
1858
  // @ts-ignore
1858
1859
  const paymentAddress = getSubscriptionPaymentAddress(subscription, paymentMethod.type);
1859
- if (!paymentAddress && ['ethereum', 'arcblock'].includes(paymentMethod.type)) {
1860
+ if (!paymentAddress && CHARGE_SUPPORTED_CHAIN_TYPES.includes(paymentMethod.type)) {
1860
1861
  return res.status(400).json({ error: `Payer not found on subscription payment detail: ${subscription.id}` });
1861
1862
  }
1862
1863
 
@@ -2015,7 +2016,7 @@ router.get('/:id/overdraft-protection', authPortal, async (req, res) => {
2015
2016
  await isSubscriptionOverdraftProtectionEnabled(subscription);
2016
2017
  const upcoming = await getUpcomingInvoiceAmount(req.params.id as string);
2017
2018
  const { price } = await ensureOverdraftProtectionPrice(subscription.livemode);
2018
- const invoicePrice = price.currency_options.find((x: any) => x.currency_id === subscription?.currency_id);
2019
+ const invoicePrice = (price?.currency_options || []).find((x: any) => x.currency_id === subscription?.currency_id);
2019
2020
  const gas = invoicePrice?.unit_amount;
2020
2021
  return res.json({
2021
2022
  enabled,