payment-kit 1.20.11 → 1.20.13

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 (92) hide show
  1. package/api/src/crons/index.ts +8 -0
  2. package/api/src/index.ts +2 -0
  3. package/api/src/integrations/stripe/handlers/invoice.ts +63 -5
  4. package/api/src/integrations/stripe/handlers/payment-intent.ts +1 -0
  5. package/api/src/integrations/stripe/resource.ts +253 -2
  6. package/api/src/libs/currency.ts +31 -0
  7. package/api/src/libs/discount/coupon.ts +1061 -0
  8. package/api/src/libs/discount/discount.ts +349 -0
  9. package/api/src/libs/discount/nft.ts +239 -0
  10. package/api/src/libs/discount/redemption.ts +636 -0
  11. package/api/src/libs/discount/vc.ts +73 -0
  12. package/api/src/libs/env.ts +1 -0
  13. package/api/src/libs/invoice.ts +44 -10
  14. package/api/src/libs/math-utils.ts +6 -0
  15. package/api/src/libs/price.ts +43 -0
  16. package/api/src/libs/session.ts +242 -57
  17. package/api/src/libs/subscription.ts +2 -6
  18. package/api/src/libs/vendor-util/adapters/launcher-adapter.ts +1 -1
  19. package/api/src/libs/vendor-util/adapters/types.ts +1 -0
  20. package/api/src/libs/vendor-util/fulfillment.ts +1 -1
  21. package/api/src/queues/auto-recharge.ts +1 -1
  22. package/api/src/queues/discount-status.ts +200 -0
  23. package/api/src/queues/subscription.ts +98 -5
  24. package/api/src/queues/usage-record.ts +1 -1
  25. package/api/src/queues/vendors/fulfillment-coordinator.ts +1 -29
  26. package/api/src/queues/vendors/return-processor.ts +184 -0
  27. package/api/src/queues/vendors/return-scanner.ts +119 -0
  28. package/api/src/queues/vendors/status-check.ts +1 -1
  29. package/api/src/routes/auto-recharge-configs.ts +5 -3
  30. package/api/src/routes/checkout-sessions.ts +755 -64
  31. package/api/src/routes/connect/change-payment.ts +6 -1
  32. package/api/src/routes/connect/change-plan.ts +6 -1
  33. package/api/src/routes/connect/setup.ts +6 -1
  34. package/api/src/routes/connect/shared.ts +80 -9
  35. package/api/src/routes/connect/subscribe.ts +12 -2
  36. package/api/src/routes/coupons.ts +518 -0
  37. package/api/src/routes/index.ts +4 -0
  38. package/api/src/routes/invoices.ts +44 -3
  39. package/api/src/routes/meter-events.ts +2 -1
  40. package/api/src/routes/payment-currencies.ts +1 -0
  41. package/api/src/routes/promotion-codes.ts +482 -0
  42. package/api/src/routes/subscriptions.ts +23 -2
  43. package/api/src/routes/vendor.ts +89 -2
  44. package/api/src/store/migrations/20250904-discount.ts +136 -0
  45. package/api/src/store/migrations/20250910-timestamp-fields.ts +116 -0
  46. package/api/src/store/migrations/20250916-add-description-fields.ts +30 -0
  47. package/api/src/store/migrations/20250918-add-vendor-extends.ts +20 -0
  48. package/api/src/store/models/checkout-session.ts +17 -2
  49. package/api/src/store/models/coupon.ts +144 -4
  50. package/api/src/store/models/discount.ts +23 -10
  51. package/api/src/store/models/index.ts +13 -2
  52. package/api/src/store/models/product-vendor.ts +6 -0
  53. package/api/src/store/models/promotion-code.ts +295 -18
  54. package/api/src/store/models/types.ts +30 -1
  55. package/api/tests/libs/session.spec.ts +48 -27
  56. package/blocklet.yml +1 -1
  57. package/package.json +20 -20
  58. package/src/app.tsx +2 -0
  59. package/src/components/customer/link.tsx +1 -1
  60. package/src/components/discount/discount-info.tsx +178 -0
  61. package/src/components/invoice/table.tsx +140 -48
  62. package/src/components/invoice-pdf/styles.ts +6 -0
  63. package/src/components/invoice-pdf/template.tsx +59 -33
  64. package/src/components/metadata/form.tsx +14 -5
  65. package/src/components/payment-link/actions.tsx +42 -0
  66. package/src/components/price/form.tsx +91 -65
  67. package/src/components/product/vendor-config.tsx +5 -3
  68. package/src/components/promotion/active-redemptions.tsx +534 -0
  69. package/src/components/promotion/currency-multi-select.tsx +350 -0
  70. package/src/components/promotion/currency-restrictions.tsx +117 -0
  71. package/src/components/promotion/product-select.tsx +292 -0
  72. package/src/components/promotion/promotion-code-form.tsx +534 -0
  73. package/src/components/subscription/portal/list.tsx +6 -1
  74. package/src/components/subscription/vendor-service-list.tsx +13 -2
  75. package/src/locales/en.tsx +227 -0
  76. package/src/locales/zh.tsx +222 -1
  77. package/src/pages/admin/billing/subscriptions/detail.tsx +5 -0
  78. package/src/pages/admin/products/coupons/applicable-products.tsx +166 -0
  79. package/src/pages/admin/products/coupons/create.tsx +612 -0
  80. package/src/pages/admin/products/coupons/detail.tsx +538 -0
  81. package/src/pages/admin/products/coupons/edit.tsx +127 -0
  82. package/src/pages/admin/products/coupons/index.tsx +210 -3
  83. package/src/pages/admin/products/index.tsx +22 -3
  84. package/src/pages/admin/products/products/detail.tsx +12 -2
  85. package/src/pages/admin/products/promotion-codes/actions.tsx +103 -0
  86. package/src/pages/admin/products/promotion-codes/create.tsx +235 -0
  87. package/src/pages/admin/products/promotion-codes/detail.tsx +416 -0
  88. package/src/pages/admin/products/promotion-codes/list.tsx +247 -0
  89. package/src/pages/admin/products/promotion-codes/verification-config.tsx +327 -0
  90. package/src/pages/admin/products/vendors/index.tsx +17 -5
  91. package/src/pages/customer/subscription/detail.tsx +5 -0
  92. package/vite.config.ts +4 -3
@@ -0,0 +1,327 @@
1
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
+ import { api } from '@blocklet/payment-react';
3
+ import { Box, Typography, Chip, Stack, Card, CardContent } from '@mui/material';
4
+ import { styled } from '@mui/system';
5
+ import { useRequest } from 'ahooks';
6
+ import CustomerLink from '../../../../components/customer/link';
7
+
8
+ interface VerificationConfigProps {
9
+ data: {
10
+ verification_type: string;
11
+ nft_config?: {
12
+ nft_addresses?: string[];
13
+ nft_tags?: string[];
14
+ min_balance?: number | string;
15
+ };
16
+ vc_config?: {
17
+ vc_roles?: string[];
18
+ vc_trusted_issuers?: string[];
19
+ };
20
+ customer_dids?: string[];
21
+ };
22
+ }
23
+
24
+ const fetchCustomers = (): Promise<{ list: any[]; count: number }> => {
25
+ return api.get('/api/customers?limit=1000').then((res: any) => res.data);
26
+ };
27
+
28
+ export default function VerificationConfig({ data }: VerificationConfigProps) {
29
+ const { t } = useLocaleContext();
30
+
31
+ const { data: customersData } = useRequest(fetchCustomers, {
32
+ ready: data.verification_type === 'user_restricted' && (data.customer_dids?.length ?? 0) > 0,
33
+ });
34
+
35
+ const customers = customersData?.list || [];
36
+
37
+ const renderNftConfig = () => {
38
+ if (!data.nft_config) {
39
+ return (
40
+ <Typography
41
+ variant="body2"
42
+ sx={{
43
+ color: 'text.secondary',
44
+ }}>
45
+ {t('admin.promotionCode.noNftConfig')}
46
+ </Typography>
47
+ );
48
+ }
49
+
50
+ return (
51
+ <Stack spacing={2}>
52
+ {data.nft_config.nft_addresses && data.nft_config.nft_addresses.length > 0 && (
53
+ <ConfigItem>
54
+ <ConfigLabel>{t('admin.promotionCode.nftAddresses')}</ConfigLabel>
55
+ <ChipContainer>
56
+ {data.nft_config.nft_addresses.map((addr: string) => (
57
+ <Chip
58
+ key={addr}
59
+ label={addr}
60
+ size="small"
61
+ variant="outlined"
62
+ color="default"
63
+ sx={{
64
+ backgroundColor: 'grey.50',
65
+ borderColor: 'grey.300',
66
+ fontFamily: 'monospace',
67
+ fontSize: '0.75rem',
68
+ maxWidth: 200,
69
+ }}
70
+ />
71
+ ))}
72
+ </ChipContainer>
73
+ </ConfigItem>
74
+ )}
75
+ {data.nft_config.nft_tags && data.nft_config.nft_tags.length > 0 && (
76
+ <ConfigItem>
77
+ <ConfigLabel>{t('admin.promotionCode.nftTags')}</ConfigLabel>
78
+ <ChipContainer>
79
+ {data.nft_config.nft_tags.map((tag: string) => (
80
+ <Chip
81
+ key={tag}
82
+ label={tag}
83
+ size="small"
84
+ variant="outlined"
85
+ color="primary"
86
+ sx={{
87
+ backgroundColor: 'primary.50',
88
+ fontWeight: 500,
89
+ }}
90
+ />
91
+ ))}
92
+ </ChipContainer>
93
+ </ConfigItem>
94
+ )}
95
+ {data.nft_config.min_balance && (
96
+ <ConfigItem>
97
+ <ConfigLabel>{t('admin.promotionCode.minBalance')}</ConfigLabel>
98
+ <Typography
99
+ variant="body2"
100
+ sx={{
101
+ fontWeight: 'medium',
102
+ }}>
103
+ {data.nft_config.min_balance}
104
+ </Typography>
105
+ </ConfigItem>
106
+ )}
107
+ </Stack>
108
+ );
109
+ };
110
+
111
+ const renderVcConfig = () => {
112
+ if (!data.vc_config) {
113
+ return (
114
+ <Typography
115
+ variant="body2"
116
+ sx={{
117
+ color: 'text.secondary',
118
+ }}>
119
+ {t('admin.promotionCode.noVcConfig')}
120
+ </Typography>
121
+ );
122
+ }
123
+
124
+ return (
125
+ <Stack spacing={2}>
126
+ {data.vc_config.vc_roles && data.vc_config.vc_roles.length > 0 && (
127
+ <ConfigItem>
128
+ <ConfigLabel>{t('admin.promotionCode.requiredRoles')}</ConfigLabel>
129
+ <ChipContainer>
130
+ {data.vc_config.vc_roles.map((role: string) => (
131
+ <Chip
132
+ key={role}
133
+ label={role}
134
+ size="small"
135
+ variant="outlined"
136
+ color="secondary"
137
+ sx={{
138
+ backgroundColor: 'secondary.50',
139
+ fontWeight: 500,
140
+ textTransform: 'capitalize',
141
+ }}
142
+ />
143
+ ))}
144
+ </ChipContainer>
145
+ </ConfigItem>
146
+ )}
147
+
148
+ {data.vc_config.vc_trusted_issuers && data.vc_config.vc_trusted_issuers.length > 0 && (
149
+ <ConfigItem>
150
+ <ConfigLabel>{t('admin.promotionCode.trustedIssuers')}</ConfigLabel>
151
+ <ChipContainer>
152
+ {data.vc_config.vc_trusted_issuers.map((issuer: string) => (
153
+ <Chip
154
+ key={issuer}
155
+ label={issuer}
156
+ size="small"
157
+ variant="outlined"
158
+ color="info"
159
+ sx={{
160
+ backgroundColor: 'info.50',
161
+ fontFamily: 'monospace',
162
+ fontSize: '0.75rem',
163
+ maxWidth: 200,
164
+ }}
165
+ />
166
+ ))}
167
+ </ChipContainer>
168
+ </ConfigItem>
169
+ )}
170
+ </Stack>
171
+ );
172
+ };
173
+
174
+ const renderUserRestrictedConfig = () => {
175
+ if (!data.customer_dids?.length) {
176
+ return (
177
+ <Typography
178
+ variant="body2"
179
+ sx={{
180
+ color: 'text.secondary',
181
+ }}>
182
+ {t('admin.promotionCode.noUsersSpecified')}
183
+ </Typography>
184
+ );
185
+ }
186
+
187
+ return (
188
+ <ConfigItem>
189
+ <ConfigLabel>{t('admin.promotionCode.allowedUsers')}</ConfigLabel>
190
+ <UserListContainer>
191
+ {data.customer_dids.map((did: string) => {
192
+ const customer = customers.find((c: any) => c.did === did || c.id === did);
193
+ if (customer) {
194
+ return (
195
+ <CustomerLink
196
+ key={did}
197
+ customer={customer}
198
+ size="small"
199
+ cardProps={{
200
+ sx: {
201
+ border: 'none',
202
+ p: 0,
203
+ minWidth: 0,
204
+ },
205
+ }}
206
+ />
207
+ );
208
+ }
209
+ // 如果找不到客户信息,显示 DID
210
+ return (
211
+ <Chip
212
+ key={did}
213
+ label={did}
214
+ size="small"
215
+ variant="outlined"
216
+ color="default"
217
+ sx={{
218
+ maxWidth: 200,
219
+ backgroundColor: 'grey.50',
220
+ borderColor: 'grey.300',
221
+ }}
222
+ />
223
+ );
224
+ })}
225
+ </UserListContainer>
226
+ </ConfigItem>
227
+ );
228
+ };
229
+
230
+ const renderConfig = () => {
231
+ switch (data.verification_type) {
232
+ case 'nft':
233
+ return renderNftConfig();
234
+ case 'vc':
235
+ return renderVcConfig();
236
+ case 'user_restricted':
237
+ return renderUserRestrictedConfig();
238
+ case 'code':
239
+ default:
240
+ return (
241
+ <Typography
242
+ variant="body2"
243
+ sx={{
244
+ color: 'text.secondary',
245
+ fontStyle: 'italic',
246
+ }}>
247
+ {t('admin.promotionCode.codeVerificationOnly')}
248
+ </Typography>
249
+ );
250
+ }
251
+ };
252
+
253
+ return (
254
+ <VerificationCard>
255
+ <CardContent>
256
+ <Stack spacing={2}>
257
+ <Box
258
+ sx={{
259
+ display: 'flex',
260
+ flexDirection: {
261
+ xs: 'column',
262
+ sm: 'row',
263
+ },
264
+ gap: {
265
+ xs: 1,
266
+ sm: 2,
267
+ },
268
+ alignItems: {
269
+ xs: 'flex-start',
270
+ sm: 'center',
271
+ },
272
+ }}>
273
+ <Typography
274
+ variant="caption"
275
+ sx={{
276
+ color: 'text.secondary',
277
+ }}>
278
+ {t('admin.promotionCode.verificationType')}
279
+ </Typography>
280
+ <Typography
281
+ variant="subtitle2"
282
+ sx={{
283
+ color: 'text.primary',
284
+ fontWeight: 'medium',
285
+ }}>
286
+ {t(`admin.promotionCode.verificationTypeMap.${data.verification_type}`)}
287
+ </Typography>
288
+ </Box>
289
+ {renderConfig()}
290
+ </Stack>
291
+ </CardContent>
292
+ </VerificationCard>
293
+ );
294
+ }
295
+
296
+ const VerificationCard = styled(Card)(({ theme }) => ({
297
+ border: `1px solid ${theme.palette.divider}`,
298
+ boxShadow: 'none',
299
+ backgroundColor: theme.palette.background.paper,
300
+ }));
301
+
302
+ const ConfigItem = styled(Box)({
303
+ display: 'flex',
304
+ flexDirection: 'column',
305
+ gap: 8,
306
+ });
307
+
308
+ const ConfigLabel = styled(Typography)(({ theme }) => ({
309
+ fontSize: '0.875rem',
310
+ fontWeight: 500,
311
+ color: theme.palette.text.secondary,
312
+ }));
313
+
314
+ const ChipContainer = styled(Box)({
315
+ display: 'flex',
316
+ flexDirection: 'row',
317
+ flexWrap: 'wrap',
318
+ gap: 8,
319
+ });
320
+
321
+ const UserListContainer = styled(Box)({
322
+ display: 'flex',
323
+ flexDirection: 'row',
324
+ flexWrap: 'wrap',
325
+ gap: 8,
326
+ alignItems: 'center',
327
+ });
@@ -96,7 +96,12 @@ export default function VendorsList() {
96
96
  <InfoCard
97
97
  name={item.name}
98
98
  description={
99
- <Typography variant="body2" color="text.secondary" sx={{ textTransform: 'capitalize' }}>
99
+ <Typography
100
+ variant="body2"
101
+ sx={{
102
+ color: 'text.secondary',
103
+ textTransform: 'capitalize',
104
+ }}>
100
105
  {item.vendor_type}
101
106
  </Typography>
102
107
  }
@@ -114,7 +119,11 @@ export default function VendorsList() {
114
119
  customBodyRenderLite: (_: string, index: number) => {
115
120
  const item = data.list[index] as Vendor;
116
121
  return (
117
- <Typography variant="body2" color="text.secondary">
122
+ <Typography
123
+ variant="body2"
124
+ sx={{
125
+ color: 'text.secondary',
126
+ }}>
118
127
  {item.description || '-'}
119
128
  </Typography>
120
129
  );
@@ -202,7 +211,12 @@ export default function VendorsList() {
202
211
  border: '1px solid',
203
212
  borderColor: 'divider',
204
213
  }}>
205
- <Typography variant="body2" color="text.secondary" sx={{ minWidth: 'fit-content' }}>
214
+ <Typography
215
+ variant="body2"
216
+ sx={{
217
+ color: 'text.secondary',
218
+ minWidth: 'fit-content',
219
+ }}>
206
220
  {t('admin.vendor.servicePublicKey')}:
207
221
  <Tooltip title={copySuccess ? t('common.copied') : t('common.copy')}>
208
222
  <IconButton
@@ -236,7 +250,6 @@ export default function VendorsList() {
236
250
  </Box>
237
251
  </Box>
238
252
  )}
239
-
240
253
  <Table
241
254
  hasRowLink
242
255
  durable={`__${listKey}__`}
@@ -284,7 +297,6 @@ export default function VendorsList() {
284
297
  loading={!data.list}
285
298
  onChange={onTableChange}
286
299
  />
287
-
288
300
  {selectedVendor && (
289
301
  <VendorCreate
290
302
  open={detailOpen}
@@ -47,6 +47,7 @@ import SubscriptionItemList from '../../../components/subscription/items';
47
47
  import SubscriptionMetrics from '../../../components/subscription/metrics';
48
48
  import SubscriptionActions, { ActionMethods } from '../../../components/subscription/portal/actions';
49
49
  import VendorServiceList from '../../../components/subscription/vendor-service-list';
50
+ import DiscountInfo from '../../../components/discount/discount-info';
50
51
  import { useSessionContext } from '../../../contexts/session';
51
52
  import { usePendingAmountForSubscription, useUnpaidInvoicesCheckForSubscription } from '../../../hooks/subscription';
52
53
  import { formatSmartDuration, TimeUnit } from '../../../libs/dayjs';
@@ -679,6 +680,10 @@ export default function CustomerSubscriptionDetail() {
679
680
  </InfoRowGroup>
680
681
  </Box>
681
682
  <Divider />
683
+
684
+ {/* Discount Information */}
685
+ {(data as any).discountStats && <DiscountInfo discountStats={(data as any).discountStats} />}
686
+
682
687
  <Box className="section">
683
688
  <Typography
684
689
  variant="h3"
package/vite.config.ts CHANGED
@@ -56,9 +56,10 @@ export default defineConfig(({ mode }) => {
56
56
  svgr(),
57
57
  process.env.ANALYZE && visualizer({ open: true, gzipSize: true, brotliSize: true }),
58
58
  // @note: 如果开发期间,文件隔离模式报错了,那么可以关闭server的文件隔离模式
59
- codeInspectorPlugin({
60
- bundler: 'vite',
61
- }),
59
+ isProduction &&
60
+ codeInspectorPlugin({
61
+ bundler: 'vite',
62
+ }),
62
63
  ].filter(Boolean),
63
64
 
64
65
  build: {