payment-kit 1.24.3 → 1.25.0
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/overdue-detection.ts +10 -1
- 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 +1 -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 +190 -3
- package/api/src/queues/payment.ts +177 -7
- 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/meter-events.ts +3 -0
- 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
|
@@ -3,16 +3,21 @@ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
|
3
3
|
import {
|
|
4
4
|
Status,
|
|
5
5
|
api,
|
|
6
|
-
|
|
6
|
+
formatAmount,
|
|
7
|
+
formatExchangeRate,
|
|
8
|
+
formatUsdAmount,
|
|
7
9
|
formatTime,
|
|
8
10
|
getInvoiceStatusColor,
|
|
11
|
+
getUsdAmountFromTokenUnits,
|
|
9
12
|
Table,
|
|
10
13
|
useDefaultPageSize,
|
|
11
14
|
getInvoiceDescriptionAndReason,
|
|
12
15
|
getTxLink,
|
|
13
16
|
} from '@blocklet/payment-react';
|
|
14
17
|
import type { TInvoiceExpanded } from '@blocklet/payment-types';
|
|
15
|
-
import { Avatar, CircularProgress, Typography } from '@mui/material';
|
|
18
|
+
import { Avatar, CircularProgress, Stack, Tooltip, Typography } from '@mui/material';
|
|
19
|
+
import WarningAmberIcon from '@mui/icons-material/WarningAmber';
|
|
20
|
+
import { BN } from '@ocap/util';
|
|
16
21
|
import { useEffect, useState } from 'react';
|
|
17
22
|
import { Link, useSearchParams } from 'react-router-dom';
|
|
18
23
|
import { useCacheState } from '../../hooks/cache-state';
|
|
@@ -20,10 +25,22 @@ import CustomerLink from '../customer/link';
|
|
|
20
25
|
import FilterToolbar from '../filter-toolbar';
|
|
21
26
|
import InvoiceActions from './action';
|
|
22
27
|
|
|
28
|
+
const getInvoiceQuoteInfo = (invoice: TInvoiceExpanded) => {
|
|
29
|
+
const lines = (invoice as any).lines || [];
|
|
30
|
+
for (const line of lines) {
|
|
31
|
+
const quote = (line.metadata as any)?.quote;
|
|
32
|
+
if (quote?.exchange_rate) {
|
|
33
|
+
return quote;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
};
|
|
38
|
+
|
|
23
39
|
const fetchData = (params: Record<string, any> = {}): Promise<{ list: TInvoiceExpanded[]; count: number }> => {
|
|
24
40
|
const search = new URLSearchParams();
|
|
25
|
-
|
|
26
|
-
|
|
41
|
+
const mergedParams: Record<string, any> = { include_quote: true, ...params };
|
|
42
|
+
Object.keys(mergedParams).forEach((key) => {
|
|
43
|
+
let v = mergedParams[key];
|
|
27
44
|
if (v === undefined || v === null || v === '') {
|
|
28
45
|
return;
|
|
29
46
|
}
|
|
@@ -195,17 +212,116 @@ export default function InvoiceList({
|
|
|
195
212
|
options: {
|
|
196
213
|
customBodyRenderLite: (_: string, index: number) => {
|
|
197
214
|
const item = data.list[index] as TInvoiceExpanded;
|
|
215
|
+
const quoteInfo = getInvoiceQuoteInfo(item);
|
|
216
|
+
const providers = quoteInfo?.providers || [];
|
|
217
|
+
const providerNames = providers.map((provider: any) => provider.provider_name).filter(Boolean);
|
|
218
|
+
// Unified provider display format: "provider_name (n sources)" for multiple, specific name for single
|
|
219
|
+
let providerDisplay = '—';
|
|
220
|
+
if (providers.length > 1) {
|
|
221
|
+
providerDisplay = `${providerNames[0] || providers[0]?.provider_id || 'data'} (${providers.length} sources)`;
|
|
222
|
+
} else if (providers.length === 1) {
|
|
223
|
+
providerDisplay = providerNames[0] || providers[0]?.provider_id || '—';
|
|
224
|
+
} else {
|
|
225
|
+
providerDisplay = quoteInfo?.rate_provider_name || quoteInfo?.rate_provider_id || '—';
|
|
226
|
+
}
|
|
227
|
+
const consensusMethod = quoteInfo?.consensus_method || (providers.length > 1 ? 'median' : 'single');
|
|
228
|
+
const rateTimestamp = quoteInfo?.rate_timestamp_ms ? formatTime(quoteInfo.rate_timestamp_ms) : '—';
|
|
229
|
+
const rateStatus = quoteInfo?.degraded ? t('customer.quote.degraded') : null;
|
|
230
|
+
const baseAmountLine = quoteInfo?.base_amount
|
|
231
|
+
? `${quoteInfo.base_amount} ${quoteInfo.base_currency || 'USD'}`
|
|
232
|
+
: null;
|
|
233
|
+
const formattedRate = formatExchangeRate(quoteInfo?.exchange_rate || null);
|
|
234
|
+
const rateLine = formattedRate
|
|
235
|
+
? (() => {
|
|
236
|
+
const currencyMap = {
|
|
237
|
+
USD: '$',
|
|
238
|
+
CNY: '¥',
|
|
239
|
+
};
|
|
240
|
+
const currencySymbol = currencyMap[quoteInfo?.base_currency as keyof typeof currencyMap];
|
|
241
|
+
return `1 ${item.paymentCurrency.symbol} ≈ ${
|
|
242
|
+
currencySymbol
|
|
243
|
+
? `${currencySymbol}${formattedRate}`
|
|
244
|
+
: `${formattedRate} ${quoteInfo?.base_currency || 'USD'}`
|
|
245
|
+
}`;
|
|
246
|
+
})()
|
|
247
|
+
: null;
|
|
248
|
+
|
|
249
|
+
let usdAmount: string | null = null;
|
|
250
|
+
if (quoteInfo?.base_amount) {
|
|
251
|
+
usdAmount = formatUsdAmount(quoteInfo.base_amount, locale);
|
|
252
|
+
} else if (quoteInfo?.exchange_rate && item.total) {
|
|
253
|
+
const calculatedUsd = getUsdAmountFromTokenUnits(
|
|
254
|
+
new BN(item.total),
|
|
255
|
+
item.paymentCurrency.decimal,
|
|
256
|
+
quoteInfo.exchange_rate
|
|
257
|
+
);
|
|
258
|
+
if (calculatedUsd) {
|
|
259
|
+
usdAmount = formatUsdAmount(calculatedUsd, locale);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
const tooltipContent = quoteInfo ? (
|
|
264
|
+
<Stack spacing={0.5}>
|
|
265
|
+
<Typography variant="caption">
|
|
266
|
+
{t('customer.quote.provider')}: {providerDisplay || '—'}
|
|
267
|
+
</Typography>
|
|
268
|
+
{baseAmountLine && (
|
|
269
|
+
<Typography variant="caption">
|
|
270
|
+
{t('customer.quote.baseAmount')}: {baseAmountLine}
|
|
271
|
+
</Typography>
|
|
272
|
+
)}
|
|
273
|
+
{rateLine && (
|
|
274
|
+
<Typography variant="caption">
|
|
275
|
+
{t('customer.quote.exchangeRate')}: {rateLine}
|
|
276
|
+
</Typography>
|
|
277
|
+
)}
|
|
278
|
+
<Typography variant="caption">
|
|
279
|
+
{t('customer.quote.rateTimestamp')}: {rateTimestamp}
|
|
280
|
+
</Typography>
|
|
281
|
+
<Typography variant="caption">
|
|
282
|
+
{t('customer.quote.consensusMethod')}: {consensusMethod}
|
|
283
|
+
</Typography>
|
|
284
|
+
{rateStatus && (
|
|
285
|
+
<Typography variant="caption">
|
|
286
|
+
{t('common.status')}: {rateStatus}
|
|
287
|
+
{quoteInfo?.degraded_reason ? ` (${quoteInfo.degraded_reason})` : ''}
|
|
288
|
+
</Typography>
|
|
289
|
+
)}
|
|
290
|
+
{quoteInfo.slippage_percent !== null && quoteInfo.slippage_percent !== undefined && (
|
|
291
|
+
<Typography variant="caption">
|
|
292
|
+
{t('customer.quote.slippage')}: {quoteInfo.slippage_percent}%
|
|
293
|
+
</Typography>
|
|
294
|
+
)}
|
|
295
|
+
</Stack>
|
|
296
|
+
) : null;
|
|
297
|
+
|
|
198
298
|
return (
|
|
199
299
|
<InvoiceLink invoice={item}>
|
|
200
|
-
<
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
300
|
+
<Stack spacing={0.25} alignItems="flex-end">
|
|
301
|
+
<Typography
|
|
302
|
+
component="strong"
|
|
303
|
+
sx={{
|
|
304
|
+
fontWeight: 600,
|
|
305
|
+
textDecoration: item.status === 'void' ? 'line-through' : 'none',
|
|
306
|
+
}}>
|
|
307
|
+
{formatAmount(item?.total, item?.paymentCurrency.decimal)}
|
|
308
|
+
{item?.paymentCurrency.symbol}
|
|
309
|
+
</Typography>
|
|
310
|
+
{usdAmount && (
|
|
311
|
+
<Tooltip title={tooltipContent} placement="top" arrow>
|
|
312
|
+
<Typography
|
|
313
|
+
variant="caption"
|
|
314
|
+
sx={{
|
|
315
|
+
color: 'text.secondary',
|
|
316
|
+
fontSize: '0.75rem',
|
|
317
|
+
fontWeight: 400,
|
|
318
|
+
lineHeight: 1.2,
|
|
319
|
+
}}>
|
|
320
|
+
≈ ${usdAmount}
|
|
321
|
+
</Typography>
|
|
322
|
+
</Tooltip>
|
|
323
|
+
)}
|
|
324
|
+
</Stack>
|
|
209
325
|
</InvoiceLink>
|
|
210
326
|
);
|
|
211
327
|
},
|
|
@@ -232,13 +348,27 @@ export default function InvoiceList({
|
|
|
232
348
|
{
|
|
233
349
|
label: t('common.status'),
|
|
234
350
|
name: 'status',
|
|
235
|
-
width:
|
|
351
|
+
width: 120,
|
|
236
352
|
options: {
|
|
237
353
|
customBodyRenderLite: (_: string, index: number) => {
|
|
238
354
|
const item = data.list[index] as TInvoiceExpanded;
|
|
355
|
+
const isSlippageExceeded = item?.status === 'uncollectible' && item?.metadata?.slippage?.below_threshold;
|
|
239
356
|
return (
|
|
240
357
|
<InvoiceLink invoice={item}>
|
|
241
|
-
<
|
|
358
|
+
<Stack direction="row" spacing={0.5} alignItems="center">
|
|
359
|
+
<Status label={item?.status} color={getInvoiceStatusColor(item?.status)} />
|
|
360
|
+
{isSlippageExceeded && (
|
|
361
|
+
<Tooltip
|
|
362
|
+
title={t('payment.customer.invoice.slippageExceededDetail', {
|
|
363
|
+
currentRate: item.metadata?.slippage?.rate_at_invoice || '—',
|
|
364
|
+
minRate: item.metadata?.slippage?.min_acceptable_rate || '—',
|
|
365
|
+
})}
|
|
366
|
+
arrow
|
|
367
|
+
placement="top">
|
|
368
|
+
<WarningAmberIcon sx={{ fontSize: 16, color: 'warning.main', cursor: 'help' }} />
|
|
369
|
+
</Tooltip>
|
|
370
|
+
)}
|
|
371
|
+
</Stack>
|
|
242
372
|
</InvoiceLink>
|
|
243
373
|
);
|
|
244
374
|
},
|
|
@@ -322,7 +452,7 @@ export default function InvoiceList({
|
|
|
322
452
|
|
|
323
453
|
if (features?.customer) {
|
|
324
454
|
// @ts-ignore
|
|
325
|
-
columns.splice(
|
|
455
|
+
columns.splice(2, 0, {
|
|
326
456
|
label: t('common.customer'),
|
|
327
457
|
name: 'customer_id',
|
|
328
458
|
options: {
|