payment-kit 1.20.7 → 1.20.9
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/crons/index.ts +1 -1
- package/api/src/index.ts +2 -2
- package/api/src/libs/{vendor → vendor-util}/adapters/factory.ts +9 -5
- package/api/src/libs/{vendor → vendor-util}/adapters/launcher-adapter.ts +3 -8
- package/api/src/libs/{vendor → vendor-util}/adapters/types.ts +1 -4
- package/api/src/libs/{vendor → vendor-util}/fulfillment.ts +11 -8
- package/api/src/queues/{vendor → vendors}/commission.ts +4 -5
- package/api/src/queues/{vendor → vendors}/fulfillment-coordinator.ts +33 -4
- package/api/src/queues/{vendor → vendors}/fulfillment.ts +2 -2
- package/api/src/queues/{vendor → vendors}/status-check.ts +2 -2
- package/api/src/routes/payment-links.ts +2 -1
- package/api/src/routes/products.ts +1 -0
- package/api/src/routes/vendor.ts +135 -213
- package/api/src/store/migrations/20250911-add-vendor-type.ts +26 -0
- package/api/src/store/models/product-vendor.ts +6 -24
- package/api/src/store/models/product.ts +1 -0
- package/blocklet.yml +1 -1
- package/doc/vendor_fulfillment_system.md +1 -1
- package/package.json +23 -22
- package/src/components/metadata/form.tsx +12 -19
- package/src/components/payment-link/before-pay.tsx +40 -0
- package/src/components/product/vendor-config.tsx +4 -11
- package/src/components/subscription/description.tsx +1 -6
- package/src/components/subscription/portal/list.tsx +82 -6
- package/src/components/subscription/vendor-service-list.tsx +128 -0
- package/src/components/vendor/actions.tsx +1 -33
- package/src/locales/en.tsx +13 -3
- package/src/locales/zh.tsx +13 -3
- package/src/pages/admin/products/links/create.tsx +2 -0
- package/src/pages/admin/products/vendors/create.tsx +108 -194
- package/src/pages/admin/products/vendors/index.tsx +14 -22
- package/src/pages/customer/subscription/detail.tsx +26 -11
package/api/src/routes/vendor.ts
CHANGED
|
@@ -1,30 +1,25 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
2
|
import Joi from 'joi';
|
|
3
3
|
|
|
4
|
-
import { joinURL } from 'ufo';
|
|
5
4
|
import { VendorAuth } from '@blocklet/payment-vendor';
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
8
|
-
import { ProductVendor } from '../store/models/product-vendor';
|
|
5
|
+
import { joinURL } from 'ufo';
|
|
6
|
+
import { MetadataSchema } from '../libs/api';
|
|
9
7
|
import { wallet } from '../libs/auth';
|
|
10
|
-
import
|
|
8
|
+
import dayjs from '../libs/dayjs';
|
|
9
|
+
import logger from '../libs/logger';
|
|
11
10
|
import { authenticate } from '../libs/security';
|
|
12
11
|
import { formatToShortUrl } from '../libs/url';
|
|
13
|
-
import
|
|
14
|
-
import {
|
|
12
|
+
import { CheckoutSession } from '../store/models';
|
|
13
|
+
import { ProductVendor } from '../store/models/product-vendor';
|
|
15
14
|
|
|
16
15
|
const authAdmin = authenticate<CheckoutSession>({ component: true, roles: ['owner', 'admin'] });
|
|
17
16
|
|
|
18
17
|
const createVendorSchema = Joi.object({
|
|
19
18
|
vendor_key: Joi.string().max(50).required(),
|
|
19
|
+
vendor_type: Joi.string().valid('launcher').default('launcher'),
|
|
20
20
|
name: Joi.string().max(255).required(),
|
|
21
21
|
description: Joi.string().max(1000).allow('').optional(),
|
|
22
22
|
app_url: Joi.string().uri().max(512).required(),
|
|
23
|
-
webhook_path: Joi.string().max(255).allow('').optional(),
|
|
24
|
-
blocklet_meta_url: Joi.string().uri().max(512).allow('').optional(),
|
|
25
|
-
default_commission_rate: Joi.number().min(0).max(100).required(),
|
|
26
|
-
default_commission_type: Joi.string().valid('percentage', 'fixed_amount').required(),
|
|
27
|
-
order_create_params: Joi.object().default({}),
|
|
28
23
|
app_pid: Joi.string().max(255).allow('').optional(),
|
|
29
24
|
app_logo: Joi.string().max(512).allow('').optional(),
|
|
30
25
|
status: Joi.string().valid('active', 'inactive').default('active'),
|
|
@@ -32,26 +27,16 @@ const createVendorSchema = Joi.object({
|
|
|
32
27
|
}).unknown(false);
|
|
33
28
|
|
|
34
29
|
const updateVendorSchema = Joi.object({
|
|
30
|
+
vendor_type: Joi.string().valid('launcher').optional(),
|
|
35
31
|
name: Joi.string().max(255).optional(),
|
|
36
32
|
description: Joi.string().max(1000).allow('').optional(),
|
|
37
33
|
app_url: Joi.string().uri().max(512).optional(),
|
|
38
|
-
webhook_path: Joi.string().max(255).allow('').optional(),
|
|
39
|
-
blocklet_meta_url: Joi.string().uri().max(512).allow('').optional(),
|
|
40
|
-
default_commission_rate: Joi.number().min(0).max(100).optional(),
|
|
41
|
-
default_commission_type: Joi.string().valid('percentage', 'fixed_amount').optional(),
|
|
42
|
-
order_create_params: Joi.object().optional(),
|
|
43
34
|
app_pid: Joi.string().max(255).allow('').optional(),
|
|
44
35
|
app_logo: Joi.string().max(512).allow('').optional(),
|
|
45
36
|
status: Joi.string().valid('active', 'inactive').optional(),
|
|
46
37
|
metadata: MetadataSchema,
|
|
47
38
|
}).unknown(true);
|
|
48
39
|
|
|
49
|
-
const testFulfillmentSchema = Joi.object({
|
|
50
|
-
productCode: Joi.string().max(100).required(),
|
|
51
|
-
quantity: Joi.number().integer().min(1).default(1),
|
|
52
|
-
customParams: Joi.object().optional(),
|
|
53
|
-
}).unknown(true);
|
|
54
|
-
|
|
55
40
|
const vendorIdParamSchema = Joi.object({
|
|
56
41
|
id: Joi.string().max(100).required(),
|
|
57
42
|
});
|
|
@@ -60,12 +45,21 @@ const sessionIdParamSchema = Joi.object({
|
|
|
60
45
|
sessionId: Joi.string().max(100).required(),
|
|
61
46
|
});
|
|
62
47
|
|
|
48
|
+
const subscriptionIdParamSchema = Joi.object({
|
|
49
|
+
subscriptionId: Joi.string().max(100).required(),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const vendorRedirectQuerySchema = Joi.object({
|
|
53
|
+
vendorId: Joi.string().max(100).required(),
|
|
54
|
+
target: Joi.string().valid('home', 'dashboard').default('home').allow(''),
|
|
55
|
+
});
|
|
56
|
+
|
|
63
57
|
function validateParams(schema: Joi.ObjectSchema) {
|
|
64
58
|
return (req: any, res: any, next: any) => {
|
|
65
59
|
const { error } = schema.validate(req.params);
|
|
66
60
|
if (error) {
|
|
67
61
|
logger.error('Invalid parameters', {
|
|
68
|
-
error
|
|
62
|
+
error,
|
|
69
63
|
params: req.params,
|
|
70
64
|
});
|
|
71
65
|
return res.status(400).json({
|
|
@@ -77,6 +71,21 @@ function validateParams(schema: Joi.ObjectSchema) {
|
|
|
77
71
|
};
|
|
78
72
|
}
|
|
79
73
|
|
|
74
|
+
function validateQuery(schema: Joi.ObjectSchema) {
|
|
75
|
+
return (req: any, res: any, next: any) => {
|
|
76
|
+
const { error, value } = schema.validate(req.query);
|
|
77
|
+
if (error) {
|
|
78
|
+
logger.error('Invalid query parameters', {
|
|
79
|
+
error,
|
|
80
|
+
query: req.query,
|
|
81
|
+
});
|
|
82
|
+
return res.redirect('/404');
|
|
83
|
+
}
|
|
84
|
+
req.query = value;
|
|
85
|
+
return next();
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
80
89
|
async function getAllVendors(_req: any, res: any) {
|
|
81
90
|
try {
|
|
82
91
|
const vendors = await ProductVendor.findAll({
|
|
@@ -90,7 +99,7 @@ async function getAllVendors(_req: any, res: any) {
|
|
|
90
99
|
});
|
|
91
100
|
} catch (error: any) {
|
|
92
101
|
logger.error('Failed to get vendors', {
|
|
93
|
-
error
|
|
102
|
+
error,
|
|
94
103
|
stack: error.stack,
|
|
95
104
|
});
|
|
96
105
|
return res.status(500).json({ error: 'Internal server error. Failed to get vendors' });
|
|
@@ -107,7 +116,7 @@ async function getVendorById(req: any, res: any) {
|
|
|
107
116
|
return res.json({ data: vendor });
|
|
108
117
|
} catch (error: any) {
|
|
109
118
|
logger.error('Failed to get vendor', {
|
|
110
|
-
error
|
|
119
|
+
error,
|
|
111
120
|
id: req.params.id,
|
|
112
121
|
});
|
|
113
122
|
return res.status(500).json({ error: 'Internal server error. Failed to get vendor' });
|
|
@@ -126,27 +135,16 @@ async function createVendor(req: any, res: any) {
|
|
|
126
135
|
|
|
127
136
|
const {
|
|
128
137
|
vendor_key: vendorKey,
|
|
138
|
+
vendor_type: vendorType,
|
|
129
139
|
name,
|
|
130
140
|
description,
|
|
131
141
|
app_url: appUrl,
|
|
132
|
-
webhook_path: webhookPath,
|
|
133
|
-
blocklet_meta_url: blockletMetaUrl,
|
|
134
|
-
default_commission_rate: defaultCommissionRate,
|
|
135
|
-
default_commission_type: defaultCommissionType,
|
|
136
|
-
order_create_params: orderCreateParams,
|
|
137
142
|
metadata,
|
|
138
143
|
app_pid: appPid,
|
|
139
144
|
app_logo: appLogo,
|
|
140
145
|
status,
|
|
141
146
|
} = value;
|
|
142
147
|
|
|
143
|
-
const blockletMetaUrlFromMetadata = metadata?.blockletMetaUrl || blockletMetaUrl;
|
|
144
|
-
if (!blockletMetaUrlFromMetadata) {
|
|
145
|
-
return res.status(400).json({
|
|
146
|
-
error: 'blockletMetaUrl is required in metadata',
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
|
|
150
148
|
const existingVendor = await ProductVendor.findOne({
|
|
151
149
|
where: { vendor_key: vendorKey },
|
|
152
150
|
});
|
|
@@ -156,20 +154,14 @@ async function createVendor(req: any, res: any) {
|
|
|
156
154
|
|
|
157
155
|
const vendor = await ProductVendor.create({
|
|
158
156
|
vendor_key: vendorKey,
|
|
157
|
+
vendor_type: vendorType || 'launcher',
|
|
159
158
|
name,
|
|
160
159
|
description,
|
|
161
160
|
app_url: appUrl,
|
|
162
|
-
webhook_path: webhookPath,
|
|
163
|
-
default_commission_rate: defaultCommissionRate,
|
|
164
|
-
default_commission_type: defaultCommissionType,
|
|
165
|
-
order_create_params: orderCreateParams || {},
|
|
166
161
|
status: status || 'active',
|
|
167
162
|
app_pid: appPid,
|
|
168
163
|
app_logo: appLogo,
|
|
169
|
-
metadata: {
|
|
170
|
-
...(metadata || {}),
|
|
171
|
-
...(blockletMetaUrl && { blockletMetaUrl }),
|
|
172
|
-
},
|
|
164
|
+
metadata: metadata || {},
|
|
173
165
|
created_by: req.user?.did || 'admin',
|
|
174
166
|
});
|
|
175
167
|
|
|
@@ -177,7 +169,7 @@ async function createVendor(req: any, res: any) {
|
|
|
177
169
|
return res.status(201).json({ data: vendor });
|
|
178
170
|
} catch (error: any) {
|
|
179
171
|
logger.error('Failed to create vendor', {
|
|
180
|
-
error
|
|
172
|
+
error,
|
|
181
173
|
});
|
|
182
174
|
return res.status(500).json({ error: 'Internal server error' });
|
|
183
175
|
}
|
|
@@ -199,14 +191,10 @@ async function updateVendor(req: any, res: any) {
|
|
|
199
191
|
}
|
|
200
192
|
|
|
201
193
|
const {
|
|
194
|
+
vendor_type: vendorType,
|
|
202
195
|
name,
|
|
203
196
|
description,
|
|
204
197
|
app_url: appUrl,
|
|
205
|
-
webhook_path: webhookPath,
|
|
206
|
-
blocklet_meta_url: blockletMetaUrl,
|
|
207
|
-
default_commission_rate: defaultCommissionRate,
|
|
208
|
-
default_commission_type: defaultCommissionType,
|
|
209
|
-
order_create_params: orderCreateParams,
|
|
210
198
|
status,
|
|
211
199
|
metadata,
|
|
212
200
|
app_pid: appPid,
|
|
@@ -222,21 +210,12 @@ async function updateVendor(req: any, res: any) {
|
|
|
222
210
|
}
|
|
223
211
|
}
|
|
224
212
|
const updates = {
|
|
213
|
+
vendor_type: vendorType,
|
|
225
214
|
name,
|
|
226
215
|
description,
|
|
227
216
|
app_url: appUrl,
|
|
228
|
-
webhook_path: webhookPath,
|
|
229
|
-
default_commission_rate: defaultCommissionRate,
|
|
230
|
-
default_commission_type: defaultCommissionType,
|
|
231
|
-
order_create_params: orderCreateParams,
|
|
232
217
|
status,
|
|
233
|
-
|
|
234
|
-
metadata: {
|
|
235
|
-
...(vendor.metadata || {}),
|
|
236
|
-
...(metadata || {}),
|
|
237
|
-
...(blockletMetaUrl !== undefined && { blockletMetaUrl }),
|
|
238
|
-
},
|
|
239
|
-
}),
|
|
218
|
+
metadata,
|
|
240
219
|
app_pid: appPid,
|
|
241
220
|
app_logo: appLogo,
|
|
242
221
|
vendor_key: req.body.vendor_key,
|
|
@@ -247,7 +226,7 @@ async function updateVendor(req: any, res: any) {
|
|
|
247
226
|
logger.info('Vendor updated', { vendorId: vendor.id });
|
|
248
227
|
return res.json({ data: vendor });
|
|
249
228
|
} catch (error: any) {
|
|
250
|
-
logger.error('Failed to update vendor', { error
|
|
229
|
+
logger.error('Failed to update vendor', { error, id: req.params.id });
|
|
251
230
|
return res.status(500).json({ error: 'Internal server error' });
|
|
252
231
|
}
|
|
253
232
|
}
|
|
@@ -264,7 +243,7 @@ async function deleteVendor(req: any, res: any) {
|
|
|
264
243
|
logger.info('Vendor deleted', { vendorId: vendor.id });
|
|
265
244
|
return res.json({ success: true });
|
|
266
245
|
} catch (error: any) {
|
|
267
|
-
logger.error('Failed to delete vendor', { error
|
|
246
|
+
logger.error('Failed to delete vendor', { error, id: req.params.id });
|
|
268
247
|
return res.status(500).json({ error: 'Internal server error' });
|
|
269
248
|
}
|
|
270
249
|
}
|
|
@@ -286,131 +265,51 @@ async function testVendorConnection(req: any, res: any) {
|
|
|
286
265
|
},
|
|
287
266
|
});
|
|
288
267
|
} catch (error: any) {
|
|
289
|
-
logger.error('Failed to test vendor connection', { error
|
|
268
|
+
logger.error('Failed to test vendor connection', { error, id: req.params.id });
|
|
290
269
|
return res.status(500).json({ error: 'Internal server error' });
|
|
291
270
|
}
|
|
292
271
|
}
|
|
293
272
|
|
|
294
|
-
async function
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
return res.status(404).json({ error: 'Vendor not found' });
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
const { error, value } = testFulfillmentSchema.validate(req.body);
|
|
302
|
-
if (error) {
|
|
303
|
-
return res.status(400).json({
|
|
304
|
-
error: 'Validation failed',
|
|
305
|
-
message: error.message,
|
|
306
|
-
});
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
const { productCode, quantity, customParams } = value;
|
|
310
|
-
|
|
311
|
-
const testCheckoutSession = {
|
|
312
|
-
id: `test_checkout_${Date.now()}`,
|
|
313
|
-
payment_intent_id: `test_payment_intent_${Date.now()}`,
|
|
314
|
-
customer_id: 'test_customer',
|
|
315
|
-
customer_did: 'z1XGnwJiV3Hnz36kCTJaDe6iiv2kF6DJokt',
|
|
316
|
-
amount: 1000,
|
|
317
|
-
currency: 'USD',
|
|
318
|
-
amount_total: 1000,
|
|
319
|
-
currency_id: 'USD',
|
|
320
|
-
status: 'paid',
|
|
321
|
-
line_items: [
|
|
322
|
-
{
|
|
323
|
-
vendor_id: productCode,
|
|
324
|
-
quantity,
|
|
325
|
-
amount: 1000,
|
|
326
|
-
custom_params: {
|
|
327
|
-
email: customParams?.email || 'test@example.com',
|
|
328
|
-
testMode: true,
|
|
329
|
-
...customParams,
|
|
330
|
-
},
|
|
331
|
-
},
|
|
332
|
-
],
|
|
333
|
-
vendor_info: [
|
|
334
|
-
{
|
|
335
|
-
vendor_key: vendor.vendor_key,
|
|
336
|
-
status: 'pending',
|
|
337
|
-
attempts: 0,
|
|
338
|
-
},
|
|
339
|
-
],
|
|
340
|
-
};
|
|
341
|
-
|
|
342
|
-
const testVendorConfig = {
|
|
343
|
-
vendor_id: vendor.id,
|
|
344
|
-
vendor_key: vendor.vendor_key,
|
|
345
|
-
app_url: vendor.app_url,
|
|
346
|
-
commission_rate: vendor.default_commission_rate,
|
|
347
|
-
commission_type: vendor.default_commission_type,
|
|
348
|
-
order_create_params: vendor.order_create_params || {},
|
|
349
|
-
custom_params: {
|
|
350
|
-
email: customParams?.email || 'test@example.com',
|
|
351
|
-
userDid: 'z1XGnwJiV3Hnz36kCTJaDe6iiv2kF6DJokt',
|
|
352
|
-
},
|
|
353
|
-
};
|
|
273
|
+
async function getVendorStatusByVendorId(vendorId: string, orderId: string, isDetail = false) {
|
|
274
|
+
if (!vendorId || !orderId) {
|
|
275
|
+
return {};
|
|
276
|
+
}
|
|
354
277
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
vendorKey: vendor.vendor_key,
|
|
358
|
-
testData: {
|
|
359
|
-
checkoutSessionId: testCheckoutSession.id,
|
|
360
|
-
productCode: req.body.productCode || 'test_product',
|
|
361
|
-
amount: testCheckoutSession.amount,
|
|
362
|
-
currency: testCheckoutSession.currency,
|
|
363
|
-
},
|
|
364
|
-
});
|
|
278
|
+
const vendor = await ProductVendor.findByPk(vendorId);
|
|
279
|
+
const url = vendor?.app_url ? joinURL(vendor.app_url, '/api/vendor/', isDetail ? 'orders' : 'status', orderId) : null;
|
|
365
280
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
customer_id: testCheckoutSession.customer_id,
|
|
370
|
-
payment_intent_id: testCheckoutSession.payment_intent_id,
|
|
371
|
-
currency_id: testCheckoutSession.currency_id,
|
|
372
|
-
customer_did: testCheckoutSession.customer_did,
|
|
373
|
-
};
|
|
374
|
-
const fulfillmentResult = await VendorFulfillmentService.fulfillSingleVendorOrder(orderInfo, testVendorConfig);
|
|
281
|
+
const { headers } = VendorAuth.signRequestWithHeaders({});
|
|
282
|
+
const name = vendor?.name;
|
|
283
|
+
const key = vendor?.vendor_key;
|
|
375
284
|
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
serviceUrl: fulfillmentResult.serviceUrl,
|
|
381
|
-
});
|
|
285
|
+
return url
|
|
286
|
+
? fetch(url, { headers })
|
|
287
|
+
.then(async (r) => {
|
|
288
|
+
const data = await r.json();
|
|
382
289
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
requestBody: req.body,
|
|
407
|
-
});
|
|
408
|
-
return res.status(500).json({
|
|
409
|
-
error: 'Test fulfillment failed',
|
|
410
|
-
details: error.message,
|
|
411
|
-
vendor: req.params.id,
|
|
412
|
-
});
|
|
413
|
-
}
|
|
290
|
+
if (!data.dashboardUrl) {
|
|
291
|
+
return {
|
|
292
|
+
...data,
|
|
293
|
+
name,
|
|
294
|
+
key,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
const validUntil = dayjs().add(20, 'minutes').format('YYYY-MM-DDTHH:mm:ss+00:00');
|
|
298
|
+
const maxVisits = 5;
|
|
299
|
+
|
|
300
|
+
const homeUrl = await formatToShortUrl({ url: data.homeUrl, maxVisits, validUntil });
|
|
301
|
+
const dashboardUrl = await formatToShortUrl({ url: data.dashboardUrl, maxVisits, validUntil });
|
|
302
|
+
|
|
303
|
+
return {
|
|
304
|
+
...data,
|
|
305
|
+
name,
|
|
306
|
+
key,
|
|
307
|
+
homeUrl,
|
|
308
|
+
dashboardUrl,
|
|
309
|
+
};
|
|
310
|
+
})
|
|
311
|
+
.catch((e) => ({ error: e.message }))
|
|
312
|
+
: null;
|
|
414
313
|
}
|
|
415
314
|
|
|
416
315
|
async function getVendorStatus(sessionId: string, isDetail = false) {
|
|
@@ -441,36 +340,8 @@ async function getVendorStatus(sessionId: string, isDetail = false) {
|
|
|
441
340
|
};
|
|
442
341
|
}
|
|
443
342
|
|
|
444
|
-
const vendors = doc.vendor_info.map(
|
|
445
|
-
|
|
446
|
-
const url = vendor?.app_url
|
|
447
|
-
? joinURL(vendor.app_url, '/api/vendor/', isDetail ? 'orders' : 'status', item.order_id)
|
|
448
|
-
: null;
|
|
449
|
-
|
|
450
|
-
const { headers } = VendorAuth.signRequestWithHeaders({});
|
|
451
|
-
|
|
452
|
-
return url
|
|
453
|
-
? fetch(url, { headers })
|
|
454
|
-
.then(async (r) => {
|
|
455
|
-
const data = await r.json();
|
|
456
|
-
|
|
457
|
-
if (!data.dashboardUrl) {
|
|
458
|
-
return data;
|
|
459
|
-
}
|
|
460
|
-
const validUntil = dayjs().add(20, 'minutes').format('YYYY-MM-DDTHH:mm:ss+00:00');
|
|
461
|
-
const maxVisits = 5;
|
|
462
|
-
|
|
463
|
-
const homeUrl = await formatToShortUrl({ url: data.homeUrl, maxVisits, validUntil });
|
|
464
|
-
const dashboardUrl = await formatToShortUrl({ url: data.dashboardUrl, maxVisits, validUntil });
|
|
465
|
-
|
|
466
|
-
return {
|
|
467
|
-
...data,
|
|
468
|
-
homeUrl,
|
|
469
|
-
dashboardUrl,
|
|
470
|
-
};
|
|
471
|
-
})
|
|
472
|
-
.catch((e) => ({ error: e.message }))
|
|
473
|
-
: null;
|
|
343
|
+
const vendors = doc.vendor_info.map((item) => {
|
|
344
|
+
return getVendorStatusByVendorId(item.vendor_id, item.order_id, isDetail);
|
|
474
345
|
});
|
|
475
346
|
|
|
476
347
|
return {
|
|
@@ -510,11 +381,63 @@ async function getVendorFulfillmentDetail(req: any, res: any) {
|
|
|
510
381
|
}
|
|
511
382
|
}
|
|
512
383
|
|
|
384
|
+
async function redirectToVendor(req: any, res: any) {
|
|
385
|
+
const { subscriptionId } = req.params;
|
|
386
|
+
const { vendorId, target } = req.query;
|
|
387
|
+
|
|
388
|
+
try {
|
|
389
|
+
const checkoutSession = await CheckoutSession.findBySubscriptionId(subscriptionId);
|
|
390
|
+
if (!checkoutSession) {
|
|
391
|
+
logger.warn('CheckoutSession not found for subscription[redirect to vendor]', { subscriptionId });
|
|
392
|
+
return res.redirect('/404');
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const order = checkoutSession?.vendor_info?.find((item) => item.vendor_id === vendorId);
|
|
396
|
+
if (!order) {
|
|
397
|
+
logger.warn('Vendor order not found in checkout session[redirect to vendor]', {
|
|
398
|
+
subscriptionId,
|
|
399
|
+
vendorId,
|
|
400
|
+
availableVendors: checkoutSession.vendor_info?.map((v) => v.vendor_key) || [],
|
|
401
|
+
});
|
|
402
|
+
return res.redirect('/404');
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const detail = await getVendorStatusByVendorId(vendorId, order.order_id || '', true);
|
|
406
|
+
if (!detail) {
|
|
407
|
+
logger.warn('Vendor status detail not found', {
|
|
408
|
+
subscriptionId,
|
|
409
|
+
vendorId,
|
|
410
|
+
orderId: order.order_id,
|
|
411
|
+
});
|
|
412
|
+
return res.redirect('/404');
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
const redirectUrl = target === 'dashboard' ? detail.dashboardUrl : detail.homeUrl;
|
|
416
|
+
return res.redirect(redirectUrl);
|
|
417
|
+
} catch (error: any) {
|
|
418
|
+
logger.error('Failed to redirect to vendor service', {
|
|
419
|
+
error,
|
|
420
|
+
subscriptionId,
|
|
421
|
+
vendorId,
|
|
422
|
+
target,
|
|
423
|
+
});
|
|
424
|
+
return res.redirect('/404');
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
513
428
|
const router = Router();
|
|
514
429
|
|
|
515
430
|
router.get('/order/:sessionId/status', validateParams(sessionIdParamSchema), getVendorFulfillmentStatus);
|
|
516
431
|
router.get('/order/:sessionId/detail', validateParams(sessionIdParamSchema), getVendorFulfillmentDetail);
|
|
517
432
|
|
|
433
|
+
router.get(
|
|
434
|
+
'/open/:subscriptionId',
|
|
435
|
+
authAdmin,
|
|
436
|
+
validateParams(subscriptionIdParamSchema),
|
|
437
|
+
validateQuery(vendorRedirectQuerySchema),
|
|
438
|
+
redirectToVendor
|
|
439
|
+
);
|
|
440
|
+
|
|
518
441
|
router.get('/', getAllVendors);
|
|
519
442
|
router.get('/:id', authAdmin, validateParams(vendorIdParamSchema), getVendorById);
|
|
520
443
|
router.post('/', authAdmin, createVendor);
|
|
@@ -522,6 +445,5 @@ router.put('/:id', authAdmin, validateParams(vendorIdParamSchema), updateVendor)
|
|
|
522
445
|
router.delete('/:id', authAdmin, validateParams(vendorIdParamSchema), deleteVendor);
|
|
523
446
|
|
|
524
447
|
router.post('/:id/test-connection', authAdmin, validateParams(vendorIdParamSchema), testVendorConnection);
|
|
525
|
-
router.post('/:id/test-fulfillment', authAdmin, validateParams(vendorIdParamSchema), testVendorFulfillment);
|
|
526
448
|
|
|
527
449
|
export default router;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { safeApplyColumnChanges, type Migration } from '../migrate';
|
|
2
|
+
|
|
3
|
+
export const up: Migration = async ({ context }) => {
|
|
4
|
+
// Add vendor_type column to product_vendors table
|
|
5
|
+
await safeApplyColumnChanges(context, {
|
|
6
|
+
product_vendors: [
|
|
7
|
+
{
|
|
8
|
+
name: 'vendor_type',
|
|
9
|
+
field: {
|
|
10
|
+
type: 'STRING',
|
|
11
|
+
allowNull: false,
|
|
12
|
+
defaultValue: 'launcher',
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
],
|
|
16
|
+
});
|
|
17
|
+
await context.removeColumn('product_vendors', 'default_commission_rate');
|
|
18
|
+
await context.removeColumn('product_vendors', 'default_commission_type');
|
|
19
|
+
await context.removeColumn('product_vendors', 'order_create_params');
|
|
20
|
+
await context.removeColumn('product_vendors', 'webhook_path');
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const down: Migration = async ({ context }) => {
|
|
24
|
+
// Remove vendor_type column if we need to rollback
|
|
25
|
+
await context.removeColumn('product_vendors', 'vendor_type');
|
|
26
|
+
};
|
|
@@ -8,17 +8,11 @@ export const nextProductVendorId = createIdGenerator('pv', 24);
|
|
|
8
8
|
export class ProductVendor extends Model<InferAttributes<ProductVendor>, InferCreationAttributes<ProductVendor>> {
|
|
9
9
|
declare id: CreationOptional<string>;
|
|
10
10
|
declare vendor_key: string;
|
|
11
|
+
declare vendor_type: string;
|
|
11
12
|
declare name: string;
|
|
12
13
|
declare description: string;
|
|
13
14
|
|
|
14
15
|
declare app_url: string;
|
|
15
|
-
declare webhook_path?: string;
|
|
16
|
-
|
|
17
|
-
declare default_commission_rate: number;
|
|
18
|
-
declare default_commission_type: 'percentage' | 'fixed_amount';
|
|
19
|
-
|
|
20
|
-
declare order_create_params: Record<string, any>;
|
|
21
|
-
|
|
22
16
|
declare app_pid?: string;
|
|
23
17
|
declare app_logo?: string;
|
|
24
18
|
|
|
@@ -41,6 +35,11 @@ export class ProductVendor extends Model<InferAttributes<ProductVendor>, InferCr
|
|
|
41
35
|
allowNull: false,
|
|
42
36
|
unique: true,
|
|
43
37
|
},
|
|
38
|
+
vendor_type: {
|
|
39
|
+
type: DataTypes.STRING(50),
|
|
40
|
+
allowNull: false,
|
|
41
|
+
defaultValue: 'launcher',
|
|
42
|
+
},
|
|
44
43
|
name: {
|
|
45
44
|
type: DataTypes.STRING(255),
|
|
46
45
|
allowNull: false,
|
|
@@ -53,23 +52,6 @@ export class ProductVendor extends Model<InferAttributes<ProductVendor>, InferCr
|
|
|
53
52
|
type: DataTypes.STRING(512),
|
|
54
53
|
allowNull: false,
|
|
55
54
|
},
|
|
56
|
-
webhook_path: {
|
|
57
|
-
type: DataTypes.STRING(255),
|
|
58
|
-
allowNull: true,
|
|
59
|
-
},
|
|
60
|
-
default_commission_rate: {
|
|
61
|
-
type: DataTypes.DECIMAL(7, 4),
|
|
62
|
-
allowNull: false,
|
|
63
|
-
},
|
|
64
|
-
default_commission_type: {
|
|
65
|
-
type: DataTypes.ENUM('percentage', 'fixed_amount'),
|
|
66
|
-
allowNull: false,
|
|
67
|
-
},
|
|
68
|
-
order_create_params: {
|
|
69
|
-
type: DataTypes.JSON,
|
|
70
|
-
allowNull: true,
|
|
71
|
-
defaultValue: {},
|
|
72
|
-
},
|
|
73
55
|
app_pid: {
|
|
74
56
|
type: DataTypes.STRING(255),
|
|
75
57
|
allowNull: true,
|
package/blocklet.yml
CHANGED
|
@@ -790,7 +790,7 @@ async function requestReturnFromSingleVendor(
|
|
|
790
790
|
): Promise<void> {
|
|
791
791
|
try {
|
|
792
792
|
// 1. 获取供应商适配器
|
|
793
|
-
const vendorAdapter = VendorFulfillmentService.getVendorAdapter(vendor.vendor_key);
|
|
793
|
+
const vendorAdapter = await VendorFulfillmentService.getVendorAdapter(vendor.vendor_key);
|
|
794
794
|
|
|
795
795
|
// 2. 调用供应商的退货请求方法
|
|
796
796
|
const returnResult = await vendorAdapter.requestReturn({
|