payment-kit 1.20.8 → 1.20.9

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 (32) hide show
  1. package/api/src/crons/index.ts +1 -1
  2. package/api/src/index.ts +2 -2
  3. package/api/src/libs/{vendor → vendor-util}/adapters/factory.ts +9 -5
  4. package/api/src/libs/{vendor → vendor-util}/adapters/launcher-adapter.ts +3 -8
  5. package/api/src/libs/{vendor → vendor-util}/adapters/types.ts +1 -4
  6. package/api/src/libs/{vendor → vendor-util}/fulfillment.ts +11 -8
  7. package/api/src/queues/{vendor → vendors}/commission.ts +4 -5
  8. package/api/src/queues/{vendor → vendors}/fulfillment-coordinator.ts +33 -4
  9. package/api/src/queues/{vendor → vendors}/fulfillment.ts +2 -2
  10. package/api/src/queues/{vendor → vendors}/status-check.ts +2 -2
  11. package/api/src/routes/payment-links.ts +2 -1
  12. package/api/src/routes/products.ts +1 -0
  13. package/api/src/routes/vendor.ts +135 -213
  14. package/api/src/store/migrations/20250911-add-vendor-type.ts +26 -0
  15. package/api/src/store/models/product-vendor.ts +6 -24
  16. package/api/src/store/models/product.ts +1 -0
  17. package/blocklet.yml +1 -1
  18. package/doc/vendor_fulfillment_system.md +1 -1
  19. package/package.json +5 -5
  20. package/src/components/metadata/form.tsx +12 -19
  21. package/src/components/payment-link/before-pay.tsx +40 -0
  22. package/src/components/product/vendor-config.tsx +4 -11
  23. package/src/components/subscription/description.tsx +1 -6
  24. package/src/components/subscription/portal/list.tsx +82 -6
  25. package/src/components/subscription/vendor-service-list.tsx +128 -0
  26. package/src/components/vendor/actions.tsx +1 -33
  27. package/src/locales/en.tsx +13 -3
  28. package/src/locales/zh.tsx +13 -3
  29. package/src/pages/admin/products/links/create.tsx +2 -0
  30. package/src/pages/admin/products/vendors/create.tsx +108 -194
  31. package/src/pages/admin/products/vendors/index.tsx +14 -22
  32. package/src/pages/customer/subscription/detail.tsx +26 -11
@@ -24,7 +24,7 @@ import logger from '../libs/logger';
24
24
  import { startCreditConsumeQueue } from '../queues/credit-consume';
25
25
  import { startDepositVaultQueue } from '../queues/payment';
26
26
  import { startSubscriptionQueue } from '../queues/subscription';
27
- import { startVendorStatusCheckSchedule } from '../queues/vendor/status-check';
27
+ import { startVendorStatusCheckSchedule } from '../queues/vendors/status-check';
28
28
  import { CheckoutSession } from '../store/models';
29
29
  import { createMeteringSubscriptionDetection } from './metering-subscription-detection';
30
30
  import { createPaymentStat } from './payment-stat';
package/api/src/index.ts CHANGED
@@ -34,8 +34,8 @@ import { startPayoutQueue } from './queues/payout';
34
34
  import { startRefundQueue } from './queues/refund';
35
35
  import { startUploadBillingInfoListener } from './queues/space';
36
36
  import { startSubscriptionQueue } from './queues/subscription';
37
- import { startVendorCommissionQueue } from './queues/vendor/commission';
38
- import { startVendorFulfillmentQueue } from './queues/vendor/fulfillment';
37
+ import { startVendorCommissionQueue } from './queues/vendors/commission';
38
+ import { startVendorFulfillmentQueue } from './queues/vendors/fulfillment';
39
39
  import routes from './routes';
40
40
  import autoRechargeAuthorizationHandlers from './routes/connect/auto-recharge-auth';
41
41
  import changePaymentHandlers from './routes/connect/change-payment';
@@ -1,6 +1,7 @@
1
1
  import { BN } from '@ocap/util';
2
2
  import { LauncherAdapter } from './launcher-adapter';
3
- import { VendorAdapter, VendorConfig } from './types';
3
+ import { VendorAdapter } from './types';
4
+ import { ProductVendor } from '../../../store/models';
4
5
 
5
6
  export function calculateVendorCommission(
6
7
  totalAmount: string,
@@ -20,13 +21,16 @@ export function calculateVendorCommission(
20
21
  }
21
22
 
22
23
  export class VendorAdapterFactory {
23
- static create(vendorConfig: string | VendorConfig): VendorAdapter {
24
- const vendorKey = typeof vendorConfig === 'string' ? vendorConfig : vendorConfig.vendor_key;
25
- switch (vendorKey) {
24
+ static async create(vendorKey: string): Promise<VendorAdapter> {
25
+ const vendorConfig = await ProductVendor.findOne({ where: { vendor_key: vendorKey } });
26
+ if (!vendorConfig) {
27
+ throw new Error(`Vendor not found: ${vendorKey}`);
28
+ }
29
+ switch (vendorConfig.vendor_type) {
26
30
  case 'launcher':
27
31
  return new LauncherAdapter(vendorConfig);
28
32
  default:
29
- throw new Error(`Unsupported vendor: ${vendorConfig}`);
33
+ throw new Error(`Unsupported vendor: ${vendorConfig.vendor_type}`);
30
34
  }
31
35
  }
32
36
 
@@ -18,14 +18,9 @@ export class LauncherAdapter implements VendorAdapter {
18
18
  private vendorConfig: VendorConfig | null = null;
19
19
  private vendorKey: string;
20
20
 
21
- constructor(vendorInfo: VendorConfig | string) {
22
- if (typeof vendorInfo === 'string') {
23
- this.vendorKey = vendorInfo;
24
- this.vendorConfig = null;
25
- } else {
26
- this.vendorKey = vendorInfo.id;
27
- this.vendorConfig = vendorInfo;
28
- }
21
+ constructor(vendorInfo: VendorConfig) {
22
+ this.vendorKey = vendorInfo.vendor_key;
23
+ this.vendorConfig = vendorInfo;
29
24
  }
30
25
 
31
26
  async getVendorConfig(): Promise<VendorConfig> {
@@ -1,13 +1,10 @@
1
1
  export interface VendorConfig {
2
2
  id: string;
3
3
  vendor_key: string;
4
+ vendor_type: string;
4
5
  name: string;
5
6
  description: string;
6
7
  app_url: string;
7
- webhook_path?: string;
8
- default_commission_rate: number;
9
- default_commission_type: 'percentage' | 'fixed_amount';
10
- order_create_params: Record<string, any>;
11
8
  status: 'active' | 'inactive';
12
9
  metadata: Record<string, any>;
13
10
  }
@@ -9,6 +9,8 @@ import { calculateVendorCommission, VendorAdapterFactory } from './adapters/fact
9
9
  import { Customer } from '../../store/models';
10
10
  import logger from '../logger';
11
11
 
12
+ const DEFAULT_COMMISSION_RATE = 20;
13
+
12
14
  export interface VendorFulfillmentResult {
13
15
  vendorId: string;
14
16
  vendorKey: string;
@@ -55,11 +57,11 @@ export class VendorFulfillmentService {
55
57
  throw new Error(`Vendor not found: ${vendorConfig.vendor_id}`);
56
58
  }
57
59
 
58
- const adapter = this.getVendorAdapter(vendor.vendor_key);
60
+ const adapter = await this.getVendorAdapter(vendor.vendor_key);
59
61
 
60
62
  // Calculate commission amount
61
- const commissionRate = vendorConfig.commission_rate || vendor.default_commission_rate;
62
- const commissionType = vendorConfig.commission_type || vendor.default_commission_type;
63
+ const commissionRate = vendorConfig.commission_rate ?? DEFAULT_COMMISSION_RATE;
64
+ const commissionType = vendorConfig.commission_type || 'percentage';
63
65
  const commissionAmount = calculateVendorCommission(
64
66
  orderInfo.amount_total,
65
67
  commissionRate,
@@ -116,7 +118,7 @@ export class VendorFulfillmentService {
116
118
  } catch (error: any) {
117
119
  logger.error('Single vendor fulfillment failed', {
118
120
  vendorId: vendorConfig.vendor_id,
119
- error: error.message,
121
+ error,
120
122
  });
121
123
 
122
124
  throw error;
@@ -200,8 +202,8 @@ export class VendorFulfillmentService {
200
202
  );
201
203
 
202
204
  commissionResults.push({
203
- vendorId: vendorConfig.vendor_key || vendorConfig.vendor_id,
204
- vendorKey: vendorConfig.vendor_key || vendorConfig.vendor_id,
205
+ vendorId: vendorConfig.vendor_key,
206
+ vendorKey: vendorConfig.vendor_key,
205
207
  orderId: `commission_${checkoutSessionId}_${product.id}_${vendorConfig.vendor_id}`,
206
208
  status: 'completed',
207
209
  commissionAmount,
@@ -303,9 +305,10 @@ export class VendorFulfillmentService {
303
305
  }
304
306
  }
305
307
 
306
- static getVendorAdapter(vendorKey: string) {
308
+ static async getVendorAdapter(vendorKey: string) {
307
309
  try {
308
- return VendorAdapterFactory.create(vendorKey);
310
+ const vendor = await VendorAdapterFactory.create(vendorKey);
311
+ return vendor;
309
312
  } catch (error: any) {
310
313
  logger.error('Failed to get vendor adapter', {
311
314
  vendorKey,
@@ -58,7 +58,7 @@ async function checkIfPaymentIntentHasVendors(
58
58
  } catch (error: any) {
59
59
  logger.error('Failed to check vendor configuration', {
60
60
  paymentIntentId: paymentIntent.id,
61
- error: error.message,
61
+ error,
62
62
  });
63
63
  return false;
64
64
  }
@@ -123,8 +123,7 @@ export const handleVendorCommission = async (job: VendorCommissionJob) => {
123
123
  } catch (error: any) {
124
124
  logger.error('Vendor commission decision failed, fallback to direct deposit vault', {
125
125
  paymentIntentId: job.paymentIntentId,
126
- error: error.message,
127
- stack: error.stack,
126
+ error,
128
127
  });
129
128
 
130
129
  if (!checkoutSession) {
@@ -137,7 +136,7 @@ export const handleVendorCommission = async (job: VendorCommissionJob) => {
137
136
  try {
138
137
  triggerCoordinatorCheck(checkoutSession.id, job.paymentIntentId, 'vendor_commission_decision_failed');
139
138
  } catch (err: any) {
140
- logger.error('Failed to trigger coordinator check[handleVendorCommission]', { error: err.message });
139
+ logger.error('Failed to trigger coordinator check[handleVendorCommission]', { error: err });
141
140
  }
142
141
  }
143
142
  };
@@ -186,7 +185,7 @@ events.on('vendor.commission.queued', async (id, job, args = {}) => {
186
185
  } catch (error: any) {
187
186
  logger.error('Failed to handle vendor commission queue event', {
188
187
  id,
189
- error: error.message,
188
+ error,
190
189
  });
191
190
  }
192
191
  });
@@ -1,8 +1,10 @@
1
+ import { BN } from '@ocap/util';
2
+ import { Op } from 'sequelize';
1
3
  import { events } from '../../libs/event';
2
4
  import { getLock } from '../../libs/lock';
3
5
  import logger from '../../libs/logger';
4
6
  import createQueue from '../../libs/queue';
5
- import { VendorFulfillmentService } from '../../libs/vendor/fulfillment';
7
+ import { VendorFulfillmentService } from '../../libs/vendor-util/fulfillment';
6
8
  import { CheckoutSession } from '../../store/models/checkout-session';
7
9
  import { PaymentIntent } from '../../store/models/payment-intent';
8
10
  import { Price } from '../../store/models/price';
@@ -444,11 +446,38 @@ export async function initiateFullRefund(paymentIntentId: string, reason: string
444
446
  await CheckoutSession.update({ fulfillment_status: 'cancelled' }, { where: { id: checkoutSession.id } });
445
447
  await requestReturnsFromCompletedVendors(checkoutSession.id, paymentIntentId, reason);
446
448
 
449
+ // Calculate remaining amount using the same logic as subscription createProration
450
+ const refunds = await Refund.findAll({
451
+ where: {
452
+ status: { [Op.not]: 'canceled' },
453
+ payment_intent_id: paymentIntentId,
454
+ type: 'refund',
455
+ },
456
+ });
457
+ const refundAmount = refunds.reduce((acc, x) => acc.add(new BN(x.amount || '0')), new BN(0));
458
+
459
+ // Calculate remaining amount, similar to subscription logic
460
+ const calcRemaining = (amount: BN, subtract: BN) =>
461
+ amount.sub(subtract).lt(new BN(0)) ? '0' : amount.sub(subtract).toString();
462
+
463
+ const remaining = calcRemaining(new BN(paymentIntent.amount), refundAmount);
464
+
465
+ // If no remaining amount or already fully refunded, skip
466
+ if (new BN(remaining).lte(new BN('0'))) {
467
+ logger.info('Payment already fully refunded, skipping', {
468
+ paymentIntentId,
469
+ paymentAmount: paymentIntent.amount,
470
+ totalRefundAmount: refundAmount.toString(),
471
+ remaining,
472
+ });
473
+ return;
474
+ }
475
+
447
476
  const refund = await Refund.create({
448
477
  type: 'refund',
449
478
  livemode: paymentIntent.livemode,
450
- amount: paymentIntent.amount,
451
- description: `Full refund due to ${reason}`,
479
+ amount: remaining,
480
+ description: `${new BN(remaining).eq(new BN(paymentIntent.amount)) ? 'Full' : 'Partial'} refund due to ${reason}`,
452
481
  status: 'pending',
453
482
  reason,
454
483
  currency_id: paymentIntent.currency_id,
@@ -573,7 +602,7 @@ async function requestReturnFromSingleVendor(
573
602
  });
574
603
 
575
604
  try {
576
- const vendorAdapter = VendorFulfillmentService.getVendorAdapter(vendor.vendor_key);
605
+ const vendorAdapter = await VendorFulfillmentService.getVendorAdapter(vendor.vendor_key);
577
606
  if (!vendorAdapter) {
578
607
  throw new Error(`No adapter found for vendor: ${vendor.vendor_id}`);
579
608
  }
@@ -1,7 +1,7 @@
1
1
  import { events } from '../../libs/event';
2
2
  import logger from '../../libs/logger';
3
3
  import createQueue from '../../libs/queue';
4
- import { VendorFulfillmentService } from '../../libs/vendor/fulfillment';
4
+ import { VendorFulfillmentService } from '../../libs/vendor-util/fulfillment';
5
5
  import { CheckoutSession } from '../../store/models/checkout-session';
6
6
  import { updateVendorFulfillmentStatus } from './fulfillment-coordinator';
7
7
 
@@ -52,7 +52,7 @@ export const handleVendorFulfillment = async (job: VendorFulfillmentJob) => {
52
52
  logger.error('Vendor fulfillment failed', {
53
53
  vendorId,
54
54
  checkoutSessionId,
55
- error: error.message,
55
+ error,
56
56
  });
57
57
 
58
58
  await updateVendorFulfillmentStatus(checkoutSessionId, paymentIntentId, vendorId, 'failed', {
@@ -6,7 +6,7 @@ import { ProductVendor } from '../../store/models';
6
6
  import { fulfillmentCoordinatorQueue } from './fulfillment-coordinator';
7
7
  import logger from '../../libs/logger';
8
8
  import { vendorTimeoutMinutes } from '../../libs/env';
9
- import { VendorFulfillmentService } from '../../libs/vendor/fulfillment';
9
+ import { VendorFulfillmentService } from '../../libs/vendor-util/fulfillment';
10
10
 
11
11
  export const startVendorStatusCheckSchedule = async () => {
12
12
  const checkoutSessions = await CheckoutSession.findAll({
@@ -109,7 +109,7 @@ export const handleVendorStatusCheck = async (job: VendorStatusCheckJob) => {
109
109
  }
110
110
  }
111
111
 
112
- const adapter = VendorFulfillmentService.getVendorAdapter(vendor.vendor_key);
112
+ const adapter = await VendorFulfillmentService.getVendorAdapter(vendor.vendor_key);
113
113
  const result = await adapter.checkOrderStatus({ appUrl: vendor.app_url });
114
114
 
115
115
  if (result.status === 'completed') {
@@ -423,7 +423,8 @@ router.post('/stash', auth, async (req, res) => {
423
423
  raw.livemode = !!req.livemode;
424
424
  raw.created_via = req.user?.via;
425
425
  raw.currency_id = raw.currency_id || req.currency.id;
426
- raw.metadata = { preview: '1' };
426
+ // Merge existing metadata with preview flag
427
+ raw.metadata = { ...raw.metadata, preview: '1' };
427
428
 
428
429
  let doc = await PaymentLink.findByPk(raw.id);
429
430
  if (doc) {
@@ -452,6 +452,7 @@ router.put('/:id', auth, async (req, res) => {
452
452
  return {
453
453
  vendor_id: vendorConfig.id,
454
454
  vendor_key: vendorConfig.vendor_key,
455
+ vendor_type: vendorConfig.vendor_type,
455
456
  name: vendorConfig.name,
456
457
  description: vendorConfig.description,
457
458
  commission_rate: Number(config.commission_rate),