payment-kit 1.13.240 → 1.13.242
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/index.ts +3 -0
- package/api/src/libs/ws.ts +25 -0
- package/api/src/queues/usage-record.ts +11 -2
- package/api/src/routes/usage-records.ts +8 -1
- package/api/src/store/models/usage-record.ts +2 -2
- package/blocklet.yml +1 -1
- package/package.json +5 -5
- package/src/components/invoice/table.tsx +103 -62
- package/src/components/subscription/items/usage-records.tsx +91 -31
- package/src/components/subscription/portal/list.tsx +4 -3
- package/src/locales/en.tsx +4 -0
- package/src/locales/zh.tsx +4 -0
- package/src/pages/admin/billing/invoices/detail.tsx +1 -0
- package/src/pages/customer/index.tsx +24 -3
- package/src/pages/customer/invoice/detail.tsx +29 -31
package/api/src/index.ts
CHANGED
|
@@ -16,6 +16,7 @@ import { ensureWebhookRegistered } from './integrations/stripe/setup';
|
|
|
16
16
|
import { handlers } from './libs/auth';
|
|
17
17
|
import logger, { accessLogStream } from './libs/logger';
|
|
18
18
|
import { ensureI18n } from './libs/middleware';
|
|
19
|
+
import { initEventBroadcast } from './libs/ws';
|
|
19
20
|
import { startCheckoutSessionQueue } from './queues/checkout-session';
|
|
20
21
|
import { startEventQueue } from './queues/event';
|
|
21
22
|
import { startInvoiceQueue } from './queues/invoice';
|
|
@@ -123,5 +124,7 @@ export const server = app.listen(port, (err?: any) => {
|
|
|
123
124
|
|
|
124
125
|
crons.init();
|
|
125
126
|
|
|
127
|
+
initEventBroadcast();
|
|
128
|
+
|
|
126
129
|
initResourceHandler();
|
|
127
130
|
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { sendToRelay } from '@blocklet/sdk/service/notification';
|
|
2
|
+
|
|
3
|
+
import type { CheckoutSession, Invoice, PaymentIntent } from '../store/models';
|
|
4
|
+
import { events } from './event';
|
|
5
|
+
|
|
6
|
+
export function broadcast(channel: string, eventName: string, data: any) {
|
|
7
|
+
sendToRelay(channel, eventName, data).catch((err: any) => {
|
|
8
|
+
console.error(`Failed to broadcast info: ${channel}.${eventName}`, err);
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function initEventBroadcast() {
|
|
13
|
+
events.on('payment_intent.succeeded', (data: PaymentIntent) => {
|
|
14
|
+
broadcast('events', 'payment_intent.succeeded', data);
|
|
15
|
+
});
|
|
16
|
+
events.on('checkout.session.completed', (data: CheckoutSession) => {
|
|
17
|
+
broadcast('events', 'checkout.session.completed', data);
|
|
18
|
+
});
|
|
19
|
+
events.on('checkout.session.nft_minted', (data: CheckoutSession) => {
|
|
20
|
+
broadcast('events', 'checkout.session.nft_minted', data);
|
|
21
|
+
});
|
|
22
|
+
events.on('invoice.paid', (data: Invoice) => {
|
|
23
|
+
broadcast('events', 'invoice.paid', data);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { BN, fromTokenToUnit, fromUnitToToken } from '@ocap/util';
|
|
2
2
|
|
|
3
3
|
import dayjs from '../libs/dayjs';
|
|
4
|
+
import { getLock } from '../libs/lock';
|
|
4
5
|
import logger from '../libs/logger';
|
|
5
6
|
import createQueue from '../libs/queue';
|
|
6
7
|
import { getPriceUintAmountByCurrency } from '../libs/session';
|
|
@@ -22,9 +23,17 @@ type UsageRecordJob = {
|
|
|
22
23
|
subscriptionItemId: string;
|
|
23
24
|
};
|
|
24
25
|
|
|
25
|
-
//
|
|
26
|
+
// We need to use lock here to prevent race conditions
|
|
27
|
+
export async function handleUsageRecord(job: UsageRecordJob) {
|
|
28
|
+
const lock = getLock(`${job.subscriptionId}-threshold`);
|
|
29
|
+
await lock.acquire();
|
|
30
|
+
const result = await doHandleUsageRecord(job);
|
|
31
|
+
lock.release();
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
|
|
26
35
|
// generate invoice for metered billing
|
|
27
|
-
export const
|
|
36
|
+
export const doHandleUsageRecord = async (job: UsageRecordJob) => {
|
|
28
37
|
logger.info('handle usage record', job);
|
|
29
38
|
|
|
30
39
|
const subscription = await Subscription.findByPk(job.subscriptionId);
|
|
@@ -78,9 +78,13 @@ router.post('/', auth, async (req, res) => {
|
|
|
78
78
|
// @link https://stripe.com/docs/api/usage_records/subscription_item_summary_list
|
|
79
79
|
const schema = createListParamSchema<{
|
|
80
80
|
subscription_item_id: string;
|
|
81
|
+
start?: number;
|
|
82
|
+
end?: number;
|
|
81
83
|
}>(
|
|
82
84
|
{
|
|
83
85
|
subscription_item_id: Joi.string().required(),
|
|
86
|
+
start: Joi.number().optional(),
|
|
87
|
+
end: Joi.number().optional(),
|
|
84
88
|
},
|
|
85
89
|
100
|
|
86
90
|
);
|
|
@@ -149,7 +153,10 @@ export function createUsageRecordQueryFn(doc?: Subscription) {
|
|
|
149
153
|
const { rows: list, count } = await UsageRecord.findAndCountAll({
|
|
150
154
|
where: {
|
|
151
155
|
subscription_item_id: query.subscription_item_id,
|
|
152
|
-
timestamp: {
|
|
156
|
+
timestamp: {
|
|
157
|
+
[Op.gt]: query.start || subscription.current_period_start,
|
|
158
|
+
[Op.lte]: query.end || subscription.current_period_end,
|
|
159
|
+
},
|
|
153
160
|
},
|
|
154
161
|
order: [['created_at', 'ASC']],
|
|
155
162
|
offset: (page - 1) * pageSize,
|
|
@@ -112,8 +112,8 @@ export class UsageRecord extends Model<InferAttributes<UsageRecord>, InferCreati
|
|
|
112
112
|
subscription_item_id: id,
|
|
113
113
|
billed: false,
|
|
114
114
|
timestamp: {
|
|
115
|
-
[Op.
|
|
116
|
-
[Op.
|
|
115
|
+
[Op.gt]: start,
|
|
116
|
+
[Op.lte]: end,
|
|
117
117
|
},
|
|
118
118
|
},
|
|
119
119
|
order: [['timestamp', 'DESC']],
|
package/blocklet.yml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.13.
|
|
3
|
+
"version": "1.13.242",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "cross-env COMPONENT_STORE_URL=https://test.store.blocklet.dev blocklet dev --open",
|
|
6
6
|
"eject": "vite eject",
|
|
@@ -51,10 +51,10 @@
|
|
|
51
51
|
"@arcblock/ux": "^2.9.77",
|
|
52
52
|
"@arcblock/validator": "^1.18.116",
|
|
53
53
|
"@blocklet/logger": "1.16.26",
|
|
54
|
-
"@blocklet/payment-react": "1.13.
|
|
54
|
+
"@blocklet/payment-react": "1.13.242",
|
|
55
55
|
"@blocklet/sdk": "1.16.26",
|
|
56
56
|
"@blocklet/ui-react": "^2.9.77",
|
|
57
|
-
"@blocklet/uploader": "^0.
|
|
57
|
+
"@blocklet/uploader": "^0.1.2",
|
|
58
58
|
"@mui/icons-material": "^5.15.16",
|
|
59
59
|
"@mui/lab": "^5.0.0-alpha.170",
|
|
60
60
|
"@mui/material": "^5.15.16",
|
|
@@ -116,7 +116,7 @@
|
|
|
116
116
|
"devDependencies": {
|
|
117
117
|
"@abtnode/types": "1.16.26",
|
|
118
118
|
"@arcblock/eslint-config-ts": "^0.3.0",
|
|
119
|
-
"@blocklet/payment-types": "1.13.
|
|
119
|
+
"@blocklet/payment-types": "1.13.242",
|
|
120
120
|
"@types/cookie-parser": "^1.4.7",
|
|
121
121
|
"@types/cors": "^2.8.17",
|
|
122
122
|
"@types/dotenv-flow": "^3.3.3",
|
|
@@ -155,5 +155,5 @@
|
|
|
155
155
|
"parser": "typescript"
|
|
156
156
|
}
|
|
157
157
|
},
|
|
158
|
-
"gitHead": "
|
|
158
|
+
"gitHead": "ec356f3a242ca99c4f86cc2707dc7057c3f2c38d"
|
|
159
159
|
}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
2
2
|
import { formatAmount, formatToDate, getPriceUintAmountByCurrency } from '@blocklet/payment-react';
|
|
3
|
-
import type { TInvoiceExpanded } from '@blocklet/payment-types';
|
|
3
|
+
import type { TInvoiceExpanded, TInvoiceItem } from '@blocklet/payment-types';
|
|
4
4
|
import { InfoOutlined } from '@mui/icons-material';
|
|
5
|
-
import { Stack, Table, TableBody, TableCell, TableHead, TableRow, Tooltip, Typography } from '@mui/material';
|
|
5
|
+
import { Box, Stack, Table, TableBody, TableCell, TableHead, TableRow, Tooltip, Typography } from '@mui/material';
|
|
6
6
|
import { styled } from '@mui/system';
|
|
7
7
|
import { toBN } from '@ocap/util';
|
|
8
|
+
import { useSetState } from 'ahooks';
|
|
8
9
|
|
|
9
10
|
import LineItemActions from '../subscription/items/actions';
|
|
11
|
+
import { UsageRecordDialog } from '../subscription/items/usage-records';
|
|
10
12
|
|
|
11
13
|
type Props = {
|
|
12
14
|
invoice: TInvoiceExpanded;
|
|
@@ -20,6 +22,7 @@ type InvoiceDetailItem = {
|
|
|
20
22
|
rawQuantity: number;
|
|
21
23
|
price: string;
|
|
22
24
|
amount: string;
|
|
25
|
+
raw: TInvoiceItem;
|
|
23
26
|
};
|
|
24
27
|
|
|
25
28
|
type InvoiceSummaryItem = {
|
|
@@ -61,6 +64,7 @@ export function getInvoiceRows(invoice: TInvoiceExpanded) {
|
|
|
61
64
|
? formatAmount(getPriceUintAmountByCurrency(line.price, invoice.paymentCurrency) || line.amount, invoice.paymentCurrency.decimal) // prettier-ignore
|
|
62
65
|
: '',
|
|
63
66
|
amount: formatAmount(line.amount, invoice.paymentCurrency.decimal),
|
|
67
|
+
raw: line,
|
|
64
68
|
}));
|
|
65
69
|
|
|
66
70
|
const summary: InvoiceSummaryItem[] = [
|
|
@@ -106,75 +110,112 @@ export function getInvoiceRows(invoice: TInvoiceExpanded) {
|
|
|
106
110
|
export default function InvoiceTable({ invoice, simple }: Props) {
|
|
107
111
|
const { t } = useLocaleContext();
|
|
108
112
|
const { detail, summary } = getInvoiceRows(invoice);
|
|
113
|
+
const [state, setState] = useSetState({
|
|
114
|
+
subscriptionId: '',
|
|
115
|
+
subscriptionItemId: '',
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
const onOpenUsageRecords = (line: InvoiceDetailItem) => {
|
|
119
|
+
if (line.rawQuantity && line.raw.subscription_id && line.raw.subscription_item_id) {
|
|
120
|
+
setState({
|
|
121
|
+
subscriptionId: line.raw.subscription_id,
|
|
122
|
+
subscriptionItemId: line.raw.subscription_item_id,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const onCloseUsageRecords = () => {
|
|
128
|
+
setState({
|
|
129
|
+
subscriptionId: '',
|
|
130
|
+
subscriptionItemId: '',
|
|
131
|
+
});
|
|
132
|
+
};
|
|
109
133
|
|
|
110
134
|
return (
|
|
111
|
-
<
|
|
112
|
-
<
|
|
113
|
-
<
|
|
114
|
-
<TableCell sx={{ textTransform: 'none', fontWeight: 'normal' }}>Description</TableCell>
|
|
115
|
-
<TableCell sx={{ textTransform: 'none', fontWeight: 'normal', width: 80 }} align="right">
|
|
116
|
-
{t('common.quantity')}
|
|
117
|
-
</TableCell>
|
|
118
|
-
<TableCell sx={{ textTransform: 'none', fontWeight: 'normal', width: 120 }} align="right">
|
|
119
|
-
{t('payment.customer.invoice.unitPrice')}
|
|
120
|
-
</TableCell>
|
|
121
|
-
<TableCell sx={{ textTransform: 'none', fontWeight: 'normal', width: 110 }} align="right">
|
|
122
|
-
{t('common.amount')}
|
|
123
|
-
</TableCell>
|
|
124
|
-
{!simple && (
|
|
125
|
-
<TableCell sx={{ textTransform: 'none', fontWeight: 'normal', width: 50 }} align="right">
|
|
126
|
-
|
|
127
|
-
</TableCell>
|
|
128
|
-
)}
|
|
129
|
-
</TableRow>
|
|
130
|
-
{invoice.period_end > 0 && invoice.period_start > 0 && (
|
|
135
|
+
<Box>
|
|
136
|
+
<StyledTable>
|
|
137
|
+
<TableHead>
|
|
131
138
|
<TableRow sx={{ borderBottom: '1px solid #eee' }}>
|
|
132
|
-
<TableCell
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
</Typography>
|
|
139
|
+
<TableCell sx={{ textTransform: 'none', fontWeight: 'normal' }}>Description</TableCell>
|
|
140
|
+
<TableCell sx={{ textTransform: 'none', fontWeight: 'normal', width: 80 }} align="right">
|
|
141
|
+
{t('common.quantity')}
|
|
136
142
|
</TableCell>
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
<TableRow key={line.id} sx={{ borderBottom: '1px solid #eee' }}>
|
|
143
|
-
<TableCell sx={{ fontWeight: 600 }}>{line.product}</TableCell>
|
|
144
|
-
<TableCell align="right">
|
|
145
|
-
<Stack direction="row" spacing={0.5} alignItems="center" justifyContent="flex-end">
|
|
146
|
-
<Typography>{line.quantity}</Typography>
|
|
147
|
-
{!!line.rawQuantity && (
|
|
148
|
-
<Tooltip
|
|
149
|
-
title={t('payment.customer.invoice.rawQuantity', { quantity: line.rawQuantity })}
|
|
150
|
-
placement="top">
|
|
151
|
-
<InfoOutlined fontSize="small" sx={{ color: 'text.secondary', cursor: 'pointer' }} />
|
|
152
|
-
</Tooltip>
|
|
153
|
-
)}
|
|
154
|
-
</Stack>
|
|
143
|
+
<TableCell sx={{ textTransform: 'none', fontWeight: 'normal', width: 120 }} align="right">
|
|
144
|
+
{t('payment.customer.invoice.unitPrice')}
|
|
145
|
+
</TableCell>
|
|
146
|
+
<TableCell sx={{ textTransform: 'none', fontWeight: 'normal', width: 110 }} align="right">
|
|
147
|
+
{t('common.amount')}
|
|
155
148
|
</TableCell>
|
|
156
|
-
<TableCell align="right">{line.price}</TableCell>
|
|
157
|
-
<TableCell align="right">{line.amount}</TableCell>
|
|
158
149
|
{!simple && (
|
|
159
|
-
<TableCell align="right">
|
|
160
|
-
|
|
150
|
+
<TableCell sx={{ textTransform: 'none', fontWeight: 'normal', width: 50 }} align="right">
|
|
151
|
+
|
|
161
152
|
</TableCell>
|
|
162
153
|
)}
|
|
163
154
|
</TableRow>
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
155
|
+
{invoice.period_end > 0 && invoice.period_start > 0 && (
|
|
156
|
+
<TableRow sx={{ borderBottom: '1px solid #eee' }}>
|
|
157
|
+
<TableCell align="left" colSpan={simple ? 4 : 5}>
|
|
158
|
+
<Typography component="span" variant="body1" color="text.secondary">
|
|
159
|
+
{formatToDate(invoice.period_start * 1000)} - {formatToDate(invoice.period_end * 1000)}
|
|
160
|
+
</Typography>
|
|
161
|
+
</TableCell>
|
|
162
|
+
</TableRow>
|
|
163
|
+
)}
|
|
164
|
+
</TableHead>
|
|
165
|
+
<TableBody>
|
|
166
|
+
{detail.map((line) => (
|
|
167
|
+
<TableRow key={line.id} sx={{ borderBottom: '1px solid #eee' }}>
|
|
168
|
+
<TableCell sx={{ fontWeight: 600 }}>{line.product}</TableCell>
|
|
169
|
+
<TableCell align="right">
|
|
170
|
+
<Stack
|
|
171
|
+
direction="row"
|
|
172
|
+
spacing={0.5}
|
|
173
|
+
alignItems="center"
|
|
174
|
+
justifyContent="flex-end"
|
|
175
|
+
sx={{ cursor: 'pointer' }}
|
|
176
|
+
onClick={() => onOpenUsageRecords(line)}>
|
|
177
|
+
<Typography component="span">{line.quantity}</Typography>
|
|
178
|
+
{!!line.rawQuantity && (
|
|
179
|
+
<Tooltip
|
|
180
|
+
title={t('payment.customer.invoice.rawQuantity', { quantity: line.rawQuantity })}
|
|
181
|
+
placement="top">
|
|
182
|
+
<InfoOutlined fontSize="small" sx={{ color: 'text.secondary', cursor: 'pointer' }} />
|
|
183
|
+
</Tooltip>
|
|
184
|
+
)}
|
|
185
|
+
</Stack>
|
|
186
|
+
</TableCell>
|
|
187
|
+
<TableCell align="right">{line.price}</TableCell>
|
|
188
|
+
<TableCell align="right">{line.amount}</TableCell>
|
|
189
|
+
{!simple && (
|
|
190
|
+
<TableCell align="right">
|
|
191
|
+
<LineItemActions data={line as any} />
|
|
192
|
+
</TableCell>
|
|
193
|
+
)}
|
|
194
|
+
</TableRow>
|
|
195
|
+
))}
|
|
196
|
+
{summary.map((line) => (
|
|
197
|
+
<TableRow key={line.key}>
|
|
198
|
+
<TableCell colSpan={3} align="right" sx={{ fontWeight: 600, color: line.color }}>
|
|
199
|
+
{t(line.key)}
|
|
200
|
+
</TableCell>
|
|
201
|
+
<TableCell align="right" sx={{ fontWeight: 600 }}>
|
|
202
|
+
{line.value}
|
|
203
|
+
</TableCell>
|
|
204
|
+
<TableCell> </TableCell>
|
|
205
|
+
</TableRow>
|
|
206
|
+
))}
|
|
207
|
+
</TableBody>
|
|
208
|
+
</StyledTable>
|
|
209
|
+
{state.subscriptionId && state.subscriptionItemId && (
|
|
210
|
+
<UsageRecordDialog
|
|
211
|
+
subscriptionId={state.subscriptionId}
|
|
212
|
+
id={state.subscriptionItemId}
|
|
213
|
+
onConfirm={onCloseUsageRecords}
|
|
214
|
+
start={invoice.metadata?.usage_start || invoice.period_start}
|
|
215
|
+
end={invoice.metadata?.usage_end || invoice.period_end}
|
|
216
|
+
/>
|
|
217
|
+
)}
|
|
218
|
+
</Box>
|
|
178
219
|
);
|
|
179
220
|
}
|
|
180
221
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
/* eslint-disable react/require-default-props */
|
|
2
|
+
import Empty from '@arcblock/ux/lib/Empty';
|
|
1
3
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
2
4
|
import { ConfirmDialog, api } from '@blocklet/payment-react';
|
|
3
5
|
import type { TUsageRecord } from '@blocklet/payment-types';
|
|
@@ -5,12 +7,26 @@ import { Alert, Box, Button, CircularProgress } from '@mui/material';
|
|
|
5
7
|
import { useRequest } from 'ahooks';
|
|
6
8
|
import { useState } from 'react';
|
|
7
9
|
import { Bar, BarChart, Rectangle, Tooltip, XAxis, YAxis } from 'recharts';
|
|
10
|
+
import { withQuery } from 'ufo';
|
|
8
11
|
|
|
9
12
|
import { stringToColor } from '../../../libs/util';
|
|
10
13
|
|
|
11
|
-
|
|
14
|
+
// FIXME: pagination here
|
|
15
|
+
const fetchData = (
|
|
16
|
+
subscriptionId: string,
|
|
17
|
+
id: string,
|
|
18
|
+
start: number = 0,
|
|
19
|
+
end: number = 0
|
|
20
|
+
): Promise<{ list: TUsageRecord[]; count: number }> => {
|
|
12
21
|
return api
|
|
13
|
-
.get(
|
|
22
|
+
.get(
|
|
23
|
+
withQuery(`/api/subscriptions/${subscriptionId}/usage-records`, {
|
|
24
|
+
subscription_item_id: id,
|
|
25
|
+
pageSize: 100,
|
|
26
|
+
start,
|
|
27
|
+
end,
|
|
28
|
+
})
|
|
29
|
+
)
|
|
14
30
|
.then((res) => res.data);
|
|
15
31
|
};
|
|
16
32
|
|
|
@@ -19,10 +35,22 @@ const colors = {
|
|
|
19
35
|
active: stringToColor('active'),
|
|
20
36
|
};
|
|
21
37
|
|
|
22
|
-
export function UsageRecordDialog(
|
|
38
|
+
export function UsageRecordDialog({
|
|
39
|
+
subscriptionId,
|
|
40
|
+
id,
|
|
41
|
+
onConfirm,
|
|
42
|
+
start = 0,
|
|
43
|
+
end = 0,
|
|
44
|
+
}: {
|
|
45
|
+
subscriptionId: string;
|
|
46
|
+
id: string;
|
|
47
|
+
onConfirm: any;
|
|
48
|
+
start?: number;
|
|
49
|
+
end?: number;
|
|
50
|
+
}) {
|
|
23
51
|
const { t } = useLocaleContext();
|
|
24
|
-
const { loading, error, data } = useRequest(() => fetchData(
|
|
25
|
-
refreshDeps: [
|
|
52
|
+
const { loading, error, data } = useRequest(() => fetchData(subscriptionId, id, start, end), {
|
|
53
|
+
refreshDeps: [subscriptionId, id, start, end],
|
|
26
54
|
});
|
|
27
55
|
|
|
28
56
|
if (error) {
|
|
@@ -30,8 +58,10 @@ export function UsageRecordDialog(props: { subscriptionId: string; id: string; o
|
|
|
30
58
|
<ConfirmDialog
|
|
31
59
|
title={t('admin.subscription.usage.current')}
|
|
32
60
|
message={<Alert severity="error">{error.message}</Alert>}
|
|
33
|
-
onConfirm={
|
|
34
|
-
onCancel={
|
|
61
|
+
onConfirm={onConfirm}
|
|
62
|
+
onCancel={onConfirm}
|
|
63
|
+
color="primary"
|
|
64
|
+
cancel={false}
|
|
35
65
|
/>
|
|
36
66
|
);
|
|
37
67
|
}
|
|
@@ -41,8 +71,10 @@ export function UsageRecordDialog(props: { subscriptionId: string; id: string; o
|
|
|
41
71
|
<ConfirmDialog
|
|
42
72
|
title={t('admin.subscription.usage.current')}
|
|
43
73
|
message={<CircularProgress />}
|
|
44
|
-
onConfirm={
|
|
45
|
-
onCancel={
|
|
74
|
+
onConfirm={onConfirm}
|
|
75
|
+
onCancel={onConfirm}
|
|
76
|
+
color="primary"
|
|
77
|
+
cancel={false}
|
|
46
78
|
/>
|
|
47
79
|
);
|
|
48
80
|
}
|
|
@@ -51,32 +83,52 @@ export function UsageRecordDialog(props: { subscriptionId: string; id: string; o
|
|
|
51
83
|
<ConfirmDialog
|
|
52
84
|
title={t('admin.subscription.usage.current')}
|
|
53
85
|
message={
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
86
|
+
data.list.length > 0 ? (
|
|
87
|
+
<BarChart
|
|
88
|
+
width={480}
|
|
89
|
+
height={240}
|
|
90
|
+
data={data.list.map((item) => ({
|
|
91
|
+
...item,
|
|
92
|
+
date: new Date(item.timestamp * 1000).toLocaleString(),
|
|
93
|
+
}))}
|
|
94
|
+
margin={{
|
|
95
|
+
top: 5,
|
|
96
|
+
right: 5,
|
|
97
|
+
left: 0,
|
|
98
|
+
bottom: 5,
|
|
99
|
+
}}>
|
|
100
|
+
<Tooltip />
|
|
101
|
+
<Bar
|
|
102
|
+
dataKey="quantity"
|
|
103
|
+
fill={colors.normal}
|
|
104
|
+
activeBar={<Rectangle fill={colors.active} strokeWidth={0} />}
|
|
105
|
+
/>
|
|
106
|
+
<XAxis dataKey="date" />
|
|
107
|
+
<YAxis mirror />
|
|
108
|
+
</BarChart>
|
|
109
|
+
) : (
|
|
110
|
+
<Empty>{t('admin.usageRecord.empty')}</Empty>
|
|
111
|
+
)
|
|
72
112
|
}
|
|
73
|
-
onConfirm={
|
|
74
|
-
onCancel={
|
|
113
|
+
onConfirm={onConfirm}
|
|
114
|
+
onCancel={onConfirm}
|
|
115
|
+
color="primary"
|
|
116
|
+
cancel={false}
|
|
75
117
|
/>
|
|
76
118
|
);
|
|
77
119
|
}
|
|
78
120
|
|
|
79
|
-
export default function UsageRecords({
|
|
121
|
+
export default function UsageRecords({
|
|
122
|
+
subscriptionId,
|
|
123
|
+
id,
|
|
124
|
+
start = 0,
|
|
125
|
+
end = 0,
|
|
126
|
+
}: {
|
|
127
|
+
subscriptionId: string;
|
|
128
|
+
id: string;
|
|
129
|
+
start?: number;
|
|
130
|
+
end?: number;
|
|
131
|
+
}) {
|
|
80
132
|
const { t } = useLocaleContext();
|
|
81
133
|
const [open, setOpen] = useState(false);
|
|
82
134
|
return (
|
|
@@ -84,7 +136,15 @@ export default function UsageRecords({ subscriptionId, id }: { subscriptionId: s
|
|
|
84
136
|
<Button size="small" variant="text" color="info" onClick={() => setOpen(true)}>
|
|
85
137
|
{t('admin.subscription.usage.view')}
|
|
86
138
|
</Button>
|
|
87
|
-
{open &&
|
|
139
|
+
{open && (
|
|
140
|
+
<UsageRecordDialog
|
|
141
|
+
subscriptionId={subscriptionId}
|
|
142
|
+
id={id}
|
|
143
|
+
start={start}
|
|
144
|
+
end={end}
|
|
145
|
+
onConfirm={() => setOpen(false)}
|
|
146
|
+
/>
|
|
147
|
+
)}
|
|
88
148
|
</Box>
|
|
89
149
|
);
|
|
90
150
|
}
|
|
@@ -24,22 +24,23 @@ const fetchData = (params: Record<string, any> = {}): Promise<Paginated<TSubscri
|
|
|
24
24
|
|
|
25
25
|
type Props = {
|
|
26
26
|
id: string;
|
|
27
|
+
status: string;
|
|
27
28
|
onChange?: (action?: string) => any | Promise<any>;
|
|
28
29
|
onClickSubscription: (subscription: TSubscriptionExpanded) => void | Promise<void>;
|
|
29
30
|
} & Omit<StackProps, 'onChange'>;
|
|
30
31
|
|
|
31
32
|
const pageSize = 4;
|
|
32
33
|
|
|
33
|
-
export default function CurrentSubscriptions({ id, onChange, onClickSubscription, ...rest }: Props) {
|
|
34
|
+
export default function CurrentSubscriptions({ id, status, onChange, onClickSubscription, ...rest }: Props) {
|
|
34
35
|
const { t } = useLocaleContext();
|
|
35
36
|
|
|
36
37
|
const { data, loadMore, loadingMore, loading } = useInfiniteScroll<Paginated<TSubscriptionExpanded>>(
|
|
37
38
|
(d) => {
|
|
38
39
|
const page = d ? Math.ceil(d.list.length / pageSize) + 1 : 1;
|
|
39
|
-
return fetchData({ page, pageSize, status
|
|
40
|
+
return fetchData({ page, pageSize, status, customer_id: id });
|
|
40
41
|
},
|
|
41
42
|
{
|
|
42
|
-
reloadDeps: [id],
|
|
43
|
+
reloadDeps: [id, status],
|
|
43
44
|
}
|
|
44
45
|
);
|
|
45
46
|
|
package/src/locales/en.tsx
CHANGED
|
@@ -360,6 +360,7 @@ export default flat({
|
|
|
360
360
|
from: 'Billed from',
|
|
361
361
|
empty: 'No invoice',
|
|
362
362
|
number: 'Invoice Number',
|
|
363
|
+
description: 'Billing Description',
|
|
363
364
|
dueDate: 'Due',
|
|
364
365
|
finalizedAt: 'Finalized At',
|
|
365
366
|
paidAt: 'Payment Date',
|
|
@@ -501,5 +502,8 @@ export default flat({
|
|
|
501
502
|
view: 'View refund detail',
|
|
502
503
|
attention: 'Failed refunds',
|
|
503
504
|
},
|
|
505
|
+
usageRecord: {
|
|
506
|
+
empty: 'No usage records',
|
|
507
|
+
},
|
|
504
508
|
},
|
|
505
509
|
});
|
package/src/locales/zh.tsx
CHANGED
|
@@ -351,6 +351,7 @@ export default flat({
|
|
|
351
351
|
from: '账单来自',
|
|
352
352
|
empty: '没有账单',
|
|
353
353
|
number: '账单编号',
|
|
354
|
+
description: '账单说明',
|
|
354
355
|
dueDate: '截止日期',
|
|
355
356
|
finalizedAt: '已完成时间',
|
|
356
357
|
paidAt: '支付日期',
|
|
@@ -491,5 +492,8 @@ export default flat({
|
|
|
491
492
|
attention: '失败的退款',
|
|
492
493
|
view: '查看退款详情',
|
|
493
494
|
},
|
|
495
|
+
usageRecord: {
|
|
496
|
+
empty: '用量记录为空',
|
|
497
|
+
},
|
|
494
498
|
},
|
|
495
499
|
});
|
|
@@ -116,6 +116,7 @@ export default function InvoiceDetail(props: { id: string }) {
|
|
|
116
116
|
<SectionHeader title={t('admin.details')} />
|
|
117
117
|
<Stack>
|
|
118
118
|
<InfoRow label={t('admin.invoice.number')} value={data.number} />
|
|
119
|
+
<InfoRow label={t('admin.invoice.description')} value={data.description} />
|
|
119
120
|
<InfoRow label={t('admin.invoice.billTo')} value={<CustomerLink customer={data.customer} />} />
|
|
120
121
|
<InfoRow
|
|
121
122
|
label={t('admin.subscription.currentPeriod')}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import DID from '@arcblock/ux/lib/DID';
|
|
2
2
|
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
3
3
|
import Toast from '@arcblock/ux/lib/Toast';
|
|
4
|
-
import { CustomerInvoiceList, formatError, getPrefix, usePaymentContext } from '@blocklet/payment-react';
|
|
4
|
+
import { CustomerInvoiceList, Switch, formatError, getPrefix, usePaymentContext } from '@blocklet/payment-react';
|
|
5
5
|
import type { GroupedBN, TCustomerExpanded } from '@blocklet/payment-types';
|
|
6
6
|
import { Edit } from '@mui/icons-material';
|
|
7
7
|
import { Alert, Box, Button, CircularProgress, Grid, Stack, Tooltip } from '@mui/material';
|
|
@@ -33,7 +33,7 @@ export default function CustomerHome() {
|
|
|
33
33
|
const { t } = useLocaleContext();
|
|
34
34
|
const { events } = useSessionContext();
|
|
35
35
|
const { livemode, setLivemode } = usePaymentContext();
|
|
36
|
-
const [state, setState] = useSetState({ editing: false, loading: false });
|
|
36
|
+
const [state, setState] = useSetState({ editing: false, loading: false, onlyActive: true });
|
|
37
37
|
const navigate = useNavigate();
|
|
38
38
|
const { isPending, startTransition } = useTransitionContext();
|
|
39
39
|
|
|
@@ -84,6 +84,10 @@ export default function CustomerHome() {
|
|
|
84
84
|
}
|
|
85
85
|
};
|
|
86
86
|
|
|
87
|
+
const onToggleActive = () => {
|
|
88
|
+
setState({ onlyActive: !state.onlyActive });
|
|
89
|
+
};
|
|
90
|
+
|
|
87
91
|
return (
|
|
88
92
|
<>
|
|
89
93
|
<ProgressBar pending={isPending} />
|
|
@@ -91,10 +95,27 @@ export default function CustomerHome() {
|
|
|
91
95
|
<Grid item xs={12} md={8}>
|
|
92
96
|
<Root direction="column" spacing={3} sx={{ my: 2 }}>
|
|
93
97
|
<Box className="section">
|
|
94
|
-
<SectionHeader title={t('payment.customer.subscriptions.current')} mb={0}
|
|
98
|
+
<SectionHeader title={t('payment.customer.subscriptions.current')} mb={0}>
|
|
99
|
+
<label
|
|
100
|
+
htmlFor="only-active-switch"
|
|
101
|
+
style={{
|
|
102
|
+
fontSize: 14,
|
|
103
|
+
fontWeight: 600,
|
|
104
|
+
cursor: 'pointer',
|
|
105
|
+
}}>
|
|
106
|
+
{t('payment.customer.subscriptions.viewAll')}
|
|
107
|
+
<Switch
|
|
108
|
+
id="only-active-switch"
|
|
109
|
+
sx={{ ml: 0.5 }}
|
|
110
|
+
checked={!state.onlyActive}
|
|
111
|
+
onChange={onToggleActive}
|
|
112
|
+
/>
|
|
113
|
+
</label>
|
|
114
|
+
</SectionHeader>
|
|
95
115
|
<Box className="section-body">
|
|
96
116
|
<CurrentSubscriptions
|
|
97
117
|
id={data.id}
|
|
118
|
+
status={state.onlyActive ? 'active,trialing' : 'active,trialing,paused,past_due,canceled'}
|
|
98
119
|
style={{
|
|
99
120
|
cursor: 'pointer',
|
|
100
121
|
}}
|
|
@@ -45,36 +45,30 @@ export default function CustomerInvoiceDetail() {
|
|
|
45
45
|
const action = searchParams.get('action');
|
|
46
46
|
|
|
47
47
|
const onPay = () => {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
});
|
|
73
|
-
} catch (err) {
|
|
74
|
-
Toast.error(formatError(err));
|
|
75
|
-
} finally {
|
|
76
|
-
setState({ paying: false });
|
|
77
|
-
}
|
|
48
|
+
setState({ paying: true });
|
|
49
|
+
connectApi.open({
|
|
50
|
+
action: 'collect',
|
|
51
|
+
messages: {
|
|
52
|
+
scan: '',
|
|
53
|
+
title: t(`payment.customer.invoice.${action || 'pay'}`),
|
|
54
|
+
success: t(`payment.customer.invoice.${action || 'pay'}Success`),
|
|
55
|
+
error: t(`payment.customer.invoice.${action || 'pay'}Error`),
|
|
56
|
+
confirm: '',
|
|
57
|
+
} as any,
|
|
58
|
+
extraParams: { invoiceId: params.id, action },
|
|
59
|
+
onSuccess: async () => {
|
|
60
|
+
connectApi.close();
|
|
61
|
+
await runAsync();
|
|
62
|
+
},
|
|
63
|
+
onClose: () => {
|
|
64
|
+
connectApi.close();
|
|
65
|
+
setState({ paying: false });
|
|
66
|
+
},
|
|
67
|
+
onError: (err: any) => {
|
|
68
|
+
setState({ paying: false });
|
|
69
|
+
Toast.error(formatError(err));
|
|
70
|
+
},
|
|
71
|
+
});
|
|
78
72
|
};
|
|
79
73
|
|
|
80
74
|
const closePay = () => {
|
|
@@ -86,7 +80,10 @@ export default function CustomerInvoiceDetail() {
|
|
|
86
80
|
// @ts-expect-error
|
|
87
81
|
if (error?.response?.status === 403) {
|
|
88
82
|
closePay();
|
|
89
|
-
} else if (
|
|
83
|
+
} else if (
|
|
84
|
+
['pay', 'renew'].includes(action as string) &&
|
|
85
|
+
['open', 'uncollectible'].includes(data?.status as string)
|
|
86
|
+
) {
|
|
90
87
|
onPay();
|
|
91
88
|
}
|
|
92
89
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
@@ -127,6 +124,7 @@ export default function CustomerInvoiceDetail() {
|
|
|
127
124
|
<Stack className="invoice-summary-wrapper">
|
|
128
125
|
<InfoRow label={t('admin.invoice.from')} value={data.statement_descriptor || blocklet.appName} />
|
|
129
126
|
<InfoRow label={t('admin.invoice.number')} value={data.number} />
|
|
127
|
+
<InfoRow label={t('admin.invoice.description')} value={data.description} />
|
|
130
128
|
<InfoRow
|
|
131
129
|
label={t('common.status')}
|
|
132
130
|
value={<Status label={data.status} color={getInvoiceStatusColor(data.status)} />}
|