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.
- package/api/src/index.ts +2 -0
- package/api/src/libs/invoice.ts +5 -3
- package/api/src/routes/connect/change-payment.ts +9 -1
- package/api/src/routes/connect/change-plan.ts +9 -1
- package/api/src/routes/connect/collect-batch.ts +7 -5
- package/api/src/routes/connect/recharge-account.ts +124 -0
- package/api/src/routes/connect/setup.ts +8 -1
- package/api/src/routes/connect/shared.ts +110 -48
- package/api/src/routes/connect/subscribe.ts +11 -1
- package/api/src/routes/customers.ts +149 -6
- package/api/src/routes/invoices.ts +46 -0
- package/api/src/routes/payment-currencies.ts +5 -2
- package/api/src/routes/subscriptions.ts +0 -3
- package/blocklet.yml +1 -1
- package/package.json +8 -8
- package/src/app.tsx +11 -3
- package/src/components/info-card.tsx +3 -1
- package/src/components/info-row.tsx +1 -0
- package/src/components/invoice/recharge.tsx +85 -56
- package/src/components/invoice/table.tsx +7 -1
- package/src/components/subscription/portal/actions.tsx +1 -1
- package/src/components/subscription/portal/list.tsx +6 -0
- package/src/locales/en.tsx +9 -0
- package/src/locales/zh.tsx +9 -0
- package/src/pages/admin/settings/vault-config/index.tsx +21 -6
- package/src/pages/customer/index.tsx +160 -265
- package/src/pages/customer/invoice/detail.tsx +24 -16
- package/src/pages/customer/invoice/past-due.tsx +45 -23
- package/src/pages/customer/recharge/account.tsx +515 -0
- package/src/pages/customer/{recharge.tsx → recharge/subscription.tsx} +11 -11
- 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 {
|
|
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 {
|
|
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
|
|
170
|
-
|
|
171
|
-
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.18.
|
|
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.
|
|
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.
|
|
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.
|
|
56
|
+
"@blocklet/payment-react": "1.18.16",
|
|
57
57
|
"@blocklet/sdk": "^1.16.39",
|
|
58
|
-
"@blocklet/ui-react": "^2.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.
|
|
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.
|
|
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": "
|
|
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
|
};
|
|
@@ -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
|
-
|
|
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({})
|
|
103
|
+
const [data, setData] = useState<{ list?: TInvoiceExpanded[]; count?: number }>({});
|
|
104
|
+
const [loading, setLoading] = useState(true);
|
|
89
105
|
|
|
90
|
-
const refresh = () =>
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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: '
|
|
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) =>
|
|
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
|
-
|
|
202
|
-
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
|
|
524
|
+
detailLinkOptions={{ enabled: false }}
|
|
525
525
|
dialogProps={{
|
|
526
526
|
open: state.batchPay,
|
|
527
527
|
onClose: () => setState({ batchPay: false }),
|