payment-kit 1.20.14 → 1.20.16
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/libs/discount/coupon.ts +34 -7
- package/api/src/libs/discount/discount.ts +83 -1
- package/api/src/libs/env.ts +1 -0
- package/api/src/libs/payment.ts +1 -1
- package/api/src/libs/url.ts +3 -3
- package/api/src/libs/vendor-util/adapters/launcher-adapter.ts +1 -1
- package/api/src/libs/vendor-util/adapters/types.ts +2 -3
- package/api/src/libs/vendor-util/fulfillment.ts +16 -30
- package/api/src/queues/checkout-session.ts +3 -0
- package/api/src/queues/payment.ts +5 -0
- package/api/src/queues/vendors/commission.ts +32 -42
- package/api/src/queues/vendors/fulfillment-coordinator.ts +68 -60
- package/api/src/queues/vendors/fulfillment.ts +5 -5
- package/api/src/queues/vendors/return-processor.ts +0 -1
- package/api/src/queues/vendors/status-check.ts +2 -2
- package/api/src/routes/checkout-sessions.ts +2 -1
- package/api/src/routes/connect/change-plan.ts +23 -12
- package/api/src/routes/connect/collect.ts +1 -0
- package/api/src/routes/connect/delegation.ts +1 -0
- package/api/src/routes/connect/shared.ts +11 -2
- package/api/src/routes/meter-events.ts +8 -1
- package/api/src/routes/products.ts +1 -0
- package/api/src/routes/vendor.ts +13 -4
- package/api/src/store/migrations/20250923-add-discount-confirmed.ts +21 -0
- package/api/src/store/models/checkout-session.ts +23 -0
- package/api/src/store/models/discount.ts +18 -7
- package/api/src/store/models/index.ts +8 -1
- package/blocklet.yml +7 -1
- package/package.json +17 -17
- package/doc/vendor_fulfillment_system.md +0 -929
|
@@ -1,10 +1,19 @@
|
|
|
1
|
-
import { BN } from '@ocap/util';
|
|
1
|
+
import { BN, fromUnitToToken } from '@ocap/util';
|
|
2
2
|
|
|
3
3
|
import pick from 'lodash/pick';
|
|
4
|
-
import {
|
|
5
|
-
|
|
4
|
+
import {
|
|
5
|
+
Coupon,
|
|
6
|
+
Customer,
|
|
7
|
+
Discount,
|
|
8
|
+
PromotionCode,
|
|
9
|
+
Subscription,
|
|
10
|
+
CheckoutSession,
|
|
11
|
+
PaymentCurrency,
|
|
12
|
+
} from '../../store/models';
|
|
13
|
+
import type { TLineItemExpanded } from '../../store/models';
|
|
6
14
|
import logger from '../logger';
|
|
7
15
|
import { emitAsync } from '../event';
|
|
16
|
+
import { formatNumber } from '../util';
|
|
8
17
|
|
|
9
18
|
export function validCoupon(coupon: Coupon, lineItems?: TLineItemExpanded[]) {
|
|
10
19
|
if (!coupon.valid) {
|
|
@@ -75,13 +84,20 @@ export async function validPromotionCode(
|
|
|
75
84
|
? promotionCode.restrictions?.minimum_amount
|
|
76
85
|
: promotionCode.restrictions?.currency_options?.[currencyId]?.minimum_amount;
|
|
77
86
|
|
|
87
|
+
const currency = await PaymentCurrency.findByPk(currencyId);
|
|
78
88
|
if (minimumAmount) {
|
|
79
89
|
const amountBN = new BN(amount);
|
|
80
90
|
const minimumBN = new BN(minimumAmount);
|
|
81
91
|
if (amountBN.lt(minimumBN)) {
|
|
92
|
+
if (!currency) {
|
|
93
|
+
return {
|
|
94
|
+
valid: false,
|
|
95
|
+
reason: 'This promotion requires a minimum purchase amount. Please add more items to your cart.',
|
|
96
|
+
};
|
|
97
|
+
}
|
|
82
98
|
return {
|
|
83
99
|
valid: false,
|
|
84
|
-
reason:
|
|
100
|
+
reason: `This promotion requires a minimum purchase amount of ${formatNumber(fromUnitToToken(minimumBN, currency?.decimal || 2))} ${currency?.symbol}. Please add more items to your cart.`,
|
|
85
101
|
};
|
|
86
102
|
}
|
|
87
103
|
}
|
|
@@ -296,7 +312,7 @@ async function processSubscriptionDiscount({
|
|
|
296
312
|
baseDiscountData: any;
|
|
297
313
|
existingDiscountMap: Map<string, any>;
|
|
298
314
|
checkoutSessionId: string;
|
|
299
|
-
}): Promise<{ discountRecord:
|
|
315
|
+
}): Promise<{ discountRecord: Discount; shouldUpdateUsage: boolean }> {
|
|
300
316
|
try {
|
|
301
317
|
const existingDiscount = existingDiscountMap.get(subscriptionId);
|
|
302
318
|
|
|
@@ -342,6 +358,7 @@ async function processSubscriptionDiscount({
|
|
|
342
358
|
const newDiscount = await Discount.create({
|
|
343
359
|
...baseDiscountData,
|
|
344
360
|
subscription_id: subscriptionId,
|
|
361
|
+
confirmed: false,
|
|
345
362
|
metadata: {
|
|
346
363
|
...baseDiscountData.metadata,
|
|
347
364
|
subscription_id: subscriptionId,
|
|
@@ -375,7 +392,7 @@ async function processNonSubscriptionDiscount(
|
|
|
375
392
|
baseDiscountData: any,
|
|
376
393
|
existingDiscountMap: Map<string, any>,
|
|
377
394
|
checkoutSessionId: string
|
|
378
|
-
): Promise<{ discountRecord:
|
|
395
|
+
): Promise<{ discountRecord: Discount; shouldUpdateUsage: boolean }> {
|
|
379
396
|
try {
|
|
380
397
|
const existingDiscount = existingDiscountMap.get('no_subscription');
|
|
381
398
|
|
|
@@ -412,7 +429,10 @@ async function processNonSubscriptionDiscount(
|
|
|
412
429
|
}
|
|
413
430
|
|
|
414
431
|
// Create new discount record
|
|
415
|
-
const newDiscount = await Discount.create(
|
|
432
|
+
const newDiscount = await Discount.create({
|
|
433
|
+
...baseDiscountData,
|
|
434
|
+
confirmed: false,
|
|
435
|
+
});
|
|
416
436
|
|
|
417
437
|
// Lock coupon and promotion code when discount is created
|
|
418
438
|
await lockDiscountResources(baseDiscountData.coupon_id, baseDiscountData.promotion_code_id);
|
|
@@ -710,6 +730,13 @@ export async function createDiscountRecordsForCheckout({
|
|
|
710
730
|
updatedPromotionCodes,
|
|
711
731
|
checkoutSessionId: checkoutSession.id,
|
|
712
732
|
});
|
|
733
|
+
await Promise.all(
|
|
734
|
+
discountRecords.map(async (discountRecord) => {
|
|
735
|
+
await discountRecord.update({
|
|
736
|
+
confirmed: true,
|
|
737
|
+
});
|
|
738
|
+
})
|
|
739
|
+
);
|
|
713
740
|
} catch (error) {
|
|
714
741
|
logger.error('Failed to update usage counts, but continuing', {
|
|
715
742
|
couponId: coupon.id,
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { BN } from '@ocap/util';
|
|
2
|
-
import { Coupon, Discount, PaymentCurrency, PromotionCode } from '../../store/models';
|
|
2
|
+
import { Coupon, Discount, PaymentCurrency, PromotionCode, CheckoutSession } from '../../store/models';
|
|
3
3
|
import type { TLineItemExpanded } from '../../store/models';
|
|
4
4
|
import { getPriceUintAmountByCurrency } from '../price';
|
|
5
5
|
import { validCoupon, checkPromotionCodeEligibility, calculateDiscountAmount } from './coupon';
|
|
6
|
+
import { emitAsync } from '../event';
|
|
6
7
|
import logger from '../logger';
|
|
7
8
|
|
|
8
9
|
const getItemsTotalAmount = (lineItems: TLineItemExpanded[], currencyId: string, options?: { trialing?: boolean }) => {
|
|
@@ -347,3 +348,84 @@ export async function applySubscriptionDiscount({
|
|
|
347
348
|
|
|
348
349
|
return result;
|
|
349
350
|
}
|
|
351
|
+
|
|
352
|
+
async function rollbackUsageForResource<T extends Coupon | PromotionCode>(
|
|
353
|
+
resource: T,
|
|
354
|
+
resourceType: 'coupon' | 'promotion-code'
|
|
355
|
+
): Promise<void> {
|
|
356
|
+
const currentUsage = resource.times_redeemed || 0;
|
|
357
|
+
if (currentUsage <= 0) return;
|
|
358
|
+
|
|
359
|
+
if (resourceType === 'coupon') {
|
|
360
|
+
await (resource as Coupon).update({
|
|
361
|
+
times_redeemed: currentUsage - 1,
|
|
362
|
+
valid: true,
|
|
363
|
+
});
|
|
364
|
+
} else {
|
|
365
|
+
await (resource as PromotionCode).update({
|
|
366
|
+
times_redeemed: currentUsage - 1,
|
|
367
|
+
active: true,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
logger.info('Updated usage for resource', {
|
|
372
|
+
resourceId: resource.id,
|
|
373
|
+
resourceType,
|
|
374
|
+
currentUsage: currentUsage - 1,
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
emitAsync('discount-status.queued', resource, resourceType, true);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
export async function rollbackDiscountUsageForCheckoutSession(checkoutSessionId: string): Promise<void> {
|
|
381
|
+
try {
|
|
382
|
+
const checkoutSession = await CheckoutSession.findByPk(checkoutSessionId);
|
|
383
|
+
if (!checkoutSession) {
|
|
384
|
+
logger.error('Checkout session not found', { checkoutSessionId });
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
if (checkoutSession.status === 'complete') {
|
|
389
|
+
logger.error('Cannot rollback completed session', { checkoutSessionId });
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const discounts = (await Discount.findAll({
|
|
394
|
+
where: { checkout_session_id: checkoutSessionId },
|
|
395
|
+
include: [
|
|
396
|
+
{ model: Coupon, as: 'coupon' },
|
|
397
|
+
{ model: PromotionCode, as: 'promotionCode', required: false },
|
|
398
|
+
],
|
|
399
|
+
})) as (Discount & { coupon: Coupon; promotionCode?: PromotionCode })[];
|
|
400
|
+
|
|
401
|
+
if (discounts.length === 0) {
|
|
402
|
+
logger.info('No discounts found for checkout session', { checkoutSessionId });
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const uniqueCoupons = new Map<string, Coupon>();
|
|
407
|
+
const uniquePromotionCodes = new Map<string, PromotionCode>();
|
|
408
|
+
|
|
409
|
+
discounts.forEach((discount) => {
|
|
410
|
+
if (discount.confirmed) {
|
|
411
|
+
if (discount.coupon?.id && !uniqueCoupons.has(discount.coupon.id)) {
|
|
412
|
+
uniqueCoupons.set(discount.coupon.id, discount.coupon);
|
|
413
|
+
}
|
|
414
|
+
if (discount.promotionCode?.id && !uniquePromotionCodes.has(discount.promotionCode.id)) {
|
|
415
|
+
uniquePromotionCodes.set(discount.promotionCode.id, discount.promotionCode);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
const rollbackPromises = [
|
|
421
|
+
...Array.from(uniquePromotionCodes.values()).map((pc) => rollbackUsageForResource(pc, 'promotion-code')),
|
|
422
|
+
...Array.from(uniqueCoupons.values()).map((c) => rollbackUsageForResource(c, 'coupon')),
|
|
423
|
+
];
|
|
424
|
+
|
|
425
|
+
await Promise.all(rollbackPromises);
|
|
426
|
+
|
|
427
|
+
await Promise.all(discounts.map((discount) => discount.destroy()));
|
|
428
|
+
} catch (error) {
|
|
429
|
+
logger.error('Discount rollback failed', { checkoutSessionId, error: error.message });
|
|
430
|
+
}
|
|
431
|
+
}
|
package/api/src/libs/env.ts
CHANGED
|
@@ -21,6 +21,7 @@ export const vendorTimeoutMinutes: number = process.env.VENDOR_TIMEOUT_MINUTES
|
|
|
21
21
|
: 10; // 默认 10 分钟超时
|
|
22
22
|
|
|
23
23
|
export const shortUrlApiKey: string = process.env.SHORT_URL_API_KEY || '';
|
|
24
|
+
export const shortUrlDomain: string = process.env.SHORT_URL_DOMAIN || 's.abtnet.io';
|
|
24
25
|
|
|
25
26
|
// sequelize 配置相关
|
|
26
27
|
export const sequelizeOptionsPoolMin: number = process.env.SEQUELIZE_OPTIONS_POOL_MIN
|
package/api/src/libs/payment.ts
CHANGED
|
@@ -368,7 +368,7 @@ export function isCreditSufficientForPayment(args: {
|
|
|
368
368
|
const balance = tokens[paymentCurrency.id] || '0';
|
|
369
369
|
|
|
370
370
|
if (amount === '0') {
|
|
371
|
-
return { sufficient:
|
|
371
|
+
return { sufficient: true, balance };
|
|
372
372
|
}
|
|
373
373
|
|
|
374
374
|
if (new BN(balance).lt(new BN(amount))) {
|
package/api/src/libs/url.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import logger from './logger';
|
|
2
|
-
import { shortUrlApiKey } from './env';
|
|
2
|
+
import { shortUrlApiKey, shortUrlDomain } from './env';
|
|
3
3
|
|
|
4
4
|
interface ShortUrlResponse {
|
|
5
5
|
shortUrl: string;
|
|
@@ -43,14 +43,14 @@ export async function formatToShortUrl({
|
|
|
43
43
|
maxVisits,
|
|
44
44
|
tags: [],
|
|
45
45
|
shortCodeLength: 8,
|
|
46
|
-
domain:
|
|
46
|
+
domain: shortUrlDomain,
|
|
47
47
|
findIfExists: true,
|
|
48
48
|
validateUrl: true,
|
|
49
49
|
forwardQuery: true,
|
|
50
50
|
crawlable: true,
|
|
51
51
|
};
|
|
52
52
|
|
|
53
|
-
const response = await fetch(
|
|
53
|
+
const response = await fetch(`https://${shortUrlDomain}/rest/v3/short-urls`, {
|
|
54
54
|
method: 'POST',
|
|
55
55
|
headers: {
|
|
56
56
|
'Content-Type': 'application/json',
|
|
@@ -89,7 +89,7 @@ export class LauncherAdapter implements VendorAdapter {
|
|
|
89
89
|
amount: params.amount,
|
|
90
90
|
currency: params.currency,
|
|
91
91
|
quantity: params.quantity,
|
|
92
|
-
|
|
92
|
+
invoiceId: params.invoiceId,
|
|
93
93
|
customParams: params.customParams,
|
|
94
94
|
},
|
|
95
95
|
installationInfo: {
|
|
@@ -14,7 +14,7 @@ export interface FulfillOrderParams {
|
|
|
14
14
|
productCode: string;
|
|
15
15
|
customerId: string;
|
|
16
16
|
quantity: number;
|
|
17
|
-
|
|
17
|
+
invoiceId: string;
|
|
18
18
|
amount: string;
|
|
19
19
|
currency: string;
|
|
20
20
|
|
|
@@ -38,7 +38,7 @@ export interface OrderDetails {
|
|
|
38
38
|
amount: string;
|
|
39
39
|
currency: string;
|
|
40
40
|
quantity: number;
|
|
41
|
-
|
|
41
|
+
invoiceId: string;
|
|
42
42
|
customParams?: Record<string, any>;
|
|
43
43
|
}
|
|
44
44
|
|
|
@@ -66,7 +66,6 @@ export interface ReturnRequestParams {
|
|
|
66
66
|
orderId: string;
|
|
67
67
|
vendorOrderId?: string;
|
|
68
68
|
reason: string;
|
|
69
|
-
paymentIntentId: string;
|
|
70
69
|
customParams?: Record<string, any>;
|
|
71
70
|
}
|
|
72
71
|
|
|
@@ -43,7 +43,7 @@ export class VendorFulfillmentService {
|
|
|
43
43
|
checkoutSessionId: string;
|
|
44
44
|
amount_total: string;
|
|
45
45
|
customer_id: string;
|
|
46
|
-
|
|
46
|
+
invoiceId: string;
|
|
47
47
|
currency_id: string;
|
|
48
48
|
customer_did: string;
|
|
49
49
|
},
|
|
@@ -75,7 +75,7 @@ export class VendorFulfillmentService {
|
|
|
75
75
|
productCode: vendorConfig.vendor_id,
|
|
76
76
|
customerId: orderInfo.customer_id || '',
|
|
77
77
|
quantity: 1,
|
|
78
|
-
|
|
78
|
+
invoiceId: orderInfo.invoiceId,
|
|
79
79
|
amount: orderInfo.amount_total,
|
|
80
80
|
currency: orderInfo.currency_id,
|
|
81
81
|
|
|
@@ -124,34 +124,18 @@ export class VendorFulfillmentService {
|
|
|
124
124
|
}
|
|
125
125
|
}
|
|
126
126
|
|
|
127
|
-
static async createVendorPayouts(
|
|
128
|
-
checkoutSessionId: string,
|
|
129
|
-
fulfillmentResults?: VendorFulfillmentResult[]
|
|
130
|
-
): Promise<void> {
|
|
127
|
+
static async createVendorPayouts(checkoutSession: CheckoutSession, paymentIntent: PaymentIntent): Promise<void> {
|
|
131
128
|
try {
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
throw new Error(`CheckoutSession not found: ${checkoutSessionId}`);
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
let paymentMethodId = '';
|
|
138
|
-
if (checkoutSession.payment_intent_id) {
|
|
139
|
-
const paymentIntent = await PaymentIntent.findByPk(checkoutSession.payment_intent_id);
|
|
140
|
-
if (paymentIntent) {
|
|
141
|
-
paymentMethodId = paymentIntent.payment_method_id;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
129
|
+
const paymentMethodId = paymentIntent.payment_method_id;
|
|
130
|
+
const paymentIntentId = paymentIntent.id;
|
|
144
131
|
|
|
145
132
|
// If fulfillmentResults not provided, calculate commission info
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
commissionAmount: vendorInfo.commissionAmount,
|
|
153
|
-
})) as VendorFulfillmentResult[];
|
|
154
|
-
}
|
|
133
|
+
const commissionData = checkoutSession.vendor_info?.map((vendorInfo: any) => ({
|
|
134
|
+
vendorId: vendorInfo.vendor_id,
|
|
135
|
+
orderId: vendorInfo.order_id,
|
|
136
|
+
status: vendorInfo.status,
|
|
137
|
+
commissionAmount: vendorInfo.commissionAmount,
|
|
138
|
+
})) as VendorFulfillmentResult[];
|
|
155
139
|
|
|
156
140
|
const payoutPromises = commissionData
|
|
157
141
|
.filter((result) => result.status !== 'failed' && new BN(result.commissionAmount).gt(new BN('0')))
|
|
@@ -170,7 +154,7 @@ export class VendorFulfillmentService {
|
|
|
170
154
|
amount: result.commissionAmount,
|
|
171
155
|
currency_id: checkoutSession.currency_id,
|
|
172
156
|
customer_id: checkoutSession.customer_id || '',
|
|
173
|
-
payment_intent_id:
|
|
157
|
+
payment_intent_id: paymentIntentId,
|
|
174
158
|
payment_method_id: paymentMethodId,
|
|
175
159
|
status: paymentMethod?.type === 'stripe' ? 'deferred' : 'pending',
|
|
176
160
|
attempt_count: 0,
|
|
@@ -187,12 +171,14 @@ export class VendorFulfillmentService {
|
|
|
187
171
|
await Promise.all(payoutPromises);
|
|
188
172
|
|
|
189
173
|
logger.info('Vendor payouts created', {
|
|
190
|
-
checkoutSessionId,
|
|
174
|
+
checkoutSessionId: checkoutSession.id,
|
|
175
|
+
paymentIntentId: paymentIntent.id,
|
|
191
176
|
payoutCount: commissionData.filter((r) => r.status !== 'failed').length,
|
|
192
177
|
});
|
|
193
178
|
} catch (error: any) {
|
|
194
179
|
logger.error('Failed to create vendor payouts', {
|
|
195
|
-
checkoutSessionId,
|
|
180
|
+
checkoutSessionId: checkoutSession.id,
|
|
181
|
+
paymentIntentId: paymentIntent.id,
|
|
196
182
|
error: error.message,
|
|
197
183
|
});
|
|
198
184
|
throw error;
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
SubscriptionItem,
|
|
22
22
|
} from '../store/models';
|
|
23
23
|
import { getCheckoutSessionSubscriptionIds } from '../libs/session';
|
|
24
|
+
import { rollbackDiscountUsageForCheckoutSession } from '../libs/discount/discount';
|
|
24
25
|
|
|
25
26
|
type CheckoutSessionJob = {
|
|
26
27
|
id: string;
|
|
@@ -240,6 +241,8 @@ events.on('checkout.session.expired', async (checkoutSession: CheckoutSession) =
|
|
|
240
241
|
);
|
|
241
242
|
}
|
|
242
243
|
|
|
244
|
+
await rollbackDiscountUsageForCheckoutSession(checkoutSession.id);
|
|
245
|
+
|
|
243
246
|
// update price lock status
|
|
244
247
|
for (const item of checkoutSession.line_items) {
|
|
245
248
|
const price = await Price.findByPk(item.price_id);
|
|
@@ -914,6 +914,11 @@ export const handlePayment = async (job: PaymentJob) => {
|
|
|
914
914
|
return;
|
|
915
915
|
}
|
|
916
916
|
|
|
917
|
+
if (paymentIntent.amount === '0') {
|
|
918
|
+
await handlePaymentSucceed(paymentIntent);
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
|
|
917
922
|
await paymentIntent.update({ status: 'processing', last_payment_error: null });
|
|
918
923
|
|
|
919
924
|
if (paymentMethod.type === 'arcblock') {
|
|
@@ -1,29 +1,26 @@
|
|
|
1
1
|
import { events } from '../../libs/event';
|
|
2
2
|
import logger from '../../libs/logger';
|
|
3
3
|
import createQueue from '../../libs/queue';
|
|
4
|
+
import { Invoice } from '../../store/models';
|
|
4
5
|
import { CheckoutSession } from '../../store/models/checkout-session';
|
|
5
6
|
import { PaymentIntent } from '../../store/models/payment-intent';
|
|
6
|
-
import { Product } from '../../store/models/product';
|
|
7
7
|
import { Price } from '../../store/models/price';
|
|
8
|
+
import { Product } from '../../store/models/product';
|
|
8
9
|
import { depositVaultQueue } from '../payment';
|
|
9
10
|
import { startVendorFulfillment, triggerCommissionProcess, triggerCoordinatorCheck } from './fulfillment-coordinator';
|
|
10
11
|
|
|
11
12
|
type VendorCommissionJob = {
|
|
12
|
-
|
|
13
|
+
invoiceId: string;
|
|
13
14
|
retryOnError?: boolean;
|
|
14
15
|
};
|
|
15
16
|
|
|
16
|
-
async function checkIfPaymentIntentHasVendors(
|
|
17
|
-
paymentIntent: PaymentIntent,
|
|
18
|
-
checkoutSession: CheckoutSession
|
|
19
|
-
): Promise<boolean> {
|
|
17
|
+
async function checkIfPaymentIntentHasVendors(checkoutSession: CheckoutSession): Promise<boolean> {
|
|
20
18
|
try {
|
|
21
19
|
// Extract price_ids from line_items, then find corresponding product_ids
|
|
22
20
|
const priceIds = checkoutSession.line_items.map((item: any) => item.price_id).filter(Boolean);
|
|
23
21
|
|
|
24
22
|
if (priceIds.length === 0) {
|
|
25
23
|
logger.warn('No price IDs found in checkout session line items', {
|
|
26
|
-
paymentIntentId: paymentIntent.id,
|
|
27
24
|
checkoutSessionId: checkoutSession.id,
|
|
28
25
|
});
|
|
29
26
|
return false;
|
|
@@ -39,7 +36,6 @@ async function checkIfPaymentIntentHasVendors(
|
|
|
39
36
|
|
|
40
37
|
if (productIds.length === 0) {
|
|
41
38
|
logger.warn('No product IDs found from prices', {
|
|
42
|
-
paymentIntentId: paymentIntent.id,
|
|
43
39
|
checkoutSessionId: checkoutSession.id,
|
|
44
40
|
priceIds,
|
|
45
41
|
});
|
|
@@ -57,28 +53,28 @@ async function checkIfPaymentIntentHasVendors(
|
|
|
57
53
|
return hasVendorConfig;
|
|
58
54
|
} catch (error: any) {
|
|
59
55
|
logger.error('Failed to check vendor configuration', {
|
|
60
|
-
paymentIntentId: paymentIntent.id,
|
|
61
56
|
error,
|
|
62
57
|
});
|
|
63
58
|
return false;
|
|
64
59
|
}
|
|
65
60
|
}
|
|
66
61
|
|
|
67
|
-
async function executeDirectDepositVault(
|
|
68
|
-
const
|
|
62
|
+
async function executeDirectDepositVault(invoice: Invoice): Promise<void> {
|
|
63
|
+
const currencyId = invoice.currency_id;
|
|
64
|
+
const exist = await depositVaultQueue.get(`deposit-vault-${currencyId}`);
|
|
69
65
|
if (!exist) {
|
|
70
66
|
depositVaultQueue.push({
|
|
71
|
-
id: `deposit-vault-${
|
|
72
|
-
job: { currencyId
|
|
67
|
+
id: `deposit-vault-${currencyId}`,
|
|
68
|
+
job: { currencyId },
|
|
73
69
|
});
|
|
74
70
|
logger.info('Deposit vault job queued', {
|
|
75
|
-
paymentIntentId:
|
|
76
|
-
currencyId
|
|
71
|
+
paymentIntentId: invoice.payment_intent_id,
|
|
72
|
+
currencyId,
|
|
77
73
|
});
|
|
78
74
|
} else {
|
|
79
75
|
logger.info('Deposit vault job already exists', {
|
|
80
|
-
paymentIntentId:
|
|
81
|
-
currencyId
|
|
76
|
+
paymentIntentId: invoice.payment_intent_id,
|
|
77
|
+
currencyId,
|
|
82
78
|
});
|
|
83
79
|
}
|
|
84
80
|
}
|
|
@@ -88,53 +84,53 @@ export const handleVendorCommission = async (job: VendorCommissionJob) => {
|
|
|
88
84
|
|
|
89
85
|
let checkoutSession: CheckoutSession | null = null;
|
|
90
86
|
try {
|
|
91
|
-
const
|
|
92
|
-
if (!
|
|
93
|
-
logger.warn('
|
|
87
|
+
const invoice = await Invoice.findByPk(job.invoiceId);
|
|
88
|
+
if (!invoice) {
|
|
89
|
+
logger.warn('invoice not found', { id: job.invoiceId });
|
|
94
90
|
return;
|
|
95
91
|
}
|
|
96
92
|
|
|
97
93
|
// Find CheckoutSession through PaymentIntent
|
|
98
|
-
checkoutSession = await CheckoutSession.
|
|
94
|
+
checkoutSession = await CheckoutSession.findByInvoiceId(invoice.id);
|
|
99
95
|
if (!checkoutSession) {
|
|
100
|
-
await executeDirectDepositVault(
|
|
96
|
+
await executeDirectDepositVault(invoice);
|
|
101
97
|
return;
|
|
102
98
|
}
|
|
103
99
|
|
|
104
|
-
const hasVendorConfig = await checkIfPaymentIntentHasVendors(
|
|
100
|
+
const hasVendorConfig = await checkIfPaymentIntentHasVendors(checkoutSession);
|
|
105
101
|
if (!hasVendorConfig) {
|
|
106
|
-
await executeDirectDepositVault(
|
|
102
|
+
await executeDirectDepositVault(invoice);
|
|
107
103
|
return;
|
|
108
104
|
}
|
|
109
105
|
|
|
110
106
|
logger.info('Vendor configuration found, starting fulfillment process', {
|
|
111
|
-
|
|
107
|
+
invoiceId: invoice.id,
|
|
112
108
|
});
|
|
113
109
|
|
|
114
110
|
if (checkoutSession.fulfillment_status === 'completed') {
|
|
115
111
|
logger.info('CheckoutSession already completed, directly trigger commission process', {
|
|
116
112
|
checkoutSessionId: checkoutSession.id,
|
|
117
113
|
});
|
|
118
|
-
await triggerCommissionProcess(checkoutSession.id,
|
|
114
|
+
await triggerCommissionProcess(checkoutSession.id, invoice.id);
|
|
119
115
|
return;
|
|
120
116
|
}
|
|
121
117
|
|
|
122
|
-
await startVendorFulfillment(checkoutSession.id,
|
|
118
|
+
await startVendorFulfillment(checkoutSession.id, invoice.id);
|
|
123
119
|
} catch (error: any) {
|
|
124
120
|
logger.error('Vendor commission decision failed, fallback to direct deposit vault', {
|
|
125
|
-
|
|
121
|
+
invoiceId: job.invoiceId,
|
|
126
122
|
error,
|
|
127
123
|
});
|
|
128
124
|
|
|
129
125
|
if (!checkoutSession) {
|
|
130
126
|
logger.error('CheckoutSession not found via any method[handleVendorCommission]', {
|
|
131
|
-
|
|
127
|
+
invoiceId: job.invoiceId,
|
|
132
128
|
});
|
|
133
129
|
return;
|
|
134
130
|
}
|
|
135
131
|
|
|
136
132
|
try {
|
|
137
|
-
triggerCoordinatorCheck(checkoutSession.id, job.
|
|
133
|
+
triggerCoordinatorCheck(checkoutSession.id, job.invoiceId, 'vendor_commission_decision_failed');
|
|
138
134
|
} catch (err: any) {
|
|
139
135
|
logger.error('Failed to trigger coordinator check[handleVendorCommission]', { error: err });
|
|
140
136
|
}
|
|
@@ -160,23 +156,17 @@ export const startVendorCommissionQueue = async () => {
|
|
|
160
156
|
});
|
|
161
157
|
|
|
162
158
|
payments.forEach(async (x) => {
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
159
|
+
const id = `vendor-commission-${x.invoice_id}`;
|
|
160
|
+
const exist = await vendorCommissionQueue.get(id);
|
|
161
|
+
if (!exist && x.invoice_id) {
|
|
162
|
+
vendorCommissionQueue.push({ id, job: { invoiceId: x.invoice_id, retryOnError: true } });
|
|
166
163
|
}
|
|
167
164
|
});
|
|
168
165
|
};
|
|
169
166
|
|
|
170
167
|
events.on('invoice.paid', async (invoice) => {
|
|
171
168
|
try {
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
if (!paymentIntent) {
|
|
175
|
-
logger.warn('PaymentIntent not found', { id: invoice.id });
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
const id = `vendor-commission-${paymentIntent.id}`;
|
|
169
|
+
const id = `vendor-commission-${invoice.id}`;
|
|
180
170
|
const exist = await vendorCommissionQueue.get(id);
|
|
181
171
|
if (exist) {
|
|
182
172
|
logger.info('Vendor commission job already exists, skipping', { id });
|
|
@@ -185,7 +175,7 @@ events.on('invoice.paid', async (invoice) => {
|
|
|
185
175
|
|
|
186
176
|
vendorCommissionQueue.push({
|
|
187
177
|
id,
|
|
188
|
-
job: {
|
|
178
|
+
job: { invoiceId: invoice.id, retryOnError: true },
|
|
189
179
|
});
|
|
190
180
|
} catch (error) {
|
|
191
181
|
logger.error('Failed to trigger vendor commission queue', { invoiceId: invoice.id, error });
|