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.
- package/api/src/crons/currency.ts +1 -1
- package/api/src/integrations/arcblock/stake.ts +4 -3
- package/api/src/libs/constants.ts +3 -0
- package/api/src/libs/invoice.ts +6 -5
- package/api/src/libs/notification/template/subscription-renew-failed.ts +4 -3
- package/api/src/libs/notification/template/subscription-renewed.ts +4 -3
- package/api/src/libs/notification/template/subscription-succeeded.ts +4 -3
- package/api/src/libs/notification/template/subscription-trial-start.ts +11 -3
- package/api/src/libs/notification/template/subscription-upgraded.ts +2 -1
- package/api/src/libs/payment.ts +5 -4
- package/api/src/libs/product.ts +24 -1
- package/api/src/queues/payment.ts +7 -5
- package/api/src/queues/refund.ts +8 -6
- package/api/src/routes/connect/change-payment.ts +3 -2
- package/api/src/routes/connect/change-plan.ts +3 -2
- package/api/src/routes/connect/collect-batch.ts +5 -4
- package/api/src/routes/connect/collect.ts +6 -5
- package/api/src/routes/connect/pay.ts +9 -4
- package/api/src/routes/connect/recharge.ts +9 -4
- package/api/src/routes/connect/setup.ts +3 -2
- package/api/src/routes/connect/shared.ts +25 -7
- package/api/src/routes/connect/subscribe.ts +3 -2
- package/api/src/routes/payment-currencies.ts +11 -10
- package/api/src/routes/payment-methods.ts +35 -19
- package/api/src/routes/payment-stats.ts +9 -3
- package/api/src/routes/prices.ts +19 -1
- package/api/src/routes/products.ts +60 -28
- package/api/src/routes/subscriptions.ts +4 -3
- package/api/src/store/models/payment-method.ts +11 -8
- package/api/src/store/models/types.ts +27 -1
- package/blocklet.yml +1 -1
- package/package.json +19 -19
- package/public/methods/base.png +0 -0
- package/src/components/payment-method/base.tsx +79 -0
- package/src/components/payment-method/form.tsx +3 -0
- package/src/components/price/upsell-select.tsx +1 -0
- package/src/components/subscription/metrics.tsx +1 -1
- package/src/components/subscription/portal/actions.tsx +1 -1
- package/src/libs/util.ts +1 -1
- package/src/locales/en.tsx +25 -0
- package/src/locales/zh.tsx +24 -0
- package/src/pages/admin/billing/invoices/detail.tsx +1 -1
- package/src/pages/admin/customers/customers/detail.tsx +2 -2
- package/src/pages/admin/overview.tsx +15 -2
- package/src/pages/admin/payments/intents/detail.tsx +1 -1
- package/src/pages/admin/payments/payouts/detail.tsx +1 -1
- package/src/pages/admin/payments/refunds/detail.tsx +1 -1
- package/src/pages/admin/products/links/detail.tsx +1 -0
- package/src/pages/admin/products/prices/actions.tsx +2 -1
- package/src/pages/admin/products/prices/detail.tsx +1 -0
- package/src/pages/admin/products/products/detail.tsx +1 -0
- package/src/pages/admin/settings/payment-methods/create.tsx +7 -0
- package/src/pages/admin/settings/payment-methods/index.tsx +99 -11
- package/src/pages/customer/index.tsx +1 -1
- package/src/pages/customer/invoice/detail.tsx +1 -1
- package/src/pages/customer/recharge.tsx +1 -1
- package/src/pages/customer/refund/list.tsx +7 -3
- package/src/pages/customer/subscription/change-payment.tsx +1 -1
|
@@ -19,7 +19,7 @@ export async function syncCurrencyLogo() {
|
|
|
19
19
|
|
|
20
20
|
const updateLogo = (item: PaymentMethod | PaymentCurrency) => {
|
|
21
21
|
const { logo } = item;
|
|
22
|
-
if (/\/methods\/(stripe|arcblock|ethereum)\.png$/.test(logo)) {
|
|
22
|
+
if (/\/methods\/(stripe|arcblock|ethereum|base)\.png$/.test(logo)) {
|
|
23
23
|
const newLogo = getUrl(logo.replace(/^.*?(\/methods\/.*)$/, '$1'));
|
|
24
24
|
promises.push((item as any).update({ logo: newLogo }));
|
|
25
25
|
}
|
|
@@ -13,6 +13,7 @@ import { events } from '../../libs/event';
|
|
|
13
13
|
import logger from '../../libs/logger';
|
|
14
14
|
import { Customer, GroupedBN, PaymentCurrency, PaymentMethod, Subscription } from '../../store/models';
|
|
15
15
|
import { fetchErc20Balance, fetchEtherBalance } from '../ethereum/token';
|
|
16
|
+
import { EVM_CHAIN_TYPES } from '../../libs/constants';
|
|
16
17
|
|
|
17
18
|
export async function ensureStakedForGas() {
|
|
18
19
|
const currencies = await PaymentCurrency.findAll({ where: { active: true, is_base_currency: true } });
|
|
@@ -225,7 +226,7 @@ export async function getStakeSummaryByDid(did: string, livemode: boolean = true
|
|
|
225
226
|
export async function getTokenSummaryByDid(
|
|
226
227
|
did: string,
|
|
227
228
|
livemode: boolean,
|
|
228
|
-
type: string = 'arcblock'
|
|
229
|
+
type: string | string[] = 'arcblock'
|
|
229
230
|
): Promise<GroupedBN> {
|
|
230
231
|
const methods = await PaymentMethod.findAll({
|
|
231
232
|
where: { type, livemode },
|
|
@@ -249,7 +250,7 @@ export async function getTokenSummaryByDid(
|
|
|
249
250
|
}
|
|
250
251
|
});
|
|
251
252
|
}
|
|
252
|
-
if (method.type
|
|
253
|
+
if (EVM_CHAIN_TYPES.includes(method.type) && isEthereumDid(did)) {
|
|
253
254
|
const client = method.getEvmClient();
|
|
254
255
|
await Promise.all(
|
|
255
256
|
// @ts-ignore
|
|
@@ -278,7 +279,7 @@ export async function getTokenByAddress(
|
|
|
278
279
|
const { tokens } = await client.getAccountTokens({ address });
|
|
279
280
|
return tokens.find((t: any) => t.address === paymentCurrency.contract)?.balance;
|
|
280
281
|
}
|
|
281
|
-
if (paymentMethod.type
|
|
282
|
+
if (EVM_CHAIN_TYPES.includes(paymentMethod.type)) {
|
|
282
283
|
const client = paymentMethod.getEvmClient();
|
|
283
284
|
if (paymentCurrency.contract) {
|
|
284
285
|
const token = await fetchErc20Balance(client, paymentCurrency.contract, address);
|
package/api/src/libs/invoice.ts
CHANGED
|
@@ -36,6 +36,7 @@ import {
|
|
|
36
36
|
} from './subscription';
|
|
37
37
|
import logger from './logger';
|
|
38
38
|
import { ensureOverdraftProtectionPrice } from './overdraft-protection';
|
|
39
|
+
import { CHARGE_SUPPORTED_CHAIN_TYPES } from './constants';
|
|
39
40
|
|
|
40
41
|
export function getCustomerInvoicePageUrl({
|
|
41
42
|
invoiceId,
|
|
@@ -124,7 +125,7 @@ export async function getInvoiceShouldPayTotal(invoice: Invoice) {
|
|
|
124
125
|
const setup = getSubscriptionCycleSetup(subscription.pending_invoice_item_interval, invoice.period_start);
|
|
125
126
|
let offset = 0;
|
|
126
127
|
let filterFunc = (item: any) => item;
|
|
127
|
-
if (
|
|
128
|
+
if (CHARGE_SUPPORTED_CHAIN_TYPES.includes(paymentMethod.type)) {
|
|
128
129
|
switch (invoice.billing_reason) {
|
|
129
130
|
case 'subscription_cancel':
|
|
130
131
|
filterFunc = (item: any) => item.price?.recurring?.usage_type === 'metered';
|
|
@@ -231,7 +232,7 @@ export async function getSetupInvoice(
|
|
|
231
232
|
if (!stakingProps?.tx_hash) {
|
|
232
233
|
return null;
|
|
233
234
|
}
|
|
234
|
-
if (paymentMethod
|
|
235
|
+
if (paymentMethod?.type !== 'arcblock') {
|
|
235
236
|
return null;
|
|
236
237
|
}
|
|
237
238
|
const firstInvoice = await Invoice.findOne({
|
|
@@ -715,7 +716,7 @@ export async function ensureOverdraftProtectionInvoiceAndItems({
|
|
|
715
716
|
props: Partial<TInvoice> & { period_start: number; period_end: number };
|
|
716
717
|
}): Promise<{ invoice: Invoice; items: InvoiceItem[] }> {
|
|
717
718
|
const { price, product } = await ensureOverdraftProtectionPrice(subscription.livemode);
|
|
718
|
-
const invoicePrice = price
|
|
719
|
+
const invoicePrice = price?.currency_options?.find((x: any) => x.currency_id === paymentIntent?.currency_id);
|
|
719
720
|
|
|
720
721
|
if (!subscription.overdraft_protection?.enabled) {
|
|
721
722
|
throw new Error('create overdraft protection invoice skipped due to overdraft protection not enabled');
|
|
@@ -757,7 +758,7 @@ export async function ensureOverdraftProtectionInvoiceAndItems({
|
|
|
757
758
|
total: invoicePrice.unit_amount || '0',
|
|
758
759
|
items: [
|
|
759
760
|
{
|
|
760
|
-
price_id: price
|
|
761
|
+
price_id: price?.id || '',
|
|
761
762
|
amount: invoicePrice.unit_amount || '0',
|
|
762
763
|
quantity: 1,
|
|
763
764
|
description: product?.name || '',
|
|
@@ -773,7 +774,7 @@ export async function ensureOverdraftProtectionInvoiceAndItems({
|
|
|
773
774
|
},
|
|
774
775
|
});
|
|
775
776
|
|
|
776
|
-
await Price.increment({ quantity_sold: 1 }, { where: { id: price
|
|
777
|
+
await Price.increment({ quantity_sold: 1 }, { where: { id: price?.id } });
|
|
777
778
|
return result;
|
|
778
779
|
}
|
|
779
780
|
|
|
@@ -9,6 +9,7 @@ import { translate } from '../../../locales';
|
|
|
9
9
|
import {
|
|
10
10
|
CheckoutSession,
|
|
11
11
|
Customer,
|
|
12
|
+
NFTMintChainType,
|
|
12
13
|
Invoice,
|
|
13
14
|
NftMintItem,
|
|
14
15
|
PaymentIntent,
|
|
@@ -109,7 +110,7 @@ export class SubscriptionRenewFailedEmailTemplate
|
|
|
109
110
|
|
|
110
111
|
const hasNft: boolean = checkoutSession?.nft_mint_status === 'minted';
|
|
111
112
|
const nftMintItem: NftMintItem | undefined = hasNft
|
|
112
|
-
? checkoutSession?.nft_mint_details?.[checkoutSession?.nft_mint_details?.type as
|
|
113
|
+
? checkoutSession?.nft_mint_details?.[checkoutSession?.nft_mint_details?.type as NFTMintChainType]
|
|
113
114
|
: undefined;
|
|
114
115
|
const paymentInfo: string = `${fromUnitToToken(invoice.amount_remaining, paymentCurrency.decimal)} ${
|
|
115
116
|
paymentCurrency.symbol
|
|
@@ -126,7 +127,7 @@ export class SubscriptionRenewFailedEmailTemplate
|
|
|
126
127
|
|
|
127
128
|
const paymentMethod: PaymentMethod | null = await PaymentMethod.findByPk(invoice.default_payment_method_id);
|
|
128
129
|
const chainHost: string | undefined =
|
|
129
|
-
paymentMethod?.settings?.[checkoutSession?.nft_mint_details?.type as
|
|
130
|
+
paymentMethod?.settings?.[checkoutSession?.nft_mint_details?.type as NFTMintChainType]?.api_host;
|
|
130
131
|
const viewSubscriptionLink = getCustomerSubscriptionPageUrl({
|
|
131
132
|
subscriptionId: subscription.id,
|
|
132
133
|
locale,
|
|
@@ -139,7 +140,7 @@ export class SubscriptionRenewFailedEmailTemplate
|
|
|
139
140
|
action: 'pay',
|
|
140
141
|
});
|
|
141
142
|
const txHash: string | undefined =
|
|
142
|
-
paymentIntent?.payment_details?.[checkoutSession?.nft_mint_details?.type as
|
|
143
|
+
paymentIntent?.payment_details?.[checkoutSession?.nft_mint_details?.type as NFTMintChainType]?.tx_hash;
|
|
143
144
|
const viewTxHashLink: string | undefined =
|
|
144
145
|
hasNft && txHash
|
|
145
146
|
? getExplorerLink({
|
|
@@ -8,6 +8,7 @@ import { translate } from '../../../locales';
|
|
|
8
8
|
import {
|
|
9
9
|
CheckoutSession,
|
|
10
10
|
Customer,
|
|
11
|
+
NFTMintChainType,
|
|
11
12
|
Invoice,
|
|
12
13
|
NftMintItem,
|
|
13
14
|
PaymentIntent,
|
|
@@ -98,7 +99,7 @@ export class SubscriptionRenewedEmailTemplate implements BaseEmailTemplate<Subsc
|
|
|
98
99
|
|
|
99
100
|
const hasNft: boolean = checkoutSession?.nft_mint_status === 'minted';
|
|
100
101
|
const nftMintItem: NftMintItem | undefined = hasNft
|
|
101
|
-
? checkoutSession?.nft_mint_details?.[checkoutSession?.nft_mint_details?.type as
|
|
102
|
+
? checkoutSession?.nft_mint_details?.[checkoutSession?.nft_mint_details?.type as NFTMintChainType]
|
|
102
103
|
: undefined;
|
|
103
104
|
const amountPaid = +fromUnitToToken(invoice.total, paymentCurrency.decimal);
|
|
104
105
|
const paymentInfo: string = `${fromUnitToToken(invoice.total, paymentCurrency.decimal)} ${paymentCurrency.symbol}`;
|
|
@@ -113,7 +114,7 @@ export class SubscriptionRenewedEmailTemplate implements BaseEmailTemplate<Subsc
|
|
|
113
114
|
|
|
114
115
|
const paymentMethod: PaymentMethod | null = await PaymentMethod.findByPk(invoice.default_payment_method_id);
|
|
115
116
|
const chainHost: string | undefined =
|
|
116
|
-
paymentMethod?.settings?.[checkoutSession?.nft_mint_details?.type as
|
|
117
|
+
paymentMethod?.settings?.[checkoutSession?.nft_mint_details?.type as NFTMintChainType]?.api_host;
|
|
117
118
|
const viewSubscriptionLink = getCustomerSubscriptionPageUrl({
|
|
118
119
|
subscriptionId: subscription.id,
|
|
119
120
|
locale,
|
|
@@ -125,7 +126,7 @@ export class SubscriptionRenewedEmailTemplate implements BaseEmailTemplate<Subsc
|
|
|
125
126
|
locale,
|
|
126
127
|
});
|
|
127
128
|
const txHash: string | undefined =
|
|
128
|
-
paymentIntent?.payment_details?.[checkoutSession?.nft_mint_details?.type as
|
|
129
|
+
paymentIntent?.payment_details?.[checkoutSession?.nft_mint_details?.type as NFTMintChainType]?.tx_hash;
|
|
129
130
|
const viewTxHashLink: string | undefined =
|
|
130
131
|
hasNft && txHash
|
|
131
132
|
? getExplorerLink({
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
Subscription,
|
|
17
17
|
PaymentCurrency,
|
|
18
18
|
Invoice,
|
|
19
|
+
NFTMintChainType,
|
|
19
20
|
} from '../../../store/models';
|
|
20
21
|
import { getCustomerInvoicePageUrl, getOneTimeProductInfo } from '../../invoice';
|
|
21
22
|
import { getMainProductName } from '../../product';
|
|
@@ -122,7 +123,7 @@ export class SubscriptionSucceededEmailTemplate
|
|
|
122
123
|
const paymentInfo: string = `${paymentAmount} ${paymentCurrency.symbol}`;
|
|
123
124
|
const hasNft: boolean = checkoutSession?.nft_mint_status === 'minted';
|
|
124
125
|
const nftMintItem: NftMintItem | undefined = hasNft
|
|
125
|
-
? checkoutSession?.nft_mint_details?.[checkoutSession?.nft_mint_details?.type as
|
|
126
|
+
? checkoutSession?.nft_mint_details?.[checkoutSession?.nft_mint_details?.type as NFTMintChainType]
|
|
126
127
|
: undefined;
|
|
127
128
|
const currentPeriodStart: string = formatTime((subscription.current_period_start || invoice.period_start) * 1000);
|
|
128
129
|
const currentPeriodEnd: string = formatTime((subscription.current_period_end || invoice.period_end) * 1000);
|
|
@@ -136,7 +137,7 @@ export class SubscriptionSucceededEmailTemplate
|
|
|
136
137
|
const paymentMethod: PaymentMethod | null = await PaymentMethod.findByPk(invoice.default_payment_method_id);
|
|
137
138
|
// @FIXME: 获取 chainHost 困难的一批?
|
|
138
139
|
const chainHost: string | undefined =
|
|
139
|
-
paymentMethod?.settings?.[checkoutSession?.nft_mint_details?.type as
|
|
140
|
+
paymentMethod?.settings?.[checkoutSession?.nft_mint_details?.type as NFTMintChainType]?.api_host;
|
|
140
141
|
const viewSubscriptionLink = getCustomerSubscriptionPageUrl({
|
|
141
142
|
subscriptionId: subscription.id,
|
|
142
143
|
locale,
|
|
@@ -150,7 +151,7 @@ export class SubscriptionSucceededEmailTemplate
|
|
|
150
151
|
|
|
151
152
|
const paymentIntent = await PaymentIntent.findByPk(invoice.payment_intent_id);
|
|
152
153
|
const txHash: string | undefined =
|
|
153
|
-
paymentIntent?.payment_details?.[checkoutSession?.nft_mint_details?.type as
|
|
154
|
+
paymentIntent?.payment_details?.[checkoutSession?.nft_mint_details?.type as NFTMintChainType]?.tx_hash;
|
|
154
155
|
const viewTxHashLink: string | undefined =
|
|
155
156
|
txHash &&
|
|
156
157
|
getExplorerLink({
|
|
@@ -7,7 +7,15 @@ import prettyMsI18n from 'pretty-ms-i18n';
|
|
|
7
7
|
import isEmpty from 'lodash/isEmpty';
|
|
8
8
|
import { getUserLocale } from '../../../integrations/blocklet/notification';
|
|
9
9
|
import { translate } from '../../../locales';
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
CheckoutSession,
|
|
12
|
+
Customer,
|
|
13
|
+
NFTMintChainType,
|
|
14
|
+
Invoice,
|
|
15
|
+
NftMintItem,
|
|
16
|
+
PaymentMethod,
|
|
17
|
+
Subscription,
|
|
18
|
+
} from '../../../store/models';
|
|
11
19
|
import { PaymentCurrency } from '../../../store/models/payment-currency';
|
|
12
20
|
import { getCustomerInvoicePageUrl, getOneTimeProductInfo } from '../../invoice';
|
|
13
21
|
import { getMainProductName } from '../../product';
|
|
@@ -106,11 +114,11 @@ export class SubscriptionTrialStartEmailTemplate
|
|
|
106
114
|
|
|
107
115
|
const hasNft: boolean = checkoutSession?.nft_mint_status === 'minted';
|
|
108
116
|
const nftMintItem: NftMintItem | undefined = hasNft
|
|
109
|
-
? checkoutSession?.nft_mint_details?.[checkoutSession?.nft_mint_details?.type as
|
|
117
|
+
? checkoutSession?.nft_mint_details?.[checkoutSession?.nft_mint_details?.type as NFTMintChainType]
|
|
110
118
|
: undefined;
|
|
111
119
|
const paymentMethod: PaymentMethod | null = await PaymentMethod.findByPk(invoice.default_payment_method_id);
|
|
112
120
|
const chainHost: string | undefined =
|
|
113
|
-
paymentMethod?.settings?.[checkoutSession?.nft_mint_details?.type as
|
|
121
|
+
paymentMethod?.settings?.[checkoutSession?.nft_mint_details?.type as NFTMintChainType]?.api_host;
|
|
114
122
|
const paymentInfo: string = `${fromUnitToToken('0', paymentCurrency.decimal)} ${paymentCurrency.symbol}`;
|
|
115
123
|
const currentPeriodStart: string = formatTime((subscription.trial_start as number) * 1000);
|
|
116
124
|
const currentPeriodEnd: string = formatTime((subscription.trial_end as number) * 1000);
|
|
@@ -8,6 +8,7 @@ import { translate } from '../../../locales';
|
|
|
8
8
|
import {
|
|
9
9
|
CheckoutSession,
|
|
10
10
|
Customer,
|
|
11
|
+
NFTMintChainType,
|
|
11
12
|
NftMintItem,
|
|
12
13
|
PaymentIntent,
|
|
13
14
|
PaymentMethod,
|
|
@@ -93,7 +94,7 @@ export class SubscriptionUpgradedEmailTemplate implements BaseEmailTemplate<Subs
|
|
|
93
94
|
)} ${paymentCurrency.symbol}`;
|
|
94
95
|
const hasNft: boolean = checkoutSession?.nft_mint_status === 'minted';
|
|
95
96
|
const nftMintItem: NftMintItem | undefined = hasNft
|
|
96
|
-
? checkoutSession?.nft_mint_details?.[checkoutSession?.nft_mint_details?.type as
|
|
97
|
+
? checkoutSession?.nft_mint_details?.[checkoutSession?.nft_mint_details?.type as NFTMintChainType]
|
|
97
98
|
: undefined;
|
|
98
99
|
const currentPeriodStart: string = formatTime(
|
|
99
100
|
skipInvoice ? subscription.current_period_start * 1000 : invoice.period_start * 1000
|
package/api/src/libs/payment.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type { TPaymentCurrency } from '../store/models/payment-currency';
|
|
|
15
15
|
import { blocklet, ethWallet, wallet } from './auth';
|
|
16
16
|
import logger from './logger';
|
|
17
17
|
import { OCAP_PAYMENT_TX_TYPE } from './util';
|
|
18
|
+
import { CHARGE_SUPPORTED_CHAIN_TYPES, EVM_CHAIN_TYPES } from './constants';
|
|
18
19
|
|
|
19
20
|
export interface SufficientForPaymentResult {
|
|
20
21
|
sufficient: boolean;
|
|
@@ -108,7 +109,7 @@ export async function isDelegationSufficientForPayment(args: {
|
|
|
108
109
|
return { sufficient: false, reason: 'NOT_SUPPORTED' };
|
|
109
110
|
}
|
|
110
111
|
|
|
111
|
-
if (paymentMethod.type
|
|
112
|
+
if (EVM_CHAIN_TYPES.includes(paymentMethod.type)) {
|
|
112
113
|
if (!paymentCurrency.contract) {
|
|
113
114
|
return { sufficient: false, reason: 'NOT_SUPPORTED' };
|
|
114
115
|
}
|
|
@@ -204,7 +205,7 @@ export async function getPaymentDetail(
|
|
|
204
205
|
return defaultResult;
|
|
205
206
|
}
|
|
206
207
|
const paymentSettings = invoice?.payment_settings;
|
|
207
|
-
if (
|
|
208
|
+
if (CHARGE_SUPPORTED_CHAIN_TYPES.includes(paymentMethod.type)) {
|
|
208
209
|
// balance enough token for payment?
|
|
209
210
|
const payer = paymentSettings?.payment_method_options.arcblock?.payer as string;
|
|
210
211
|
const result = await isDelegationSufficientForPayment({
|
|
@@ -295,7 +296,7 @@ export async function getTokenLimitsForDelegation(
|
|
|
295
296
|
return [entry];
|
|
296
297
|
}
|
|
297
298
|
|
|
298
|
-
if (paymentMethod.type
|
|
299
|
+
if (EVM_CHAIN_TYPES.includes(paymentMethod.type)) {
|
|
299
300
|
// FIXME: @wangshijun better control from customer.token_limits
|
|
300
301
|
entry.totalAllowance = new BN(amount).mul(new BN(12)).toString();
|
|
301
302
|
return [entry];
|
|
@@ -326,7 +327,7 @@ export async function isBalanceSufficientForRefund(args: {
|
|
|
326
327
|
return { sufficient: true, token };
|
|
327
328
|
}
|
|
328
329
|
|
|
329
|
-
if (paymentMethod.type
|
|
330
|
+
if (EVM_CHAIN_TYPES.includes(paymentMethod.type)) {
|
|
330
331
|
const provider = paymentMethod.getEvmClient();
|
|
331
332
|
const balance = paymentCurrency.contract
|
|
332
333
|
? await fetchErc20Balance(provider, paymentCurrency.contract, ethWallet.address)
|
package/api/src/libs/product.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import isEmpty from 'lodash/isEmpty';
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import { Op } from 'sequelize';
|
|
4
|
+
import { CheckoutSession, PaymentCurrency, PaymentMethod, Price, Product, Subscription } from '../store/models';
|
|
5
|
+
import { EVM_CHAIN_TYPES } from './constants';
|
|
4
6
|
|
|
5
7
|
export async function getMainProductName(subscriptionId: string): Promise<string> {
|
|
6
8
|
const subscription = await Subscription.findByPk(subscriptionId);
|
|
@@ -62,3 +64,24 @@ export async function getMainProductNameByCheckoutSession(checkoutSession: Check
|
|
|
62
64
|
|
|
63
65
|
return product.name;
|
|
64
66
|
}
|
|
67
|
+
|
|
68
|
+
export async function checkCurrencySupportRecurring(currencyIds: string[] | string, isRecurring: boolean) {
|
|
69
|
+
if (!isRecurring) {
|
|
70
|
+
return {
|
|
71
|
+
notSupportCurrencies: [],
|
|
72
|
+
validate: true,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const currencyIdsArray = Array.isArray(currencyIds) ? currencyIds : [currencyIds];
|
|
76
|
+
const currencies = (await PaymentCurrency.findAll({
|
|
77
|
+
where: { id: { [Op.in]: currencyIdsArray } },
|
|
78
|
+
include: [{ model: PaymentMethod, as: 'payment_method' }],
|
|
79
|
+
})) as (PaymentCurrency & { payment_method: PaymentMethod })[];
|
|
80
|
+
const notSupportCurrencies = currencies.filter(
|
|
81
|
+
(c) => EVM_CHAIN_TYPES.includes(c.payment_method?.type) && c.symbol === 'ETH'
|
|
82
|
+
);
|
|
83
|
+
return {
|
|
84
|
+
notSupportCurrencies,
|
|
85
|
+
validate: notSupportCurrencies?.length === 0,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
@@ -33,12 +33,13 @@ import { Payout } from '../store/models/payout';
|
|
|
33
33
|
import { Price } from '../store/models/price';
|
|
34
34
|
import { Subscription } from '../store/models/subscription';
|
|
35
35
|
import { SubscriptionItem } from '../store/models/subscription-item';
|
|
36
|
-
import type { PaymentError, PaymentSettings } from '../store/models/types';
|
|
36
|
+
import type { EVMChainType, PaymentError, PaymentSettings } from '../store/models/types';
|
|
37
37
|
import { notificationQueue } from './notification';
|
|
38
38
|
import { ensureOverdraftProtectionInvoiceAndItems } from '../libs/invoice';
|
|
39
39
|
import { Lock } from '../store/models';
|
|
40
40
|
import { ensureOverdraftProtectionPrice } from '../libs/overdraft-protection';
|
|
41
41
|
import createQueue from '../libs/queue';
|
|
42
|
+
import { EVM_CHAIN_TYPES } from '../libs/constants';
|
|
42
43
|
|
|
43
44
|
type PaymentJob = {
|
|
44
45
|
paymentIntentId: string;
|
|
@@ -391,7 +392,7 @@ export const handlePaymentFailed = async (
|
|
|
391
392
|
const { enabled: enableOverdraftProtection, unused: unusedAmount } =
|
|
392
393
|
await isSubscriptionOverdraftProtectionEnabled(subscription);
|
|
393
394
|
const { price } = await ensureOverdraftProtectionPrice(subscription.livemode);
|
|
394
|
-
const invoicePrice = (price
|
|
395
|
+
const invoicePrice = (price?.currency_options || []).find((x: any) => x.currency_id === paymentIntent?.currency_id);
|
|
395
396
|
|
|
396
397
|
if (subscription.overdraft_protection?.enabled && !enableOverdraftProtection) {
|
|
397
398
|
// overdraft protection is enabled but not enough
|
|
@@ -776,13 +777,14 @@ export const handlePayment = async (job: PaymentJob) => {
|
|
|
776
777
|
await handlePaymentSucceed(paymentIntent);
|
|
777
778
|
}
|
|
778
779
|
|
|
779
|
-
if (paymentMethod.type
|
|
780
|
+
if (EVM_CHAIN_TYPES.includes(paymentMethod.type)) {
|
|
780
781
|
if (!paymentCurrency.contract) {
|
|
781
782
|
throw new Error('Payment capture not supported for ethereum payment currencies without contract');
|
|
782
783
|
}
|
|
783
784
|
|
|
784
785
|
const client = paymentMethod.getEvmClient();
|
|
785
|
-
const
|
|
786
|
+
const paymentType = paymentMethod.type;
|
|
787
|
+
const payer = paymentSettings?.payment_method_options[paymentType as EVMChainType]?.payer as string;
|
|
786
788
|
|
|
787
789
|
// check balance before capture with transaction
|
|
788
790
|
result = await isDelegationSufficientForPayment({
|
|
@@ -805,7 +807,7 @@ export const handlePayment = async (job: PaymentJob) => {
|
|
|
805
807
|
last_payment_error: null,
|
|
806
808
|
amount_received: paymentIntent.amount,
|
|
807
809
|
payment_details: {
|
|
808
|
-
|
|
810
|
+
[paymentType]: {
|
|
809
811
|
tx_hash: receipt.hash,
|
|
810
812
|
payer: payer as string,
|
|
811
813
|
block_height: receipt.blockNumber.toString(),
|
package/api/src/queues/refund.ts
CHANGED
|
@@ -14,7 +14,8 @@ import { PaymentIntent } from '../store/models/payment-intent';
|
|
|
14
14
|
import { PaymentMethod } from '../store/models/payment-method';
|
|
15
15
|
import { Refund } from '../store/models/refund';
|
|
16
16
|
import { Subscription } from '../store/models/subscription';
|
|
17
|
-
import type { PaymentError } from '../store/models/types';
|
|
17
|
+
import type { EVMChainType, PaymentError } from '../store/models/types';
|
|
18
|
+
import { EVM_CHAIN_TYPES } from '../libs/constants';
|
|
18
19
|
|
|
19
20
|
type RefundJob = {
|
|
20
21
|
refundId: string;
|
|
@@ -117,7 +118,7 @@ const handleRefundJob = async (
|
|
|
117
118
|
paymentMethod: PaymentMethod,
|
|
118
119
|
customer: Customer
|
|
119
120
|
) => {
|
|
120
|
-
const refundSupport = ['arcblock', 'ethereum', 'stripe'].includes(paymentMethod.type);
|
|
121
|
+
const refundSupport = ['arcblock', 'ethereum', 'stripe', 'base'].includes(paymentMethod.type);
|
|
121
122
|
if (refundSupport === false) {
|
|
122
123
|
logger.warn(`PaymentMethod does not support auto charge: ${paymentCurrency.payment_method_id}`);
|
|
123
124
|
return;
|
|
@@ -183,9 +184,10 @@ const handleRefundJob = async (
|
|
|
183
184
|
logger.info('Refund status updated to succeeded', { id: refund.id, txHash });
|
|
184
185
|
}
|
|
185
186
|
|
|
186
|
-
if (paymentMethod.type
|
|
187
|
+
if (EVM_CHAIN_TYPES.includes(paymentMethod.type)) {
|
|
188
|
+
const paymentType = paymentMethod.type;
|
|
187
189
|
if (!paymentCurrency.contract) {
|
|
188
|
-
throw new Error(
|
|
190
|
+
throw new Error(`Refund not supported for ${paymentType} payment currencies without contract`);
|
|
189
191
|
}
|
|
190
192
|
|
|
191
193
|
// check balance before transfer with transaction
|
|
@@ -197,7 +199,7 @@ const handleRefundJob = async (
|
|
|
197
199
|
|
|
198
200
|
// do the capture
|
|
199
201
|
const client = paymentMethod.getEvmClient();
|
|
200
|
-
const payer = paymentIntent!.payment_details?.
|
|
202
|
+
const payer = paymentIntent!.payment_details?.[paymentType as EVMChainType]?.payer as string;
|
|
201
203
|
const receipt = await sendErc20ToUser(client, paymentCurrency.contract, payer, refund.amount);
|
|
202
204
|
logger.info('refund transfer done', { id: refund.id, txHash: receipt.hash });
|
|
203
205
|
|
|
@@ -205,7 +207,7 @@ const handleRefundJob = async (
|
|
|
205
207
|
status: 'succeeded',
|
|
206
208
|
last_attempt_error: null,
|
|
207
209
|
payment_details: {
|
|
208
|
-
|
|
210
|
+
[paymentType]: {
|
|
209
211
|
tx_hash: receipt.hash,
|
|
210
212
|
payer: wallet.address,
|
|
211
213
|
block_height: receipt.blockNumber.toString(),
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
updateStripeSubscriptionAfterChangePayment,
|
|
12
12
|
} from './shared';
|
|
13
13
|
import { ensureStakeInvoice } from '../../libs/invoice';
|
|
14
|
+
import { EVM_CHAIN_TYPES } from '../../libs/constants';
|
|
14
15
|
|
|
15
16
|
export default {
|
|
16
17
|
action: 'change-payment',
|
|
@@ -65,7 +66,7 @@ export default {
|
|
|
65
66
|
return claims;
|
|
66
67
|
}
|
|
67
68
|
|
|
68
|
-
if (paymentMethod.type
|
|
69
|
+
if (EVM_CHAIN_TYPES.includes(paymentMethod.type)) {
|
|
69
70
|
if (!paymentCurrency.contract) {
|
|
70
71
|
throw new Error(`Payment currency ${paymentMethod.type}:${paymentCurrency.id} does not support subscription`);
|
|
71
72
|
}
|
|
@@ -169,7 +170,7 @@ export default {
|
|
|
169
170
|
return { hash: paymentDetails.tx_hash };
|
|
170
171
|
}
|
|
171
172
|
|
|
172
|
-
if (paymentMethod.type
|
|
173
|
+
if (EVM_CHAIN_TYPES.includes(paymentMethod.type)) {
|
|
173
174
|
await prepareTxExecution();
|
|
174
175
|
const paymentDetails = await executeEvmTransaction('approve', userDid, claims, paymentMethod);
|
|
175
176
|
waitForEvmTxConfirm(paymentMethod.getEvmClient(), +paymentDetails.block_height, paymentMethod.confirmation.block)
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
getStakeTxClaim,
|
|
16
16
|
} from './shared';
|
|
17
17
|
import { ensureStakeInvoice } from '../../libs/invoice';
|
|
18
|
+
import { EVM_CHAIN_TYPES } from '../../libs/constants';
|
|
18
19
|
|
|
19
20
|
export default {
|
|
20
21
|
action: 'change-plan',
|
|
@@ -79,7 +80,7 @@ export default {
|
|
|
79
80
|
return claims;
|
|
80
81
|
}
|
|
81
82
|
|
|
82
|
-
if (paymentMethod.type
|
|
83
|
+
if (EVM_CHAIN_TYPES.includes(paymentMethod.type)) {
|
|
83
84
|
if (!paymentCurrency.contract) {
|
|
84
85
|
throw new Error(`Payment currency ${paymentMethod.type}:${paymentCurrency.id} does not support subscription`);
|
|
85
86
|
}
|
|
@@ -177,7 +178,7 @@ export default {
|
|
|
177
178
|
return { hash: paymentDetails.tx_hash };
|
|
178
179
|
}
|
|
179
180
|
|
|
180
|
-
if (paymentMethod.type
|
|
181
|
+
if (EVM_CHAIN_TYPES.includes(paymentMethod.type)) {
|
|
181
182
|
await prepareTxExecution();
|
|
182
183
|
|
|
183
184
|
const paymentDetails = await executeEvmTransaction('approve', userDid, claims, paymentMethod);
|
|
@@ -11,8 +11,9 @@ import { getGasPayerExtra } from '../../libs/payment';
|
|
|
11
11
|
import { getTxMetadata } from '../../libs/util';
|
|
12
12
|
import { invoiceQueue } from '../../queues/invoice';
|
|
13
13
|
import { handlePaymentSucceed, paymentQueue } from '../../queues/payment';
|
|
14
|
-
import { PaymentIntent } from '../../store/models';
|
|
14
|
+
import { EVMChainType, PaymentIntent } from '../../store/models';
|
|
15
15
|
import { ensureSubscriptionForCollectBatch, getAuthPrincipalClaim } from './shared';
|
|
16
|
+
import { EVM_CHAIN_TYPES } from '../../libs/constants';
|
|
16
17
|
|
|
17
18
|
// Used to collect uncollectible invoices for a subscription and currency
|
|
18
19
|
export default {
|
|
@@ -62,14 +63,14 @@ export default {
|
|
|
62
63
|
return claims;
|
|
63
64
|
}
|
|
64
65
|
|
|
65
|
-
if (paymentMethod.type
|
|
66
|
+
if (EVM_CHAIN_TYPES.includes(paymentMethod.type)) {
|
|
66
67
|
return {
|
|
67
68
|
signature: {
|
|
68
69
|
type: 'eth:transaction',
|
|
69
70
|
data: toBase58(
|
|
70
71
|
Buffer.from(
|
|
71
72
|
JSON.stringify({
|
|
72
|
-
network: paymentMethod.settings?.
|
|
73
|
+
network: paymentMethod.settings[paymentMethod.type as EVMChainType]?.chain_id,
|
|
73
74
|
tx: encodeTransferItx(ethWallet.address, amount!, paymentCurrency.contract),
|
|
74
75
|
}),
|
|
75
76
|
'utf-8'
|
|
@@ -139,7 +140,7 @@ export default {
|
|
|
139
140
|
|
|
140
141
|
return { hash: txHash };
|
|
141
142
|
}
|
|
142
|
-
if (paymentMethod.type
|
|
143
|
+
if (EVM_CHAIN_TYPES.includes(paymentMethod.type)) {
|
|
143
144
|
const client = paymentMethod.getEvmClient();
|
|
144
145
|
const claim = claims.find((x) => x.type === 'signature');
|
|
145
146
|
|
|
@@ -12,8 +12,9 @@ import { onSubscriptionUpdateConnected } from '../../libs/subscription';
|
|
|
12
12
|
import { getTxMetadata } from '../../libs/util';
|
|
13
13
|
import { invoiceQueue } from '../../queues/invoice';
|
|
14
14
|
import { handlePaymentSucceed, paymentQueue } from '../../queues/payment';
|
|
15
|
-
import { Price, Subscription, SubscriptionItem } from '../../store/models';
|
|
15
|
+
import { EVMChainType, Price, Subscription, SubscriptionItem } from '../../store/models';
|
|
16
16
|
import { ensureInvoiceForCollect, getAuthPrincipalClaim, getDelegationTxClaim } from './shared';
|
|
17
|
+
import { EVM_CHAIN_TYPES } from '../../libs/constants';
|
|
17
18
|
|
|
18
19
|
// Used to collect an open invoice failed to collect automatically
|
|
19
20
|
export default {
|
|
@@ -73,10 +74,10 @@ export default {
|
|
|
73
74
|
return claims;
|
|
74
75
|
}
|
|
75
76
|
|
|
76
|
-
if (paymentMethod.type
|
|
77
|
+
if (EVM_CHAIN_TYPES.includes(paymentMethod.type)) {
|
|
77
78
|
// FIXME: @wangshijun can we support this
|
|
78
79
|
if (action === 'renew' && invoice.subscription_id) {
|
|
79
|
-
throw new Error(
|
|
80
|
+
throw new Error(`Renew subscription on collect payment not supported for ${paymentMethod.type}`);
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
return {
|
|
@@ -85,7 +86,7 @@ export default {
|
|
|
85
86
|
data: toBase58(
|
|
86
87
|
Buffer.from(
|
|
87
88
|
JSON.stringify({
|
|
88
|
-
network: paymentMethod.settings?.
|
|
89
|
+
network: paymentMethod.settings[paymentMethod.type as EVMChainType]?.chain_id,
|
|
89
90
|
tx: encodeTransferItx(ethWallet.address, paymentIntent.amount, paymentCurrency.contract),
|
|
90
91
|
}),
|
|
91
92
|
'utf-8'
|
|
@@ -170,7 +171,7 @@ export default {
|
|
|
170
171
|
}
|
|
171
172
|
}
|
|
172
173
|
|
|
173
|
-
if (paymentMethod.type
|
|
174
|
+
if (EVM_CHAIN_TYPES.includes(paymentMethod.type)) {
|
|
174
175
|
await paymentIntent.update({ status: 'processing' });
|
|
175
176
|
const client = paymentMethod.getEvmClient();
|
|
176
177
|
const claim = claims.find((x) => x.type === 'signature');
|
|
@@ -11,6 +11,8 @@ import { createPaymentOutput } from '../../libs/session';
|
|
|
11
11
|
import { getTxMetadata } from '../../libs/util';
|
|
12
12
|
import { handlePaymentSucceed } from '../../queues/payment';
|
|
13
13
|
import { ensureInvoiceForCheckout, ensurePaymentIntent, getAuthPrincipalClaim } from './shared';
|
|
14
|
+
import { EVMChainType } from '../../store/models';
|
|
15
|
+
import { EVM_CHAIN_TYPES } from '../../libs/constants';
|
|
14
16
|
|
|
15
17
|
export default {
|
|
16
18
|
action: 'payment',
|
|
@@ -63,14 +65,14 @@ export default {
|
|
|
63
65
|
};
|
|
64
66
|
}
|
|
65
67
|
|
|
66
|
-
if (paymentMethod.type
|
|
68
|
+
if (EVM_CHAIN_TYPES.includes(paymentMethod.type)) {
|
|
67
69
|
return {
|
|
68
70
|
signature: {
|
|
69
71
|
type: 'eth:transaction',
|
|
70
72
|
data: toBase58(
|
|
71
73
|
Buffer.from(
|
|
72
74
|
JSON.stringify({
|
|
73
|
-
network: paymentMethod.settings?.
|
|
75
|
+
network: paymentMethod.settings[paymentMethod.type as EVMChainType]?.chain_id,
|
|
74
76
|
tx: encodeTransferItx(ethWallet.address, paymentIntent.amount, paymentCurrency.contract),
|
|
75
77
|
}),
|
|
76
78
|
'utf-8'
|
|
@@ -140,7 +142,7 @@ export default {
|
|
|
140
142
|
}
|
|
141
143
|
}
|
|
142
144
|
|
|
143
|
-
if (paymentMethod.type
|
|
145
|
+
if (EVM_CHAIN_TYPES.includes(paymentMethod.type)) {
|
|
144
146
|
try {
|
|
145
147
|
await paymentIntent.update({ status: 'processing' });
|
|
146
148
|
|
|
@@ -164,7 +166,10 @@ export default {
|
|
|
164
166
|
return { hash: paymentDetails.tx_hash };
|
|
165
167
|
} catch (err) {
|
|
166
168
|
console.error(err);
|
|
167
|
-
logger.error(
|
|
169
|
+
logger.error(`Failed to finalize paymentIntent on ${paymentMethod.type}`, {
|
|
170
|
+
paymentIntent: paymentIntent.id,
|
|
171
|
+
error: err,
|
|
172
|
+
});
|
|
168
173
|
await paymentIntent.update({ status: 'requires_capture' });
|
|
169
174
|
throw err;
|
|
170
175
|
}
|