payment-kit 1.24.4 → 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.
Files changed (115) hide show
  1. package/api/src/index.ts +3 -0
  2. package/api/src/libs/credit-utils.ts +21 -0
  3. package/api/src/libs/discount/discount.ts +13 -0
  4. package/api/src/libs/env.ts +5 -0
  5. package/api/src/libs/error.ts +14 -0
  6. package/api/src/libs/exchange-rate/coingecko-provider.ts +193 -0
  7. package/api/src/libs/exchange-rate/coinmarketcap-provider.ts +180 -0
  8. package/api/src/libs/exchange-rate/index.ts +5 -0
  9. package/api/src/libs/exchange-rate/service.ts +583 -0
  10. package/api/src/libs/exchange-rate/token-address-mapping.ts +84 -0
  11. package/api/src/libs/exchange-rate/token-addresses.json +2147 -0
  12. package/api/src/libs/exchange-rate/token-data-provider.ts +142 -0
  13. package/api/src/libs/exchange-rate/types.ts +114 -0
  14. package/api/src/libs/exchange-rate/validator.ts +319 -0
  15. package/api/src/libs/invoice-quote.ts +158 -0
  16. package/api/src/libs/invoice.ts +143 -7
  17. package/api/src/libs/math-utils.ts +46 -0
  18. package/api/src/libs/notification/template/billing-discrepancy.ts +3 -4
  19. package/api/src/libs/notification/template/customer-auto-recharge-failed.ts +174 -79
  20. package/api/src/libs/notification/template/customer-credit-grant-granted.ts +2 -3
  21. package/api/src/libs/notification/template/customer-credit-insufficient.ts +3 -3
  22. package/api/src/libs/notification/template/customer-credit-low-balance.ts +3 -3
  23. package/api/src/libs/notification/template/customer-revenue-succeeded.ts +2 -3
  24. package/api/src/libs/notification/template/customer-reward-succeeded.ts +9 -4
  25. package/api/src/libs/notification/template/exchange-rate-alert.ts +202 -0
  26. package/api/src/libs/notification/template/subscription-slippage-exceeded.ts +203 -0
  27. package/api/src/libs/notification/template/subscription-slippage-warning.ts +212 -0
  28. package/api/src/libs/notification/template/subscription-will-canceled.ts +2 -2
  29. package/api/src/libs/notification/template/subscription-will-renew.ts +22 -8
  30. package/api/src/libs/payment.ts +1 -1
  31. package/api/src/libs/price.ts +4 -1
  32. package/api/src/libs/queue/index.ts +8 -0
  33. package/api/src/libs/quote-service.ts +1132 -0
  34. package/api/src/libs/quote-validation.ts +388 -0
  35. package/api/src/libs/session.ts +686 -39
  36. package/api/src/libs/slippage.ts +135 -0
  37. package/api/src/libs/subscription.ts +185 -15
  38. package/api/src/libs/util.ts +64 -3
  39. package/api/src/locales/en.ts +50 -0
  40. package/api/src/locales/zh.ts +48 -0
  41. package/api/src/queues/auto-recharge.ts +295 -21
  42. package/api/src/queues/exchange-rate-health.ts +242 -0
  43. package/api/src/queues/invoice.ts +48 -1
  44. package/api/src/queues/notification.ts +167 -1
  45. package/api/src/queues/payment.ts +177 -7
  46. package/api/src/queues/subscription.ts +436 -6
  47. package/api/src/routes/auto-recharge-configs.ts +71 -6
  48. package/api/src/routes/checkout-sessions.ts +1730 -81
  49. package/api/src/routes/connect/auto-recharge-auth.ts +2 -0
  50. package/api/src/routes/connect/change-payer.ts +2 -0
  51. package/api/src/routes/connect/change-payment.ts +61 -8
  52. package/api/src/routes/connect/change-plan.ts +161 -17
  53. package/api/src/routes/connect/collect.ts +9 -6
  54. package/api/src/routes/connect/delegation.ts +1 -0
  55. package/api/src/routes/connect/pay.ts +157 -0
  56. package/api/src/routes/connect/setup.ts +32 -10
  57. package/api/src/routes/connect/shared.ts +159 -13
  58. package/api/src/routes/connect/subscribe.ts +32 -9
  59. package/api/src/routes/credit-grants.ts +99 -0
  60. package/api/src/routes/exchange-rate-providers.ts +248 -0
  61. package/api/src/routes/exchange-rates.ts +87 -0
  62. package/api/src/routes/index.ts +4 -0
  63. package/api/src/routes/invoices.ts +280 -2
  64. package/api/src/routes/payment-links.ts +13 -0
  65. package/api/src/routes/prices.ts +84 -2
  66. package/api/src/routes/subscriptions.ts +526 -15
  67. package/api/src/store/migrations/20251220-dynamic-pricing.ts +245 -0
  68. package/api/src/store/migrations/20251223-exchange-rate-provider-type.ts +28 -0
  69. package/api/src/store/migrations/20260110-add-quote-locked-at.ts +23 -0
  70. package/api/src/store/migrations/20260112-add-checkout-session-slippage-percent.ts +22 -0
  71. package/api/src/store/migrations/20260113-add-price-quote-slippage-fields.ts +45 -0
  72. package/api/src/store/migrations/20260116-subscription-slippage.ts +21 -0
  73. package/api/src/store/migrations/20260120-auto-recharge-slippage.ts +21 -0
  74. package/api/src/store/models/auto-recharge-config.ts +12 -0
  75. package/api/src/store/models/checkout-session.ts +7 -0
  76. package/api/src/store/models/exchange-rate-provider.ts +225 -0
  77. package/api/src/store/models/index.ts +6 -0
  78. package/api/src/store/models/payment-intent.ts +6 -0
  79. package/api/src/store/models/price-quote.ts +284 -0
  80. package/api/src/store/models/price.ts +53 -5
  81. package/api/src/store/models/subscription.ts +11 -0
  82. package/api/src/store/models/types.ts +61 -1
  83. package/api/tests/libs/change-payment-plan.spec.ts +282 -0
  84. package/api/tests/libs/exchange-rate-service.spec.ts +341 -0
  85. package/api/tests/libs/quote-service.spec.ts +199 -0
  86. package/api/tests/libs/session.spec.ts +464 -0
  87. package/api/tests/libs/slippage.spec.ts +109 -0
  88. package/api/tests/libs/token-data-provider.spec.ts +267 -0
  89. package/api/tests/models/exchange-rate-provider.spec.ts +121 -0
  90. package/api/tests/models/price-dynamic.spec.ts +100 -0
  91. package/api/tests/models/price-quote.spec.ts +112 -0
  92. package/api/tests/routes/exchange-rate-providers.spec.ts +215 -0
  93. package/api/tests/routes/subscription-slippage.spec.ts +254 -0
  94. package/blocklet.yml +1 -1
  95. package/package.json +7 -6
  96. package/src/components/customer/credit-overview.tsx +14 -0
  97. package/src/components/discount/discount-info.tsx +8 -2
  98. package/src/components/invoice/list.tsx +146 -16
  99. package/src/components/invoice/table.tsx +276 -71
  100. package/src/components/invoice-pdf/template.tsx +3 -7
  101. package/src/components/metadata/form.tsx +6 -8
  102. package/src/components/price/form.tsx +519 -149
  103. package/src/components/promotion/active-redemptions.tsx +5 -3
  104. package/src/components/quote/info.tsx +234 -0
  105. package/src/hooks/subscription.ts +132 -2
  106. package/src/locales/en.tsx +145 -0
  107. package/src/locales/zh.tsx +143 -1
  108. package/src/pages/admin/billing/invoices/detail.tsx +41 -4
  109. package/src/pages/admin/products/exchange-rate-providers/edit-dialog.tsx +354 -0
  110. package/src/pages/admin/products/exchange-rate-providers/index.tsx +363 -0
  111. package/src/pages/admin/products/index.tsx +12 -1
  112. package/src/pages/customer/invoice/detail.tsx +36 -12
  113. package/src/pages/customer/subscription/change-payment.tsx +65 -3
  114. package/src/pages/customer/subscription/change-plan.tsx +207 -38
  115. 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
- formatBNStr,
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
- Object.keys(params).forEach((key) => {
26
- let v = params[key];
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
- <Typography
201
- component="strong"
202
- sx={{
203
- fontWeight: 600,
204
- textDecoration: item.status === 'void' ? 'line-through' : 'none',
205
- }}>
206
- {formatBNStr(item?.total, item?.paymentCurrency.decimal)}&nbsp;
207
- {item?.paymentCurrency.symbol}
208
- </Typography>
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)}&nbsp;
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: 80,
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
- <Status label={item?.status} color={getInvoiceStatusColor(item?.status)} />
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(3, 0, {
455
+ columns.splice(2, 0, {
326
456
  label: t('common.customer'),
327
457
  name: 'customer_id',
328
458
  options: {