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.
Files changed (39) hide show
  1. package/api/src/crons/index.ts +11 -3
  2. package/api/src/index.ts +18 -14
  3. package/api/src/libs/adapters/launcher-adapter.ts +177 -0
  4. package/api/src/libs/env.ts +7 -0
  5. package/api/src/libs/url.ts +77 -0
  6. package/api/src/libs/vendor-adapter-factory.ts +22 -0
  7. package/api/src/libs/vendor-adapter.ts +109 -0
  8. package/api/src/libs/vendor-fulfillment.ts +321 -0
  9. package/api/src/queues/payment.ts +14 -10
  10. package/api/src/queues/payout.ts +1 -0
  11. package/api/src/queues/vendor/vendor-commission.ts +192 -0
  12. package/api/src/queues/vendor/vendor-fulfillment-coordinator.ts +627 -0
  13. package/api/src/queues/vendor/vendor-fulfillment.ts +97 -0
  14. package/api/src/queues/vendor/vendor-status-check.ts +179 -0
  15. package/api/src/routes/checkout-sessions.ts +3 -0
  16. package/api/src/routes/index.ts +2 -0
  17. package/api/src/routes/products.ts +72 -1
  18. package/api/src/routes/vendor.ts +526 -0
  19. package/api/src/store/migrations/20250820-add-product-vendor.ts +102 -0
  20. package/api/src/store/migrations/20250822-add-vendor-config-to-products.ts +56 -0
  21. package/api/src/store/models/checkout-session.ts +84 -18
  22. package/api/src/store/models/index.ts +3 -0
  23. package/api/src/store/models/payout.ts +11 -0
  24. package/api/src/store/models/product-vendor.ts +118 -0
  25. package/api/src/store/models/product.ts +15 -0
  26. package/blocklet.yml +8 -2
  27. package/doc/vendor_fulfillment_system.md +931 -0
  28. package/package.json +5 -4
  29. package/src/components/collapse.tsx +1 -0
  30. package/src/components/product/edit.tsx +9 -0
  31. package/src/components/product/form.tsx +11 -0
  32. package/src/components/product/vendor-config.tsx +249 -0
  33. package/src/components/vendor/actions.tsx +145 -0
  34. package/src/locales/en.tsx +89 -0
  35. package/src/locales/zh.tsx +89 -0
  36. package/src/pages/admin/products/index.tsx +11 -1
  37. package/src/pages/admin/products/products/detail.tsx +79 -2
  38. package/src/pages/admin/products/vendors/create.tsx +418 -0
  39. package/src/pages/admin/products/vendors/index.tsx +313 -0
@@ -0,0 +1,627 @@
1
+ import { events } from '../../libs/event';
2
+ import { getLock } from '../../libs/lock';
3
+ import logger from '../../libs/logger';
4
+ import createQueue from '../../libs/queue';
5
+ import { VendorFulfillmentService } from '../../libs/vendor-fulfillment';
6
+ import { CheckoutSession } from '../../store/models/checkout-session';
7
+ import { PaymentIntent } from '../../store/models/payment-intent';
8
+ import { Price } from '../../store/models/price';
9
+ import { Product } from '../../store/models/product';
10
+ import { Refund } from '../../store/models/refund';
11
+ import { sequelize } from '../../store/sequelize';
12
+ import { depositVaultQueue } from '../payment';
13
+
14
+ type VendorInfo = {
15
+ vendor_id: string;
16
+ vendor_key: string;
17
+ order_id: string;
18
+ status:
19
+ | 'pending'
20
+ | 'processing'
21
+ | 'completed'
22
+ | 'failed'
23
+ | 'cancelled'
24
+ | 'max_retries_exceeded'
25
+ | 'return_requested'
26
+ | 'sent';
27
+ service_url?: string;
28
+ error_message?: string;
29
+ amount: string;
30
+
31
+ attempts?: number;
32
+ lastAttemptAt?: string;
33
+ completedAt?: string;
34
+ commissionAmount?: string;
35
+
36
+ returnRequest?: {
37
+ reason: string;
38
+ requestedAt: string;
39
+ status: 'pending' | 'accepted' | 'rejected';
40
+ returnDetails?: string;
41
+ };
42
+ };
43
+
44
+ interface CoordinatorJob {
45
+ checkoutSessionId: string;
46
+ paymentIntentId: string;
47
+ triggeredBy: string;
48
+ }
49
+
50
+ const MAX_FULFILLMENT_TIMEOUT = 300000;
51
+
52
+ export const fulfillmentCoordinatorQueue = createQueue({
53
+ name: 'fulfillment-coordinator',
54
+ onJob: handleFulfillmentCoordination,
55
+ });
56
+
57
+ export async function startVendorFulfillment(checkoutSessionId: string, paymentIntentId: string): Promise<void> {
58
+ try {
59
+ logger.info('Starting vendor fulfillment process', {
60
+ checkoutSessionId,
61
+ paymentIntentId,
62
+ });
63
+
64
+ const vendorConfigs = await getVendorConfigurations(checkoutSessionId);
65
+
66
+ // No vendor configuration → directly to cold wallets
67
+ if (vendorConfigs.length === 0) {
68
+ logger.info('No vendor configurations found, triggering commission directly', {
69
+ checkoutSessionId,
70
+ });
71
+
72
+ await triggerCommissionProcess(checkoutSessionId, paymentIntentId);
73
+ return;
74
+ }
75
+
76
+ const initialVendorInfo: VendorInfo[] = vendorConfigs.map((config) => ({
77
+ vendor_id: config.vendor_id,
78
+ vendor_key: config.vendor_key,
79
+ order_id: '',
80
+ status: 'pending' as 'pending',
81
+ amount: config.amount || '0',
82
+ attempts: 0,
83
+ lastAttemptAt: new Date().toISOString(),
84
+ }));
85
+
86
+ await updateVendorInfo(checkoutSessionId, initialVendorInfo);
87
+
88
+ // Trigger a separate vendor-fulfillment queue for each vendor
89
+ for (const vendorConfig of vendorConfigs) {
90
+ const vendorFulfillmentJobId = `vendor-fulfillment-${checkoutSessionId}-${vendorConfig.vendor_id}`;
91
+
92
+ events.emit('vendor.fulfillment.queued', vendorFulfillmentJobId, {
93
+ checkoutSessionId,
94
+ paymentIntentId,
95
+ vendorId: vendorConfig.vendor_id,
96
+ vendorConfig,
97
+ retryOnError: true,
98
+ });
99
+ }
100
+
101
+ logger.info('Vendor fulfillment process has been triggered', {
102
+ checkoutSessionId,
103
+ vendorCount: vendorConfigs.length,
104
+ });
105
+ } catch (error: any) {
106
+ logger.error('Failed to start vendor fulfillment', {
107
+ checkoutSessionId,
108
+ paymentIntentId,
109
+ error: error.message,
110
+ });
111
+ throw error;
112
+ }
113
+ }
114
+
115
+ export async function updateVendorFulfillmentStatus(
116
+ checkoutSessionId: string,
117
+ paymentIntentId: string,
118
+ vendorId: string,
119
+ result: 'completed' | 'failed' | 'max_retries_exceeded' | 'return_requested' | 'sent',
120
+ details?: {
121
+ orderId?: string;
122
+ attempts?: number;
123
+ lastError?: string;
124
+ commissionAmount?: string;
125
+ serviceUrl?: string;
126
+ }
127
+ ): Promise<void> {
128
+ await updateSingleVendorInfo(checkoutSessionId, vendorId, {
129
+ status: result,
130
+ order_id: details?.orderId || '',
131
+ attempts: details?.attempts || 0,
132
+ error_message: details?.lastError,
133
+ commissionAmount: details?.commissionAmount,
134
+ service_url: details?.serviceUrl,
135
+ completedAt: result === 'completed' ? new Date().toISOString() : undefined,
136
+ lastAttemptAt: new Date().toISOString(),
137
+ });
138
+
139
+ await triggerCoordinatorCheck(checkoutSessionId, paymentIntentId, `vendor_${vendorId}_${result}`);
140
+ }
141
+
142
+ export async function handleFulfillmentCoordination(job: CoordinatorJob) {
143
+ const { checkoutSessionId, paymentIntentId, triggeredBy } = job;
144
+
145
+ logger.info('Processing fulfillment coordination', {
146
+ checkoutSessionId,
147
+ triggeredBy,
148
+ });
149
+
150
+ try {
151
+ const vendorInfo = await getVendorInfo(checkoutSessionId);
152
+ const vendorConfigs = await getVendorConfigurations(checkoutSessionId);
153
+
154
+ if (vendorConfigs.length === 0) {
155
+ logger.info('No vendors to coordinate, triggering commission directly', {
156
+ checkoutSessionId,
157
+ });
158
+ await triggerCommissionProcess(checkoutSessionId, paymentIntentId);
159
+ return;
160
+ }
161
+
162
+ const analysis = analyzeVendorStates(vendorInfo, vendorConfigs);
163
+
164
+ logger.info('Vendor fulfillment analysis', {
165
+ checkoutSessionId,
166
+ analysis,
167
+ });
168
+
169
+ if (analysis.allCompleted) {
170
+ logger.info('All vendors completed successfully, triggering commission', {
171
+ checkoutSessionId,
172
+ successfulVendors: analysis.successfulVendors.length,
173
+ });
174
+
175
+ await triggerCommissionProcess(checkoutSessionId, paymentIntentId);
176
+ } else if (analysis.anyMaxRetriesExceeded || analysis.shouldTimeout || analysis.failedVendors.length > 0) {
177
+ logger.warn('Some vendors failed, initiating full refund', {
178
+ checkoutSessionId,
179
+ failedVendors: analysis.failedVendors.length,
180
+ maxRetriesExceededVendors: analysis.maxRetriesExceededVendors.length,
181
+ timeoutVendors: analysis.shouldTimeout ? 'detected' : 'none',
182
+ });
183
+
184
+ await initiateFullRefund(paymentIntentId, 'vendor_failure_detected');
185
+ } else {
186
+ logger.info('Some vendors still in progress, waiting for completion', {
187
+ checkoutSessionId,
188
+ pendingVendors: analysis.pendingVendors.length,
189
+ processingVendors: analysis.processingVendors.length,
190
+ failedVendors: analysis.failedVendors.length,
191
+ });
192
+ }
193
+ } catch (error: any) {
194
+ logger.error('Fulfillment coordination failed', {
195
+ checkoutSessionId,
196
+ error: error.message,
197
+ });
198
+
199
+ await initiateFullRefund(paymentIntentId, 'coordination_failed');
200
+ }
201
+ }
202
+
203
+ async function getVendorConfigurations(checkoutSessionId: string): Promise<any[]> {
204
+ const checkoutSession = await CheckoutSession.findByPk(checkoutSessionId);
205
+ if (!checkoutSession?.line_items) {
206
+ return [];
207
+ }
208
+
209
+ const priceIds = checkoutSession.line_items.map((item: any) => item.price_id).filter(Boolean);
210
+ if (priceIds.length === 0) {
211
+ return [];
212
+ }
213
+
214
+ const prices = await Price.findAll({
215
+ where: { id: priceIds },
216
+ attributes: ['id', 'product_id'],
217
+ });
218
+
219
+ const productIds = prices.map((price: any) => price.product_id).filter(Boolean);
220
+ if (productIds.length === 0) {
221
+ return [];
222
+ }
223
+
224
+ const product = await Product.findByPk(productIds[0]);
225
+ return product?.vendor_config || [];
226
+ }
227
+
228
+ async function getVendorInfo(checkoutSessionId: string): Promise<VendorInfo[]> {
229
+ const checkoutSession = await CheckoutSession.findByPk(checkoutSessionId);
230
+ return (checkoutSession?.vendor_info as VendorInfo[]) || [];
231
+ }
232
+
233
+ async function updateVendorInfo(checkoutSessionId: string, vendorInfo: VendorInfo[]): Promise<void> {
234
+ await CheckoutSession.update({ vendor_info: vendorInfo }, { where: { id: checkoutSessionId } });
235
+ }
236
+
237
+ async function updateSingleVendorInfo(
238
+ checkoutSessionId: string,
239
+ vendorId: string,
240
+ update: Partial<VendorInfo>
241
+ ): Promise<void> {
242
+ const lockKey = `vendor-info-${checkoutSessionId}`;
243
+
244
+ logger.info('updateVendorInfo - Before update', {
245
+ checkoutSessionId,
246
+ vendorId,
247
+ update,
248
+ lockKey,
249
+ });
250
+
251
+ const lock = await getLock(lockKey);
252
+
253
+ try {
254
+ await sequelize.transaction(async (transaction: any) => {
255
+ const checkoutSession = await CheckoutSession.findByPk(checkoutSessionId, {
256
+ transaction,
257
+ lock: transaction.LOCK.UPDATE,
258
+ });
259
+
260
+ if (!checkoutSession) {
261
+ throw new Error(`CheckoutSession not found: ${checkoutSessionId}`);
262
+ }
263
+
264
+ const current = (checkoutSession.vendor_info as VendorInfo[]) || [];
265
+ const index = current.findIndex((vendor) => vendor.vendor_id === vendorId);
266
+
267
+ if (index >= 0) {
268
+ current[index] = {
269
+ ...current[index],
270
+ ...update,
271
+ vendor_id: current[index]?.vendor_id || vendorId,
272
+ order_id: update.order_id || current[index]?.order_id || '',
273
+ status: update.status || current[index]?.status || 'pending',
274
+ amount: update.amount || current[index]?.amount || '0',
275
+ } as VendorInfo;
276
+ } else {
277
+ current.push({
278
+ vendor_id: vendorId,
279
+ order_id: update.order_id || '',
280
+ status: update.status || 'pending',
281
+ amount: update.amount || '0',
282
+ ...update,
283
+ } as VendorInfo);
284
+ }
285
+
286
+ logger.info('updateVendorInfo - After merge, before database update (in transaction)', {
287
+ checkoutSessionId,
288
+ vendorId,
289
+ finalVendorInfo: current,
290
+ updatedVendorStatus: index >= 0 ? current[index]?.status : current[current.length - 1]?.status,
291
+ });
292
+
293
+ const fulfillmentStatus =
294
+ !checkoutSession.fulfillment_status ||
295
+ !['completed', 'failed', 'cancelled', 'max_retries_exceeded', 'return_requested'].includes(
296
+ checkoutSession.fulfillment_status
297
+ )
298
+ ? update.status
299
+ : undefined;
300
+
301
+ await CheckoutSession.update(
302
+ {
303
+ vendor_info: current,
304
+ fulfillment_status: fulfillmentStatus,
305
+ },
306
+ {
307
+ where: { id: checkoutSessionId },
308
+ transaction,
309
+ }
310
+ );
311
+
312
+ logger.info('updateVendorInfo - Database update completed (in transaction)', {
313
+ checkoutSessionId,
314
+ vendorId,
315
+ updateStatus: update.status,
316
+ });
317
+ });
318
+ } catch (error: any) {
319
+ logger.error('updateVendorInfo - Update failed', {
320
+ checkoutSessionId,
321
+ vendorId,
322
+ error: error.message,
323
+ });
324
+ throw error;
325
+ } finally {
326
+ lock.release();
327
+ logger.info('updateVendorInfo - Lock released', {
328
+ checkoutSessionId,
329
+ vendorId,
330
+ lockKey,
331
+ });
332
+ }
333
+ }
334
+
335
+ function analyzeVendorStates(vendorInfo: VendorInfo[], vendorConfigs: any[]) {
336
+ const vendorIds = vendorConfigs.map((config) => config.vendor_id);
337
+
338
+ const successfulVendors = vendorInfo.filter((vendor) => vendor.status === 'completed');
339
+ const failedVendors = vendorInfo.filter((vendor) => vendor.status === 'failed');
340
+ const maxRetriesExceededVendors = vendorInfo.filter((vendor) => vendor.status === 'max_retries_exceeded');
341
+ const pendingVendors = vendorInfo.filter((vendor) => vendor.status === 'pending');
342
+ const processingVendors = vendorInfo.filter((vendor) => vendor.status === 'processing' || vendor.status === 'sent');
343
+ const sentVendors = vendorInfo.filter((vendor) => vendor.status === 'sent');
344
+
345
+ const allCompleted = successfulVendors.length === vendorIds.length;
346
+ const anyMaxRetriesExceeded = maxRetriesExceededVendors.length > 0;
347
+ const allPending = pendingVendors.length === vendorIds.length;
348
+ const someProcessing = processingVendors.length > 0;
349
+
350
+ const now = Date.now();
351
+ const shouldTimeout = vendorInfo.some((vendor) => {
352
+ if (!vendor.lastAttemptAt) return false;
353
+ const timeSinceLastAttempt = now - new Date(vendor.lastAttemptAt).getTime();
354
+
355
+ if (vendor.status === 'pending' && timeSinceLastAttempt > MAX_FULFILLMENT_TIMEOUT) {
356
+ logger.warn('Vendor has been pending for too long, considering as timeout', {
357
+ vendorId: vendor.vendor_id,
358
+ status: vendor.status,
359
+ timeSinceLastAttempt: `${Math.floor(timeSinceLastAttempt / 1000)}s`,
360
+ timeoutThreshold: `${Math.floor(MAX_FULFILLMENT_TIMEOUT / 1000)}s`,
361
+ });
362
+ return true;
363
+ }
364
+
365
+ return timeSinceLastAttempt > MAX_FULFILLMENT_TIMEOUT;
366
+ });
367
+
368
+ return {
369
+ allCompleted,
370
+ sentVendors,
371
+ anyMaxRetriesExceeded,
372
+ allPending,
373
+ someProcessing,
374
+ shouldTimeout,
375
+ successfulVendors,
376
+ failedVendors,
377
+ maxRetriesExceededVendors,
378
+ pendingVendors,
379
+ processingVendors,
380
+ totalVendors: vendorIds.length,
381
+ };
382
+ }
383
+
384
+ export function triggerCoordinatorCheck(checkoutSessionId: string, paymentIntentId: string, triggeredBy: string) {
385
+ const jobId = `coordinator-${checkoutSessionId}-${Date.now()}`;
386
+
387
+ return fulfillmentCoordinatorQueue.push({
388
+ id: jobId,
389
+ job: {
390
+ checkoutSessionId,
391
+ paymentIntentId,
392
+ triggeredBy,
393
+ },
394
+ });
395
+ }
396
+
397
+ async function triggerCommissionProcess(checkoutSessionId: string, paymentIntentId: string): Promise<void> {
398
+ logger.info('Triggering commission process', {
399
+ checkoutSessionId,
400
+ paymentIntentId,
401
+ });
402
+
403
+ await VendorFulfillmentService.createVendorPayouts(checkoutSessionId);
404
+ await CheckoutSession.update({ fulfillment_status: 'completed' }, { where: { id: checkoutSessionId } });
405
+
406
+ const paymentIntent = await PaymentIntent.findByPk(paymentIntentId);
407
+ if (paymentIntent) {
408
+ const jobId = `deposit-vault-${paymentIntent.currency_id}`;
409
+ const existingJob = await depositVaultQueue.get(jobId);
410
+
411
+ if (!existingJob) {
412
+ await depositVaultQueue.push({
413
+ id: jobId,
414
+ job: { currencyId: paymentIntent.currency_id },
415
+ });
416
+ }
417
+ }
418
+
419
+ logger.info('Commission process triggered successfully', {
420
+ checkoutSessionId,
421
+ paymentIntentId,
422
+ });
423
+ }
424
+
425
+ export async function initiateFullRefund(paymentIntentId: string, reason: string): Promise<void> {
426
+ logger.warn('Initiating full refund with compensation', {
427
+ paymentIntentId,
428
+ reason,
429
+ });
430
+
431
+ try {
432
+ const paymentIntent = await PaymentIntent.findByPk(paymentIntentId);
433
+ const checkoutSession = await CheckoutSession.findByPaymentIntentId(paymentIntentId);
434
+
435
+ if (!checkoutSession || !paymentIntent) {
436
+ logger.error('Missing data for full refund', {
437
+ paymentIntentId,
438
+ hasCheckoutSession: !!checkoutSession,
439
+ hasPaymentIntent: !!paymentIntent,
440
+ });
441
+ return;
442
+ }
443
+
444
+ await CheckoutSession.update({ fulfillment_status: 'cancelled' }, { where: { id: checkoutSession.id } });
445
+ await requestReturnsFromCompletedVendors(checkoutSession.id, paymentIntentId, reason);
446
+
447
+ const refund = await Refund.create({
448
+ type: 'refund',
449
+ livemode: paymentIntent.livemode,
450
+ amount: paymentIntent.amount,
451
+ description: `Full refund due to ${reason}`,
452
+ status: 'pending',
453
+ reason,
454
+ currency_id: paymentIntent.currency_id,
455
+ customer_id: paymentIntent.customer_id || '',
456
+ payment_method_id: paymentIntent.payment_method_id,
457
+ payment_intent_id: paymentIntent.id,
458
+ invoice_id: paymentIntent.invoice_id,
459
+ subscription_id: checkoutSession.subscription_id || undefined,
460
+ attempt_count: 0,
461
+ attempted: false,
462
+ next_attempt: 0,
463
+ last_attempt_error: null,
464
+ starting_balance: '0',
465
+ ending_balance: '0',
466
+ starting_token_balance: {},
467
+ ending_token_balance: {},
468
+ metadata: {
469
+ checkout_session_id: checkoutSession.id,
470
+ refund_reason: reason,
471
+ auto_refund: true,
472
+ requested_by: 'coordinator',
473
+ ...(checkoutSession.subscription_id && { subscription_id: checkoutSession.subscription_id }),
474
+ },
475
+ });
476
+
477
+ logger.info('Full refund created, triggering refund processing', {
478
+ refundId: refund.id,
479
+ checkoutSessionId: checkoutSession.id,
480
+ paymentIntentId,
481
+ amount: refund.amount,
482
+ reason,
483
+ });
484
+
485
+ events.emit('refund.created', refund);
486
+ } catch (error: any) {
487
+ logger.error('Failed to create full refund', {
488
+ paymentIntentId,
489
+ reason,
490
+ error: error.message,
491
+ });
492
+ }
493
+ }
494
+
495
+ async function requestReturnsFromCompletedVendors(
496
+ checkoutSessionId: string,
497
+ paymentIntentId: string,
498
+ reason: string
499
+ ): Promise<void> {
500
+ logger.info('Starting return request process', {
501
+ checkoutSessionId,
502
+ paymentIntentId,
503
+ reason,
504
+ });
505
+
506
+ try {
507
+ const checkoutSession = await CheckoutSession.findByPk(checkoutSessionId);
508
+ if (!checkoutSession) {
509
+ logger.error('CheckoutSession not found for return request', { checkoutSessionId });
510
+ return;
511
+ }
512
+
513
+ const vendorInfos = (checkoutSession.vendor_info as VendorInfo[]) || [];
514
+ const completedVendors = vendorInfos.filter((vendor) => vendor.status === 'completed');
515
+
516
+ if (completedVendors.length === 0) {
517
+ logger.info('No completed vendors to request returns from', { checkoutSessionId });
518
+ return;
519
+ }
520
+
521
+ logger.info(`Found ${completedVendors.length} completed vendors requiring return requests`, {
522
+ checkoutSessionId,
523
+ completedVendorIds: completedVendors.map((v) => v.vendor_id),
524
+ });
525
+
526
+ const returnRequestPromises = completedVendors.map((vendor) => {
527
+ return requestReturnFromSingleVendor(checkoutSessionId, paymentIntentId, vendor, reason);
528
+ });
529
+
530
+ const returnResults = await Promise.allSettled(returnRequestPromises);
531
+
532
+ returnResults.forEach((result, index) => {
533
+ const vendor = completedVendors[index];
534
+ if (vendor) {
535
+ if (result.status === 'fulfilled') {
536
+ logger.info('Return request submitted successfully', {
537
+ vendorId: vendor.vendor_id,
538
+ orderId: vendor.order_id,
539
+ });
540
+ } else {
541
+ logger.error('Return request failed', {
542
+ vendorId: vendor.vendor_id,
543
+ orderId: vendor.order_id,
544
+ error: result.reason,
545
+ });
546
+ }
547
+ }
548
+ });
549
+
550
+ logger.info('Return request process completed', {
551
+ checkoutSessionId,
552
+ totalVendors: completedVendors.length,
553
+ successful: returnResults.filter((r) => r.status === 'fulfilled').length,
554
+ failed: returnResults.filter((r) => r.status === 'rejected').length,
555
+ });
556
+ } catch (error: any) {
557
+ logger.error('Return request process failed', {
558
+ checkoutSessionId,
559
+ paymentIntentId,
560
+ error: error.message,
561
+ });
562
+ }
563
+ }
564
+
565
+ async function requestReturnFromSingleVendor(
566
+ checkoutSessionId: string,
567
+ paymentIntentId: string,
568
+ vendor: VendorInfo,
569
+ reason: string
570
+ ): Promise<void> {
571
+ logger.info('Requesting return for vendor', {
572
+ vendorId: vendor.vendor_id,
573
+ orderId: vendor.order_id,
574
+ reason,
575
+ });
576
+
577
+ try {
578
+ const vendorAdapter = await VendorFulfillmentService.getVendorAdapter(vendor.vendor_id);
579
+ if (!vendorAdapter) {
580
+ throw new Error(`No adapter found for vendor: ${vendor.vendor_id}`);
581
+ }
582
+
583
+ const returnResult = await vendorAdapter.requestReturn({
584
+ orderId: vendor.order_id,
585
+ reason: `Return request due to: ${reason}`,
586
+ paymentIntentId,
587
+ customParams: {
588
+ returnType: 'order_failure',
589
+ originalAmount: vendor.amount,
590
+ },
591
+ });
592
+
593
+ let { status } = returnResult;
594
+ if (returnResult.status === 'requested') {
595
+ status = 'pending' as 'accepted';
596
+ } else if (returnResult.status === 'failed') {
597
+ status = 'rejected' as 'rejected';
598
+ }
599
+
600
+ await updateSingleVendorInfo(checkoutSessionId, vendor.vendor_id, {
601
+ status: 'return_requested',
602
+ returnRequest: {
603
+ reason,
604
+ requestedAt: new Date().toISOString(),
605
+ status: status as 'pending' | 'accepted' | 'rejected',
606
+ returnDetails: returnResult.message,
607
+ },
608
+ });
609
+
610
+ logger.info('Return request submitted successfully', {
611
+ vendorId: vendor.vendor_id,
612
+ orderId: vendor.order_id,
613
+ returnStatus: returnResult.status,
614
+ });
615
+ } catch (error: any) {
616
+ logger.error('Return request failed', {
617
+ vendorId: vendor.vendor_id,
618
+ orderId: vendor.order_id,
619
+ error: error.message,
620
+ });
621
+
622
+ await updateSingleVendorInfo(checkoutSessionId, vendor.vendor_id, {
623
+ status: 'return_requested',
624
+ error_message: `Return request failed: ${error.message}`,
625
+ });
626
+ }
627
+ }