payment-kit 1.23.0 → 1.23.2
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/libs/util.ts +34 -0
- package/api/src/routes/checkout-sessions.ts +93 -28
- package/api/src/routes/credit-grants.ts +8 -2
- package/api/src/routes/credit-transactions.ts +20 -3
- package/api/src/routes/meter-events.ts +4 -0
- package/api/src/routes/payment-links.ts +3 -1
- package/api/src/store/migrations/20251211-optimize-slow-queries.ts +33 -0
- package/api/src/store/models/index.ts +2 -1
- package/api/src/store/sequelize.ts +1 -0
- package/blocklet.yml +1 -1
- package/package.json +15 -15
- package/src/app.tsx +10 -0
- package/src/components/customer/credit-overview.tsx +19 -3
- package/src/locales/en.tsx +1 -1
- package/src/locales/zh.tsx +1 -1
- package/src/pages/admin/customers/customers/credit-grant/detail.tsx +13 -0
- package/src/pages/admin/customers/customers/credit-transaction/detail.tsx +324 -0
- package/src/pages/admin/customers/index.tsx +5 -0
- package/src/pages/customer/credit-grant/detail.tsx +14 -1
- package/src/pages/customer/credit-transaction/detail.tsx +289 -0
- package/src/pages/customer/invoice/detail.tsx +1 -1
- package/src/pages/customer/recharge/subscription.tsx +1 -1
- package/src/pages/customer/subscription/detail.tsx +1 -1
package/api/src/libs/util.ts
CHANGED
|
@@ -97,6 +97,40 @@ export function createCodeGenerator(prefix: string, size: number = 24) {
|
|
|
97
97
|
return prefix ? () => `${prefix}_${generator()}` : generator;
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
export function hasObjectChanged<T extends Record<string, any>>(
|
|
101
|
+
updates: Partial<T>,
|
|
102
|
+
original: T,
|
|
103
|
+
options?: {
|
|
104
|
+
deepCompare?: string[];
|
|
105
|
+
}
|
|
106
|
+
): boolean {
|
|
107
|
+
const deepFields = options?.deepCompare || [];
|
|
108
|
+
|
|
109
|
+
for (const [key, newValue] of Object.entries(updates)) {
|
|
110
|
+
if (newValue !== undefined) {
|
|
111
|
+
const oldValue = original[key];
|
|
112
|
+
|
|
113
|
+
if (deepFields.includes(key)) {
|
|
114
|
+
if (typeof newValue === 'object' && newValue !== null && typeof oldValue === 'object' && oldValue !== null) {
|
|
115
|
+
const newObj = newValue as Record<string, any>;
|
|
116
|
+
const oldObj = (oldValue || {}) as Record<string, any>;
|
|
117
|
+
for (const subKey of Object.keys(newObj)) {
|
|
118
|
+
if (newObj[subKey] !== oldObj[subKey]) {
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
} else if (newValue !== oldValue) {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
} else if (newValue !== oldValue) {
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
|
|
100
134
|
export function stringToStream(str: string): Readable {
|
|
101
135
|
const stream = new Readable();
|
|
102
136
|
stream.push(str);
|
|
@@ -54,6 +54,7 @@ import {
|
|
|
54
54
|
getConnectQueryParam,
|
|
55
55
|
getDataObjectFromQuery,
|
|
56
56
|
getUserOrAppInfo,
|
|
57
|
+
hasObjectChanged,
|
|
57
58
|
isUserInBlocklist,
|
|
58
59
|
} from '../libs/util';
|
|
59
60
|
import {
|
|
@@ -387,7 +388,13 @@ export async function calculateAndUpdateAmount(
|
|
|
387
388
|
|
|
388
389
|
logger.info('Amount calculated', {
|
|
389
390
|
checkoutSessionId: checkoutSession.id,
|
|
390
|
-
amount
|
|
391
|
+
amount: {
|
|
392
|
+
subtotal: amount.subtotal,
|
|
393
|
+
total: amount.total,
|
|
394
|
+
discount: amount.discount,
|
|
395
|
+
shipping: amount.shipping,
|
|
396
|
+
tax: amount.tax,
|
|
397
|
+
},
|
|
391
398
|
});
|
|
392
399
|
|
|
393
400
|
if (checkoutSession.mode === 'payment' && new BN(amount.total || '0').lt(new BN('0'))) {
|
|
@@ -1190,8 +1197,11 @@ export async function startCheckoutSessionFromPaymentLink(id: string, req: Reque
|
|
|
1190
1197
|
item.upsell_price_id = item.price.upsell.upsells_to_id;
|
|
1191
1198
|
}
|
|
1192
1199
|
});
|
|
1193
|
-
|
|
1194
|
-
await doc.update(
|
|
1200
|
+
const amounts = await getCheckoutSessionAmounts(doc);
|
|
1201
|
+
await doc.update({
|
|
1202
|
+
line_items: updatedItems,
|
|
1203
|
+
...amounts,
|
|
1204
|
+
});
|
|
1195
1205
|
doc.line_items = await Price.expand(updatedItems, { upsell: true });
|
|
1196
1206
|
}
|
|
1197
1207
|
|
|
@@ -1534,29 +1544,43 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
1534
1544
|
updates.invoice_prefix = Customer.getInvoicePrefix();
|
|
1535
1545
|
}
|
|
1536
1546
|
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
await blocklet.updateUserAddress(
|
|
1540
|
-
{
|
|
1541
|
-
did: customer.did,
|
|
1542
|
-
address: Customer.formatAddressFromCustomer(customer),
|
|
1543
|
-
// @ts-ignore
|
|
1544
|
-
phone: customer.phone,
|
|
1545
|
-
},
|
|
1546
|
-
{
|
|
1547
|
-
headers: {
|
|
1548
|
-
cookie: req.headers.cookie || '',
|
|
1549
|
-
},
|
|
1550
|
-
}
|
|
1551
|
-
);
|
|
1552
|
-
logger.info('updateUserAddress success', {
|
|
1547
|
+
if (!hasObjectChanged(updates, customer, { deepCompare: ['address'] })) {
|
|
1548
|
+
logger.info('customer update skipped (no changes)', {
|
|
1553
1549
|
did: customer.did,
|
|
1554
1550
|
});
|
|
1555
|
-
}
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1551
|
+
} else {
|
|
1552
|
+
await customer.update(updates);
|
|
1553
|
+
logger.info('customer updated', {
|
|
1554
|
+
did: customer.did,
|
|
1559
1555
|
});
|
|
1556
|
+
|
|
1557
|
+
try {
|
|
1558
|
+
// eslint-disable-next-line no-console
|
|
1559
|
+
console.time('updateUserAddress');
|
|
1560
|
+
await blocklet.updateUserAddress(
|
|
1561
|
+
{
|
|
1562
|
+
did: customer.did,
|
|
1563
|
+
address: Customer.formatAddressFromCustomer(customer),
|
|
1564
|
+
// @ts-ignore
|
|
1565
|
+
phone: customer.phone,
|
|
1566
|
+
},
|
|
1567
|
+
{
|
|
1568
|
+
headers: {
|
|
1569
|
+
cookie: req.headers.cookie || '',
|
|
1570
|
+
},
|
|
1571
|
+
}
|
|
1572
|
+
);
|
|
1573
|
+
// eslint-disable-next-line no-console
|
|
1574
|
+
console.timeEnd('updateUserAddress');
|
|
1575
|
+
logger.info('updateUserAddress success', {
|
|
1576
|
+
did: customer.did,
|
|
1577
|
+
});
|
|
1578
|
+
} catch (err) {
|
|
1579
|
+
logger.error('updateUserAddress failed', {
|
|
1580
|
+
error: err,
|
|
1581
|
+
customerId: customer.id,
|
|
1582
|
+
});
|
|
1583
|
+
}
|
|
1560
1584
|
}
|
|
1561
1585
|
}
|
|
1562
1586
|
|
|
@@ -3027,10 +3051,27 @@ router.delete('/:id/remove-promotion', user, ensureCheckoutSessionOpen, async (r
|
|
|
3027
3051
|
}
|
|
3028
3052
|
});
|
|
3029
3053
|
|
|
3054
|
+
const amountSchema = Joi.object({
|
|
3055
|
+
amount: Joi.string()
|
|
3056
|
+
.pattern(/^\d+(\.\d+)?$/)
|
|
3057
|
+
.required()
|
|
3058
|
+
.messages({
|
|
3059
|
+
'string.pattern.base': 'Amount must be a valid number',
|
|
3060
|
+
'any.required': 'Amount is required',
|
|
3061
|
+
}),
|
|
3062
|
+
priceId: Joi.string().required(),
|
|
3063
|
+
});
|
|
3030
3064
|
// change payment amount
|
|
3031
3065
|
router.put('/:id/amount', ensureCheckoutSessionOpen, async (req, res) => {
|
|
3032
3066
|
try {
|
|
3033
|
-
const {
|
|
3067
|
+
const { error, value } = amountSchema.validate(req.body, {
|
|
3068
|
+
stripUnknown: true,
|
|
3069
|
+
});
|
|
3070
|
+
if (error) {
|
|
3071
|
+
return res.status(400).json({ error: error.message });
|
|
3072
|
+
}
|
|
3073
|
+
|
|
3074
|
+
const { amount, priceId } = value;
|
|
3034
3075
|
const checkoutSession = req.doc as CheckoutSession;
|
|
3035
3076
|
const items = await Price.expand(checkoutSession.line_items);
|
|
3036
3077
|
const item = items.find((x) => x.price_id === priceId);
|
|
@@ -3090,7 +3131,7 @@ router.put('/:id/amount', ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
3090
3131
|
newItem.custom_amount = amount;
|
|
3091
3132
|
}
|
|
3092
3133
|
await checkoutSession.update({ line_items: newItems.map((x) => omit(x, ['price'])) as LineItem[] });
|
|
3093
|
-
logger.info('CheckoutSession updated on amount', { id: req.params.id,
|
|
3134
|
+
logger.info('CheckoutSession updated on amount', { id: req.params.id, amount, priceId });
|
|
3094
3135
|
|
|
3095
3136
|
// recalculate amount
|
|
3096
3137
|
await checkoutSession.update(await getCheckoutSessionAmounts(checkoutSession));
|
|
@@ -3185,11 +3226,35 @@ router.get('/', auth, async (req, res) => {
|
|
|
3185
3226
|
include: [],
|
|
3186
3227
|
});
|
|
3187
3228
|
|
|
3188
|
-
const condition = { where: { livemode: !!req.livemode } };
|
|
3189
|
-
const products = (await Product.findAll(condition)).map((x) => x.toJSON());
|
|
3190
|
-
const prices = (await Price.findAll(condition)).map((x) => x.toJSON());
|
|
3191
3229
|
const docs = list.map((x) => x.toJSON());
|
|
3192
3230
|
|
|
3231
|
+
const productIds = new Set<string>();
|
|
3232
|
+
const priceIds = new Set<string>();
|
|
3233
|
+
docs.forEach((x) => {
|
|
3234
|
+
x.line_items?.forEach((item: any) => {
|
|
3235
|
+
if (item.price_id) {
|
|
3236
|
+
priceIds.add(item.price_id);
|
|
3237
|
+
}
|
|
3238
|
+
if (item.product_id) {
|
|
3239
|
+
productIds.add(item.product_id);
|
|
3240
|
+
}
|
|
3241
|
+
});
|
|
3242
|
+
});
|
|
3243
|
+
|
|
3244
|
+
const condition = { where: { livemode: !!req.livemode } };
|
|
3245
|
+
const products =
|
|
3246
|
+
productIds.size > 0
|
|
3247
|
+
? (await Product.findAll({ ...condition, where: { ...condition.where, id: Array.from(productIds) } })).map(
|
|
3248
|
+
(x) => x.toJSON()
|
|
3249
|
+
)
|
|
3250
|
+
: [];
|
|
3251
|
+
const prices =
|
|
3252
|
+
priceIds.size > 0
|
|
3253
|
+
? (await Price.findAll({ ...condition, where: { ...condition.where, id: Array.from(priceIds) } })).map((x) =>
|
|
3254
|
+
x.toJSON()
|
|
3255
|
+
)
|
|
3256
|
+
: [];
|
|
3257
|
+
|
|
3193
3258
|
docs.forEach((x) => {
|
|
3194
3259
|
// @ts-ignore
|
|
3195
3260
|
expandLineItems(x.line_items, products, prices);
|
|
@@ -355,19 +355,25 @@ router.get('/verify-availability', authMine, async (req, res) => {
|
|
|
355
355
|
});
|
|
356
356
|
|
|
357
357
|
router.get('/:id', authPortal, async (req, res) => {
|
|
358
|
-
const creditGrant = await CreditGrant.findByPk(req.params.id, {
|
|
358
|
+
const creditGrant = (await CreditGrant.findByPk(req.params.id, {
|
|
359
359
|
include: [
|
|
360
360
|
{ model: Customer, as: 'customer' },
|
|
361
361
|
{ model: PaymentCurrency, as: 'paymentCurrency' },
|
|
362
362
|
],
|
|
363
|
-
});
|
|
363
|
+
})) as CreditGrant & { paymentCurrency?: PaymentCurrency };
|
|
364
364
|
if (!creditGrant) {
|
|
365
365
|
return res.status(404).json({ error: `Credit grant ${req.params.id} not found` });
|
|
366
366
|
}
|
|
367
|
+
|
|
368
|
+
let paymentMethod = null;
|
|
369
|
+
if (creditGrant.paymentCurrency) {
|
|
370
|
+
paymentMethod = await PaymentMethod.findByPk(creditGrant.paymentCurrency.payment_method_id);
|
|
371
|
+
}
|
|
367
372
|
const expandedPrices = await expandScopePrices(creditGrant);
|
|
368
373
|
return res.json({
|
|
369
374
|
...creditGrant.toJSON(),
|
|
370
375
|
items: expandedPrices,
|
|
376
|
+
paymentMethod,
|
|
371
377
|
});
|
|
372
378
|
});
|
|
373
379
|
|
|
@@ -14,6 +14,8 @@ import {
|
|
|
14
14
|
MeterEvent,
|
|
15
15
|
Subscription,
|
|
16
16
|
PaymentCurrency,
|
|
17
|
+
PaymentMethod,
|
|
18
|
+
TCreditTransactionExpanded,
|
|
17
19
|
} from '../store/models';
|
|
18
20
|
|
|
19
21
|
const router = Router();
|
|
@@ -303,7 +305,7 @@ router.get('/summary', authMine, async (req, res) => {
|
|
|
303
305
|
|
|
304
306
|
router.get('/:id', authPortal, async (req, res) => {
|
|
305
307
|
try {
|
|
306
|
-
const transaction = await CreditTransaction.findByPk(req.params.id, {
|
|
308
|
+
const transaction = (await CreditTransaction.findByPk(req.params.id, {
|
|
307
309
|
include: [
|
|
308
310
|
{
|
|
309
311
|
model: Customer,
|
|
@@ -339,14 +341,29 @@ router.get('/:id', authPortal, async (req, res) => {
|
|
|
339
341
|
],
|
|
340
342
|
required: false,
|
|
341
343
|
},
|
|
344
|
+
{
|
|
345
|
+
model: MeterEvent,
|
|
346
|
+
as: 'meterEvent',
|
|
347
|
+
attributes: ['id', 'source_data'],
|
|
348
|
+
required: false,
|
|
349
|
+
},
|
|
342
350
|
],
|
|
343
|
-
})
|
|
351
|
+
})) as CreditTransaction &
|
|
352
|
+
TCreditTransactionExpanded & { creditGrant?: CreditGrant & { paymentCurrency?: PaymentCurrency } };
|
|
344
353
|
|
|
345
354
|
if (!transaction) {
|
|
346
355
|
return res.status(404).json({ error: 'Credit transaction not found' });
|
|
347
356
|
}
|
|
348
357
|
|
|
349
|
-
|
|
358
|
+
let paymentMethod = null;
|
|
359
|
+
if (transaction.creditGrant?.paymentCurrency?.payment_method_id) {
|
|
360
|
+
paymentMethod = await PaymentMethod.findByPk(transaction.creditGrant.paymentCurrency.payment_method_id);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return res.json({
|
|
364
|
+
...transaction.toJSON(),
|
|
365
|
+
paymentMethod,
|
|
366
|
+
});
|
|
350
367
|
} catch (err) {
|
|
351
368
|
logger.error('get credit transaction failed', err);
|
|
352
369
|
return res.status(400).json({ error: err.message });
|
|
@@ -335,7 +335,11 @@ router.get('/pending-amount', authMine, async (req, res) => {
|
|
|
335
335
|
}
|
|
336
336
|
params.customerId = customer.id;
|
|
337
337
|
}
|
|
338
|
+
// eslint-disable-next-line no-console
|
|
339
|
+
console.time('pending-amount: getPendingAmounts');
|
|
338
340
|
const [summary] = await MeterEvent.getPendingAmounts(params);
|
|
341
|
+
// eslint-disable-next-line no-console
|
|
342
|
+
console.timeEnd('pending-amount: getPendingAmounts');
|
|
339
343
|
return res.json(summary);
|
|
340
344
|
} catch (err) {
|
|
341
345
|
logger.error('Error getting meter event pending amount', err);
|
|
@@ -260,10 +260,12 @@ router.get('/', auth, async (req, res) => {
|
|
|
260
260
|
const priceIds: string[] = list.reduce((acc: string[], x) => acc.concat(x.line_items.map((i) => i.price_id)), []);
|
|
261
261
|
const prices = await Price.findAll({ where: { id: priceIds }, include: [{ model: Product, as: 'product' }] });
|
|
262
262
|
|
|
263
|
+
const priceMap = new Map(prices.map((p) => [p.id, p]));
|
|
264
|
+
|
|
263
265
|
list.forEach((x) => {
|
|
264
266
|
x.line_items.forEach((i) => {
|
|
265
267
|
// @ts-ignore
|
|
266
|
-
i.price =
|
|
268
|
+
i.price = priceMap.get(i.price_id);
|
|
267
269
|
});
|
|
268
270
|
});
|
|
269
271
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
import { createIndexIfNotExists, type Migration } from '../migrate';
|
|
3
|
+
|
|
4
|
+
export const up: Migration = async ({ context }) => {
|
|
5
|
+
await createIndexIfNotExists(
|
|
6
|
+
context,
|
|
7
|
+
'meter_events',
|
|
8
|
+
['livemode', 'event_name', 'timestamp'],
|
|
9
|
+
'idx_meter_events_livemode_event_timestamp'
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
await createIndexIfNotExists(
|
|
13
|
+
context,
|
|
14
|
+
'payment_currencies',
|
|
15
|
+
['is_base_currency', 'livemode'],
|
|
16
|
+
'idx_payment_currencies_base_livemode'
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
await createIndexIfNotExists(context, 'jobs', ['queue', 'id'], 'idx_jobs_queue_id');
|
|
20
|
+
await createIndexIfNotExists(
|
|
21
|
+
context,
|
|
22
|
+
'jobs',
|
|
23
|
+
['queue', 'cancelled', 'will_run_at', 'delay'],
|
|
24
|
+
'idx_jobs_queue_cancelled_run_at_delay'
|
|
25
|
+
);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const down: Migration = async ({ context }) => {
|
|
29
|
+
await context.removeIndex('meter_events', 'idx_meter_events_livemode_event_timestamp');
|
|
30
|
+
await context.removeIndex('payment_currencies', 'idx_payment_currencies_base_livemode');
|
|
31
|
+
await context.removeIndex('jobs', 'idx_jobs_queue_id');
|
|
32
|
+
await context.removeIndex('jobs', 'idx_jobs_queue_cancelled_run_at_delay');
|
|
33
|
+
};
|
|
@@ -305,9 +305,10 @@ export type TCreditTransactionExpanded = TCreditTransaction & {
|
|
|
305
305
|
customer: TCustomer;
|
|
306
306
|
paymentCurrency: TPaymentCurrency;
|
|
307
307
|
paymentMethod?: TPaymentMethod;
|
|
308
|
-
creditGrant: TCreditGrant;
|
|
308
|
+
creditGrant: TCreditGrant & { paymentCurrency?: TPaymentCurrency };
|
|
309
309
|
meter: TMeter;
|
|
310
310
|
subscription: TSubscription;
|
|
311
|
+
meterEvent?: TMeterEvent;
|
|
311
312
|
};
|
|
312
313
|
|
|
313
314
|
export type TMeterEventExpanded = TMeterEvent & {
|
|
@@ -17,6 +17,7 @@ Sequelize.useCLS(namespace);
|
|
|
17
17
|
export const sequelize = new Sequelize({
|
|
18
18
|
dialect: 'sqlite',
|
|
19
19
|
logging: process.env.SQL_LOG === '1',
|
|
20
|
+
benchmark: process.env.SQL_LOG === '1' && process.env.SQL_BENCHMARK === '1',
|
|
20
21
|
storage: join(env.dataDir, 'payment-kit.db'),
|
|
21
22
|
pool: {
|
|
22
23
|
min: sequelizeOptionsPoolMin,
|
package/blocklet.yml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.23.
|
|
3
|
+
"version": "1.23.2",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "blocklet dev --open",
|
|
6
6
|
"lint": "tsc --noEmit && eslint src api/src --ext .mjs,.js,.jsx,.ts,.tsx",
|
|
@@ -44,25 +44,25 @@
|
|
|
44
44
|
]
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
-
"@abtnode/cron": "^1.17.
|
|
47
|
+
"@abtnode/cron": "^1.17.5",
|
|
48
48
|
"@arcblock/did": "^1.27.14",
|
|
49
|
-
"@arcblock/did-connect-react": "^3.2.
|
|
49
|
+
"@arcblock/did-connect-react": "^3.2.13",
|
|
50
50
|
"@arcblock/did-connect-storage-nedb": "^1.8.0",
|
|
51
51
|
"@arcblock/did-util": "^1.27.14",
|
|
52
52
|
"@arcblock/jwt": "^1.27.14",
|
|
53
|
-
"@arcblock/react-hooks": "^3.2.
|
|
54
|
-
"@arcblock/ux": "^3.2.
|
|
53
|
+
"@arcblock/react-hooks": "^3.2.13",
|
|
54
|
+
"@arcblock/ux": "^3.2.13",
|
|
55
55
|
"@arcblock/validator": "^1.27.14",
|
|
56
56
|
"@arcblock/vc": "^1.27.14",
|
|
57
57
|
"@blocklet/did-space-js": "^1.2.9",
|
|
58
58
|
"@blocklet/error": "^0.3.4",
|
|
59
|
-
"@blocklet/js-sdk": "^1.17.
|
|
60
|
-
"@blocklet/logger": "^1.17.
|
|
61
|
-
"@blocklet/payment-broker-client": "1.23.
|
|
62
|
-
"@blocklet/payment-react": "1.23.
|
|
63
|
-
"@blocklet/payment-vendor": "1.23.
|
|
64
|
-
"@blocklet/sdk": "^1.17.
|
|
65
|
-
"@blocklet/ui-react": "^3.2.
|
|
59
|
+
"@blocklet/js-sdk": "^1.17.5",
|
|
60
|
+
"@blocklet/logger": "^1.17.5",
|
|
61
|
+
"@blocklet/payment-broker-client": "1.23.2",
|
|
62
|
+
"@blocklet/payment-react": "1.23.2",
|
|
63
|
+
"@blocklet/payment-vendor": "1.23.2",
|
|
64
|
+
"@blocklet/sdk": "^1.17.5",
|
|
65
|
+
"@blocklet/ui-react": "^3.2.13",
|
|
66
66
|
"@blocklet/uploader": "^0.3.14",
|
|
67
67
|
"@blocklet/xss": "^0.3.12",
|
|
68
68
|
"@mui/icons-material": "^7.1.2",
|
|
@@ -128,9 +128,9 @@
|
|
|
128
128
|
"web3": "^4.16.0"
|
|
129
129
|
},
|
|
130
130
|
"devDependencies": {
|
|
131
|
-
"@abtnode/types": "^1.17.
|
|
131
|
+
"@abtnode/types": "^1.17.5",
|
|
132
132
|
"@arcblock/eslint-config-ts": "^0.3.3",
|
|
133
|
-
"@blocklet/payment-types": "1.23.
|
|
133
|
+
"@blocklet/payment-types": "1.23.2",
|
|
134
134
|
"@types/cookie-parser": "^1.4.9",
|
|
135
135
|
"@types/cors": "^2.8.19",
|
|
136
136
|
"@types/debug": "^4.1.12",
|
|
@@ -177,5 +177,5 @@
|
|
|
177
177
|
"parser": "typescript"
|
|
178
178
|
}
|
|
179
179
|
},
|
|
180
|
-
"gitHead": "
|
|
180
|
+
"gitHead": "facbd56b5c169d3999f007f74602a010e6dd0da2"
|
|
181
181
|
}
|
package/src/app.tsx
CHANGED
|
@@ -28,6 +28,7 @@ const CustomerSubscriptionEmbed = React.lazy(() => import('./pages/customer/subs
|
|
|
28
28
|
const CustomerSubscriptionChangePlan = React.lazy(() => import('./pages/customer/subscription/change-plan'));
|
|
29
29
|
const CustomerSubscriptionChangePayment = React.lazy(() => import('./pages/customer/subscription/change-payment'));
|
|
30
30
|
const CustomerCreditGrantDetail = React.lazy(() => import('./pages/customer/credit-grant/detail'));
|
|
31
|
+
const CustomerCreditTransactionDetail = React.lazy(() => import('./pages/customer/credit-transaction/detail'));
|
|
31
32
|
const CustomerRecharge = React.lazy(() => import('./pages/customer/recharge/subscription'));
|
|
32
33
|
const CustomerPayoutDetail = React.lazy(() => import('./pages/customer/payout/detail'));
|
|
33
34
|
const IntegrationsPage = React.lazy(() => import('./pages/integrations'));
|
|
@@ -161,6 +162,15 @@ function App() {
|
|
|
161
162
|
</UserLayout>
|
|
162
163
|
}
|
|
163
164
|
/>
|
|
165
|
+
<Route
|
|
166
|
+
key="customer-credit-transaction"
|
|
167
|
+
path="/customer/credit-transaction/:id"
|
|
168
|
+
element={
|
|
169
|
+
<UserLayout>
|
|
170
|
+
<CustomerCreditTransactionDetail />
|
|
171
|
+
</UserLayout>
|
|
172
|
+
}
|
|
173
|
+
/>
|
|
164
174
|
<Route key="customer-fallback" path="/customer/*" element={<Navigate to="/customer" />} />,
|
|
165
175
|
<Route path="*" element={<Navigate to="/" />} />
|
|
166
176
|
</Routes>
|
|
@@ -136,7 +136,7 @@ export default function CreditOverview({ customerId, settings, mode = 'portal' }
|
|
|
136
136
|
const grantData = creditSummary?.grants?.[currencyId];
|
|
137
137
|
const pendingAmount = creditSummary?.pendingAmount?.[currencyId] || '0';
|
|
138
138
|
|
|
139
|
-
if (!grantData) {
|
|
139
|
+
if (!grantData || grantData.meter?.status === 'inactive') {
|
|
140
140
|
return null;
|
|
141
141
|
}
|
|
142
142
|
|
|
@@ -145,6 +145,8 @@ export default function CreditOverview({ customerId, settings, mode = 'portal' }
|
|
|
145
145
|
const totalAmount = grantData.totalAmount || '0';
|
|
146
146
|
const remainingAmount = grantData.remainingAmount || '0';
|
|
147
147
|
|
|
148
|
+
const cardTitle = grantData.meter?.name || currency.name;
|
|
149
|
+
|
|
148
150
|
return (
|
|
149
151
|
<Card
|
|
150
152
|
key={currency.id}
|
|
@@ -173,7 +175,7 @@ export default function CreditOverview({ customerId, settings, mode = 'portal' }
|
|
|
173
175
|
pb: 2,
|
|
174
176
|
}}>
|
|
175
177
|
<Typography variant="h6" component="div">
|
|
176
|
-
{
|
|
178
|
+
{cardTitle}
|
|
177
179
|
</Typography>
|
|
178
180
|
{showRecharge && (
|
|
179
181
|
<SplitButton
|
|
@@ -217,7 +219,15 @@ export default function CreditOverview({ customerId, settings, mode = 'portal' }
|
|
|
217
219
|
{totalAmount === '0' && remainingAmount === '0' ? (
|
|
218
220
|
<>0 </>
|
|
219
221
|
) : (
|
|
220
|
-
<>
|
|
222
|
+
<>
|
|
223
|
+
{formatBNStr(remainingAmount, currency.decimal, 6, true)}
|
|
224
|
+
{currency.symbol !== cardTitle && (
|
|
225
|
+
<Typography variant="body2" component="span">
|
|
226
|
+
{' '}
|
|
227
|
+
{currency.symbol}
|
|
228
|
+
</Typography>
|
|
229
|
+
)}
|
|
230
|
+
</>
|
|
221
231
|
)}
|
|
222
232
|
</Typography>
|
|
223
233
|
</Box>
|
|
@@ -239,6 +249,12 @@ export default function CreditOverview({ customerId, settings, mode = 'portal' }
|
|
|
239
249
|
color: 'error.main',
|
|
240
250
|
}}>
|
|
241
251
|
{formatBNStr(pendingAmount, currency.decimal, 6, true)}
|
|
252
|
+
{currency.symbol !== cardTitle && (
|
|
253
|
+
<Typography variant="body2" component="span">
|
|
254
|
+
{' '}
|
|
255
|
+
{currency.symbol}
|
|
256
|
+
</Typography>
|
|
257
|
+
)}
|
|
242
258
|
</Typography>
|
|
243
259
|
</Box>
|
|
244
260
|
)}
|
package/src/locales/en.tsx
CHANGED
|
@@ -317,7 +317,7 @@ export default flat({
|
|
|
317
317
|
label: 'Meter name',
|
|
318
318
|
required: 'Meter name is required',
|
|
319
319
|
placeholder: 'API requests',
|
|
320
|
-
help:
|
|
320
|
+
help: "A descriptive name for this meter that will be displayed in your dashboard and user's personal bill.",
|
|
321
321
|
},
|
|
322
322
|
eventName: {
|
|
323
323
|
label: 'Event name',
|
package/src/locales/zh.tsx
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
CreditTransactionsList,
|
|
10
10
|
CreditStatusChip,
|
|
11
11
|
getCustomerAvatar,
|
|
12
|
+
TxLink,
|
|
12
13
|
} from '@blocklet/payment-react';
|
|
13
14
|
import type { TCreditGrantExpanded } from '@blocklet/payment-types';
|
|
14
15
|
import { ArrowBackOutlined } from '@mui/icons-material';
|
|
@@ -325,6 +326,18 @@ export default function AdminCreditGrantDetail({ id }: { id: string }) {
|
|
|
325
326
|
{data.expires_at && (
|
|
326
327
|
<InfoRow label={t('common.expirationDate')} value={formatTime(data.expires_at * 1000)} />
|
|
327
328
|
)}
|
|
329
|
+
{data.chain_detail?.mint?.hash && data.paymentMethod?.type === 'arcblock' && (
|
|
330
|
+
<InfoRow
|
|
331
|
+
label={t('common.mintTxHash')}
|
|
332
|
+
value={
|
|
333
|
+
<TxLink
|
|
334
|
+
details={{ arcblock: { tx_hash: data.chain_detail.mint.hash, payer: '' } }}
|
|
335
|
+
method={data.paymentMethod}
|
|
336
|
+
mode="dashboard"
|
|
337
|
+
/>
|
|
338
|
+
}
|
|
339
|
+
/>
|
|
340
|
+
)}
|
|
328
341
|
</InfoRowGroup>
|
|
329
342
|
</Box>
|
|
330
343
|
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
2
|
+
import { api, formatBNStr, formatError, getCustomerAvatar, TxLink, SourceDataViewer } from '@blocklet/payment-react';
|
|
3
|
+
import type { TCreditTransactionExpanded } from '@blocklet/payment-types';
|
|
4
|
+
import { ArrowBackOutlined } from '@mui/icons-material';
|
|
5
|
+
import { Alert, Avatar, Box, Button, Chip, CircularProgress, Divider, Stack, Typography } from '@mui/material';
|
|
6
|
+
import { useRequest } from 'ahooks';
|
|
7
|
+
import { styled } from '@mui/system';
|
|
8
|
+
import { useCallback } from 'react';
|
|
9
|
+
import { useNavigate } from 'react-router-dom';
|
|
10
|
+
|
|
11
|
+
import InfoMetric from '../../../../../components/info-metric';
|
|
12
|
+
import InfoRow from '../../../../../components/info-row';
|
|
13
|
+
import InfoRowGroup from '../../../../../components/info-row-group';
|
|
14
|
+
import Copyable from '../../../../../components/copyable';
|
|
15
|
+
import SectionHeader from '../../../../../components/section/header';
|
|
16
|
+
import MetadataList from '../../../../../components/metadata/list';
|
|
17
|
+
import { goBackOrFallback } from '../../../../../libs/util';
|
|
18
|
+
import EventList from '../../../../../components/event/list';
|
|
19
|
+
|
|
20
|
+
const fetchData = (id: string | undefined): Promise<TCreditTransactionExpanded> => {
|
|
21
|
+
return api.get(`/api/credit-transactions/${id}`).then((res) => res.data);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export default function AdminCreditTransactionDetail({ id }: { id: string }) {
|
|
25
|
+
const { t } = useLocaleContext();
|
|
26
|
+
const navigate = useNavigate();
|
|
27
|
+
|
|
28
|
+
const { loading, error, data } = useRequest(() => fetchData(id));
|
|
29
|
+
|
|
30
|
+
const handleBack = useCallback(() => {
|
|
31
|
+
goBackOrFallback('/admin/customers');
|
|
32
|
+
}, []);
|
|
33
|
+
|
|
34
|
+
if (error) {
|
|
35
|
+
return <Alert severity="error">{formatError(error)}</Alert>;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (loading || !data) {
|
|
39
|
+
return <CircularProgress />;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const getTransferStatusChip = (status: string | null | undefined) => {
|
|
43
|
+
if (!status) return null;
|
|
44
|
+
|
|
45
|
+
const statusConfig = {
|
|
46
|
+
pending: { label: t('common.pending'), color: 'warning' as const },
|
|
47
|
+
completed: { label: t('common.completed'), color: 'success' as const },
|
|
48
|
+
failed: { label: t('common.failed'), color: 'error' as const },
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const config = statusConfig[status as keyof typeof statusConfig] || {
|
|
52
|
+
label: status,
|
|
53
|
+
color: 'default' as const,
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
return <Chip label={config.label} size="small" color={config.color} />;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<Root direction="column" spacing={2.5} sx={{ mb: 4 }}>
|
|
61
|
+
<Box>
|
|
62
|
+
<Stack className="page-header" direction="row" justifyContent="space-between" alignItems="center">
|
|
63
|
+
<Stack
|
|
64
|
+
direction="row"
|
|
65
|
+
alignItems="center"
|
|
66
|
+
sx={{ fontWeight: 'normal', cursor: 'pointer' }}
|
|
67
|
+
onClick={handleBack}>
|
|
68
|
+
<ArrowBackOutlined fontSize="small" sx={{ mr: 0.5, color: 'text.secondary' }} />
|
|
69
|
+
<Typography variant="h6" sx={{ color: 'text.secondary', fontWeight: 'normal' }}>
|
|
70
|
+
{t('admin.creditTransactions.title')}
|
|
71
|
+
</Typography>
|
|
72
|
+
</Stack>
|
|
73
|
+
<Stack direction="row" spacing={1}>
|
|
74
|
+
{data.creditGrant && (
|
|
75
|
+
<Button
|
|
76
|
+
variant="outlined"
|
|
77
|
+
size="small"
|
|
78
|
+
onClick={() => navigate(`/admin/customers/${data.credit_grant_id}`)}>
|
|
79
|
+
{t('common.viewGrant')}
|
|
80
|
+
</Button>
|
|
81
|
+
)}
|
|
82
|
+
{data.subscription && (
|
|
83
|
+
<Button
|
|
84
|
+
variant="outlined"
|
|
85
|
+
size="small"
|
|
86
|
+
onClick={() => navigate(`/admin/billing/${data.subscription_id}`)}>
|
|
87
|
+
{t('common.viewSubscription')}
|
|
88
|
+
</Button>
|
|
89
|
+
)}
|
|
90
|
+
</Stack>
|
|
91
|
+
</Stack>
|
|
92
|
+
|
|
93
|
+
<Box
|
|
94
|
+
mt={4}
|
|
95
|
+
mb={3}
|
|
96
|
+
sx={{
|
|
97
|
+
display: 'flex',
|
|
98
|
+
gap: {
|
|
99
|
+
xs: 2,
|
|
100
|
+
sm: 2,
|
|
101
|
+
md: 5,
|
|
102
|
+
},
|
|
103
|
+
flexWrap: 'wrap',
|
|
104
|
+
flexDirection: {
|
|
105
|
+
xs: 'column',
|
|
106
|
+
sm: 'column',
|
|
107
|
+
md: 'row',
|
|
108
|
+
},
|
|
109
|
+
alignItems: {
|
|
110
|
+
xs: 'flex-start',
|
|
111
|
+
sm: 'flex-start',
|
|
112
|
+
md: 'center',
|
|
113
|
+
},
|
|
114
|
+
}}>
|
|
115
|
+
<Stack direction="row" justifyContent="space-between" alignItems="center">
|
|
116
|
+
<Stack direction="column" spacing={0.5}>
|
|
117
|
+
<Typography variant="h2" sx={{ fontWeight: 600 }}>
|
|
118
|
+
{data.description || t('common.creditTransaction')}
|
|
119
|
+
</Typography>
|
|
120
|
+
<Copyable text={data.id} />
|
|
121
|
+
</Stack>
|
|
122
|
+
</Stack>
|
|
123
|
+
|
|
124
|
+
<Stack
|
|
125
|
+
className="section-body"
|
|
126
|
+
justifyContent="flex-start"
|
|
127
|
+
flexWrap="wrap"
|
|
128
|
+
sx={{
|
|
129
|
+
'hr.MuiDivider-root:last-child': {
|
|
130
|
+
display: 'none',
|
|
131
|
+
},
|
|
132
|
+
flexDirection: {
|
|
133
|
+
xs: 'column',
|
|
134
|
+
sm: 'column',
|
|
135
|
+
md: 'row',
|
|
136
|
+
},
|
|
137
|
+
alignItems: 'flex-start',
|
|
138
|
+
gap: {
|
|
139
|
+
xs: 1,
|
|
140
|
+
sm: 1,
|
|
141
|
+
md: 3,
|
|
142
|
+
},
|
|
143
|
+
}}>
|
|
144
|
+
<InfoMetric
|
|
145
|
+
label={t('common.creditAmount')}
|
|
146
|
+
value={
|
|
147
|
+
<Stack direction="row" alignItems="center" spacing={0.5}>
|
|
148
|
+
<Typography variant="body2" sx={{ color: 'error.main' }}>
|
|
149
|
+
-{formatBNStr(data.credit_amount, data.creditGrant?.paymentCurrency?.decimal || 0)}{' '}
|
|
150
|
+
{data.creditGrant?.paymentCurrency?.symbol}
|
|
151
|
+
</Typography>
|
|
152
|
+
</Stack>
|
|
153
|
+
}
|
|
154
|
+
divider={!!data.transfer_status}
|
|
155
|
+
/>
|
|
156
|
+
{data.transfer_status && (
|
|
157
|
+
<InfoMetric label={t('common.transferStatus')} value={getTransferStatusChip(data.transfer_status)} />
|
|
158
|
+
)}
|
|
159
|
+
</Stack>
|
|
160
|
+
</Box>
|
|
161
|
+
<Divider />
|
|
162
|
+
</Box>
|
|
163
|
+
|
|
164
|
+
<Stack
|
|
165
|
+
sx={{
|
|
166
|
+
flexDirection: {
|
|
167
|
+
xs: 'column',
|
|
168
|
+
lg: 'row',
|
|
169
|
+
},
|
|
170
|
+
gap: {
|
|
171
|
+
xs: 2.5,
|
|
172
|
+
md: 4,
|
|
173
|
+
},
|
|
174
|
+
'.transaction-column-1': {
|
|
175
|
+
minWidth: {
|
|
176
|
+
xs: '100%',
|
|
177
|
+
lg: '600px',
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
'.transaction-column-2': {
|
|
181
|
+
width: {
|
|
182
|
+
xs: '100%',
|
|
183
|
+
md: '100%',
|
|
184
|
+
lg: '320px',
|
|
185
|
+
},
|
|
186
|
+
maxWidth: {
|
|
187
|
+
xs: '100%',
|
|
188
|
+
md: '33%',
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
}}>
|
|
192
|
+
<Box flex={1} className="transaction-column-1" sx={{ gap: 2.5, display: 'flex', flexDirection: 'column' }}>
|
|
193
|
+
<Box className="section" sx={{ containerType: 'inline-size' }}>
|
|
194
|
+
<SectionHeader title={t('admin.details')} />
|
|
195
|
+
<InfoRowGroup
|
|
196
|
+
sx={{
|
|
197
|
+
display: 'grid',
|
|
198
|
+
gridTemplateColumns: {
|
|
199
|
+
xs: 'repeat(1, 1fr)',
|
|
200
|
+
xl: 'repeat(2, 1fr)',
|
|
201
|
+
},
|
|
202
|
+
'@container (min-width: 1000px)': {
|
|
203
|
+
gridTemplateColumns: 'repeat(2, 1fr)',
|
|
204
|
+
},
|
|
205
|
+
'.info-row-wrapper': {
|
|
206
|
+
gap: 1,
|
|
207
|
+
flexDirection: {
|
|
208
|
+
xs: 'column',
|
|
209
|
+
xl: 'row',
|
|
210
|
+
},
|
|
211
|
+
alignItems: {
|
|
212
|
+
xs: 'flex-start',
|
|
213
|
+
xl: 'center',
|
|
214
|
+
},
|
|
215
|
+
'@container (min-width: 1000px)': {
|
|
216
|
+
flexDirection: 'row',
|
|
217
|
+
alignItems: 'center',
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
}}>
|
|
221
|
+
<InfoRow
|
|
222
|
+
label={t('common.customer')}
|
|
223
|
+
value={
|
|
224
|
+
<Stack direction="row" alignItems="center" spacing={1}>
|
|
225
|
+
<Avatar
|
|
226
|
+
src={getCustomerAvatar(
|
|
227
|
+
data.customer?.did,
|
|
228
|
+
data.customer?.updated_at ? new Date(data.customer.updated_at).toISOString() : '',
|
|
229
|
+
24
|
|
230
|
+
)}
|
|
231
|
+
alt={data.customer?.name}
|
|
232
|
+
sx={{ width: 24, height: 24 }}
|
|
233
|
+
/>
|
|
234
|
+
<Typography>{data.customer?.name}</Typography>
|
|
235
|
+
</Stack>
|
|
236
|
+
}
|
|
237
|
+
/>
|
|
238
|
+
<InfoRow
|
|
239
|
+
label={t('common.creditGrant')}
|
|
240
|
+
value={
|
|
241
|
+
<Typography
|
|
242
|
+
component="span"
|
|
243
|
+
onClick={() => navigate(`/admin/customers/${data.credit_grant_id}`)}
|
|
244
|
+
sx={{ color: 'text.link', cursor: 'pointer' }}>
|
|
245
|
+
{data.creditGrant?.name || data.credit_grant_id}
|
|
246
|
+
</Typography>
|
|
247
|
+
}
|
|
248
|
+
/>
|
|
249
|
+
{data.meter && (
|
|
250
|
+
<InfoRow
|
|
251
|
+
label={t('common.meterEvent')}
|
|
252
|
+
value={
|
|
253
|
+
<Typography
|
|
254
|
+
component="span"
|
|
255
|
+
onClick={() => navigate(`/admin/billing/${data.meter_id}`)}
|
|
256
|
+
sx={{ color: 'text.link', cursor: 'pointer' }}>
|
|
257
|
+
{data.meter.name || data.meter.event_name}
|
|
258
|
+
</Typography>
|
|
259
|
+
}
|
|
260
|
+
/>
|
|
261
|
+
)}
|
|
262
|
+
{data.subscription && (
|
|
263
|
+
<InfoRow
|
|
264
|
+
label={t('admin.subscription.name')}
|
|
265
|
+
value={
|
|
266
|
+
<Typography
|
|
267
|
+
component="span"
|
|
268
|
+
onClick={() => navigate(`/admin/billing/${data.subscription_id}`)}
|
|
269
|
+
sx={{ color: 'text.link', cursor: 'pointer' }}>
|
|
270
|
+
{data.subscription.description || data.subscription_id}
|
|
271
|
+
</Typography>
|
|
272
|
+
}
|
|
273
|
+
/>
|
|
274
|
+
)}
|
|
275
|
+
{data.transfer_hash && data.paymentMethod && (
|
|
276
|
+
<InfoRow
|
|
277
|
+
label={t('common.transferTxHash')}
|
|
278
|
+
value={
|
|
279
|
+
<TxLink
|
|
280
|
+
details={{ arcblock: { tx_hash: data.transfer_hash, payer: '' } }}
|
|
281
|
+
method={data.paymentMethod}
|
|
282
|
+
mode="dashboard"
|
|
283
|
+
/>
|
|
284
|
+
}
|
|
285
|
+
/>
|
|
286
|
+
)}
|
|
287
|
+
</InfoRowGroup>
|
|
288
|
+
</Box>
|
|
289
|
+
|
|
290
|
+
{data.meterEvent?.source_data && (
|
|
291
|
+
<>
|
|
292
|
+
<Divider />
|
|
293
|
+
<Box className="section">
|
|
294
|
+
<SectionHeader title={t('common.sourceData')} />
|
|
295
|
+
<Box className="section-body">
|
|
296
|
+
<SourceDataViewer data={data.meterEvent.source_data} />
|
|
297
|
+
</Box>
|
|
298
|
+
</Box>
|
|
299
|
+
</>
|
|
300
|
+
)}
|
|
301
|
+
|
|
302
|
+
<Divider />
|
|
303
|
+
<Box className="section">
|
|
304
|
+
<SectionHeader title={t('admin.events.title')} />
|
|
305
|
+
<Box className="section-body">
|
|
306
|
+
<EventList features={{ toolbar: false }} object_id={data.id} />
|
|
307
|
+
</Box>
|
|
308
|
+
</Box>
|
|
309
|
+
</Box>
|
|
310
|
+
|
|
311
|
+
<Box className="transaction-column-2" sx={{ gap: 2.5, display: 'flex', flexDirection: 'column' }}>
|
|
312
|
+
<Box className="section">
|
|
313
|
+
<SectionHeader title={t('common.metadata.label')} />
|
|
314
|
+
<Box className="section-body">
|
|
315
|
+
<MetadataList data={data.metadata} />
|
|
316
|
+
</Box>
|
|
317
|
+
</Box>
|
|
318
|
+
</Box>
|
|
319
|
+
</Stack>
|
|
320
|
+
</Root>
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const Root = styled(Stack)``;
|
|
@@ -7,6 +7,7 @@ import { useTransitionContext } from '../../../components/progress-bar';
|
|
|
7
7
|
|
|
8
8
|
const CustomerDetail = React.lazy(() => import('./customers/detail'));
|
|
9
9
|
const CreditGrantDetail = React.lazy(() => import('./customers/credit-grant/detail'));
|
|
10
|
+
const CreditTransactionDetail = React.lazy(() => import('./customers/credit-transaction/detail'));
|
|
10
11
|
|
|
11
12
|
const pages = {
|
|
12
13
|
overview: React.lazy(() => import('./customers')),
|
|
@@ -26,6 +27,10 @@ export default function CustomerIndex() {
|
|
|
26
27
|
return <CreditGrantDetail id={page} />;
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
if (page.startsWith('cbtxn_')) {
|
|
31
|
+
return <CreditTransactionDetail id={page} />;
|
|
32
|
+
}
|
|
33
|
+
|
|
29
34
|
const onTabChange = (newTab: string) => {
|
|
30
35
|
startTransition(() => {
|
|
31
36
|
navigate(`/admin/customers/${newTab}`);
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
CreditTransactionsList,
|
|
8
8
|
CreditStatusChip,
|
|
9
9
|
getCustomerAvatar,
|
|
10
|
+
TxLink,
|
|
10
11
|
} from '@blocklet/payment-react';
|
|
11
12
|
import type { TCreditGrantExpanded } from '@blocklet/payment-types';
|
|
12
13
|
import { ArrowBackOutlined } from '@mui/icons-material';
|
|
@@ -40,7 +41,7 @@ export default function CustomerCreditGrantDetail() {
|
|
|
40
41
|
}, [navigate]);
|
|
41
42
|
|
|
42
43
|
if (data?.customer?.did && session?.user?.did && data.customer.did !== session.user.did) {
|
|
43
|
-
return <Alert severity="error">
|
|
44
|
+
return <Alert severity="error">{t('common.accessDenied')}</Alert>;
|
|
44
45
|
}
|
|
45
46
|
if (error) {
|
|
46
47
|
return <Alert severity="error">{error.message}</Alert>;
|
|
@@ -271,6 +272,18 @@ export default function CustomerCreditGrantDetail() {
|
|
|
271
272
|
/>
|
|
272
273
|
<InfoRow label={t('admin.creditProduct.priority.label')} value={<Typography>{data.priority}</Typography>} />
|
|
273
274
|
<InfoRow label={t('common.createdAt')} value={formatTime(data.created_at)} />
|
|
275
|
+
{data.chain_detail?.mint?.hash && data.paymentMethod?.type === 'arcblock' && (
|
|
276
|
+
<InfoRow
|
|
277
|
+
label={t('common.mintTxHash')}
|
|
278
|
+
value={
|
|
279
|
+
<TxLink
|
|
280
|
+
details={{ arcblock: { tx_hash: data.chain_detail.mint.hash, payer: '' } }}
|
|
281
|
+
method={data.paymentMethod}
|
|
282
|
+
mode="customer"
|
|
283
|
+
/>
|
|
284
|
+
}
|
|
285
|
+
/>
|
|
286
|
+
)}
|
|
274
287
|
</InfoRowGroup>
|
|
275
288
|
</Box>
|
|
276
289
|
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
|
|
2
|
+
import { api, formatBNStr, formatTime, getCustomerAvatar, TxLink, SourceDataViewer } from '@blocklet/payment-react';
|
|
3
|
+
import type { TCreditTransactionExpanded } from '@blocklet/payment-types';
|
|
4
|
+
import { ArrowBackOutlined } from '@mui/icons-material';
|
|
5
|
+
import { Alert, Avatar, Box, Button, Chip, CircularProgress, Divider, Stack, Typography } from '@mui/material';
|
|
6
|
+
import { useRequest } from 'ahooks';
|
|
7
|
+
import { useNavigate, useParams } from 'react-router-dom';
|
|
8
|
+
import { styled } from '@mui/system';
|
|
9
|
+
import { useCallback } from 'react';
|
|
10
|
+
import InfoMetric from '../../../components/info-metric';
|
|
11
|
+
import { useSessionContext } from '../../../contexts/session';
|
|
12
|
+
import SectionHeader from '../../../components/section/header';
|
|
13
|
+
import InfoRow from '../../../components/info-row';
|
|
14
|
+
import InfoRowGroup from '../../../components/info-row-group';
|
|
15
|
+
import { useArcsphere } from '../../../hooks/browser';
|
|
16
|
+
|
|
17
|
+
const fetchData = (id: string | undefined): Promise<TCreditTransactionExpanded> => {
|
|
18
|
+
return api.get(`/api/credit-transactions/${id}`).then((res: any) => res.data);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export default function CustomerCreditTransactionDetail() {
|
|
22
|
+
const { id } = useParams() as { id: string };
|
|
23
|
+
const navigate = useNavigate();
|
|
24
|
+
const { t } = useLocaleContext();
|
|
25
|
+
const { session } = useSessionContext();
|
|
26
|
+
const inArcsphere = useArcsphere();
|
|
27
|
+
const { loading, error, data } = useRequest(() => fetchData(id));
|
|
28
|
+
|
|
29
|
+
const handleBack = useCallback(() => {
|
|
30
|
+
navigate('/customer', { replace: true });
|
|
31
|
+
}, [navigate]);
|
|
32
|
+
|
|
33
|
+
if (data?.customer?.did && session?.user?.did && data.customer.did !== session.user.did) {
|
|
34
|
+
return <Alert severity="error">{t('common.accessDenied')}</Alert>;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (error) {
|
|
38
|
+
return <Alert severity="error">{error.message}</Alert>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (loading || !data) {
|
|
42
|
+
return <CircularProgress />;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const getTransferStatusChip = (status: string | null | undefined) => {
|
|
46
|
+
if (!status) return null;
|
|
47
|
+
|
|
48
|
+
const statusConfig = {
|
|
49
|
+
pending: { label: t('common.pending'), color: 'warning' as const },
|
|
50
|
+
completed: { label: t('common.completed'), color: 'success' as const },
|
|
51
|
+
failed: { label: t('common.failed'), color: 'error' as const },
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const config = statusConfig[status as keyof typeof statusConfig] || {
|
|
55
|
+
label: status,
|
|
56
|
+
color: 'default' as const,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return <Chip label={config.label} size="small" color={config.color} />;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<Root>
|
|
64
|
+
<Box>
|
|
65
|
+
<Stack
|
|
66
|
+
className="page-header"
|
|
67
|
+
direction="row"
|
|
68
|
+
justifyContent="space-between"
|
|
69
|
+
alignItems="center"
|
|
70
|
+
sx={{ position: 'relative' }}>
|
|
71
|
+
{!inArcsphere ? (
|
|
72
|
+
<Stack
|
|
73
|
+
direction="row"
|
|
74
|
+
onClick={handleBack}
|
|
75
|
+
alignItems="center"
|
|
76
|
+
sx={{ fontWeight: 'normal', cursor: 'pointer' }}>
|
|
77
|
+
<ArrowBackOutlined fontSize="small" sx={{ mr: 0.5, color: 'text.secondary' }} />
|
|
78
|
+
<Typography variant="h6" sx={{ color: 'text.secondary', fontWeight: 'normal' }}>
|
|
79
|
+
{t('admin.creditTransactions.title')}
|
|
80
|
+
</Typography>
|
|
81
|
+
</Stack>
|
|
82
|
+
) : (
|
|
83
|
+
<Box />
|
|
84
|
+
)}
|
|
85
|
+
<Stack direction="row" spacing={1}>
|
|
86
|
+
{data.creditGrant && (
|
|
87
|
+
<Button
|
|
88
|
+
variant="outlined"
|
|
89
|
+
size="small"
|
|
90
|
+
onClick={() => navigate(`/customer/credit-grant/${data.credit_grant_id}`)}>
|
|
91
|
+
{t('common.viewGrant')}
|
|
92
|
+
</Button>
|
|
93
|
+
)}
|
|
94
|
+
{data.subscription && (
|
|
95
|
+
<Button
|
|
96
|
+
variant="outlined"
|
|
97
|
+
size="small"
|
|
98
|
+
onClick={() => navigate(`/customer/subscription/${data.subscription_id}`)}>
|
|
99
|
+
{t('common.viewSubscription')}
|
|
100
|
+
</Button>
|
|
101
|
+
)}
|
|
102
|
+
</Stack>
|
|
103
|
+
</Stack>
|
|
104
|
+
|
|
105
|
+
<Box
|
|
106
|
+
mt={4}
|
|
107
|
+
sx={{
|
|
108
|
+
display: 'flex',
|
|
109
|
+
gap: {
|
|
110
|
+
xs: 2,
|
|
111
|
+
sm: 2,
|
|
112
|
+
md: 5,
|
|
113
|
+
},
|
|
114
|
+
flexWrap: 'wrap',
|
|
115
|
+
flexDirection: {
|
|
116
|
+
xs: 'column',
|
|
117
|
+
sm: 'column',
|
|
118
|
+
md: 'row',
|
|
119
|
+
},
|
|
120
|
+
alignItems: {
|
|
121
|
+
xs: 'flex-start',
|
|
122
|
+
sm: 'flex-start',
|
|
123
|
+
md: 'center',
|
|
124
|
+
},
|
|
125
|
+
}}>
|
|
126
|
+
<Stack direction="row" justifyContent="space-between" alignItems="center">
|
|
127
|
+
<Stack direction="column" alignItems="flex-start" justifyContent="space-around">
|
|
128
|
+
<Typography variant="h2" color="text.primary">
|
|
129
|
+
{data.description || t('common.creditTransaction')}
|
|
130
|
+
</Typography>
|
|
131
|
+
<Typography variant="body2" color="text.secondary" sx={{ mt: 0.5 }}>
|
|
132
|
+
{data.id}
|
|
133
|
+
</Typography>
|
|
134
|
+
</Stack>
|
|
135
|
+
</Stack>
|
|
136
|
+
|
|
137
|
+
<Stack
|
|
138
|
+
className="section-body"
|
|
139
|
+
justifyContent="flex-start"
|
|
140
|
+
flexWrap="wrap"
|
|
141
|
+
sx={{
|
|
142
|
+
'hr.MuiDivider-root:last-child': {
|
|
143
|
+
display: 'none',
|
|
144
|
+
},
|
|
145
|
+
flexDirection: {
|
|
146
|
+
xs: 'column',
|
|
147
|
+
sm: 'column',
|
|
148
|
+
md: 'row',
|
|
149
|
+
},
|
|
150
|
+
alignItems: 'flex-start',
|
|
151
|
+
gap: {
|
|
152
|
+
xs: 1,
|
|
153
|
+
sm: 1,
|
|
154
|
+
md: 3,
|
|
155
|
+
},
|
|
156
|
+
}}>
|
|
157
|
+
<InfoMetric
|
|
158
|
+
label={t('common.creditAmount')}
|
|
159
|
+
value={
|
|
160
|
+
<Stack direction="row" alignItems="center" spacing={0.5}>
|
|
161
|
+
<Typography variant="body2" sx={{ color: 'error.main' }}>
|
|
162
|
+
-{formatBNStr(data.credit_amount, data.creditGrant?.paymentCurrency?.decimal || 0)}{' '}
|
|
163
|
+
{data.creditGrant?.paymentCurrency?.symbol}
|
|
164
|
+
</Typography>
|
|
165
|
+
</Stack>
|
|
166
|
+
}
|
|
167
|
+
divider
|
|
168
|
+
/>
|
|
169
|
+
{data.transfer_status && (
|
|
170
|
+
<InfoMetric
|
|
171
|
+
label={t('common.transferStatus')}
|
|
172
|
+
value={getTransferStatusChip(data.transfer_status)}
|
|
173
|
+
divider
|
|
174
|
+
/>
|
|
175
|
+
)}
|
|
176
|
+
<InfoMetric label={t('common.createdAt')} value={formatTime(data.created_at)} />
|
|
177
|
+
</Stack>
|
|
178
|
+
</Box>
|
|
179
|
+
</Box>
|
|
180
|
+
|
|
181
|
+
<Box className="section" sx={{ containerType: 'inline-size' }}>
|
|
182
|
+
<SectionHeader title={t('admin.details')} />
|
|
183
|
+
<InfoRowGroup
|
|
184
|
+
sx={{
|
|
185
|
+
display: 'grid',
|
|
186
|
+
gridTemplateColumns: {
|
|
187
|
+
xs: 'repeat(1, 1fr)',
|
|
188
|
+
xl: 'repeat(2, 1fr)',
|
|
189
|
+
},
|
|
190
|
+
'@container (min-width: 1000px)': {
|
|
191
|
+
gridTemplateColumns: 'repeat(2, 1fr)',
|
|
192
|
+
},
|
|
193
|
+
'.info-row-wrapper': {
|
|
194
|
+
gap: 1,
|
|
195
|
+
flexDirection: {
|
|
196
|
+
xs: 'column',
|
|
197
|
+
xl: 'row',
|
|
198
|
+
},
|
|
199
|
+
alignItems: {
|
|
200
|
+
xs: 'flex-start',
|
|
201
|
+
xl: 'center',
|
|
202
|
+
},
|
|
203
|
+
'@container (min-width: 1000px)': {
|
|
204
|
+
flexDirection: 'row',
|
|
205
|
+
alignItems: 'center',
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
}}>
|
|
209
|
+
<InfoRow
|
|
210
|
+
label={t('common.customer')}
|
|
211
|
+
value={
|
|
212
|
+
<Stack direction="row" alignItems="center" spacing={1}>
|
|
213
|
+
<Avatar
|
|
214
|
+
src={getCustomerAvatar(
|
|
215
|
+
data.customer?.did,
|
|
216
|
+
data.customer?.updated_at ? new Date(data.customer.updated_at).toISOString() : '',
|
|
217
|
+
24
|
|
218
|
+
)}
|
|
219
|
+
alt={data.customer?.name}
|
|
220
|
+
sx={{ width: 24, height: 24 }}
|
|
221
|
+
/>
|
|
222
|
+
<Typography>{data.customer?.name}</Typography>
|
|
223
|
+
</Stack>
|
|
224
|
+
}
|
|
225
|
+
/>
|
|
226
|
+
<InfoRow
|
|
227
|
+
label={t('common.creditGrant')}
|
|
228
|
+
value={
|
|
229
|
+
<Typography
|
|
230
|
+
component="span"
|
|
231
|
+
onClick={() => navigate(`/customer/credit-grant/${data.credit_grant_id}`)}
|
|
232
|
+
sx={{ color: 'text.link', cursor: 'pointer' }}>
|
|
233
|
+
{data.creditGrant?.name || data.credit_grant_id}
|
|
234
|
+
</Typography>
|
|
235
|
+
}
|
|
236
|
+
/>
|
|
237
|
+
{data.subscription && (
|
|
238
|
+
<InfoRow
|
|
239
|
+
label={t('admin.subscription.name')}
|
|
240
|
+
value={
|
|
241
|
+
<Typography
|
|
242
|
+
component="span"
|
|
243
|
+
onClick={() => navigate(`/customer/subscription/${data.subscription_id}`)}
|
|
244
|
+
sx={{ color: 'text.link', cursor: 'pointer' }}>
|
|
245
|
+
{data.subscription.description || data.subscription_id}
|
|
246
|
+
</Typography>
|
|
247
|
+
}
|
|
248
|
+
/>
|
|
249
|
+
)}
|
|
250
|
+
{data.transfer_hash && data.paymentMethod && (
|
|
251
|
+
<InfoRow
|
|
252
|
+
label={t('common.transferTxHash')}
|
|
253
|
+
value={
|
|
254
|
+
<TxLink
|
|
255
|
+
details={{ arcblock: { tx_hash: data.transfer_hash, payer: '' } }}
|
|
256
|
+
method={data.paymentMethod}
|
|
257
|
+
mode="customer"
|
|
258
|
+
/>
|
|
259
|
+
}
|
|
260
|
+
/>
|
|
261
|
+
)}
|
|
262
|
+
</InfoRowGroup>
|
|
263
|
+
</Box>
|
|
264
|
+
|
|
265
|
+
{data.meterEvent?.source_data && (
|
|
266
|
+
<>
|
|
267
|
+
<Divider />
|
|
268
|
+
<Box className="section">
|
|
269
|
+
<SectionHeader title={t('common.sourceData')} />
|
|
270
|
+
<Box className="section-body">
|
|
271
|
+
<SourceDataViewer data={data.meterEvent.source_data} />
|
|
272
|
+
</Box>
|
|
273
|
+
</Box>
|
|
274
|
+
</>
|
|
275
|
+
)}
|
|
276
|
+
</Root>
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const Root = styled(Stack)`
|
|
281
|
+
margin-bottom: 24px;
|
|
282
|
+
gap: 24px;
|
|
283
|
+
flex-direction: column;
|
|
284
|
+
@media (max-width: ${({ theme }) => theme.breakpoints.values.md}px) {
|
|
285
|
+
.section-header {
|
|
286
|
+
font-size: 18px;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
`;
|
|
@@ -122,7 +122,7 @@ export default function CustomerInvoiceDetail() {
|
|
|
122
122
|
}, [error]);
|
|
123
123
|
|
|
124
124
|
if (data?.customer?.did && session?.user?.did && data.customer.did !== session.user.did) {
|
|
125
|
-
return <Alert severity="error">
|
|
125
|
+
return <Alert severity="error">{t('common.accessDenied')}</Alert>;
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
if (error) {
|
|
@@ -228,7 +228,7 @@ export default function RechargePage() {
|
|
|
228
228
|
}
|
|
229
229
|
|
|
230
230
|
if (subscription?.customer?.did && session?.user?.did && subscription.customer.did !== session.user.did) {
|
|
231
|
-
return <Alert severity="error">
|
|
231
|
+
return <Alert severity="error">{t('common.accessDenied')}</Alert>;
|
|
232
232
|
}
|
|
233
233
|
const currentBalance = formatBNStr(payerValue?.token || '0', paymentCurrency?.decimal, 6, false);
|
|
234
234
|
|
|
@@ -119,7 +119,7 @@ export default function CustomerSubscriptionDetail() {
|
|
|
119
119
|
}, []);
|
|
120
120
|
|
|
121
121
|
if (data?.customer?.did && session?.user?.did && data.customer.did !== session.user.did) {
|
|
122
|
-
return <Alert severity="error">
|
|
122
|
+
return <Alert severity="error">{t('common.accessDenied')}</Alert>;
|
|
123
123
|
}
|
|
124
124
|
if (error) {
|
|
125
125
|
return <Alert severity="error">{error.message}</Alert>;
|