payment-kit 1.18.0 → 1.18.1
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/notification/template/customer-revenue-succeeded.ts +254 -0
- package/api/src/libs/notification/template/customer-reward-succeeded.ts +12 -11
- package/api/src/libs/payment.ts +47 -2
- package/api/src/libs/payout.ts +24 -0
- package/api/src/libs/util.ts +83 -1
- package/api/src/locales/en.ts +16 -1
- package/api/src/locales/zh.ts +28 -12
- package/api/src/queues/notification.ts +23 -1
- package/api/src/routes/invoices.ts +42 -5
- package/api/src/routes/payment-intents.ts +14 -1
- package/api/src/routes/payment-links.ts +17 -0
- package/api/src/routes/payouts.ts +103 -8
- package/api/src/store/migrations/20250206-update-donation-products.ts +56 -0
- package/api/src/store/models/payout.ts +6 -2
- package/api/src/store/models/types.ts +2 -0
- package/blocklet.yml +1 -1
- package/package.json +4 -4
- package/public/methods/default.png +0 -0
- package/src/app.tsx +10 -0
- package/src/components/customer/link.tsx +11 -2
- package/src/components/customer/overdraft-protection.tsx +2 -2
- package/src/components/info-card.tsx +6 -5
- package/src/components/invoice/table.tsx +4 -0
- package/src/components/payouts/list.tsx +17 -2
- package/src/components/payouts/portal/list.tsx +192 -0
- package/src/components/subscription/items/actions.tsx +1 -2
- package/src/libs/util.ts +42 -1
- package/src/locales/en.tsx +10 -0
- package/src/locales/zh.tsx +10 -0
- package/src/pages/admin/billing/invoices/detail.tsx +21 -0
- package/src/pages/admin/payments/payouts/detail.tsx +65 -4
- package/src/pages/customer/index.tsx +12 -25
- package/src/pages/customer/invoice/detail.tsx +27 -3
- package/src/pages/customer/payout/detail.tsx +264 -0
- package/src/pages/customer/recharge.tsx +2 -2
- package/vite.config.ts +1 -0
|
@@ -5,12 +5,13 @@ import Joi from 'joi';
|
|
|
5
5
|
import pick from 'lodash/pick';
|
|
6
6
|
import { Op } from 'sequelize';
|
|
7
7
|
|
|
8
|
+
import { BN } from '@ocap/util';
|
|
8
9
|
import { syncStripeInvoice } from '../integrations/stripe/handlers/invoice';
|
|
9
10
|
import { syncStripePayment } from '../integrations/stripe/handlers/payment-intent';
|
|
10
11
|
import { createListParamSchema, getWhereFromKvQuery, MetadataSchema } from '../libs/api';
|
|
11
12
|
import { authenticate } from '../libs/security';
|
|
12
13
|
import { expandLineItems } from '../libs/session';
|
|
13
|
-
import { formatMetadata } from '../libs/util';
|
|
14
|
+
import { formatMetadata, getBlockletJson, getUserOrAppInfo } from '../libs/util';
|
|
14
15
|
import { Customer } from '../store/models/customer';
|
|
15
16
|
import { Invoice } from '../store/models/invoice';
|
|
16
17
|
import { InvoiceItem } from '../store/models/invoice-item';
|
|
@@ -21,6 +22,7 @@ import { Price } from '../store/models/price';
|
|
|
21
22
|
import { Product } from '../store/models/product';
|
|
22
23
|
import { Subscription } from '../store/models/subscription';
|
|
23
24
|
import { getReturnStakeInvoices, getStakingInvoices } from '../libs/invoice';
|
|
25
|
+
import { CheckoutSession, PaymentLink, TInvoiceExpanded } from '../store/models';
|
|
24
26
|
|
|
25
27
|
const router = Router();
|
|
26
28
|
const authAdmin = authenticate<Subscription>({ component: true, roles: ['owner', 'admin'] });
|
|
@@ -206,7 +208,7 @@ router.get('/search', authMine, async (req, res) => {
|
|
|
206
208
|
|
|
207
209
|
router.get('/:id', authPortal, async (req, res) => {
|
|
208
210
|
try {
|
|
209
|
-
const doc = await Invoice.findOne({
|
|
211
|
+
const doc = (await Invoice.findOne({
|
|
210
212
|
where: { id: req.params.id },
|
|
211
213
|
include: [
|
|
212
214
|
{ model: PaymentCurrency, as: 'paymentCurrency' },
|
|
@@ -216,10 +218,11 @@ router.get('/:id', authPortal, async (req, res) => {
|
|
|
216
218
|
{ model: InvoiceItem, as: 'lines' },
|
|
217
219
|
{ model: Customer, as: 'customer' },
|
|
218
220
|
],
|
|
219
|
-
});
|
|
221
|
+
})) as TInvoiceExpanded | null;
|
|
220
222
|
|
|
221
223
|
if (doc) {
|
|
222
224
|
if (doc.metadata?.stripe_id && (doc.status !== 'paid' || req.query.forceSync)) {
|
|
225
|
+
// @ts-ignore
|
|
223
226
|
await syncStripeInvoice(doc);
|
|
224
227
|
}
|
|
225
228
|
if (doc.payment_intent_id) {
|
|
@@ -227,7 +230,37 @@ router.get('/:id', authPortal, async (req, res) => {
|
|
|
227
230
|
await syncStripePayment(paymentIntent!);
|
|
228
231
|
}
|
|
229
232
|
|
|
233
|
+
let checkoutSession = null;
|
|
234
|
+
let paymentLink = null;
|
|
235
|
+
if (doc.checkout_session_id) {
|
|
236
|
+
try {
|
|
237
|
+
checkoutSession = await CheckoutSession.findByPk(doc.checkout_session_id);
|
|
238
|
+
if (checkoutSession?.payment_link_id) {
|
|
239
|
+
paymentLink = await PaymentLink.findByPk(checkoutSession.payment_link_id);
|
|
240
|
+
}
|
|
241
|
+
} catch (err) {
|
|
242
|
+
console.error('Failed to get checkout session', err);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// @ts-ignore
|
|
230
246
|
const json = doc.toJSON();
|
|
247
|
+
if (doc.paymentIntent?.beneficiaries) {
|
|
248
|
+
const blockletJson = await getBlockletJson();
|
|
249
|
+
let total = new BN('0');
|
|
250
|
+
const promises = doc.paymentIntent.beneficiaries?.map((x: any) => {
|
|
251
|
+
total = total.add(new BN(x.share));
|
|
252
|
+
return getUserOrAppInfo(x.address, blockletJson);
|
|
253
|
+
});
|
|
254
|
+
const users = await Promise.all(promises);
|
|
255
|
+
json.paymentIntent.beneficiaries = json.paymentIntent.beneficiaries.map((x: any, i: number) => {
|
|
256
|
+
return {
|
|
257
|
+
...x,
|
|
258
|
+
...(users[i] || {}),
|
|
259
|
+
percent: (parseFloat(new BN(x.share).mul(new BN('10000')).div(total).toString()) / 100).toFixed(1),
|
|
260
|
+
amount: x.share,
|
|
261
|
+
};
|
|
262
|
+
});
|
|
263
|
+
}
|
|
231
264
|
const products = (await Product.findAll()).map((x) => x.toJSON());
|
|
232
265
|
const prices = (await Price.findAll()).map((x) => x.toJSON());
|
|
233
266
|
// @ts-ignore
|
|
@@ -236,9 +269,13 @@ router.get('/:id', authPortal, async (req, res) => {
|
|
|
236
269
|
const relatedInvoice = await Invoice.findByPk(doc.metadata.invoice_id, {
|
|
237
270
|
attributes: ['id', 'number', 'status', 'billing_reason'],
|
|
238
271
|
});
|
|
239
|
-
return res.json({ ...json, relatedInvoice });
|
|
272
|
+
return res.json({ ...json, relatedInvoice, paymentLink, checkoutSession });
|
|
240
273
|
}
|
|
241
|
-
return res.json(
|
|
274
|
+
return res.json({
|
|
275
|
+
...json,
|
|
276
|
+
paymentLink,
|
|
277
|
+
checkoutSession,
|
|
278
|
+
});
|
|
242
279
|
}
|
|
243
280
|
return res.status(404).json(null);
|
|
244
281
|
} catch (err) {
|
|
@@ -24,7 +24,7 @@ import { PaymentIntent } from '../store/models/payment-intent';
|
|
|
24
24
|
import { PaymentMethod } from '../store/models/payment-method';
|
|
25
25
|
import { Subscription } from '../store/models/subscription';
|
|
26
26
|
import logger from '../libs/logger';
|
|
27
|
-
import { Refund } from '../store/models';
|
|
27
|
+
import { Payout, Refund } from '../store/models';
|
|
28
28
|
import { getRefundAmountSetup } from '../libs/refund';
|
|
29
29
|
|
|
30
30
|
const router = Router();
|
|
@@ -352,6 +352,19 @@ router.get('/:id/refundable-amount', authPortal, async (req, res) => {
|
|
|
352
352
|
customerId: doc.customer_id,
|
|
353
353
|
subscriptionId: invoice?.subscription_id,
|
|
354
354
|
});
|
|
355
|
+
const payouts = await Payout.findAll({
|
|
356
|
+
where: {
|
|
357
|
+
payment_intent_id: doc.id,
|
|
358
|
+
},
|
|
359
|
+
attributes: ['id', 'amount'],
|
|
360
|
+
});
|
|
361
|
+
if (payouts.length > 0) {
|
|
362
|
+
let totalPayoutAmount = new BN('0');
|
|
363
|
+
payouts.forEach((payout) => {
|
|
364
|
+
totalPayoutAmount = totalPayoutAmount.add(new BN(payout.amount));
|
|
365
|
+
});
|
|
366
|
+
result.amount = result.amount.sub(totalPayoutAmount);
|
|
367
|
+
}
|
|
355
368
|
res.json(result);
|
|
356
369
|
} else {
|
|
357
370
|
res.status(404).json(null);
|
|
@@ -12,6 +12,7 @@ import { formatMetadata } from '../libs/util';
|
|
|
12
12
|
import { PaymentLink } from '../store/models/payment-link';
|
|
13
13
|
import { Price } from '../store/models/price';
|
|
14
14
|
import { Product } from '../store/models/product';
|
|
15
|
+
import { getDonationBenefits } from '../libs/payment';
|
|
15
16
|
|
|
16
17
|
const router = Router();
|
|
17
18
|
const auth = authenticate<PaymentLink>({ component: true, roles: ['owner', 'admin'] });
|
|
@@ -426,4 +427,20 @@ router.post('/stash', auth, async (req, res) => {
|
|
|
426
427
|
}
|
|
427
428
|
});
|
|
428
429
|
|
|
430
|
+
router.get('/:id/benefits', async (req, res) => {
|
|
431
|
+
try {
|
|
432
|
+
const doc = await PaymentLink.findByPk(req.params.id, {
|
|
433
|
+
attributes: ['id', 'donation_settings'],
|
|
434
|
+
});
|
|
435
|
+
if (!doc) {
|
|
436
|
+
return res.status(404).json({ error: 'payment link not found' });
|
|
437
|
+
}
|
|
438
|
+
const benefits = await getDonationBenefits(doc);
|
|
439
|
+
return res.json(benefits);
|
|
440
|
+
} catch (err) {
|
|
441
|
+
logger.error('Get donation benefits error', { error: err.message, stack: err.stack, id: req.params.id });
|
|
442
|
+
return res.status(400).json({ error: err.message });
|
|
443
|
+
}
|
|
444
|
+
});
|
|
445
|
+
|
|
429
446
|
export default router;
|
|
@@ -3,6 +3,7 @@ import { Router } from 'express';
|
|
|
3
3
|
import Joi from 'joi';
|
|
4
4
|
import pick from 'lodash/pick';
|
|
5
5
|
|
|
6
|
+
import sessionMiddleware from '@blocklet/sdk/lib/middlewares/session';
|
|
6
7
|
import { createListParamSchema, getWhereFromKvQuery, MetadataSchema } from '../libs/api';
|
|
7
8
|
import { authenticate } from '../libs/security';
|
|
8
9
|
import { formatMetadata } from '../libs/util';
|
|
@@ -11,6 +12,8 @@ import { PaymentCurrency } from '../store/models/payment-currency';
|
|
|
11
12
|
import { PaymentIntent } from '../store/models/payment-intent';
|
|
12
13
|
import { PaymentMethod } from '../store/models/payment-method';
|
|
13
14
|
import { Payout } from '../store/models/payout';
|
|
15
|
+
import { PaymentLink, TPaymentIntentExpanded } from '../store/models';
|
|
16
|
+
import { CheckoutSession } from '../store/models/checkout-session';
|
|
14
17
|
|
|
15
18
|
const router = Router();
|
|
16
19
|
const authAdmin = authenticate<Payout>({ component: true, roles: ['owner', 'admin'] });
|
|
@@ -104,9 +107,76 @@ router.get('/', authMine, async (req, res) => {
|
|
|
104
107
|
}
|
|
105
108
|
});
|
|
106
109
|
|
|
110
|
+
const mineRecordPaginationSchema = createListParamSchema<{
|
|
111
|
+
currency_id?: string;
|
|
112
|
+
status?: string;
|
|
113
|
+
customer_id?: string;
|
|
114
|
+
}>({
|
|
115
|
+
currency_id: Joi.string().empty(''),
|
|
116
|
+
status: Joi.string().empty(''),
|
|
117
|
+
});
|
|
118
|
+
router.get('/mine', sessionMiddleware(), async (req, res) => {
|
|
119
|
+
try {
|
|
120
|
+
const {
|
|
121
|
+
page,
|
|
122
|
+
pageSize,
|
|
123
|
+
currency_id: currencyId,
|
|
124
|
+
customer_id: customerId,
|
|
125
|
+
status,
|
|
126
|
+
} = await mineRecordPaginationSchema.validateAsync(req.query, {
|
|
127
|
+
stripUnknown: true,
|
|
128
|
+
allowUnknown: true,
|
|
129
|
+
});
|
|
130
|
+
if (!req.user) {
|
|
131
|
+
return res.status(403).json({ code: 'REQUIRE_LOGIN', error: 'Please login to continue' });
|
|
132
|
+
}
|
|
133
|
+
const customer = await Customer.findOne({ where: { did: customerId || req.user?.did } });
|
|
134
|
+
if (!customer) {
|
|
135
|
+
throw new Error(`Customer not found: ${req.user?.did}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const where: any = { customer_id: customer.id };
|
|
139
|
+
if (currencyId) {
|
|
140
|
+
where.currency_id = currencyId;
|
|
141
|
+
}
|
|
142
|
+
if (status) {
|
|
143
|
+
where.status = status;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const { rows: list, count } = await Payout.findAndCountAll({
|
|
147
|
+
where,
|
|
148
|
+
order: [['created_at', 'DESC']],
|
|
149
|
+
offset: (page - 1) * pageSize,
|
|
150
|
+
limit: pageSize,
|
|
151
|
+
include: [
|
|
152
|
+
{ model: PaymentCurrency, as: 'paymentCurrency' },
|
|
153
|
+
{ model: PaymentMethod, as: 'paymentMethod' },
|
|
154
|
+
{ model: Customer, as: 'customer' },
|
|
155
|
+
],
|
|
156
|
+
});
|
|
157
|
+
const json = list.map((x) => x.toJSON());
|
|
158
|
+
const result = await Promise.all(
|
|
159
|
+
json.map(async (x) => {
|
|
160
|
+
const paymentIntent = await PaymentIntent.findByPk(x.payment_intent_id, {
|
|
161
|
+
include: [{ model: Customer, as: 'customer' }],
|
|
162
|
+
});
|
|
163
|
+
if (paymentIntent) {
|
|
164
|
+
// @ts-ignore
|
|
165
|
+
x.paymentIntent = paymentIntent.toJSON();
|
|
166
|
+
}
|
|
167
|
+
return x;
|
|
168
|
+
})
|
|
169
|
+
);
|
|
170
|
+
return res.json({ count, list: result, paging: { page, pageSize } });
|
|
171
|
+
} catch (err) {
|
|
172
|
+
console.error(err);
|
|
173
|
+
return res.status(400).json({ error: err.message });
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
107
177
|
router.get('/:id', authPortal, async (req, res) => {
|
|
108
178
|
try {
|
|
109
|
-
const doc = await Payout.findOne({
|
|
179
|
+
const doc = (await Payout.findOne({
|
|
110
180
|
where: { id: req.params.id },
|
|
111
181
|
include: [
|
|
112
182
|
{ model: PaymentCurrency, as: 'paymentCurrency' },
|
|
@@ -114,16 +184,41 @@ router.get('/:id', authPortal, async (req, res) => {
|
|
|
114
184
|
{ model: PaymentIntent, as: 'paymentIntent' },
|
|
115
185
|
{ model: Customer, as: 'customer' },
|
|
116
186
|
],
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
187
|
+
})) as Payout & { paymentIntent: TPaymentIntentExpanded & { customer: Customer } };
|
|
188
|
+
if (!doc) {
|
|
189
|
+
return res.status(404).json(null);
|
|
190
|
+
}
|
|
191
|
+
let checkoutSession = null;
|
|
192
|
+
let paymentLink = null;
|
|
193
|
+
if (doc.payment_intent_id) {
|
|
194
|
+
try {
|
|
195
|
+
checkoutSession = (await CheckoutSession.findOne({
|
|
196
|
+
where: { payment_intent_id: doc.payment_intent_id },
|
|
197
|
+
})) as CheckoutSession & { paymentLink: PaymentLink };
|
|
198
|
+
if (checkoutSession && checkoutSession.payment_link_id) {
|
|
199
|
+
paymentLink = await PaymentLink.findByPk(checkoutSession.payment_link_id);
|
|
200
|
+
}
|
|
201
|
+
} catch (err) {
|
|
202
|
+
console.error(err);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const payout = doc.toJSON();
|
|
206
|
+
if (doc.paymentIntent && doc.paymentIntent.customer_id) {
|
|
207
|
+
const paymentCustomer = await Customer.findOne({ where: { id: doc.paymentIntent.customer_id } });
|
|
208
|
+
if (paymentCustomer) {
|
|
209
|
+
// @ts-ignore
|
|
210
|
+
payout.paymentIntent.customer = paymentCustomer.toJSON();
|
|
211
|
+
return res.json({
|
|
212
|
+
...payout,
|
|
213
|
+
checkoutSession,
|
|
214
|
+
paymentLink,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
123
217
|
}
|
|
218
|
+
return res.json(payout);
|
|
124
219
|
} catch (err) {
|
|
125
220
|
console.error(err);
|
|
126
|
-
res.status(500).json({ error: `Failed to get payout: ${err.message}` });
|
|
221
|
+
return res.status(500).json({ error: `Failed to get payout: ${err.message}` });
|
|
127
222
|
}
|
|
128
223
|
});
|
|
129
224
|
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { QueryTypes } from 'sequelize';
|
|
2
|
+
import { Migration } from '../migrate';
|
|
3
|
+
|
|
4
|
+
export const up: Migration = async ({ context }) => {
|
|
5
|
+
try {
|
|
6
|
+
// 查找 submit_type 为 donate 的 payment_links 记录
|
|
7
|
+
const donateLinks = await context.sequelize.query(
|
|
8
|
+
"SELECT line_items FROM payment_links WHERE submit_type = 'donate'",
|
|
9
|
+
{ type: QueryTypes.SELECT }
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
// 提取所有 price_id
|
|
13
|
+
const priceIds = (donateLinks as any[]).flatMap((link: any) => {
|
|
14
|
+
const lineItems = JSON.parse(link.line_items || '[]') || [];
|
|
15
|
+
return lineItems.map((item: any) => item.price_id);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const uniquePriceIds = Array.from(new Set(priceIds));
|
|
19
|
+
|
|
20
|
+
// 查询 prices 表并直接联接 products 表,过滤出符合条件的 product_id
|
|
21
|
+
const priceRecords = await context.sequelize.query(
|
|
22
|
+
`
|
|
23
|
+
SELECT p.id AS price_id, p.product_id
|
|
24
|
+
FROM prices p
|
|
25
|
+
JOIN products pr ON pr.id = p.product_id
|
|
26
|
+
WHERE p.id IN (:priceIds) AND pr.created_via != :createdVia
|
|
27
|
+
`,
|
|
28
|
+
{
|
|
29
|
+
type: QueryTypes.SELECT,
|
|
30
|
+
replacements: { priceIds: uniquePriceIds, createdVia: 'donation' },
|
|
31
|
+
}
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
if (!priceRecords || priceRecords.length === 0) {
|
|
35
|
+
// eslint-disable-next-line no-console
|
|
36
|
+
console.info('No price records found or no products to update, migration completed with no changes.');
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 提取 uniqueProductIds
|
|
41
|
+
const uniqueProductIds = Array.from(new Set(priceRecords.map((record: any) => record.product_id)));
|
|
42
|
+
|
|
43
|
+
// 更新 products 表的 created_via 字段
|
|
44
|
+
await context.sequelize.query("UPDATE products SET created_via = 'donation' WHERE id IN (:productIds)", {
|
|
45
|
+
replacements: { productIds: uniqueProductIds },
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// eslint-disable-next-line no-console
|
|
49
|
+
console.info('update donation products success:', uniqueProductIds);
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error('update donation products failed:', error);
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export const down: Migration = async () => {};
|
|
@@ -176,8 +176,12 @@ export class Payout extends Model<InferAttributes<Payout>, InferCreationAttribut
|
|
|
176
176
|
createdAt: 'created_at',
|
|
177
177
|
updatedAt: 'updated_at',
|
|
178
178
|
hooks: {
|
|
179
|
-
afterCreate: (model: Payout, options) =>
|
|
180
|
-
createEvent('Payout', 'payout.created', model, options).catch(console.error)
|
|
179
|
+
afterCreate: (model: Payout, options) => {
|
|
180
|
+
createEvent('Payout', 'payout.created', model, options).catch(console.error);
|
|
181
|
+
if (model.status === 'paid') {
|
|
182
|
+
createEvent('Payout', 'payout.paid', model, options).catch(console.error);
|
|
183
|
+
}
|
|
184
|
+
},
|
|
181
185
|
afterUpdate: (model: Payout, options) =>
|
|
182
186
|
createStatusEvent(
|
|
183
187
|
'Payout',
|
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.1",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "blocklet dev --open",
|
|
6
6
|
"eject": "vite eject",
|
|
@@ -53,7 +53,7 @@
|
|
|
53
53
|
"@arcblock/validator": "^1.19.3",
|
|
54
54
|
"@blocklet/js-sdk": "^1.16.37",
|
|
55
55
|
"@blocklet/logger": "^1.16.37",
|
|
56
|
-
"@blocklet/payment-react": "1.18.
|
|
56
|
+
"@blocklet/payment-react": "1.18.1",
|
|
57
57
|
"@blocklet/sdk": "^1.16.37",
|
|
58
58
|
"@blocklet/ui-react": "^2.11.27",
|
|
59
59
|
"@blocklet/uploader": "^0.1.64",
|
|
@@ -121,7 +121,7 @@
|
|
|
121
121
|
"devDependencies": {
|
|
122
122
|
"@abtnode/types": "^1.16.37",
|
|
123
123
|
"@arcblock/eslint-config-ts": "^0.3.3",
|
|
124
|
-
"@blocklet/payment-types": "1.18.
|
|
124
|
+
"@blocklet/payment-types": "1.18.1",
|
|
125
125
|
"@types/cookie-parser": "^1.4.7",
|
|
126
126
|
"@types/cors": "^2.8.17",
|
|
127
127
|
"@types/debug": "^4.1.12",
|
|
@@ -167,5 +167,5 @@
|
|
|
167
167
|
"parser": "typescript"
|
|
168
168
|
}
|
|
169
169
|
},
|
|
170
|
-
"gitHead": "
|
|
170
|
+
"gitHead": "22060b542145f7c62282ee0d3ec132ff581e13ec"
|
|
171
171
|
}
|
|
Binary file
|
package/src/app.tsx
CHANGED
|
@@ -29,6 +29,7 @@ const CustomerSubscriptionEmbed = React.lazy(() => import('./pages/customer/subs
|
|
|
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
31
|
const CustomerRecharge = React.lazy(() => import('./pages/customer/recharge'));
|
|
32
|
+
const CustomerPayoutDetail = React.lazy(() => import('./pages/customer/payout/detail'));
|
|
32
33
|
|
|
33
34
|
// const theme = createTheme({
|
|
34
35
|
// typography: {
|
|
@@ -133,6 +134,15 @@ function App() {
|
|
|
133
134
|
</Layout>
|
|
134
135
|
}
|
|
135
136
|
/>
|
|
137
|
+
<Route
|
|
138
|
+
key="customer-payout"
|
|
139
|
+
path="/customer/payout/:id"
|
|
140
|
+
element={
|
|
141
|
+
<Layout>
|
|
142
|
+
<CustomerPayoutDetail />
|
|
143
|
+
</Layout>
|
|
144
|
+
}
|
|
145
|
+
/>
|
|
136
146
|
<Route key="customer-fallback" path="/customer/*" element={<Navigate to="/customer" />} />,
|
|
137
147
|
<Route path="*" element={<Navigate to="/" />} />
|
|
138
148
|
</Routes>
|
|
@@ -4,13 +4,21 @@ import { Link } from 'react-router-dom';
|
|
|
4
4
|
import { getCustomerAvatar } from '@blocklet/payment-react';
|
|
5
5
|
import InfoCard from '../info-card';
|
|
6
6
|
|
|
7
|
-
export default function CustomerLink({
|
|
7
|
+
export default function CustomerLink({
|
|
8
|
+
customer,
|
|
9
|
+
linked,
|
|
10
|
+
linkTo,
|
|
11
|
+
}: {
|
|
12
|
+
customer: TCustomer;
|
|
13
|
+
linked?: boolean;
|
|
14
|
+
linkTo?: string;
|
|
15
|
+
}) {
|
|
8
16
|
if (!customer) {
|
|
9
17
|
return null;
|
|
10
18
|
}
|
|
11
19
|
if (linked) {
|
|
12
20
|
return (
|
|
13
|
-
<Link to={`/admin/customers/${customer.id}`}>
|
|
21
|
+
<Link to={linkTo || `/admin/customers/${customer.id}`}>
|
|
14
22
|
<InfoCard
|
|
15
23
|
logo={getCustomerAvatar(
|
|
16
24
|
customer?.did,
|
|
@@ -35,4 +43,5 @@ export default function CustomerLink({ customer, linked }: { customer: TCustomer
|
|
|
35
43
|
|
|
36
44
|
CustomerLink.defaultProps = {
|
|
37
45
|
linked: true,
|
|
46
|
+
linkTo: '',
|
|
38
47
|
};
|
|
@@ -398,7 +398,7 @@ export default function OverdraftProtectionDialog({
|
|
|
398
398
|
boxShadow: 3,
|
|
399
399
|
},
|
|
400
400
|
...(amount === presetAmount && !customAmount
|
|
401
|
-
? { borderColor: 'primary.main', borderWidth:
|
|
401
|
+
? { borderColor: 'primary.main', borderWidth: 1 }
|
|
402
402
|
: {}),
|
|
403
403
|
}}>
|
|
404
404
|
<CardActionArea
|
|
@@ -430,7 +430,7 @@ export default function OverdraftProtectionDialog({
|
|
|
430
430
|
transform: 'translateY(-4px)',
|
|
431
431
|
boxShadow: 3,
|
|
432
432
|
},
|
|
433
|
-
...(customAmount ? { borderColor: 'primary.main', borderWidth:
|
|
433
|
+
...(customAmount ? { borderColor: 'primary.main', borderWidth: 1 } : {}),
|
|
434
434
|
}}>
|
|
435
435
|
<CardActionArea onClick={handleCustomSelect} sx={{ height: '100%', p: 2 }}>
|
|
436
436
|
<Stack spacing={1} alignItems="center">
|
|
@@ -4,31 +4,32 @@ import type { LiteralUnion } from 'type-fest';
|
|
|
4
4
|
|
|
5
5
|
type Props = {
|
|
6
6
|
logo?: string;
|
|
7
|
-
name: string;
|
|
7
|
+
name: string | React.ReactNode;
|
|
8
8
|
description: any;
|
|
9
9
|
size?: number;
|
|
10
10
|
variant?: LiteralUnion<'square' | 'rounded' | 'circular', string>;
|
|
11
11
|
sx?: SxProps;
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
-
// FIXME: @wangshijun add image filter for logo
|
|
15
14
|
export default function InfoCard(props: Props) {
|
|
16
15
|
const dimensions = { width: props.size, height: props.size, ...props.sx };
|
|
16
|
+
const avatarName = typeof props.name === 'string' ? props.name : props.logo;
|
|
17
17
|
return (
|
|
18
18
|
<Stack direction="row" alignItems="center" spacing={1}>
|
|
19
19
|
{props.logo ? (
|
|
20
|
-
<Avatar src={props.logo} alt={
|
|
20
|
+
<Avatar src={props.logo} alt={avatarName} variant={props.variant as any} sx={dimensions} />
|
|
21
21
|
) : (
|
|
22
22
|
<Avatar variant={props.variant as any} sx={dimensions}>
|
|
23
|
-
{
|
|
23
|
+
{avatarName?.slice(0, 1)}
|
|
24
24
|
</Avatar>
|
|
25
25
|
)}
|
|
26
26
|
<Stack
|
|
27
27
|
direction="column"
|
|
28
28
|
alignItems="flex-start"
|
|
29
29
|
justifyContent="space-around"
|
|
30
|
+
className="info-card"
|
|
30
31
|
sx={{ wordBreak: getWordBreakStyle(props.name), minWidth: 140 }}>
|
|
31
|
-
<Typography variant="body1" color="text.primary">
|
|
32
|
+
<Typography variant="body1" color="text.primary" component="div">
|
|
32
33
|
{props.name}
|
|
33
34
|
</Typography>
|
|
34
35
|
<Typography variant="subtitle1" color="text.secondary">
|
|
@@ -27,6 +27,8 @@ type InvoiceDetailItem = {
|
|
|
27
27
|
price: string;
|
|
28
28
|
amount: string;
|
|
29
29
|
raw: TInvoiceItem;
|
|
30
|
+
price_id: string;
|
|
31
|
+
product_id: string;
|
|
30
32
|
};
|
|
31
33
|
|
|
32
34
|
type InvoiceSummaryItem = {
|
|
@@ -72,6 +74,8 @@ export function getInvoiceRows(invoice: TInvoiceExpanded) {
|
|
|
72
74
|
price: !line.proration ? formatAmount(price, invoice.paymentCurrency.decimal) : '',
|
|
73
75
|
amount: formatAmount(line.amount, invoice.paymentCurrency.decimal),
|
|
74
76
|
raw: line,
|
|
77
|
+
price_id: line.price.id,
|
|
78
|
+
product_id: line.price.product.id,
|
|
75
79
|
};
|
|
76
80
|
});
|
|
77
81
|
|
|
@@ -15,10 +15,11 @@ import { useLocalStorageState } from 'ahooks';
|
|
|
15
15
|
import { useEffect, useState } from 'react';
|
|
16
16
|
import { Link } from 'react-router-dom';
|
|
17
17
|
|
|
18
|
-
import { debounce } from '../../libs/util';
|
|
18
|
+
import { debounce, getAppInfo } from '../../libs/util';
|
|
19
19
|
import CustomerLink from '../customer/link';
|
|
20
20
|
import FilterToolbar from '../filter-toolbar';
|
|
21
21
|
import PayoutActions from './actions';
|
|
22
|
+
import InfoCard from '../info-card';
|
|
22
23
|
|
|
23
24
|
const fetchData = (params: Record<string, any> = {}): Promise<{ list: TPayoutExpanded[]; count: number }> => {
|
|
24
25
|
const search = new URLSearchParams();
|
|
@@ -194,7 +195,21 @@ export default function PayoutList({ customer_id, payment_intent_id, status, fea
|
|
|
194
195
|
filter: true,
|
|
195
196
|
customBodyRenderLite: (_: string, index: number) => {
|
|
196
197
|
const item = data.list[index] as TPayoutExpanded;
|
|
197
|
-
|
|
198
|
+
if (item.customer) {
|
|
199
|
+
return <CustomerLink customer={item.customer} />;
|
|
200
|
+
}
|
|
201
|
+
const appInfo = getAppInfo(item.destination);
|
|
202
|
+
if (appInfo) {
|
|
203
|
+
return (
|
|
204
|
+
<InfoCard
|
|
205
|
+
name={appInfo.name}
|
|
206
|
+
description={`${item.destination.slice(0, 6)}...${item.destination.slice(-6)}`}
|
|
207
|
+
logo={appInfo.avatar}
|
|
208
|
+
size={40}
|
|
209
|
+
/>
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
return item.destination;
|
|
198
213
|
},
|
|
199
214
|
} as any,
|
|
200
215
|
});
|