payment-kit 1.20.11 → 1.20.12
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.
- package/api/src/index.ts +2 -0
- package/api/src/integrations/stripe/handlers/invoice.ts +63 -5
- package/api/src/integrations/stripe/handlers/payment-intent.ts +1 -0
- package/api/src/integrations/stripe/resource.ts +253 -2
- package/api/src/libs/currency.ts +31 -0
- package/api/src/libs/discount/coupon.ts +1061 -0
- package/api/src/libs/discount/discount.ts +349 -0
- package/api/src/libs/discount/nft.ts +239 -0
- package/api/src/libs/discount/redemption.ts +636 -0
- package/api/src/libs/discount/vc.ts +73 -0
- package/api/src/libs/invoice.ts +44 -10
- package/api/src/libs/math-utils.ts +6 -0
- package/api/src/libs/price.ts +43 -0
- package/api/src/libs/session.ts +242 -57
- package/api/src/libs/subscription.ts +2 -6
- package/api/src/queues/auto-recharge.ts +1 -1
- package/api/src/queues/discount-status.ts +200 -0
- package/api/src/queues/subscription.ts +98 -5
- package/api/src/queues/usage-record.ts +1 -1
- package/api/src/routes/auto-recharge-configs.ts +5 -3
- package/api/src/routes/checkout-sessions.ts +755 -64
- package/api/src/routes/connect/change-payment.ts +6 -1
- package/api/src/routes/connect/change-plan.ts +6 -1
- package/api/src/routes/connect/setup.ts +6 -1
- package/api/src/routes/connect/shared.ts +80 -9
- package/api/src/routes/connect/subscribe.ts +12 -2
- package/api/src/routes/coupons.ts +518 -0
- package/api/src/routes/index.ts +4 -0
- package/api/src/routes/invoices.ts +44 -3
- package/api/src/routes/meter-events.ts +2 -1
- package/api/src/routes/payment-currencies.ts +1 -0
- package/api/src/routes/promotion-codes.ts +482 -0
- package/api/src/routes/subscriptions.ts +23 -2
- package/api/src/store/migrations/20250904-discount.ts +136 -0
- package/api/src/store/migrations/20250910-timestamp-fields.ts +116 -0
- package/api/src/store/migrations/20250916-add-description-fields.ts +30 -0
- package/api/src/store/models/checkout-session.ts +12 -0
- package/api/src/store/models/coupon.ts +144 -4
- package/api/src/store/models/discount.ts +23 -10
- package/api/src/store/models/index.ts +13 -2
- package/api/src/store/models/promotion-code.ts +295 -18
- package/api/src/store/models/types.ts +30 -1
- package/api/tests/libs/session.spec.ts +48 -27
- package/blocklet.yml +1 -1
- package/package.json +20 -20
- package/src/app.tsx +2 -0
- package/src/components/customer/link.tsx +1 -1
- package/src/components/discount/discount-info.tsx +178 -0
- package/src/components/invoice/table.tsx +140 -48
- package/src/components/invoice-pdf/styles.ts +6 -0
- package/src/components/invoice-pdf/template.tsx +59 -33
- package/src/components/metadata/form.tsx +14 -5
- package/src/components/payment-link/actions.tsx +42 -0
- package/src/components/price/form.tsx +91 -65
- package/src/components/product/vendor-config.tsx +5 -3
- package/src/components/promotion/active-redemptions.tsx +534 -0
- package/src/components/promotion/currency-multi-select.tsx +350 -0
- package/src/components/promotion/currency-restrictions.tsx +117 -0
- package/src/components/promotion/product-select.tsx +292 -0
- package/src/components/promotion/promotion-code-form.tsx +534 -0
- package/src/components/subscription/portal/list.tsx +6 -1
- package/src/components/subscription/vendor-service-list.tsx +13 -2
- package/src/locales/en.tsx +227 -0
- package/src/locales/zh.tsx +222 -1
- package/src/pages/admin/billing/subscriptions/detail.tsx +5 -0
- package/src/pages/admin/products/coupons/applicable-products.tsx +166 -0
- package/src/pages/admin/products/coupons/create.tsx +612 -0
- package/src/pages/admin/products/coupons/detail.tsx +538 -0
- package/src/pages/admin/products/coupons/edit.tsx +127 -0
- package/src/pages/admin/products/coupons/index.tsx +210 -3
- package/src/pages/admin/products/index.tsx +22 -3
- package/src/pages/admin/products/products/detail.tsx +12 -2
- package/src/pages/admin/products/promotion-codes/actions.tsx +103 -0
- package/src/pages/admin/products/promotion-codes/create.tsx +235 -0
- package/src/pages/admin/products/promotion-codes/detail.tsx +416 -0
- package/src/pages/admin/products/promotion-codes/list.tsx +247 -0
- package/src/pages/admin/products/promotion-codes/verification-config.tsx +327 -0
- package/src/pages/admin/products/vendors/index.tsx +17 -5
- package/src/pages/customer/subscription/detail.tsx +5 -0
- 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
|
+
}
|