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,292 @@
1
+ import { useState } from 'react';
2
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
3
+ import { Box, Avatar, Typography, Stack, IconButton, Menu, MenuItem, Select, FormControl } from '@mui/material';
4
+ import { AddOutlined, MoreHorizOutlined } from '@mui/icons-material';
5
+ import type { TProductExpanded } from '@blocklet/payment-types';
6
+ import { findCurrency, formatPrice, usePaymentContext } from '@blocklet/payment-react';
7
+ import { dispatch } from 'use-bus';
8
+ import cloneDeep from 'lodash/cloneDeep';
9
+ import { useProductsContext } from '../../contexts/products';
10
+
11
+ type ProductDynamicSelectProps = {
12
+ value?: string[];
13
+ onChange: (productIds: string[]) => void;
14
+ disabled?: boolean;
15
+ };
16
+
17
+ const filterPrices = (product: TProductExpanded, hasSelected: (price: any) => boolean) => {
18
+ product.prices = product.prices.filter((x) => {
19
+ const isActive = x.active;
20
+ const notSelected = !hasSelected(x);
21
+ return isActive && notSelected;
22
+ });
23
+ return product;
24
+ };
25
+
26
+ const filterProducts = (products: TProductExpanded[], hasSelected: (price: any) => boolean) => {
27
+ const filtered = cloneDeep(products).map((x) => filterPrices(x, hasSelected));
28
+ return filtered.filter((x) => x.prices.length);
29
+ };
30
+
31
+ export default function ProductDynamicSelect({ value = [], onChange, disabled = false }: ProductDynamicSelectProps) {
32
+ const { t } = useLocaleContext();
33
+ const { products } = useProductsContext();
34
+ const { settings } = usePaymentContext();
35
+ const [menuAnchor, setMenuAnchor] = useState<null | HTMLElement>(null);
36
+ const [selectedProductForMenu, setSelectedProductForMenu] = useState<string | null>(null);
37
+ const [showAdditionalSelector, setShowAdditionalSelector] = useState(false);
38
+ const [selectorOpen, setSelectorOpen] = useState(false);
39
+
40
+ const selectedProducts = (products || []).filter((product) => value.includes(product.id));
41
+ const availableProducts = (products || []).filter((product) => !value.includes(product.id));
42
+
43
+ const getPriceDisplay = (product: TProductExpanded) => {
44
+ if (!product.prices || product.prices.length === 0) {
45
+ return null;
46
+ }
47
+
48
+ if (product.prices.length === 1) {
49
+ const price = product.prices[0];
50
+ if (price) {
51
+ const currency = findCurrency(settings.paymentMethods, price.currency_id ?? '') || settings.baseCurrency;
52
+ return formatPrice(price, currency!);
53
+ }
54
+ }
55
+
56
+ return t('admin.price.count', { count: product.prices.length });
57
+ };
58
+
59
+ const handleProductSelect = (productId: string) => {
60
+ if (productId === 'add') {
61
+ dispatch('product.create.request');
62
+ return;
63
+ }
64
+
65
+ if (productId && !value.includes(productId)) {
66
+ const newValue = [...value, productId];
67
+ onChange(newValue);
68
+ // Hide the additional selector and close dropdown after selection
69
+ setShowAdditionalSelector(false);
70
+ setSelectorOpen(false);
71
+ }
72
+ };
73
+
74
+ const handleRemoveProduct = (productId: string) => {
75
+ const newValue = value.filter((id) => id !== productId);
76
+ onChange(newValue);
77
+ setMenuAnchor(null);
78
+ setSelectedProductForMenu(null);
79
+ };
80
+
81
+ const handleMenuClick = (event: React.MouseEvent<HTMLElement>, productId: string) => {
82
+ setMenuAnchor(event.currentTarget);
83
+ setSelectedProductForMenu(productId);
84
+ };
85
+
86
+ const handleMenuClose = () => {
87
+ setMenuAnchor(null);
88
+ setSelectedProductForMenu(null);
89
+ };
90
+
91
+ const handleAddAnotherProduct = () => {
92
+ setShowAdditionalSelector(true);
93
+ setSelectorOpen(true);
94
+ };
95
+
96
+ const handleSelectorClose = () => {
97
+ setSelectorOpen(false);
98
+ };
99
+
100
+ const handleSelectorOpen = () => {
101
+ setSelectorOpen(true);
102
+ };
103
+
104
+ const renderProductItems = () => {
105
+ const filteredProducts = filterProducts(products, () => false); // Show all available products
106
+
107
+ if (filteredProducts.length === 0) {
108
+ return [];
109
+ }
110
+
111
+ return filteredProducts.map((product) => (
112
+ <MenuItem
113
+ key={`product-${product.id}`}
114
+ value={product.id}
115
+ disabled={value.includes(product.id)} // Disable if already selected
116
+ >
117
+ <Stack
118
+ direction="row"
119
+ spacing={1}
120
+ sx={{
121
+ alignItems: 'center',
122
+ width: '100%',
123
+ }}>
124
+ {product.images[0] ? (
125
+ <Avatar src={product.images[0]} alt={product.name} variant="square" sx={{ width: 24, height: 24 }} />
126
+ ) : (
127
+ <Avatar variant="square" sx={{ width: 24, height: 24, fontSize: '0.75rem' }}>
128
+ {product.name.slice(0, 1)}
129
+ </Avatar>
130
+ )}
131
+ <Box sx={{ flex: 1, minWidth: 0 }}>
132
+ <Typography variant="body2" noWrap>
133
+ {product.name}
134
+ </Typography>
135
+ </Box>
136
+ <Typography
137
+ variant="body2"
138
+ sx={{
139
+ color: 'text.secondary',
140
+ ml: 'auto',
141
+ flexShrink: 0,
142
+ }}>
143
+ {value.includes(product.id) ? 'Added' : getPriceDisplay(product)}
144
+ </Typography>
145
+ </Stack>
146
+ </MenuItem>
147
+ ));
148
+ };
149
+
150
+ return (
151
+ <Stack spacing={2}>
152
+ {/* Selected Products */}
153
+ {selectedProducts.map((product) => (
154
+ <Box
155
+ key={product.id}
156
+ sx={{
157
+ display: 'flex',
158
+ alignItems: 'center',
159
+ gap: 2,
160
+ p: 2,
161
+ border: '1px solid',
162
+ borderColor: 'divider',
163
+ borderRadius: 1,
164
+ backgroundColor: 'background.paper',
165
+ }}>
166
+ {/* Product Icon */}
167
+ {product.images[0] ? (
168
+ <Avatar src={product.images[0]} alt={product.name} variant="square" sx={{ width: 40, height: 40 }} />
169
+ ) : (
170
+ <Avatar variant="square" sx={{ width: 40, height: 40, fontSize: '1rem' }}>
171
+ {product.name.slice(0, 1)}
172
+ </Avatar>
173
+ )}
174
+
175
+ {/* Product Info */}
176
+ <Box sx={{ flex: 1, minWidth: 0 }}>
177
+ <Typography
178
+ variant="body1"
179
+ noWrap
180
+ sx={{
181
+ fontWeight: 'medium',
182
+ }}>
183
+ {product.name}
184
+ </Typography>
185
+ {getPriceDisplay(product) && (
186
+ <Typography
187
+ variant="body2"
188
+ sx={{
189
+ color: 'text.secondary',
190
+ }}>
191
+ {getPriceDisplay(product)}
192
+ </Typography>
193
+ )}
194
+ </Box>
195
+
196
+ {/* Menu Button */}
197
+ <IconButton
198
+ size="small"
199
+ disabled={disabled}
200
+ onClick={(e) => handleMenuClick(e, product.id)}
201
+ sx={{ color: 'text.secondary' }}>
202
+ <MoreHorizOutlined />
203
+ </IconButton>
204
+ </Box>
205
+ ))}
206
+ {/* Product Selector - Show if no products selected OR if user clicked "add another" */}
207
+ {!disabled && availableProducts.length > 0 && (value.length === 0 || showAdditionalSelector) && (
208
+ <FormControl fullWidth size="small">
209
+ <Select
210
+ value=""
211
+ open={selectorOpen}
212
+ onClose={handleSelectorClose}
213
+ onOpen={handleSelectorOpen}
214
+ displayEmpty
215
+ onChange={(e) => handleProductSelect(e.target.value as string)}
216
+ renderValue={(selected) => {
217
+ if (!selected) {
218
+ return (
219
+ <Typography
220
+ variant="body2"
221
+ sx={{
222
+ color: 'text.secondary',
223
+ }}>
224
+ {t('admin.product.find')}
225
+ </Typography>
226
+ );
227
+ }
228
+ return selected;
229
+ }}
230
+ MenuProps={{
231
+ style: { maxHeight: 480 },
232
+ }}>
233
+ <MenuItem value="add">
234
+ <Stack
235
+ direction="row"
236
+ spacing={1}
237
+ sx={{
238
+ alignItems: 'center',
239
+ }}>
240
+ <AddOutlined fontSize="small" color="primary" />
241
+ <Typography
242
+ variant="body2"
243
+ sx={{
244
+ color: 'primary.main',
245
+ fontWeight: 'medium',
246
+ }}>
247
+ {t('admin.product.add')}
248
+ </Typography>
249
+ </Stack>
250
+ </MenuItem>
251
+ {renderProductItems()}
252
+ </Select>
253
+ </FormControl>
254
+ )}
255
+ {/* Add Another Product Link - Only show if products are selected and additional selector is not shown */}
256
+ {!disabled && availableProducts.length > 0 && value.length > 0 && !showAdditionalSelector && (
257
+ <Typography
258
+ variant="body2"
259
+ component="button"
260
+ onClick={handleAddAnotherProduct}
261
+ sx={{
262
+ color: 'primary.main',
263
+ textDecoration: 'none',
264
+ cursor: 'pointer',
265
+ border: 'none',
266
+ background: 'none',
267
+ p: 0,
268
+ textAlign: 'left',
269
+ fontWeight: 'normal',
270
+ '&:hover': {
271
+ textDecoration: 'underline',
272
+ },
273
+ }}>
274
+ {t('admin.coupon.addProduct')}
275
+ </Typography>
276
+ )}
277
+ {/* Context Menu for Product Actions */}
278
+ <Menu
279
+ anchorEl={menuAnchor}
280
+ open={Boolean(menuAnchor)}
281
+ onClose={handleMenuClose}
282
+ anchorOrigin={{
283
+ vertical: 'bottom',
284
+ horizontal: 'right',
285
+ }}>
286
+ <MenuItem onClick={() => selectedProductForMenu && handleRemoveProduct(selectedProductForMenu)}>
287
+ {t('admin.product.remove')}
288
+ </MenuItem>
289
+ </Menu>
290
+ </Stack>
291
+ );
292
+ }