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.
Files changed (36) hide show
  1. package/api/src/libs/notification/template/customer-revenue-succeeded.ts +254 -0
  2. package/api/src/libs/notification/template/customer-reward-succeeded.ts +12 -11
  3. package/api/src/libs/payment.ts +47 -2
  4. package/api/src/libs/payout.ts +24 -0
  5. package/api/src/libs/util.ts +83 -1
  6. package/api/src/locales/en.ts +16 -1
  7. package/api/src/locales/zh.ts +28 -12
  8. package/api/src/queues/notification.ts +23 -1
  9. package/api/src/routes/invoices.ts +42 -5
  10. package/api/src/routes/payment-intents.ts +14 -1
  11. package/api/src/routes/payment-links.ts +17 -0
  12. package/api/src/routes/payouts.ts +103 -8
  13. package/api/src/store/migrations/20250206-update-donation-products.ts +56 -0
  14. package/api/src/store/models/payout.ts +6 -2
  15. package/api/src/store/models/types.ts +2 -0
  16. package/blocklet.yml +1 -1
  17. package/package.json +4 -4
  18. package/public/methods/default.png +0 -0
  19. package/src/app.tsx +10 -0
  20. package/src/components/customer/link.tsx +11 -2
  21. package/src/components/customer/overdraft-protection.tsx +2 -2
  22. package/src/components/info-card.tsx +6 -5
  23. package/src/components/invoice/table.tsx +4 -0
  24. package/src/components/payouts/list.tsx +17 -2
  25. package/src/components/payouts/portal/list.tsx +192 -0
  26. package/src/components/subscription/items/actions.tsx +1 -2
  27. package/src/libs/util.ts +42 -1
  28. package/src/locales/en.tsx +10 -0
  29. package/src/locales/zh.tsx +10 -0
  30. package/src/pages/admin/billing/invoices/detail.tsx +21 -0
  31. package/src/pages/admin/payments/payouts/detail.tsx +65 -4
  32. package/src/pages/customer/index.tsx +12 -25
  33. package/src/pages/customer/invoice/detail.tsx +27 -3
  34. package/src/pages/customer/payout/detail.tsx +264 -0
  35. package/src/pages/customer/recharge.tsx +2 -2
  36. 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(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
- if (doc) {
120
- res.json({ ...doc.toJSON() });
121
- } else {
122
- res.status(404).json(null);
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',
@@ -340,6 +340,8 @@ export type PaymentDetails = {
340
340
  export type PaymentBeneficiary = {
341
341
  address: string;
342
342
  share: string;
343
+ name?: string;
344
+ avatar?: string;
343
345
  memo?: string;
344
346
  };
345
347
 
package/blocklet.yml CHANGED
@@ -14,7 +14,7 @@ repository:
14
14
  type: git
15
15
  url: git+https://github.com/blocklet/payment-kit.git
16
16
  specVersion: 1.2.8
17
- version: 1.18.0
17
+ version: 1.18.1
18
18
  logo: logo.png
19
19
  files:
20
20
  - dist
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payment-kit",
3
- "version": "1.18.0",
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.0",
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.0",
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": "052c301e1f565558766c9346ba355f2fd072108e"
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({ customer, linked }: { customer: TCustomer; linked?: boolean }) {
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: 2 }
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: 2 } : {}),
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={props.name} variant={props.variant as any} sx={dimensions} />
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
- {props.name.slice(0, 1)}
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
- return item.customer ? <CustomerLink customer={item.customer} /> : item.destination;
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
  });