payment-kit 1.24.4 → 1.25.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api/src/index.ts +3 -0
- package/api/src/libs/credit-utils.ts +21 -0
- package/api/src/libs/discount/discount.ts +13 -0
- package/api/src/libs/env.ts +5 -0
- package/api/src/libs/error.ts +14 -0
- package/api/src/libs/exchange-rate/coingecko-provider.ts +193 -0
- package/api/src/libs/exchange-rate/coinmarketcap-provider.ts +180 -0
- package/api/src/libs/exchange-rate/index.ts +5 -0
- package/api/src/libs/exchange-rate/service.ts +583 -0
- package/api/src/libs/exchange-rate/token-address-mapping.ts +84 -0
- package/api/src/libs/exchange-rate/token-addresses.json +2147 -0
- package/api/src/libs/exchange-rate/token-data-provider.ts +142 -0
- package/api/src/libs/exchange-rate/types.ts +114 -0
- package/api/src/libs/exchange-rate/validator.ts +319 -0
- package/api/src/libs/invoice-quote.ts +158 -0
- package/api/src/libs/invoice.ts +143 -7
- package/api/src/libs/math-utils.ts +46 -0
- package/api/src/libs/notification/template/billing-discrepancy.ts +3 -4
- package/api/src/libs/notification/template/customer-auto-recharge-failed.ts +174 -79
- package/api/src/libs/notification/template/customer-credit-grant-granted.ts +2 -3
- package/api/src/libs/notification/template/customer-credit-insufficient.ts +3 -3
- package/api/src/libs/notification/template/customer-credit-low-balance.ts +3 -3
- package/api/src/libs/notification/template/customer-revenue-succeeded.ts +2 -3
- package/api/src/libs/notification/template/customer-reward-succeeded.ts +9 -4
- package/api/src/libs/notification/template/exchange-rate-alert.ts +202 -0
- package/api/src/libs/notification/template/subscription-slippage-exceeded.ts +203 -0
- package/api/src/libs/notification/template/subscription-slippage-warning.ts +212 -0
- package/api/src/libs/notification/template/subscription-will-canceled.ts +2 -2
- package/api/src/libs/notification/template/subscription-will-renew.ts +22 -8
- package/api/src/libs/payment.ts +3 -1
- package/api/src/libs/price.ts +4 -1
- package/api/src/libs/queue/index.ts +8 -0
- package/api/src/libs/quote-service.ts +1132 -0
- package/api/src/libs/quote-validation.ts +388 -0
- package/api/src/libs/session.ts +686 -39
- package/api/src/libs/slippage.ts +135 -0
- package/api/src/libs/subscription.ts +185 -15
- package/api/src/libs/util.ts +64 -3
- package/api/src/locales/en.ts +50 -0
- package/api/src/locales/zh.ts +48 -0
- package/api/src/queues/auto-recharge.ts +295 -21
- package/api/src/queues/exchange-rate-health.ts +242 -0
- package/api/src/queues/invoice.ts +48 -1
- package/api/src/queues/notification.ts +167 -1
- package/api/src/queues/payment.ts +177 -7
- package/api/src/queues/refund.ts +41 -9
- package/api/src/queues/subscription.ts +436 -6
- package/api/src/routes/auto-recharge-configs.ts +71 -6
- package/api/src/routes/checkout-sessions.ts +1730 -81
- package/api/src/routes/connect/auto-recharge-auth.ts +2 -0
- package/api/src/routes/connect/change-payer.ts +2 -0
- package/api/src/routes/connect/change-payment.ts +61 -8
- package/api/src/routes/connect/change-plan.ts +161 -17
- package/api/src/routes/connect/collect.ts +9 -6
- package/api/src/routes/connect/delegation.ts +1 -0
- package/api/src/routes/connect/pay.ts +157 -0
- package/api/src/routes/connect/setup.ts +32 -10
- package/api/src/routes/connect/shared.ts +159 -13
- package/api/src/routes/connect/subscribe.ts +32 -9
- package/api/src/routes/credit-grants.ts +99 -0
- package/api/src/routes/exchange-rate-providers.ts +248 -0
- package/api/src/routes/exchange-rates.ts +87 -0
- package/api/src/routes/index.ts +4 -0
- package/api/src/routes/invoices.ts +280 -2
- package/api/src/routes/payment-links.ts +13 -0
- package/api/src/routes/prices.ts +84 -2
- package/api/src/routes/subscriptions.ts +526 -15
- package/api/src/store/migrations/20251220-dynamic-pricing.ts +245 -0
- package/api/src/store/migrations/20251223-exchange-rate-provider-type.ts +28 -0
- package/api/src/store/migrations/20260110-add-quote-locked-at.ts +23 -0
- package/api/src/store/migrations/20260112-add-checkout-session-slippage-percent.ts +22 -0
- package/api/src/store/migrations/20260113-add-price-quote-slippage-fields.ts +45 -0
- package/api/src/store/migrations/20260116-subscription-slippage.ts +21 -0
- package/api/src/store/migrations/20260120-auto-recharge-slippage.ts +21 -0
- package/api/src/store/models/auto-recharge-config.ts +12 -0
- package/api/src/store/models/checkout-session.ts +7 -0
- package/api/src/store/models/exchange-rate-provider.ts +225 -0
- package/api/src/store/models/index.ts +6 -0
- package/api/src/store/models/payment-intent.ts +6 -0
- package/api/src/store/models/price-quote.ts +284 -0
- package/api/src/store/models/price.ts +53 -5
- package/api/src/store/models/subscription.ts +11 -0
- package/api/src/store/models/types.ts +61 -1
- package/api/tests/libs/change-payment-plan.spec.ts +282 -0
- package/api/tests/libs/exchange-rate-service.spec.ts +341 -0
- package/api/tests/libs/quote-service.spec.ts +199 -0
- package/api/tests/libs/session.spec.ts +464 -0
- package/api/tests/libs/slippage.spec.ts +109 -0
- package/api/tests/libs/token-data-provider.spec.ts +267 -0
- package/api/tests/models/exchange-rate-provider.spec.ts +121 -0
- package/api/tests/models/price-dynamic.spec.ts +100 -0
- package/api/tests/models/price-quote.spec.ts +112 -0
- package/api/tests/routes/exchange-rate-providers.spec.ts +215 -0
- package/api/tests/routes/subscription-slippage.spec.ts +254 -0
- package/blocklet.yml +1 -1
- package/package.json +7 -6
- package/src/components/customer/credit-overview.tsx +14 -0
- package/src/components/discount/discount-info.tsx +8 -2
- package/src/components/invoice/list.tsx +146 -16
- package/src/components/invoice/table.tsx +276 -71
- package/src/components/invoice-pdf/template.tsx +3 -7
- package/src/components/metadata/form.tsx +6 -8
- package/src/components/price/form.tsx +519 -149
- package/src/components/promotion/active-redemptions.tsx +5 -3
- package/src/components/quote/info.tsx +234 -0
- package/src/hooks/subscription.ts +132 -2
- package/src/locales/en.tsx +145 -0
- package/src/locales/zh.tsx +143 -1
- package/src/pages/admin/billing/invoices/detail.tsx +41 -4
- package/src/pages/admin/products/exchange-rate-providers/edit-dialog.tsx +354 -0
- package/src/pages/admin/products/exchange-rate-providers/index.tsx +363 -0
- package/src/pages/admin/products/index.tsx +12 -1
- package/src/pages/customer/invoice/detail.tsx +36 -12
- package/src/pages/customer/subscription/change-payment.tsx +65 -3
- package/src/pages/customer/subscription/change-plan.tsx +207 -38
- package/src/pages/customer/subscription/detail.tsx +599 -419
|
@@ -7,7 +7,7 @@ import pick from 'lodash/pick';
|
|
|
7
7
|
import uniq from 'lodash/uniq';
|
|
8
8
|
|
|
9
9
|
import { literal, Op, OrderItem } from 'sequelize';
|
|
10
|
-
import { BN } from '@ocap/util';
|
|
10
|
+
import { BN, fromTokenToUnit } from '@ocap/util';
|
|
11
11
|
import { createEvent } from '../libs/audit';
|
|
12
12
|
import { ensureStripeCustomer, ensureStripePrice, ensureStripeSubscription } from '../integrations/stripe/resource';
|
|
13
13
|
import { createListParamSchema, getOrder, getWhereFromKvQuery, getWhereFromQuery, MetadataSchema } from '../libs/api';
|
|
@@ -15,7 +15,15 @@ import dayjs from '../libs/dayjs';
|
|
|
15
15
|
import logger from '../libs/logger';
|
|
16
16
|
import { isDelegationSufficientForPayment } from '../libs/payment';
|
|
17
17
|
import { authenticate } from '../libs/security';
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
expandLineItems,
|
|
20
|
+
getFastCheckoutAmount,
|
|
21
|
+
getSubscriptionCreateSetup,
|
|
22
|
+
isLineItemAligned,
|
|
23
|
+
SlippageOptions,
|
|
24
|
+
} from '../libs/session';
|
|
25
|
+
import { getExchangeRateService } from '../libs/exchange-rate/service';
|
|
26
|
+
import { getExchangeRateSymbol } from '../libs/exchange-rate/token-address-mapping';
|
|
19
27
|
import {
|
|
20
28
|
checkRemainingStake,
|
|
21
29
|
createProration,
|
|
@@ -30,6 +38,7 @@ import {
|
|
|
30
38
|
isSubscriptionOverdraftProtectionEnabled,
|
|
31
39
|
} from '../libs/subscription';
|
|
32
40
|
import { MAX_SUBSCRIPTION_ITEM_COUNT, formatMetadata } from '../libs/util';
|
|
41
|
+
import { trimDecimals, limitTokenPrecision } from '../libs/math-utils';
|
|
33
42
|
import { invoiceQueue } from '../queues/invoice';
|
|
34
43
|
import {
|
|
35
44
|
addSubscriptionJob,
|
|
@@ -39,7 +48,7 @@ import {
|
|
|
39
48
|
slashStakeQueue,
|
|
40
49
|
subscriptionQueue,
|
|
41
50
|
} from '../queues/subscription';
|
|
42
|
-
import type { TLineItemExpanded } from '../store/models';
|
|
51
|
+
import type { TLineItemExpanded, ChainType } from '../store/models';
|
|
43
52
|
import { Customer } from '../store/models/customer';
|
|
44
53
|
import { Invoice } from '../store/models/invoice';
|
|
45
54
|
import { InvoiceItem } from '../store/models/invoice-item';
|
|
@@ -48,6 +57,7 @@ import { PaymentCurrency } from '../store/models/payment-currency';
|
|
|
48
57
|
import { PaymentIntent } from '../store/models/payment-intent';
|
|
49
58
|
import { PaymentMethod } from '../store/models/payment-method';
|
|
50
59
|
import { Price } from '../store/models/price';
|
|
60
|
+
import { PriceQuote } from '../store/models/price-quote';
|
|
51
61
|
import { PricingTable } from '../store/models/pricing-table';
|
|
52
62
|
import { Product } from '../store/models/product';
|
|
53
63
|
import { SetupIntent } from '../store/models/setup-intent';
|
|
@@ -70,6 +80,7 @@ import { getSubscriptionDiscountStats } from '../libs/discount/redemption';
|
|
|
70
80
|
const router = Router();
|
|
71
81
|
const auth = authenticate<Subscription>({ component: true, roles: ['owner', 'admin'] });
|
|
72
82
|
const authMine = authenticate<Subscription>({ component: true, roles: ['owner', 'admin'], mine: true });
|
|
83
|
+
const exchangeRateService = getExchangeRateService();
|
|
73
84
|
const authPortal = authenticate<Subscription>({
|
|
74
85
|
component: true,
|
|
75
86
|
embed: true,
|
|
@@ -98,6 +109,7 @@ const schema = createListParamSchema<{
|
|
|
98
109
|
activeFirst?: boolean;
|
|
99
110
|
price_id?: string;
|
|
100
111
|
showTotalCount?: boolean;
|
|
112
|
+
include_latest_invoice_quote?: boolean;
|
|
101
113
|
}>({
|
|
102
114
|
status: Joi.string().empty(''),
|
|
103
115
|
customer_id: Joi.string().empty(''),
|
|
@@ -105,8 +117,121 @@ const schema = createListParamSchema<{
|
|
|
105
117
|
activeFirst: Joi.boolean().optional(),
|
|
106
118
|
price_id: Joi.string().empty(''),
|
|
107
119
|
showTotalCount: Joi.boolean().optional(),
|
|
120
|
+
include_latest_invoice_quote: Joi.boolean().optional(),
|
|
108
121
|
});
|
|
109
122
|
|
|
123
|
+
const buildQuoteMetadata = (quote: PriceQuote) => {
|
|
124
|
+
const slippagePercent = quote.slippage_percent ?? (quote.metadata as any)?.slippage?.percent ?? null;
|
|
125
|
+
const minAcceptableRate = quote.min_acceptable_rate ?? (quote.metadata as any)?.slippage?.min_acceptable_rate ?? null;
|
|
126
|
+
const maxPayableToken = quote.max_payable_token ?? (quote.metadata as any)?.slippage?.max_payable_token ?? null;
|
|
127
|
+
const derivedAtMs = quote.slippage_derived_at_ms ?? (quote.metadata as any)?.slippage?.derived_at_ms ?? null;
|
|
128
|
+
const degraded = (quote.metadata as any)?.risk?.degraded ?? null;
|
|
129
|
+
const degradedReason = (quote.metadata as any)?.risk?.degraded_reason ?? null;
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
id: quote.id,
|
|
133
|
+
base_currency: quote.base_currency,
|
|
134
|
+
base_amount: quote.base_amount,
|
|
135
|
+
quoted_amount: quote.quoted_amount,
|
|
136
|
+
target_currency_id: quote.target_currency_id,
|
|
137
|
+
rate_currency_symbol: quote.rate_currency_symbol,
|
|
138
|
+
exchange_rate: quote.exchange_rate,
|
|
139
|
+
rate_provider_name: quote.rate_provider_name,
|
|
140
|
+
rate_provider_id: quote.rate_provider_id,
|
|
141
|
+
rate_timestamp_ms: quote.rate_timestamp_ms,
|
|
142
|
+
consensus_method: (quote.metadata as any)?.rate?.consensus_method || null,
|
|
143
|
+
providers: (quote.metadata as any)?.rate?.providers || [],
|
|
144
|
+
degraded,
|
|
145
|
+
degraded_reason: degradedReason,
|
|
146
|
+
slippage_percent: slippagePercent,
|
|
147
|
+
min_acceptable_rate: minAcceptableRate,
|
|
148
|
+
max_payable_token: maxPayableToken,
|
|
149
|
+
derived_at_ms: derivedAtMs,
|
|
150
|
+
status: quote.status,
|
|
151
|
+
};
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
const attachQuoteMetadataToLines = (lines: any[] | undefined, quotesById: Map<string, PriceQuote>) => {
|
|
155
|
+
if (!lines?.length) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
lines.forEach((line) => {
|
|
159
|
+
const quoteId = line?.metadata?.quote_id;
|
|
160
|
+
if (!quoteId) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
const quote = quotesById.get(quoteId);
|
|
164
|
+
if (!quote) {
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
line.metadata = {
|
|
168
|
+
...(line.metadata || {}),
|
|
169
|
+
quote: {
|
|
170
|
+
...(line.metadata?.quote || {}),
|
|
171
|
+
...buildQuoteMetadata(quote),
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
});
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
const attachLatestInvoiceQuotes = async (subscriptions: any[]) => {
|
|
178
|
+
if (!subscriptions?.length) {
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const invoiceIds = uniq(subscriptions.map((sub) => sub.latest_invoice_id).filter(Boolean));
|
|
183
|
+
if (invoiceIds.length === 0) {
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const invoices = await Invoice.findAll({
|
|
188
|
+
where: { id: invoiceIds },
|
|
189
|
+
include: [
|
|
190
|
+
{
|
|
191
|
+
model: InvoiceItem,
|
|
192
|
+
as: 'lines',
|
|
193
|
+
attributes: ['id', 'metadata', 'amount', 'quantity', 'price_id'],
|
|
194
|
+
},
|
|
195
|
+
],
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
if (!invoices.length) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const invoiceDocs = invoices.map((invoice) => invoice.toJSON()) as any[];
|
|
203
|
+
const quoteIds = new Set<string>();
|
|
204
|
+
invoiceDocs.forEach((invoice) => {
|
|
205
|
+
invoice.lines?.forEach((line: any) => {
|
|
206
|
+
const quoteId = line?.metadata?.quote_id;
|
|
207
|
+
if (typeof quoteId === 'string' && quoteId.length > 0) {
|
|
208
|
+
quoteIds.add(quoteId);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
if (quoteIds.size > 0) {
|
|
214
|
+
const quotes = await PriceQuote.findAll({ where: { id: Array.from(quoteIds) } });
|
|
215
|
+
const quotesById = new Map(quotes.map((quote) => [quote.id, quote]));
|
|
216
|
+
invoiceDocs.forEach((invoice) => {
|
|
217
|
+
attachQuoteMetadataToLines(invoice.lines, quotesById);
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const invoiceById = new Map(invoiceDocs.map((invoice) => [invoice.id, invoice]));
|
|
222
|
+
subscriptions.forEach((subscription) => {
|
|
223
|
+
const invoiceId = subscription.latest_invoice_id;
|
|
224
|
+
if (!invoiceId) {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const invoice = invoiceById.get(invoiceId);
|
|
228
|
+
if (!invoice) {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
subscription.latest_invoice = pick(invoice, ['id', 'status', 'created_at', 'lines']);
|
|
232
|
+
});
|
|
233
|
+
};
|
|
234
|
+
|
|
110
235
|
// Create subscription directly (for SDK use)
|
|
111
236
|
const createSchema = Joi.object({
|
|
112
237
|
customer_id: Joi.string().required(),
|
|
@@ -325,10 +450,18 @@ router.post('/', auth, async (req, res) => {
|
|
|
325
450
|
});
|
|
326
451
|
|
|
327
452
|
router.get('/', authMine, async (req, res) => {
|
|
328
|
-
const {
|
|
453
|
+
const {
|
|
454
|
+
page,
|
|
455
|
+
pageSize,
|
|
456
|
+
status,
|
|
457
|
+
livemode,
|
|
458
|
+
include_latest_invoice_quote: includeLatestInvoiceQuoteParam = false,
|
|
459
|
+
...query
|
|
460
|
+
} = await schema.validateAsync(req.query, {
|
|
329
461
|
stripUnknown: false,
|
|
330
462
|
allowUnknown: true,
|
|
331
463
|
});
|
|
464
|
+
const includeLatestInvoiceQuote = includeLatestInvoiceQuoteParam === true;
|
|
332
465
|
const where = getWhereFromKvQuery(query.q);
|
|
333
466
|
|
|
334
467
|
if (status) {
|
|
@@ -395,6 +528,9 @@ router.get('/', authMine, async (req, res) => {
|
|
|
395
528
|
const docs = list.map((x) => x.toJSON());
|
|
396
529
|
// @ts-ignore
|
|
397
530
|
docs.forEach((x) => expandLineItems(x.items, products, prices));
|
|
531
|
+
if (includeLatestInvoiceQuote) {
|
|
532
|
+
await attachLatestInvoiceQuotes(docs);
|
|
533
|
+
}
|
|
398
534
|
|
|
399
535
|
if (query.showTotalCount) {
|
|
400
536
|
const totalCount = await Subscription.count({
|
|
@@ -417,14 +553,25 @@ router.get('/', authMine, async (req, res) => {
|
|
|
417
553
|
// search subscriptions
|
|
418
554
|
const searchSchema = createListParamSchema<{
|
|
419
555
|
query: string;
|
|
556
|
+
include_latest_invoice_quote?: boolean;
|
|
420
557
|
}>({
|
|
421
558
|
query: Joi.string(),
|
|
559
|
+
include_latest_invoice_quote: Joi.boolean().optional(),
|
|
422
560
|
});
|
|
423
561
|
router.get('/search', auth, async (req, res) => {
|
|
424
|
-
const {
|
|
562
|
+
const {
|
|
563
|
+
page,
|
|
564
|
+
pageSize,
|
|
565
|
+
query,
|
|
566
|
+
livemode,
|
|
567
|
+
q,
|
|
568
|
+
o,
|
|
569
|
+
include_latest_invoice_quote: includeLatestInvoiceQuoteParam = false,
|
|
570
|
+
} = await searchSchema.validateAsync(req.query, {
|
|
425
571
|
stripUnknown: false,
|
|
426
572
|
allowUnknown: true,
|
|
427
573
|
});
|
|
574
|
+
const includeLatestInvoiceQuote = includeLatestInvoiceQuoteParam === true;
|
|
428
575
|
|
|
429
576
|
const where = q != null ? getWhereFromKvQuery(q) : getWhereFromQuery(query);
|
|
430
577
|
if (typeof livemode === 'boolean') {
|
|
@@ -450,6 +597,9 @@ router.get('/search', auth, async (req, res) => {
|
|
|
450
597
|
const docs = list.map((x) => x.toJSON());
|
|
451
598
|
// @ts-ignore
|
|
452
599
|
docs.forEach((x) => expandLineItems(x.items, products, prices));
|
|
600
|
+
if (includeLatestInvoiceQuote) {
|
|
601
|
+
await attachLatestInvoiceQuotes(docs);
|
|
602
|
+
}
|
|
453
603
|
res.json({ count, list: docs, paging: { page, pageSize } });
|
|
454
604
|
});
|
|
455
605
|
|
|
@@ -1250,7 +1400,64 @@ router.put('/:id', authPortal, async (req, res) => {
|
|
|
1250
1400
|
} else {
|
|
1251
1401
|
// update subscription period settings
|
|
1252
1402
|
// HINT: if we are adding new items, we need to reset the anchor to now
|
|
1253
|
-
|
|
1403
|
+
// For change-plan (proration), we need exact amounts, not authorization amounts with slippage buffer.
|
|
1404
|
+
// So we don't pass minAcceptableRate or slippage percent here.
|
|
1405
|
+
// The custom_amount we set below will be used as-is.
|
|
1406
|
+
const slippageOptions: SlippageOptions = {
|
|
1407
|
+
percent: 0, // No slippage multiplier for actual payment
|
|
1408
|
+
// Don't include minAcceptableRate - it would override custom_amount calculation
|
|
1409
|
+
currencyDecimal: paymentCurrency.decimal,
|
|
1410
|
+
};
|
|
1411
|
+
|
|
1412
|
+
// For dynamic pricing items, calculate custom_amount using current exchange rate
|
|
1413
|
+
// This ensures the total is calculated with current rate, not the stale unit_amount
|
|
1414
|
+
const hasDynamicPricing = newItems.some((x) => (x.upsell_price || x.price).pricing_type === 'dynamic');
|
|
1415
|
+
if (hasDynamicPricing) {
|
|
1416
|
+
const currencyPaymentMethod =
|
|
1417
|
+
(paymentCurrency as any).payment_method ||
|
|
1418
|
+
(await PaymentMethod.findByPk(paymentCurrency.payment_method_id));
|
|
1419
|
+
const rateSymbol = getExchangeRateSymbol(paymentCurrency.symbol, currencyPaymentMethod?.type as ChainType);
|
|
1420
|
+
try {
|
|
1421
|
+
const rateResult = await exchangeRateService.getRate(rateSymbol);
|
|
1422
|
+
if (rateResult?.rate) {
|
|
1423
|
+
const USD_DECIMALS = 8;
|
|
1424
|
+
const currentRate = rateResult.rate;
|
|
1425
|
+
// Set custom_amount for each dynamic pricing item
|
|
1426
|
+
newItems.forEach((item: any) => {
|
|
1427
|
+
const price = item.upsell_price || item.price;
|
|
1428
|
+
if (price.pricing_type === 'dynamic' && price.base_amount) {
|
|
1429
|
+
// Calculate: base_amount / rate * 10^decimal
|
|
1430
|
+
// Use trimDecimals to avoid "too many decimal places" error
|
|
1431
|
+
const baseAmountBN = fromTokenToUnit(trimDecimals(price.base_amount, USD_DECIMALS), USD_DECIMALS);
|
|
1432
|
+
const rateBN = fromTokenToUnit(trimDecimals(currentRate, USD_DECIMALS), USD_DECIMALS);
|
|
1433
|
+
if (rateBN.gt(new BN(0))) {
|
|
1434
|
+
const amountBN = baseAmountBN
|
|
1435
|
+
.mul(new BN(10).pow(new BN(paymentCurrency.decimal)))
|
|
1436
|
+
.add(rateBN.sub(new BN(1))) // Round up
|
|
1437
|
+
.div(rateBN);
|
|
1438
|
+
// Apply same precision limiting as quote service (10 significant decimal places)
|
|
1439
|
+
const totalAmountBN = amountBN.mul(new BN(item.quantity));
|
|
1440
|
+
item.custom_amount = limitTokenPrecision(totalAmountBN, paymentCurrency.decimal, 10).toString();
|
|
1441
|
+
logger.info('Set custom_amount for dynamic pricing item in subscription update', {
|
|
1442
|
+
subscriptionId: subscription.id,
|
|
1443
|
+
priceId: price.id,
|
|
1444
|
+
baseAmount: price.base_amount,
|
|
1445
|
+
currentRate,
|
|
1446
|
+
customAmount: item.custom_amount,
|
|
1447
|
+
});
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
});
|
|
1451
|
+
}
|
|
1452
|
+
} catch (err) {
|
|
1453
|
+
logger.warn('Failed to fetch exchange rate for subscription update, using unit_amount', {
|
|
1454
|
+
subscriptionId: subscription.id,
|
|
1455
|
+
error: err,
|
|
1456
|
+
});
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
const setup = getSubscriptionCreateSetup(newItems, paymentCurrency.id, 0, 0, slippageOptions);
|
|
1254
1461
|
// Check if the subscription is currently in trial
|
|
1255
1462
|
const isInTrial =
|
|
1256
1463
|
subscription.status === 'trialing' && subscription.trial_end && subscription.trial_end > dayjs().unix();
|
|
@@ -1354,17 +1561,49 @@ router.put('/:id', authPortal, async (req, res) => {
|
|
|
1354
1561
|
|
|
1355
1562
|
// 5. check do we need to connect
|
|
1356
1563
|
let hasNext = true;
|
|
1357
|
-
|
|
1564
|
+
let needsNewStake = false;
|
|
1565
|
+
|
|
1566
|
+
// Check if stake is required and if we need a new one
|
|
1567
|
+
const requiresStake = paymentMethod.type === 'arcblock' && !subscription.billing_thresholds?.no_stake;
|
|
1568
|
+
if (requiresStake) {
|
|
1569
|
+
const existingStakeAddress = subscription.payment_details?.arcblock?.staking?.address;
|
|
1570
|
+
if (existingStakeAddress) {
|
|
1571
|
+
const stakeCheck = await checkRemainingStake(paymentMethod, paymentCurrency, existingStakeAddress, '1');
|
|
1572
|
+
needsNewStake = !stakeCheck.enough;
|
|
1573
|
+
logger.info('Change plan: checking existing stake in API', {
|
|
1574
|
+
subscriptionId: subscription.id,
|
|
1575
|
+
existingStakeAddress,
|
|
1576
|
+
hasValidStake: stakeCheck.enough,
|
|
1577
|
+
needsNewStake,
|
|
1578
|
+
});
|
|
1579
|
+
} else {
|
|
1580
|
+
needsNewStake = true;
|
|
1581
|
+
}
|
|
1582
|
+
}
|
|
1583
|
+
|
|
1584
|
+
if (due === '0' && !needsNewStake) {
|
|
1358
1585
|
hasNext = false;
|
|
1359
1586
|
} else {
|
|
1360
1587
|
const payer = getSubscriptionPaymentAddress(subscription, paymentMethod.type);
|
|
1588
|
+
// For change-plan, we should check if delegation is sufficient for the due amount,
|
|
1589
|
+
// not the full new plan price. The due amount is what user actually needs to pay.
|
|
1361
1590
|
const delegation = await isDelegationSufficientForPayment({
|
|
1362
1591
|
paymentMethod,
|
|
1363
1592
|
paymentCurrency,
|
|
1364
1593
|
userDid: payer,
|
|
1365
|
-
amount:
|
|
1594
|
+
amount: due || '0',
|
|
1595
|
+
});
|
|
1596
|
+
logger.info('delegation sufficient for payment', {
|
|
1597
|
+
subscription: subscription.id,
|
|
1598
|
+
paymentMethod: paymentMethod.type,
|
|
1599
|
+
paymentCurrency: paymentCurrency.id,
|
|
1600
|
+
userDid: payer,
|
|
1601
|
+
amount: due,
|
|
1602
|
+
delegation,
|
|
1603
|
+
needsNewStake,
|
|
1366
1604
|
});
|
|
1367
|
-
if (delegation.sufficient) {
|
|
1605
|
+
if (delegation.sufficient && !needsNewStake) {
|
|
1606
|
+
// Both delegation is sufficient and no new stake needed
|
|
1368
1607
|
hasNext = false;
|
|
1369
1608
|
} else if (['NO_DID_WALLET'].includes(delegation.reason as string)) {
|
|
1370
1609
|
throw new Error('Subscription update can only be done when you do have connected DID Wallet');
|
|
@@ -1645,6 +1884,8 @@ router.post('/:id/change-plan', authPortal, async (req, res) => {
|
|
|
1645
1884
|
const { newItems } = await validateSubscriptionUpdateRequest(subscription, req.body.items);
|
|
1646
1885
|
|
|
1647
1886
|
// do the simulation
|
|
1887
|
+
// Note: For dynamic pricing, the actual amount is calculated by frontend using current exchange rate
|
|
1888
|
+
// Backend only provides the base structure, frontend handles display with real-time rates
|
|
1648
1889
|
const setup = getSubscriptionCreateSetup(newItems, subscription.currency_id, 0);
|
|
1649
1890
|
const result = await createProration(subscription, setup, dayjs().unix());
|
|
1650
1891
|
|
|
@@ -1756,6 +1997,260 @@ router.get('/:id/change-payment', authPortal, async (req, res) => {
|
|
|
1756
1997
|
return res.json({ subscription, setupIntent });
|
|
1757
1998
|
});
|
|
1758
1999
|
|
|
2000
|
+
router.get('/:id/exchange-rate', authPortal, async (req, res) => {
|
|
2001
|
+
try {
|
|
2002
|
+
const subscription = await Subscription.findByPk(req.params.id);
|
|
2003
|
+
if (!subscription) {
|
|
2004
|
+
return res.status(404).json({ error: 'Subscription not found' });
|
|
2005
|
+
}
|
|
2006
|
+
|
|
2007
|
+
const currencyId = (req.query.currency_id as string) || subscription.currency_id;
|
|
2008
|
+
const paymentCurrency = (await PaymentCurrency.findByPk(currencyId, {
|
|
2009
|
+
include: [
|
|
2010
|
+
{
|
|
2011
|
+
model: PaymentMethod,
|
|
2012
|
+
as: 'payment_method',
|
|
2013
|
+
},
|
|
2014
|
+
],
|
|
2015
|
+
})) as PaymentCurrency & { payment_method: PaymentMethod };
|
|
2016
|
+
|
|
2017
|
+
if (!paymentCurrency) {
|
|
2018
|
+
return res.status(400).json({ error: 'Currency not found' });
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
const paymentMethod =
|
|
2022
|
+
paymentCurrency.payment_method || (await PaymentMethod.findByPk(paymentCurrency.payment_method_id));
|
|
2023
|
+
if (!paymentMethod) {
|
|
2024
|
+
return res.status(400).json({ error: 'Payment method not found' });
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
if (paymentMethod.type === 'stripe') {
|
|
2028
|
+
return res.status(400).json({ error: 'Stripe currency does not require exchange rate.' });
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2031
|
+
const rateSymbol = getExchangeRateSymbol(paymentCurrency.symbol, paymentMethod.type as any);
|
|
2032
|
+
const rateResult = await exchangeRateService.getRate(rateSymbol);
|
|
2033
|
+
const serverNow = Date.now();
|
|
2034
|
+
|
|
2035
|
+
return res.json({
|
|
2036
|
+
server_now: serverNow,
|
|
2037
|
+
rate: rateResult.rate,
|
|
2038
|
+
timestamp_ms: rateResult.timestamp_ms,
|
|
2039
|
+
fetched_at: rateResult.fetched_at,
|
|
2040
|
+
provider_id: rateResult.provider_id,
|
|
2041
|
+
provider_name: rateResult.provider_name,
|
|
2042
|
+
providers: rateResult.providers,
|
|
2043
|
+
consensus_method: rateResult.consensus_method,
|
|
2044
|
+
degraded: rateResult.degraded,
|
|
2045
|
+
degraded_reason: rateResult.degraded_reason,
|
|
2046
|
+
currency: paymentCurrency.symbol,
|
|
2047
|
+
base_currency: 'USD',
|
|
2048
|
+
});
|
|
2049
|
+
} catch (err: any) {
|
|
2050
|
+
logger.error('Failed to fetch exchange rate for subscription change payment', {
|
|
2051
|
+
subscriptionId: req.params.id,
|
|
2052
|
+
error: err.message,
|
|
2053
|
+
});
|
|
2054
|
+
return res.status(400).json({ error: err.message });
|
|
2055
|
+
}
|
|
2056
|
+
});
|
|
2057
|
+
|
|
2058
|
+
router.put('/:id/slippage', authPortal, async (req, res) => {
|
|
2059
|
+
try {
|
|
2060
|
+
const subscription = await Subscription.findByPk(req.params.id);
|
|
2061
|
+
if (!subscription) {
|
|
2062
|
+
return res.status(404).json({ error: 'Subscription not found' });
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
const { slippage_percent: slippagePercent } = req.body;
|
|
2066
|
+
const rawConfig = req.body?.slippage_config || req.body?.slippage || null;
|
|
2067
|
+
const normalizePercent = (value: any) => {
|
|
2068
|
+
const normalized = typeof value === 'string' ? Number(value) : value;
|
|
2069
|
+
// Only validate that it's a non-negative finite number, no upper limit
|
|
2070
|
+
if (!Number.isFinite(normalized) || normalized < 0) {
|
|
2071
|
+
return null;
|
|
2072
|
+
}
|
|
2073
|
+
return normalized;
|
|
2074
|
+
};
|
|
2075
|
+
|
|
2076
|
+
// Helper: get current exchange rate for subscription currency
|
|
2077
|
+
const getCurrentRate = async (): Promise<{ rate: string; baseCurrency: string } | null> => {
|
|
2078
|
+
const currency = (await PaymentCurrency.findByPk(subscription.currency_id, {
|
|
2079
|
+
include: [{ model: PaymentMethod, as: 'payment_method' }],
|
|
2080
|
+
})) as PaymentCurrency & { payment_method: PaymentMethod };
|
|
2081
|
+
if (!currency || currency.payment_method?.type === 'stripe') {
|
|
2082
|
+
return null;
|
|
2083
|
+
}
|
|
2084
|
+
const rateService = getExchangeRateService();
|
|
2085
|
+
const rateSymbol = getExchangeRateSymbol(currency.symbol, currency.payment_method?.type as ChainType);
|
|
2086
|
+
const rateResult = await rateService.getRate(rateSymbol);
|
|
2087
|
+
return { rate: rateResult.rate, baseCurrency: 'USD' };
|
|
2088
|
+
};
|
|
2089
|
+
|
|
2090
|
+
// Helper: calculate min_acceptable_rate from percent
|
|
2091
|
+
const calcMinRateFromPercent = (percent: number, currentRate: string): string => {
|
|
2092
|
+
const rateNum = Number(currentRate);
|
|
2093
|
+
if (!Number.isFinite(rateNum) || rateNum <= 0) {
|
|
2094
|
+
return '0';
|
|
2095
|
+
}
|
|
2096
|
+
// min_acceptable_rate = current_rate / (1 + percent/100)
|
|
2097
|
+
const minRate = rateNum / (1 + percent / 100);
|
|
2098
|
+
return minRate.toFixed(8);
|
|
2099
|
+
};
|
|
2100
|
+
|
|
2101
|
+
let config: any = null;
|
|
2102
|
+
if (rawConfig && typeof rawConfig === 'object') {
|
|
2103
|
+
const mode = rawConfig.mode === 'rate' ? 'rate' : 'percent';
|
|
2104
|
+
const minRate = rawConfig.min_acceptable_rate ?? rawConfig.minAcceptableRate;
|
|
2105
|
+
|
|
2106
|
+
// For rate mode, min_acceptable_rate is required; percent is derived
|
|
2107
|
+
if (mode === 'rate') {
|
|
2108
|
+
if (minRate === undefined || minRate === null || minRate === '') {
|
|
2109
|
+
return res.status(400).json({ error: 'min_acceptable_rate is required for rate mode' });
|
|
2110
|
+
}
|
|
2111
|
+
// Accept any non-negative percent value for rate mode (calculated from rate)
|
|
2112
|
+
const percent = normalizePercent(rawConfig.percent);
|
|
2113
|
+
const baseCurrency = rawConfig.base_currency ?? rawConfig.baseCurrency ?? 'USD';
|
|
2114
|
+
const rateInfo = await getCurrentRate();
|
|
2115
|
+
config = {
|
|
2116
|
+
mode,
|
|
2117
|
+
percent: percent ?? 0,
|
|
2118
|
+
min_acceptable_rate: String(minRate),
|
|
2119
|
+
base_currency: String(baseCurrency),
|
|
2120
|
+
...(rateInfo ? { rate_at_config_time: rateInfo.rate } : {}),
|
|
2121
|
+
updated_at_ms: Date.now(),
|
|
2122
|
+
};
|
|
2123
|
+
} else {
|
|
2124
|
+
// Percent mode: validate percent
|
|
2125
|
+
const percent = normalizePercent(rawConfig.percent);
|
|
2126
|
+
if (percent === null) {
|
|
2127
|
+
return res.status(400).json({ error: 'slippage_percent must be a non-negative number' });
|
|
2128
|
+
}
|
|
2129
|
+
const baseCurrency = rawConfig.base_currency ?? rawConfig.baseCurrency ?? 'USD';
|
|
2130
|
+
// Use min_acceptable_rate from frontend if provided, otherwise calculate it
|
|
2131
|
+
const frontendMinRate = rawConfig.min_acceptable_rate ?? rawConfig.minAcceptableRate;
|
|
2132
|
+
let minAcceptableRate: string | undefined;
|
|
2133
|
+
let rateAtConfigTime: string | undefined;
|
|
2134
|
+
if (frontendMinRate) {
|
|
2135
|
+
// Frontend already calculated it - use directly
|
|
2136
|
+
minAcceptableRate = String(frontendMinRate);
|
|
2137
|
+
} else {
|
|
2138
|
+
// Frontend didn't provide - calculate using same algorithm
|
|
2139
|
+
const rateInfo = await getCurrentRate();
|
|
2140
|
+
if (rateInfo) {
|
|
2141
|
+
minAcceptableRate = calcMinRateFromPercent(percent, rateInfo.rate);
|
|
2142
|
+
rateAtConfigTime = rateInfo.rate;
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
config = {
|
|
2146
|
+
mode,
|
|
2147
|
+
percent,
|
|
2148
|
+
base_currency: String(baseCurrency),
|
|
2149
|
+
...(minAcceptableRate ? { min_acceptable_rate: minAcceptableRate } : {}),
|
|
2150
|
+
...(rateAtConfigTime ? { rate_at_config_time: rateAtConfigTime } : {}),
|
|
2151
|
+
updated_at_ms: Date.now(),
|
|
2152
|
+
};
|
|
2153
|
+
}
|
|
2154
|
+
} else if (slippagePercent !== undefined && slippagePercent !== null) {
|
|
2155
|
+
const value = normalizePercent(slippagePercent);
|
|
2156
|
+
if (value === null) {
|
|
2157
|
+
return res.status(400).json({ error: 'slippage_percent must be a non-negative number' });
|
|
2158
|
+
}
|
|
2159
|
+
const rateInfo = await getCurrentRate();
|
|
2160
|
+
const minAcceptableRate = rateInfo ? calcMinRateFromPercent(value, rateInfo.rate) : undefined;
|
|
2161
|
+
config = {
|
|
2162
|
+
mode: 'percent',
|
|
2163
|
+
percent: value,
|
|
2164
|
+
base_currency: 'USD',
|
|
2165
|
+
...(minAcceptableRate ? { min_acceptable_rate: minAcceptableRate } : {}),
|
|
2166
|
+
...(rateInfo ? { rate_at_config_time: rateInfo.rate } : {}),
|
|
2167
|
+
updated_at_ms: Date.now(),
|
|
2168
|
+
};
|
|
2169
|
+
} else {
|
|
2170
|
+
return res.status(400).json({ error: 'slippage config is required' });
|
|
2171
|
+
}
|
|
2172
|
+
|
|
2173
|
+
await subscription.update({ slippage_config: config });
|
|
2174
|
+
logger.info('Subscription slippage updated', {
|
|
2175
|
+
subscriptionId: subscription.id,
|
|
2176
|
+
slippageConfig: config,
|
|
2177
|
+
});
|
|
2178
|
+
|
|
2179
|
+
// Check if authorization is sufficient with the new slippage config
|
|
2180
|
+
let delegationWarning: {
|
|
2181
|
+
sufficient: boolean;
|
|
2182
|
+
reason?: string;
|
|
2183
|
+
required_amount?: string;
|
|
2184
|
+
current_allowance?: string;
|
|
2185
|
+
} | null = null;
|
|
2186
|
+
|
|
2187
|
+
try {
|
|
2188
|
+
const paymentCurrency = (await PaymentCurrency.findByPk(subscription.currency_id, {
|
|
2189
|
+
include: [{ model: PaymentMethod, as: 'payment_method' }],
|
|
2190
|
+
})) as PaymentCurrency & { payment_method: PaymentMethod };
|
|
2191
|
+
|
|
2192
|
+
// Only check delegation for non-Stripe payment methods
|
|
2193
|
+
if (paymentCurrency && paymentCurrency.payment_method?.type !== 'stripe') {
|
|
2194
|
+
const paymentMethod = paymentCurrency.payment_method;
|
|
2195
|
+
const payer = getSubscriptionPaymentAddress(subscription, paymentMethod.type);
|
|
2196
|
+
|
|
2197
|
+
// Get subscription items and expand them
|
|
2198
|
+
const subscriptionItems = await SubscriptionItem.findAll({
|
|
2199
|
+
where: { subscription_id: subscription.id },
|
|
2200
|
+
});
|
|
2201
|
+
const lineItems = await Price.expand(subscriptionItems.map((x) => x.toJSON()));
|
|
2202
|
+
|
|
2203
|
+
// Calculate the required amount with the new slippage
|
|
2204
|
+
const requiredAmount = await getFastCheckoutAmount({
|
|
2205
|
+
items: lineItems,
|
|
2206
|
+
mode: 'subscription',
|
|
2207
|
+
currencyId: paymentCurrency.id,
|
|
2208
|
+
trialing: subscription.status === 'trialing',
|
|
2209
|
+
});
|
|
2210
|
+
|
|
2211
|
+
// Check if delegation is sufficient
|
|
2212
|
+
const delegation = await isDelegationSufficientForPayment({
|
|
2213
|
+
paymentMethod,
|
|
2214
|
+
paymentCurrency,
|
|
2215
|
+
userDid: payer,
|
|
2216
|
+
amount: requiredAmount,
|
|
2217
|
+
});
|
|
2218
|
+
|
|
2219
|
+
if (!delegation.sufficient) {
|
|
2220
|
+
delegationWarning = {
|
|
2221
|
+
sufficient: false,
|
|
2222
|
+
reason: delegation.reason || 'INSUFFICIENT_AUTHORIZATION',
|
|
2223
|
+
required_amount: requiredAmount,
|
|
2224
|
+
};
|
|
2225
|
+
logger.info('Slippage update may require re-authorization', {
|
|
2226
|
+
subscriptionId: subscription.id,
|
|
2227
|
+
newSlippagePercent: config.percent,
|
|
2228
|
+
requiredAmount,
|
|
2229
|
+
delegationReason: delegation.reason,
|
|
2230
|
+
});
|
|
2231
|
+
}
|
|
2232
|
+
}
|
|
2233
|
+
} catch (delegationError: any) {
|
|
2234
|
+
// Don't fail the slippage update if delegation check fails
|
|
2235
|
+
logger.warn('Failed to check delegation after slippage update', {
|
|
2236
|
+
subscriptionId: subscription.id,
|
|
2237
|
+
error: delegationError.message,
|
|
2238
|
+
});
|
|
2239
|
+
}
|
|
2240
|
+
|
|
2241
|
+
return res.json({
|
|
2242
|
+
...subscription.toJSON(),
|
|
2243
|
+
...(delegationWarning ? { delegation_warning: delegationWarning } : {}),
|
|
2244
|
+
});
|
|
2245
|
+
} catch (err: any) {
|
|
2246
|
+
logger.error('Failed to update subscription slippage', {
|
|
2247
|
+
subscriptionId: req.params.id,
|
|
2248
|
+
error: err.message,
|
|
2249
|
+
});
|
|
2250
|
+
return res.status(500).json({ error: err.message });
|
|
2251
|
+
}
|
|
2252
|
+
});
|
|
2253
|
+
|
|
1759
2254
|
// Prepare setupIntent for payment change
|
|
1760
2255
|
router.post('/:id/change-payment', authPortal, async (req, res) => {
|
|
1761
2256
|
try {
|
|
@@ -1964,16 +2459,32 @@ router.post('/:id/change-payment', authPortal, async (req, res) => {
|
|
|
1964
2459
|
} else {
|
|
1965
2460
|
// changing from crypto to crypto: just update the subscription
|
|
1966
2461
|
const payer = getSubscriptionPaymentAddress(subscription, paymentMethod.type);
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
2462
|
+
|
|
2463
|
+
// Calculate required amount considering slippage_config for dynamic pricing
|
|
2464
|
+
const slippageConfig = subscription?.slippage_config;
|
|
2465
|
+
let requiredAmount: string;
|
|
2466
|
+
if (slippageConfig?.min_acceptable_rate) {
|
|
2467
|
+
const slippageOptions: SlippageOptions = {
|
|
2468
|
+
percent: slippageConfig.percent ?? 0.5,
|
|
2469
|
+
minAcceptableRate: slippageConfig.min_acceptable_rate,
|
|
2470
|
+
currencyDecimal: paymentCurrency.decimal,
|
|
2471
|
+
};
|
|
2472
|
+
const setup = getSubscriptionCreateSetup(lineItems, paymentCurrency.id, 0, 0, slippageOptions);
|
|
2473
|
+
requiredAmount = setup.amount.setup;
|
|
2474
|
+
} else {
|
|
2475
|
+
requiredAmount = await getFastCheckoutAmount({
|
|
1972
2476
|
items: lineItems,
|
|
1973
2477
|
mode: 'subscription',
|
|
1974
2478
|
currencyId: paymentCurrency.id,
|
|
1975
2479
|
trialing: false,
|
|
1976
|
-
})
|
|
2480
|
+
});
|
|
2481
|
+
}
|
|
2482
|
+
|
|
2483
|
+
delegation = await isDelegationSufficientForPayment({
|
|
2484
|
+
paymentMethod,
|
|
2485
|
+
paymentCurrency,
|
|
2486
|
+
userDid: payer,
|
|
2487
|
+
amount: requiredAmount,
|
|
1977
2488
|
});
|
|
1978
2489
|
const noStake = subscription.billing_thresholds?.no_stake;
|
|
1979
2490
|
if (paymentMethod.type === 'arcblock' && delegation.sufficient && !noStake) {
|