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.
- package/api/src/crons/index.ts +11 -3
- package/api/src/index.ts +18 -14
- package/api/src/libs/adapters/launcher-adapter.ts +177 -0
- package/api/src/libs/env.ts +7 -0
- package/api/src/libs/url.ts +77 -0
- package/api/src/libs/vendor-adapter-factory.ts +22 -0
- package/api/src/libs/vendor-adapter.ts +109 -0
- package/api/src/libs/vendor-fulfillment.ts +321 -0
- package/api/src/queues/payment.ts +14 -10
- package/api/src/queues/payout.ts +1 -0
- package/api/src/queues/vendor/vendor-commission.ts +192 -0
- package/api/src/queues/vendor/vendor-fulfillment-coordinator.ts +627 -0
- package/api/src/queues/vendor/vendor-fulfillment.ts +97 -0
- package/api/src/queues/vendor/vendor-status-check.ts +179 -0
- package/api/src/routes/checkout-sessions.ts +3 -0
- package/api/src/routes/index.ts +2 -0
- package/api/src/routes/products.ts +72 -1
- package/api/src/routes/vendor.ts +526 -0
- package/api/src/store/migrations/20250820-add-product-vendor.ts +102 -0
- package/api/src/store/migrations/20250822-add-vendor-config-to-products.ts +56 -0
- package/api/src/store/models/checkout-session.ts +84 -18
- package/api/src/store/models/index.ts +3 -0
- package/api/src/store/models/payout.ts +11 -0
- package/api/src/store/models/product-vendor.ts +118 -0
- package/api/src/store/models/product.ts +15 -0
- package/blocklet.yml +8 -2
- package/doc/vendor_fulfillment_system.md +931 -0
- package/package.json +5 -4
- package/src/components/collapse.tsx +1 -0
- package/src/components/product/edit.tsx +9 -0
- package/src/components/product/form.tsx +11 -0
- package/src/components/product/vendor-config.tsx +249 -0
- package/src/components/vendor/actions.tsx +145 -0
- package/src/locales/en.tsx +89 -0
- package/src/locales/zh.tsx +89 -0
- package/src/pages/admin/products/index.tsx +11 -1
- package/src/pages/admin/products/products/detail.tsx +79 -2
- package/src/pages/admin/products/vendors/create.tsx +418 -0
- package/src/pages/admin/products/vendors/index.tsx +313 -0
|
@@ -0,0 +1,526 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import Joi from 'joi';
|
|
3
|
+
|
|
4
|
+
import { joinURL } from 'ufo';
|
|
5
|
+
import { VendorAuth } from '@blocklet/payment-vendor';
|
|
6
|
+
import { VendorFulfillmentService } from '../libs/vendor-fulfillment';
|
|
7
|
+
import logger from '../libs/logger';
|
|
8
|
+
import { ProductVendor } from '../store/models/product-vendor';
|
|
9
|
+
import { wallet } from '../libs/auth';
|
|
10
|
+
import { CheckoutSession } from '../store/models';
|
|
11
|
+
import { authenticate } from '../libs/security';
|
|
12
|
+
import { formatToShortUrl } from '../libs/url';
|
|
13
|
+
import dayjs from '../libs/dayjs';
|
|
14
|
+
import { MetadataSchema } from '../libs/api';
|
|
15
|
+
|
|
16
|
+
const authAdmin = authenticate<CheckoutSession>({ component: true, roles: ['owner', 'admin'] });
|
|
17
|
+
|
|
18
|
+
const createVendorSchema = Joi.object({
|
|
19
|
+
vendor_key: Joi.string().max(50).required(),
|
|
20
|
+
name: Joi.string().max(255).required(),
|
|
21
|
+
description: Joi.string().max(1000).allow('').optional(),
|
|
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
|
+
app_pid: Joi.string().max(255).allow('').optional(),
|
|
29
|
+
app_logo: Joi.string().max(512).allow('').optional(),
|
|
30
|
+
status: Joi.string().valid('active', 'inactive').default('active'),
|
|
31
|
+
metadata: MetadataSchema,
|
|
32
|
+
}).unknown(false);
|
|
33
|
+
|
|
34
|
+
const updateVendorSchema = Joi.object({
|
|
35
|
+
name: Joi.string().max(255).optional(),
|
|
36
|
+
description: Joi.string().max(1000).allow('').optional(),
|
|
37
|
+
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
|
+
app_pid: Joi.string().max(255).allow('').optional(),
|
|
44
|
+
app_logo: Joi.string().max(512).allow('').optional(),
|
|
45
|
+
status: Joi.string().valid('active', 'inactive').optional(),
|
|
46
|
+
metadata: MetadataSchema,
|
|
47
|
+
}).unknown(true);
|
|
48
|
+
|
|
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
|
+
const vendorIdParamSchema = Joi.object({
|
|
56
|
+
id: Joi.string().max(100).required(),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const sessionIdParamSchema = Joi.object({
|
|
60
|
+
sessionId: Joi.string().max(100).required(),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
function validateParams(schema: Joi.ObjectSchema) {
|
|
64
|
+
return (req: any, res: any, next: any) => {
|
|
65
|
+
const { error } = schema.validate(req.params);
|
|
66
|
+
if (error) {
|
|
67
|
+
logger.error('Invalid parameters', {
|
|
68
|
+
error: error.message,
|
|
69
|
+
params: req.params,
|
|
70
|
+
});
|
|
71
|
+
return res.status(400).json({
|
|
72
|
+
error: 'Invalid parameters',
|
|
73
|
+
message: error.message,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
return next();
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function getAllVendors(_req: any, res: any) {
|
|
81
|
+
try {
|
|
82
|
+
const vendors = await ProductVendor.findAll({
|
|
83
|
+
order: [['created_at', 'DESC']],
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return res.json({
|
|
87
|
+
data: vendors,
|
|
88
|
+
pk: wallet.publicKey,
|
|
89
|
+
total: vendors.length,
|
|
90
|
+
});
|
|
91
|
+
} catch (error: any) {
|
|
92
|
+
logger.error('Failed to get vendors', {
|
|
93
|
+
error: error.message || error.toString() || 'Unknown error',
|
|
94
|
+
stack: error.stack,
|
|
95
|
+
});
|
|
96
|
+
return res.status(500).json({ error: 'Internal server error. Failed to get vendors' });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function getVendorById(req: any, res: any) {
|
|
101
|
+
try {
|
|
102
|
+
const vendor = await ProductVendor.findByPk(req.params.id);
|
|
103
|
+
if (!vendor) {
|
|
104
|
+
return res.status(404).json({ error: 'Vendor not found' });
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return res.json({ data: vendor });
|
|
108
|
+
} catch (error: any) {
|
|
109
|
+
logger.error('Failed to get vendor', {
|
|
110
|
+
error: error.message || error.toString() || 'Unknown error',
|
|
111
|
+
id: req.params.id,
|
|
112
|
+
});
|
|
113
|
+
return res.status(500).json({ error: 'Internal server error. Failed to get vendor' });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function createVendor(req: any, res: any) {
|
|
118
|
+
try {
|
|
119
|
+
const { error, value } = createVendorSchema.validate(req.body);
|
|
120
|
+
if (error) {
|
|
121
|
+
return res.status(400).json({
|
|
122
|
+
error: 'Validation failed',
|
|
123
|
+
message: error.message,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const {
|
|
128
|
+
vendor_key: vendorKey,
|
|
129
|
+
name,
|
|
130
|
+
description,
|
|
131
|
+
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
|
+
metadata,
|
|
138
|
+
app_pid: appPid,
|
|
139
|
+
app_logo: appLogo,
|
|
140
|
+
status,
|
|
141
|
+
} = value;
|
|
142
|
+
|
|
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
|
+
const existingVendor = await ProductVendor.findOne({
|
|
151
|
+
where: { vendor_key: vendorKey },
|
|
152
|
+
});
|
|
153
|
+
if (existingVendor) {
|
|
154
|
+
return res.status(400).json({ error: 'Vendor key already exists' });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const vendor = await ProductVendor.create({
|
|
158
|
+
vendor_key: vendorKey,
|
|
159
|
+
name,
|
|
160
|
+
description,
|
|
161
|
+
app_url: appUrl,
|
|
162
|
+
webhook_path: webhookPath,
|
|
163
|
+
default_commission_rate: defaultCommissionRate,
|
|
164
|
+
default_commission_type: defaultCommissionType,
|
|
165
|
+
order_create_params: orderCreateParams || {},
|
|
166
|
+
status: status || 'active',
|
|
167
|
+
app_pid: appPid,
|
|
168
|
+
app_logo: appLogo,
|
|
169
|
+
metadata: {
|
|
170
|
+
...(metadata || {}),
|
|
171
|
+
...(blockletMetaUrl && { blockletMetaUrl }),
|
|
172
|
+
},
|
|
173
|
+
created_by: req.user?.did || 'admin',
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
logger.info('Vendor created', { vendorId: vendor.id, vendorKey: vendor.vendor_key });
|
|
177
|
+
return res.status(201).json({ data: vendor });
|
|
178
|
+
} catch (error: any) {
|
|
179
|
+
logger.error('Failed to create vendor', {
|
|
180
|
+
error: error.message || error.toString() || 'Unknown error',
|
|
181
|
+
});
|
|
182
|
+
return res.status(500).json({ error: 'Internal server error' });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function updateVendor(req: any, res: any) {
|
|
187
|
+
try {
|
|
188
|
+
const vendor = await ProductVendor.findByPk(req.params.id);
|
|
189
|
+
if (!vendor) {
|
|
190
|
+
return res.status(404).json({ error: 'Vendor not found' });
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const { error, value } = updateVendorSchema.validate(req.body);
|
|
194
|
+
if (error) {
|
|
195
|
+
return res.status(400).json({
|
|
196
|
+
error: 'Validation failed',
|
|
197
|
+
message: error.message,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const {
|
|
202
|
+
name,
|
|
203
|
+
description,
|
|
204
|
+
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
|
+
status,
|
|
211
|
+
metadata,
|
|
212
|
+
app_pid: appPid,
|
|
213
|
+
app_logo: appLogo,
|
|
214
|
+
} = value;
|
|
215
|
+
|
|
216
|
+
if (req.body.vendorKey && req.body.vendorKey !== vendor.vendor_key) {
|
|
217
|
+
const existingVendor = await ProductVendor.findOne({
|
|
218
|
+
where: { vendor_key: req.body.vendorKey },
|
|
219
|
+
});
|
|
220
|
+
if (existingVendor) {
|
|
221
|
+
return res.status(400).json({ error: 'Vendor key already exists' });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
const updates = {
|
|
225
|
+
name,
|
|
226
|
+
description,
|
|
227
|
+
app_url: appUrl,
|
|
228
|
+
webhook_path: webhookPath,
|
|
229
|
+
default_commission_rate: defaultCommissionRate,
|
|
230
|
+
default_commission_type: defaultCommissionType,
|
|
231
|
+
order_create_params: orderCreateParams,
|
|
232
|
+
status,
|
|
233
|
+
...((metadata || blockletMetaUrl !== undefined) && {
|
|
234
|
+
metadata: {
|
|
235
|
+
...(vendor.metadata || {}),
|
|
236
|
+
...(metadata || {}),
|
|
237
|
+
...(blockletMetaUrl !== undefined && { blockletMetaUrl }),
|
|
238
|
+
},
|
|
239
|
+
}),
|
|
240
|
+
app_pid: appPid,
|
|
241
|
+
app_logo: appLogo,
|
|
242
|
+
vendor_key: req.body.vendor_key,
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
await vendor.update(Object.fromEntries(Object.entries(updates).filter(([, v]) => v !== undefined)));
|
|
246
|
+
|
|
247
|
+
logger.info('Vendor updated', { vendorId: vendor.id });
|
|
248
|
+
return res.json({ data: vendor });
|
|
249
|
+
} catch (error: any) {
|
|
250
|
+
logger.error('Failed to update vendor', { error: error.message, id: req.params.id });
|
|
251
|
+
return res.status(500).json({ error: 'Internal server error' });
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function deleteVendor(req: any, res: any) {
|
|
256
|
+
try {
|
|
257
|
+
const vendor = await ProductVendor.findByPk(req.params.id);
|
|
258
|
+
if (!vendor) {
|
|
259
|
+
return res.status(404).json({ error: 'Vendor not found' });
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
await vendor.destroy();
|
|
263
|
+
|
|
264
|
+
logger.info('Vendor deleted', { vendorId: vendor.id });
|
|
265
|
+
return res.json({ success: true });
|
|
266
|
+
} catch (error: any) {
|
|
267
|
+
logger.error('Failed to delete vendor', { error: error.message, id: req.params.id });
|
|
268
|
+
return res.status(500).json({ error: 'Internal server error' });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async function testVendorConnection(req: any, res: any) {
|
|
273
|
+
try {
|
|
274
|
+
const vendor = await ProductVendor.findByPk(req.params.id);
|
|
275
|
+
if (!vendor) {
|
|
276
|
+
return res.status(404).json({ error: 'Vendor not found' });
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return res.json({
|
|
280
|
+
success: true,
|
|
281
|
+
message: 'Connection test completed',
|
|
282
|
+
vendor: {
|
|
283
|
+
id: vendor.id,
|
|
284
|
+
name: vendor.name,
|
|
285
|
+
app_url: vendor.app_url,
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
} catch (error: any) {
|
|
289
|
+
logger.error('Failed to test vendor connection', { error: error.message, id: req.params.id });
|
|
290
|
+
return res.status(500).json({ error: 'Internal server error' });
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async function testVendorFulfillment(req: any, res: any) {
|
|
295
|
+
try {
|
|
296
|
+
const vendor = await ProductVendor.findByPk(req.params.id);
|
|
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
|
+
amount_total: testCheckoutSession.amount_total.toString(),
|
|
368
|
+
customer_id: testCheckoutSession.customer_id,
|
|
369
|
+
payment_intent_id: testCheckoutSession.payment_intent_id,
|
|
370
|
+
currency_id: testCheckoutSession.currency_id,
|
|
371
|
+
customer_did: testCheckoutSession.customer_did,
|
|
372
|
+
};
|
|
373
|
+
const fulfillmentResult = await VendorFulfillmentService.fulfillSingleVendorOrder(orderInfo, testVendorConfig);
|
|
374
|
+
|
|
375
|
+
logger.info('Test fulfillment completed', {
|
|
376
|
+
vendorId: vendor.id,
|
|
377
|
+
orderId: fulfillmentResult.orderId,
|
|
378
|
+
status: fulfillmentResult.status,
|
|
379
|
+
serviceUrl: fulfillmentResult.serviceUrl,
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
return res.json({
|
|
383
|
+
success: true,
|
|
384
|
+
message: 'Test fulfillment completed successfully!',
|
|
385
|
+
vendor: {
|
|
386
|
+
id: vendor.id,
|
|
387
|
+
name: vendor.name,
|
|
388
|
+
vendor_key: vendor.vendor_key,
|
|
389
|
+
app_url: vendor.app_url,
|
|
390
|
+
},
|
|
391
|
+
testData: {
|
|
392
|
+
checkoutSessionId: testCheckoutSession.id,
|
|
393
|
+
paymentIntentId: testCheckoutSession.payment_intent_id,
|
|
394
|
+
productCode: req.body.productCode || 'test_product',
|
|
395
|
+
amount: testCheckoutSession.amount,
|
|
396
|
+
currency: testCheckoutSession.currency,
|
|
397
|
+
},
|
|
398
|
+
fulfillmentResult,
|
|
399
|
+
});
|
|
400
|
+
} catch (error: any) {
|
|
401
|
+
logger.error('Test fulfillment failed', {
|
|
402
|
+
error: error.message,
|
|
403
|
+
stack: error.stack,
|
|
404
|
+
vendorId: req.params.id,
|
|
405
|
+
requestBody: req.body,
|
|
406
|
+
});
|
|
407
|
+
return res.status(500).json({
|
|
408
|
+
error: 'Test fulfillment failed',
|
|
409
|
+
details: error.message,
|
|
410
|
+
vendor: req.params.id,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
async function getVendorStatus(sessionId: string, isDetail = false) {
|
|
416
|
+
const doc = await CheckoutSession.findByPk(sessionId);
|
|
417
|
+
|
|
418
|
+
if (!doc) {
|
|
419
|
+
return {
|
|
420
|
+
code: 404,
|
|
421
|
+
error: 'CheckoutSession not found',
|
|
422
|
+
vendors: [],
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (doc.status !== 'complete') {
|
|
427
|
+
return {
|
|
428
|
+
payment_status: doc.payment_status,
|
|
429
|
+
session_status: doc.status,
|
|
430
|
+
error: 'CheckoutSession not complete',
|
|
431
|
+
vendors: [],
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
if (!doc.vendor_info) {
|
|
435
|
+
return {
|
|
436
|
+
payment_status: doc.payment_status,
|
|
437
|
+
session_status: doc.status,
|
|
438
|
+
error: 'Vendor info not found',
|
|
439
|
+
vendors: [],
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const vendors = doc.vendor_info.map(async (item) => {
|
|
444
|
+
const vendor = await ProductVendor.findByPk(item.vendor_id);
|
|
445
|
+
const url = vendor?.app_url
|
|
446
|
+
? joinURL(vendor.app_url, '/api/vendor/', isDetail ? 'orders' : 'status', item.order_id)
|
|
447
|
+
: null;
|
|
448
|
+
|
|
449
|
+
const { headers } = VendorAuth.signRequestWithHeaders({});
|
|
450
|
+
|
|
451
|
+
return url
|
|
452
|
+
? fetch(url, { headers })
|
|
453
|
+
.then(async (r) => {
|
|
454
|
+
const data = await r.json();
|
|
455
|
+
|
|
456
|
+
if (!data.dashboardUrl) {
|
|
457
|
+
return data;
|
|
458
|
+
}
|
|
459
|
+
const validUntil = dayjs().add(20, 'minutes').format('YYYY-MM-DDTHH:mm:ss+00:00');
|
|
460
|
+
const maxVisits = 5;
|
|
461
|
+
|
|
462
|
+
const homeUrl = await formatToShortUrl({ url: data.homeUrl, maxVisits, validUntil });
|
|
463
|
+
const dashboardUrl = await formatToShortUrl({ url: data.dashboardUrl, maxVisits, validUntil });
|
|
464
|
+
|
|
465
|
+
return {
|
|
466
|
+
...data,
|
|
467
|
+
homeUrl,
|
|
468
|
+
dashboardUrl,
|
|
469
|
+
};
|
|
470
|
+
})
|
|
471
|
+
.catch((e) => ({ error: e.message }))
|
|
472
|
+
: null;
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
return {
|
|
476
|
+
payment_status: doc.payment_status,
|
|
477
|
+
session_status: doc.status,
|
|
478
|
+
vendors: await Promise.all(vendors),
|
|
479
|
+
error: null,
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
async function getVendorFulfillmentStatus(req: any, res: any) {
|
|
484
|
+
const { sessionId } = req.params;
|
|
485
|
+
|
|
486
|
+
try {
|
|
487
|
+
const status = await getVendorStatus(sessionId);
|
|
488
|
+
|
|
489
|
+
if (status.code) {
|
|
490
|
+
return res.status(status.code).json({ error: status.error });
|
|
491
|
+
}
|
|
492
|
+
return res.json(status);
|
|
493
|
+
} catch (error: any) {
|
|
494
|
+
return res.status(500).json({ error: error.message });
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
async function getVendorFulfillmentDetail(req: any, res: any) {
|
|
499
|
+
const { sessionId } = req.params;
|
|
500
|
+
|
|
501
|
+
try {
|
|
502
|
+
const detail = await getVendorStatus(sessionId, true);
|
|
503
|
+
if (detail.code) {
|
|
504
|
+
return res.status(detail.code).json({ error: detail.error });
|
|
505
|
+
}
|
|
506
|
+
return res.json(detail);
|
|
507
|
+
} catch (error: any) {
|
|
508
|
+
return res.status(500).json({ error: error.message });
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const router = Router();
|
|
513
|
+
|
|
514
|
+
router.get('/order/:sessionId/status', validateParams(sessionIdParamSchema), getVendorFulfillmentStatus);
|
|
515
|
+
router.get('/order/:sessionId/detail', validateParams(sessionIdParamSchema), getVendorFulfillmentDetail);
|
|
516
|
+
|
|
517
|
+
router.get('/', getAllVendors);
|
|
518
|
+
router.get('/:id', authAdmin, validateParams(vendorIdParamSchema), getVendorById);
|
|
519
|
+
router.post('/', authAdmin, createVendor);
|
|
520
|
+
router.put('/:id', authAdmin, validateParams(vendorIdParamSchema), updateVendor);
|
|
521
|
+
router.delete('/:id', authAdmin, validateParams(vendorIdParamSchema), deleteVendor);
|
|
522
|
+
|
|
523
|
+
router.post('/:id/test-connection', authAdmin, validateParams(vendorIdParamSchema), testVendorConnection);
|
|
524
|
+
router.post('/:id/test-fulfillment', authAdmin, validateParams(vendorIdParamSchema), testVendorFulfillment);
|
|
525
|
+
|
|
526
|
+
export default router;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { createIndexIfNotExists, type Migration } from '../migrate';
|
|
2
|
+
|
|
3
|
+
export const up: Migration = async ({ context }) => {
|
|
4
|
+
await context.createTable('product_vendors', {
|
|
5
|
+
id: {
|
|
6
|
+
type: 'STRING',
|
|
7
|
+
primaryKey: true,
|
|
8
|
+
allowNull: false,
|
|
9
|
+
field: 'id',
|
|
10
|
+
},
|
|
11
|
+
vendor_key: {
|
|
12
|
+
type: 'STRING',
|
|
13
|
+
allowNull: false,
|
|
14
|
+
unique: true,
|
|
15
|
+
field: 'vendor_key',
|
|
16
|
+
},
|
|
17
|
+
name: {
|
|
18
|
+
type: 'STRING',
|
|
19
|
+
allowNull: false,
|
|
20
|
+
field: 'name',
|
|
21
|
+
},
|
|
22
|
+
description: {
|
|
23
|
+
type: 'TEXT',
|
|
24
|
+
allowNull: true,
|
|
25
|
+
field: 'description',
|
|
26
|
+
},
|
|
27
|
+
app_url: {
|
|
28
|
+
type: 'STRING',
|
|
29
|
+
allowNull: false,
|
|
30
|
+
field: 'app_url',
|
|
31
|
+
},
|
|
32
|
+
app_pid: {
|
|
33
|
+
type: 'STRING',
|
|
34
|
+
allowNull: false,
|
|
35
|
+
field: 'app_pid',
|
|
36
|
+
},
|
|
37
|
+
app_logo: {
|
|
38
|
+
type: 'STRING',
|
|
39
|
+
allowNull: true,
|
|
40
|
+
field: 'app_logo',
|
|
41
|
+
},
|
|
42
|
+
webhook_path: {
|
|
43
|
+
type: 'STRING',
|
|
44
|
+
allowNull: true,
|
|
45
|
+
field: 'webhook_path',
|
|
46
|
+
},
|
|
47
|
+
default_commission_rate: {
|
|
48
|
+
type: 'DECIMAL',
|
|
49
|
+
allowNull: false,
|
|
50
|
+
field: 'default_commission_rate',
|
|
51
|
+
},
|
|
52
|
+
default_commission_type: {
|
|
53
|
+
type: 'STRING',
|
|
54
|
+
allowNull: false,
|
|
55
|
+
field: 'default_commission_type',
|
|
56
|
+
},
|
|
57
|
+
order_create_params: {
|
|
58
|
+
type: 'JSON',
|
|
59
|
+
allowNull: true,
|
|
60
|
+
defaultValue: '{}',
|
|
61
|
+
field: 'order_create_params',
|
|
62
|
+
},
|
|
63
|
+
status: {
|
|
64
|
+
type: 'STRING',
|
|
65
|
+
allowNull: false,
|
|
66
|
+
defaultValue: 'active',
|
|
67
|
+
field: 'status',
|
|
68
|
+
},
|
|
69
|
+
metadata: {
|
|
70
|
+
type: 'JSON',
|
|
71
|
+
allowNull: true,
|
|
72
|
+
defaultValue: '{}',
|
|
73
|
+
field: 'metadata',
|
|
74
|
+
},
|
|
75
|
+
created_by: {
|
|
76
|
+
type: 'STRING',
|
|
77
|
+
allowNull: true,
|
|
78
|
+
field: 'created_by',
|
|
79
|
+
},
|
|
80
|
+
created_at: {
|
|
81
|
+
type: 'DATE',
|
|
82
|
+
defaultValue: 'CURRENT_TIMESTAMP',
|
|
83
|
+
allowNull: false,
|
|
84
|
+
field: 'created_at',
|
|
85
|
+
},
|
|
86
|
+
updated_at: {
|
|
87
|
+
type: 'DATE',
|
|
88
|
+
defaultValue: 'CURRENT_TIMESTAMP',
|
|
89
|
+
allowNull: false,
|
|
90
|
+
field: 'updated_at',
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
// 添加索引
|
|
95
|
+
await createIndexIfNotExists(context, 'product_vendors', ['vendor_key'], 'idx_product_vendors_vendor_key');
|
|
96
|
+
await createIndexIfNotExists(context, 'product_vendors', ['status'], 'idx_product_vendors_status');
|
|
97
|
+
await createIndexIfNotExists(context, 'product_vendors', ['created_at'], 'idx_product_vendors_created_at');
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export const down: Migration = async ({ context }) => {
|
|
101
|
+
await context.dropTable('product_vendors');
|
|
102
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { DataTypes } from 'sequelize';
|
|
2
|
+
import { createIndexIfNotExists, safeApplyColumnChanges, type Migration } from '../migrate';
|
|
3
|
+
|
|
4
|
+
export const up: Migration = async ({ context }) => {
|
|
5
|
+
await safeApplyColumnChanges(context, {
|
|
6
|
+
products: [
|
|
7
|
+
{
|
|
8
|
+
name: 'vendor_config',
|
|
9
|
+
field: {
|
|
10
|
+
type: DataTypes.JSON,
|
|
11
|
+
allowNull: true,
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
],
|
|
15
|
+
checkout_sessions: [
|
|
16
|
+
{
|
|
17
|
+
name: 'fulfillment_status',
|
|
18
|
+
field: {
|
|
19
|
+
type: DataTypes.STRING,
|
|
20
|
+
allowNull: true,
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
name: 'vendor_info',
|
|
25
|
+
field: {
|
|
26
|
+
type: DataTypes.JSON,
|
|
27
|
+
allowNull: true,
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
payouts: [
|
|
32
|
+
{
|
|
33
|
+
name: 'vendor_info',
|
|
34
|
+
field: {
|
|
35
|
+
type: DataTypes.JSON,
|
|
36
|
+
allowNull: true,
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
await createIndexIfNotExists(
|
|
43
|
+
context,
|
|
44
|
+
'checkout_sessions',
|
|
45
|
+
['fulfillment_status'],
|
|
46
|
+
'idx_checkout_sessions_fulfillment_status'
|
|
47
|
+
);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const down: Migration = async ({ context }) => {
|
|
51
|
+
// 移除所有供应商相关字段
|
|
52
|
+
await context.removeColumn('products', 'vendor_config');
|
|
53
|
+
await context.removeColumn('checkout_sessions', 'vendor_info');
|
|
54
|
+
await context.removeColumn('checkout_sessions', 'fulfillment_status');
|
|
55
|
+
await context.removeColumn('payouts', 'vendor_info');
|
|
56
|
+
};
|