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
|
@@ -5,12 +5,16 @@ import {
|
|
|
5
5
|
Table,
|
|
6
6
|
getPriceUintAmountByCurrency,
|
|
7
7
|
formatNumber,
|
|
8
|
+
formatTime,
|
|
9
|
+
formatExchangeRate,
|
|
10
|
+
getUsdAmountFromTokenUnits,
|
|
11
|
+
formatUsdAmount,
|
|
8
12
|
formatCreditForCheckout,
|
|
9
13
|
} from '@blocklet/payment-react';
|
|
10
14
|
import type { TInvoiceExpanded, TInvoiceItem } from '@blocklet/payment-types';
|
|
11
15
|
import { InfoOutlined } from '@mui/icons-material';
|
|
12
16
|
import { Box, Stack, Tooltip, Typography } from '@mui/material';
|
|
13
|
-
import { toBN } from '@ocap/util';
|
|
17
|
+
import { toBN, fromUnitToToken, fromTokenToUnit, BN } from '@ocap/util';
|
|
14
18
|
import { useSetState } from 'ahooks';
|
|
15
19
|
|
|
16
20
|
import { styled } from '@mui/system';
|
|
@@ -37,6 +41,11 @@ type InvoiceDetailItem = {
|
|
|
37
41
|
quantity: number;
|
|
38
42
|
rawQuantity: number;
|
|
39
43
|
price: string;
|
|
44
|
+
referencePrice?: {
|
|
45
|
+
amount: string;
|
|
46
|
+
currency: string;
|
|
47
|
+
decimals: number;
|
|
48
|
+
} | null;
|
|
40
49
|
amount: string;
|
|
41
50
|
raw: TInvoiceItem;
|
|
42
51
|
price_id: string;
|
|
@@ -45,8 +54,11 @@ type InvoiceDetailItem = {
|
|
|
45
54
|
|
|
46
55
|
type InvoiceSummaryItem = {
|
|
47
56
|
key: string;
|
|
48
|
-
value: string;
|
|
57
|
+
value: string | React.ReactNode;
|
|
49
58
|
color: string;
|
|
59
|
+
usdReference?: string; // ≈ $X.XX format
|
|
60
|
+
tooltip?: React.ReactNode;
|
|
61
|
+
desc?: React.ReactNode;
|
|
50
62
|
};
|
|
51
63
|
|
|
52
64
|
export function getAppliedBalance(invoice: TInvoiceExpanded) {
|
|
@@ -71,11 +83,54 @@ export function getAppliedBalance(invoice: TInvoiceExpanded) {
|
|
|
71
83
|
return '0';
|
|
72
84
|
}
|
|
73
85
|
|
|
74
|
-
export function getInvoiceRows(invoice: TInvoiceExpanded, t: (key: string) => string) {
|
|
86
|
+
export function getInvoiceRows(invoice: TInvoiceExpanded, t: (key: string) => string, locale?: string) {
|
|
75
87
|
const detail: InvoiceDetailItem[] = invoice.lines.map((line) => {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
88
|
+
// Calculate actual payment unit price (in payment currency)
|
|
89
|
+
// line.amount is in unit format (smallest unit)
|
|
90
|
+
// line.quantity can be fractional (e.g., 0.5)
|
|
91
|
+
let actualUnitPrice: string;
|
|
92
|
+
if (line.quantity) {
|
|
93
|
+
if (Number.isInteger(line.quantity)) {
|
|
94
|
+
// If quantity is integer, use BN for precision
|
|
95
|
+
actualUnitPrice = toBN(line.amount).div(toBN(line.quantity)).toString();
|
|
96
|
+
} else {
|
|
97
|
+
// If quantity is fractional, use floating point division
|
|
98
|
+
const amountToken = fromUnitToToken(line.amount, invoice.paymentCurrency.decimal);
|
|
99
|
+
const unitPriceToken = (Number(amountToken) / line.quantity).toFixed(invoice.paymentCurrency.decimal);
|
|
100
|
+
const unitPriceResult = fromTokenToUnit(unitPriceToken, invoice.paymentCurrency.decimal);
|
|
101
|
+
actualUnitPrice = typeof unitPriceResult === 'string' ? unitPriceResult : unitPriceResult.toString();
|
|
102
|
+
}
|
|
103
|
+
} else {
|
|
104
|
+
actualUnitPrice = getPriceUintAmountByCurrency(line.price, invoice.paymentCurrency) || line.amount;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Get quote info from metadata if available (for dynamic pricing)
|
|
108
|
+
const quoteInfo = (line.metadata as any)?.quote;
|
|
109
|
+
let referencePrice: { amount: string; currency: string; decimals: number } | null = null;
|
|
110
|
+
|
|
111
|
+
if (quoteInfo?.base_amount && quoteInfo?.quantity) {
|
|
112
|
+
// Calculate USD unit price from quote
|
|
113
|
+
// base_amount is in USD with 8 decimals (token format, e.g., "10.50")
|
|
114
|
+
// quantity can be fractional (e.g., 0.5)
|
|
115
|
+
const USD_DECIMALS = 8;
|
|
116
|
+
const baseAmount = Number(quoteInfo.base_amount);
|
|
117
|
+
const quantity = Number(quoteInfo.quantity);
|
|
118
|
+
|
|
119
|
+
if (quantity > 0 && !Number.isNaN(baseAmount)) {
|
|
120
|
+
// Use floating point division for accuracy with fractional quantities
|
|
121
|
+
const unitPriceToken = (baseAmount / quantity).toFixed(USD_DECIMALS);
|
|
122
|
+
// Convert from token format to unit format (smallest unit)
|
|
123
|
+
const unitPriceUnitResult = fromTokenToUnit(unitPriceToken, USD_DECIMALS);
|
|
124
|
+
const unitPriceUnit =
|
|
125
|
+
typeof unitPriceUnitResult === 'string' ? unitPriceUnitResult : unitPriceUnitResult.toString();
|
|
126
|
+
|
|
127
|
+
referencePrice = {
|
|
128
|
+
amount: unitPriceUnit,
|
|
129
|
+
currency: quoteInfo.base_currency || 'USD',
|
|
130
|
+
decimals: USD_DECIMALS,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
79
134
|
|
|
80
135
|
const creditInfo = (line.price as any).credit;
|
|
81
136
|
let credits: { total: number; currency: string } | undefined;
|
|
@@ -89,25 +144,143 @@ export function getInvoiceRows(invoice: TInvoiceExpanded, t: (key: string) => st
|
|
|
89
144
|
|
|
90
145
|
return {
|
|
91
146
|
id: line.id,
|
|
92
|
-
product:
|
|
93
|
-
line.price.product.unit_label ? ` (per ${line.price.product.unit_label})` : ''
|
|
94
|
-
}`.trim(),
|
|
147
|
+
product: line.description.trim(),
|
|
95
148
|
credits,
|
|
96
149
|
quantity: line.quantity,
|
|
97
150
|
rawQuantity: line.metadata?.quantity || 0,
|
|
98
|
-
price: !line.proration
|
|
99
|
-
|
|
151
|
+
price: !line.proration
|
|
152
|
+
? `${formatAmount(actualUnitPrice, invoice.paymentCurrency.decimal)} ${invoice.paymentCurrency.symbol}`
|
|
153
|
+
: '',
|
|
154
|
+
referencePrice, // Add reference price (USD)
|
|
155
|
+
amount: `${formatAmount(line.amount, invoice.paymentCurrency.decimal)} ${invoice.paymentCurrency.symbol}`,
|
|
100
156
|
raw: line,
|
|
101
157
|
price_id: line.price.id,
|
|
102
158
|
product_id: line.price.product.id,
|
|
103
159
|
};
|
|
104
160
|
});
|
|
105
161
|
|
|
162
|
+
const quoteLine = invoice.lines.find((line) => (line.metadata as any)?.quote?.exchange_rate);
|
|
163
|
+
const quoteInfo = quoteLine ? (quoteLine.metadata as any).quote : null;
|
|
164
|
+
|
|
165
|
+
// Calculate USD amount and exchange rate for subtotal
|
|
166
|
+
let subtotalUsdAmount: string | null = null;
|
|
167
|
+
let formattedRate: string | null = null;
|
|
168
|
+
let rateLine: string | null = null;
|
|
169
|
+
let exchangeRateTooltip: React.ReactNode = null;
|
|
170
|
+
|
|
171
|
+
if (quoteInfo) {
|
|
172
|
+
const providers = quoteInfo?.providers || [];
|
|
173
|
+
const providerNames = providers.map((provider: any) => provider.provider_name).filter(Boolean);
|
|
174
|
+
// Unified provider display format: "provider_name (n sources)" for multiple, specific name for single
|
|
175
|
+
let providerDisplay: string;
|
|
176
|
+
if (providers.length > 1) {
|
|
177
|
+
providerDisplay = `${providerNames[0] || providers[0]?.provider_id || 'data'} (${providers.length} sources)`;
|
|
178
|
+
} else if (providers.length === 1) {
|
|
179
|
+
providerDisplay = providerNames[0] || providers[0]?.provider_id || '—';
|
|
180
|
+
} else {
|
|
181
|
+
providerDisplay = quoteInfo?.rate_provider_name || quoteInfo?.rate_provider_id;
|
|
182
|
+
}
|
|
183
|
+
const rateTimestamp = quoteInfo?.rate_timestamp_ms ? formatTime(quoteInfo.rate_timestamp_ms) : '—';
|
|
184
|
+
|
|
185
|
+
// Calculate USD amount
|
|
186
|
+
if (quoteInfo.exchange_rate && invoice.subtotal) {
|
|
187
|
+
const calculatedUsd = getUsdAmountFromTokenUnits(
|
|
188
|
+
new BN(invoice.subtotal),
|
|
189
|
+
invoice.paymentCurrency.decimal,
|
|
190
|
+
quoteInfo.exchange_rate
|
|
191
|
+
);
|
|
192
|
+
if (calculatedUsd) {
|
|
193
|
+
subtotalUsdAmount = formatUsdAmount(calculatedUsd, locale);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Format exchange rate
|
|
198
|
+
formattedRate = formatExchangeRate(quoteInfo.exchange_rate || null);
|
|
199
|
+
if (formattedRate) {
|
|
200
|
+
const currencyMap = {
|
|
201
|
+
USD: '$',
|
|
202
|
+
CNY: '¥',
|
|
203
|
+
};
|
|
204
|
+
const currencySymbol = currencyMap[quoteInfo.base_currency as keyof typeof currencyMap];
|
|
205
|
+
rateLine = `1 ${invoice.paymentCurrency.symbol} ≈ ${currencySymbol ? `${currencySymbol}${formattedRate}` : `${formattedRate} ${quoteInfo.base_currency || 'USD'}`}`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Create tooltip content (same style as list.tsx)
|
|
209
|
+
exchangeRateTooltip = (
|
|
210
|
+
<Stack spacing={0.5} sx={{ p: 1 }}>
|
|
211
|
+
<Stack direction="row" justifyContent="space-between" spacing={2}>
|
|
212
|
+
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
|
|
213
|
+
{t('payment.customer.invoice.quote.providers')}:
|
|
214
|
+
</Typography>
|
|
215
|
+
<Typography variant="caption" sx={{ color: 'text.primary' }}>
|
|
216
|
+
{providerDisplay || '—'}
|
|
217
|
+
</Typography>
|
|
218
|
+
</Stack>
|
|
219
|
+
{rateLine && (
|
|
220
|
+
<Stack direction="row" justifyContent="space-between" spacing={2}>
|
|
221
|
+
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
|
|
222
|
+
{t('payment.customer.invoice.quote.exchangeRate')}:
|
|
223
|
+
</Typography>
|
|
224
|
+
<Typography variant="caption" sx={{ color: 'text.primary' }}>
|
|
225
|
+
{rateLine}
|
|
226
|
+
</Typography>
|
|
227
|
+
</Stack>
|
|
228
|
+
)}
|
|
229
|
+
<Stack direction="row" justifyContent="space-between" spacing={2}>
|
|
230
|
+
<Typography variant="caption" sx={{ color: 'text.secondary' }}>
|
|
231
|
+
{t('payment.customer.invoice.quote.rateTimestamp')}:
|
|
232
|
+
</Typography>
|
|
233
|
+
<Typography variant="caption" sx={{ color: 'text.primary' }}>
|
|
234
|
+
{rateTimestamp}
|
|
235
|
+
</Typography>
|
|
236
|
+
</Stack>
|
|
237
|
+
</Stack>
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Build subtotal value with USD reference
|
|
242
|
+
const subtotalValue = (() => {
|
|
243
|
+
if (!subtotalUsdAmount || !rateLine) {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
const rateText = (t as any)('payment.customer.invoice.quote.atPaymentRate', { rate: rateLine });
|
|
247
|
+
return (
|
|
248
|
+
<Stack alignItems="flex-end" sx={{ minWidth: '100px', textAlign: 'right' }}>
|
|
249
|
+
<Tooltip
|
|
250
|
+
title={exchangeRateTooltip}
|
|
251
|
+
placement="top"
|
|
252
|
+
arrow
|
|
253
|
+
slotProps={{
|
|
254
|
+
tooltip: {
|
|
255
|
+
sx: {
|
|
256
|
+
backgroundColor: 'background.paper',
|
|
257
|
+
boxShadow: 1,
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
}}>
|
|
261
|
+
<Typography
|
|
262
|
+
component="span"
|
|
263
|
+
variant="caption"
|
|
264
|
+
sx={{
|
|
265
|
+
color: 'text.secondary',
|
|
266
|
+
fontSize: '0.75rem',
|
|
267
|
+
fontWeight: 400,
|
|
268
|
+
lineHeight: 1.2,
|
|
269
|
+
cursor: 'help',
|
|
270
|
+
}}>
|
|
271
|
+
≈ ${subtotalUsdAmount} {rateText}
|
|
272
|
+
</Typography>
|
|
273
|
+
</Tooltip>
|
|
274
|
+
</Stack>
|
|
275
|
+
);
|
|
276
|
+
})();
|
|
277
|
+
|
|
106
278
|
const summary: InvoiceSummaryItem[] = [
|
|
107
279
|
{
|
|
108
280
|
key: 'common.subtotal',
|
|
109
|
-
value: formatAmount(invoice.subtotal, invoice.paymentCurrency.decimal)
|
|
281
|
+
value: `${formatAmount(invoice.subtotal, invoice.paymentCurrency.decimal)} ${invoice.paymentCurrency.symbol}`,
|
|
110
282
|
color: 'text.secondary',
|
|
283
|
+
desc: subtotalValue,
|
|
111
284
|
},
|
|
112
285
|
];
|
|
113
286
|
|
|
@@ -149,7 +322,7 @@ export function getInvoiceRows(invoice: TInvoiceExpanded, t: (key: string) => st
|
|
|
149
322
|
|
|
150
323
|
summary.push({
|
|
151
324
|
key: discountLabel,
|
|
152
|
-
value: `-${formattedDiscountAmount}`,
|
|
325
|
+
value: `-${formattedDiscountAmount} ${invoice.paymentCurrency.symbol}`,
|
|
153
326
|
color: 'text.secondary',
|
|
154
327
|
});
|
|
155
328
|
}
|
|
@@ -159,13 +332,13 @@ export function getInvoiceRows(invoice: TInvoiceExpanded, t: (key: string) => st
|
|
|
159
332
|
|
|
160
333
|
summary.push({
|
|
161
334
|
key: 'common.total',
|
|
162
|
-
value: formatAmount(invoice.total, invoice.paymentCurrency.decimal)
|
|
335
|
+
value: `${formatAmount(invoice.total, invoice.paymentCurrency.decimal)} ${invoice.paymentCurrency.symbol}`,
|
|
163
336
|
color: 'text.secondary',
|
|
164
337
|
});
|
|
165
338
|
if (invoice.amount_paid !== '0') {
|
|
166
339
|
summary.push({
|
|
167
340
|
key: 'payment.customer.invoice.amountPaid',
|
|
168
|
-
value: formatAmount(invoice.amount_paid, invoice.paymentCurrency.decimal)
|
|
341
|
+
value: `${formatAmount(invoice.amount_paid, invoice.paymentCurrency.decimal)} ${invoice.paymentCurrency.symbol}`,
|
|
169
342
|
color: 'text.secondary',
|
|
170
343
|
});
|
|
171
344
|
}
|
|
@@ -174,13 +347,13 @@ export function getInvoiceRows(invoice: TInvoiceExpanded, t: (key: string) => st
|
|
|
174
347
|
if (appliedBalance !== '0') {
|
|
175
348
|
summary.push({
|
|
176
349
|
key: 'payment.customer.invoice.amountApplied',
|
|
177
|
-
value: formatAmount(appliedBalance, invoice.paymentCurrency.decimal)
|
|
350
|
+
value: `${formatAmount(appliedBalance, invoice.paymentCurrency.decimal)} ${invoice.paymentCurrency.symbol}`,
|
|
178
351
|
color: 'text.secondary',
|
|
179
352
|
});
|
|
180
353
|
}
|
|
181
354
|
summary.push({
|
|
182
355
|
key: 'payment.customer.invoice.amountDue',
|
|
183
|
-
value: formatAmount(invoice.amount_remaining, invoice.paymentCurrency.decimal)
|
|
356
|
+
value: `${formatAmount(invoice.amount_remaining, invoice.paymentCurrency.decimal)} ${invoice.paymentCurrency.symbol}`,
|
|
184
357
|
color: invoice.amount_remaining !== '0' ? 'text.primary' : 'text.secondary',
|
|
185
358
|
});
|
|
186
359
|
|
|
@@ -194,7 +367,7 @@ export default function InvoiceTable({ invoice, simple = false, emptyNodeText =
|
|
|
194
367
|
const { t, locale } = useLocaleContext();
|
|
195
368
|
const isAdmin = mode === 'admin';
|
|
196
369
|
const navigate = useNavigate();
|
|
197
|
-
const { detail, summary } = getInvoiceRows(invoice, t);
|
|
370
|
+
const { detail, summary } = getInvoiceRows(invoice, t, locale);
|
|
198
371
|
const [state, setState] = useSetState({
|
|
199
372
|
subscriptionId: '',
|
|
200
373
|
subscriptionItemId: '',
|
|
@@ -225,6 +398,7 @@ export default function InvoiceTable({ invoice, simple = false, emptyNodeText =
|
|
|
225
398
|
options: {
|
|
226
399
|
customBodyRenderLite: (_: string, index: number) => {
|
|
227
400
|
const item = detail[index] as InvoiceDetailItem;
|
|
401
|
+
|
|
228
402
|
return (
|
|
229
403
|
<Box>
|
|
230
404
|
<Typography
|
|
@@ -301,31 +475,20 @@ export default function InvoiceTable({ invoice, simple = false, emptyNodeText =
|
|
|
301
475
|
name: 'price',
|
|
302
476
|
width: 200,
|
|
303
477
|
align: 'right',
|
|
304
|
-
},
|
|
305
|
-
|
|
306
|
-
{
|
|
307
|
-
label: t('common.discount'),
|
|
308
|
-
name: 'discount',
|
|
309
|
-
width: 200,
|
|
310
|
-
align: 'right',
|
|
311
478
|
options: {
|
|
312
479
|
customBodyRenderLite: (_: string, index: number) => {
|
|
313
480
|
const item = detail[index] as InvoiceDetailItem;
|
|
314
|
-
// Calculate discount amount for this line item
|
|
315
|
-
let itemDiscountAmount = toBN(0);
|
|
316
|
-
if (item.raw.discount_amounts && item.raw.discount_amounts.length > 0) {
|
|
317
|
-
item.raw.discount_amounts.forEach((discount: any) => {
|
|
318
|
-
if (discount.amount) {
|
|
319
|
-
itemDiscountAmount = itemDiscountAmount.add(toBN(discount.amount));
|
|
320
|
-
}
|
|
321
|
-
});
|
|
322
|
-
}
|
|
323
|
-
|
|
324
481
|
return (
|
|
325
|
-
<Typography
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
482
|
+
<Typography
|
|
483
|
+
component="div"
|
|
484
|
+
variant="body2"
|
|
485
|
+
sx={{
|
|
486
|
+
textAlign: {
|
|
487
|
+
xs: 'left',
|
|
488
|
+
md: 'right',
|
|
489
|
+
},
|
|
490
|
+
}}>
|
|
491
|
+
{item.price}
|
|
329
492
|
</Typography>
|
|
330
493
|
);
|
|
331
494
|
},
|
|
@@ -344,6 +507,7 @@ export default function InvoiceTable({ invoice, simple = false, emptyNodeText =
|
|
|
344
507
|
},
|
|
345
508
|
},
|
|
346
509
|
},
|
|
510
|
+
|
|
347
511
|
...(isAdmin
|
|
348
512
|
? [
|
|
349
513
|
{
|
|
@@ -384,6 +548,34 @@ export default function InvoiceTable({ invoice, simple = false, emptyNodeText =
|
|
|
384
548
|
},
|
|
385
549
|
]
|
|
386
550
|
: []),
|
|
551
|
+
{
|
|
552
|
+
label: t('common.discount'),
|
|
553
|
+
name: 'discount',
|
|
554
|
+
width: 200,
|
|
555
|
+
align: 'right',
|
|
556
|
+
options: {
|
|
557
|
+
customBodyRenderLite: (_: string, index: number) => {
|
|
558
|
+
const item = detail[index] as InvoiceDetailItem;
|
|
559
|
+
// Calculate discount amount for this line item
|
|
560
|
+
let itemDiscountAmount = toBN(0);
|
|
561
|
+
if (item.raw.discount_amounts && item.raw.discount_amounts.length > 0) {
|
|
562
|
+
item.raw.discount_amounts.forEach((discount: any) => {
|
|
563
|
+
if (discount.amount) {
|
|
564
|
+
itemDiscountAmount = itemDiscountAmount.add(toBN(discount.amount));
|
|
565
|
+
}
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
return (
|
|
570
|
+
<Typography component="span" sx={{ color: 'text.secondary' }}>
|
|
571
|
+
{itemDiscountAmount.gt(toBN(0))
|
|
572
|
+
? `-${formatAmount(itemDiscountAmount.toString(), invoice.paymentCurrency.decimal)} ${invoice.paymentCurrency.symbol}`
|
|
573
|
+
: '-'}
|
|
574
|
+
</Typography>
|
|
575
|
+
);
|
|
576
|
+
},
|
|
577
|
+
},
|
|
578
|
+
},
|
|
387
579
|
...(simple
|
|
388
580
|
? []
|
|
389
581
|
: [
|
|
@@ -447,40 +639,53 @@ export default function InvoiceTable({ invoice, simple = false, emptyNodeText =
|
|
|
447
639
|
const showDivider = isTotal && index > 0;
|
|
448
640
|
|
|
449
641
|
return (
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
justifyContent: 'flex-end',
|
|
455
|
-
width: '100%',
|
|
456
|
-
py: 0.5,
|
|
457
|
-
|
|
458
|
-
...(showDivider && {
|
|
459
|
-
borderTop: '1px solid',
|
|
460
|
-
borderTopColor: 'divider',
|
|
461
|
-
pt: 1,
|
|
462
|
-
mt: 0.5,
|
|
463
|
-
}),
|
|
464
|
-
}}>
|
|
465
|
-
<Typography
|
|
466
|
-
variant="body2"
|
|
642
|
+
<>
|
|
643
|
+
<Stack
|
|
644
|
+
key={line.key}
|
|
645
|
+
direction="row"
|
|
467
646
|
sx={{
|
|
468
|
-
|
|
469
|
-
|
|
647
|
+
justifyContent: 'flex-end',
|
|
648
|
+
width: '100%',
|
|
649
|
+
py: 0.5,
|
|
650
|
+
gap: 2,
|
|
651
|
+
|
|
652
|
+
...(showDivider && {
|
|
653
|
+
borderTop: '1px solid',
|
|
654
|
+
borderTopColor: 'divider',
|
|
655
|
+
pt: 1,
|
|
656
|
+
mt: 0.5,
|
|
657
|
+
}),
|
|
470
658
|
}}>
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
659
|
+
<Typography
|
|
660
|
+
variant="body2"
|
|
661
|
+
sx={{
|
|
662
|
+
color: 'text.primary',
|
|
663
|
+
fontWeight: isTotal ? 500 : 400,
|
|
664
|
+
}}>
|
|
665
|
+
{line.key.startsWith('common.') || line.key.startsWith('payment.') ? t(line.key) : line.key}
|
|
666
|
+
</Typography>
|
|
667
|
+
<Typography
|
|
668
|
+
variant="body2"
|
|
669
|
+
sx={{
|
|
670
|
+
color: 'text.primary',
|
|
671
|
+
fontWeight: isTotal ? 500 : 400,
|
|
672
|
+
minWidth: '100px',
|
|
673
|
+
textAlign: 'right',
|
|
674
|
+
}}>
|
|
675
|
+
{line.value}
|
|
676
|
+
</Typography>
|
|
677
|
+
</Stack>
|
|
678
|
+
{(() => {
|
|
679
|
+
if (line.tooltip && line.desc) {
|
|
680
|
+
return (
|
|
681
|
+
<Tooltip title={line.tooltip} placement="top" arrow>
|
|
682
|
+
<Box component="span">{line.desc}</Box>
|
|
683
|
+
</Tooltip>
|
|
684
|
+
);
|
|
685
|
+
}
|
|
686
|
+
return line.desc;
|
|
687
|
+
})()}
|
|
688
|
+
</>
|
|
484
689
|
);
|
|
485
690
|
})}
|
|
486
691
|
</Box>
|
|
@@ -125,9 +125,7 @@ export function InvoiceTemplate({ data, t }: InvoicePDFProps) {
|
|
|
125
125
|
<span style={composeStyles('dark right')}>{line.quantity}</span>
|
|
126
126
|
</div>
|
|
127
127
|
<div style={composeStyles('w-15 p-4-8 pb-15')}>
|
|
128
|
-
<span style={composeStyles('dark right')}>
|
|
129
|
-
{line.price ? `${line.price} ${data.paymentCurrency.symbol}` : ''}
|
|
130
|
-
</span>
|
|
128
|
+
<span style={composeStyles('dark right')}>{line.price || ''}</span>
|
|
131
129
|
</div>
|
|
132
130
|
<div style={composeStyles('w-15 p-4-8 pb-15')}>
|
|
133
131
|
<span style={composeStyles('dark right')}>
|
|
@@ -139,9 +137,7 @@ export function InvoiceTemplate({ data, t }: InvoicePDFProps) {
|
|
|
139
137
|
</span>
|
|
140
138
|
</div>
|
|
141
139
|
<div style={composeStyles('w-17 p-4-8 pb-15')}>
|
|
142
|
-
<span style={composeStyles('dark right')}>
|
|
143
|
-
{line.amount} {data.paymentCurrency.symbol}
|
|
144
|
-
</span>
|
|
140
|
+
<span style={composeStyles('dark right')}>{line.amount}</span>
|
|
145
141
|
</div>
|
|
146
142
|
</div>
|
|
147
143
|
);
|
|
@@ -212,7 +208,7 @@ export function InvoiceTemplate({ data, t }: InvoicePDFProps) {
|
|
|
212
208
|
...composeStyles('bold dark text-right'),
|
|
213
209
|
minWidth: '80px',
|
|
214
210
|
}}>
|
|
215
|
-
{line.value}
|
|
211
|
+
{line.value}
|
|
216
212
|
</span>
|
|
217
213
|
</div>
|
|
218
214
|
</div>
|
|
@@ -294,15 +294,13 @@ export default function MetadataForm({
|
|
|
294
294
|
<Tooltip
|
|
295
295
|
title={formatError || t('common.metadata.formatJson')}
|
|
296
296
|
placement="top"
|
|
297
|
-
|
|
297
|
+
slotProps={{
|
|
298
298
|
tooltip: {
|
|
299
|
-
sx:
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
}
|
|
305
|
-
: {},
|
|
299
|
+
sx: {
|
|
300
|
+
maxWidth: 300,
|
|
301
|
+
backgroundColor: 'error.main',
|
|
302
|
+
opacity: 0.8,
|
|
303
|
+
},
|
|
306
304
|
},
|
|
307
305
|
}}>
|
|
308
306
|
<IconButton
|