payment-kit 1.20.20 → 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.
Files changed (30) hide show
  1. package/api/src/integrations/stripe/resource.ts +1 -1
  2. package/api/src/libs/discount/coupon.ts +41 -73
  3. package/api/src/libs/invoice.ts +17 -0
  4. package/api/src/libs/notification/template/subscription-renew-failed.ts +22 -1
  5. package/api/src/libs/notification/template/subscription-will-renew.ts +22 -0
  6. package/api/src/locales/en.ts +1 -0
  7. package/api/src/locales/zh.ts +1 -0
  8. package/api/src/queues/checkout-session.ts +2 -2
  9. package/api/src/routes/checkout-sessions.ts +84 -0
  10. package/api/src/routes/connect/collect-batch.ts +2 -2
  11. package/api/src/routes/connect/pay.ts +1 -1
  12. package/api/src/store/migrations/20250926-change-customer-did-unique.ts +49 -0
  13. package/api/src/store/models/customer.ts +1 -0
  14. package/api/tests/libs/coupon.spec.ts +219 -0
  15. package/api/tests/libs/discount.spec.ts +250 -0
  16. package/blocklet.yml +1 -1
  17. package/package.json +7 -7
  18. package/src/components/discount/discount-info.tsx +0 -1
  19. package/src/components/invoice/action.tsx +26 -0
  20. package/src/components/invoice/table.tsx +2 -9
  21. package/src/components/invoice-pdf/styles.ts +2 -0
  22. package/src/components/invoice-pdf/template.tsx +44 -12
  23. package/src/components/metadata/list.tsx +1 -0
  24. package/src/components/subscription/metrics.tsx +7 -3
  25. package/src/locales/en.tsx +7 -0
  26. package/src/locales/zh.tsx +7 -0
  27. package/src/pages/admin/billing/subscriptions/detail.tsx +11 -3
  28. package/src/pages/admin/products/coupons/applicable-products.tsx +20 -37
  29. package/src/pages/customer/invoice/detail.tsx +1 -1
  30. 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(`${itemDiscountAmount > 0 ? 'green' : 'dark'} right`)}>
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 style={composeStyles('flex')}>
142
- <div style={composeStyles('w-60 mt-20')} />
143
- <div style={composeStyles('w-40 mt-20')}>
144
- {summary.map((line) => (
145
- <div style={composeStyles('flex')} key={line.key}>
146
- <div style={composeStyles('w-60 p-5')}>
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
- </div>
151
- <div style={composeStyles('w-40 p-5')}>
152
- <span style={composeStyles('right bold dark')}>
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
- </div>
189
+ );
190
+ })}
159
191
  </div>
160
192
  </div>
161
193
  );
@@ -22,6 +22,7 @@ export default function MetadataList({
22
22
  variant="subtitle1"
23
23
  sx={{
24
24
  color: 'text.primary',
25
+ fontWeight: 500,
25
26
  }}>
26
27
  {t('common.metadata.empty')}
27
28
  </Typography>
@@ -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
- if (isInsufficientBalance && !isCredit) {
85
+ const canRecharge = mode === 'portal';
86
+
87
+ if (isInsufficientBalance && !isCredit && canRecharge) {
84
88
  return (
85
89
  <Button
86
90
  component="a"
@@ -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',
@@ -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} showBalance={false} />
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 && <DiscountInfo discountStats={(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
- <ApplicableProductsTableRoot>
130
- <Table
131
- data={products}
132
- columns={columns}
133
- loading={false}
134
- footer={false}
135
- toolbar={false}
136
- components={{
137
- TableToolbar: () => null,
138
- TableFooter: () => null,
139
- }}
140
- mobileTDFlexDirection="row"
141
- options={{
142
- count: products.length,
143
- page: 0,
144
- rowsPerPage: 100,
145
- selectableRows: 'none',
146
- pagination: false,
147
- }}
148
- emptyNodeText={t('admin.coupon.noApplicableProducts')}
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
- `;
@@ -447,7 +447,7 @@ export default function CustomerInvoiceDetail() {
447
447
  sx={{
448
448
  mb: 1.5,
449
449
  }}>
450
- {t('payment.customer.products')}
450
+ {t('admin.invoiceItems')}
451
451
  </Typography>
452
452
  <InvoiceTable invoice={data} simple />
453
453
  </Box>
@@ -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 && <DiscountInfo discountStats={(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
- <Divider />
723
+ <Box className="divider" />
715
724
  {isCredit ? (
716
725
  <Box className="section">
717
726
  <Typography variant="h3" className="section-header">