payment-kit 1.20.5 → 1.20.7
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/env.ts +7 -0
- package/api/src/libs/url.ts +77 -0
- package/api/src/libs/vendor/adapters/factory.ts +40 -0
- package/api/src/libs/vendor/adapters/launcher-adapter.ts +179 -0
- package/api/src/libs/vendor/adapters/types.ts +91 -0
- package/api/src/libs/vendor/fulfillment.ts +317 -0
- package/api/src/queues/payment.ts +14 -10
- package/api/src/queues/payout.ts +1 -0
- package/api/src/queues/vendor/commission.ts +192 -0
- package/api/src/queues/vendor/fulfillment-coordinator.ts +625 -0
- package/api/src/queues/vendor/fulfillment.ts +98 -0
- package/api/src/queues/vendor/status-check.ts +178 -0
- package/api/src/routes/checkout-sessions.ts +12 -0
- package/api/src/routes/index.ts +2 -0
- package/api/src/routes/products.ts +72 -1
- package/api/src/routes/vendor.ts +527 -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 +929 -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,98 @@
|
|
|
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 './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
|
+
checkoutSessionId,
|
|
32
|
+
amount_total: checkoutSession.amount_total,
|
|
33
|
+
customer_id: checkoutSession.customer_id || '',
|
|
34
|
+
payment_intent_id: checkoutSession.payment_intent_id || '',
|
|
35
|
+
currency_id: checkoutSession.currency_id,
|
|
36
|
+
customer_did: checkoutSession.customer_did || '',
|
|
37
|
+
};
|
|
38
|
+
const fulfillmentResult = await VendorFulfillmentService.fulfillSingleVendorOrder(orderInfo, vendorConfig);
|
|
39
|
+
|
|
40
|
+
logger.info('Vendor fulfillment has been sent', {
|
|
41
|
+
vendorId,
|
|
42
|
+
orderId: fulfillmentResult.orderId,
|
|
43
|
+
status: fulfillmentResult.status,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
await updateVendorFulfillmentStatus(checkoutSessionId, paymentIntentId, vendorId, 'sent', {
|
|
47
|
+
orderId: fulfillmentResult.orderId,
|
|
48
|
+
commissionAmount: fulfillmentResult.commissionAmount,
|
|
49
|
+
serviceUrl: fulfillmentResult.serviceUrl,
|
|
50
|
+
});
|
|
51
|
+
} catch (error: any) {
|
|
52
|
+
logger.error('Vendor fulfillment failed', {
|
|
53
|
+
vendorId,
|
|
54
|
+
checkoutSessionId,
|
|
55
|
+
error: error.message,
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
await updateVendorFulfillmentStatus(checkoutSessionId, paymentIntentId, vendorId, 'failed', {
|
|
59
|
+
lastError: error.message,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
throw error;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export const vendorFulfillmentQueue = createQueue<VendorFulfillmentJob>({
|
|
67
|
+
name: 'vendor-fulfillment',
|
|
68
|
+
onJob: handleVendorFulfillment,
|
|
69
|
+
options: {
|
|
70
|
+
concurrency: 1,
|
|
71
|
+
maxRetries: 3,
|
|
72
|
+
enableScheduledJob: true,
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
export const startVendorFulfillmentQueue = () => {
|
|
77
|
+
logger.debug('startVendorFulfillmentQueue');
|
|
78
|
+
return Promise.resolve();
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
events.on('vendor.fulfillment.queued', async (id, job) => {
|
|
82
|
+
try {
|
|
83
|
+
const exist = await vendorFulfillmentQueue.get(id);
|
|
84
|
+
if (!exist) {
|
|
85
|
+
vendorFulfillmentQueue.push({
|
|
86
|
+
id,
|
|
87
|
+
job,
|
|
88
|
+
});
|
|
89
|
+
} else {
|
|
90
|
+
logger.info('Vendor fulfillment job already exists, skipping', { id });
|
|
91
|
+
}
|
|
92
|
+
} catch (error: any) {
|
|
93
|
+
logger.error('Failed to handle vendor fulfillment queue event', {
|
|
94
|
+
id,
|
|
95
|
+
error: error.message,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
});
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { joinURL } from 'ufo';
|
|
2
|
+
import createQueue from '../../libs/queue';
|
|
3
|
+
import { getBlockletJson } from '../../libs/util';
|
|
4
|
+
import { CheckoutSession } from '../../store/models/checkout-session';
|
|
5
|
+
import { ProductVendor } from '../../store/models';
|
|
6
|
+
import { fulfillmentCoordinatorQueue } from './fulfillment-coordinator';
|
|
7
|
+
import logger from '../../libs/logger';
|
|
8
|
+
import { vendorTimeoutMinutes } from '../../libs/env';
|
|
9
|
+
import { VendorFulfillmentService } from '../../libs/vendor/fulfillment';
|
|
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
|
+
const adapter = VendorFulfillmentService.getVendorAdapter(vendor.vendor_key);
|
|
113
|
+
const result = await adapter.checkOrderStatus({ appUrl: vendor.app_url });
|
|
114
|
+
|
|
115
|
+
if (result.status === 'completed') {
|
|
116
|
+
vendor.status = 'completed';
|
|
117
|
+
vendor.lastAttemptAt = new Date().toISOString();
|
|
118
|
+
} else if (result.status === 'failed') {
|
|
119
|
+
vendor.status = 'failed';
|
|
120
|
+
vendor.lastAttemptAt = new Date().toISOString();
|
|
121
|
+
} else if (result.status === 'processing') {
|
|
122
|
+
vendor.status = 'processing';
|
|
123
|
+
vendor.lastAttemptAt = new Date().toISOString();
|
|
124
|
+
}
|
|
125
|
+
} catch (error) {
|
|
126
|
+
logger.warn('Failed to check vendor status', {
|
|
127
|
+
checkoutSessionId,
|
|
128
|
+
vendorId,
|
|
129
|
+
error: error.message,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const vendorInfo = checkoutSession?.vendor_info?.map((v) => {
|
|
134
|
+
if (v.vendor_id === vendorId) {
|
|
135
|
+
v.app_url = vendor.app_url || '';
|
|
136
|
+
vendor.app_url = vendor.app_url || '';
|
|
137
|
+
vendor.status = vendor.status || 'sent';
|
|
138
|
+
}
|
|
139
|
+
return v;
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
logger.info('update vendor info', { vendorInfo });
|
|
143
|
+
|
|
144
|
+
await CheckoutSession.update(
|
|
145
|
+
{
|
|
146
|
+
vendor_info: vendorInfo,
|
|
147
|
+
},
|
|
148
|
+
{ where: { id: checkoutSessionId } }
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
if (vendor.status === 'completed' || vendor.status === 'failed') {
|
|
152
|
+
fulfillmentCoordinatorQueue.push({
|
|
153
|
+
id: `fulfillment-coordinator-${checkoutSessionId}-${vendorId}`,
|
|
154
|
+
job: {
|
|
155
|
+
checkoutSessionId,
|
|
156
|
+
paymentIntentId: checkoutSession?.payment_intent_id || '',
|
|
157
|
+
triggeredBy: 'vendor-status-check',
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return { status: vendor.status };
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
type VendorStatusCheckJob = {
|
|
166
|
+
checkoutSessionId: string;
|
|
167
|
+
vendorId: string;
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
export const vendorStatusCheckQueue = createQueue<VendorStatusCheckJob>({
|
|
171
|
+
name: 'vendor-status-check',
|
|
172
|
+
onJob: handleVendorStatusCheck,
|
|
173
|
+
options: {
|
|
174
|
+
concurrency: 1,
|
|
175
|
+
maxRetries: 3,
|
|
176
|
+
enableScheduledJob: true,
|
|
177
|
+
},
|
|
178
|
+
});
|
|
@@ -94,6 +94,7 @@ import { CHARGE_SUPPORTED_CHAIN_TYPES } from '../libs/constants';
|
|
|
94
94
|
import { blocklet } from '../libs/auth';
|
|
95
95
|
import { addSubscriptionJob } from '../queues/subscription';
|
|
96
96
|
import { updateDataConcurrency } from '../libs/env';
|
|
97
|
+
import { formatToShortUrl } from '../libs/url';
|
|
97
98
|
|
|
98
99
|
const router = Router();
|
|
99
100
|
|
|
@@ -824,6 +825,7 @@ router.post('/', authLogin, async (req, res) => {
|
|
|
824
825
|
});
|
|
825
826
|
|
|
826
827
|
export async function startCheckoutSessionFromPaymentLink(id: string, req: Request, res: Response) {
|
|
828
|
+
const { metadata, needShortUrl = false } = req.body;
|
|
827
829
|
try {
|
|
828
830
|
const link = await PaymentLink.findByPk(id);
|
|
829
831
|
if (!link) {
|
|
@@ -942,6 +944,7 @@ export async function startCheckoutSessionFromPaymentLink(id: string, req: Reque
|
|
|
942
944
|
raw.metadata = {
|
|
943
945
|
...link.metadata,
|
|
944
946
|
...getDataObjectFromQuery(req.query),
|
|
947
|
+
...metadata,
|
|
945
948
|
days_until_due: getDaysUntilDue(req.query),
|
|
946
949
|
days_until_cancel: getDaysUntilCancel(req.query),
|
|
947
950
|
passport: await checkPassportForPaymentLink(link),
|
|
@@ -952,6 +955,7 @@ export async function startCheckoutSessionFromPaymentLink(id: string, req: Reque
|
|
|
952
955
|
raw.metadata = {
|
|
953
956
|
...link.metadata,
|
|
954
957
|
...getDataObjectFromQuery(req.query),
|
|
958
|
+
...metadata,
|
|
955
959
|
days_until_due: getDaysUntilDue(req.query),
|
|
956
960
|
days_until_cancel: getDaysUntilCancel(req.query),
|
|
957
961
|
passport: await checkPassportForPaymentLink(link),
|
|
@@ -977,7 +981,15 @@ export async function startCheckoutSessionFromPaymentLink(id: string, req: Reque
|
|
|
977
981
|
doc.line_items = await Price.expand(updatedItems, { upsell: true });
|
|
978
982
|
}
|
|
979
983
|
|
|
984
|
+
let paymentUrl = getUrl(`/checkout/pay/${doc.id}`);
|
|
985
|
+
if (needShortUrl) {
|
|
986
|
+
const validUntil = dayjs().add(20, 'minutes').format('YYYY-MM-DDTHH:mm:ss+00:00');
|
|
987
|
+
const maxVisits = 5;
|
|
988
|
+
paymentUrl = await formatToShortUrl({ url: paymentUrl, validUntil, maxVisits });
|
|
989
|
+
}
|
|
990
|
+
|
|
980
991
|
res.json({
|
|
992
|
+
paymentUrl,
|
|
981
993
|
checkoutSession: doc.toJSON(),
|
|
982
994
|
paymentMethods: await getPaymentMethods(doc),
|
|
983
995
|
paymentLink: 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}` });
|