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.
Files changed (36) hide show
  1. package/api/src/crons/index.ts +1 -1
  2. package/api/src/index.ts +2 -2
  3. package/api/src/integrations/stripe/handlers/index.ts +10 -2
  4. package/api/src/libs/{vendor → vendor-util}/adapters/factory.ts +9 -5
  5. package/api/src/libs/{vendor → vendor-util}/adapters/launcher-adapter.ts +20 -18
  6. package/api/src/libs/{vendor → vendor-util}/adapters/types.ts +1 -4
  7. package/api/src/libs/{vendor → vendor-util}/fulfillment.ts +24 -127
  8. package/api/src/queues/payment.ts +1 -5
  9. package/api/src/queues/{vendor → vendors}/commission.ts +19 -18
  10. package/api/src/queues/{vendor → vendors}/fulfillment-coordinator.ts +35 -6
  11. package/api/src/queues/{vendor → vendors}/fulfillment.ts +2 -2
  12. package/api/src/queues/{vendor → vendors}/status-check.ts +13 -8
  13. package/api/src/routes/payment-links.ts +2 -1
  14. package/api/src/routes/products.ts +1 -0
  15. package/api/src/routes/vendor.ts +157 -216
  16. package/api/src/store/migrations/20250911-add-vendor-type.ts +26 -0
  17. package/api/src/store/migrations/20250916-add-vendor-did.ts +20 -0
  18. package/api/src/store/models/payout.ts +2 -2
  19. package/api/src/store/models/product-vendor.ts +11 -24
  20. package/api/src/store/models/product.ts +2 -0
  21. package/blocklet.yml +1 -1
  22. package/doc/vendor_fulfillment_system.md +1 -1
  23. package/package.json +5 -5
  24. package/src/components/metadata/form.tsx +12 -19
  25. package/src/components/payment-link/before-pay.tsx +40 -0
  26. package/src/components/product/vendor-config.tsx +4 -11
  27. package/src/components/subscription/description.tsx +1 -6
  28. package/src/components/subscription/portal/list.tsx +82 -6
  29. package/src/components/subscription/vendor-service-list.tsx +128 -0
  30. package/src/components/vendor/actions.tsx +1 -33
  31. package/src/locales/en.tsx +16 -3
  32. package/src/locales/zh.tsx +18 -5
  33. package/src/pages/admin/products/links/create.tsx +2 -0
  34. package/src/pages/admin/products/vendors/create.tsx +140 -190
  35. package/src/pages/admin/products/vendors/index.tsx +14 -22
  36. package/src/pages/customer/subscription/detail.tsx +26 -11
@@ -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 { VendorFulfillmentService } from '../libs/vendor/fulfillment';
7
- import logger from '../libs/logger';
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 { CheckoutSession } from '../store/models';
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 dayjs from '../libs/dayjs';
14
- import { MetadataSchema } from '../libs/api';
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: error.message,
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: error.message || error.toString() || 'Unknown 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: error.message || error.toString() || 'Unknown 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
- webhook_path: webhookPath,
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
- webhook_path: webhookPath,
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: error.message || error.toString() || 'Unknown 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
- webhook_path: webhookPath,
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
- webhook_path: webhookPath,
229
- default_commission_rate: defaultCommissionRate,
230
- default_commission_type: defaultCommissionType,
231
- order_create_params: orderCreateParams,
226
+ vendor_did: vendorDid,
232
227
  status,
233
- ...((metadata || blockletMetaUrl !== undefined) && {
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: error.message, id: req.params.id });
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: error.message, id: req.params.id });
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: error.message, id: req.params.id });
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 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
- 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(async (item) => {
445
- const vendor = await ProductVendor.findByPk(item.vendor_id);
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