payment-kit 1.18.51 → 1.18.53

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.
@@ -12,6 +12,7 @@ import { createListParamSchema, getOrder, getWhereFromKvQuery, MetadataSchema }
12
12
  import { authenticate } from '../libs/security';
13
13
  import { expandLineItems } from '../libs/session';
14
14
  import { formatMetadata, getBlockletJson, getUserOrAppInfo } from '../libs/util';
15
+ import dayjs from '../libs/dayjs';
15
16
  import { Customer } from '../store/models/customer';
16
17
  import { Invoice } from '../store/models/invoice';
17
18
  import { InvoiceItem } from '../store/models/invoice-item';
@@ -671,4 +672,50 @@ router.put('/:id', authAdmin, async (req, res) => {
671
672
  }
672
673
  });
673
674
 
675
+ router.post('/:id/void', authAdmin, async (req, res) => {
676
+ const invoice = await Invoice.findByPk(req.params.id as string);
677
+ if (!invoice) {
678
+ return res.status(404).json({ error: 'Invoice not found' });
679
+ }
680
+ if (['paid', 'void', 'draft'].includes(invoice.status)) {
681
+ return res.status(400).json({ error: 'Can not void this invoice' });
682
+ }
683
+ const paymentMethod = await PaymentMethod.findByPk(invoice.default_payment_method_id);
684
+ if (!paymentMethod) {
685
+ return res.status(400).json({ error: 'Payment method not found' });
686
+ }
687
+ if (invoice.subscription_id) {
688
+ const subscription = await Subscription.findByPk(invoice.subscription_id);
689
+ if (subscription && !subscription.isImmutable()) {
690
+ return res.status(400).json({ error: 'Subscription is not immutable, can not void invoice' });
691
+ }
692
+ }
693
+ try {
694
+ if (invoice.payment_intent_id) {
695
+ const paymentIntent = await PaymentIntent.findByPk(invoice.payment_intent_id);
696
+ if (paymentIntent && paymentIntent.status !== 'canceled') {
697
+ await paymentIntent.update({
698
+ status: 'canceled',
699
+ canceled_at: dayjs().unix(),
700
+ cancellation_reason: 'void_invoice',
701
+ });
702
+ }
703
+ }
704
+ if (paymentMethod.type === 'stripe' && invoice.metadata?.stripe_id) {
705
+ const client = paymentMethod.getStripeClient();
706
+ await client.invoices.voidInvoice(invoice.metadata.stripe_id);
707
+ }
708
+ await invoice.update({
709
+ status: 'void',
710
+ status_transitions: {
711
+ ...(invoice.status_transitions || {}),
712
+ voided_at: dayjs().unix(),
713
+ },
714
+ });
715
+ return res.json(invoice);
716
+ } catch (error) {
717
+ logger.error('Failed to void invoice', { error, invoiceId: invoice.id });
718
+ return res.status(400).json({ error: 'Failed to void invoice' });
719
+ }
720
+ });
674
721
  export default router;
package/blocklet.yml CHANGED
@@ -14,7 +14,7 @@ repository:
14
14
  type: git
15
15
  url: git+https://github.com/blocklet/payment-kit.git
16
16
  specVersion: 1.2.8
17
- version: 1.18.51
17
+ version: 1.18.53
18
18
  logo: logo.png
19
19
  files:
20
20
  - dist
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payment-kit",
3
- "version": "1.18.51",
3
+ "version": "1.18.53",
4
4
  "scripts": {
5
5
  "dev": "blocklet dev --open",
6
6
  "eject": "vite eject",
@@ -44,31 +44,31 @@
44
44
  ]
45
45
  },
46
46
  "dependencies": {
47
- "@abtnode/cron": "^1.16.43",
48
- "@arcblock/did": "^1.20.12",
47
+ "@abtnode/cron": "^1.16.44",
48
+ "@arcblock/did": "^1.20.13",
49
49
  "@arcblock/did-auth-storage-nedb": "^1.7.1",
50
- "@arcblock/did-connect": "^2.13.61",
51
- "@arcblock/did-util": "^1.20.12",
52
- "@arcblock/jwt": "^1.20.12",
53
- "@arcblock/ux": "^2.13.61",
54
- "@arcblock/validator": "^1.20.12",
50
+ "@arcblock/did-connect": "^2.13.62",
51
+ "@arcblock/did-util": "^1.20.13",
52
+ "@arcblock/jwt": "^1.20.13",
53
+ "@arcblock/ux": "^2.13.62",
54
+ "@arcblock/validator": "^1.20.13",
55
55
  "@blocklet/did-space-js": "^1.0.57",
56
- "@blocklet/js-sdk": "^1.16.43",
57
- "@blocklet/logger": "^1.16.43",
58
- "@blocklet/payment-react": "1.18.51",
59
- "@blocklet/sdk": "^1.16.43",
60
- "@blocklet/ui-react": "^2.13.61",
56
+ "@blocklet/js-sdk": "^1.16.44",
57
+ "@blocklet/logger": "^1.16.44",
58
+ "@blocklet/payment-react": "1.18.53",
59
+ "@blocklet/sdk": "^1.16.44",
60
+ "@blocklet/ui-react": "^2.13.62",
61
61
  "@blocklet/uploader": "^0.1.95",
62
62
  "@blocklet/xss": "^0.1.36",
63
63
  "@mui/icons-material": "^5.16.6",
64
64
  "@mui/lab": "^5.0.0-alpha.173",
65
65
  "@mui/material": "^5.16.6",
66
66
  "@mui/system": "^5.16.6",
67
- "@ocap/asset": "^1.20.12",
68
- "@ocap/client": "^1.20.12",
69
- "@ocap/mcrypto": "^1.20.12",
70
- "@ocap/util": "^1.20.12",
71
- "@ocap/wallet": "^1.20.12",
67
+ "@ocap/asset": "^1.20.13",
68
+ "@ocap/client": "^1.20.13",
69
+ "@ocap/mcrypto": "^1.20.13",
70
+ "@ocap/util": "^1.20.13",
71
+ "@ocap/wallet": "^1.20.13",
72
72
  "@stripe/react-stripe-js": "^2.7.3",
73
73
  "@stripe/stripe-js": "^2.4.0",
74
74
  "ahooks": "^3.8.0",
@@ -121,9 +121,9 @@
121
121
  "web3": "^4.16.0"
122
122
  },
123
123
  "devDependencies": {
124
- "@abtnode/types": "^1.16.43",
124
+ "@abtnode/types": "^1.16.44",
125
125
  "@arcblock/eslint-config-ts": "^0.3.3",
126
- "@blocklet/payment-types": "1.18.51",
126
+ "@blocklet/payment-types": "1.18.53",
127
127
  "@types/cookie-parser": "^1.4.7",
128
128
  "@types/cors": "^2.8.17",
129
129
  "@types/debug": "^4.1.12",
@@ -153,7 +153,7 @@
153
153
  "vite": "^5.3.5",
154
154
  "vite-node": "^2.0.4",
155
155
  "vite-plugin-babel-import": "^2.0.5",
156
- "vite-plugin-blocklet": "^0.9.32",
156
+ "vite-plugin-blocklet": "^0.9.33",
157
157
  "vite-plugin-node-polyfills": "^0.21.0",
158
158
  "vite-plugin-svgr": "^4.2.0",
159
159
  "vite-tsconfig-paths": "^4.3.2",
@@ -169,5 +169,5 @@
169
169
  "parser": "typescript"
170
170
  }
171
171
  },
172
- "gitHead": "7aa2db4015aad46e2eb8529ff013791d5321c7bf"
172
+ "gitHead": "39ab3a392c2367ceff0441fa968c442074827d92"
173
173
  }
@@ -68,6 +68,10 @@ export default function InvoiceActions({ data, variant, onChange, mode }: Props)
68
68
  Toast.error(result.error);
69
69
  }
70
70
  }
71
+ if (state.action === 'void') {
72
+ await api.post(`/api/invoices/${data.id}/void`).then((res) => res.data);
73
+ Toast.success(t('admin.invoice.void.success'));
74
+ }
71
75
  onChange(state.action);
72
76
  } catch (err) {
73
77
  console.error(err);
@@ -99,6 +103,13 @@ export default function InvoiceActions({ data, variant, onChange, mode }: Props)
99
103
  color: 'primary',
100
104
  divider: true,
101
105
  },
106
+ isAdmin &&
107
+ !['paid', 'void', 'draft'].includes(data.status) && {
108
+ label: t('admin.invoice.void.title'),
109
+ handler: () => setState({ action: 'void' }),
110
+ color: 'primary',
111
+ divider: true,
112
+ },
102
113
  {
103
114
  label: t('admin.customer.view'),
104
115
  handler: () => {
@@ -138,6 +149,15 @@ export default function InvoiceActions({ data, variant, onChange, mode }: Props)
138
149
  loading={state.loading}
139
150
  />
140
151
  )}
152
+ {state.action === 'void' && (
153
+ <ConfirmDialog
154
+ onConfirm={handleAction}
155
+ onCancel={() => setState({ action: '' })}
156
+ title={t('admin.invoice.void.title')}
157
+ message={t('admin.invoice.void.tip')}
158
+ loading={state.loading}
159
+ />
160
+ )}
141
161
  </ClickBoundary>
142
162
  );
143
163
  }
@@ -174,7 +174,10 @@ export default function InvoiceList({
174
174
  const item = data.list[index] as TInvoiceExpanded;
175
175
  return (
176
176
  <InvoiceLink invoice={item}>
177
- <Typography component="strong" fontWeight={600}>
177
+ <Typography
178
+ component="strong"
179
+ fontWeight={600}
180
+ sx={{ textDecoration: item.status === 'void' ? 'line-through' : 'none' }}>
178
181
  {formatBNStr(item?.total, item?.paymentCurrency.decimal)}&nbsp;
179
182
  {item?.paymentCurrency.symbol}
180
183
  </Typography>
@@ -117,12 +117,10 @@ export default function PaymentList({ customer_id, invoice_id, features }: ListP
117
117
  options: {
118
118
  customBodyRenderLite: (_: string, index: number) => {
119
119
  const item = data.list[index] as TPaymentIntentExpanded;
120
+ const highlight = item.amount_received === '0' && item.status !== 'canceled';
120
121
  return (
121
122
  <Link to={`/admin/payments/${item.id}`}>
122
- <Typography
123
- component="strong"
124
- sx={{ color: item.amount_received === '0' ? 'warning.main' : 'inherit' }}
125
- fontWeight={600}>
123
+ <Typography component="strong" sx={{ color: highlight ? 'warning.main' : 'inherit' }} fontWeight={600}>
126
124
  {formatBNStr(
127
125
  item.amount_received === '0' ? item.amount : item.amount_received,
128
126
  item?.paymentCurrency.decimal
@@ -47,6 +47,7 @@ type ListProps = {
47
47
  status?: string;
48
48
  customer_id?: string;
49
49
  currency_id?: string;
50
+ setHasRevenues?: (hasRevenues: boolean) => void;
50
51
  };
51
52
 
52
53
  const getListKey = (props: ListProps) => {
@@ -60,9 +61,10 @@ CustomerRevenueList.defaultProps = {
60
61
  status: '',
61
62
  currency_id: '',
62
63
  customer_id: '',
64
+ setHasRevenues: () => {},
63
65
  };
64
66
 
65
- export default function CustomerRevenueList({ currency_id, status, customer_id }: ListProps) {
67
+ export default function CustomerRevenueList({ currency_id, status, customer_id, setHasRevenues }: ListProps) {
66
68
  const { t } = useLocaleContext();
67
69
  const { isMobile } = useMobile('sm');
68
70
 
@@ -87,6 +89,9 @@ export default function CustomerRevenueList({ currency_id, status, customer_id }
87
89
  debounce(() => {
88
90
  fetchData(search).then((res: any) => {
89
91
  setData(res);
92
+ if (setHasRevenues) {
93
+ setHasRevenues(res.count > 0);
94
+ }
90
95
  });
91
96
  }, 300)();
92
97
  }, [search]);
@@ -524,6 +524,11 @@ export default flat({
524
524
  tip: 'Are you sure you want to return the stake? This action will return the stake to the customer immediately.',
525
525
  success: 'Stake return application has been successfully created',
526
526
  },
527
+ void: {
528
+ title: 'Void Invoice',
529
+ tip: 'Are you sure you want to void this invoice? This action will immediately void the invoice.',
530
+ success: 'Invoice voided',
531
+ },
527
532
  },
528
533
  subscription: {
529
534
  view: 'View subscription',
@@ -513,6 +513,11 @@ export default flat({
513
513
  tip: '您确定要退还质押吗?此操作将立即退还质押给客户。',
514
514
  success: '质押退还申请已提交',
515
515
  },
516
+ void: {
517
+ title: '作废账单',
518
+ tip: '您确定要作废此账单吗?此操作将立即作废账单。',
519
+ success: '账单作废成功',
520
+ },
516
521
  },
517
522
  subscription: {
518
523
  view: '查看订阅',
@@ -197,6 +197,7 @@ export default function CustomerHome() {
197
197
  const navigate = useNavigate();
198
198
  const [subscriptionStatus, setSubscriptionStatus] = useState(false);
199
199
  const [hasSubscriptions, setHasSubscriptions] = useState(false);
200
+ const [hasRevenues, setHasRevenues] = useState(false);
200
201
  const { startTransition } = useTransitionContext();
201
202
  const {
202
203
  data,
@@ -433,7 +434,7 @@ export default function CustomerHome() {
433
434
  );
434
435
 
435
436
  const InvoiceCard = loadingCard ? (
436
- <CardSkeleton height={300} />
437
+ <CardSkeleton height={200} />
437
438
  ) : (
438
439
  <Box className="base-card section section-invoices">
439
440
  <Box className="section-header">
@@ -460,16 +461,15 @@ export default function CustomerHome() {
460
461
  </Box>
461
462
  );
462
463
 
463
- const RevenueCard = loadingCard ? (
464
- <CardSkeleton height={200} />
465
- ) : (
466
- <Box className="base-card section section-revenue">
467
- <Box className="section-header">
468
- <Typography variant="h3">{t('customer.payout.title')}</Typography>
464
+ const RevenueCard =
465
+ loadingCard || !hasRevenues ? null : (
466
+ <Box className="base-card section section-revenue">
467
+ <Box className="section-header">
468
+ <Typography variant="h3">{t('customer.payout.title')}</Typography>
469
+ </Box>
470
+ <CustomerRevenueList setHasRevenues={setHasRevenues} />
469
471
  </Box>
470
- <CustomerRevenueList />
471
- </Box>
472
- );
472
+ );
473
473
 
474
474
  return (
475
475
  <Content>
@@ -37,11 +37,11 @@ import {
37
37
  Typography,
38
38
  } from '@mui/material';
39
39
  import { useRequest, useSetState } from 'ahooks';
40
+ import SplitButton from '@arcblock/ux/lib/SplitButton';
40
41
  import { useMemo } from 'react';
41
42
  import { joinURL, withQuery } from 'ufo';
42
43
  import prettyMs from 'pretty-ms-i18n';
43
44
  import { isEmpty } from 'lodash';
44
- import { PaymentOutlined } from '@mui/icons-material';
45
45
  import InfoRow from '../../../components/info-row';
46
46
  import InfoCard from '../../../components/info-card';
47
47
  import { useSessionContext } from '../../../contexts/session';
@@ -82,7 +82,6 @@ export default function SubscriptionEmbed() {
82
82
  const [state, setState] = useSetState({
83
83
  batchPay: false,
84
84
  });
85
-
86
85
  const { session, connectApi } = useSessionContext();
87
86
 
88
87
  const subscriptionId = params.get('id') || '';
@@ -327,19 +326,30 @@ export default function SubscriptionEmbed() {
327
326
  {x.text[locale] || x.text.en || x.name}
328
327
  </Button>
329
328
  ))}
330
- <Button
331
- variant="contained"
332
- sx={{ width: subscription.service_actions?.length ? 'auto' : '100%' }}
333
- target="_blank"
334
- href={subscriptionPageUrl}>
335
- {t('payment.customer.subscriptions.view')}
336
- </Button>
337
- {hasPastDue && (
338
- <Tooltip title={t('admin.subscription.batchPay.button')}>
339
- <Button variant="outlined" color="error" onClick={() => setState({ batchPay: true })}>
340
- <PaymentOutlined />
341
- </Button>
342
- </Tooltip>
329
+ {hasPastDue ? (
330
+ <SplitButton
331
+ size="small"
332
+ color="error"
333
+ variant="contained"
334
+ menu={[
335
+ <SplitButton.Item key="view-subscription" component={Link} target="_blank" href={subscriptionPageUrl}>
336
+ {t('payment.customer.subscriptions.view')}
337
+ </SplitButton.Item>,
338
+ ]}>
339
+ {() => (
340
+ <Button variant="contained" color="error" onClick={() => setState({ batchPay: true })}>
341
+ {t('payment.subscription.overdue.payNow')}
342
+ </Button>
343
+ )}
344
+ </SplitButton>
345
+ ) : (
346
+ <Button
347
+ variant="contained"
348
+ sx={{ width: subscription.service_actions?.length ? 'auto' : '100%' }}
349
+ target="_blank"
350
+ href={subscriptionPageUrl}>
351
+ {t('payment.customer.subscriptions.view')}
352
+ </Button>
343
353
  )}
344
354
  </Stack>
345
355
  </Box>