payment-kit 1.20.7 → 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.
- package/api/src/crons/index.ts +1 -1
- package/api/src/index.ts +2 -2
- package/api/src/libs/{vendor → vendor-util}/adapters/factory.ts +9 -5
- package/api/src/libs/{vendor → vendor-util}/adapters/launcher-adapter.ts +3 -8
- package/api/src/libs/{vendor → vendor-util}/adapters/types.ts +1 -4
- package/api/src/libs/{vendor → vendor-util}/fulfillment.ts +11 -8
- package/api/src/queues/{vendor → vendors}/commission.ts +4 -5
- package/api/src/queues/{vendor → vendors}/fulfillment-coordinator.ts +33 -4
- package/api/src/queues/{vendor → vendors}/fulfillment.ts +2 -2
- package/api/src/queues/{vendor → vendors}/status-check.ts +2 -2
- package/api/src/routes/payment-links.ts +2 -1
- package/api/src/routes/products.ts +1 -0
- package/api/src/routes/vendor.ts +135 -213
- package/api/src/store/migrations/20250911-add-vendor-type.ts +26 -0
- package/api/src/store/models/product-vendor.ts +6 -24
- package/api/src/store/models/product.ts +1 -0
- package/blocklet.yml +1 -1
- package/doc/vendor_fulfillment_system.md +1 -1
- package/package.json +23 -22
- 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 +13 -3
- package/src/locales/zh.tsx +13 -3
- package/src/pages/admin/products/links/create.tsx +2 -0
- package/src/pages/admin/products/vendors/create.tsx +108 -194
- 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';
|
|
@@ -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
|
|
|
@@ -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
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
62
|
-
const commissionType = vendorConfig.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
|
|
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
|
|
204
|
-
vendorKey: vendorConfig.vendor_key
|
|
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
|
-
|
|
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
|
|
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
|
};
|
|
@@ -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
|
|
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:
|
|
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', {
|
|
@@ -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
|
-
|
|
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),
|