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
@@ -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';
@@ -6,7 +6,7 @@ import { handlePaymentIntentEvent } from './payment-intent';
6
6
  import { handleSetupIntentEvent } from './setup-intent';
7
7
  import { handleSubscriptionEvent } from './subscription';
8
8
 
9
- export default function handleStripeEvent(event: any, client: Stripe) {
9
+ export default async function handleStripeEvent(event: any, client: Stripe) {
10
10
  switch (event.type) {
11
11
  case 'payment_intent.canceled':
12
12
  case 'payment_intent.created':
@@ -14,8 +14,16 @@ export default function handleStripeEvent(event: any, client: Stripe) {
14
14
  case 'payment_intent.payment_failed':
15
15
  case 'payment_intent.processing':
16
16
  case 'payment_intent.requires_action':
17
- case 'payment_intent.succeeded':
17
+ case 'payment_intent.succeeded': {
18
+ if (event.data?.object?.id) {
19
+ const record = await client.paymentIntents.retrieve(event.data.object.id);
20
+ event.data.object = {
21
+ ...event.data.object,
22
+ ...record,
23
+ };
24
+ }
18
25
  return handlePaymentIntentEvent(event, client);
26
+ }
19
27
 
20
28
  // case 'setup_intent.created':
21
29
  case 'setup_intent.canceled':
@@ -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
 
@@ -1,5 +1,6 @@
1
1
  import { VendorAuth } from '@blocklet/payment-vendor';
2
2
 
3
+ import { joinURL } from 'ufo';
3
4
  import { ProductVendor } from '../../../store/models';
4
5
  import logger from '../../logger';
5
6
  import { api } from '../../util';
@@ -18,14 +19,9 @@ export class LauncherAdapter implements VendorAdapter {
18
19
  private vendorConfig: VendorConfig | null = null;
19
20
  private vendorKey: string;
20
21
 
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
- }
22
+ constructor(vendorInfo: VendorConfig) {
23
+ this.vendorKey = vendorInfo.vendor_key;
24
+ this.vendorConfig = vendorInfo;
29
25
  }
30
26
 
31
27
  async getVendorConfig(): Promise<VendorConfig> {
@@ -58,11 +54,14 @@ export class LauncherAdapter implements VendorAdapter {
58
54
  };
59
55
 
60
56
  const { headers, body } = VendorAuth.signRequestWithHeaders(orderData);
61
- const response = await fetch(`${launcherApiUrl}/api/vendor/deliveries`, {
62
- method: 'POST',
63
- headers,
64
- body,
65
- });
57
+ const response = await fetch(
58
+ joinURL(launcherApiUrl, vendorConfig.metadata?.mountPoint || '', '/api/vendor/deliveries'),
59
+ {
60
+ method: 'POST',
61
+ headers,
62
+ body,
63
+ }
64
+ );
66
65
 
67
66
  if (!response.ok) {
68
67
  const errorBody = await response.text();
@@ -130,11 +129,14 @@ export class LauncherAdapter implements VendorAdapter {
130
129
  const launcherApiUrl = vendorConfig.app_url;
131
130
  const { headers, body } = VendorAuth.signRequestWithHeaders(params);
132
131
 
133
- const response = await fetch(`${launcherApiUrl}/api/vendor/return`, {
134
- method: 'POST',
135
- headers,
136
- body,
137
- });
132
+ const response = await fetch(
133
+ joinURL(launcherApiUrl, vendorConfig.metadata?.mountPoint || '', '/api/vendor/return'),
134
+ {
135
+ method: 'POST',
136
+ headers,
137
+ body,
138
+ }
139
+ );
138
140
 
139
141
  if (!response.ok) {
140
142
  const errorBody = await response.text();
@@ -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
  }
@@ -1,13 +1,14 @@
1
1
  import { BN } from '@ocap/util';
2
+ import { Customer, PaymentMethod } from '../../store/models';
2
3
  import { CheckoutSession } from '../../store/models/checkout-session';
3
- import { Product } from '../../store/models/product';
4
- import { ProductVendor } from '../../store/models/product-vendor';
5
- import { Payout } from '../../store/models/payout';
6
4
  import { PaymentIntent } from '../../store/models/payment-intent';
7
- import { Price } from '../../store/models/price';
8
- import { calculateVendorCommission, VendorAdapterFactory } from './adapters/factory';
9
- import { Customer } from '../../store/models';
5
+ import { Payout } from '../../store/models/payout';
6
+ import { ProductVendor } from '../../store/models/product-vendor';
7
+ import env from '../env';
10
8
  import logger from '../logger';
9
+ import { calculateVendorCommission, VendorAdapterFactory } from './adapters/factory';
10
+
11
+ const DEFAULT_COMMISSION_RATE = 20;
11
12
 
12
13
  export interface VendorFulfillmentResult {
13
14
  vendorId: string;
@@ -55,11 +56,11 @@ export class VendorFulfillmentService {
55
56
  throw new Error(`Vendor not found: ${vendorConfig.vendor_id}`);
56
57
  }
57
58
 
58
- const adapter = this.getVendorAdapter(vendor.vendor_key);
59
+ const adapter = await this.getVendorAdapter(vendor.vendor_key);
59
60
 
60
61
  // Calculate commission amount
61
- const commissionRate = vendorConfig.commission_rate || vendor.default_commission_rate;
62
- const commissionType = vendorConfig.commission_type || vendor.default_commission_type;
62
+ const commissionRate = vendorConfig.commission_rate ?? DEFAULT_COMMISSION_RATE;
63
+ const commissionType = vendorConfig.commission_type || 'percentage';
63
64
  const commissionAmount = calculateVendorCommission(
64
65
  orderInfo.amount_total,
65
66
  commissionRate,
@@ -78,11 +79,11 @@ export class VendorFulfillmentService {
78
79
  amount: orderInfo.amount_total,
79
80
  currency: orderInfo.currency_id,
80
81
 
81
- description: 'This is build a instance for Third Party',
82
+ description: `This is an app launched by ${env.appName || 'Third Party'}`,
82
83
  userInfo: {
83
84
  userDid: orderInfo.customer_did!,
84
85
  email: userEmail,
85
- description: 'This is build a instance for Third Party',
86
+ description: `This is an app launched by ${env.appName || 'Third Party'}`,
86
87
  },
87
88
  deliveryParams: {
88
89
  blockletMetaUrl: vendor.metadata?.blockletMetaUrl,
@@ -116,120 +117,9 @@ export class VendorFulfillmentService {
116
117
  } catch (error: any) {
117
118
  logger.error('Single vendor fulfillment failed', {
118
119
  vendorId: vendorConfig.vendor_id,
119
- error: error.message,
120
- });
121
-
122
- throw error;
123
- }
124
- }
125
-
126
- // Calculate commission data (no fulfillment, only calculate commission)
127
- private static async calculateCommissionData(checkoutSessionId: string): Promise<VendorFulfillmentResult[]> {
128
- try {
129
- logger.info('Calculating commission data for checkout session', { checkoutSessionId });
130
-
131
- const checkoutSession = await CheckoutSession.findByPk(checkoutSessionId);
132
- if (!checkoutSession) {
133
- throw new Error(`CheckoutSession not found: ${checkoutSessionId}`);
134
- }
135
-
136
- const priceIds = checkoutSession.line_items.map((item: any) => item.price_id).filter(Boolean);
137
-
138
- if (priceIds.length === 0) {
139
- throw new Error(`No price IDs found in checkout session line items: ${checkoutSessionId}`);
140
- }
141
-
142
- // Find corresponding product_ids through price_ids
143
- const prices = await Price.findAll({
144
- where: { id: priceIds },
145
- attributes: ['id', 'product_id'],
146
- });
147
-
148
- // Create a map of price_id to line_item for amount calculation
149
- const priceToLineItemMap = new Map();
150
- checkoutSession.line_items.forEach((item: any) => {
151
- if (item.price_id) {
152
- priceToLineItemMap.set(item.price_id, item);
153
- }
120
+ error,
154
121
  });
155
122
 
156
- const productIds = prices.map((price: any) => price.product_id).filter(Boolean);
157
-
158
- if (productIds.length === 0) {
159
- throw new Error(`No product IDs found from prices: ${checkoutSessionId}`);
160
- }
161
-
162
- // Get all products with vendor configurations
163
- const products = await Product.findAll({
164
- where: { id: productIds },
165
- attributes: ['id', 'vendor_config'],
166
- });
167
-
168
- const commissionResults: VendorFulfillmentResult[] = [];
169
-
170
- // Process each product separately
171
- for (const product of products) {
172
- if (!product.vendor_config || product.vendor_config.length === 0) {
173
- logger.debug('No vendor configuration found for product', {
174
- productId: product.id,
175
- checkoutSessionId,
176
- });
177
- // eslint-disable-next-line no-continue
178
- continue;
179
- }
180
-
181
- // Find all prices for this product
182
- const productPrices = prices.filter((price: any) => price.product_id === product.id);
183
-
184
- // Calculate total amount for this product across all line items
185
- let productTotalAmount = '0';
186
- for (const price of productPrices) {
187
- const lineItem = priceToLineItemMap.get(price.id);
188
- if (lineItem && lineItem.amount_total) {
189
- productTotalAmount = new BN(productTotalAmount).add(new BN(lineItem.amount_total || '0')).toString();
190
- }
191
- }
192
-
193
- // Calculate commission for each vendor of this product
194
- for (const vendorConfig of product.vendor_config) {
195
- const commissionAmount = calculateVendorCommission(
196
- productTotalAmount, // Use product-specific amount instead of total order amount
197
- vendorConfig.commission_rate || 0,
198
- vendorConfig.commission_type || 'percentage',
199
- vendorConfig.amount
200
- );
201
-
202
- commissionResults.push({
203
- vendorId: vendorConfig.vendor_key || vendorConfig.vendor_id,
204
- vendorKey: vendorConfig.vendor_key || vendorConfig.vendor_id,
205
- orderId: `commission_${checkoutSessionId}_${product.id}_${vendorConfig.vendor_id}`,
206
- status: 'completed',
207
- commissionAmount,
208
- commissionType: vendorConfig.commission_type || 'percentage',
209
- commissionRate: vendorConfig.commission_rate || 0,
210
- productId: product.id, // Add product ID for tracking
211
- });
212
- }
213
- }
214
-
215
- if (commissionResults.length === 0) {
216
- logger.warn('No vendor configurations found for any products', {
217
- productIds,
218
- checkoutSessionId,
219
- });
220
- }
221
-
222
- logger.info('Commission data calculated', {
223
- checkoutSessionId,
224
- commissionCount: commissionResults.length,
225
- });
226
-
227
- return commissionResults;
228
- } catch (error: any) {
229
- logger.error('Failed to calculate commission data', {
230
- checkoutSessionId,
231
- error: error.message,
232
- });
233
123
  throw error;
234
124
  }
235
125
  }
@@ -255,13 +145,19 @@ export class VendorFulfillmentService {
255
145
  // If fulfillmentResults not provided, calculate commission info
256
146
  let commissionData = fulfillmentResults;
257
147
  if (!commissionData) {
258
- commissionData = await this.calculateCommissionData(checkoutSessionId);
148
+ commissionData = checkoutSession.vendor_info?.map((vendorInfo: any) => ({
149
+ vendorId: vendorInfo.vendor_id,
150
+ orderId: vendorInfo.order_id,
151
+ status: vendorInfo.status,
152
+ commissionAmount: vendorInfo.commissionAmount,
153
+ })) as VendorFulfillmentResult[];
259
154
  }
260
155
 
261
156
  const payoutPromises = commissionData
262
157
  .filter((result) => result.status !== 'failed' && new BN(result.commissionAmount).gt(new BN('0')))
263
158
  .map(async (result) => {
264
159
  const vendor = await ProductVendor.findByPk(result.vendorId);
160
+ const paymentMethod = await PaymentMethod.findByPk(paymentMethodId);
265
161
 
266
162
  const appPid = vendor?.app_pid;
267
163
  const destination = appPid || vendor?.metadata?.wallet_address || vendor?.metadata?.destination || '';
@@ -276,7 +172,7 @@ export class VendorFulfillmentService {
276
172
  customer_id: checkoutSession.customer_id || '',
277
173
  payment_intent_id: checkoutSession.payment_intent_id || '',
278
174
  payment_method_id: paymentMethodId,
279
- status: 'pending',
175
+ status: paymentMethod?.type === 'stripe' ? 'deferred' : 'pending',
280
176
  attempt_count: 0,
281
177
  attempted: false,
282
178
  vendor_info: {
@@ -303,9 +199,10 @@ export class VendorFulfillmentService {
303
199
  }
304
200
  }
305
201
 
306
- static getVendorAdapter(vendorKey: string) {
202
+ static async getVendorAdapter(vendorKey: string) {
307
203
  try {
308
- return VendorAdapterFactory.create(vendorKey);
204
+ const vendor = await VendorAdapterFactory.create(vendorKey);
205
+ return vendor;
309
206
  } catch (error: any) {
310
207
  logger.error('Failed to get vendor adapter', {
311
208
  vendorKey,
@@ -373,11 +373,7 @@ export const handlePaymentSucceed = async (
373
373
  );
374
374
  }
375
375
 
376
- // Trigger vendor commission queue
377
- events.emit('vendor.commission.queued', `vendor-commission-${paymentIntent.id}`, {
378
- paymentIntentId: paymentIntent.id,
379
- retryOnError: true,
380
- });
376
+ // Trigger vendor commission queue by invoice.paid
381
377
 
382
378
  let invoice;
383
379
  if (paymentIntent.invoice_id) {
@@ -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
  };
@@ -168,25 +167,27 @@ export const startVendorCommissionQueue = async () => {
168
167
  });
169
168
  };
170
169
 
171
- events.on('vendor.commission.queued', async (id, job, args = {}) => {
170
+ events.on('invoice.paid', async (invoice) => {
172
171
  try {
173
- const { ...extraArgs } = args;
174
- const exist = await vendorCommissionQueue.get(id);
172
+ const paymentIntent = await PaymentIntent.findOne({ where: { invoice_id: invoice.id } });
175
173
 
176
- if (!exist) {
177
- logger.info('Vendor commission job added successfully', { id });
178
- vendorCommissionQueue.push({
179
- id,
180
- job,
181
- ...extraArgs,
182
- });
183
- } else {
174
+ if (!paymentIntent) {
175
+ logger.warn('PaymentIntent not found', { id: invoice.id });
176
+ return;
177
+ }
178
+
179
+ const id = `vendor-commission-${paymentIntent.id}`;
180
+ const exist = await vendorCommissionQueue.get(id);
181
+ if (exist) {
184
182
  logger.info('Vendor commission job already exists, skipping', { id });
183
+ return;
185
184
  }
186
- } catch (error: any) {
187
- logger.error('Failed to handle vendor commission queue event', {
185
+
186
+ vendorCommissionQueue.push({
188
187
  id,
189
- error: error.message,
188
+ job: { paymentIntentId: paymentIntent.id, retryOnError: true },
190
189
  });
190
+ } catch (error) {
191
+ logger.error('Failed to trigger vendor commission queue', { invoiceId: invoice.id, error });
191
192
  }
192
193
  });
@@ -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';
@@ -271,14 +273,14 @@ async function updateSingleVendorInfo(
271
273
  vendor_id: current[index]?.vendor_id || vendorId,
272
274
  order_id: update.order_id || current[index]?.order_id || '',
273
275
  status: update.status || current[index]?.status || 'pending',
274
- amount: update.amount || current[index]?.amount || '0',
276
+ amount: update.commissionAmount || current[index]?.amount || '0',
275
277
  } as VendorInfo;
276
278
  } else {
277
279
  current.push({
278
280
  vendor_id: vendorId,
279
281
  order_id: update.order_id || '',
280
282
  status: update.status || 'pending',
281
- amount: update.amount || '0',
283
+ amount: update.commissionAmount || '0',
282
284
  ...update,
283
285
  } as VendorInfo);
284
286
  }
@@ -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', {
@@ -1,12 +1,12 @@
1
1
  import { joinURL } from 'ufo';
2
+ import { VendorAuth } from '@blocklet/payment-vendor';
2
3
  import createQueue from '../../libs/queue';
3
- import { getBlockletJson } from '../../libs/util';
4
4
  import { CheckoutSession } from '../../store/models/checkout-session';
5
5
  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({
@@ -99,17 +99,22 @@ export const handleVendorStatusCheck = async (job: VendorStatusCheckJob) => {
99
99
  logger.info('found vendor url', { url, productVendor });
100
100
 
101
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);
102
+ const serverStatusUrl = joinURL(
103
+ url,
104
+ productVendor?.metadata?.mountPoint,
105
+ '/api/vendor/status',
106
+ vendor.order_id
107
+ );
108
+ const { headers } = VendorAuth.signRequestWithHeaders({});
109
+
110
+ const result = await fetch(serverStatusUrl, { headers });
107
111
  const data = await result.json();
112
+
108
113
  vendor.app_url = data?.appUrl || '';
109
114
  }
110
115
  }
111
116
 
112
- const adapter = VendorFulfillmentService.getVendorAdapter(vendor.vendor_key);
117
+ const adapter = await VendorFulfillmentService.getVendorAdapter(vendor.vendor_key);
113
118
  const result = await adapter.checkOrderStatus({ appUrl: vendor.app_url });
114
119
 
115
120
  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),