payment-kit 1.13.72 → 1.13.74

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 (66) hide show
  1. package/api/src/{schedule → crons}/base.ts +1 -1
  2. package/api/src/index.ts +7 -7
  3. package/api/src/integrations/stripe/handlers/customer.ts +24 -0
  4. package/api/src/integrations/stripe/handlers/index.ts +4 -0
  5. package/api/src/integrations/stripe/handlers/payment-intent.ts +1 -1
  6. package/api/src/integrations/stripe/resource.ts +1 -1
  7. package/api/src/libs/audit.ts +34 -28
  8. package/api/src/libs/payment.ts +48 -4
  9. package/api/src/libs/queue/index.ts +18 -1
  10. package/api/src/libs/queue/store.ts +6 -5
  11. package/api/src/libs/session.ts +13 -12
  12. package/api/src/libs/subscription.ts +26 -0
  13. package/api/src/libs/util.ts +5 -1
  14. package/api/src/{jobs → queues}/checkout-session.ts +23 -1
  15. package/api/src/{jobs → queues}/invoice.ts +15 -6
  16. package/api/src/{jobs → queues}/payment.ts +182 -30
  17. package/api/src/{jobs → queues}/subscription.ts +36 -104
  18. package/api/src/{jobs → queues}/webhook.ts +2 -0
  19. package/api/src/routes/checkout-sessions.ts +72 -26
  20. package/api/src/routes/connect/collect.ts +2 -2
  21. package/api/src/routes/connect/pay.ts +1 -1
  22. package/api/src/routes/connect/setup.ts +10 -3
  23. package/api/src/routes/connect/shared.ts +98 -49
  24. package/api/src/routes/connect/subscribe.ts +10 -4
  25. package/api/src/routes/pricing-table.ts +2 -0
  26. package/api/src/routes/subscription-items.ts +1 -1
  27. package/api/src/routes/subscriptions.ts +434 -13
  28. package/api/src/store/migrate.ts +0 -1
  29. package/api/src/store/migrations/20231204-subupdate.ts +50 -0
  30. package/api/src/store/migrations/20231220-setup-intent.ts +22 -0
  31. package/api/src/store/models/checkout-session.ts +8 -0
  32. package/api/src/store/models/customer.ts +52 -15
  33. package/api/src/store/models/invoice-item.ts +6 -1
  34. package/api/src/store/models/invoice.ts +41 -22
  35. package/api/src/store/models/payment-intent.ts +4 -0
  36. package/api/src/store/models/setup-intent.ts +4 -0
  37. package/api/src/store/models/subscription-item.ts +0 -4
  38. package/api/src/store/models/subscription.ts +77 -44
  39. package/api/src/store/models/types.ts +1 -0
  40. package/api/src/store/sequelize.ts +6 -0
  41. package/api/third.d.ts +2 -0
  42. package/blocklet.yml +1 -1
  43. package/jest.config.js +14 -0
  44. package/package.json +24 -19
  45. package/src/components/blockchain/tx.tsx +20 -11
  46. package/src/components/checkout/form/index.tsx +1 -1
  47. package/src/components/invoice/table.tsx +58 -19
  48. package/src/components/layout/admin.tsx +17 -5
  49. package/src/components/portal/invoice/list.tsx +12 -8
  50. package/src/components/portal/subscription/list.tsx +114 -77
  51. package/src/components/subscription/status.tsx +21 -19
  52. package/src/global.css +4 -0
  53. package/src/locales/en.tsx +14 -1
  54. package/src/locales/zh.tsx +14 -0
  55. package/src/pages/admin/customers/customers/detail.tsx +47 -3
  56. package/src/pages/admin/overview.tsx +21 -1
  57. package/src/pages/admin/payments/intents/detail.tsx +12 -3
  58. package/src/pages/customer/invoice.tsx +15 -1
  59. package/src/pages/customer/subscription/index.tsx +9 -2
  60. package/tests/api/libs/subscription.spec.ts +45 -0
  61. /package/api/src/{schedule → crons}/index.ts +0 -0
  62. /package/api/src/{schedule → crons}/interface/diff.ts +0 -0
  63. /package/api/src/{schedule → crons}/subscription-trail-will-end.ts +0 -0
  64. /package/api/src/{schedule → crons}/subscription-will-renew.ts +0 -0
  65. /package/api/src/{jobs → queues}/event.ts +0 -0
  66. /package/api/src/{jobs → queues}/notification.ts +0 -0
@@ -2,10 +2,11 @@
2
2
  import DidAddress from '@arcblock/ux/lib/DID';
3
3
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
4
4
  import Toast from '@arcblock/ux/lib/Toast';
5
- import type { TCustomerExpanded } from '@did-pay/types';
5
+ import type { TCustomerExpanded, TPaymentMethodExpanded } from '@did-pay/types';
6
6
  import { ArrowBackOutlined, Edit } from '@mui/icons-material';
7
7
  import { Alert, Box, Button, CircularProgress, Stack, Typography } from '@mui/material';
8
8
  import { styled } from '@mui/system';
9
+ import { fromUnitToToken } from '@ocap/util';
9
10
  import { useRequest, useSetState } from 'ahooks';
10
11
  import { isEmpty } from 'lodash';
11
12
  import { FlagEmoji } from 'react-international-phone';
@@ -22,6 +23,7 @@ import MetadataEditor from '../../../../components/metadata/editor';
22
23
  import PaymentList from '../../../../components/payment-intent/list';
23
24
  import SectionHeader from '../../../../components/section/header';
24
25
  import SubscriptionList from '../../../../components/subscription/list';
26
+ import { useSettingsContext } from '../../../../contexts/settings';
25
27
  import api from '../../../../libs/api';
26
28
  import { formatError, formatTime } from '../../../../libs/util';
27
29
 
@@ -29,9 +31,43 @@ const fetchData = (id: string): Promise<TCustomerExpanded> => {
29
31
  return api.get(`/api/customers/${id}`).then((res) => res.data);
30
32
  };
31
33
 
34
+ function getTokenBalances(customer: TCustomerExpanded, paymentMethods: TPaymentMethodExpanded[]) {
35
+ const tokens: { currency: string; balance: string }[] = [];
36
+ // @ts-ignore
37
+ const currencies = paymentMethods.reduce((acc, x) => acc.concat(x.payment_currencies), []);
38
+
39
+ if (customer.balance) {
40
+ const currency = currencies.find((x: any) => x.symbol === 'USD');
41
+ tokens.push({
42
+ // @ts-ignore
43
+ currency: currency.symbol,
44
+ // @ts-ignore
45
+ balance: fromUnitToToken(customer.balance, currency.decimal),
46
+ });
47
+ }
48
+
49
+ if (customer.token_balance) {
50
+ Object.keys(customer.token_balance).forEach((currencyId) => {
51
+ const currency = currencies.find((x: any) => x.id === currencyId);
52
+ // @ts-ignore
53
+ if (currency && customer.token_balance[currencyId] !== '0') {
54
+ tokens.push({
55
+ // @ts-ignore
56
+ currency: currency.symbol,
57
+ // @ts-ignore
58
+ balance: fromUnitToToken(customer.token_balance[currencyId], currency.decimal),
59
+ });
60
+ }
61
+ });
62
+ }
63
+
64
+ return tokens;
65
+ }
66
+
32
67
  export default function CustomerDetail(props: { id: string }) {
33
68
  const { t } = useLocaleContext();
34
69
  const navigate = useNavigate();
70
+ const { settings } = useSettingsContext();
35
71
  const [state, setState] = useSetState({
36
72
  adding: {
37
73
  price: false,
@@ -92,6 +128,8 @@ export default function CustomerDetail(props: { id: string }) {
92
128
  }
93
129
  };
94
130
 
131
+ const tokenBalances = getTokenBalances(data, settings.paymentMethods);
132
+
95
133
  return (
96
134
  <Root direction="column" spacing={4} sx={{ mb: 4 }}>
97
135
  <Box>
@@ -123,8 +161,14 @@ export default function CustomerDetail(props: { id: string }) {
123
161
  <InfoMetric label={t('common.createdAt')} value={formatTime(data.created_at)} divider />
124
162
  <InfoMetric label={t('common.updatedAt')} value={formatTime(data.updated_at)} divider />
125
163
  <InfoMetric label={t('admin.customer.spent')} value={0} divider />
126
- <InfoMetric label={t('admin.customer.refund')} value={0} divider />
127
- <InfoMetric label={t('admin.customer.dispute')} value={0} divider />
164
+ {tokenBalances.map((x) => (
165
+ <InfoMetric
166
+ key={x.currency}
167
+ label={t('admin.customer.balance', { currency: x.currency })}
168
+ value={x.balance}
169
+ divider
170
+ />
171
+ ))}
128
172
  </Stack>
129
173
  </Box>
130
174
  </Box>
@@ -1,3 +1,23 @@
1
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
+ import { Button, Stack } from '@mui/material';
3
+ import { Link } from 'react-router-dom';
4
+
1
5
  export default function Overview() {
2
- return <div>Overview</div>;
6
+ const { t } = useLocaleContext();
7
+ return (
8
+ <Stack direction="row" spacing={2}>
9
+ <Button component={Link} variant="outlined" to="/admin/products">
10
+ {t('admin.products')}
11
+ </Button>
12
+ <Button component={Link} variant="outlined" to="/admin/payments">
13
+ {t('admin.payments')}
14
+ </Button>
15
+ <Button component={Link} variant="outlined" to="/admin/customers">
16
+ {t('admin.customers')}
17
+ </Button>
18
+ <Button component={Link} variant="outlined" to="/admin/billing/subscriptions">
19
+ {t('admin.subscriptions')}
20
+ </Button>
21
+ </Stack>
22
+ );
3
23
  }
@@ -2,8 +2,8 @@
2
2
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
3
  import Toast from '@arcblock/ux/lib/Toast';
4
4
  import type { TPaymentIntentExpanded } from '@did-pay/types';
5
- import { ArrowBackOutlined, Edit } from '@mui/icons-material';
6
- import { Alert, Box, Button, CircularProgress, Stack, Typography } from '@mui/material';
5
+ import { ArrowBackOutlined, Edit, InfoOutlined } from '@mui/icons-material';
6
+ import { Alert, Box, Button, CircularProgress, Stack, Tooltip, Typography } from '@mui/material';
7
7
  import { styled } from '@mui/system';
8
8
  import { fromUnitToToken } from '@ocap/util';
9
9
  import { useRequest, useSetState } from 'ahooks';
@@ -118,7 +118,16 @@ export default function PaymentIntentDetail(props: { id: string }) {
118
118
  <InfoRow label={t('common.amount')} value={amount} />
119
119
  <InfoRow
120
120
  label={t('common.status')}
121
- value={<Status label={data.status} color={getPaymentIntentStatusColor(data.status)} />}
121
+ value={
122
+ <Stack direction="row" alignItems="center" spacing={1}>
123
+ <Status label={data.status} color={getPaymentIntentStatusColor(data.status)} />
124
+ {data.last_payment_error && (
125
+ <Tooltip title={<pre>{JSON.stringify(data.last_payment_error, null, 2)}</pre>}>
126
+ <InfoOutlined fontSize="small" color="error" />
127
+ </Tooltip>
128
+ )}
129
+ </Stack>
130
+ }
122
131
  />
123
132
  <InfoRow label={t('common.description')} value={data.description} />
124
133
  <InfoRow label={t('common.statementDescriptor')} value={data.statement_descriptor} />
@@ -5,7 +5,8 @@ import type { TInvoiceExpanded } from '@did-pay/types';
5
5
  import { ArrowBackOutlined } from '@mui/icons-material';
6
6
  import { Alert, Box, Button, CircularProgress, Grid, Stack, Typography } from '@mui/material';
7
7
  import { useRequest, useSetState } from 'ahooks';
8
- import { Link, useParams } from 'react-router-dom';
8
+ import { useEffect } from 'react';
9
+ import { Link, useParams, useSearchParams } from 'react-router-dom';
9
10
 
10
11
  import TxLink from '../../components/blockchain/tx';
11
12
  import Currency from '../../components/currency';
@@ -24,6 +25,7 @@ const fetchData = (id: string): Promise<TInvoiceExpanded> => {
24
25
  // TODO: download feature using: https://react-pdf.org/
25
26
  export default function CustomerHome() {
26
27
  const { t } = useLocaleContext();
28
+ const [searchParams] = useSearchParams();
27
29
  const { connectApi } = useSessionContext();
28
30
  const params = useParams<{ id: string }>();
29
31
  const [state, setState] = useSetState({
@@ -39,6 +41,11 @@ export default function CustomerHome() {
39
41
  connectApi.open({
40
42
  action: 'collect',
41
43
  timeout: 5 * 60 * 1000,
44
+ messages: {
45
+ title: t('customer.invoice.pay'),
46
+ success: t('customer.invoice.paySuccess'),
47
+ error: t('customer.invoice.payError'),
48
+ },
42
49
  extraParams: { invoiceId: params.id },
43
50
  onSuccess: async () => {
44
51
  connectApi.close();
@@ -60,6 +67,13 @@ export default function CustomerHome() {
60
67
  }
61
68
  };
62
69
 
70
+ useEffect(() => {
71
+ if (searchParams.get('action') === 'pay') {
72
+ onPay();
73
+ }
74
+ // eslint-disable-next-line react-hooks/exhaustive-deps
75
+ }, []);
76
+
63
77
  if (error) {
64
78
  return <Alert severity="error">{formatError(error)}</Alert>;
65
79
  }
@@ -70,20 +70,27 @@ export default function CustomerSubscription() {
70
70
  value={formatTime(data.start_date ? data.start_date * 1000 : data.created_at)}
71
71
  divider
72
72
  />
73
- {!data.cancel_at && (
73
+ {data.status === 'active' && !data.cancel_at && (
74
74
  <InfoMetric
75
75
  label={t('admin.subscription.nextInvoice')}
76
76
  value={formatTime(data.current_period_end * 1000)}
77
77
  divider
78
78
  />
79
79
  )}
80
- {data.cancel_at && (
80
+ {['active', 'trailing'].includes(data.status) && data.cancel_at && (
81
81
  <InfoMetric
82
82
  label={t('admin.subscription.cancel.schedule')}
83
83
  value={formatTime(data.cancel_at * 1000)}
84
84
  divider
85
85
  />
86
86
  )}
87
+ {data.status === 'canceled' && data.canceled_at && (
88
+ <InfoMetric
89
+ label={t('admin.subscription.cancel.done')}
90
+ value={formatTime(data.canceled_at * 1000)}
91
+ divider
92
+ />
93
+ )}
87
94
  </Stack>
88
95
  </Box>
89
96
  </Box>
@@ -0,0 +1,45 @@
1
+ import { getDaysUntilDue, getDueUnit } from '../../../api/src/libs/subscription';
2
+
3
+ describe('getDueUnit', () => {
4
+ it('should return 60 for recurring interval of "hour"', () => {
5
+ const result = getDueUnit('hour');
6
+ expect(result).toBe(60);
7
+ });
8
+
9
+ it('should return 3600 for recurring interval of "day"', () => {
10
+ const result = getDueUnit('day');
11
+ expect(result).toBe(3600);
12
+ });
13
+
14
+ it('should return 86400 for recurring interval other than "hour" or "day"', () => {
15
+ expect(getDueUnit('week')).toBe(86400);
16
+ expect(getDueUnit('month')).toBe(86400);
17
+ expect(getDueUnit('year')).toBe(86400);
18
+ });
19
+ });
20
+
21
+ describe('getDaysUntilDue', () => {
22
+ it('should return the number of days from the query when days_until_due is present', () => {
23
+ const query = { days_until_due: '5' };
24
+ const result = getDaysUntilDue(query);
25
+ expect(result).toBe(5);
26
+ });
27
+
28
+ it('should return the number of days from the environment variable when days_until_due is not present in the query', () => {
29
+ process.env.PAYMENT_DAYS_UNTIL_DUE = '10';
30
+ const result = getDaysUntilDue({});
31
+ expect(result).toBe(10);
32
+ delete process.env.PAYMENT_DAYS_UNTIL_DUE;
33
+ });
34
+
35
+ it('should return null when days_until_due is not present in the query and the environment variable', () => {
36
+ const result = getDaysUntilDue({});
37
+ expect(result).toBe(null);
38
+ });
39
+
40
+ it('should return null when days_until_due is not a number', () => {
41
+ const query = { days_until_due: 'not a number' };
42
+ const result = getDaysUntilDue(query);
43
+ expect(result).toBe(null);
44
+ });
45
+ });
File without changes
File without changes
File without changes
File without changes