payment-kit 1.18.56 → 1.19.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 (214) hide show
  1. package/.eslintrc.js +6 -0
  2. package/api/src/crons/index.ts +8 -0
  3. package/api/src/index.ts +4 -0
  4. package/api/src/libs/credit-grant.ts +146 -0
  5. package/api/src/libs/env.ts +1 -0
  6. package/api/src/libs/invoice.ts +4 -3
  7. package/api/src/libs/notification/template/base.ts +388 -2
  8. package/api/src/libs/notification/template/customer-credit-grant-granted.ts +149 -0
  9. package/api/src/libs/notification/template/customer-credit-grant-low-balance.ts +151 -0
  10. package/api/src/libs/notification/template/customer-credit-insufficient.ts +254 -0
  11. package/api/src/libs/notification/template/subscription-canceled.ts +193 -202
  12. package/api/src/libs/notification/template/subscription-refund-succeeded.ts +215 -237
  13. package/api/src/libs/notification/template/subscription-renewed.ts +130 -200
  14. package/api/src/libs/notification/template/subscription-succeeded.ts +100 -202
  15. package/api/src/libs/notification/template/subscription-trial-start.ts +142 -188
  16. package/api/src/libs/notification/template/subscription-trial-will-end.ts +146 -174
  17. package/api/src/libs/notification/template/subscription-upgraded.ts +96 -192
  18. package/api/src/libs/notification/template/subscription-will-canceled.ts +94 -135
  19. package/api/src/libs/notification/template/subscription-will-renew.ts +220 -245
  20. package/api/src/libs/payment.ts +69 -0
  21. package/api/src/libs/queue/index.ts +3 -2
  22. package/api/src/libs/session.ts +8 -0
  23. package/api/src/libs/subscription.ts +74 -3
  24. package/api/src/libs/ws.ts +23 -1
  25. package/api/src/locales/en.ts +33 -0
  26. package/api/src/locales/zh.ts +31 -0
  27. package/api/src/queues/credit-consume.ts +715 -0
  28. package/api/src/queues/credit-grant.ts +572 -0
  29. package/api/src/queues/notification.ts +173 -128
  30. package/api/src/queues/payment.ts +210 -122
  31. package/api/src/queues/subscription.ts +179 -0
  32. package/api/src/routes/checkout-sessions.ts +157 -9
  33. package/api/src/routes/connect/shared.ts +3 -2
  34. package/api/src/routes/credit-grants.ts +241 -0
  35. package/api/src/routes/credit-transactions.ts +208 -0
  36. package/api/src/routes/index.ts +8 -0
  37. package/api/src/routes/meter-events.ts +347 -0
  38. package/api/src/routes/meters.ts +219 -0
  39. package/api/src/routes/payment-currencies.ts +14 -2
  40. package/api/src/routes/payment-links.ts +1 -1
  41. package/api/src/routes/payment-methods.ts +14 -2
  42. package/api/src/routes/prices.ts +43 -0
  43. package/api/src/routes/pricing-table.ts +13 -7
  44. package/api/src/routes/products.ts +63 -4
  45. package/api/src/routes/settings.ts +1 -1
  46. package/api/src/routes/subscriptions.ts +4 -0
  47. package/api/src/store/migrations/20250610-billing-credit.ts +43 -0
  48. package/api/src/store/models/credit-grant.ts +486 -0
  49. package/api/src/store/models/credit-transaction.ts +268 -0
  50. package/api/src/store/models/customer.ts +8 -0
  51. package/api/src/store/models/index.ts +52 -1
  52. package/api/src/store/models/meter-event.ts +423 -0
  53. package/api/src/store/models/meter.ts +176 -0
  54. package/api/src/store/models/payment-currency.ts +66 -14
  55. package/api/src/store/models/price.ts +6 -0
  56. package/api/src/store/models/product.ts +2 -2
  57. package/api/src/store/models/subscription.ts +24 -0
  58. package/api/src/store/models/types.ts +28 -2
  59. package/api/tests/libs/subscription.spec.ts +53 -0
  60. package/blocklet.yml +9 -1
  61. package/package.json +57 -58
  62. package/scripts/sdk.js +233 -1
  63. package/src/app.tsx +10 -0
  64. package/src/components/actions.tsx +22 -9
  65. package/src/components/balance-list.tsx +40 -12
  66. package/src/components/collapse.tsx +33 -15
  67. package/src/components/copyable.tsx +8 -7
  68. package/src/components/currency.tsx +15 -7
  69. package/src/components/customer/actions.tsx +1 -5
  70. package/src/components/customer/credit-grant-item-list.tsx +99 -0
  71. package/src/components/customer/credit-overview.tsx +233 -0
  72. package/src/components/customer/form.tsx +7 -2
  73. package/src/components/customer/link.tsx +4 -12
  74. package/src/components/customer/notification-preference.tsx +18 -9
  75. package/src/components/customer/overdraft-protection.tsx +112 -41
  76. package/src/components/drawer-form.tsx +42 -18
  77. package/src/components/error.tsx +1 -5
  78. package/src/components/event/list.tsx +9 -10
  79. package/src/components/filter-toolbar.tsx +20 -19
  80. package/src/components/info-card.tsx +32 -18
  81. package/src/components/info-metric.tsx +16 -6
  82. package/src/components/info-row-group.tsx +1 -7
  83. package/src/components/info-row.tsx +30 -24
  84. package/src/components/invoice/action.tsx +1 -7
  85. package/src/components/invoice/list.tsx +34 -26
  86. package/src/components/invoice/recharge.tsx +5 -7
  87. package/src/components/invoice/table.tsx +17 -12
  88. package/src/components/layout/user.tsx +1 -1
  89. package/src/components/metadata/form.tsx +290 -94
  90. package/src/components/metadata/list.tsx +11 -3
  91. package/src/components/meter/actions.tsx +101 -0
  92. package/src/components/meter/add-usage-dialog.tsx +239 -0
  93. package/src/components/meter/events-list.tsx +657 -0
  94. package/src/components/meter/form.tsx +245 -0
  95. package/src/components/meter/products.tsx +264 -0
  96. package/src/components/meter/usage-guide.tsx +174 -0
  97. package/src/components/passport/actions.tsx +9 -4
  98. package/src/components/payment-currency/add.tsx +16 -3
  99. package/src/components/payment-currency/form.tsx +14 -6
  100. package/src/components/payment-intent/actions.tsx +24 -16
  101. package/src/components/payment-intent/list.tsx +30 -9
  102. package/src/components/payment-link/actions.tsx +1 -5
  103. package/src/components/payment-link/after-pay.tsx +4 -2
  104. package/src/components/payment-link/before-pay.tsx +14 -4
  105. package/src/components/payment-link/item.tsx +27 -6
  106. package/src/components/payment-link/preview.tsx +9 -9
  107. package/src/components/payment-link/product-select.tsx +69 -15
  108. package/src/components/payment-method/arcblock.tsx +8 -1
  109. package/src/components/payment-method/base.tsx +8 -1
  110. package/src/components/payment-method/bitcoin.tsx +8 -1
  111. package/src/components/payment-method/ethereum.tsx +8 -1
  112. package/src/components/payment-method/evm-rpc-input.tsx +11 -7
  113. package/src/components/payment-method/form.tsx +2 -7
  114. package/src/components/payment-method/stripe.tsx +2 -0
  115. package/src/components/payouts/actions.tsx +1 -5
  116. package/src/components/payouts/list.tsx +30 -10
  117. package/src/components/payouts/portal/list.tsx +11 -9
  118. package/src/components/price/currency-select.tsx +63 -32
  119. package/src/components/price/form.tsx +895 -370
  120. package/src/components/price/upsell-select.tsx +10 -2
  121. package/src/components/price/upsell.tsx +7 -2
  122. package/src/components/pricing-table/actions.tsx +1 -5
  123. package/src/components/pricing-table/customer-settings.tsx +5 -1
  124. package/src/components/pricing-table/payment-settings.tsx +14 -4
  125. package/src/components/pricing-table/preview.tsx +9 -9
  126. package/src/components/pricing-table/price-item.tsx +6 -1
  127. package/src/components/pricing-table/product-item.tsx +6 -1
  128. package/src/components/pricing-table/product-settings.tsx +17 -4
  129. package/src/components/product/actions.tsx +1 -5
  130. package/src/components/product/add-price.tsx +9 -7
  131. package/src/components/product/create.tsx +8 -9
  132. package/src/components/product/cross-sell-select.tsx +5 -1
  133. package/src/components/product/cross-sell.tsx +7 -2
  134. package/src/components/product/edit-price.tsx +21 -12
  135. package/src/components/product/features.tsx +26 -6
  136. package/src/components/product/form.tsx +115 -72
  137. package/src/components/progress-bar.tsx +1 -1
  138. package/src/components/refund/actions.tsx +1 -7
  139. package/src/components/refund/list.tsx +31 -18
  140. package/src/components/section/header.tsx +12 -14
  141. package/src/components/subscription/actions/cancel.tsx +22 -5
  142. package/src/components/subscription/actions/index.tsx +9 -10
  143. package/src/components/subscription/actions/pause.tsx +32 -6
  144. package/src/components/subscription/actions/slash-stake.tsx +5 -3
  145. package/src/components/subscription/description.tsx +12 -8
  146. package/src/components/subscription/items/index.tsx +31 -16
  147. package/src/components/subscription/items/usage-records.tsx +19 -5
  148. package/src/components/subscription/list.tsx +5 -7
  149. package/src/components/subscription/metrics.tsx +62 -15
  150. package/src/components/subscription/portal/actions.tsx +78 -71
  151. package/src/components/subscription/portal/cancel.tsx +10 -3
  152. package/src/components/subscription/portal/list.tsx +48 -26
  153. package/src/components/uploader.tsx +5 -13
  154. package/src/components/webhook/attempts.tsx +51 -16
  155. package/src/components/webhook/request-info.tsx +8 -6
  156. package/src/contexts/products.tsx +27 -10
  157. package/src/hooks/subscription.ts +34 -0
  158. package/src/libs/meter-utils.ts +196 -0
  159. package/src/libs/util.ts +4 -0
  160. package/src/locales/en.tsx +385 -4
  161. package/src/locales/zh.tsx +364 -0
  162. package/src/pages/admin/billing/index.tsx +61 -33
  163. package/src/pages/admin/billing/invoices/detail.tsx +49 -13
  164. package/src/pages/admin/billing/meters/create.tsx +60 -0
  165. package/src/pages/admin/billing/meters/detail.tsx +435 -0
  166. package/src/pages/admin/billing/meters/index.tsx +210 -0
  167. package/src/pages/admin/billing/meters/meter-event.tsx +346 -0
  168. package/src/pages/admin/billing/subscriptions/detail.tsx +90 -25
  169. package/src/pages/admin/customers/customers/credit-grant/detail.tsx +391 -0
  170. package/src/pages/admin/customers/customers/detail.tsx +67 -14
  171. package/src/pages/admin/customers/customers/index.tsx +6 -1
  172. package/src/pages/admin/customers/index.tsx +5 -0
  173. package/src/pages/admin/developers/events/detail.tsx +37 -11
  174. package/src/pages/admin/developers/index.tsx +1 -1
  175. package/src/pages/admin/developers/webhooks/detail.tsx +41 -11
  176. package/src/pages/admin/index.tsx +15 -2
  177. package/src/pages/admin/overview.tsx +107 -19
  178. package/src/pages/admin/payments/intents/detail.tsx +58 -14
  179. package/src/pages/admin/payments/payouts/detail.tsx +63 -15
  180. package/src/pages/admin/payments/refunds/detail.tsx +58 -14
  181. package/src/pages/admin/products/index.tsx +11 -4
  182. package/src/pages/admin/products/links/create.tsx +22 -4
  183. package/src/pages/admin/products/links/detail.tsx +43 -14
  184. package/src/pages/admin/products/passports/index.tsx +23 -4
  185. package/src/pages/admin/products/prices/actions.tsx +16 -9
  186. package/src/pages/admin/products/prices/detail.tsx +73 -14
  187. package/src/pages/admin/products/prices/list.tsx +15 -3
  188. package/src/pages/admin/products/pricing-tables/create.tsx +45 -12
  189. package/src/pages/admin/products/pricing-tables/detail.tsx +45 -14
  190. package/src/pages/admin/products/products/create.tsx +233 -54
  191. package/src/pages/admin/products/products/detail.tsx +74 -18
  192. package/src/pages/admin/settings/index.tsx +8 -1
  193. package/src/pages/admin/settings/payment-methods/index.tsx +87 -19
  194. package/src/pages/admin/settings/vault-config/edit-form.tsx +42 -28
  195. package/src/pages/admin/settings/vault-config/index.tsx +57 -10
  196. package/src/pages/customer/credit-grant/detail.tsx +308 -0
  197. package/src/pages/customer/index.tsx +76 -17
  198. package/src/pages/customer/invoice/detail.tsx +63 -14
  199. package/src/pages/customer/invoice/past-due.tsx +11 -3
  200. package/src/pages/customer/payout/detail.tsx +56 -13
  201. package/src/pages/customer/recharge/account.tsx +78 -18
  202. package/src/pages/customer/recharge/subscription.tsx +86 -25
  203. package/src/pages/customer/refund/list.tsx +60 -24
  204. package/src/pages/customer/subscription/change-payment.tsx +17 -6
  205. package/src/pages/customer/subscription/change-plan.tsx +34 -7
  206. package/src/pages/customer/subscription/detail.tsx +134 -34
  207. package/src/pages/customer/subscription/embed.tsx +25 -5
  208. package/src/pages/home.tsx +26 -4
  209. package/src/pages/integrations/donations/edit-form.tsx +25 -9
  210. package/src/pages/integrations/donations/index.tsx +26 -9
  211. package/src/pages/integrations/donations/preview.tsx +59 -15
  212. package/src/pages/integrations/index.tsx +10 -1
  213. package/src/pages/integrations/overview.tsx +78 -17
  214. package/vite.config.ts +60 -30
@@ -1,7 +1,15 @@
1
1
  /* eslint-disable react/no-unstable-nested-components */
2
2
  import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
3
  import Toast from '@arcblock/ux/lib/Toast';
4
- import { TxLink, api, formatError, formatTime, useMobile, hasDelegateTxHash } from '@blocklet/payment-react';
4
+ import {
5
+ TxLink,
6
+ api,
7
+ formatError,
8
+ formatTime,
9
+ useMobile,
10
+ hasDelegateTxHash,
11
+ CreditTransactionsList,
12
+ } from '@blocklet/payment-react';
5
13
  import type { TProduct, TSubscriptionExpanded } from '@blocklet/payment-types';
6
14
  import { ArrowBackOutlined } from '@mui/icons-material';
7
15
  import { Alert, Box, Button, CircularProgress, Divider, Stack, Typography } from '@mui/material';
@@ -76,15 +84,26 @@ export default function SubscriptionDetail(props: { id: string }) {
76
84
  setState((prev) => ({ editing: { ...prev.editing, metadata: true } }));
77
85
  };
78
86
 
87
+ const isCredit = data.paymentCurrency.type === 'credit';
88
+
79
89
  return (
80
90
  <Root direction="column" spacing={2.5} sx={{ mb: 4 }}>
81
91
  <Box>
82
- <Stack className="page-header" direction="row" justifyContent="space-between" alignItems="center">
92
+ <Stack
93
+ className="page-header"
94
+ direction="row"
95
+ sx={{
96
+ justifyContent: 'space-between',
97
+ alignItems: 'center',
98
+ }}>
83
99
  <Stack
84
100
  direction="row"
85
- alignItems="center"
86
- sx={{ fontWeight: 'normal', cursor: 'pointer' }}
87
- onClick={() => goBackOrFallback('/admin/billing/subscriptions')}>
101
+ onClick={() => goBackOrFallback('/admin/billing/subscriptions')}
102
+ sx={{
103
+ alignItems: 'center',
104
+ fontWeight: 'normal',
105
+ cursor: 'pointer',
106
+ }}>
88
107
  <ArrowBackOutlined fontSize="small" sx={{ mr: 0.5, color: 'text.secondary' }} />
89
108
  <Typography variant="h6" sx={{ color: 'text.secondary', fontWeight: 'normal' }}>
90
109
  {t('admin.subscriptions')}
@@ -93,29 +112,41 @@ export default function SubscriptionDetail(props: { id: string }) {
93
112
  <SubscriptionActions data={data} onChange={runAsync} variant="normal" />
94
113
  </Stack>
95
114
  <Box
96
- mt={4}
97
- mb={3}
98
115
  sx={{
116
+ mt: 4,
117
+ mb: 3,
99
118
  display: 'flex',
119
+
100
120
  gap: {
101
121
  xs: 2,
102
122
  sm: 2,
103
123
  md: 5,
104
124
  },
125
+
105
126
  flexWrap: 'wrap',
127
+
106
128
  flexDirection: {
107
129
  xs: 'column',
108
130
  sm: 'column',
109
131
  md: 'row',
110
132
  },
133
+
111
134
  alignItems: {
112
135
  xs: 'flex-start',
113
136
  sm: 'flex-start',
114
137
  md: 'center',
115
138
  },
116
139
  }}>
117
- <Stack direction="column" gap={1}>
118
- <Stack direction="row" alignItems="center">
140
+ <Stack
141
+ direction="column"
142
+ sx={{
143
+ gap: 1,
144
+ }}>
145
+ <Stack
146
+ direction="row"
147
+ sx={{
148
+ alignItems: 'center',
149
+ }}>
119
150
  <Typography variant="h2" sx={{ fontWeight: 600 }}>
120
151
  {data.description}
121
152
  </Typography>
@@ -124,18 +155,22 @@ export default function SubscriptionDetail(props: { id: string }) {
124
155
  </Stack>
125
156
  <Stack
126
157
  className="section-body"
127
- justifyContent="flex-start"
128
- flexWrap="wrap"
129
158
  sx={{
159
+ justifyContent: 'flex-start',
160
+ flexWrap: 'wrap',
161
+
130
162
  'hr.MuiDivider-root:last-child': {
131
163
  display: 'none',
132
164
  },
165
+
133
166
  flexDirection: {
134
167
  xs: 'column',
135
168
  sm: 'column',
136
169
  md: 'row',
137
170
  },
171
+
138
172
  alignItems: 'flex-start',
173
+
139
174
  gap: {
140
175
  xs: 1,
141
176
  sm: 1,
@@ -175,7 +210,14 @@ export default function SubscriptionDetail(props: { id: string }) {
175
210
  },
176
211
  },
177
212
  }}>
178
- <Box flex={1} className="payment-link-column-1" sx={{ gap: 2.5, display: 'flex', flexDirection: 'column' }}>
213
+ <Box
214
+ className="payment-link-column-1"
215
+ sx={{
216
+ flex: 1,
217
+ gap: 2.5,
218
+ display: 'flex',
219
+ flexDirection: 'column',
220
+ }}>
179
221
  <Box className="section" sx={{ containerType: 'inline-size' }}>
180
222
  <SectionHeader title={t('admin.details')} />
181
223
  <InfoRowGroup
@@ -278,22 +320,45 @@ export default function SubscriptionDetail(props: { id: string }) {
278
320
  </Box>
279
321
  </Box>
280
322
  <Divider />
281
- <Box className="section">
282
- <SectionHeader title={t('admin.invoices')} />
283
- <Box className="section-body">
284
- <InvoiceList features={{ customer: true, toolbar: false }} subscription_id={data.id} include_staking />
285
- </Box>
286
- </Box>
287
- <Divider />
288
- <Box className="section">
289
- <SectionHeader title={t('admin.refunds')} />
290
- <Box className="section-body">
291
- <RefundList features={{ customer: true, toolbar: false }} subscription_id={data.id} />
323
+ {isCredit ? (
324
+ <Box className="section">
325
+ <Typography variant="h3" className="section-header">
326
+ {t('admin.creditTransactions.title')}
327
+ </Typography>
328
+ <Box className="section-body">
329
+ <CreditTransactionsList
330
+ customer_id={data.customer_id}
331
+ subscription_id={data.id}
332
+ showAdminColumns
333
+ showTimeFilter
334
+ mode="dashboard"
335
+ />
336
+ </Box>
292
337
  </Box>
293
- </Box>
338
+ ) : (
339
+ <>
340
+ <Box className="section">
341
+ <SectionHeader title={t('admin.invoices')} />
342
+ <Box className="section-body">
343
+ <InvoiceList
344
+ features={{ customer: true, toolbar: false }}
345
+ subscription_id={data.id}
346
+ include_staking
347
+ />
348
+ </Box>
349
+ </Box>
350
+ <Divider />
351
+ <Box className="section">
352
+ <SectionHeader title={t('admin.refunds')} />
353
+ <Box className="section-body">
354
+ <RefundList features={{ customer: true, toolbar: false }} subscription_id={data.id} />
355
+ </Box>
356
+ </Box>
357
+ </>
358
+ )}
294
359
  <Divider />
295
360
  <Box className="section">
296
- <SectionHeader title={t('admin.events')} />
361
+ <SectionHeader title={t('admin.events.title')} />
297
362
  <Box className="section-body">
298
363
  <EventList features={{ toolbar: false }} object_id={data.id} />
299
364
  </Box>
@@ -0,0 +1,391 @@
1
+ /* eslint-disable react/no-unstable-nested-components */
2
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
+ import Toast from '@arcblock/ux/lib/Toast';
4
+ import {
5
+ api,
6
+ formatBNStr,
7
+ formatError,
8
+ formatTime,
9
+ CreditTransactionsList,
10
+ CreditStatusChip,
11
+ getCustomerAvatar,
12
+ } from '@blocklet/payment-react';
13
+ import type { TCreditGrantExpanded } from '@blocklet/payment-types';
14
+ import { ArrowBackOutlined } from '@mui/icons-material';
15
+ import { Alert, Avatar, Box, Button, CircularProgress, Divider, Stack, Typography } from '@mui/material';
16
+ import { useRequest, useSetState } from 'ahooks';
17
+ import { styled } from '@mui/system';
18
+ import { useCallback } from 'react';
19
+
20
+ import InfoMetric from '../../../../../components/info-metric';
21
+ import InfoRow from '../../../../../components/info-row';
22
+ import InfoRowGroup from '../../../../../components/info-row-group';
23
+ import Copyable from '../../../../../components/copyable';
24
+ import SectionHeader from '../../../../../components/section/header';
25
+ import MetadataList from '../../../../../components/metadata/list';
26
+ import MetadataEditor from '../../../../../components/metadata/editor';
27
+ import CreditGrantItemList from '../../../../../components/customer/credit-grant-item-list';
28
+ import { goBackOrFallback } from '../../../../../libs/util';
29
+ import EventList from '../../../../../components/event/list';
30
+
31
+ const fetchData = (id: string | undefined): Promise<TCreditGrantExpanded> => {
32
+ return api.get(`/api/credit-grants/${id}`).then((res) => res.data);
33
+ };
34
+
35
+ export default function AdminCreditGrantDetail({ id }: { id: string }) {
36
+ const { t } = useLocaleContext();
37
+ const [state, setState] = useSetState({
38
+ editing: {
39
+ metadata: false,
40
+ creditGrant: false,
41
+ },
42
+ loading: {
43
+ metadata: false,
44
+ creditGrant: false,
45
+ },
46
+ });
47
+
48
+ const { loading, error, data, runAsync } = useRequest(() => fetchData(id));
49
+
50
+ const handleBack = useCallback(() => {
51
+ goBackOrFallback('/admin/customers');
52
+ }, []);
53
+
54
+ if (error) {
55
+ return <Alert severity="error">{formatError(error)}</Alert>;
56
+ }
57
+
58
+ if (loading || !data) {
59
+ return <CircularProgress />;
60
+ }
61
+
62
+ const isDepleted = data?.status === 'depleted';
63
+
64
+ const getStatusText = (status: string) => {
65
+ switch (status) {
66
+ case 'granted':
67
+ return t('admin.customer.creditGrants.status.granted');
68
+ case 'pending':
69
+ return t('admin.customer.creditGrants.status.pending');
70
+ case 'depleted':
71
+ return t('admin.customer.creditGrants.status.depleted');
72
+ case 'expired':
73
+ return t('admin.customer.creditGrants.status.expired');
74
+ case 'voided':
75
+ return t('admin.customer.creditGrants.status.voided');
76
+ default:
77
+ return status;
78
+ }
79
+ };
80
+
81
+ const getUsagePercentage = () => {
82
+ if (!data.amount || !data.remaining_amount) return 0;
83
+ const total = parseFloat(data.amount);
84
+ const remaining = parseFloat(data.remaining_amount);
85
+ return ((total - remaining) / total) * 100;
86
+ };
87
+
88
+ const onUpdateMetadata = async (updates: any) => {
89
+ try {
90
+ setState((prev) => ({ loading: { ...prev.loading, metadata: true } }));
91
+ await api.put(`/api/credit-grants/${id}`, updates);
92
+ Toast.success(t('common.saved'));
93
+ runAsync();
94
+ } catch (err) {
95
+ console.error(err);
96
+ Toast.error(formatError(err));
97
+ } finally {
98
+ setState((prev) => ({ loading: { ...prev.loading, metadata: false } }));
99
+ }
100
+ };
101
+
102
+ const handleEditMetadata = () => {
103
+ setState((prev) => ({ editing: { ...prev.editing, metadata: true } }));
104
+ };
105
+
106
+ return (
107
+ <Root direction="column" spacing={2.5} sx={{ mb: 4 }}>
108
+ <Box>
109
+ <Stack className="page-header" direction="row" justifyContent="space-between" alignItems="center">
110
+ <Stack
111
+ direction="row"
112
+ alignItems="center"
113
+ sx={{ fontWeight: 'normal', cursor: 'pointer' }}
114
+ onClick={handleBack}>
115
+ <ArrowBackOutlined fontSize="small" sx={{ mr: 0.5, color: 'text.secondary' }} />
116
+ <Typography variant="h6" sx={{ color: 'text.secondary', fontWeight: 'normal' }}>
117
+ {t('admin.customer.creditGrants.title')}
118
+ </Typography>
119
+ </Stack>
120
+ </Stack>
121
+
122
+ <Box
123
+ mt={4}
124
+ mb={3}
125
+ sx={{
126
+ display: 'flex',
127
+ gap: {
128
+ xs: 2,
129
+ sm: 2,
130
+ md: 5,
131
+ },
132
+ flexWrap: 'wrap',
133
+ flexDirection: {
134
+ xs: 'column',
135
+ sm: 'column',
136
+ md: 'row',
137
+ },
138
+ alignItems: {
139
+ xs: 'flex-start',
140
+ sm: 'flex-start',
141
+ md: 'center',
142
+ },
143
+ }}>
144
+ <Stack direction="row" justifyContent="space-between" alignItems="center">
145
+ <Stack direction="column" spacing={0.5}>
146
+ <Typography variant="h2" sx={{ fontWeight: 600 }}>
147
+ {data.name || `${t('admin.customer.creditGrants.grant')} ${data.id.slice(-8)}`}
148
+ </Typography>
149
+ <Copyable text={data.id} />
150
+ </Stack>
151
+ </Stack>
152
+
153
+ <Stack
154
+ className="section-body"
155
+ justifyContent="flex-start"
156
+ flexWrap="wrap"
157
+ sx={{
158
+ 'hr.MuiDivider-root:last-child': {
159
+ display: 'none',
160
+ },
161
+ flexDirection: {
162
+ xs: 'column',
163
+ sm: 'column',
164
+ md: 'row',
165
+ },
166
+ alignItems: 'flex-start',
167
+ gap: {
168
+ xs: 1,
169
+ sm: 1,
170
+ md: 3,
171
+ },
172
+ }}>
173
+ <InfoMetric
174
+ label={t('common.status')}
175
+ value={<CreditStatusChip status={data.status} label={getStatusText(data.status)} />}
176
+ divider
177
+ />
178
+ <InfoMetric
179
+ label={t('admin.customer.creditGrants.originalAmount')}
180
+ value={
181
+ <Stack direction="row" alignItems="center" spacing={0.5}>
182
+ <Avatar
183
+ src={data.paymentCurrency?.logo}
184
+ sx={{ width: 16, height: 16 }}
185
+ alt={data.paymentCurrency?.symbol}
186
+ />
187
+ <Typography variant="body2" sx={{ color: 'text.secondary' }}>
188
+ {formatBNStr(data.amount, data.paymentCurrency.decimal)} {data.paymentCurrency.symbol}
189
+ </Typography>
190
+ </Stack>
191
+ }
192
+ divider
193
+ />
194
+ <InfoMetric
195
+ label={t('common.remainingCredit')}
196
+ value={
197
+ <Stack direction="row" alignItems="center" spacing={0.5}>
198
+ <Avatar
199
+ src={data.paymentCurrency?.logo}
200
+ sx={{ width: 16, height: 16 }}
201
+ alt={data.paymentCurrency?.symbol}
202
+ />
203
+ <Typography variant="body2" color={isDepleted ? 'error.main' : 'text.secondary'}>
204
+ {formatBNStr(data.remaining_amount, data.paymentCurrency.decimal)} {data.paymentCurrency.symbol}
205
+ </Typography>
206
+ </Stack>
207
+ }
208
+ divider
209
+ />
210
+ <InfoMetric
211
+ label={t('admin.customer.creditGrants.usage')}
212
+ value={
213
+ <Typography variant="body2" color={isDepleted ? 'error.main' : 'text.secondary'}>
214
+ {getUsagePercentage().toFixed(1)}%
215
+ </Typography>
216
+ }
217
+ />
218
+ </Stack>
219
+ </Box>
220
+ <Divider />
221
+ </Box>
222
+
223
+ <Stack
224
+ sx={{
225
+ flexDirection: {
226
+ xs: 'column',
227
+ lg: 'row',
228
+ },
229
+ gap: {
230
+ xs: 2.5,
231
+ md: 4,
232
+ },
233
+ '.credit-grant-column-1': {
234
+ minWidth: {
235
+ xs: '100%',
236
+ lg: '600px',
237
+ },
238
+ },
239
+ '.credit-grant-column-2': {
240
+ width: {
241
+ xs: '100%',
242
+ md: '100%',
243
+ lg: '320px',
244
+ },
245
+ maxWidth: {
246
+ xs: '100%',
247
+ md: '33%',
248
+ },
249
+ },
250
+ }}>
251
+ <Box flex={1} className="credit-grant-column-1" sx={{ gap: 2.5, display: 'flex', flexDirection: 'column' }}>
252
+ {/* 基本信息 */}
253
+ <Box className="section" sx={{ containerType: 'inline-size' }}>
254
+ <SectionHeader title={t('admin.details')} />
255
+ <InfoRowGroup
256
+ sx={{
257
+ display: 'grid',
258
+ gridTemplateColumns: {
259
+ xs: 'repeat(1, 1fr)',
260
+ xl: 'repeat(2, 1fr)',
261
+ },
262
+ '@container (min-width: 1000px)': {
263
+ gridTemplateColumns: 'repeat(2, 1fr)',
264
+ },
265
+ '.info-row-wrapper': {
266
+ gap: 1,
267
+ flexDirection: {
268
+ xs: 'column',
269
+ xl: 'row',
270
+ },
271
+ alignItems: {
272
+ xs: 'flex-start',
273
+ xl: 'center',
274
+ },
275
+ '@container (min-width: 1000px)': {
276
+ flexDirection: 'row',
277
+ alignItems: 'center',
278
+ },
279
+ },
280
+ }}>
281
+ <InfoRow
282
+ label={t('common.customer')}
283
+ value={
284
+ <Stack direction="row" alignItems="center" spacing={1}>
285
+ <Avatar
286
+ src={getCustomerAvatar(
287
+ data.customer?.did,
288
+ data.customer?.updated_at ? new Date(data.customer.updated_at).toISOString() : '',
289
+ 24
290
+ )}
291
+ alt={data.customer?.name}
292
+ sx={{ width: 24, height: 24 }}
293
+ />
294
+ <Typography>{data.customer?.name}</Typography>
295
+ </Stack>
296
+ }
297
+ />
298
+ <InfoRow
299
+ label={t('common.scope')}
300
+ value={
301
+ <Typography>
302
+ {data.applicability_config?.scope?.prices ? t('common.specific') : t('common.general')}
303
+ </Typography>
304
+ }
305
+ />
306
+ <InfoRow
307
+ label={t('admin.creditProduct.priority.label')}
308
+ value={<Typography>{data.priority}</Typography>}
309
+ />
310
+ <InfoRow label={t('common.createdAt')} value={formatTime(data.created_at)} />
311
+ <InfoRow
312
+ label={t('common.effectiveDate')}
313
+ value={formatTime(data.effective_at ? data.effective_at * 1000 : data.created_at)}
314
+ />
315
+ {data.expires_at && (
316
+ <InfoRow label={t('common.expirationDate')} value={formatTime(data.expires_at * 1000)} />
317
+ )}
318
+ </InfoRowGroup>
319
+ </Box>
320
+
321
+ <Divider />
322
+
323
+ {/* 适用范围 - 使用新的CreditGrantItemList组件 */}
324
+ {data.items && data.items.length > 0 && (
325
+ <>
326
+ <Box className="section">
327
+ <SectionHeader title={t('admin.creditProduct.associatedPrices.label')} />
328
+ <Box className="section-body">
329
+ <CreditGrantItemList data={data.items} currency={data.paymentCurrency} mode="dashboard" />
330
+ </Box>
331
+ </Box>
332
+ <Divider />
333
+ </>
334
+ )}
335
+
336
+ {/* 交易记录 */}
337
+ <Box className="section">
338
+ <SectionHeader title={t('admin.creditTransactions.title')} />
339
+ <Box className="section-body">
340
+ <CreditTransactionsList
341
+ customer_id={data.customer_id}
342
+ credit_grant_id={data.id}
343
+ showAdminColumns
344
+ showTimeFilter
345
+ mode="dashboard"
346
+ />
347
+ </Box>
348
+ </Box>
349
+
350
+ <Divider />
351
+ <Box className="section">
352
+ <SectionHeader title={t('admin.events.title')} />
353
+ <Box className="section-body">
354
+ <EventList features={{ toolbar: false }} object_id={data.id} />
355
+ </Box>
356
+ </Box>
357
+ </Box>
358
+
359
+ <Box className="credit-grant-column-2" sx={{ gap: 2.5, display: 'flex', flexDirection: 'column' }}>
360
+ {/* 元数据 */}
361
+ <Box className="section">
362
+ <SectionHeader title={t('common.metadata.label')}>
363
+ <Button
364
+ variant="text"
365
+ color="inherit"
366
+ size="small"
367
+ sx={{ color: 'text.link' }}
368
+ disabled={state.editing.metadata}
369
+ onClick={handleEditMetadata}>
370
+ {t('common.edit')}
371
+ </Button>
372
+ </SectionHeader>
373
+ <Box className="section-body">
374
+ <MetadataList data={data.metadata} handleEditMetadata={handleEditMetadata} />
375
+ {state.editing.metadata && (
376
+ <MetadataEditor
377
+ data={data}
378
+ loading={state.loading.metadata}
379
+ onSave={onUpdateMetadata}
380
+ onCancel={() => setState((prev) => ({ editing: { ...prev.editing, metadata: false } }))}
381
+ />
382
+ )}
383
+ </Box>
384
+ </Box>
385
+ </Box>
386
+ </Stack>
387
+ </Root>
388
+ );
389
+ }
390
+
391
+ const Root = styled(Stack)``;