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,97 @@
|
|
|
1
|
+
import { events } from '../../libs/event';
|
|
2
|
+
import logger from '../../libs/logger';
|
|
3
|
+
import createQueue from '../../libs/queue';
|
|
4
|
+
import { VendorFulfillmentService } from '../../libs/vendor-fulfillment';
|
|
5
|
+
import { CheckoutSession } from '../../store/models/checkout-session';
|
|
6
|
+
import { updateVendorFulfillmentStatus } from './vendor-fulfillment-coordinator';
|
|
7
|
+
|
|
8
|
+
type VendorFulfillmentJob = {
|
|
9
|
+
checkoutSessionId: string;
|
|
10
|
+
paymentIntentId: string;
|
|
11
|
+
vendorId: string;
|
|
12
|
+
vendorConfig: any;
|
|
13
|
+
retryOnError?: boolean;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const handleVendorFulfillment = async (job: VendorFulfillmentJob) => {
|
|
17
|
+
logger.debug('Raw job received in handleVendorFulfillment', {
|
|
18
|
+
job,
|
|
19
|
+
jobKeys: Object.keys(job),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const { checkoutSessionId, paymentIntentId, vendorId, vendorConfig } = job;
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const checkoutSession = await CheckoutSession.findByPk(checkoutSessionId);
|
|
26
|
+
if (!checkoutSession) {
|
|
27
|
+
throw new Error(`CheckoutSession not found: ${checkoutSessionId}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const orderInfo = {
|
|
31
|
+
amount_total: checkoutSession.amount_total,
|
|
32
|
+
customer_id: checkoutSession.customer_id || '',
|
|
33
|
+
payment_intent_id: checkoutSession.payment_intent_id || '',
|
|
34
|
+
currency_id: checkoutSession.currency_id,
|
|
35
|
+
customer_did: checkoutSession.customer_did || '',
|
|
36
|
+
};
|
|
37
|
+
const fulfillmentResult = await VendorFulfillmentService.fulfillSingleVendorOrder(orderInfo, vendorConfig);
|
|
38
|
+
|
|
39
|
+
logger.info('Vendor fulfillment has been sent', {
|
|
40
|
+
vendorId,
|
|
41
|
+
orderId: fulfillmentResult.orderId,
|
|
42
|
+
status: fulfillmentResult.status,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
await updateVendorFulfillmentStatus(checkoutSessionId, paymentIntentId, vendorId, 'sent', {
|
|
46
|
+
orderId: fulfillmentResult.orderId,
|
|
47
|
+
commissionAmount: fulfillmentResult.commissionAmount,
|
|
48
|
+
serviceUrl: fulfillmentResult.serviceUrl,
|
|
49
|
+
});
|
|
50
|
+
} catch (error: any) {
|
|
51
|
+
logger.error('Vendor fulfillment failed', {
|
|
52
|
+
vendorId,
|
|
53
|
+
checkoutSessionId,
|
|
54
|
+
error: error.message,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
await updateVendorFulfillmentStatus(checkoutSessionId, paymentIntentId, vendorId, 'failed', {
|
|
58
|
+
lastError: error.message,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
throw error;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const vendorFulfillmentQueue = createQueue<VendorFulfillmentJob>({
|
|
66
|
+
name: 'vendor-fulfillment',
|
|
67
|
+
onJob: handleVendorFulfillment,
|
|
68
|
+
options: {
|
|
69
|
+
concurrency: 1,
|
|
70
|
+
maxRetries: 3,
|
|
71
|
+
enableScheduledJob: true,
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
export const startVendorFulfillmentQueue = () => {
|
|
76
|
+
logger.debug('startVendorFulfillmentQueue');
|
|
77
|
+
return Promise.resolve();
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
events.on('vendor.fulfillment.queued', async (id, job) => {
|
|
81
|
+
try {
|
|
82
|
+
const exist = await vendorFulfillmentQueue.get(id);
|
|
83
|
+
if (!exist) {
|
|
84
|
+
vendorFulfillmentQueue.push({
|
|
85
|
+
id,
|
|
86
|
+
job,
|
|
87
|
+
});
|
|
88
|
+
} else {
|
|
89
|
+
logger.info('Vendor fulfillment job already exists, skipping', { id });
|
|
90
|
+
}
|
|
91
|
+
} catch (error: any) {
|
|
92
|
+
logger.error('Failed to handle vendor fulfillment queue event', {
|
|
93
|
+
id,
|
|
94
|
+
error: error.message,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
});
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { joinURL } from 'ufo';
|
|
2
|
+
import createQueue from '../../libs/queue';
|
|
3
|
+
import { getBlockletJson } from '../../libs/util';
|
|
4
|
+
import { VendorAdapterFactory } from '../../libs/vendor-adapter-factory';
|
|
5
|
+
import { CheckoutSession } from '../../store/models/checkout-session';
|
|
6
|
+
import { ProductVendor } from '../../store/models';
|
|
7
|
+
import { fulfillmentCoordinatorQueue } from './vendor-fulfillment-coordinator';
|
|
8
|
+
import logger from '../../libs/logger';
|
|
9
|
+
import { vendorTimeoutMinutes } from '../../libs/env';
|
|
10
|
+
|
|
11
|
+
export const startVendorStatusCheckSchedule = async () => {
|
|
12
|
+
const checkoutSessions = await CheckoutSession.findAll({
|
|
13
|
+
where: { fulfillment_status: 'sent' },
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
if (!checkoutSessions.length) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
for (const checkoutSession of checkoutSessions) {
|
|
21
|
+
const vendorInfo = checkoutSession.vendor_info;
|
|
22
|
+
if (!vendorInfo?.length) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
for (const vendor of vendorInfo) {
|
|
27
|
+
if (vendor.status === 'sent') {
|
|
28
|
+
vendorStatusCheckQueue.push({
|
|
29
|
+
id: `vendor-status-check-${checkoutSession.id}-${vendor.vendor_id}`,
|
|
30
|
+
job: {
|
|
31
|
+
checkoutSessionId: checkoutSession.id,
|
|
32
|
+
vendorId: vendor.vendor_id,
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const handleVendorStatusCheck = async (job: VendorStatusCheckJob) => {
|
|
41
|
+
const { checkoutSessionId, vendorId } = job;
|
|
42
|
+
logger.info('handleVendorStatusCheck', { checkoutSessionId, vendorId });
|
|
43
|
+
|
|
44
|
+
const checkoutSession = await CheckoutSession.findByPk(checkoutSessionId, {
|
|
45
|
+
attributes: ['vendor_info', 'payment_intent_id'],
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const vendor = checkoutSession?.vendor_info?.find((v) => v.vendor_id === vendorId);
|
|
49
|
+
if (!vendor) {
|
|
50
|
+
logger.error('vendor not found[handleVendorStatusCheck]', { checkoutSessionId, vendorId });
|
|
51
|
+
return { status: 'fail' };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const TIMEOUT_THRESHOLD = vendorTimeoutMinutes * 60 * 1000;
|
|
55
|
+
const now = Date.now();
|
|
56
|
+
|
|
57
|
+
if (vendor.lastAttemptAt) {
|
|
58
|
+
const timeSinceLastAttempt = now - new Date(vendor.lastAttemptAt).getTime();
|
|
59
|
+
|
|
60
|
+
if (timeSinceLastAttempt > TIMEOUT_THRESHOLD) {
|
|
61
|
+
logger.warn('Vendor timeout detected during status check', {
|
|
62
|
+
checkoutSessionId,
|
|
63
|
+
vendorId,
|
|
64
|
+
status: vendor.status,
|
|
65
|
+
timeSinceLastAttempt: `${Math.floor(timeSinceLastAttempt / 1000)}s`,
|
|
66
|
+
timeoutThreshold: `${vendorTimeoutMinutes}min (${Math.floor(TIMEOUT_THRESHOLD / 1000)}s)`,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
vendor.status = 'failed';
|
|
70
|
+
vendor.error_message = `Timeout: ${vendor.status} status exceeded ${vendorTimeoutMinutes} minutes`;
|
|
71
|
+
vendor.lastAttemptAt = new Date().toISOString();
|
|
72
|
+
|
|
73
|
+
const updatedVendorInfo = checkoutSession?.vendor_info?.map((v) => {
|
|
74
|
+
if (v.vendor_id === vendorId) {
|
|
75
|
+
return { ...v, ...vendor };
|
|
76
|
+
}
|
|
77
|
+
return v;
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
await CheckoutSession.update({ vendor_info: updatedVendorInfo }, { where: { id: checkoutSessionId } });
|
|
81
|
+
|
|
82
|
+
fulfillmentCoordinatorQueue.push({
|
|
83
|
+
id: `fulfillment-coordinator-${checkoutSessionId}-${vendorId}`,
|
|
84
|
+
job: {
|
|
85
|
+
checkoutSessionId,
|
|
86
|
+
paymentIntentId: checkoutSession?.payment_intent_id || '',
|
|
87
|
+
triggeredBy: 'vendor-status-check-timeout',
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return { status: 'failed', reason: 'timeout' };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
if (!vendor.app_url) {
|
|
97
|
+
const productVendor = await ProductVendor.findByPk(vendorId);
|
|
98
|
+
const url = productVendor?.app_url;
|
|
99
|
+
logger.info('found vendor url', { url, productVendor });
|
|
100
|
+
|
|
101
|
+
if (url) {
|
|
102
|
+
const vendorMeta = await getBlockletJson(url);
|
|
103
|
+
const mountPoint = vendorMeta.componentMountPoints?.find((x: any) => x.appId === vendorId)?.mountPoint;
|
|
104
|
+
const serverStatusUrl = joinURL(url, mountPoint, '/api/vendor/status', vendor.order_id);
|
|
105
|
+
|
|
106
|
+
const result = await fetch(serverStatusUrl);
|
|
107
|
+
const data = await result.json();
|
|
108
|
+
vendor.app_url = data?.appUrl || '';
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
logger.info('checkOrderStatus', { vendor });
|
|
113
|
+
const adapter = await VendorAdapterFactory.create(vendor.vendor_key);
|
|
114
|
+
const result = await adapter.checkOrderStatus({ appUrl: vendor.app_url });
|
|
115
|
+
|
|
116
|
+
if (result.status === 'completed') {
|
|
117
|
+
vendor.status = 'completed';
|
|
118
|
+
vendor.lastAttemptAt = new Date().toISOString();
|
|
119
|
+
} else if (result.status === 'failed') {
|
|
120
|
+
vendor.status = 'failed';
|
|
121
|
+
vendor.lastAttemptAt = new Date().toISOString();
|
|
122
|
+
} else if (result.status === 'processing') {
|
|
123
|
+
vendor.status = 'processing';
|
|
124
|
+
vendor.lastAttemptAt = new Date().toISOString();
|
|
125
|
+
}
|
|
126
|
+
} catch (error) {
|
|
127
|
+
logger.warn('Failed to check vendor status', {
|
|
128
|
+
checkoutSessionId,
|
|
129
|
+
vendorId,
|
|
130
|
+
error: error.message,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const vendorInfo = checkoutSession?.vendor_info?.map((v) => {
|
|
135
|
+
if (v.vendor_id === vendorId) {
|
|
136
|
+
v.app_url = vendor.app_url || '';
|
|
137
|
+
vendor.app_url = vendor.app_url || '';
|
|
138
|
+
vendor.status = vendor.status || 'sent';
|
|
139
|
+
}
|
|
140
|
+
return v;
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
logger.info('update vendor info', { vendorInfo });
|
|
144
|
+
|
|
145
|
+
await CheckoutSession.update(
|
|
146
|
+
{
|
|
147
|
+
vendor_info: vendorInfo,
|
|
148
|
+
},
|
|
149
|
+
{ where: { id: checkoutSessionId } }
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
if (vendor.status === 'completed' || vendor.status === 'failed') {
|
|
153
|
+
fulfillmentCoordinatorQueue.push({
|
|
154
|
+
id: `fulfillment-coordinator-${checkoutSessionId}-${vendorId}`,
|
|
155
|
+
job: {
|
|
156
|
+
checkoutSessionId,
|
|
157
|
+
paymentIntentId: checkoutSession?.payment_intent_id || '',
|
|
158
|
+
triggeredBy: 'vendor-status-check',
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return { status: vendor.status };
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
type VendorStatusCheckJob = {
|
|
167
|
+
checkoutSessionId: string;
|
|
168
|
+
vendorId: string;
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
export const vendorStatusCheckQueue = createQueue<VendorStatusCheckJob>({
|
|
172
|
+
name: 'vendor-status-check',
|
|
173
|
+
onJob: handleVendorStatusCheck,
|
|
174
|
+
options: {
|
|
175
|
+
concurrency: 1,
|
|
176
|
+
maxRetries: 3,
|
|
177
|
+
enableScheduledJob: true,
|
|
178
|
+
},
|
|
179
|
+
});
|
|
@@ -824,6 +824,7 @@ router.post('/', authLogin, async (req, res) => {
|
|
|
824
824
|
});
|
|
825
825
|
|
|
826
826
|
export async function startCheckoutSessionFromPaymentLink(id: string, req: Request, res: Response) {
|
|
827
|
+
const { metadata } = req.body;
|
|
827
828
|
try {
|
|
828
829
|
const link = await PaymentLink.findByPk(id);
|
|
829
830
|
if (!link) {
|
|
@@ -942,6 +943,7 @@ export async function startCheckoutSessionFromPaymentLink(id: string, req: Reque
|
|
|
942
943
|
raw.metadata = {
|
|
943
944
|
...link.metadata,
|
|
944
945
|
...getDataObjectFromQuery(req.query),
|
|
946
|
+
...metadata,
|
|
945
947
|
days_until_due: getDaysUntilDue(req.query),
|
|
946
948
|
days_until_cancel: getDaysUntilCancel(req.query),
|
|
947
949
|
passport: await checkPassportForPaymentLink(link),
|
|
@@ -952,6 +954,7 @@ export async function startCheckoutSessionFromPaymentLink(id: string, req: Reque
|
|
|
952
954
|
raw.metadata = {
|
|
953
955
|
...link.metadata,
|
|
954
956
|
...getDataObjectFromQuery(req.query),
|
|
957
|
+
...metadata,
|
|
955
958
|
days_until_due: getDaysUntilDue(req.query),
|
|
956
959
|
days_until_cancel: getDaysUntilCancel(req.query),
|
|
957
960
|
passport: await checkPassportForPaymentLink(link),
|
package/api/src/routes/index.ts
CHANGED
|
@@ -30,6 +30,7 @@ import subscriptions from './subscriptions';
|
|
|
30
30
|
import usageRecords from './usage-records';
|
|
31
31
|
import webhookAttempts from './webhook-attempts';
|
|
32
32
|
import webhookEndpoints from './webhook-endpoints';
|
|
33
|
+
import vendor from './vendor';
|
|
33
34
|
|
|
34
35
|
const router = Router();
|
|
35
36
|
|
|
@@ -81,5 +82,6 @@ router.use('/subscriptions', subscriptions);
|
|
|
81
82
|
router.use('/usage-records', usageRecords);
|
|
82
83
|
router.use('/webhook-attempts', webhookAttempts);
|
|
83
84
|
router.use('/webhook-endpoints', webhookEndpoints);
|
|
85
|
+
router.use('/vendors', vendor);
|
|
84
86
|
|
|
85
87
|
export default router;
|
|
@@ -12,6 +12,8 @@ import { formatMetadata } from '../libs/util';
|
|
|
12
12
|
import { PaymentCurrency } from '../store/models/payment-currency';
|
|
13
13
|
import { Price } from '../store/models/price';
|
|
14
14
|
import { Product } from '../store/models/product';
|
|
15
|
+
import { ProductVendor } from '../store/models/product-vendor';
|
|
16
|
+
|
|
15
17
|
import type { CustomUnitAmount } from '../store/models/types';
|
|
16
18
|
import { checkCurrencySupportRecurring } from '../libs/product';
|
|
17
19
|
|
|
@@ -19,6 +21,16 @@ const router = Router();
|
|
|
19
21
|
|
|
20
22
|
const auth = authenticate<Product>({ component: true, roles: ['owner', 'admin'] });
|
|
21
23
|
|
|
24
|
+
const VendorConfigSchema = Joi.array()
|
|
25
|
+
.items(
|
|
26
|
+
Joi.object({
|
|
27
|
+
vendor_id: Joi.string().required(),
|
|
28
|
+
commission_rate: Joi.number().min(0).max(100).required(),
|
|
29
|
+
commission_type: Joi.string().valid('percentage', 'fixed').required(),
|
|
30
|
+
}).unknown(true)
|
|
31
|
+
)
|
|
32
|
+
.optional();
|
|
33
|
+
|
|
22
34
|
const ProductAndPriceSchema = Joi.object({
|
|
23
35
|
name: Joi.string().max(64).empty('').optional(),
|
|
24
36
|
type: Joi.string().valid('service', 'good', 'credit').empty('').optional(),
|
|
@@ -55,6 +67,7 @@ const ProductAndPriceSchema = Joi.object({
|
|
|
55
67
|
}).unknown(true)
|
|
56
68
|
)
|
|
57
69
|
.optional(),
|
|
70
|
+
vendor_config: VendorConfigSchema,
|
|
58
71
|
}).unknown(true);
|
|
59
72
|
|
|
60
73
|
const CreditConfigSchema = Joi.object({
|
|
@@ -67,7 +80,6 @@ const CreditConfigSchema = Joi.object({
|
|
|
67
80
|
});
|
|
68
81
|
|
|
69
82
|
export async function createProductAndPrices(payload: any) {
|
|
70
|
-
// 1. 准备 product 数据
|
|
71
83
|
const raw: Partial<Product> = pick(payload, [
|
|
72
84
|
'name',
|
|
73
85
|
'type',
|
|
@@ -80,6 +92,36 @@ export async function createProductAndPrices(payload: any) {
|
|
|
80
92
|
'features',
|
|
81
93
|
'metadata',
|
|
82
94
|
]);
|
|
95
|
+
|
|
96
|
+
if (Array.isArray(payload.vendor_config) && payload.vendor_config.length > 0) {
|
|
97
|
+
// validate vendor_config data
|
|
98
|
+
const { error: vendorConfigError } = VendorConfigSchema.validate(payload.vendor_config);
|
|
99
|
+
if (vendorConfigError) {
|
|
100
|
+
throw new Error(`vendor_config validation failed: ${vendorConfigError.message}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const vendorConfigs = await ProductVendor.findAll({
|
|
104
|
+
where: {
|
|
105
|
+
id: payload.vendor_config.map((x: any) => x.vendor_id),
|
|
106
|
+
},
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
raw.vendor_config = payload.vendor_config.map((config: any) => {
|
|
110
|
+
const vendorConfig = vendorConfigs.find((x) => x.id === config.vendor_id);
|
|
111
|
+
if (!vendorConfig) {
|
|
112
|
+
throw new Error(`vendor ${config.vendor_id} not found`);
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
vendor_id: vendorConfig.id,
|
|
116
|
+
vendor_key: vendorConfig.vendor_key,
|
|
117
|
+
name: vendorConfig.name,
|
|
118
|
+
description: vendorConfig.description,
|
|
119
|
+
commission_rate: Number(config.commission_rate),
|
|
120
|
+
commission_type: config.commission_type === 'fixed' ? 'fixed_amount' : config.commission_type,
|
|
121
|
+
custom_params: {},
|
|
122
|
+
};
|
|
123
|
+
});
|
|
124
|
+
}
|
|
83
125
|
raw.active = true;
|
|
84
126
|
raw.type = raw.type || 'service';
|
|
85
127
|
raw.livemode = !!payload.livemode;
|
|
@@ -389,6 +431,35 @@ router.put('/:id', auth, async (req, res) => {
|
|
|
389
431
|
'metadata',
|
|
390
432
|
'cross_sell',
|
|
391
433
|
]);
|
|
434
|
+
|
|
435
|
+
if (Array.isArray(req.body.vendor_config)) {
|
|
436
|
+
const { error: vendorConfigError } = VendorConfigSchema.validate(req.body.vendor_config);
|
|
437
|
+
if (vendorConfigError) {
|
|
438
|
+
return res.status(400).json({ error: `vendor_config validation failed: ${vendorConfigError.message}` });
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
const vendorConfigs = await ProductVendor.findAll({
|
|
442
|
+
where: {
|
|
443
|
+
id: req.body.vendor_config.map((x: any) => x.vendor_id),
|
|
444
|
+
},
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
updates.vendor_config = req.body.vendor_config.map((config: any) => {
|
|
448
|
+
const vendorConfig = vendorConfigs.find((x) => x.id === config.vendor_id);
|
|
449
|
+
if (!vendorConfig) {
|
|
450
|
+
throw new Error(`vendor ${config.vendor_id} not found`);
|
|
451
|
+
}
|
|
452
|
+
return {
|
|
453
|
+
vendor_id: vendorConfig.id,
|
|
454
|
+
vendor_key: vendorConfig.vendor_key,
|
|
455
|
+
name: vendorConfig.name,
|
|
456
|
+
description: vendorConfig.description,
|
|
457
|
+
commission_rate: Number(config.commission_rate),
|
|
458
|
+
commission_type: config.commission_type === 'fixed' ? 'fixed_amount' : config.commission_type,
|
|
459
|
+
custom_params: {},
|
|
460
|
+
};
|
|
461
|
+
});
|
|
462
|
+
}
|
|
392
463
|
const { error } = ProductAndPriceSchema.validate(updates);
|
|
393
464
|
if (error) {
|
|
394
465
|
return res.status(400).json({ error: `Product update request invalid: ${error.message}` });
|