payment-kit 1.20.11 → 1.20.13
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/index.ts +8 -0
- package/api/src/index.ts +2 -0
- package/api/src/integrations/stripe/handlers/invoice.ts +63 -5
- package/api/src/integrations/stripe/handlers/payment-intent.ts +1 -0
- package/api/src/integrations/stripe/resource.ts +253 -2
- package/api/src/libs/currency.ts +31 -0
- package/api/src/libs/discount/coupon.ts +1061 -0
- package/api/src/libs/discount/discount.ts +349 -0
- package/api/src/libs/discount/nft.ts +239 -0
- package/api/src/libs/discount/redemption.ts +636 -0
- package/api/src/libs/discount/vc.ts +73 -0
- package/api/src/libs/env.ts +1 -0
- package/api/src/libs/invoice.ts +44 -10
- package/api/src/libs/math-utils.ts +6 -0
- package/api/src/libs/price.ts +43 -0
- package/api/src/libs/session.ts +242 -57
- package/api/src/libs/subscription.ts +2 -6
- package/api/src/libs/vendor-util/adapters/launcher-adapter.ts +1 -1
- package/api/src/libs/vendor-util/adapters/types.ts +1 -0
- package/api/src/libs/vendor-util/fulfillment.ts +1 -1
- package/api/src/queues/auto-recharge.ts +1 -1
- package/api/src/queues/discount-status.ts +200 -0
- package/api/src/queues/subscription.ts +98 -5
- package/api/src/queues/usage-record.ts +1 -1
- package/api/src/queues/vendors/fulfillment-coordinator.ts +1 -29
- package/api/src/queues/vendors/return-processor.ts +184 -0
- package/api/src/queues/vendors/return-scanner.ts +119 -0
- package/api/src/queues/vendors/status-check.ts +1 -1
- package/api/src/routes/auto-recharge-configs.ts +5 -3
- package/api/src/routes/checkout-sessions.ts +755 -64
- package/api/src/routes/connect/change-payment.ts +6 -1
- package/api/src/routes/connect/change-plan.ts +6 -1
- package/api/src/routes/connect/setup.ts +6 -1
- package/api/src/routes/connect/shared.ts +80 -9
- package/api/src/routes/connect/subscribe.ts +12 -2
- package/api/src/routes/coupons.ts +518 -0
- package/api/src/routes/index.ts +4 -0
- package/api/src/routes/invoices.ts +44 -3
- package/api/src/routes/meter-events.ts +2 -1
- package/api/src/routes/payment-currencies.ts +1 -0
- package/api/src/routes/promotion-codes.ts +482 -0
- package/api/src/routes/subscriptions.ts +23 -2
- package/api/src/routes/vendor.ts +89 -2
- package/api/src/store/migrations/20250904-discount.ts +136 -0
- package/api/src/store/migrations/20250910-timestamp-fields.ts +116 -0
- package/api/src/store/migrations/20250916-add-description-fields.ts +30 -0
- package/api/src/store/migrations/20250918-add-vendor-extends.ts +20 -0
- package/api/src/store/models/checkout-session.ts +17 -2
- package/api/src/store/models/coupon.ts +144 -4
- package/api/src/store/models/discount.ts +23 -10
- package/api/src/store/models/index.ts +13 -2
- package/api/src/store/models/product-vendor.ts +6 -0
- package/api/src/store/models/promotion-code.ts +295 -18
- package/api/src/store/models/types.ts +30 -1
- package/api/tests/libs/session.spec.ts +48 -27
- package/blocklet.yml +1 -1
- package/package.json +20 -20
- package/src/app.tsx +2 -0
- package/src/components/customer/link.tsx +1 -1
- package/src/components/discount/discount-info.tsx +178 -0
- package/src/components/invoice/table.tsx +140 -48
- package/src/components/invoice-pdf/styles.ts +6 -0
- package/src/components/invoice-pdf/template.tsx +59 -33
- package/src/components/metadata/form.tsx +14 -5
- package/src/components/payment-link/actions.tsx +42 -0
- package/src/components/price/form.tsx +91 -65
- package/src/components/product/vendor-config.tsx +5 -3
- package/src/components/promotion/active-redemptions.tsx +534 -0
- package/src/components/promotion/currency-multi-select.tsx +350 -0
- package/src/components/promotion/currency-restrictions.tsx +117 -0
- package/src/components/promotion/product-select.tsx +292 -0
- package/src/components/promotion/promotion-code-form.tsx +534 -0
- package/src/components/subscription/portal/list.tsx +6 -1
- package/src/components/subscription/vendor-service-list.tsx +13 -2
- package/src/locales/en.tsx +227 -0
- package/src/locales/zh.tsx +222 -1
- package/src/pages/admin/billing/subscriptions/detail.tsx +5 -0
- package/src/pages/admin/products/coupons/applicable-products.tsx +166 -0
- package/src/pages/admin/products/coupons/create.tsx +612 -0
- package/src/pages/admin/products/coupons/detail.tsx +538 -0
- package/src/pages/admin/products/coupons/edit.tsx +127 -0
- package/src/pages/admin/products/coupons/index.tsx +210 -3
- package/src/pages/admin/products/index.tsx +22 -3
- package/src/pages/admin/products/products/detail.tsx +12 -2
- package/src/pages/admin/products/promotion-codes/actions.tsx +103 -0
- package/src/pages/admin/products/promotion-codes/create.tsx +235 -0
- package/src/pages/admin/products/promotion-codes/detail.tsx +416 -0
- package/src/pages/admin/products/promotion-codes/list.tsx +247 -0
- package/src/pages/admin/products/promotion-codes/verification-config.tsx +327 -0
- package/src/pages/admin/products/vendors/index.tsx +17 -5
- package/src/pages/customer/subscription/detail.tsx +5 -0
- package/vite.config.ts +4 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.13",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "blocklet dev --open",
|
|
6
6
|
"lint": "tsc --noEmit && eslint src api/src --ext .mjs,.js,.jsx,.ts,.tsx",
|
|
@@ -45,32 +45,32 @@
|
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
47
|
"@abtnode/cron": "^1.16.52-beta-20250912-112002-e3499e9c",
|
|
48
|
-
"@arcblock/did": "^1.
|
|
49
|
-
"@arcblock/did-connect-react": "^3.1.
|
|
48
|
+
"@arcblock/did": "^1.25.1",
|
|
49
|
+
"@arcblock/did-connect-react": "^3.1.41",
|
|
50
50
|
"@arcblock/did-connect-storage-nedb": "^1.8.0",
|
|
51
|
-
"@arcblock/did-util": "^1.
|
|
52
|
-
"@arcblock/jwt": "^1.
|
|
53
|
-
"@arcblock/ux": "^3.1.
|
|
54
|
-
"@arcblock/validator": "^1.
|
|
55
|
-
"@blocklet/did-space-js": "^1.1.
|
|
51
|
+
"@arcblock/did-util": "^1.25.1",
|
|
52
|
+
"@arcblock/jwt": "^1.25.1",
|
|
53
|
+
"@arcblock/ux": "^3.1.41",
|
|
54
|
+
"@arcblock/validator": "^1.25.1",
|
|
55
|
+
"@blocklet/did-space-js": "^1.1.24",
|
|
56
56
|
"@blocklet/error": "^0.2.5",
|
|
57
57
|
"@blocklet/js-sdk": "^1.16.52-beta-20250912-112002-e3499e9c",
|
|
58
58
|
"@blocklet/logger": "^1.16.52-beta-20250912-112002-e3499e9c",
|
|
59
|
-
"@blocklet/payment-react": "1.20.
|
|
60
|
-
"@blocklet/payment-vendor": "1.20.
|
|
59
|
+
"@blocklet/payment-react": "1.20.13",
|
|
60
|
+
"@blocklet/payment-vendor": "1.20.13",
|
|
61
61
|
"@blocklet/sdk": "^1.16.52-beta-20250912-112002-e3499e9c",
|
|
62
|
-
"@blocklet/ui-react": "^3.1.
|
|
63
|
-
"@blocklet/uploader": "^0.2.
|
|
62
|
+
"@blocklet/ui-react": "^3.1.41",
|
|
63
|
+
"@blocklet/uploader": "^0.2.11",
|
|
64
64
|
"@blocklet/xss": "^0.2.7",
|
|
65
65
|
"@mui/icons-material": "^7.1.2",
|
|
66
66
|
"@mui/lab": "7.0.0-beta.14",
|
|
67
67
|
"@mui/material": "^7.1.2",
|
|
68
68
|
"@mui/system": "^7.1.1",
|
|
69
|
-
"@ocap/asset": "^1.
|
|
70
|
-
"@ocap/client": "^1.
|
|
71
|
-
"@ocap/mcrypto": "^1.
|
|
72
|
-
"@ocap/util": "^1.
|
|
73
|
-
"@ocap/wallet": "^1.
|
|
69
|
+
"@ocap/asset": "^1.25.1",
|
|
70
|
+
"@ocap/client": "^1.25.1",
|
|
71
|
+
"@ocap/mcrypto": "^1.25.1",
|
|
72
|
+
"@ocap/util": "^1.25.1",
|
|
73
|
+
"@ocap/wallet": "^1.25.1",
|
|
74
74
|
"@stripe/react-stripe-js": "^2.9.0",
|
|
75
75
|
"@stripe/stripe-js": "^2.4.0",
|
|
76
76
|
"ahooks": "^3.8.5",
|
|
@@ -126,7 +126,7 @@
|
|
|
126
126
|
"devDependencies": {
|
|
127
127
|
"@abtnode/types": "^1.16.52-beta-20250912-112002-e3499e9c",
|
|
128
128
|
"@arcblock/eslint-config-ts": "^0.3.3",
|
|
129
|
-
"@blocklet/payment-types": "1.20.
|
|
129
|
+
"@blocklet/payment-types": "1.20.13",
|
|
130
130
|
"@types/cookie-parser": "^1.4.9",
|
|
131
131
|
"@types/cors": "^2.8.19",
|
|
132
132
|
"@types/debug": "^4.1.12",
|
|
@@ -157,7 +157,7 @@
|
|
|
157
157
|
"vite": "^7.0.0",
|
|
158
158
|
"vite-node": "^3.2.4",
|
|
159
159
|
"vite-plugin-babel-import": "^2.0.5",
|
|
160
|
-
"vite-plugin-blocklet": "^0.
|
|
160
|
+
"vite-plugin-blocklet": "^0.11.0",
|
|
161
161
|
"vite-plugin-node-polyfills": "^0.23.0",
|
|
162
162
|
"vite-plugin-svgr": "^4.3.0",
|
|
163
163
|
"vite-tsconfig-paths": "^5.1.4",
|
|
@@ -173,5 +173,5 @@
|
|
|
173
173
|
"parser": "typescript"
|
|
174
174
|
}
|
|
175
175
|
},
|
|
176
|
-
"gitHead": "
|
|
176
|
+
"gitHead": "0cbe918549f7d06561b5307fb947bfbbd2250984"
|
|
177
177
|
}
|
package/src/app.tsx
CHANGED
|
@@ -58,6 +58,8 @@ function App() {
|
|
|
58
58
|
<Route key="admin-index" path="/admin" element={<AdminPage />} />,
|
|
59
59
|
<Route key="admin-tabs" path="/admin/:group" element={<AdminPage />} />,
|
|
60
60
|
<Route key="admin-sub" path="/admin/:group/:page" element={<AdminPage />} />,
|
|
61
|
+
<Route key="admin-detail" path="/admin/:group/:page/:id" element={<AdminPage />} />,
|
|
62
|
+
<Route key="admin-nested" path="/admin/:group/:page/:subpage/:id" element={<AdminPage />} />,
|
|
61
63
|
<Route key="admin-fallback" path="/admin/*" element={<AdminPage />} />,
|
|
62
64
|
<Route key="integrations-index" path="/integrations" element={<IntegrationsPage />} />
|
|
63
65
|
<Route key="integrations-tabs" path="/integrations/:group" element={<IntegrationsPage />} />
|
|
@@ -40,7 +40,7 @@ export default function CustomerLink({
|
|
|
40
40
|
popupInfoType={InfoType.Minimal}
|
|
41
41
|
showDid={size !== 'small'}
|
|
42
42
|
popupShowDid
|
|
43
|
-
{...(customer
|
|
43
|
+
{...(customer?.metadata?.anonymous === true
|
|
44
44
|
? {
|
|
45
45
|
user: {
|
|
46
46
|
fullName: customer.name || customer.email,
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { Box, Typography, Stack } from '@mui/material';
|
|
2
|
+
import { LocalOfferOutlined } from '@mui/icons-material';
|
|
3
|
+
import { formatTime, formatAmount, findCurrency, usePaymentContext, useMobile } from '@blocklet/payment-react';
|
|
4
|
+
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
5
|
+
|
|
6
|
+
type DiscountStats = {
|
|
7
|
+
subscription_id: string;
|
|
8
|
+
total_discount_records: number;
|
|
9
|
+
total_savings: Record<
|
|
10
|
+
string,
|
|
11
|
+
{
|
|
12
|
+
amount: string;
|
|
13
|
+
currency: any;
|
|
14
|
+
formattedAmount: string;
|
|
15
|
+
}
|
|
16
|
+
>;
|
|
17
|
+
coupons_used: string[];
|
|
18
|
+
promotion_codes_used: any[];
|
|
19
|
+
discount_records: any[];
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
type DiscountInfoProps = {
|
|
23
|
+
discountStats?: DiscountStats | null;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export default function DiscountInfo({ discountStats = null }: DiscountInfoProps) {
|
|
27
|
+
const { t } = useLocaleContext();
|
|
28
|
+
const { settings } = usePaymentContext();
|
|
29
|
+
const { isMobile } = useMobile();
|
|
30
|
+
|
|
31
|
+
if (!discountStats || discountStats.total_discount_records === 0) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Get primary coupon info
|
|
36
|
+
const primaryRecord = discountStats.discount_records?.[0];
|
|
37
|
+
const coupon = primaryRecord?.coupon;
|
|
38
|
+
|
|
39
|
+
if (!coupon) {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Use coupon name instead of promotion code
|
|
44
|
+
const couponName = coupon.name || t('admin.coupon.discount');
|
|
45
|
+
|
|
46
|
+
// Format discount type
|
|
47
|
+
const getDiscountType = () => {
|
|
48
|
+
if (coupon.percent_off && coupon.percent_off > 0) {
|
|
49
|
+
return t('admin.coupon.couponTermsPercentage', { percent: coupon.percent_off });
|
|
50
|
+
}
|
|
51
|
+
if (coupon.amount_off && coupon.amount_off !== '0') {
|
|
52
|
+
const currency = findCurrency(settings.paymentMethods, coupon.currency_id) || settings.baseCurrency;
|
|
53
|
+
return t('admin.coupon.couponTermsFixedAmount', {
|
|
54
|
+
amount: formatAmount(coupon.amount_off, currency.decimal),
|
|
55
|
+
symbol: currency.symbol,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return t('admin.coupon.noDiscount');
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// Format duration
|
|
62
|
+
const getDuration = () => {
|
|
63
|
+
if (coupon.duration === 'repeating' && coupon.duration_in_months) {
|
|
64
|
+
return t(`admin.coupon.couponTermsDuration.${coupon.duration}`, { months: coupon.duration_in_months });
|
|
65
|
+
}
|
|
66
|
+
return t(`admin.coupon.couponTermsDuration.${coupon.duration}`);
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
// Format total savings
|
|
70
|
+
const totalSavingsEntries = Object.entries(discountStats.total_savings);
|
|
71
|
+
const savingsText =
|
|
72
|
+
totalSavingsEntries.length > 0
|
|
73
|
+
? totalSavingsEntries.map(([, savings]) => savings.formattedAmount).join(', ')
|
|
74
|
+
: '--';
|
|
75
|
+
|
|
76
|
+
// Get validity period
|
|
77
|
+
const getValidityPeriod = () => {
|
|
78
|
+
if (coupon.duration !== 'repeating') {
|
|
79
|
+
return '';
|
|
80
|
+
}
|
|
81
|
+
if (primaryRecord?.discount_start && primaryRecord?.discount_end) {
|
|
82
|
+
const startDate = formatTime(primaryRecord.discount_start * 1000);
|
|
83
|
+
const endDate = formatTime(primaryRecord.discount_end * 1000);
|
|
84
|
+
return `${startDate} ~ ${endDate}`;
|
|
85
|
+
}
|
|
86
|
+
return '';
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
return (
|
|
90
|
+
<Box
|
|
91
|
+
sx={{
|
|
92
|
+
bgcolor: 'grey.50',
|
|
93
|
+
borderRadius: 1.5,
|
|
94
|
+
p: 2.5,
|
|
95
|
+
my: 2,
|
|
96
|
+
border: '1px solid',
|
|
97
|
+
borderColor: 'divider',
|
|
98
|
+
}}>
|
|
99
|
+
<Stack spacing={1}>
|
|
100
|
+
<Stack
|
|
101
|
+
direction={isMobile ? 'column' : 'row'}
|
|
102
|
+
spacing={2}
|
|
103
|
+
sx={{
|
|
104
|
+
alignItems: isMobile ? 'flex-start' : 'center',
|
|
105
|
+
}}>
|
|
106
|
+
<Stack
|
|
107
|
+
direction="row"
|
|
108
|
+
spacing={1}
|
|
109
|
+
sx={{
|
|
110
|
+
alignItems: 'center',
|
|
111
|
+
flex: 1,
|
|
112
|
+
}}>
|
|
113
|
+
<LocalOfferOutlined sx={{ color: 'primary.main', fontSize: 20 }} />
|
|
114
|
+
<Typography variant="subtitle2" sx={{ fontWeight: 600, color: 'text.primary' }}>
|
|
115
|
+
{couponName}
|
|
116
|
+
</Typography>
|
|
117
|
+
</Stack>
|
|
118
|
+
|
|
119
|
+
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
|
|
120
|
+
<Typography variant="body2" sx={{ fontWeight: 500, color: 'text.secondary' }}>
|
|
121
|
+
{getDiscountType()}
|
|
122
|
+
</Typography>
|
|
123
|
+
{getDuration() && (
|
|
124
|
+
<>
|
|
125
|
+
<Typography
|
|
126
|
+
variant="body2"
|
|
127
|
+
sx={{
|
|
128
|
+
color: 'text.disabled',
|
|
129
|
+
}}>
|
|
130
|
+
|
|
|
131
|
+
</Typography>
|
|
132
|
+
<Typography
|
|
133
|
+
variant="body2"
|
|
134
|
+
sx={{
|
|
135
|
+
color: 'text.secondary',
|
|
136
|
+
}}>
|
|
137
|
+
{getDuration()}
|
|
138
|
+
</Typography>
|
|
139
|
+
</>
|
|
140
|
+
)}
|
|
141
|
+
</Box>
|
|
142
|
+
</Stack>
|
|
143
|
+
|
|
144
|
+
<Stack
|
|
145
|
+
direction={isMobile ? 'column' : 'row'}
|
|
146
|
+
spacing={2}
|
|
147
|
+
sx={{
|
|
148
|
+
alignItems: isMobile ? 'flex-start' : 'center',
|
|
149
|
+
}}>
|
|
150
|
+
<Box sx={{ flex: 1 }}>
|
|
151
|
+
<Typography
|
|
152
|
+
variant="body2"
|
|
153
|
+
sx={{
|
|
154
|
+
color: 'text.secondary',
|
|
155
|
+
}}>
|
|
156
|
+
{t('admin.discount.totalSaved')}:{' '}
|
|
157
|
+
<Typography component="span" sx={{ color: 'success.main', fontWeight: 600, ml: 0.5 }}>
|
|
158
|
+
{savingsText}
|
|
159
|
+
</Typography>
|
|
160
|
+
</Typography>
|
|
161
|
+
</Box>
|
|
162
|
+
|
|
163
|
+
{getValidityPeriod() && (
|
|
164
|
+
<Box>
|
|
165
|
+
<Typography
|
|
166
|
+
variant="body2"
|
|
167
|
+
sx={{
|
|
168
|
+
color: 'text.primary',
|
|
169
|
+
}}>
|
|
170
|
+
{t('admin.coupon.discountPeriod')}: {getValidityPeriod()}
|
|
171
|
+
</Typography>
|
|
172
|
+
</Box>
|
|
173
|
+
)}
|
|
174
|
+
</Stack>
|
|
175
|
+
</Stack>
|
|
176
|
+
</Box>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
@@ -63,7 +63,7 @@ export function getAppliedBalance(invoice: TInvoiceExpanded) {
|
|
|
63
63
|
return '0';
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
export function getInvoiceRows(invoice: TInvoiceExpanded) {
|
|
66
|
+
export function getInvoiceRows(invoice: TInvoiceExpanded, t: (key: string) => string) {
|
|
67
67
|
const detail: InvoiceDetailItem[] = invoice.lines.map((line) => {
|
|
68
68
|
const price = line.quantity
|
|
69
69
|
? toBN(line.amount).div(toBN(line.quantity)).toString()
|
|
@@ -99,14 +99,61 @@ export function getInvoiceRows(invoice: TInvoiceExpanded) {
|
|
|
99
99
|
{
|
|
100
100
|
key: 'common.subtotal',
|
|
101
101
|
value: formatAmount(invoice.subtotal, invoice.paymentCurrency.decimal),
|
|
102
|
-
color: 'text.
|
|
103
|
-
},
|
|
104
|
-
{
|
|
105
|
-
key: 'common.total',
|
|
106
|
-
value: formatAmount(invoice.total, invoice.paymentCurrency.decimal),
|
|
107
|
-
color: 'text.primary',
|
|
102
|
+
color: 'text.secondary',
|
|
108
103
|
},
|
|
109
104
|
];
|
|
105
|
+
|
|
106
|
+
// Add discount information from discountDetails
|
|
107
|
+
if ((invoice as any).discountDetails && (invoice as any).discountDetails.length > 0) {
|
|
108
|
+
(invoice as any).discountDetails.forEach((discount: any) => {
|
|
109
|
+
if (discount.coupon) {
|
|
110
|
+
let discountAmount = '0';
|
|
111
|
+
|
|
112
|
+
// Calculate discount amount from total_discount_amounts
|
|
113
|
+
if (invoice.total_discount_amounts && invoice.total_discount_amounts.length > 0) {
|
|
114
|
+
const matchingDiscount = invoice.total_discount_amounts.find(
|
|
115
|
+
(tda: any) => tda.discount === discount.id || tda.coupon === discount.coupon_id
|
|
116
|
+
);
|
|
117
|
+
if (matchingDiscount) {
|
|
118
|
+
discountAmount = matchingDiscount.amount.toString();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (toBN(discountAmount).gt(toBN(0))) {
|
|
123
|
+
const couponName = discount.coupon.name || t('common.coupon');
|
|
124
|
+
const formattedDiscountAmount = formatAmount(discountAmount, invoice.paymentCurrency.decimal);
|
|
125
|
+
|
|
126
|
+
// Format discount display following Stripe style: coupon_name (discount_type)
|
|
127
|
+
let discountLabel = couponName;
|
|
128
|
+
if (discount.coupon.percent_off) {
|
|
129
|
+
const discountText = (t as any)('admin.coupon.couponTermsPercentage', {
|
|
130
|
+
percent: discount.coupon.percent_off,
|
|
131
|
+
});
|
|
132
|
+
discountLabel = `${couponName} (${discountText})`;
|
|
133
|
+
} else if (discount.coupon.amount_off) {
|
|
134
|
+
const fixedAmount = formatAmount(discount.coupon.amount_off, invoice.paymentCurrency.decimal);
|
|
135
|
+
const discountText = (t as any)('admin.coupon.couponTermsFixedAmount', {
|
|
136
|
+
amount: fixedAmount,
|
|
137
|
+
symbol: invoice.paymentCurrency.symbol,
|
|
138
|
+
});
|
|
139
|
+
discountLabel = `${couponName} (${discountText})`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
summary.push({
|
|
143
|
+
key: discountLabel,
|
|
144
|
+
value: `-${formattedDiscountAmount}`,
|
|
145
|
+
color: 'text.secondary',
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
summary.push({
|
|
153
|
+
key: 'common.total',
|
|
154
|
+
value: formatAmount(invoice.total, invoice.paymentCurrency.decimal),
|
|
155
|
+
color: 'text.secondary',
|
|
156
|
+
});
|
|
110
157
|
if (invoice.amount_paid !== '0') {
|
|
111
158
|
summary.push({
|
|
112
159
|
key: 'payment.customer.invoice.amountPaid',
|
|
@@ -126,7 +173,7 @@ export function getInvoiceRows(invoice: TInvoiceExpanded) {
|
|
|
126
173
|
summary.push({
|
|
127
174
|
key: 'payment.customer.invoice.amountDue',
|
|
128
175
|
value: formatAmount(invoice.amount_remaining, invoice.paymentCurrency.decimal),
|
|
129
|
-
color: 'text.primary',
|
|
176
|
+
color: invoice.amount_remaining !== '0' ? 'text.primary' : 'text.secondary',
|
|
130
177
|
});
|
|
131
178
|
|
|
132
179
|
return {
|
|
@@ -137,7 +184,7 @@ export function getInvoiceRows(invoice: TInvoiceExpanded) {
|
|
|
137
184
|
|
|
138
185
|
export default function InvoiceTable({ invoice, simple = false, emptyNodeText = '' }: Props) {
|
|
139
186
|
const { t, locale } = useLocaleContext();
|
|
140
|
-
const { detail, summary } = getInvoiceRows(invoice);
|
|
187
|
+
const { detail, summary } = getInvoiceRows(invoice, t);
|
|
141
188
|
const [state, setState] = useSetState({
|
|
142
189
|
subscriptionId: '',
|
|
143
190
|
subscriptionItemId: '',
|
|
@@ -246,6 +293,34 @@ export default function InvoiceTable({ invoice, simple = false, emptyNodeText =
|
|
|
246
293
|
width: 200,
|
|
247
294
|
align: 'right',
|
|
248
295
|
},
|
|
296
|
+
{
|
|
297
|
+
label: t('common.discount'),
|
|
298
|
+
name: 'discount',
|
|
299
|
+
width: 200,
|
|
300
|
+
align: 'right',
|
|
301
|
+
options: {
|
|
302
|
+
customBodyRenderLite: (_: string, index: number) => {
|
|
303
|
+
const item = detail[index] as InvoiceDetailItem;
|
|
304
|
+
// Calculate discount amount for this line item
|
|
305
|
+
let itemDiscountAmount = toBN(0);
|
|
306
|
+
if (item.raw.discount_amounts && item.raw.discount_amounts.length > 0) {
|
|
307
|
+
item.raw.discount_amounts.forEach((discount: any) => {
|
|
308
|
+
if (discount.amount) {
|
|
309
|
+
itemDiscountAmount = itemDiscountAmount.add(toBN(discount.amount));
|
|
310
|
+
}
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return (
|
|
315
|
+
<Typography component="span" sx={{ color: 'text.secondary' }}>
|
|
316
|
+
{itemDiscountAmount.gt(toBN(0))
|
|
317
|
+
? `-${formatAmount(itemDiscountAmount.toString(), invoice.paymentCurrency.decimal)}`
|
|
318
|
+
: '-'}
|
|
319
|
+
</Typography>
|
|
320
|
+
);
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
},
|
|
249
324
|
{
|
|
250
325
|
label: t('common.amount'),
|
|
251
326
|
name: 'amount',
|
|
@@ -299,49 +374,72 @@ export default function InvoiceTable({ invoice, simple = false, emptyNodeText =
|
|
|
299
374
|
emptyNodeText={emptyNodeText || t('payment.customer.invoice.emptyList')}
|
|
300
375
|
/>
|
|
301
376
|
{!isEmpty(detail) && invoice.status !== 'void' && (
|
|
302
|
-
<
|
|
377
|
+
<Box
|
|
303
378
|
className="invoice-summary"
|
|
304
379
|
sx={{
|
|
305
380
|
display: 'flex',
|
|
306
|
-
flexDirection:
|
|
307
|
-
|
|
308
|
-
|
|
381
|
+
flexDirection: 'column',
|
|
382
|
+
alignItems: 'flex-end',
|
|
383
|
+
float: 'right',
|
|
384
|
+
pr: 2,
|
|
385
|
+
mt: {
|
|
386
|
+
xs: 1,
|
|
387
|
+
md: 1,
|
|
309
388
|
},
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
md: 'center',
|
|
389
|
+
width: {
|
|
390
|
+
xs: '100%',
|
|
391
|
+
md: 'auto',
|
|
314
392
|
},
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
md: 3,
|
|
393
|
+
minWidth: {
|
|
394
|
+
md: '280px',
|
|
318
395
|
},
|
|
319
|
-
|
|
320
|
-
xs:
|
|
321
|
-
md:
|
|
396
|
+
maxWidth: {
|
|
397
|
+
xs: '100%',
|
|
398
|
+
md: '400px',
|
|
322
399
|
},
|
|
323
400
|
}}>
|
|
324
|
-
{summary.map((line) =>
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
{t(line.key)}:
|
|
333
|
-
</Typography>
|
|
334
|
-
<Typography
|
|
335
|
-
component="span"
|
|
336
|
-
variant="body1"
|
|
401
|
+
{summary.map((line, index) => {
|
|
402
|
+
const isTotal = line.key === 'common.total';
|
|
403
|
+
const showDivider = isTotal && index > 0;
|
|
404
|
+
|
|
405
|
+
return (
|
|
406
|
+
<Stack
|
|
407
|
+
key={line.key}
|
|
408
|
+
direction="row"
|
|
337
409
|
sx={{
|
|
338
|
-
|
|
410
|
+
justifyContent: 'flex-end',
|
|
411
|
+
width: '100%',
|
|
412
|
+
py: 0.5,
|
|
413
|
+
|
|
414
|
+
...(showDivider && {
|
|
415
|
+
borderTop: '1px solid',
|
|
416
|
+
borderTopColor: 'divider',
|
|
417
|
+
pt: 1,
|
|
418
|
+
mt: 0.5,
|
|
419
|
+
}),
|
|
339
420
|
}}>
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
421
|
+
<Typography
|
|
422
|
+
variant="body2"
|
|
423
|
+
sx={{
|
|
424
|
+
color: 'text.primary',
|
|
425
|
+
fontWeight: isTotal ? 500 : 400,
|
|
426
|
+
}}>
|
|
427
|
+
{line.key.startsWith('common.') || line.key.startsWith('payment.') ? t(line.key) : line.key}
|
|
428
|
+
</Typography>
|
|
429
|
+
<Typography
|
|
430
|
+
variant="body2"
|
|
431
|
+
sx={{
|
|
432
|
+
color: 'text.primary',
|
|
433
|
+
fontWeight: isTotal ? 500 : 400,
|
|
434
|
+
minWidth: '80px',
|
|
435
|
+
textAlign: 'right',
|
|
436
|
+
}}>
|
|
437
|
+
{line.value}
|
|
438
|
+
</Typography>
|
|
439
|
+
</Stack>
|
|
440
|
+
);
|
|
441
|
+
})}
|
|
442
|
+
</Box>
|
|
345
443
|
)}
|
|
346
444
|
{state.subscriptionId && state.subscriptionItemId && (
|
|
347
445
|
<UsageRecordDialog
|
|
@@ -359,9 +457,6 @@ export default function InvoiceTable({ invoice, simple = false, emptyNodeText =
|
|
|
359
457
|
}
|
|
360
458
|
|
|
361
459
|
const Root = styled(Box)`
|
|
362
|
-
.invoice-summary {
|
|
363
|
-
padding-right: 16px;
|
|
364
|
-
}
|
|
365
460
|
@media (max-width: ${({ theme }) => theme.breakpoints.values.md}px) {
|
|
366
461
|
.MuiTable-root > .MuiTableBody-root > .MuiTableRow-root > td.MuiTableCell-root {
|
|
367
462
|
> div {
|
|
@@ -370,8 +465,5 @@ const Root = styled(Box)`
|
|
|
370
465
|
font-size: 14px;
|
|
371
466
|
}
|
|
372
467
|
}
|
|
373
|
-
.invoice-summary {
|
|
374
|
-
padding-right: 20px;
|
|
375
|
-
}
|
|
376
468
|
}
|
|
377
469
|
`;
|
|
@@ -4,6 +4,7 @@ export const colorDark = '#222';
|
|
|
4
4
|
export const colorDark2 = '#888';
|
|
5
5
|
export const colorGray = '#e3e3e3';
|
|
6
6
|
export const colorWhite = '#fff';
|
|
7
|
+
export const colorGreen = '#4caf50';
|
|
7
8
|
|
|
8
9
|
export const pdfStyles: InvoiceStyles = {
|
|
9
10
|
template: {
|
|
@@ -29,6 +30,9 @@ export const pdfStyles: InvoiceStyles = {
|
|
|
29
30
|
gray: {
|
|
30
31
|
color: colorDark2,
|
|
31
32
|
},
|
|
33
|
+
green: {
|
|
34
|
+
color: colorGreen,
|
|
35
|
+
},
|
|
32
36
|
'bg-dark': {
|
|
33
37
|
backgroundColor: colorDark2,
|
|
34
38
|
},
|
|
@@ -55,8 +59,10 @@ export const pdfStyles: InvoiceStyles = {
|
|
|
55
59
|
'w-60': { width: '60%' },
|
|
56
60
|
'w-40': { width: '40%' },
|
|
57
61
|
'w-48': { width: '48%' },
|
|
62
|
+
'w-38': { width: '38%' },
|
|
58
63
|
'w-17': { width: '17%' },
|
|
59
64
|
'w-18': { width: '18%' },
|
|
65
|
+
'w-15': { width: '15%' },
|
|
60
66
|
row: {
|
|
61
67
|
borderBottom: `1px solid ${colorGray}`,
|
|
62
68
|
marginBottom: '2px',
|