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.
- package/api/src/{schedule → crons}/base.ts +1 -1
- package/api/src/index.ts +7 -7
- package/api/src/integrations/stripe/handlers/customer.ts +24 -0
- package/api/src/integrations/stripe/handlers/index.ts +4 -0
- package/api/src/integrations/stripe/handlers/payment-intent.ts +1 -1
- package/api/src/integrations/stripe/resource.ts +1 -1
- package/api/src/libs/audit.ts +34 -28
- package/api/src/libs/payment.ts +48 -4
- package/api/src/libs/queue/index.ts +18 -1
- package/api/src/libs/queue/store.ts +6 -5
- package/api/src/libs/session.ts +13 -12
- package/api/src/libs/subscription.ts +26 -0
- package/api/src/libs/util.ts +5 -1
- package/api/src/{jobs → queues}/checkout-session.ts +23 -1
- package/api/src/{jobs → queues}/invoice.ts +15 -6
- package/api/src/{jobs → queues}/payment.ts +182 -30
- package/api/src/{jobs → queues}/subscription.ts +36 -104
- package/api/src/{jobs → queues}/webhook.ts +2 -0
- package/api/src/routes/checkout-sessions.ts +72 -26
- package/api/src/routes/connect/collect.ts +2 -2
- package/api/src/routes/connect/pay.ts +1 -1
- package/api/src/routes/connect/setup.ts +10 -3
- package/api/src/routes/connect/shared.ts +98 -49
- package/api/src/routes/connect/subscribe.ts +10 -4
- package/api/src/routes/pricing-table.ts +2 -0
- package/api/src/routes/subscription-items.ts +1 -1
- package/api/src/routes/subscriptions.ts +434 -13
- package/api/src/store/migrate.ts +0 -1
- package/api/src/store/migrations/20231204-subupdate.ts +50 -0
- package/api/src/store/migrations/20231220-setup-intent.ts +22 -0
- package/api/src/store/models/checkout-session.ts +8 -0
- package/api/src/store/models/customer.ts +52 -15
- package/api/src/store/models/invoice-item.ts +6 -1
- package/api/src/store/models/invoice.ts +41 -22
- package/api/src/store/models/payment-intent.ts +4 -0
- package/api/src/store/models/setup-intent.ts +4 -0
- package/api/src/store/models/subscription-item.ts +0 -4
- package/api/src/store/models/subscription.ts +77 -44
- package/api/src/store/models/types.ts +1 -0
- package/api/src/store/sequelize.ts +6 -0
- package/api/third.d.ts +2 -0
- package/blocklet.yml +1 -1
- package/jest.config.js +14 -0
- package/package.json +24 -19
- package/src/components/blockchain/tx.tsx +20 -11
- package/src/components/checkout/form/index.tsx +1 -1
- package/src/components/invoice/table.tsx +58 -19
- package/src/components/layout/admin.tsx +17 -5
- package/src/components/portal/invoice/list.tsx +12 -8
- package/src/components/portal/subscription/list.tsx +114 -77
- package/src/components/subscription/status.tsx +21 -19
- package/src/global.css +4 -0
- package/src/locales/en.tsx +14 -1
- package/src/locales/zh.tsx +14 -0
- package/src/pages/admin/customers/customers/detail.tsx +47 -3
- package/src/pages/admin/overview.tsx +21 -1
- package/src/pages/admin/payments/intents/detail.tsx +12 -3
- package/src/pages/customer/invoice.tsx +15 -1
- package/src/pages/customer/subscription/index.tsx +9 -2
- package/tests/api/libs/subscription.spec.ts +45 -0
- /package/api/src/{schedule → crons}/index.ts +0 -0
- /package/api/src/{schedule → crons}/interface/diff.ts +0 -0
- /package/api/src/{schedule → crons}/subscription-trail-will-end.ts +0 -0
- /package/api/src/{schedule → crons}/subscription-will-renew.ts +0 -0
- /package/api/src/{jobs → queues}/event.ts +0 -0
- /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
|
-
|
|
127
|
-
|
|
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
|
-
|
|
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={
|
|
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 {
|
|
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
|
|
File without changes
|
|
File without changes
|