payment-kit 1.20.19 → 1.20.21
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/integrations/stripe/resource.ts +1 -1
- package/api/src/libs/discount/coupon.ts +41 -73
- package/api/src/libs/invoice.ts +17 -0
- package/api/src/libs/notification/template/subscription-renew-failed.ts +22 -1
- package/api/src/libs/notification/template/subscription-will-renew.ts +22 -0
- package/api/src/locales/en.ts +1 -0
- package/api/src/locales/zh.ts +1 -0
- package/api/src/queues/checkout-session.ts +2 -2
- package/api/src/routes/checkout-sessions.ts +84 -0
- package/api/src/routes/connect/collect-batch.ts +2 -2
- package/api/src/routes/connect/pay.ts +1 -1
- package/api/src/routes/vendor.ts +3 -1
- package/api/src/store/migrations/20250926-change-customer-did-unique.ts +49 -0
- package/api/src/store/models/customer.ts +1 -0
- package/api/tests/libs/coupon.spec.ts +219 -0
- package/api/tests/libs/discount.spec.ts +250 -0
- package/blocklet.yml +1 -1
- package/package.json +7 -7
- package/src/components/discount/discount-info.tsx +0 -1
- package/src/components/invoice/action.tsx +26 -0
- package/src/components/invoice/table.tsx +2 -9
- package/src/components/invoice-pdf/styles.ts +2 -0
- package/src/components/invoice-pdf/template.tsx +44 -12
- package/src/components/metadata/list.tsx +1 -0
- package/src/components/subscription/metrics.tsx +7 -3
- package/src/locales/en.tsx +7 -0
- package/src/locales/zh.tsx +7 -0
- package/src/pages/admin/billing/subscriptions/detail.tsx +11 -3
- package/src/pages/admin/products/coupons/applicable-products.tsx +20 -37
- package/src/pages/customer/invoice/detail.tsx +1 -1
- package/src/pages/customer/subscription/detail.tsx +12 -3
|
@@ -120,7 +120,7 @@ export function InvoiceTemplate({ data, t }: InvoicePDFProps) {
|
|
|
120
120
|
</span>
|
|
121
121
|
</div>
|
|
122
122
|
<div style={composeStyles('w-15 p-4-8 pb-15')}>
|
|
123
|
-
<span style={composeStyles(
|
|
123
|
+
<span style={composeStyles('dark right')}>
|
|
124
124
|
{itemDiscountAmount > 0
|
|
125
125
|
? `-${formatAmount(itemDiscountAmount.toString(), data.paymentCurrency.decimal)} ${
|
|
126
126
|
data.paymentCurrency.symbol
|
|
@@ -138,24 +138,56 @@ export function InvoiceTemplate({ data, t }: InvoicePDFProps) {
|
|
|
138
138
|
})}
|
|
139
139
|
|
|
140
140
|
{/* Summary */}
|
|
141
|
-
<div
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
141
|
+
<div
|
|
142
|
+
style={{
|
|
143
|
+
display: 'flex',
|
|
144
|
+
flexDirection: 'column',
|
|
145
|
+
alignItems: 'flex-end',
|
|
146
|
+
float: 'right',
|
|
147
|
+
paddingRight: '8px',
|
|
148
|
+
marginTop: '8px',
|
|
149
|
+
width: '100%',
|
|
150
|
+
minWidth: '280px',
|
|
151
|
+
}}>
|
|
152
|
+
{summary.map((line) => {
|
|
153
|
+
const isTotal = line.key === 'Total' || line.key === 'common.total' || line.key === 'payment.total';
|
|
154
|
+
const showDivider = isTotal;
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
<div key={line.key} style={{ width: '100%' }}>
|
|
158
|
+
{showDivider && (
|
|
159
|
+
<div
|
|
160
|
+
style={{
|
|
161
|
+
borderTop: '1px solid #e3e3e3',
|
|
162
|
+
width: '100%',
|
|
163
|
+
marginBottom: '8px',
|
|
164
|
+
marginTop: '4px',
|
|
165
|
+
}}
|
|
166
|
+
/>
|
|
167
|
+
)}
|
|
168
|
+
<div
|
|
169
|
+
style={{
|
|
170
|
+
display: 'flex',
|
|
171
|
+
justifyContent: 'flex-end',
|
|
172
|
+
width: '100%',
|
|
173
|
+
padding: '4px 0',
|
|
174
|
+
paddingRight: '8%',
|
|
175
|
+
gap: '20px',
|
|
176
|
+
}}>
|
|
147
177
|
<span style={composeStyles('bold')}>
|
|
148
178
|
{line.key.startsWith('common.') || line.key.startsWith('payment.') ? t(line.key) : line.key}
|
|
149
179
|
</span>
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
180
|
+
<span
|
|
181
|
+
style={{
|
|
182
|
+
...composeStyles('bold dark text-right'),
|
|
183
|
+
minWidth: '80px',
|
|
184
|
+
}}>
|
|
153
185
|
{line.value} {data.paymentCurrency.symbol}
|
|
154
186
|
</span>
|
|
155
187
|
</div>
|
|
156
188
|
</div>
|
|
157
|
-
)
|
|
158
|
-
|
|
189
|
+
);
|
|
190
|
+
})}
|
|
159
191
|
</div>
|
|
160
192
|
</div>
|
|
161
193
|
);
|
|
@@ -13,6 +13,7 @@ import SubscriptionStatus from './status';
|
|
|
13
13
|
type Props = {
|
|
14
14
|
subscription: TSubscriptionExpanded;
|
|
15
15
|
showBalance?: boolean;
|
|
16
|
+
mode?: 'portal' | 'admin';
|
|
16
17
|
};
|
|
17
18
|
|
|
18
19
|
const fetchUpcoming = (id: string): Promise<{ amount: string }> => {
|
|
@@ -35,7 +36,7 @@ const fetchCreditBalance = ({
|
|
|
35
36
|
.then((res) => res.data);
|
|
36
37
|
};
|
|
37
38
|
|
|
38
|
-
export default function SubscriptionMetrics({ subscription, showBalance = true }: Props) {
|
|
39
|
+
export default function SubscriptionMetrics({ subscription, showBalance = true, mode = 'portal' }: Props) {
|
|
39
40
|
const { t } = useLocaleContext();
|
|
40
41
|
const isCredit = subscription.paymentCurrency?.type === 'credit';
|
|
41
42
|
const { data: upcoming, loading: upcomingLoading } = useRequest(() => fetchUpcoming(subscription.id));
|
|
@@ -59,6 +60,7 @@ export default function SubscriptionMetrics({ subscription, showBalance = true }
|
|
|
59
60
|
);
|
|
60
61
|
|
|
61
62
|
const supportShowBalance = showBalance && ['arcblock', 'ethereum', 'base'].includes(subscription.paymentMethod.type);
|
|
63
|
+
|
|
62
64
|
// let scheduleToCancelTime = 0;
|
|
63
65
|
// if (['active', 'trialing', 'past_due'].includes(subscription.status) && subscription.cancel_at) {
|
|
64
66
|
// scheduleToCancelTime = subscription.cancel_at * 1000;
|
|
@@ -69,7 +71,7 @@ export default function SubscriptionMetrics({ subscription, showBalance = true }
|
|
|
69
71
|
const isInsufficientBalance = new BN(payerValue?.token || '0').lt(new BN(upcoming?.amount || '0'));
|
|
70
72
|
|
|
71
73
|
const handleRecharge = () => {
|
|
72
|
-
if (isCredit) {
|
|
74
|
+
if (isCredit || mode !== 'portal') {
|
|
73
75
|
return;
|
|
74
76
|
}
|
|
75
77
|
navigate(`/customer/subscription/${subscription.id}/recharge`);
|
|
@@ -80,7 +82,9 @@ export default function SubscriptionMetrics({ subscription, showBalance = true }
|
|
|
80
82
|
return <CircularProgress size={16} />;
|
|
81
83
|
}
|
|
82
84
|
|
|
83
|
-
|
|
85
|
+
const canRecharge = mode === 'portal';
|
|
86
|
+
|
|
87
|
+
if (isInsufficientBalance && !isCredit && canRecharge) {
|
|
84
88
|
return (
|
|
85
89
|
<Button
|
|
86
90
|
component="a"
|
package/src/locales/en.tsx
CHANGED
|
@@ -132,6 +132,7 @@ export default flat({
|
|
|
132
132
|
paymentMethods: 'Payment methods',
|
|
133
133
|
customers: 'Customers',
|
|
134
134
|
products: 'Products',
|
|
135
|
+
invoiceItems: 'Invoice Items',
|
|
135
136
|
pricing: 'Pricing',
|
|
136
137
|
coupons: 'Coupons',
|
|
137
138
|
pricingTables: 'Pricing tables',
|
|
@@ -1086,6 +1087,11 @@ export default flat({
|
|
|
1086
1087
|
download: 'Download PDF',
|
|
1087
1088
|
edit: 'Edit Invoice',
|
|
1088
1089
|
duplicate: 'Duplicate Invoice',
|
|
1090
|
+
retryUncollectible: {
|
|
1091
|
+
title: 'Retry collection',
|
|
1092
|
+
tip: 'Are you sure you want to retry collecting this invoice? This will attempt to charge the customer again.',
|
|
1093
|
+
success: 'Retry request submitted',
|
|
1094
|
+
},
|
|
1089
1095
|
returnStake: {
|
|
1090
1096
|
title: 'Return Stake',
|
|
1091
1097
|
tip: 'Are you sure you want to return the stake? This action will return the stake to the customer immediately.',
|
|
@@ -1546,6 +1552,7 @@ export default flat({
|
|
|
1546
1552
|
subscriptions: 'No Subscriptions',
|
|
1547
1553
|
customers: 'No Customers',
|
|
1548
1554
|
products: 'No Products',
|
|
1555
|
+
invoiceItems: 'No Invoice Items',
|
|
1549
1556
|
payouts: 'No Payouts',
|
|
1550
1557
|
paymentLinks: 'No Payment Links',
|
|
1551
1558
|
paymentMethods: 'No Payment Methods',
|
package/src/locales/zh.tsx
CHANGED
|
@@ -131,6 +131,7 @@ export default flat({
|
|
|
131
131
|
paymentMethods: '支付方式',
|
|
132
132
|
customers: '客户管理',
|
|
133
133
|
products: '产品定价',
|
|
134
|
+
invoiceItems: '账单明细',
|
|
134
135
|
coupons: '优惠券',
|
|
135
136
|
pricing: '定价',
|
|
136
137
|
pricingTables: '定价表',
|
|
@@ -1057,6 +1058,11 @@ export default flat({
|
|
|
1057
1058
|
download: '下载PDF',
|
|
1058
1059
|
edit: '编辑账单',
|
|
1059
1060
|
duplicate: '复制账单',
|
|
1061
|
+
retryUncollectible: {
|
|
1062
|
+
title: '重新收款',
|
|
1063
|
+
tip: '确定要重新尝试收取该笔账单吗?系统将再次尝试向客户发起扣款。',
|
|
1064
|
+
success: '重新收款请求已提交',
|
|
1065
|
+
},
|
|
1060
1066
|
attention: '未完成的账单',
|
|
1061
1067
|
returnStake: {
|
|
1062
1068
|
title: '退还质押',
|
|
@@ -1496,6 +1502,7 @@ export default flat({
|
|
|
1496
1502
|
image: '无图片',
|
|
1497
1503
|
refunds: '没有退款记录',
|
|
1498
1504
|
invoices: '没有账单',
|
|
1505
|
+
invoiceItems: '没有账单明细',
|
|
1499
1506
|
subscriptions: '没有订阅记录',
|
|
1500
1507
|
customers: '没有客户',
|
|
1501
1508
|
products: '没有产品',
|
|
@@ -178,7 +178,7 @@ export default function SubscriptionDetail(props: { id: string }) {
|
|
|
178
178
|
md: 3,
|
|
179
179
|
},
|
|
180
180
|
}}>
|
|
181
|
-
<SubscriptionMetrics subscription={data}
|
|
181
|
+
<SubscriptionMetrics subscription={data} mode="admin" />
|
|
182
182
|
</Stack>
|
|
183
183
|
</Box>
|
|
184
184
|
<Divider />
|
|
@@ -273,7 +273,6 @@ export default function SubscriptionDetail(props: { id: string }) {
|
|
|
273
273
|
<InfoRow label={t('common.resumesAt')} value={formatTime(data.pause_collection.resumes_at * 1000)} />
|
|
274
274
|
)}
|
|
275
275
|
<InfoRow label={t('admin.subscription.collectionMethod')} value={data.collection_method} />
|
|
276
|
-
<InfoRow label={t('admin.subscription.discount')} value={data.discount_id ? data.discount_id : ''} />
|
|
277
276
|
<InfoRow
|
|
278
277
|
label={t('admin.paymentCurrency.name')}
|
|
279
278
|
value={
|
|
@@ -316,7 +315,16 @@ export default function SubscriptionDetail(props: { id: string }) {
|
|
|
316
315
|
<Divider />
|
|
317
316
|
|
|
318
317
|
{/* Discount Information */}
|
|
319
|
-
{(data as any).discountStats &&
|
|
318
|
+
{(data as any).discountStats && (
|
|
319
|
+
<Box className="section">
|
|
320
|
+
<Typography variant="h3" className="section-header" sx={{ mb: 1.5 }}>
|
|
321
|
+
{t('admin.subscription.discount')}
|
|
322
|
+
</Typography>
|
|
323
|
+
<Box className="section-body">
|
|
324
|
+
<DiscountInfo discountStats={(data as any).discountStats} />
|
|
325
|
+
</Box>
|
|
326
|
+
</Box>
|
|
327
|
+
)}
|
|
320
328
|
|
|
321
329
|
<Box className="section">
|
|
322
330
|
<SectionHeader title={t('admin.product.pricing')} />
|
|
@@ -11,7 +11,6 @@ import {
|
|
|
11
11
|
} from '@blocklet/payment-react';
|
|
12
12
|
import type { TProductExpanded } from '@blocklet/payment-types';
|
|
13
13
|
import { Avatar, Stack, Typography, Box } from '@mui/material';
|
|
14
|
-
import { styled } from '@mui/system';
|
|
15
14
|
import { Link } from 'react-router-dom';
|
|
16
15
|
|
|
17
16
|
interface Props {
|
|
@@ -126,41 +125,25 @@ export default function ApplicableProductsList({ products }: Props) {
|
|
|
126
125
|
].filter(Boolean);
|
|
127
126
|
|
|
128
127
|
return (
|
|
129
|
-
<
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
/>
|
|
150
|
-
</ApplicableProductsTableRoot>
|
|
128
|
+
<Table
|
|
129
|
+
data={products}
|
|
130
|
+
columns={columns}
|
|
131
|
+
loading={false}
|
|
132
|
+
footer={false}
|
|
133
|
+
toolbar={false}
|
|
134
|
+
components={{
|
|
135
|
+
TableToolbar: () => null,
|
|
136
|
+
TableFooter: () => null,
|
|
137
|
+
}}
|
|
138
|
+
mobileTDFlexDirection="row"
|
|
139
|
+
options={{
|
|
140
|
+
count: products.length,
|
|
141
|
+
page: 0,
|
|
142
|
+
rowsPerPage: 100,
|
|
143
|
+
selectableRows: 'none',
|
|
144
|
+
pagination: false,
|
|
145
|
+
}}
|
|
146
|
+
emptyNodeText={t('admin.coupon.noApplicableProducts')}
|
|
147
|
+
/>
|
|
151
148
|
);
|
|
152
149
|
}
|
|
153
|
-
|
|
154
|
-
const ApplicableProductsTableRoot = styled(Box)`
|
|
155
|
-
@media (max-width: ${({ theme }) => theme.breakpoints.values.md}px) {
|
|
156
|
-
.MuiTable-root > .MuiTableBody-root > .MuiTableRow-root > td.MuiTableCell-root {
|
|
157
|
-
align-items: center;
|
|
158
|
-
padding: 4px 0;
|
|
159
|
-
> div {
|
|
160
|
-
width: fit-content;
|
|
161
|
-
flex: inherit;
|
|
162
|
-
font-size: 14px;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
`;
|
|
@@ -609,7 +609,6 @@ export default function CustomerSubscriptionDetail() {
|
|
|
609
609
|
)}
|
|
610
610
|
|
|
611
611
|
<InfoRow label={t('admin.subscription.collectionMethod')} value={data.collection_method} />
|
|
612
|
-
<InfoRow label={t('admin.subscription.discount')} value={data.discount_id ? data.discount_id : ''} />
|
|
613
612
|
|
|
614
613
|
<InfoRow
|
|
615
614
|
label={t('admin.paymentMethod._name')}
|
|
@@ -682,7 +681,17 @@ export default function CustomerSubscriptionDetail() {
|
|
|
682
681
|
<Divider />
|
|
683
682
|
|
|
684
683
|
{/* Discount Information */}
|
|
685
|
-
{(data as any).discountStats &&
|
|
684
|
+
{(data as any).discountStats && (
|
|
685
|
+
<Box className="section">
|
|
686
|
+
<Typography variant="h3" className="section-header" sx={{ mb: 1.5 }}>
|
|
687
|
+
{t('admin.subscription.discount')}
|
|
688
|
+
</Typography>
|
|
689
|
+
<Box className="section-body">
|
|
690
|
+
<DiscountInfo discountStats={(data as any).discountStats} />
|
|
691
|
+
</Box>
|
|
692
|
+
</Box>
|
|
693
|
+
)}
|
|
694
|
+
<Box className="divider" />
|
|
686
695
|
|
|
687
696
|
<Box className="section">
|
|
688
697
|
<Typography
|
|
@@ -711,7 +720,7 @@ export default function CustomerSubscriptionDetail() {
|
|
|
711
720
|
</>
|
|
712
721
|
);
|
|
713
722
|
})()}
|
|
714
|
-
<
|
|
723
|
+
<Box className="divider" />
|
|
715
724
|
{isCredit ? (
|
|
716
725
|
<Box className="section">
|
|
717
726
|
<Typography variant="h3" className="section-header">
|