payment-kit 1.20.5 → 1.20.6

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 (39) hide show
  1. package/api/src/crons/index.ts +11 -3
  2. package/api/src/index.ts +18 -14
  3. package/api/src/libs/adapters/launcher-adapter.ts +177 -0
  4. package/api/src/libs/env.ts +7 -0
  5. package/api/src/libs/url.ts +77 -0
  6. package/api/src/libs/vendor-adapter-factory.ts +22 -0
  7. package/api/src/libs/vendor-adapter.ts +109 -0
  8. package/api/src/libs/vendor-fulfillment.ts +321 -0
  9. package/api/src/queues/payment.ts +14 -10
  10. package/api/src/queues/payout.ts +1 -0
  11. package/api/src/queues/vendor/vendor-commission.ts +192 -0
  12. package/api/src/queues/vendor/vendor-fulfillment-coordinator.ts +627 -0
  13. package/api/src/queues/vendor/vendor-fulfillment.ts +97 -0
  14. package/api/src/queues/vendor/vendor-status-check.ts +179 -0
  15. package/api/src/routes/checkout-sessions.ts +3 -0
  16. package/api/src/routes/index.ts +2 -0
  17. package/api/src/routes/products.ts +72 -1
  18. package/api/src/routes/vendor.ts +526 -0
  19. package/api/src/store/migrations/20250820-add-product-vendor.ts +102 -0
  20. package/api/src/store/migrations/20250822-add-vendor-config-to-products.ts +56 -0
  21. package/api/src/store/models/checkout-session.ts +84 -18
  22. package/api/src/store/models/index.ts +3 -0
  23. package/api/src/store/models/payout.ts +11 -0
  24. package/api/src/store/models/product-vendor.ts +118 -0
  25. package/api/src/store/models/product.ts +15 -0
  26. package/blocklet.yml +8 -2
  27. package/doc/vendor_fulfillment_system.md +931 -0
  28. package/package.json +5 -4
  29. package/src/components/collapse.tsx +1 -0
  30. package/src/components/product/edit.tsx +9 -0
  31. package/src/components/product/form.tsx +11 -0
  32. package/src/components/product/vendor-config.tsx +249 -0
  33. package/src/components/vendor/actions.tsx +145 -0
  34. package/src/locales/en.tsx +89 -0
  35. package/src/locales/zh.tsx +89 -0
  36. package/src/pages/admin/products/index.tsx +11 -1
  37. package/src/pages/admin/products/products/detail.tsx +79 -2
  38. package/src/pages/admin/products/vendors/create.tsx +418 -0
  39. package/src/pages/admin/products/vendors/index.tsx +313 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "payment-kit",
3
- "version": "1.20.5",
3
+ "version": "1.20.6",
4
4
  "scripts": {
5
5
  "dev": "blocklet dev --open",
6
6
  "lint": "tsc --noEmit && eslint src api/src --ext .mjs,.js,.jsx,.ts,.tsx",
@@ -55,7 +55,8 @@
55
55
  "@blocklet/error": "^0.2.5",
56
56
  "@blocklet/js-sdk": "^1.16.52-beta-20250908-085420-224a58fa",
57
57
  "@blocklet/logger": "^1.16.52-beta-20250908-085420-224a58fa",
58
- "@blocklet/payment-react": "1.20.5",
58
+ "@blocklet/payment-react": "1.20.6",
59
+ "@blocklet/payment-vendor": "1.20.6",
59
60
  "@blocklet/sdk": "^1.16.52-beta-20250908-085420-224a58fa",
60
61
  "@blocklet/ui-react": "^3.1.37",
61
62
  "@blocklet/uploader": "^0.2.10",
@@ -124,7 +125,7 @@
124
125
  "devDependencies": {
125
126
  "@abtnode/types": "^1.16.52-beta-20250908-085420-224a58fa",
126
127
  "@arcblock/eslint-config-ts": "^0.3.3",
127
- "@blocklet/payment-types": "1.20.5",
128
+ "@blocklet/payment-types": "1.20.6",
128
129
  "@types/cookie-parser": "^1.4.9",
129
130
  "@types/cors": "^2.8.19",
130
131
  "@types/debug": "^4.1.12",
@@ -171,5 +172,5 @@
171
172
  "parser": "typescript"
172
173
  }
173
174
  },
174
- "gitHead": "998ec07a1e35502729edcc3689305a1afa3b0390"
175
+ "gitHead": "d04ed7906bd1e13240198623aa0d4eef4db8886e"
175
176
  }
@@ -74,6 +74,7 @@ export default function IconCollapse(rawProps: Props) {
74
74
  {props.addons} {expanded ? <ExpandLessOutlined /> : <ExpandMoreOutlined />}
75
75
  </Stack>
76
76
  </Stack>
77
+
77
78
  <Collapse in={expanded} sx={{ width: '100%' }}>
78
79
  {expanded || props.lazy ? props.children : null}
79
80
  </Collapse>
@@ -30,6 +30,15 @@ export default function EditProduct({
30
30
  : // @ts-ignore
31
31
  Object.keys(product.metadata).map((x) => ({ key: x, value: product.metadata[x] })),
32
32
  prices: [],
33
+ // 处理 vendor_config 数据回显
34
+ vendor_config: product.vendor_config
35
+ ? product.vendor_config.map((vendor: any) => ({
36
+ vendor_id: vendor.vendor_id, // 直接使用 vendor_id
37
+ commission_rate: vendor.commission_rate?.toString() || '0',
38
+ commission_type:
39
+ vendor.commission_type === 'fixed_amount' ? 'fixed' : vendor.commission_type || 'percentage',
40
+ }))
41
+ : [],
33
42
  },
34
43
  });
35
44
 
@@ -9,9 +9,15 @@ import MetadataForm from '../metadata/form';
9
9
  import type { Price } from '../price/form';
10
10
  import Uploader from '../uploader';
11
11
  import ProductFeatures from './features';
12
+ import VendorConfig from './vendor-config';
12
13
 
13
14
  export type Product = InferFormType<TProduct> & {
14
15
  prices: Price[];
16
+ vendor_config: {
17
+ vendor_id: string;
18
+ commission_rate: number;
19
+ commission_type: 'percentage' | 'fixed';
20
+ }[];
15
21
  };
16
22
 
17
23
  type Props = {
@@ -114,6 +120,11 @@ export default function ProductForm({ simple = false }: Props) {
114
120
  <Uploader onUploaded={onUploaded} preview={images[0]} />
115
121
  </Box>
116
122
  </Stack>
123
+ <Stack sx={{ '& .vendor-config': { mt: 2 } }}>
124
+ <Collapse trigger={t('admin.product.vendorConfig.title')}>
125
+ <VendorConfig />
126
+ </Collapse>
127
+ </Stack>
117
128
  <Collapse trigger={t('admin.product.additional')}>
118
129
  <Stack
119
130
  spacing={2}
@@ -0,0 +1,249 @@
1
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
+ import { FormInput, FormLabel, api } from '@blocklet/payment-react';
3
+ import { AddOutlined, DeleteOutlineOutlined } from '@mui/icons-material';
4
+ import { Alert, Box, Button, Divider, IconButton, MenuItem, Select, Stack, Typography } from '@mui/material';
5
+ import { useEffect, useRef, useState } from 'react';
6
+ import { Controller, useFieldArray, useFormContext, useWatch } from 'react-hook-form';
7
+
8
+ interface Vendor {
9
+ id: string;
10
+ name: string;
11
+ description: string;
12
+ vendor_key: string;
13
+ default_commission_rate: number;
14
+ default_commission_type: 'percentage' | 'fixed_amount';
15
+ }
16
+
17
+ export default function VendorConfig() {
18
+ const { t } = useLocaleContext();
19
+ const { control, setValue, setError: setFormError, clearErrors } = useFormContext();
20
+ const [vendors, setVendors] = useState<Vendor[]>([]);
21
+ const [loading, setLoading] = useState(false);
22
+ const [error, setError] = useState<string | null>(null);
23
+ const lastItemRef = useRef<HTMLDivElement | null>(null);
24
+
25
+ const vendorConfigs = useFieldArray({ control, name: 'vendor_config' });
26
+
27
+ useEffect(() => {
28
+ const fetchVendors = async () => {
29
+ try {
30
+ setLoading(true);
31
+ setError(null);
32
+ const response = await api.get('/api/vendors');
33
+ setVendors(response.data?.data || []);
34
+ } catch (err: any) {
35
+ setError(err.message || 'Failed to fetch vendors');
36
+ console.error('Failed to fetch vendors:', err);
37
+ } finally {
38
+ setLoading(false);
39
+ }
40
+ };
41
+
42
+ fetchVendors();
43
+ }, []);
44
+
45
+ const selectedVendorIds = vendorConfigs.fields.map((item: any) => item.vendor_id);
46
+ const noneSelectedVendors = vendors.filter((v) => !selectedVendorIds.includes(v.id));
47
+
48
+ const watchedVendorConfig = useWatch({ control, name: 'vendor_config' });
49
+
50
+ const [totalCommission, setTotalCommission] = useState(0);
51
+
52
+ useEffect(() => {
53
+ const vendorConfigArray = watchedVendorConfig || [];
54
+
55
+ const percentageFields = vendorConfigArray.filter((field: any) => field?.commission_type === 'percentage');
56
+
57
+ const total = percentageFields.reduce((sum: number, field: any) => {
58
+ const rate = Number(field?.commission_rate || 0);
59
+ return sum + rate;
60
+ }, 0);
61
+
62
+ setTotalCommission(total);
63
+
64
+ const isValid = total <= 100;
65
+
66
+ if (vendorConfigArray.length > 0) {
67
+ if (isValid) {
68
+ clearErrors('vendor_config_total');
69
+ } else {
70
+ setFormError('vendor_config_total', {
71
+ type: 'manual',
72
+ message: t('admin.product.vendorConfig.totalCommissionExceeded', {
73
+ total: total.toFixed(0),
74
+ }),
75
+ });
76
+ }
77
+ } else {
78
+ clearErrors('vendor_config_total');
79
+ }
80
+ }, [watchedVendorConfig, clearErrors, setFormError, t]);
81
+
82
+ const validateTotalCommission = () => {
83
+ return totalCommission <= 100;
84
+ };
85
+
86
+ return (
87
+ <Box sx={{ width: '100%', height: '100%', display: 'flex', flexDirection: 'column' }} className="vendor-config">
88
+ {error && (
89
+ <Alert severity="error" onClose={() => setError(null)} sx={{ mt: 1 }}>
90
+ {error}
91
+ </Alert>
92
+ )}
93
+
94
+ <Stack
95
+ sx={{
96
+ maxHeight: 400,
97
+ minHeight: 'auto',
98
+ overflow: 'auto',
99
+ flex: 1,
100
+ }}>
101
+ {vendorConfigs.fields.map((vendorField: any, index: number) => (
102
+ <Stack
103
+ key={vendorField.id}
104
+ ref={index === vendorConfigs.fields.length - 1 ? lastItemRef : null}
105
+ sx={{ mb: 2, alignItems: 'flex-end', '&:last-child': { mb: 0 } }}
106
+ spacing={2}
107
+ direction="row">
108
+ <Stack direction="row" spacing={2} sx={{ flex: 1 }}>
109
+ <Controller
110
+ name={`vendor_config.${index}.vendor_id`}
111
+ control={control}
112
+ render={({ field }) => {
113
+ return (
114
+ <Box sx={{ minWidth: 320 }}>
115
+ <FormLabel>{t('admin.product.vendorConfig.productCode')}</FormLabel>
116
+ <Select
117
+ {...field}
118
+ fullWidth
119
+ onChange={(e) => {
120
+ const vendorId = e.target.value as string;
121
+ field.onChange(vendorId);
122
+
123
+ const selectedVendor = vendors.find((v) => v.id === vendorId);
124
+ if (selectedVendor) {
125
+ setValue(`vendor_config.${index}.commission_rate`, selectedVendor.default_commission_rate);
126
+ const commissionType =
127
+ selectedVendor.default_commission_type === 'fixed_amount'
128
+ ? 'fixed'
129
+ : selectedVendor.default_commission_type;
130
+ setValue(`vendor_config.${index}.commission_type`, commissionType);
131
+ }
132
+ }}
133
+ renderValue={(value: string) => {
134
+ const vendor = vendors.find((v) => v.id === value);
135
+ if (!vendor) return null;
136
+ return <Box sx={{ display: 'flex', flexDirection: 'column' }}>{vendor.name}</Box>;
137
+ }}>
138
+ {noneSelectedVendors.length ? (
139
+ noneSelectedVendors.map((vendor) => (
140
+ <MenuItem key={vendor.id} value={vendor.id} sx={{ gap: 1 }}>
141
+ <Typography variant="h5">{vendor.name}</Typography>
142
+ <Typography variant="body2" color="text.secondary">
143
+ {vendor.vendor_key}
144
+ </Typography>
145
+ </MenuItem>
146
+ ))
147
+ ) : (
148
+ <MenuItem disabled value="">
149
+ {t('admin.product.vendorConfig.noVendor')}
150
+ </MenuItem>
151
+ )}
152
+ </Select>
153
+ </Box>
154
+ );
155
+ }}
156
+ />
157
+
158
+ <Controller
159
+ name={`vendor_config.${index}.commission_type`}
160
+ control={control}
161
+ render={({ field }) => (
162
+ <Box sx={{ minWidth: 140, display: 'none' }}>
163
+ <FormLabel>{t('admin.product.vendorConfig.commissionType')}</FormLabel>
164
+ <Select {...field} fullWidth>
165
+ <MenuItem value="percentage">{t('admin.vendor.percentage')}</MenuItem>
166
+ <MenuItem value="fixed">{t('admin.vendor.fixedAmount')}</MenuItem>
167
+ </Select>
168
+ </Box>
169
+ )}
170
+ />
171
+
172
+ <Controller
173
+ name={`vendor_config.${index}.commission_rate`}
174
+ control={control}
175
+ rules={{
176
+ required: t('admin.product.vendorConfig.commissionRateRequired'),
177
+ min: { value: 0, message: t('admin.product.vendorConfig.commissionRateMin') },
178
+ max: { value: 100, message: t('admin.product.vendorConfig.commissionRateMax') },
179
+ }}
180
+ render={({ field }) => (
181
+ <FormInput
182
+ {...(field as any)}
183
+ size="small"
184
+ placeholder={t('admin.product.vendorConfig.commissionRate')}
185
+ label={t('admin.product.vendorConfig.commissionRate')}
186
+ type="number"
187
+ inputProps={{
188
+ min: 0,
189
+ max: 100,
190
+ step: 10,
191
+ }}
192
+ onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
193
+ field.onChange(e);
194
+ }}
195
+ />
196
+ )}
197
+ />
198
+ </Stack>
199
+ <IconButton
200
+ onClick={() => vendorConfigs.remove(index)}
201
+ sx={{
202
+ border: '1px solid',
203
+ borderColor: 'divider',
204
+ borderRadius: 1,
205
+ padding: '8px',
206
+ }}>
207
+ <DeleteOutlineOutlined color="error" sx={{ opacity: 0.75 }} />
208
+ </IconButton>
209
+ </Stack>
210
+ ))}
211
+ </Stack>
212
+ {(watchedVendorConfig || []).length > 0 && (
213
+ <Alert severity={validateTotalCommission() ? 'info' : 'error'} sx={{ mt: 1, mb: 2 }}>
214
+ {validateTotalCommission()
215
+ ? t('admin.product.vendorConfig.totalCommission', {
216
+ total: totalCommission.toFixed(0),
217
+ })
218
+ : t('admin.product.vendorConfig.totalCommissionExceeded', {
219
+ total: totalCommission.toFixed(0),
220
+ })}
221
+ </Alert>
222
+ )}
223
+
224
+ <Divider />
225
+ <Stack
226
+ direction="row"
227
+ sx={{
228
+ mt: vendorConfigs.fields.length ? 2 : 1,
229
+ justifyContent: 'space-between',
230
+ }}>
231
+ <Button
232
+ size="small"
233
+ variant="outlined"
234
+ color="primary"
235
+ sx={{ color: 'text.primary' }}
236
+ onClick={() =>
237
+ vendorConfigs.append({
238
+ vendor_id: '',
239
+ commission_rate: 0,
240
+ commission_type: 'percentage',
241
+ })
242
+ }
243
+ disabled={loading || selectedVendorIds.length === vendors.length || noneSelectedVendors.length === 0}>
244
+ <AddOutlined fontSize="small" /> {t('admin.product.vendorConfig.add')}
245
+ </Button>
246
+ </Stack>
247
+ </Box>
248
+ );
249
+ }
@@ -0,0 +1,145 @@
1
+ import { useLocaleContext } from '@arcblock/ux/lib/Locale/context';
2
+ import Toast from '@arcblock/ux/lib/Toast';
3
+ import { ConfirmDialog, api, formatError } from '@blocklet/payment-react';
4
+ import { useSetState } from 'ahooks';
5
+ import type { LiteralUnion } from 'type-fest';
6
+
7
+ import Actions from '../actions';
8
+ import ClickBoundary from '../click-boundary';
9
+
10
+ interface Vendor {
11
+ id: string;
12
+ vendor_key: string;
13
+ name: string;
14
+ description: string;
15
+ app_url: string;
16
+ webhook_path: string;
17
+ default_commission_rate: number;
18
+ default_commission_type: 'percentage' | 'fixed_amount';
19
+ status: 'active' | 'inactive';
20
+ order_create_params: Record<string, any>;
21
+ metadata: Record<string, any>;
22
+ created_at: string;
23
+ updated_at: string;
24
+ }
25
+
26
+ type VendorActionProps = {
27
+ data: Vendor;
28
+ onChange: (action: string) => void;
29
+ variant?: LiteralUnion<'compact' | 'normal', string>;
30
+ };
31
+
32
+ export default function VendorActions({ data, variant = 'compact', onChange }: VendorActionProps) {
33
+ const { t } = useLocaleContext();
34
+ const [state, setState] = useSetState({
35
+ action: '',
36
+ loading: false,
37
+ });
38
+
39
+ const onToggleStatus = async () => {
40
+ try {
41
+ setState({ loading: true });
42
+ const newStatus = data.status === 'active' ? 'inactive' : 'active';
43
+ await api.put(`/api/vendors/${data.id}`, { status: newStatus }).then((res: any) => res.data);
44
+ Toast.success(t('common.saved'));
45
+ onChange(state.action);
46
+ } catch (err) {
47
+ console.error(err);
48
+ Toast.error(formatError(err));
49
+ } finally {
50
+ setState({ loading: false, action: '' });
51
+ }
52
+ };
53
+
54
+ const onRemove = async () => {
55
+ try {
56
+ setState({ loading: true });
57
+ await api.delete(`/api/vendors/${data.id}`).then((res: any) => res.data);
58
+ Toast.success(t('common.removed'));
59
+ onChange(state.action);
60
+ } catch (err) {
61
+ console.error(err);
62
+ Toast.error(formatError(err));
63
+ } finally {
64
+ setState({ loading: false, action: '' });
65
+ }
66
+ };
67
+
68
+ // 测试发货功能
69
+ const onTestFulfillment = async () => {
70
+ try {
71
+ setState({ loading: true });
72
+ const result = await api
73
+ .post(`/api/vendors/${data.id}/test-fulfillment`, {
74
+ productCode: 'test_product',
75
+ })
76
+ .then((res: any) => res.data);
77
+
78
+ Toast.success(
79
+ '发货测试成功!\n' +
80
+ `订单号: ${result.fulfillmentResult?.orderId || 'N/A'}\n` +
81
+ `状态: ${result.fulfillmentResult?.status || 'N/A'}\n` +
82
+ `服务地址: ${result.fulfillmentResult?.serviceUrl || 'N/A'}`
83
+ );
84
+ onChange(state.action);
85
+ } catch (err) {
86
+ Toast.error(`发货测试失败: ${formatError(err)}`);
87
+ } finally {
88
+ setState({ loading: false, action: '' });
89
+ }
90
+ };
91
+
92
+ return (
93
+ <ClickBoundary>
94
+ <Actions
95
+ variant={variant}
96
+ actions={[
97
+ {
98
+ label: '测试发货',
99
+ handler: onTestFulfillment,
100
+ color: 'success',
101
+ },
102
+ data.status === 'active'
103
+ ? {
104
+ label: t('admin.vendor.deactivate'),
105
+ handler: () => setState({ action: 'toggle_status' }),
106
+ color: 'primary',
107
+ }
108
+ : {
109
+ label: t('admin.vendor.activate'),
110
+ handler: () => setState({ action: 'toggle_status' }),
111
+ color: 'primary',
112
+ },
113
+ {
114
+ label: t('admin.vendor.delete'),
115
+ handler: () => setState({ action: 'remove' }),
116
+ color: 'error',
117
+ divider: true,
118
+ },
119
+ ].filter(Boolean)}
120
+ />
121
+ {state.action === 'toggle_status' && (
122
+ <ConfirmDialog
123
+ onConfirm={onToggleStatus}
124
+ onCancel={() => setState({ action: '' })}
125
+ title={data.status === 'active' ? t('admin.vendor.deactivate') : t('admin.vendor.activate')}
126
+ message={
127
+ data.status === 'active'
128
+ ? t('admin.vendor.deactivateTip', { name: data.name })
129
+ : t('admin.vendor.activateTip', { name: data.name })
130
+ }
131
+ loading={state.loading}
132
+ />
133
+ )}
134
+ {state.action === 'remove' && (
135
+ <ConfirmDialog
136
+ onConfirm={onRemove}
137
+ onCancel={() => setState({ action: '' })}
138
+ title={t('admin.vendor.delete')}
139
+ message={t('admin.vendor.deleteTip', { name: data.name })}
140
+ loading={state.loading}
141
+ />
142
+ )}
143
+ </ClickBoundary>
144
+ );
145
+ }
@@ -40,6 +40,7 @@ export default flat({
40
40
  advancedFeatures: 'Advanced Features',
41
41
  quickStarts: 'Quick Starts',
42
42
  copy: 'Copy',
43
+ copied: 'Copied',
43
44
  copySuccess: 'Copy Success',
44
45
  copyFailed: 'Copy Failed',
45
46
  copyTip: 'Please copy manually',
@@ -98,6 +99,13 @@ export default flat({
98
99
  overview: 'Overview',
99
100
  payments: 'Payments',
100
101
  connections: 'Connections',
102
+ back: 'Back',
103
+ actions: 'Actions',
104
+ create: 'Create',
105
+ creating: 'Creating...',
106
+ save: 'Save',
107
+ saving: 'Saving...',
108
+ cancel: 'Cancel',
101
109
  paymentLinks: 'Payment links',
102
110
  paymentMethods: 'Payment methods',
103
111
  customers: 'Customers',
@@ -105,6 +113,7 @@ export default flat({
105
113
  pricing: 'Pricing',
106
114
  coupons: 'Coupons',
107
115
  pricingTables: 'Pricing tables',
116
+ vendors: 'Vendors',
108
117
  billing: 'Billing',
109
118
  invoices: 'Invoices',
110
119
  subscriptions: 'Subscriptions',
@@ -419,6 +428,25 @@ export default flat({
419
428
  credit: 'Credit',
420
429
  good: 'Good',
421
430
  },
431
+ vendorConfig: {
432
+ title: 'Vendor Configuration',
433
+ add: 'Add Vendor',
434
+ empty: 'No vendors configured. Click "Add Vendor" to configure vendor services.',
435
+ vendor: 'Vendor',
436
+ vendorRequired: 'Vendor is required',
437
+ productCode: 'Product Code',
438
+ productCodeRequired: 'Product code is required',
439
+ commissionType: 'Commission Type',
440
+ commissionRate: 'Commission Rate',
441
+ commissionRateRequired: 'Commission rate is required',
442
+ commissionRateMin: 'Commission rate must be greater than or equal to 0',
443
+ commissionRateMax: 'Commission rate cannot exceed 100%',
444
+ amount: 'Fixed Amount',
445
+ noVendor: 'No vendor available',
446
+ amountRequired: 'Fixed amount is required',
447
+ totalCommission: 'Total commission rate: {total}%',
448
+ totalCommissionExceeded: 'Total commission rate exceeds 100%: {total}%',
449
+ },
422
450
  },
423
451
  price: {
424
452
  name: 'Price',
@@ -835,6 +863,67 @@ export default flat({
835
863
  success: 'Invoice voided',
836
864
  },
837
865
  },
866
+ vendor: {
867
+ create: 'Create Vendor',
868
+ edit: 'Edit Vendor',
869
+ delete: 'Delete Vendor',
870
+ save: 'Save Vendor',
871
+ saved: 'Vendor saved successfully',
872
+ test: 'Test',
873
+ testConnection: 'Test Connection',
874
+ testSuccess: 'Connection test successful',
875
+ testAfterSave: 'Vendor saved successfully. You can test the connection to verify the configuration.',
876
+ testToEnable: 'Vendor saved successfully. Test the connection to enable the vendor.',
877
+ enabled: 'Enabled',
878
+ disabled: 'Disabled',
879
+ addressCheckFailed: 'Address check failed, cannot save when enabled',
880
+ updateSuccess: 'Vendor updated successfully',
881
+ testFailed: 'Connection test failed (Status: {status})',
882
+ testError: 'Connection test failed - network error',
883
+ vendorKeyInvalid: 'Vendor type must contain only lowercase letters, numbers, and underscores',
884
+ deleteTitle: 'Delete Vendor',
885
+ deleteContent: 'Are you sure you want to delete vendor "{name}"? This action cannot be undone.',
886
+ deleteTip: 'Are you sure you want to delete vendor "{name}"? This action cannot be undone.',
887
+ activate: 'Activate Vendor',
888
+ activateTip: 'Are you sure you want to activate vendor "{name}"?',
889
+ deactivate: 'Deactivate Vendor',
890
+ deactivateTip: 'Are you sure you want to deactivate vendor "{name}"?',
891
+ name: 'Vendor Name',
892
+ nameRequired: 'Vendor name is required',
893
+ vendorKey: 'Vendor Type',
894
+ vendorKeyRequired: 'Vendor type is required',
895
+ vendorKeyHelp: 'Unique identifier for the vendor (e.g., launcher, did_names)',
896
+ description: 'Description',
897
+ appUrl: 'App URL',
898
+ appUrlRequired: 'App URL is required',
899
+ appUrlInvalid: 'Please enter a valid URL starting with http:// or https://',
900
+ appUrlHelp: 'The base URL of the vendor application',
901
+ webhookPath: 'Webhook Path',
902
+ webhookPathInvalid: 'Please enter a valid path starting with /',
903
+ webhookPathHelp: 'Optional webhook callback path (e.g., /webhooks/status)',
904
+ blockletMetaUrl: 'Blocklet Meta URL',
905
+ blockletMetaUrlRequired: 'Blocklet Meta URL is required',
906
+ blockletMetaUrlInvalid: 'Please enter a valid URL starting with http:// or https://',
907
+ blockletMetaUrlHelp: 'Required blocklet metadata URL for application information',
908
+ commission: 'Commission',
909
+ commissionRate: 'Commission Rate',
910
+ commissionRateRequired: 'Commission rate is required',
911
+ commissionRateMin: 'Commission rate must be greater than or equal to 0',
912
+ commissionRateMax: 'Commission rate is too high',
913
+ commissionType: 'Commission Type',
914
+ commissionRateHelp: 'Commission rate as percentage',
915
+ commissionAmountHelp: 'Fixed commission amount',
916
+ percentage: 'Percentage',
917
+ fixedAmount: 'Fixed Amount',
918
+ active: 'Active',
919
+ inactive: 'Inactive',
920
+ basicInfo: 'Basic Information',
921
+ apiConfig: 'API Configuration',
922
+ commissionConfig: 'Commission Configuration',
923
+ status: 'Status',
924
+ servicePublicKey: 'Service Public Key',
925
+ servicePublicKeyDescription: 'Used for vendor communication authentication',
926
+ },
838
927
  subscription: {
839
928
  view: 'View subscription',
840
929
  name: 'Subscription',