payment-kit 1.22.32 → 1.23.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 (49) hide show
  1. package/api/src/index.ts +4 -0
  2. package/api/src/integrations/arcblock/token.ts +599 -0
  3. package/api/src/libs/credit-grant.ts +7 -6
  4. package/api/src/libs/util.ts +34 -0
  5. package/api/src/queues/credit-consume.ts +29 -4
  6. package/api/src/queues/credit-grant.ts +245 -50
  7. package/api/src/queues/credit-reconciliation.ts +253 -0
  8. package/api/src/queues/refund.ts +263 -30
  9. package/api/src/queues/token-transfer.ts +331 -0
  10. package/api/src/routes/checkout-sessions.ts +94 -29
  11. package/api/src/routes/credit-grants.ts +35 -9
  12. package/api/src/routes/credit-tokens.ts +38 -0
  13. package/api/src/routes/credit-transactions.ts +20 -3
  14. package/api/src/routes/index.ts +2 -0
  15. package/api/src/routes/meter-events.ts +4 -0
  16. package/api/src/routes/meters.ts +32 -10
  17. package/api/src/routes/payment-currencies.ts +103 -0
  18. package/api/src/routes/payment-links.ts +3 -1
  19. package/api/src/routes/products.ts +2 -2
  20. package/api/src/routes/settings.ts +4 -3
  21. package/api/src/store/migrations/20251120-add-token-config-to-currencies.ts +20 -0
  22. package/api/src/store/migrations/20251204-add-chain-fields.ts +74 -0
  23. package/api/src/store/migrations/20251211-optimize-slow-queries.ts +33 -0
  24. package/api/src/store/models/credit-grant.ts +47 -9
  25. package/api/src/store/models/credit-transaction.ts +18 -1
  26. package/api/src/store/models/index.ts +2 -1
  27. package/api/src/store/models/payment-currency.ts +31 -4
  28. package/api/src/store/models/refund.ts +12 -2
  29. package/api/src/store/models/types.ts +48 -0
  30. package/api/src/store/sequelize.ts +1 -0
  31. package/api/third.d.ts +2 -0
  32. package/blocklet.yml +1 -1
  33. package/package.json +7 -6
  34. package/src/app.tsx +10 -0
  35. package/src/components/customer/credit-overview.tsx +19 -3
  36. package/src/components/meter/form.tsx +191 -18
  37. package/src/components/price/form.tsx +49 -37
  38. package/src/locales/en.tsx +25 -1
  39. package/src/locales/zh.tsx +27 -1
  40. package/src/pages/admin/billing/meters/create.tsx +42 -13
  41. package/src/pages/admin/billing/meters/detail.tsx +56 -5
  42. package/src/pages/admin/customers/customers/credit-grant/detail.tsx +13 -0
  43. package/src/pages/admin/customers/customers/credit-transaction/detail.tsx +324 -0
  44. package/src/pages/admin/customers/index.tsx +5 -0
  45. package/src/pages/customer/credit-grant/detail.tsx +14 -1
  46. package/src/pages/customer/credit-transaction/detail.tsx +289 -0
  47. package/src/pages/customer/invoice/detail.tsx +1 -1
  48. package/src/pages/customer/recharge/subscription.tsx +1 -1
  49. package/src/pages/customer/subscription/detail.tsx +1 -1
@@ -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">You do not have permission to access other customer data</Alert>;
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">You do not have permission to access other customer data</Alert>;
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">You do not have permission to access other customer data</Alert>;
122
+ return <Alert severity="error">{t('common.accessDenied')}</Alert>;
123
123
  }
124
124
  if (error) {
125
125
  return <Alert severity="error">{error.message}</Alert>;