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.
Files changed (117) hide show
  1. package/api/src/crons/overdue-detection.ts +10 -1
  2. package/api/src/index.ts +3 -0
  3. package/api/src/libs/credit-utils.ts +21 -0
  4. package/api/src/libs/discount/discount.ts +13 -0
  5. package/api/src/libs/env.ts +5 -0
  6. package/api/src/libs/error.ts +14 -0
  7. package/api/src/libs/exchange-rate/coingecko-provider.ts +193 -0
  8. package/api/src/libs/exchange-rate/coinmarketcap-provider.ts +180 -0
  9. package/api/src/libs/exchange-rate/index.ts +5 -0
  10. package/api/src/libs/exchange-rate/service.ts +583 -0
  11. package/api/src/libs/exchange-rate/token-address-mapping.ts +84 -0
  12. package/api/src/libs/exchange-rate/token-addresses.json +2147 -0
  13. package/api/src/libs/exchange-rate/token-data-provider.ts +142 -0
  14. package/api/src/libs/exchange-rate/types.ts +114 -0
  15. package/api/src/libs/exchange-rate/validator.ts +319 -0
  16. package/api/src/libs/invoice-quote.ts +158 -0
  17. package/api/src/libs/invoice.ts +143 -7
  18. package/api/src/libs/math-utils.ts +46 -0
  19. package/api/src/libs/notification/template/billing-discrepancy.ts +3 -4
  20. package/api/src/libs/notification/template/customer-auto-recharge-failed.ts +174 -79
  21. package/api/src/libs/notification/template/customer-credit-grant-granted.ts +2 -3
  22. package/api/src/libs/notification/template/customer-credit-insufficient.ts +3 -3
  23. package/api/src/libs/notification/template/customer-credit-low-balance.ts +3 -3
  24. package/api/src/libs/notification/template/customer-revenue-succeeded.ts +2 -3
  25. package/api/src/libs/notification/template/customer-reward-succeeded.ts +9 -4
  26. package/api/src/libs/notification/template/exchange-rate-alert.ts +202 -0
  27. package/api/src/libs/notification/template/subscription-slippage-exceeded.ts +203 -0
  28. package/api/src/libs/notification/template/subscription-slippage-warning.ts +212 -0
  29. package/api/src/libs/notification/template/subscription-will-canceled.ts +2 -2
  30. package/api/src/libs/notification/template/subscription-will-renew.ts +22 -8
  31. package/api/src/libs/payment.ts +1 -1
  32. package/api/src/libs/price.ts +4 -1
  33. package/api/src/libs/queue/index.ts +8 -0
  34. package/api/src/libs/quote-service.ts +1132 -0
  35. package/api/src/libs/quote-validation.ts +388 -0
  36. package/api/src/libs/session.ts +686 -39
  37. package/api/src/libs/slippage.ts +135 -0
  38. package/api/src/libs/subscription.ts +185 -15
  39. package/api/src/libs/util.ts +64 -3
  40. package/api/src/locales/en.ts +50 -0
  41. package/api/src/locales/zh.ts +48 -0
  42. package/api/src/queues/auto-recharge.ts +295 -21
  43. package/api/src/queues/exchange-rate-health.ts +242 -0
  44. package/api/src/queues/invoice.ts +48 -1
  45. package/api/src/queues/notification.ts +190 -3
  46. package/api/src/queues/payment.ts +177 -7
  47. package/api/src/queues/subscription.ts +436 -6
  48. package/api/src/routes/auto-recharge-configs.ts +71 -6
  49. package/api/src/routes/checkout-sessions.ts +1730 -81
  50. package/api/src/routes/connect/auto-recharge-auth.ts +2 -0
  51. package/api/src/routes/connect/change-payer.ts +2 -0
  52. package/api/src/routes/connect/change-payment.ts +61 -8
  53. package/api/src/routes/connect/change-plan.ts +161 -17
  54. package/api/src/routes/connect/collect.ts +9 -6
  55. package/api/src/routes/connect/delegation.ts +1 -0
  56. package/api/src/routes/connect/pay.ts +157 -0
  57. package/api/src/routes/connect/setup.ts +32 -10
  58. package/api/src/routes/connect/shared.ts +159 -13
  59. package/api/src/routes/connect/subscribe.ts +32 -9
  60. package/api/src/routes/credit-grants.ts +99 -0
  61. package/api/src/routes/exchange-rate-providers.ts +248 -0
  62. package/api/src/routes/exchange-rates.ts +87 -0
  63. package/api/src/routes/index.ts +4 -0
  64. package/api/src/routes/invoices.ts +280 -2
  65. package/api/src/routes/meter-events.ts +3 -0
  66. package/api/src/routes/payment-links.ts +13 -0
  67. package/api/src/routes/prices.ts +84 -2
  68. package/api/src/routes/subscriptions.ts +526 -15
  69. package/api/src/store/migrations/20251220-dynamic-pricing.ts +245 -0
  70. package/api/src/store/migrations/20251223-exchange-rate-provider-type.ts +28 -0
  71. package/api/src/store/migrations/20260110-add-quote-locked-at.ts +23 -0
  72. package/api/src/store/migrations/20260112-add-checkout-session-slippage-percent.ts +22 -0
  73. package/api/src/store/migrations/20260113-add-price-quote-slippage-fields.ts +45 -0
  74. package/api/src/store/migrations/20260116-subscription-slippage.ts +21 -0
  75. package/api/src/store/migrations/20260120-auto-recharge-slippage.ts +21 -0
  76. package/api/src/store/models/auto-recharge-config.ts +12 -0
  77. package/api/src/store/models/checkout-session.ts +7 -0
  78. package/api/src/store/models/exchange-rate-provider.ts +225 -0
  79. package/api/src/store/models/index.ts +6 -0
  80. package/api/src/store/models/payment-intent.ts +6 -0
  81. package/api/src/store/models/price-quote.ts +284 -0
  82. package/api/src/store/models/price.ts +53 -5
  83. package/api/src/store/models/subscription.ts +11 -0
  84. package/api/src/store/models/types.ts +61 -1
  85. package/api/tests/libs/change-payment-plan.spec.ts +282 -0
  86. package/api/tests/libs/exchange-rate-service.spec.ts +341 -0
  87. package/api/tests/libs/quote-service.spec.ts +199 -0
  88. package/api/tests/libs/session.spec.ts +464 -0
  89. package/api/tests/libs/slippage.spec.ts +109 -0
  90. package/api/tests/libs/token-data-provider.spec.ts +267 -0
  91. package/api/tests/models/exchange-rate-provider.spec.ts +121 -0
  92. package/api/tests/models/price-dynamic.spec.ts +100 -0
  93. package/api/tests/models/price-quote.spec.ts +112 -0
  94. package/api/tests/routes/exchange-rate-providers.spec.ts +215 -0
  95. package/api/tests/routes/subscription-slippage.spec.ts +254 -0
  96. package/blocklet.yml +1 -1
  97. package/package.json +7 -6
  98. package/src/components/customer/credit-overview.tsx +14 -0
  99. package/src/components/discount/discount-info.tsx +8 -2
  100. package/src/components/invoice/list.tsx +146 -16
  101. package/src/components/invoice/table.tsx +276 -71
  102. package/src/components/invoice-pdf/template.tsx +3 -7
  103. package/src/components/metadata/form.tsx +6 -8
  104. package/src/components/price/form.tsx +519 -149
  105. package/src/components/promotion/active-redemptions.tsx +5 -3
  106. package/src/components/quote/info.tsx +234 -0
  107. package/src/hooks/subscription.ts +132 -2
  108. package/src/locales/en.tsx +145 -0
  109. package/src/locales/zh.tsx +143 -1
  110. package/src/pages/admin/billing/invoices/detail.tsx +41 -4
  111. package/src/pages/admin/products/exchange-rate-providers/edit-dialog.tsx +354 -0
  112. package/src/pages/admin/products/exchange-rate-providers/index.tsx +363 -0
  113. package/src/pages/admin/products/index.tsx +12 -1
  114. package/src/pages/customer/invoice/detail.tsx +36 -12
  115. package/src/pages/customer/subscription/change-payment.tsx +65 -3
  116. package/src/pages/customer/subscription/change-plan.tsx +207 -38
  117. package/src/pages/customer/subscription/detail.tsx +599 -419
@@ -1,6 +1,6 @@
1
1
  /* eslint-disable react/display-name, react/no-unstable-nested-components */
2
2
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
- import { api, formatTime, Link, Table } from '@blocklet/payment-react';
3
+ import { api, formatTime, formatAmount, Link, Table } from '@blocklet/payment-react';
4
4
  import { Box, Tab, Tabs, Typography, Chip } from '@mui/material';
5
5
  import { useRequest } from 'ahooks';
6
6
  import { useState } from 'react';
@@ -210,10 +210,11 @@ export default function ActiveRedemptions({ couponId = '', promotionCodeId = ''
210
210
  if (!value) {
211
211
  return null;
212
212
  }
213
+ const v = value as any;
213
214
  return (
214
215
  <Typography key={currencyId} variant="body2" sx={{ display: 'inline-flex' }}>
215
216
  {i > 0 && '、'}
216
- {value.formattedAmount}
217
+ {formatAmount(v.amount, v.currency?.decimal || 18)} {v.currency?.symbol || ''}
217
218
  </Typography>
218
219
  );
219
220
  })}
@@ -371,10 +372,11 @@ export default function ActiveRedemptions({ couponId = '', promotionCodeId = ''
371
372
  if (!value) {
372
373
  return null;
373
374
  }
375
+ const v = value as any;
374
376
  return (
375
377
  <Typography key={currencyId} variant="body2" sx={{ lineHeight: 1.2 }}>
376
378
  {i > 0 && ', '}
377
- {value.formattedAmount}
379
+ {formatAmount(v.amount, v.currency?.decimal || 18)} {v.currency?.symbol || ''}
378
380
  </Typography>
379
381
  );
380
382
  })}
@@ -0,0 +1,234 @@
1
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
+ import { formatTime } from '@blocklet/payment-react';
3
+ import { LockClockOutlined, TrendingUpOutlined } from '@mui/icons-material';
4
+ import { Box, Chip, Paper, Stack, Tooltip, Typography } from '@mui/material';
5
+
6
+ import Copyable from '../copyable';
7
+ import InfoRow from '../info-row';
8
+
9
+ interface QuoteMetadata {
10
+ calculation?: {
11
+ token_amount_raw?: string;
12
+ unit_amount_raw?: string;
13
+ };
14
+ rounding?: {
15
+ mode?: string;
16
+ token_decimals?: number;
17
+ };
18
+ risk?: {
19
+ anomaly_detected?: boolean;
20
+ deviation_percent?: number;
21
+ degraded?: boolean;
22
+ degraded_reason?: string | null;
23
+ };
24
+ context?: Record<string, any>;
25
+ }
26
+
27
+ interface PriceQuote {
28
+ id: string;
29
+ price_id: string;
30
+ base_currency: string;
31
+ base_amount: string;
32
+ target_currency_id: string;
33
+ rate_currency_symbol: string;
34
+ exchange_rate: string;
35
+ quoted_amount: string;
36
+ rate_provider_id: string;
37
+ rate_provider_name: string;
38
+ rate_timestamp_ms: number;
39
+ expires_at: number;
40
+ status: 'active' | 'used' | 'paid' | 'expired' | 'cancelled' | 'failed';
41
+ metadata: QuoteMetadata | null;
42
+ created_at: string;
43
+ }
44
+
45
+ interface QuoteInfoProps {
46
+ quotes: PriceQuote[];
47
+ }
48
+
49
+ const getStatusColor = (status: string) => {
50
+ switch (status) {
51
+ case 'paid':
52
+ return 'success';
53
+ case 'used':
54
+ return 'primary';
55
+ case 'active':
56
+ return 'info';
57
+ case 'expired':
58
+ case 'cancelled':
59
+ case 'failed':
60
+ return 'error';
61
+ default:
62
+ return 'default';
63
+ }
64
+ };
65
+
66
+ export default function QuoteInfo({ quotes }: QuoteInfoProps) {
67
+ const { t } = useLocaleContext();
68
+
69
+ if (!quotes || quotes.length === 0) {
70
+ return (
71
+ <Typography variant="body2" color="text.secondary">
72
+ {t('admin.quote.noQuotes')}
73
+ </Typography>
74
+ );
75
+ }
76
+
77
+ return (
78
+ <Stack spacing={2}>
79
+ {quotes.map((quote) => (
80
+ <Paper key={quote.id} variant="outlined" sx={{ p: 2 }}>
81
+ <Stack spacing={2}>
82
+ {/* Header */}
83
+ <Stack direction="row" justifyContent="space-between" alignItems="center">
84
+ <Stack direction="row" spacing={1} alignItems="center">
85
+ <LockClockOutlined fontSize="small" color="primary" />
86
+ <Typography variant="subtitle2" fontWeight={600}>
87
+ {t('admin.quote.title')}
88
+ </Typography>
89
+ </Stack>
90
+ <Chip label={t(`admin.quote.status.${quote.status}`)} color={getStatusColor(quote.status)} size="small" />
91
+ </Stack>
92
+
93
+ {/* Quote ID */}
94
+ <InfoRow
95
+ label={t('admin.quote.id')}
96
+ value={<Copyable text={quote.id} />}
97
+ direction="row"
98
+ alignItems="center"
99
+ />
100
+
101
+ {/* Pricing Details */}
102
+ <Box>
103
+ <Typography variant="caption" color="text.secondary" gutterBottom>
104
+ {t('admin.quote.pricingDetails')}
105
+ </Typography>
106
+ <Stack spacing={1} sx={{ mt: 1 }}>
107
+ <InfoRow
108
+ label={t('admin.quote.baseAmount')}
109
+ value={
110
+ <Typography variant="body2">
111
+ {quote.base_amount} {quote.base_currency}
112
+ </Typography>
113
+ }
114
+ direction="row"
115
+ alignItems="center"
116
+ />
117
+ <InfoRow
118
+ label={t('admin.quote.exchangeRate')}
119
+ value={
120
+ <Stack direction="row" spacing={0.5} alignItems="center">
121
+ <Typography variant="body2">
122
+ 1 {quote.rate_currency_symbol} = {quote.exchange_rate} {quote.base_currency}
123
+ </Typography>
124
+ {quote.metadata?.risk?.degraded && (
125
+ <Tooltip title={quote.metadata.risk.degraded_reason || ''}>
126
+ <Chip label={t('admin.quote.degraded')} color="warning" size="small" variant="outlined" />
127
+ </Tooltip>
128
+ )}
129
+ </Stack>
130
+ }
131
+ direction="row"
132
+ alignItems="center"
133
+ />
134
+ <InfoRow
135
+ label={t('admin.quote.quotedAmount')}
136
+ value={
137
+ <Typography variant="body2" fontWeight={500}>
138
+ {quote.metadata?.calculation?.token_amount_raw || '-'} {quote.rate_currency_symbol}
139
+ </Typography>
140
+ }
141
+ direction="row"
142
+ alignItems="center"
143
+ />
144
+ </Stack>
145
+ </Box>
146
+
147
+ {/* Rate Provider Info */}
148
+ <Box>
149
+ <Typography variant="caption" color="text.secondary" gutterBottom>
150
+ {t('admin.quote.providerInfo')}
151
+ </Typography>
152
+ <Stack spacing={1} sx={{ mt: 1 }}>
153
+ <InfoRow
154
+ label={t('admin.quote.provider')}
155
+ value={<Typography variant="body2">{quote.rate_provider_name}</Typography>}
156
+ direction="row"
157
+ alignItems="center"
158
+ />
159
+ <InfoRow
160
+ label={t('admin.quote.rateTimestamp')}
161
+ value={
162
+ <Tooltip title={formatTime(quote.rate_timestamp_ms)}>
163
+ <Typography variant="body2" color="text.secondary">
164
+ {formatTime(quote.rate_timestamp_ms, 'relative')}
165
+ </Typography>
166
+ </Tooltip>
167
+ }
168
+ direction="row"
169
+ alignItems="center"
170
+ />
171
+ </Stack>
172
+ </Box>
173
+
174
+ {/* Lifecycle */}
175
+ <Box>
176
+ <Typography variant="caption" color="text.secondary" gutterBottom>
177
+ {t('admin.quote.lifecycle')}
178
+ </Typography>
179
+ <Stack spacing={1} sx={{ mt: 1 }}>
180
+ <InfoRow
181
+ label={t('common.createdAt')}
182
+ value={
183
+ <Typography variant="body2" color="text.secondary">
184
+ {formatTime(quote.created_at)}
185
+ </Typography>
186
+ }
187
+ direction="row"
188
+ alignItems="center"
189
+ />
190
+ <InfoRow
191
+ label={t('admin.quote.expiresAt')}
192
+ value={
193
+ <Typography variant="body2" color="text.secondary">
194
+ {formatTime(quote.expires_at * 1000)}
195
+ </Typography>
196
+ }
197
+ direction="row"
198
+ alignItems="center"
199
+ />
200
+ </Stack>
201
+ </Box>
202
+
203
+ {/* Risk Information */}
204
+ {quote.metadata?.risk && (
205
+ <Box>
206
+ <Typography variant="caption" color="text.secondary" gutterBottom>
207
+ {t('admin.quote.riskInfo')}
208
+ </Typography>
209
+ <Stack spacing={1} sx={{ mt: 1 }}>
210
+ {quote.metadata.risk.deviation_percent !== undefined && (
211
+ <InfoRow
212
+ label={t('admin.quote.deviation')}
213
+ value={
214
+ <Stack direction="row" spacing={0.5} alignItems="center">
215
+ <TrendingUpOutlined fontSize="small" />
216
+ <Typography variant="body2">{quote.metadata.risk.deviation_percent.toFixed(2)}%</Typography>
217
+ </Stack>
218
+ }
219
+ direction="row"
220
+ alignItems="center"
221
+ />
222
+ )}
223
+ {quote.metadata.risk.anomaly_detected && (
224
+ <Chip label={t('admin.quote.anomalyDetected')} color="warning" size="small" variant="outlined" />
225
+ )}
226
+ </Stack>
227
+ </Box>
228
+ )}
229
+ </Stack>
230
+ </Paper>
231
+ ))}
232
+ </Stack>
233
+ );
234
+ }
@@ -1,7 +1,7 @@
1
- import { useCallback } from 'react';
1
+ import { useCallback, useEffect, useRef } from 'react';
2
2
  import Toast from '@arcblock/ux/lib/Toast';
3
3
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
4
- import { useRequest } from 'ahooks';
4
+ import { useRequest, useSetState } from 'ahooks';
5
5
  import { api } from '@blocklet/payment-react';
6
6
  import type { TPaymentCurrency } from '@blocklet/payment-types';
7
7
 
@@ -62,3 +62,133 @@ export function usePendingAmountForSubscription(subscriptionId: string, paymentC
62
62
  checkPendingAmount,
63
63
  };
64
64
  }
65
+
66
+ type SubscriptionExchangeRateInfo = {
67
+ rate?: string;
68
+ provider_id?: string;
69
+ provider_name?: string;
70
+ base_currency?: string;
71
+ timestamp_ms?: number;
72
+ fetched_at?: number;
73
+ };
74
+
75
+ type UseSubscriptionExchangeRateOptions = {
76
+ subscriptionId?: string;
77
+ currencyId?: string;
78
+ enabled?: boolean;
79
+ pollingInterval?: number;
80
+ };
81
+
82
+ export function useSubscriptionExchangeRate({
83
+ subscriptionId,
84
+ currencyId,
85
+ enabled = false,
86
+ pollingInterval = 30000,
87
+ }: UseSubscriptionExchangeRateOptions) {
88
+ const [state, setState] = useSetState<{
89
+ liveRateInfo?: SubscriptionExchangeRateInfo;
90
+ liveRateUnavailable: boolean;
91
+ liveRateError?: string;
92
+ }>({
93
+ liveRateInfo: undefined,
94
+ liveRateUnavailable: false,
95
+ liveRateError: undefined,
96
+ });
97
+ const liveRateRefreshRef = useRef(false);
98
+
99
+ useEffect(() => {
100
+ if (!enabled || !subscriptionId || !currencyId) {
101
+ setState({
102
+ liveRateInfo: undefined,
103
+ liveRateUnavailable: false,
104
+ liveRateError: undefined,
105
+ });
106
+ return undefined;
107
+ }
108
+
109
+ let cancelled = false;
110
+ let timer: ReturnType<typeof setInterval> | null = null;
111
+
112
+ const fetchRate = async (isManualRetry = false) => {
113
+ if (typeof document !== 'undefined' && document.hidden && !isManualRetry) {
114
+ return;
115
+ }
116
+ if (typeof navigator !== 'undefined' && !navigator.onLine) {
117
+ return;
118
+ }
119
+ if (liveRateRefreshRef.current) {
120
+ return;
121
+ }
122
+ liveRateRefreshRef.current = true;
123
+ try {
124
+ const { data } = await api.get(`/api/subscriptions/${subscriptionId}/exchange-rate`, {
125
+ params: { currency_id: currencyId },
126
+ });
127
+ if (cancelled) {
128
+ return;
129
+ }
130
+ setState({
131
+ liveRateInfo: data,
132
+ liveRateUnavailable: false,
133
+ liveRateError: undefined,
134
+ });
135
+ scheduleNext();
136
+ } catch (err: any) {
137
+ if (cancelled) {
138
+ return;
139
+ }
140
+ console.error('[Subscription Exchange Rate Fetch Error]', {
141
+ subscriptionId,
142
+ currencyId,
143
+ message: err?.message,
144
+ });
145
+ setState({
146
+ liveRateUnavailable: true,
147
+ liveRateError: err?.response?.data?.error || err?.message,
148
+ });
149
+ } finally {
150
+ liveRateRefreshRef.current = false;
151
+ }
152
+ };
153
+
154
+ const scheduleNext = () => {
155
+ if (!timer) {
156
+ // no existing timer
157
+ } else {
158
+ clearInterval(timer);
159
+ }
160
+ if (typeof window === 'undefined') {
161
+ return;
162
+ }
163
+ timer = setInterval(() => {
164
+ fetchRate(false);
165
+ }, pollingInterval);
166
+ };
167
+
168
+ fetchRate(false);
169
+ const handleVisibilityChange = () => {
170
+ if (typeof document !== 'undefined' && !document.hidden) {
171
+ fetchRate(false);
172
+ }
173
+ };
174
+ if (typeof document !== 'undefined') {
175
+ document.addEventListener('visibilitychange', handleVisibilityChange);
176
+ }
177
+ return () => {
178
+ cancelled = true;
179
+ if (timer) {
180
+ clearInterval(timer);
181
+ }
182
+ if (typeof document !== 'undefined') {
183
+ document.removeEventListener('visibilitychange', handleVisibilityChange);
184
+ }
185
+ };
186
+ // eslint-disable-next-line react-hooks/exhaustive-deps
187
+ }, [subscriptionId, currencyId, enabled, pollingInterval]);
188
+
189
+ return {
190
+ liveRateInfo: state.liveRateInfo,
191
+ liveRateUnavailable: state.liveRateUnavailable,
192
+ liveRateError: state.liveRateError,
193
+ };
194
+ }
@@ -9,6 +9,8 @@ export default flat({
9
9
  active: 'Active',
10
10
  every: 'Every',
11
11
  inactive: 'Inactive',
12
+ enabled: 'Enabled',
13
+ disabled: 'Disabled',
12
14
  metadata: {
13
15
  label: 'Metadata',
14
16
  description: 'Add custom key-value pairs to store additional information about this meter.',
@@ -40,6 +42,10 @@ export default flat({
40
42
  estimatedDuration: '{duration} est.',
41
43
  detail: 'Detail',
42
44
  setting: 'Setting',
45
+ slippage: 'Slippage Limit',
46
+ slippageMinRate: 'Min acceptable rate {rate} {currency}',
47
+ slippageTooltip:
48
+ 'The minimum acceptable exchange rate for automatic payments. If the rate drops below this, payment will be paused.',
43
49
  welcome: 'Welcome to Payment Kit',
44
50
  welcomeDesc: 'Start accepting payments in minutes with Payment Kit. Choose a feature to get started.',
45
51
  quickStart: 'Quick Start Guides',
@@ -55,10 +61,14 @@ export default flat({
55
61
  copyTip: 'Please copy manually',
56
62
  save: 'Save',
57
63
  saving: 'Saving...',
64
+ saved: 'Saved successfully',
65
+ refresh: 'Refresh',
58
66
  cancel: 'Cancel',
59
67
  back: 'Back',
60
68
  know: 'I Know',
61
69
  confirm: 'Confirm',
70
+ increased: 'increased',
71
+ decreased: 'decreased',
62
72
  edit: 'Edit',
63
73
  view: 'View',
64
74
  select: 'Select',
@@ -804,6 +814,33 @@ export default flat({
804
814
  description: 'Enter the number of units that can be purchased in a single checkout, 0 means unlimited',
805
815
  },
806
816
  inventory: 'Inventory Settings',
817
+ dynamicPricing: {
818
+ label: 'Enable Dynamic Pricing',
819
+ description: 'Price fluctuates based on real-time exchange rates',
820
+ config: {
821
+ title: 'Dynamic Pricing Configuration',
822
+ baseAmount: {
823
+ label: 'Base Price',
824
+ description: 'The base price in fiat currency',
825
+ required: 'Base price is required',
826
+ },
827
+ },
828
+ validation: {
829
+ checking: 'Checking exchange rate availability...',
830
+ // eslint-disable-next-line no-template-curly-in-string
831
+ rateLine: 'Current rate: 1 {currency} ≈ ${rate}',
832
+ // eslint-disable-next-line no-template-curly-in-string
833
+ usdLine: 'USD estimate: ≈ ${amount}',
834
+ error: 'Failed to fetch exchange rate, currency not supported or data source exception',
835
+ // eslint-disable-next-line no-template-curly-in-string
836
+ estimatedLine: 'Estimated amount: ≈ {amount} {currency}',
837
+ useAmount: 'Fill amount',
838
+ },
839
+ },
840
+ referencePricing: {
841
+ label: 'Reference Exchange Rate Pricing',
842
+ description: 'Use current exchange rates to estimate token amounts without enabling dynamic pricing',
843
+ },
807
844
  },
808
845
  coupon: {
809
846
  create: 'Create Coupon',
@@ -1324,6 +1361,8 @@ export default flat({
1324
1361
  finalizedAt: 'Finalized At',
1325
1362
  paidAt: 'Payment Date',
1326
1363
  summary: 'Summary',
1364
+ billingContextNote: 'Invoice amounts are calculated based on real-time prices at the time of billing.',
1365
+ dynamicPricingNote: 'This invoice uses dynamic pricing with locked exchange rates.',
1327
1366
  billTo: 'Billed to',
1328
1367
  billing: 'Billing Method',
1329
1368
  download: 'Download PDF',
@@ -1816,6 +1855,71 @@ export default flat({
1816
1855
  totalCreditUsed: 'Total Credit Used',
1817
1856
  transactionDate: 'Transaction Date',
1818
1857
  },
1858
+ exchangeRateProvider: {
1859
+ title: 'Exchange Rate Providers',
1860
+ subtitle: 'Manage exchange rate data sources for dynamic pricing',
1861
+ medianStrategyNote:
1862
+ 'Exchange rates are calculated automatically using multiple data sources. You only need to manage this list when a provider is consistently failing or needs to be temporarily disabled.',
1863
+ disableConfirmTitle: 'Disable Provider?',
1864
+ disableConfirmMessage: 'Disabling this provider may affect dynamic pricing payments. Are you sure?',
1865
+ table: {
1866
+ name: 'Name',
1867
+ participation: 'Participation',
1868
+ health: 'Health',
1869
+ recentActivity: 'Recent Activity',
1870
+ trustLevel: 'Trust Level',
1871
+ trustLevelTip:
1872
+ 'Indicates confidence and eligibility for rate calculation. Higher values do not mean preferred or primary source.',
1873
+ enabled: 'Enabled',
1874
+ lastUpdate: 'Last update: {time}',
1875
+ failures24h: 'Failures (24h): {count}',
1876
+ },
1877
+ participation: {
1878
+ included: 'Included',
1879
+ excluded: 'Excluded',
1880
+ },
1881
+ health: {
1882
+ active: 'Healthy',
1883
+ degraded: 'Unstable',
1884
+ paused: 'Outlier',
1885
+ inactive: 'Outlier',
1886
+ },
1887
+ status: {
1888
+ active: 'Active',
1889
+ degraded: 'Degraded',
1890
+ paused: 'Paused',
1891
+ inactive: 'Inactive',
1892
+ },
1893
+ create: {
1894
+ title: 'Add Provider',
1895
+ primaryAction: 'Add Exchange Rate Provider',
1896
+ },
1897
+ edit: {
1898
+ title: 'Edit {name}',
1899
+ name: 'Name',
1900
+ nameHelp: 'Unique identifier for this data source',
1901
+ nameRequired: 'Name is required',
1902
+ type: 'Provider Type',
1903
+ typeHelp: 'Data source type. Cannot be changed after creation.',
1904
+ baseUrl: 'Base URL (Optional)',
1905
+ baseUrlHelp: 'Custom base URL for proxy or self-hosted instance. Default: {defaultUrl}',
1906
+ apiKey: 'API Key',
1907
+ apiKeyHelp: 'API key for accessing the data source.',
1908
+ apiKeyLinkText: 'Get your API key at',
1909
+ apiKeyLinkAction: 'CoinMarketCap account',
1910
+ testConnection: 'Test Connection',
1911
+ testing: 'Testing...',
1912
+ testSuccess: 'Connection successful ({symbol}/USD: {rate}, {time}ms)',
1913
+ testFailed: 'Connection failed: {error}',
1914
+ enabled: 'Enabled',
1915
+ priority: 'Priority',
1916
+ priorityHelp: 'Lower number = higher priority. Providers are used in priority order.',
1917
+ status: 'Status',
1918
+ statusHelp: 'Set to "paused" to temporarily disable this provider.',
1919
+ pausedReason: 'Paused Reason',
1920
+ pausedReasonHelp: 'Explain why this provider is paused (optional).',
1921
+ },
1922
+ },
1819
1923
  },
1820
1924
  empty: {
1821
1925
  image: 'No Image',
@@ -1924,6 +2028,17 @@ export default flat({
1924
2028
  donation: 'Donation',
1925
2029
  creditsInfo: 'Total {amount} included',
1926
2030
  appliedDiscounts: 'Applied Discounts',
2031
+ priceChanged:
2032
+ 'The exchange rate has changed by {percent}%. The payment amount will be updated. Do you want to continue?',
2033
+ paymentCancelled: 'Payment cancelled',
2034
+ paymentMethodChanged: 'Payment method changed',
2035
+ priceChangeTitle: 'Price Changed',
2036
+ priceChangeDescription: 'The exchange rate has {direction} by {percent}%. Your payment amount will be updated.',
2037
+ currentPaymentMethod: 'Current Payment Method',
2038
+ otherPaymentMethods: 'Or pay with another method',
2039
+ confirmAndPay: 'Confirm & Pay',
2040
+ switchAndPay: 'Switch & Pay',
2041
+ current: 'current',
1927
2042
  },
1928
2043
  payout: {
1929
2044
  empty: 'No Revenues',
@@ -1940,6 +2055,36 @@ export default flat({
1940
2055
  alert: 'You have due invoices. Please pay them promptly to avoid service interruption.',
1941
2056
  title: 'Settle Due Invoices',
1942
2057
  },
2058
+ quote: {
2059
+ title: 'Price Quotes',
2060
+ noQuotes: 'No price quotes for this invoice',
2061
+ id: 'Quote ID',
2062
+ pricingDetails: 'Pricing Details',
2063
+ baseAmount: 'Base Amount',
2064
+ exchangeRate: 'Exchange Rate',
2065
+ consensusMethod: 'Consensus',
2066
+ // eslint-disable-next-line no-template-curly-in-string
2067
+ referenceRate: 'Reference Rate: 1 {symbol} ≈ ${rate}',
2068
+ quotedAmount: 'Quoted Amount',
2069
+ providerInfo: 'Rate Provider',
2070
+ provider: 'Provider',
2071
+ rateTimestamp: 'Rate Timestamp',
2072
+ slippage: 'Slippage',
2073
+ lifecycle: 'Lifecycle',
2074
+ expiresAt: 'Expires At',
2075
+ riskInfo: 'Risk Information',
2076
+ deviation: 'Price Deviation',
2077
+ anomalyDetected: 'Anomaly Detected',
2078
+ degraded: 'Degraded',
2079
+ status: {
2080
+ active: 'Active',
2081
+ used: 'Used',
2082
+ paid: 'Paid',
2083
+ expired: 'Expired',
2084
+ cancelled: 'Cancelled',
2085
+ failed: 'Failed',
2086
+ },
2087
+ },
1943
2088
  },
1944
2089
  integrations: {
1945
2090
  description: 'Configure and manage how Payment Kit integrates with your application.',