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.
- package/api/src/crons/index.ts +1 -1
- package/api/src/index.ts +2 -2
- package/api/src/integrations/stripe/handlers/index.ts +10 -2
- package/api/src/libs/{vendor → vendor-util}/adapters/factory.ts +9 -5
- package/api/src/libs/{vendor → vendor-util}/adapters/launcher-adapter.ts +20 -18
- package/api/src/libs/{vendor → vendor-util}/adapters/types.ts +1 -4
- package/api/src/libs/{vendor → vendor-util}/fulfillment.ts +24 -127
- package/api/src/queues/payment.ts +1 -5
- package/api/src/queues/{vendor → vendors}/commission.ts +19 -18
- package/api/src/queues/{vendor → vendors}/fulfillment-coordinator.ts +35 -6
- package/api/src/queues/{vendor → vendors}/fulfillment.ts +2 -2
- package/api/src/queues/{vendor → vendors}/status-check.ts +13 -8
- package/api/src/routes/payment-links.ts +2 -1
- package/api/src/routes/products.ts +1 -0
- package/api/src/routes/vendor.ts +157 -216
- package/api/src/store/migrations/20250911-add-vendor-type.ts +26 -0
- package/api/src/store/migrations/20250916-add-vendor-did.ts +20 -0
- package/api/src/store/models/payout.ts +2 -2
- package/api/src/store/models/product-vendor.ts +11 -24
- package/api/src/store/models/product.ts +2 -0
- package/blocklet.yml +1 -1
- package/doc/vendor_fulfillment_system.md +1 -1
- package/package.json +5 -5
- package/src/components/metadata/form.tsx +12 -19
- package/src/components/payment-link/before-pay.tsx +40 -0
- package/src/components/product/vendor-config.tsx +4 -11
- package/src/components/subscription/description.tsx +1 -6
- package/src/components/subscription/portal/list.tsx +82 -6
- package/src/components/subscription/vendor-service-list.tsx +128 -0
- package/src/components/vendor/actions.tsx +1 -33
- package/src/locales/en.tsx +16 -3
- package/src/locales/zh.tsx +18 -5
- package/src/pages/admin/products/links/create.tsx +2 -0
- package/src/pages/admin/products/vendors/create.tsx +140 -190
- package/src/pages/admin/products/vendors/index.tsx +14 -22
- package/src/pages/customer/subscription/detail.tsx +26 -11
package/api/src/crons/index.ts
CHANGED
|
@@ -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/
|
|
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/
|
|
38
|
-
import { startVendorFulfillmentQueue } from './queues/
|
|
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
|
|
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(
|
|
24
|
-
const
|
|
25
|
-
|
|
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
|
|
22
|
-
|
|
23
|
-
|
|
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(
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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(
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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 {
|
|
8
|
-
import {
|
|
9
|
-
import
|
|
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
|
|
62
|
-
const commissionType = vendorConfig.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:
|
|
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:
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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('
|
|
170
|
+
events.on('invoice.paid', async (invoice) => {
|
|
172
171
|
try {
|
|
173
|
-
const {
|
|
174
|
-
const exist = await vendorCommissionQueue.get(id);
|
|
172
|
+
const paymentIntent = await PaymentIntent.findOne({ where: { invoice_id: invoice.id } });
|
|
175
173
|
|
|
176
|
-
if (!
|
|
177
|
-
logger.
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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
|
-
|
|
187
|
-
|
|
185
|
+
|
|
186
|
+
vendorCommissionQueue.push({
|
|
188
187
|
id,
|
|
189
|
-
|
|
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.
|
|
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.
|
|
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:
|
|
451
|
-
description:
|
|
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
|
|
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
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
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),
|