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 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
- // FIXME: support subscription item level billing_thresholds
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 handleUsageRecord = async (job: UsageRecordJob) => {
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: { [Op.gte]: subscription.current_period_start, [Op.lt]: subscription.current_period_end },
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.gte]: start,
116
- [Op.lt]: end,
115
+ [Op.gt]: start,
116
+ [Op.lte]: end,
117
117
  },
118
118
  },
119
119
  order: [['timestamp', 'DESC']],
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.13.240
17
+ version: 1.13.242
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.13.240",
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.240",
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.0.78",
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.240",
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": "02b0f1dc4e711c889dd88f7ace2b56062c5d2f0e"
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
- <StyledTable>
112
- <TableHead>
113
- <TableRow sx={{ borderBottom: '1px solid #eee' }}>
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
- &nbsp;
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 align="left" colSpan={simple ? 4 : 5}>
133
- <Typography component="span" variant="body1" color="text.secondary">
134
- {formatToDate(invoice.period_start * 1000)} - {formatToDate(invoice.period_end * 1000)}
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
- </TableRow>
138
- )}
139
- </TableHead>
140
- <TableBody>
141
- {detail.map((line) => (
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
- <LineItemActions data={line as any} />
150
+ <TableCell sx={{ textTransform: 'none', fontWeight: 'normal', width: 50 }} align="right">
151
+ &nbsp;
161
152
  </TableCell>
162
153
  )}
163
154
  </TableRow>
164
- ))}
165
- {summary.map((line) => (
166
- <TableRow key={line.key}>
167
- <TableCell colSpan={3} align="right" sx={{ fontWeight: 600, color: line.color }}>
168
- {t(line.key)}
169
- </TableCell>
170
- <TableCell align="right" sx={{ fontWeight: 600 }}>
171
- {line.value}
172
- </TableCell>
173
- <TableCell>&nbsp;</TableCell>
174
- </TableRow>
175
- ))}
176
- </TableBody>
177
- </StyledTable>
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>&nbsp;</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
- const fetchData = (subscriptionId: string, id: string): Promise<{ list: TUsageRecord[]; count: number }> => {
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(`/api/subscriptions/${subscriptionId}/usage-records?subscription_item_id=${id}&pageSize=100`)
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(props: { subscriptionId: string; id: string; onConfirm: any }) {
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(props.subscriptionId, props.id), {
25
- refreshDeps: [props.subscriptionId, props.id],
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={props.onConfirm}
34
- onCancel={props.onConfirm}
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={props.onConfirm}
45
- onCancel={props.onConfirm}
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
- <BarChart
55
- width={480}
56
- height={240}
57
- data={data.list.map((item) => ({
58
- ...item,
59
- date: new Date(item.timestamp * 1000).toLocaleString(),
60
- }))}
61
- margin={{
62
- top: 5,
63
- right: 5,
64
- left: 0,
65
- bottom: 5,
66
- }}>
67
- <Tooltip />
68
- <Bar dataKey="quantity" fill={colors.normal} activeBar={<Rectangle fill={colors.active} strokeWidth={0} />} />
69
- <XAxis dataKey="date" />
70
- <YAxis mirror />
71
- </BarChart>
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={props.onConfirm}
74
- onCancel={props.onConfirm}
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({ subscriptionId, id }: { subscriptionId: string; id: string }) {
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 && <UsageRecordDialog subscriptionId={subscriptionId} id={id} onConfirm={() => setOpen(false)} />}
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: 'active,trialing,paused,past_due,canceled', customer_id: id });
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
 
@@ -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
  });
@@ -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
- try {
49
- setState({ paying: true });
50
- connectApi.open({
51
- action: 'collect',
52
- messages: {
53
- scan: '',
54
- title: t(`payment.customer.invoice.${action || 'pay'}`),
55
- success: t(`payment.customer.invoice.${action || 'pay'}Success`),
56
- error: t(`payment.customer.invoice.${action || 'pay'}Error`),
57
- confirm: '',
58
- } as any,
59
- extraParams: { invoiceId: params.id, action },
60
- onSuccess: async () => {
61
- connectApi.close();
62
- await runAsync();
63
- },
64
- onClose: () => {
65
- connectApi.close();
66
- setState({ paying: false });
67
- },
68
- onError: (err: any) => {
69
- setState({ paying: false });
70
- Toast.error(formatError(err));
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 (['pay', 'renew'].includes(action as string)) {
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)} />}