payment-kit 1.18.14 → 1.18.16

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 (31) hide show
  1. package/api/src/index.ts +2 -0
  2. package/api/src/libs/invoice.ts +5 -3
  3. package/api/src/routes/connect/change-payment.ts +9 -1
  4. package/api/src/routes/connect/change-plan.ts +9 -1
  5. package/api/src/routes/connect/collect-batch.ts +7 -5
  6. package/api/src/routes/connect/recharge-account.ts +124 -0
  7. package/api/src/routes/connect/setup.ts +8 -1
  8. package/api/src/routes/connect/shared.ts +110 -48
  9. package/api/src/routes/connect/subscribe.ts +11 -1
  10. package/api/src/routes/customers.ts +149 -6
  11. package/api/src/routes/invoices.ts +46 -0
  12. package/api/src/routes/payment-currencies.ts +5 -2
  13. package/api/src/routes/subscriptions.ts +0 -3
  14. package/blocklet.yml +1 -1
  15. package/package.json +8 -8
  16. package/src/app.tsx +11 -3
  17. package/src/components/info-card.tsx +3 -1
  18. package/src/components/info-row.tsx +1 -0
  19. package/src/components/invoice/recharge.tsx +85 -56
  20. package/src/components/invoice/table.tsx +7 -1
  21. package/src/components/subscription/portal/actions.tsx +1 -1
  22. package/src/components/subscription/portal/list.tsx +6 -0
  23. package/src/locales/en.tsx +9 -0
  24. package/src/locales/zh.tsx +9 -0
  25. package/src/pages/admin/settings/vault-config/index.tsx +21 -6
  26. package/src/pages/customer/index.tsx +160 -265
  27. package/src/pages/customer/invoice/detail.tsx +24 -16
  28. package/src/pages/customer/invoice/past-due.tsx +45 -23
  29. package/src/pages/customer/recharge/account.tsx +515 -0
  30. package/src/pages/customer/{recharge.tsx → recharge/subscription.tsx} +11 -11
  31. package/src/pages/customer/subscription/embed.tsx +16 -1
@@ -4,14 +4,26 @@ import Joi from 'joi';
4
4
  import pick from 'lodash/pick';
5
5
  import isEmail from 'validator/es/lib/isEmail';
6
6
 
7
- import { getStakeSummaryByDid, getTokenSummaryByDid } from '../integrations/arcblock/stake';
7
+ import { Op } from 'sequelize';
8
+ import { BN } from '@ocap/util';
9
+ import { getStakeSummaryByDid, getTokenSummaryByDid, getTokenByAddress } from '../integrations/arcblock/stake';
8
10
  import { createListParamSchema, getWhereFromKvQuery, getWhereFromQuery, MetadataSchema } from '../libs/api';
9
11
  import { authenticate } from '../libs/security';
10
12
  import { formatMetadata } from '../libs/util';
11
13
  import { Customer } from '../store/models/customer';
12
14
  import { blocklet } from '../libs/auth';
13
15
  import logger from '../libs/logger';
14
- import { Invoice } from '../store/models';
16
+ import {
17
+ Invoice,
18
+ PaymentCurrency,
19
+ PaymentMethod,
20
+ Price,
21
+ Product,
22
+ Subscription,
23
+ SubscriptionItem,
24
+ } from '../store/models';
25
+ import { getSubscriptionPaymentAddress } from '../libs/subscription';
26
+ import { expandLineItems } from '../libs/session';
15
27
 
16
28
  const router = Router();
17
29
  const auth = authenticate<Customer>({ component: true, roles: ['owner', 'admin'] });
@@ -166,16 +178,48 @@ router.get('/:id/overdue/invoices', auth, async (req, res) => {
166
178
  if (!doc) {
167
179
  return res.status(404).json({ error: 'Customer not found' });
168
180
  }
169
- const [summary, detail, invoices] = await Invoice!.getUncollectibleAmount({
170
- customerId: doc.id,
171
- livemode: req.query.livemode ? !!req.query.livemode : doc.livemode,
181
+ const { rows: invoices, count } = await Invoice.findAndCountAll({
182
+ where: {
183
+ customer_id: doc.id,
184
+ status: ['uncollectible'],
185
+ amount_remaining: { [Op.gt]: '0' },
186
+ },
187
+ include: [
188
+ { model: PaymentCurrency, as: 'paymentCurrency' },
189
+ { model: PaymentMethod, as: 'paymentMethod' },
190
+ ],
191
+ });
192
+ if (count === 0) {
193
+ return res.json({
194
+ summary: null,
195
+ invoices: [],
196
+ subscriptionCount: 0,
197
+ });
198
+ }
199
+ const summary: Record<string, { amount: string; currency: PaymentCurrency; method: PaymentMethod }> = {};
200
+ invoices.forEach((invoice) => {
201
+ const key = invoice.currency_id;
202
+ if (!summary[key]) {
203
+ summary[key] = {
204
+ amount: '0',
205
+ // @ts-ignore
206
+ currency: invoice.paymentCurrency,
207
+ // @ts-ignore
208
+ method: invoice.paymentMethod,
209
+ };
210
+ }
211
+ if (invoice && summary[key]) {
212
+ // @ts-ignore
213
+ summary[key].amount = new BN(summary[key]?.amount || '0')
214
+ .add(new BN(invoice.amount_remaining || '0'))
215
+ .toString();
216
+ }
172
217
  });
173
218
  const subscriptionCount = new Set(invoices.map((x) => x.subscription_id)).size;
174
219
  return res.json({
175
220
  summary,
176
221
  invoices,
177
222
  subscriptionCount,
178
- detail,
179
223
  });
180
224
  } catch (err) {
181
225
  logger.error(err);
@@ -183,6 +227,105 @@ router.get('/:id/overdue/invoices', auth, async (req, res) => {
183
227
  }
184
228
  });
185
229
 
230
+ router.get('/recharge', sessionMiddleware(), async (req, res) => {
231
+ if (!req.user) {
232
+ return res.status(403).json({ error: 'Unauthorized' });
233
+ }
234
+ if (!req.query.currencyId) {
235
+ return res.status(400).json({ error: 'Currency ID is required' });
236
+ }
237
+ try {
238
+ const customer = await Customer.findByPkOrDid(req.user.did as string);
239
+ if (!customer) {
240
+ return res.status(404).json({ error: 'Customer not found' });
241
+ }
242
+
243
+ const paymentCurrency = await PaymentCurrency.findByPk(req.query.currencyId as string);
244
+ if (!paymentCurrency) {
245
+ return res.status(404).json({ error: 'Currency not found' });
246
+ }
247
+
248
+ const paymentMethod = await PaymentMethod.findByPk(paymentCurrency.payment_method_id);
249
+ if (!paymentMethod) {
250
+ return res.status(404).json({ error: 'Payment method not found' });
251
+ }
252
+
253
+ let subscriptions = await Subscription.findAll({
254
+ where: {
255
+ customer_id: customer.id,
256
+ currency_id: paymentCurrency.id,
257
+ status: {
258
+ [Op.in]: ['active', 'trialing', 'past_due'],
259
+ },
260
+ },
261
+ include: [{ model: SubscriptionItem, as: 'items' }],
262
+ });
263
+
264
+ const products = (await Product.findAll()).map((x) => x.toJSON());
265
+ const prices = (await Price.findAll()).map((x) => x.toJSON());
266
+ subscriptions = subscriptions.map((x) => x.toJSON());
267
+ // @ts-ignore
268
+ subscriptions.forEach((x) => expandLineItems(x.items, products, prices));
269
+
270
+ const relatedSubscriptions = subscriptions.filter((sub) => {
271
+ const payerAddress = getSubscriptionPaymentAddress(sub, paymentMethod.type);
272
+ return payerAddress === customer.did;
273
+ });
274
+
275
+ return res.json({
276
+ currency: {
277
+ ...paymentCurrency.toJSON(),
278
+ paymentMethod,
279
+ },
280
+ relatedSubscriptions,
281
+ });
282
+ } catch (err) {
283
+ logger.error('Error getting balance recharge info', err);
284
+ return res.status(500).json({ error: err.message });
285
+ }
286
+ });
287
+
288
+ // get address token
289
+ router.get('/payer-token', sessionMiddleware(), async (req, res) => {
290
+ if (!req.user) {
291
+ return res.status(403).json({ error: 'Unauthorized' });
292
+ }
293
+ if (!req.query.currencyId) {
294
+ return res.status(400).json({ error: 'Currency ID is required' });
295
+ }
296
+ try {
297
+ const customer = await Customer.findByPkOrDid(req.user.did as string);
298
+ if (!customer) {
299
+ return res.status(404).json({ error: 'Customer not found' });
300
+ }
301
+
302
+ const paymentCurrency = await PaymentCurrency.findByPk(req.query.currencyId as string);
303
+ if (!paymentCurrency) {
304
+ return res.status(404).json({ error: 'Currency not found' });
305
+ }
306
+
307
+ const paymentMethod = await PaymentMethod.findByPk(paymentCurrency.payment_method_id);
308
+ if (!paymentMethod) {
309
+ return res.status(404).json({ error: 'Payment method not found' });
310
+ }
311
+
312
+ if (paymentMethod.type !== 'arcblock') {
313
+ return res.status(400).json({ error: `Payment method not supported: ${paymentMethod.type}` });
314
+ }
315
+
316
+ const paymentAddress = customer.did;
317
+ if (!paymentAddress) {
318
+ return res.status(400).json({ error: `Payment address not found for customer: ${customer.id}` });
319
+ }
320
+
321
+ const token = await getTokenByAddress(paymentAddress, paymentMethod, paymentCurrency);
322
+ return res.json({ token, paymentAddress });
323
+ } catch (err) {
324
+ logger.error('Error getting customer payer token', err);
325
+ return res.status(500).json({ error: err.message });
326
+ }
327
+ });
328
+
186
329
  router.get('/:id', auth, async (req, res) => {
187
330
  try {
188
331
  const doc = await Customer.findByPkOrDid(req.params.id as string);
@@ -180,6 +180,52 @@ router.get('/', authMine, async (req, res) => {
180
180
  }
181
181
  });
182
182
 
183
+ const rechargeSchema = createListParamSchema<{
184
+ status?: string;
185
+ customer_id?: string;
186
+ currency_id?: string;
187
+ }>({
188
+ status: Joi.string().empty(''),
189
+ customer_id: Joi.string().empty(''),
190
+ currency_id: Joi.string().empty(''),
191
+ });
192
+
193
+ router.get('/recharge', authMine, async (req, res) => {
194
+ const { page, pageSize, ...query } = await rechargeSchema.validateAsync(req.query, {
195
+ stripUnknown: false,
196
+ allowUnknown: true,
197
+ });
198
+ const where = getWhereFromKvQuery(query.q);
199
+ if (query.customer_id) {
200
+ where.customer_id = query.customer_id;
201
+ }
202
+ if (query.currency_id) {
203
+ where.currency_id = query.currency_id;
204
+ }
205
+
206
+ try {
207
+ const { rows: invoices, count } = await Invoice.findAndCountAll({
208
+ where: {
209
+ billing_reason: 'recharge',
210
+ paid: true,
211
+ ...where,
212
+ },
213
+ offset: (page - 1) * pageSize,
214
+ limit: pageSize,
215
+ order: [['created_at', 'DESC']],
216
+ include: [
217
+ { model: PaymentCurrency, as: 'paymentCurrency' },
218
+ { model: PaymentMethod, as: 'paymentMethod' },
219
+ ],
220
+ });
221
+
222
+ return res.json({ count, list: invoices, paging: { page, pageSize } });
223
+ } catch (err) {
224
+ logger.error(err);
225
+ return res.status(400).json({ error: err.message });
226
+ }
227
+ });
228
+
183
229
  const searchSchema = createListParamSchema<{}>({});
184
230
  router.get('/search', authMine, async (req, res) => {
185
231
  const { page, pageSize, livemode, q, o } = await searchSchema.validateAsync(req.query, {
@@ -148,7 +148,10 @@ router.get('/', auth, async (req, res) => {
148
148
  router.get('/vault-config', auth, async (req, res) => {
149
149
  const vaultAddress = await getVaultAddress();
150
150
  if (!vaultAddress) {
151
- return res.json([]);
151
+ return res.json({
152
+ list: [],
153
+ balances: {},
154
+ });
152
155
  }
153
156
  const chainTypes = resolveAddressChainTypes(vaultAddress);
154
157
  try {
@@ -189,7 +192,7 @@ router.get('/vault-config', auth, async (req, res) => {
189
192
  }
190
193
  } catch (err) {
191
194
  logger.error('get payment currency vault config failed', err);
192
- return res.status(400).json({ error: err.message });
195
+ return res.status(400).json({ error: err.message, list: [], balances: {} });
193
196
  }
194
197
  });
195
198
 
@@ -1972,9 +1972,6 @@ router.get('/:id/overdue/invoices', authPortal, async (req, res) => {
1972
1972
  });
1973
1973
 
1974
1974
  router.get('/:id/delegation', authPortal, async (req, res) => {
1975
- if (!req.user) {
1976
- return res.status(403).json({ error: 'Unauthorized' });
1977
- }
1978
1975
  try {
1979
1976
  const subscription = (await Subscription.findByPk(req.params.id, {
1980
1977
  include: [
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.14
17
+ version: 1.18.16
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.14",
3
+ "version": "1.18.16",
4
4
  "scripts": {
5
5
  "dev": "blocklet dev --open",
6
6
  "eject": "vite eject",
@@ -46,16 +46,16 @@
46
46
  "@abtnode/cron": "^1.16.39",
47
47
  "@arcblock/did": "^1.19.15",
48
48
  "@arcblock/did-auth-storage-nedb": "^1.7.1",
49
- "@arcblock/did-connect": "^2.12.12",
49
+ "@arcblock/did-connect": "^2.12.17",
50
50
  "@arcblock/did-util": "^1.19.15",
51
51
  "@arcblock/jwt": "^1.19.15",
52
- "@arcblock/ux": "^2.12.12",
52
+ "@arcblock/ux": "^2.12.17",
53
53
  "@arcblock/validator": "^1.19.15",
54
54
  "@blocklet/js-sdk": "^1.16.39",
55
55
  "@blocklet/logger": "^1.16.39",
56
- "@blocklet/payment-react": "1.18.14",
56
+ "@blocklet/payment-react": "1.18.16",
57
57
  "@blocklet/sdk": "^1.16.39",
58
- "@blocklet/ui-react": "^2.12.12",
58
+ "@blocklet/ui-react": "^2.12.17",
59
59
  "@blocklet/uploader": "^0.1.71",
60
60
  "@blocklet/xss": "^0.1.27",
61
61
  "@mui/icons-material": "^5.16.6",
@@ -121,7 +121,7 @@
121
121
  "devDependencies": {
122
122
  "@abtnode/types": "^1.16.39",
123
123
  "@arcblock/eslint-config-ts": "^0.3.3",
124
- "@blocklet/payment-types": "1.18.14",
124
+ "@blocklet/payment-types": "1.18.16",
125
125
  "@types/cookie-parser": "^1.4.7",
126
126
  "@types/cors": "^2.8.17",
127
127
  "@types/debug": "^4.1.12",
@@ -151,7 +151,7 @@
151
151
  "vite": "^5.3.5",
152
152
  "vite-node": "^2.0.4",
153
153
  "vite-plugin-babel-import": "^2.0.5",
154
- "vite-plugin-blocklet": "^0.9.23",
154
+ "vite-plugin-blocklet": "^0.9.24",
155
155
  "vite-plugin-node-polyfills": "^0.21.0",
156
156
  "vite-plugin-svgr": "^4.2.0",
157
157
  "vite-tsconfig-paths": "^4.3.2",
@@ -167,5 +167,5 @@
167
167
  "parser": "typescript"
168
168
  }
169
169
  },
170
- "gitHead": "84cf68d98fcf1df0655e90567b98d6bab38f300e"
170
+ "gitHead": "676b921c6fc399d3a031b848bfbe0fbffee430a1"
171
171
  }
package/src/app.tsx CHANGED
@@ -28,9 +28,10 @@ const CustomerSubscriptionDetail = React.lazy(() => import('./pages/customer/sub
28
28
  const CustomerSubscriptionEmbed = React.lazy(() => import('./pages/customer/subscription/embed'));
29
29
  const CustomerSubscriptionChangePlan = React.lazy(() => import('./pages/customer/subscription/change-plan'));
30
30
  const CustomerSubscriptionChangePayment = React.lazy(() => import('./pages/customer/subscription/change-payment'));
31
- const CustomerRecharge = React.lazy(() => import('./pages/customer/recharge'));
31
+ const CustomerRecharge = React.lazy(() => import('./pages/customer/recharge/subscription'));
32
32
  const CustomerPayoutDetail = React.lazy(() => import('./pages/customer/payout/detail'));
33
33
  const IntegrationsPage = React.lazy(() => import('./pages/integrations'));
34
+ const CustomerBalanceRecharge = React.lazy(() => import('./pages/customer/recharge/account'));
34
35
 
35
36
  // const theme = createTheme({
36
37
  // typography: {
@@ -108,14 +109,21 @@ function App() {
108
109
  </UserLayout>
109
110
  }
110
111
  />
112
+ <Route
113
+ key="customer-balance-recharge"
114
+ path="/customer/recharge/:currencyId"
115
+ element={
116
+ <UserLayout>
117
+ <CustomerBalanceRecharge />
118
+ </UserLayout>
119
+ }
120
+ />
111
121
  <Route key="customer-embed" path="/customer/embed/subscription" element={<CustomerSubscriptionEmbed />} />
112
- ,
113
122
  <Route
114
123
  key="subscription-embed"
115
124
  path="/embed/customer/subscription"
116
125
  element={<CustomerSubscriptionEmbed />}
117
126
  />
118
- ,
119
127
  <Route
120
128
  key="customer-due"
121
129
  path="/customer/invoice/past-due"
@@ -9,13 +9,14 @@ type Props = {
9
9
  size?: number;
10
10
  variant?: LiteralUnion<'square' | 'rounded' | 'circular', string>;
11
11
  sx?: SxProps;
12
+ className?: string;
12
13
  };
13
14
 
14
15
  export default function InfoCard(props: Props) {
15
16
  const dimensions = { width: props.size, height: props.size, ...props.sx };
16
17
  const avatarName = typeof props.name === 'string' ? props.name : props.logo;
17
18
  return (
18
- <Stack direction="row" alignItems="center" spacing={1}>
19
+ <Stack direction="row" alignItems="center" spacing={1} className={`info-card-wrapper ${props.className}`}>
19
20
  {props.logo ? (
20
21
  <Avatar src={props.logo} alt={avatarName} variant={props.variant as any} sx={dimensions} />
21
22
  ) : (
@@ -45,4 +46,5 @@ InfoCard.defaultProps = {
45
46
  size: 40,
46
47
  variant: 'rounded',
47
48
  sx: {},
49
+ className: '',
48
50
  };
@@ -45,6 +45,7 @@ export default function InfoRow(props: Props) {
45
45
  {props.showLabel && (
46
46
  <Box
47
47
  flex={sizes[0]}
48
+ className="info-row-label"
48
49
  color="text.primary"
49
50
  fontWeight={500}
50
51
  fontSize={14}
@@ -7,7 +7,6 @@ import {
7
7
  getInvoiceStatusColor,
8
8
  Table,
9
9
  useDefaultPageSize,
10
- getInvoiceDescriptionAndReason,
11
10
  getTxLink,
12
11
  formatToDate,
13
12
  } from '@blocklet/payment-react';
@@ -19,9 +18,12 @@ import { styled } from '@mui/system';
19
18
 
20
19
  const fetchData = (
21
20
  subscriptionId: string,
21
+ currencyId: string,
22
22
  params: Record<string, any> = {}
23
23
  ): Promise<{ list: TInvoiceExpanded[]; count: number }> => {
24
24
  const search = new URLSearchParams();
25
+
26
+ // 处理通用查询参数
25
27
  Object.keys(params).forEach((key) => {
26
28
  let v = params[key];
27
29
  if (key === 'q') {
@@ -32,7 +34,16 @@ const fetchData = (
32
34
  search.set(key, String(v));
33
35
  });
34
36
 
35
- return api.get(`/api/subscriptions/${subscriptionId}/recharge?${search.toString()}`).then((res) => res.data);
37
+ if (subscriptionId) {
38
+ return api.get(`/api/subscriptions/${subscriptionId}/recharge?${search.toString()}`).then((res) => res.data);
39
+ }
40
+
41
+ if (currencyId) {
42
+ search.set('currency_id', currencyId);
43
+ return api.get(`/api/invoices/recharge?${search.toString()}`).then((res) => res.data);
44
+ }
45
+
46
+ return Promise.resolve({ list: [], count: 0 });
36
47
  };
37
48
 
38
49
  type SearchProps = {
@@ -60,6 +71,9 @@ const getListKey = (props: ListProps) => {
60
71
  if (props.subscription_id) {
61
72
  return `subscription-recharge-${props.subscription_id}`;
62
73
  }
74
+ if (props.currency_id) {
75
+ return `currency-recharge-${props.currency_id}`;
76
+ }
63
77
  return 'invoices';
64
78
  };
65
79
 
@@ -80,27 +94,32 @@ export default function RechargeList({ currency_id, subscription_id, features }:
80
94
  const [search, setSearch] = useLocalStorageState<SearchProps>(listKey, {
81
95
  defaultValue: {
82
96
  currency_id,
97
+ subscription_id,
83
98
  pageSize: defaultPageSize,
84
99
  page: 1,
85
100
  },
86
101
  });
87
102
 
88
- const [data, setData] = useState({}) as any;
103
+ const [data, setData] = useState<{ list?: TInvoiceExpanded[]; count?: number }>({});
104
+ const [loading, setLoading] = useState(true);
89
105
 
90
- const refresh = () =>
91
- fetchData(subscription_id!, {
92
- ...search,
93
- }).then((res: any) => {
106
+ const refresh = async () => {
107
+ setLoading(true);
108
+ try {
109
+ const res = await fetchData(subscription_id || '', currency_id || '', {
110
+ ...search,
111
+ });
94
112
  setData(res);
95
- });
113
+ } catch (error) {
114
+ console.error('Failed to fetch recharge records:', error);
115
+ } finally {
116
+ setLoading(false);
117
+ }
118
+ };
96
119
 
97
120
  useEffect(() => {
98
121
  refresh();
99
- }, [search]);
100
-
101
- if (!data.list) {
102
- return <CircularProgress />;
103
- }
122
+ }, [search, currency_id, subscription_id]);
104
123
 
105
124
  const getInvoiceLink = (invoice: TInvoiceExpanded) => {
106
125
  return {
@@ -118,7 +137,9 @@ export default function RechargeList({ currency_id, subscription_id, features }:
118
137
  align: 'right',
119
138
  options: {
120
139
  customBodyRenderLite: (_: string, index: number) => {
121
- const invoice = data?.list[index] as TInvoiceExpanded;
140
+ const invoice = data?.list?.[index] as TInvoiceExpanded;
141
+ if (!invoice) return null;
142
+
122
143
  const link = getInvoiceLink(invoice);
123
144
  return (
124
145
  <a href={link.url} target="_blank" rel="noreferrer">
@@ -136,7 +157,9 @@ export default function RechargeList({ currency_id, subscription_id, features }:
136
157
  name: 'number',
137
158
  options: {
138
159
  customBodyRenderLite: (_: string, index: number) => {
139
- const invoice = data?.list[index] as TInvoiceExpanded;
160
+ const invoice = data?.list?.[index] as TInvoiceExpanded;
161
+ if (!invoice) return null;
162
+
140
163
  const link = getInvoiceLink(invoice);
141
164
  return (
142
165
  <a href={link.url} target="_blank" rel="noreferrer">
@@ -148,10 +171,12 @@ export default function RechargeList({ currency_id, subscription_id, features }:
148
171
  },
149
172
  {
150
173
  label: t('common.rechargeTime'),
151
- name: 'name',
174
+ name: 'created_at',
152
175
  options: {
153
176
  customBodyRenderLite: (_: string, index: number) => {
154
- const invoice = data?.list[index] as TInvoiceExpanded;
177
+ const invoice = data?.list?.[index] as TInvoiceExpanded;
178
+ if (!invoice) return null;
179
+
155
180
  const link = getInvoiceLink(invoice);
156
181
  return (
157
182
  <a href={link.url} target="_blank" rel="noreferrer">
@@ -161,28 +186,14 @@ export default function RechargeList({ currency_id, subscription_id, features }:
161
186
  },
162
187
  },
163
188
  },
164
- {
165
- label: t('common.description'),
166
- name: '',
167
- options: {
168
- sort: false,
169
- customBodyRenderLite: (_: string, index: number) => {
170
- const invoice = data?.list[index] as TInvoiceExpanded;
171
- const link = getInvoiceLink(invoice);
172
- return (
173
- <a href={link.url} target="_blank" rel="noreferrer">
174
- {getInvoiceDescriptionAndReason(invoice, locale)?.description || invoice.id}
175
- </a>
176
- );
177
- },
178
- },
179
- },
180
189
  {
181
190
  label: t('common.status'),
182
191
  name: 'status',
183
192
  options: {
184
193
  customBodyRenderLite: (_: string, index: number) => {
185
- const invoice = data?.list[index] as TInvoiceExpanded;
194
+ const invoice = data?.list?.[index] as TInvoiceExpanded;
195
+ if (!invoice) return null;
196
+
186
197
  const link = getInvoiceLink(invoice);
187
198
  return (
188
199
  <a href={link.url} target="_blank" rel="noreferrer">
@@ -196,34 +207,52 @@ export default function RechargeList({ currency_id, subscription_id, features }:
196
207
 
197
208
  const onTableChange = ({ page, rowsPerPage }: any) => {
198
209
  if (search!.pageSize !== rowsPerPage) {
199
- setSearch((x) => ({ ...x, pageSize: rowsPerPage, page: 1 }));
210
+ setSearch((x) => {
211
+ const newState = { ...x };
212
+ if (newState) {
213
+ newState.pageSize = rowsPerPage;
214
+ newState.page = 1;
215
+ }
216
+ return newState as SearchProps;
217
+ });
200
218
  } else if (search!.page !== page + 1) {
201
- // @ts-ignore
202
- setSearch((x) => ({ ...x, page: page + 1 }));
219
+ setSearch((x) => {
220
+ const newState = { ...x };
221
+ if (newState) {
222
+ newState.page = page + 1;
223
+ }
224
+ return newState as SearchProps;
225
+ });
203
226
  }
204
227
  };
205
228
 
206
229
  return (
207
230
  <InvoiceTableRoot>
208
- <Table
209
- hasRowLink
210
- data={data.list}
211
- durable={`__${listKey}__`}
212
- durableKeys={['searchText']}
213
- columns={columns}
214
- loading={!data.list}
215
- onChange={onTableChange}
216
- mobileTDFlexDirection="row"
217
- options={{
218
- count: data.count,
219
- page: search!.page - 1,
220
- rowsPerPage: search!.pageSize,
221
- }}
222
- toolbar={false}
223
- showMobile={false}
224
- footer={features?.footer}
225
- emptyNodeText={`${t('empty.invoices')}`}
226
- />
231
+ {loading ? (
232
+ <Box sx={{ display: 'flex', justifyContent: 'center', p: 3 }}>
233
+ <CircularProgress />
234
+ </Box>
235
+ ) : (
236
+ <Table
237
+ hasRowLink
238
+ data={data.list || []}
239
+ durable={`__${listKey}__`}
240
+ durableKeys={['searchText']}
241
+ columns={columns}
242
+ loading={loading}
243
+ onChange={onTableChange}
244
+ mobileTDFlexDirection="row"
245
+ options={{
246
+ count: data.count || 0,
247
+ page: search!.page - 1,
248
+ rowsPerPage: search!.pageSize,
249
+ }}
250
+ toolbar={false}
251
+ showMobile={false}
252
+ footer={features?.footer}
253
+ emptyNodeText={`${t('empty.invoices')}`}
254
+ />
255
+ )}
227
256
  </InvoiceTableRoot>
228
257
  );
229
258
  }
@@ -196,6 +196,12 @@ export default function InvoiceTable({ invoice, simple, emptyNodeText }: Props)
196
196
  name: 'amount',
197
197
  width: 200,
198
198
  align: 'right',
199
+ options: {
200
+ customBodyRenderLite: (_: string, index: number) => {
201
+ const item = detail[index] as InvoiceDetailItem;
202
+ return <Typography component="span">{invoice.status === 'void' ? '0' : item.amount}</Typography>;
203
+ },
204
+ },
199
205
  },
200
206
  ...(simple
201
207
  ? []
@@ -237,7 +243,7 @@ export default function InvoiceTable({ invoice, simple, emptyNodeText }: Props)
237
243
  mobileTDFlexDirection="row"
238
244
  emptyNodeText={emptyNodeText || t('payment.customer.invoice.emptyList')}
239
245
  />
240
- {!isEmpty(detail) && (
246
+ {!isEmpty(detail) && invoice.status !== 'void' && (
241
247
  <Stack
242
248
  className="invoice-summary"
243
249
  sx={{
@@ -521,7 +521,7 @@ export function SubscriptionActionsInner({
521
521
  setState({ batchPay: false });
522
522
  onChange?.('batch-pay');
523
523
  }}
524
- inSubscriptionDetail
524
+ detailLinkOptions={{ enabled: false }}
525
525
  dialogProps={{
526
526
  open: state.batchPay,
527
527
  onClose: () => setState({ batchPay: false }),