payment-kit 1.20.8 → 1.20.10
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/integrations/stripe/handlers/index.ts +10 -2
- package/api/src/libs/{vendor → vendor-util}/adapters/factory.ts +9 -5
- package/api/src/libs/{vendor → vendor-util}/adapters/launcher-adapter.ts +20 -18
- package/api/src/libs/{vendor → vendor-util}/adapters/types.ts +1 -4
- package/api/src/libs/{vendor → vendor-util}/fulfillment.ts +24 -127
- package/api/src/queues/payment.ts +1 -5
- package/api/src/queues/{vendor → vendors}/commission.ts +19 -18
- package/api/src/queues/{vendor → vendors}/fulfillment-coordinator.ts +35 -6
- package/api/src/queues/{vendor → vendors}/fulfillment.ts +2 -2
- package/api/src/queues/{vendor → vendors}/status-check.ts +13 -8
- package/api/src/routes/payment-links.ts +2 -1
- package/api/src/routes/products.ts +1 -0
- package/api/src/routes/vendor.ts +157 -216
- package/api/src/store/migrations/20250911-add-vendor-type.ts +26 -0
- package/api/src/store/migrations/20250916-add-vendor-did.ts +20 -0
- package/api/src/store/models/payout.ts +2 -2
- package/api/src/store/models/product-vendor.ts +11 -24
- package/api/src/store/models/product.ts +2 -0
- package/blocklet.yml +1 -1
- package/doc/vendor_fulfillment_system.md +1 -1
- package/package.json +5 -5
- 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 +16 -3
- package/src/locales/zh.tsx +18 -5
- package/src/pages/admin/products/links/create.tsx +2 -0
- package/src/pages/admin/products/vendors/create.tsx +140 -190
- 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,57 +1,48 @@
|
|
|
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(),
|
|
25
|
+
vendor_did: Joi.string()
|
|
26
|
+
.pattern(/^(did:abt:)?[1-9A-HJ-NP-Za-km-z]{37}$/)
|
|
27
|
+
.required(),
|
|
30
28
|
status: Joi.string().valid('active', 'inactive').default('active'),
|
|
31
29
|
metadata: MetadataSchema,
|
|
32
30
|
}).unknown(false);
|
|
33
31
|
|
|
34
32
|
const updateVendorSchema = Joi.object({
|
|
33
|
+
vendor_type: Joi.string().valid('launcher').optional(),
|
|
35
34
|
name: Joi.string().max(255).optional(),
|
|
36
35
|
description: Joi.string().max(1000).allow('').optional(),
|
|
37
36
|
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
37
|
app_pid: Joi.string().max(255).allow('').optional(),
|
|
44
38
|
app_logo: Joi.string().max(512).allow('').optional(),
|
|
39
|
+
vendor_did: Joi.string()
|
|
40
|
+
.pattern(/^(did:abt:)?[1-9A-HJ-NP-Za-km-z]{37}$/)
|
|
41
|
+
.required(),
|
|
45
42
|
status: Joi.string().valid('active', 'inactive').optional(),
|
|
46
43
|
metadata: MetadataSchema,
|
|
47
44
|
}).unknown(true);
|
|
48
45
|
|
|
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
46
|
const vendorIdParamSchema = Joi.object({
|
|
56
47
|
id: Joi.string().max(100).required(),
|
|
57
48
|
});
|
|
@@ -60,12 +51,21 @@ const sessionIdParamSchema = Joi.object({
|
|
|
60
51
|
sessionId: Joi.string().max(100).required(),
|
|
61
52
|
});
|
|
62
53
|
|
|
54
|
+
const subscriptionIdParamSchema = Joi.object({
|
|
55
|
+
subscriptionId: Joi.string().max(100).required(),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const vendorRedirectQuerySchema = Joi.object({
|
|
59
|
+
vendorId: Joi.string().max(100).required(),
|
|
60
|
+
target: Joi.string().valid('home', 'dashboard').default('home').allow(''),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
63
|
function validateParams(schema: Joi.ObjectSchema) {
|
|
64
64
|
return (req: any, res: any, next: any) => {
|
|
65
65
|
const { error } = schema.validate(req.params);
|
|
66
66
|
if (error) {
|
|
67
67
|
logger.error('Invalid parameters', {
|
|
68
|
-
error
|
|
68
|
+
error,
|
|
69
69
|
params: req.params,
|
|
70
70
|
});
|
|
71
71
|
return res.status(400).json({
|
|
@@ -77,6 +77,21 @@ function validateParams(schema: Joi.ObjectSchema) {
|
|
|
77
77
|
};
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
function validateQuery(schema: Joi.ObjectSchema) {
|
|
81
|
+
return (req: any, res: any, next: any) => {
|
|
82
|
+
const { error, value } = schema.validate(req.query);
|
|
83
|
+
if (error) {
|
|
84
|
+
logger.error('Invalid query parameters', {
|
|
85
|
+
error,
|
|
86
|
+
query: req.query,
|
|
87
|
+
});
|
|
88
|
+
return res.redirect('/404');
|
|
89
|
+
}
|
|
90
|
+
req.query = value;
|
|
91
|
+
return next();
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
80
95
|
async function getAllVendors(_req: any, res: any) {
|
|
81
96
|
try {
|
|
82
97
|
const vendors = await ProductVendor.findAll({
|
|
@@ -90,7 +105,7 @@ async function getAllVendors(_req: any, res: any) {
|
|
|
90
105
|
});
|
|
91
106
|
} catch (error: any) {
|
|
92
107
|
logger.error('Failed to get vendors', {
|
|
93
|
-
error
|
|
108
|
+
error,
|
|
94
109
|
stack: error.stack,
|
|
95
110
|
});
|
|
96
111
|
return res.status(500).json({ error: 'Internal server error. Failed to get vendors' });
|
|
@@ -107,7 +122,7 @@ async function getVendorById(req: any, res: any) {
|
|
|
107
122
|
return res.json({ data: vendor });
|
|
108
123
|
} catch (error: any) {
|
|
109
124
|
logger.error('Failed to get vendor', {
|
|
110
|
-
error
|
|
125
|
+
error,
|
|
111
126
|
id: req.params.id,
|
|
112
127
|
});
|
|
113
128
|
return res.status(500).json({ error: 'Internal server error. Failed to get vendor' });
|
|
@@ -126,27 +141,17 @@ async function createVendor(req: any, res: any) {
|
|
|
126
141
|
|
|
127
142
|
const {
|
|
128
143
|
vendor_key: vendorKey,
|
|
144
|
+
vendor_type: vendorType,
|
|
129
145
|
name,
|
|
130
146
|
description,
|
|
131
147
|
app_url: appUrl,
|
|
132
|
-
|
|
133
|
-
blocklet_meta_url: blockletMetaUrl,
|
|
134
|
-
default_commission_rate: defaultCommissionRate,
|
|
135
|
-
default_commission_type: defaultCommissionType,
|
|
136
|
-
order_create_params: orderCreateParams,
|
|
148
|
+
vendor_did: vendorDid,
|
|
137
149
|
metadata,
|
|
138
150
|
app_pid: appPid,
|
|
139
151
|
app_logo: appLogo,
|
|
140
152
|
status,
|
|
141
153
|
} = value;
|
|
142
154
|
|
|
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
155
|
const existingVendor = await ProductVendor.findOne({
|
|
151
156
|
where: { vendor_key: vendorKey },
|
|
152
157
|
});
|
|
@@ -156,20 +161,15 @@ async function createVendor(req: any, res: any) {
|
|
|
156
161
|
|
|
157
162
|
const vendor = await ProductVendor.create({
|
|
158
163
|
vendor_key: vendorKey,
|
|
164
|
+
vendor_type: vendorType || 'launcher',
|
|
159
165
|
name,
|
|
160
166
|
description,
|
|
161
167
|
app_url: appUrl,
|
|
162
|
-
|
|
163
|
-
default_commission_rate: defaultCommissionRate,
|
|
164
|
-
default_commission_type: defaultCommissionType,
|
|
165
|
-
order_create_params: orderCreateParams || {},
|
|
168
|
+
vendor_did: vendorDid?.replace('did:abt:', '').trim(),
|
|
166
169
|
status: status || 'active',
|
|
167
170
|
app_pid: appPid,
|
|
168
171
|
app_logo: appLogo,
|
|
169
|
-
metadata: {
|
|
170
|
-
...(metadata || {}),
|
|
171
|
-
...(blockletMetaUrl && { blockletMetaUrl }),
|
|
172
|
-
},
|
|
172
|
+
metadata: metadata || {},
|
|
173
173
|
created_by: req.user?.did || 'admin',
|
|
174
174
|
});
|
|
175
175
|
|
|
@@ -177,7 +177,7 @@ async function createVendor(req: any, res: any) {
|
|
|
177
177
|
return res.status(201).json({ data: vendor });
|
|
178
178
|
} catch (error: any) {
|
|
179
179
|
logger.error('Failed to create vendor', {
|
|
180
|
-
error
|
|
180
|
+
error,
|
|
181
181
|
});
|
|
182
182
|
return res.status(500).json({ error: 'Internal server error' });
|
|
183
183
|
}
|
|
@@ -199,14 +199,11 @@ async function updateVendor(req: any, res: any) {
|
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
const {
|
|
202
|
+
vendor_type: vendorType,
|
|
202
203
|
name,
|
|
203
204
|
description,
|
|
204
205
|
app_url: appUrl,
|
|
205
|
-
|
|
206
|
-
blocklet_meta_url: blockletMetaUrl,
|
|
207
|
-
default_commission_rate: defaultCommissionRate,
|
|
208
|
-
default_commission_type: defaultCommissionType,
|
|
209
|
-
order_create_params: orderCreateParams,
|
|
206
|
+
vendor_did: vendorDid,
|
|
210
207
|
status,
|
|
211
208
|
metadata,
|
|
212
209
|
app_pid: appPid,
|
|
@@ -222,21 +219,13 @@ async function updateVendor(req: any, res: any) {
|
|
|
222
219
|
}
|
|
223
220
|
}
|
|
224
221
|
const updates = {
|
|
222
|
+
vendor_type: vendorType,
|
|
225
223
|
name,
|
|
226
224
|
description,
|
|
227
225
|
app_url: appUrl,
|
|
228
|
-
|
|
229
|
-
default_commission_rate: defaultCommissionRate,
|
|
230
|
-
default_commission_type: defaultCommissionType,
|
|
231
|
-
order_create_params: orderCreateParams,
|
|
226
|
+
vendor_did: vendorDid,
|
|
232
227
|
status,
|
|
233
|
-
|
|
234
|
-
metadata: {
|
|
235
|
-
...(vendor.metadata || {}),
|
|
236
|
-
...(metadata || {}),
|
|
237
|
-
...(blockletMetaUrl !== undefined && { blockletMetaUrl }),
|
|
238
|
-
},
|
|
239
|
-
}),
|
|
228
|
+
metadata,
|
|
240
229
|
app_pid: appPid,
|
|
241
230
|
app_logo: appLogo,
|
|
242
231
|
vendor_key: req.body.vendor_key,
|
|
@@ -247,7 +236,7 @@ async function updateVendor(req: any, res: any) {
|
|
|
247
236
|
logger.info('Vendor updated', { vendorId: vendor.id });
|
|
248
237
|
return res.json({ data: vendor });
|
|
249
238
|
} catch (error: any) {
|
|
250
|
-
logger.error('Failed to update vendor', { error
|
|
239
|
+
logger.error('Failed to update vendor', { error, id: req.params.id });
|
|
251
240
|
return res.status(500).json({ error: 'Internal server error' });
|
|
252
241
|
}
|
|
253
242
|
}
|
|
@@ -264,7 +253,7 @@ async function deleteVendor(req: any, res: any) {
|
|
|
264
253
|
logger.info('Vendor deleted', { vendorId: vendor.id });
|
|
265
254
|
return res.json({ success: true });
|
|
266
255
|
} catch (error: any) {
|
|
267
|
-
logger.error('Failed to delete vendor', { error
|
|
256
|
+
logger.error('Failed to delete vendor', { error, id: req.params.id });
|
|
268
257
|
return res.status(500).json({ error: 'Internal server error' });
|
|
269
258
|
}
|
|
270
259
|
}
|
|
@@ -286,131 +275,59 @@ async function testVendorConnection(req: any, res: any) {
|
|
|
286
275
|
},
|
|
287
276
|
});
|
|
288
277
|
} catch (error: any) {
|
|
289
|
-
logger.error('Failed to test vendor connection', { error
|
|
278
|
+
logger.error('Failed to test vendor connection', { error, id: req.params.id });
|
|
290
279
|
return res.status(500).json({ error: 'Internal server error' });
|
|
291
280
|
}
|
|
292
281
|
}
|
|
293
282
|
|
|
294
|
-
async function
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
if (!vendor) {
|
|
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
|
-
};
|
|
354
|
-
|
|
355
|
-
logger.info('Starting test fulfillment', {
|
|
356
|
-
vendorId: vendor.id,
|
|
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
|
-
});
|
|
365
|
-
|
|
366
|
-
const orderInfo = {
|
|
367
|
-
checkoutSessionId: testCheckoutSession.id,
|
|
368
|
-
amount_total: testCheckoutSession.amount_total.toString(),
|
|
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);
|
|
375
|
-
|
|
376
|
-
logger.info('Test fulfillment completed', {
|
|
377
|
-
vendorId: vendor.id,
|
|
378
|
-
orderId: fulfillmentResult.orderId,
|
|
379
|
-
status: fulfillmentResult.status,
|
|
380
|
-
serviceUrl: fulfillmentResult.serviceUrl,
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
return res.json({
|
|
384
|
-
success: true,
|
|
385
|
-
message: 'Test fulfillment completed successfully!',
|
|
386
|
-
vendor: {
|
|
387
|
-
id: vendor.id,
|
|
388
|
-
name: vendor.name,
|
|
389
|
-
vendor_key: vendor.vendor_key,
|
|
390
|
-
app_url: vendor.app_url,
|
|
391
|
-
},
|
|
392
|
-
testData: {
|
|
393
|
-
checkoutSessionId: testCheckoutSession.id,
|
|
394
|
-
paymentIntentId: testCheckoutSession.payment_intent_id,
|
|
395
|
-
productCode: req.body.productCode || 'test_product',
|
|
396
|
-
amount: testCheckoutSession.amount,
|
|
397
|
-
currency: testCheckoutSession.currency,
|
|
398
|
-
},
|
|
399
|
-
fulfillmentResult,
|
|
400
|
-
});
|
|
401
|
-
} catch (error: any) {
|
|
402
|
-
logger.error('Test fulfillment failed', {
|
|
403
|
-
error: error.message,
|
|
404
|
-
stack: error.stack,
|
|
405
|
-
vendorId: req.params.id,
|
|
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
|
-
});
|
|
283
|
+
async function getVendorStatusByVendorId(vendorId: string, orderId: string, isDetail = false) {
|
|
284
|
+
if (!vendorId || !orderId) {
|
|
285
|
+
return {};
|
|
413
286
|
}
|
|
287
|
+
|
|
288
|
+
const vendor = await ProductVendor.findByPk(vendorId);
|
|
289
|
+
const url = vendor?.app_url
|
|
290
|
+
? joinURL(
|
|
291
|
+
vendor.app_url,
|
|
292
|
+
vendor.metadata?.mountPoint || '',
|
|
293
|
+
'/api/vendor/',
|
|
294
|
+
isDetail ? 'orders' : 'status',
|
|
295
|
+
orderId
|
|
296
|
+
)
|
|
297
|
+
: null;
|
|
298
|
+
|
|
299
|
+
const { headers } = VendorAuth.signRequestWithHeaders({});
|
|
300
|
+
const name = vendor?.name;
|
|
301
|
+
const key = vendor?.vendor_key;
|
|
302
|
+
|
|
303
|
+
return url
|
|
304
|
+
? fetch(url, { headers })
|
|
305
|
+
.then(async (r) => {
|
|
306
|
+
const data = await r.json();
|
|
307
|
+
|
|
308
|
+
if (!data.dashboardUrl) {
|
|
309
|
+
return {
|
|
310
|
+
...data,
|
|
311
|
+
name,
|
|
312
|
+
key,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
const validUntil = dayjs().add(20, 'minutes').format('YYYY-MM-DDTHH:mm:ss+00:00');
|
|
316
|
+
const maxVisits = 5;
|
|
317
|
+
|
|
318
|
+
const homeUrl = await formatToShortUrl({ url: data.homeUrl, maxVisits, validUntil });
|
|
319
|
+
const dashboardUrl = await formatToShortUrl({ url: data.dashboardUrl, maxVisits, validUntil });
|
|
320
|
+
|
|
321
|
+
return {
|
|
322
|
+
...data,
|
|
323
|
+
name,
|
|
324
|
+
key,
|
|
325
|
+
homeUrl,
|
|
326
|
+
dashboardUrl,
|
|
327
|
+
};
|
|
328
|
+
})
|
|
329
|
+
.catch((e) => ({ error: e.message }))
|
|
330
|
+
: null;
|
|
414
331
|
}
|
|
415
332
|
|
|
416
333
|
async function getVendorStatus(sessionId: string, isDetail = false) {
|
|
@@ -441,36 +358,8 @@ async function getVendorStatus(sessionId: string, isDetail = false) {
|
|
|
441
358
|
};
|
|
442
359
|
}
|
|
443
360
|
|
|
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;
|
|
361
|
+
const vendors = doc.vendor_info.map((item) => {
|
|
362
|
+
return getVendorStatusByVendorId(item.vendor_id, item.order_id, isDetail);
|
|
474
363
|
});
|
|
475
364
|
|
|
476
365
|
return {
|
|
@@ -510,11 +399,64 @@ async function getVendorFulfillmentDetail(req: any, res: any) {
|
|
|
510
399
|
}
|
|
511
400
|
}
|
|
512
401
|
|
|
402
|
+
async function redirectToVendor(req: any, res: any) {
|
|
403
|
+
const { subscriptionId } = req.params;
|
|
404
|
+
const { vendorId, target } = req.query;
|
|
405
|
+
|
|
406
|
+
try {
|
|
407
|
+
const checkoutSession = await CheckoutSession.findBySubscriptionId(subscriptionId);
|
|
408
|
+
if (!checkoutSession) {
|
|
409
|
+
logger.warn('CheckoutSession not found for subscription[redirect to vendor]', { subscriptionId });
|
|
410
|
+
return res.redirect('/404');
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const order = checkoutSession?.vendor_info?.find((item) => item.vendor_id === vendorId);
|
|
414
|
+
if (!order) {
|
|
415
|
+
logger.warn('Vendor order not found in checkout session[redirect to vendor]', {
|
|
416
|
+
subscriptionId,
|
|
417
|
+
vendorId,
|
|
418
|
+
availableVendors: checkoutSession.vendor_info?.map((v) => v.vendor_key) || [],
|
|
419
|
+
});
|
|
420
|
+
return res.redirect('/404');
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const detail = await getVendorStatusByVendorId(vendorId, order.order_id || '', true);
|
|
424
|
+
if (!detail) {
|
|
425
|
+
logger.warn('Vendor status detail not found', {
|
|
426
|
+
subscriptionId,
|
|
427
|
+
vendorId,
|
|
428
|
+
orderId: order.order_id,
|
|
429
|
+
});
|
|
430
|
+
return res.redirect('/404');
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
const redirectUrl = target === 'dashboard' ? detail.dashboardUrl : detail.homeUrl;
|
|
434
|
+
return res.redirect(redirectUrl);
|
|
435
|
+
} catch (error: any) {
|
|
436
|
+
logger.error('Failed to redirect to vendor service', {
|
|
437
|
+
error,
|
|
438
|
+
subscriptionId,
|
|
439
|
+
vendorId,
|
|
440
|
+
target,
|
|
441
|
+
});
|
|
442
|
+
return res.redirect('/404');
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
513
446
|
const router = Router();
|
|
514
447
|
|
|
448
|
+
// FIXME: Authentication not yet added, awaiting implementation @Pengfei
|
|
515
449
|
router.get('/order/:sessionId/status', validateParams(sessionIdParamSchema), getVendorFulfillmentStatus);
|
|
516
450
|
router.get('/order/:sessionId/detail', validateParams(sessionIdParamSchema), getVendorFulfillmentDetail);
|
|
517
451
|
|
|
452
|
+
router.get(
|
|
453
|
+
'/open/:subscriptionId',
|
|
454
|
+
authAdmin,
|
|
455
|
+
validateParams(subscriptionIdParamSchema),
|
|
456
|
+
validateQuery(vendorRedirectQuerySchema),
|
|
457
|
+
redirectToVendor
|
|
458
|
+
);
|
|
459
|
+
|
|
518
460
|
router.get('/', getAllVendors);
|
|
519
461
|
router.get('/:id', authAdmin, validateParams(vendorIdParamSchema), getVendorById);
|
|
520
462
|
router.post('/', authAdmin, createVendor);
|
|
@@ -522,6 +464,5 @@ router.put('/:id', authAdmin, validateParams(vendorIdParamSchema), updateVendor)
|
|
|
522
464
|
router.delete('/:id', authAdmin, validateParams(vendorIdParamSchema), deleteVendor);
|
|
523
465
|
|
|
524
466
|
router.post('/:id/test-connection', authAdmin, validateParams(vendorIdParamSchema), testVendorConnection);
|
|
525
|
-
router.post('/:id/test-fulfillment', authAdmin, validateParams(vendorIdParamSchema), testVendorFulfillment);
|
|
526
467
|
|
|
527
468
|
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
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { safeApplyColumnChanges, type Migration } from '../migrate';
|
|
2
|
+
|
|
3
|
+
export const up: Migration = async ({ context }) => {
|
|
4
|
+
// Add vendor_did column to product_vendors table
|
|
5
|
+
await safeApplyColumnChanges(context, {
|
|
6
|
+
product_vendors: [
|
|
7
|
+
{
|
|
8
|
+
name: 'vendor_did',
|
|
9
|
+
field: {
|
|
10
|
+
type: 'STRING',
|
|
11
|
+
allowNull: true,
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
],
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const down: Migration = async ({ context }) => {
|
|
19
|
+
await context.removeColumn('product_vendors', 'vendor_did');
|
|
20
|
+
};
|
|
@@ -38,7 +38,7 @@ export class Payout extends Model<InferAttributes<Payout>, InferCreationAttribut
|
|
|
38
38
|
commission_amount: string;
|
|
39
39
|
};
|
|
40
40
|
|
|
41
|
-
declare status: LiteralUnion<'pending' | 'paid' | 'failed' | 'canceled' | 'in_transit', string>;
|
|
41
|
+
declare status: LiteralUnion<'pending' | 'paid' | 'failed' | 'canceled' | 'in_transit' | 'deferred', string>;
|
|
42
42
|
|
|
43
43
|
// retry logic
|
|
44
44
|
declare failure_message?: string;
|
|
@@ -118,7 +118,7 @@ export class Payout extends Model<InferAttributes<Payout>, InferCreationAttribut
|
|
|
118
118
|
allowNull: true,
|
|
119
119
|
},
|
|
120
120
|
status: {
|
|
121
|
-
type: DataTypes.ENUM('paid', 'pending', 'failed', 'canceled', 'in_transit'),
|
|
121
|
+
type: DataTypes.ENUM('paid', 'pending', 'failed', 'canceled', 'in_transit', 'deferred'),
|
|
122
122
|
allowNull: false,
|
|
123
123
|
},
|
|
124
124
|
failure_message: {
|
|
@@ -8,19 +8,14 @@ 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;
|
|
18
|
+
declare vendor_did?: string;
|
|
24
19
|
|
|
25
20
|
declare status: 'active' | 'inactive';
|
|
26
21
|
declare metadata: Record<string, any>;
|
|
@@ -41,6 +36,11 @@ export class ProductVendor extends Model<InferAttributes<ProductVendor>, InferCr
|
|
|
41
36
|
allowNull: false,
|
|
42
37
|
unique: true,
|
|
43
38
|
},
|
|
39
|
+
vendor_type: {
|
|
40
|
+
type: DataTypes.STRING(50),
|
|
41
|
+
allowNull: false,
|
|
42
|
+
defaultValue: 'launcher',
|
|
43
|
+
},
|
|
44
44
|
name: {
|
|
45
45
|
type: DataTypes.STRING(255),
|
|
46
46
|
allowNull: false,
|
|
@@ -53,23 +53,6 @@ export class ProductVendor extends Model<InferAttributes<ProductVendor>, InferCr
|
|
|
53
53
|
type: DataTypes.STRING(512),
|
|
54
54
|
allowNull: false,
|
|
55
55
|
},
|
|
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
56
|
app_pid: {
|
|
74
57
|
type: DataTypes.STRING(255),
|
|
75
58
|
allowNull: true,
|
|
@@ -78,6 +61,10 @@ export class ProductVendor extends Model<InferAttributes<ProductVendor>, InferCr
|
|
|
78
61
|
type: DataTypes.STRING(512),
|
|
79
62
|
allowNull: true,
|
|
80
63
|
},
|
|
64
|
+
vendor_did: {
|
|
65
|
+
type: DataTypes.STRING(255),
|
|
66
|
+
allowNull: true,
|
|
67
|
+
},
|
|
81
68
|
status: {
|
|
82
69
|
type: DataTypes.ENUM('active', 'inactive'),
|
|
83
70
|
allowNull: false,
|
|
@@ -54,11 +54,13 @@ export class Product extends Model<InferAttributes<Product>, InferCreationAttrib
|
|
|
54
54
|
declare vendor_config?: Array<{
|
|
55
55
|
vendor_id: string;
|
|
56
56
|
vendor_key: string;
|
|
57
|
+
vendor_type: string;
|
|
57
58
|
name?: string;
|
|
58
59
|
description?: string;
|
|
59
60
|
commission_rate?: number;
|
|
60
61
|
commission_type?: 'percentage' | 'fixed_amount';
|
|
61
62
|
amount?: string;
|
|
63
|
+
commissionAmount?: string;
|
|
62
64
|
custom_params?: Record<string, any>;
|
|
63
65
|
}>;
|
|
64
66
|
|