payment-kit 1.20.13 → 1.20.15
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/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/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 +15 -2
- package/api/src/routes/coupons.ts +7 -0
- package/api/src/routes/credit-grants.ts +8 -1
- package/api/src/routes/credit-transactions.ts +153 -13
- package/api/src/routes/invoices.ts +35 -1
- package/api/src/routes/meter-events.ts +31 -3
- package/api/src/routes/meters.ts +4 -0
- package/api/src/routes/payment-currencies.ts +2 -1
- package/api/src/routes/promotion-codes.ts +2 -2
- package/api/src/routes/subscription-items.ts +4 -0
- package/api/src/routes/vendor.ts +13 -4
- package/api/src/routes/webhook-endpoints.ts +4 -0
- package/api/src/store/migrations/20250919-add-source-data.ts +20 -0
- package/api/src/store/models/checkout-session.ts +23 -0
- package/api/src/store/models/credit-transaction.ts +5 -0
- package/api/src/store/models/meter-event.ts +22 -12
- package/api/src/store/models/types.ts +18 -0
- package/blocklet.yml +1 -1
- package/package.json +5 -5
- package/src/components/customer/credit-overview.tsx +1 -1
- package/src/components/customer/related-credit-grants.tsx +194 -0
- package/src/components/meter/add-usage-dialog.tsx +8 -0
- package/src/components/meter/events-list.tsx +93 -96
- package/src/components/product/form.tsx +0 -1
- package/src/locales/en.tsx +9 -0
- package/src/locales/zh.tsx +9 -0
- package/src/pages/admin/billing/invoices/detail.tsx +21 -2
- package/src/pages/customer/invoice/detail.tsx +11 -2
- package/doc/vendor_fulfillment_system.md +0 -929
|
@@ -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;
|
|
@@ -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 });
|
|
@@ -12,12 +12,13 @@ import { Product } from '../../store/models/product';
|
|
|
12
12
|
import { Refund } from '../../store/models/refund';
|
|
13
13
|
import { sequelize } from '../../store/sequelize';
|
|
14
14
|
import { depositVaultQueue } from '../payment';
|
|
15
|
+
import { Invoice } from '../../store/models';
|
|
15
16
|
|
|
16
17
|
export type VendorInfo = NonNullable<CheckoutSession['vendor_info']>[number];
|
|
17
18
|
|
|
18
19
|
interface CoordinatorJob {
|
|
19
20
|
checkoutSessionId: string;
|
|
20
|
-
|
|
21
|
+
invoiceId: string;
|
|
21
22
|
triggeredBy: string;
|
|
22
23
|
}
|
|
23
24
|
|
|
@@ -28,11 +29,11 @@ export const fulfillmentCoordinatorQueue = createQueue({
|
|
|
28
29
|
onJob: handleFulfillmentCoordination,
|
|
29
30
|
});
|
|
30
31
|
|
|
31
|
-
export async function startVendorFulfillment(checkoutSessionId: string,
|
|
32
|
+
export async function startVendorFulfillment(checkoutSessionId: string, invoiceId: string): Promise<void> {
|
|
32
33
|
try {
|
|
33
34
|
logger.info('Starting vendor fulfillment process', {
|
|
34
35
|
checkoutSessionId,
|
|
35
|
-
|
|
36
|
+
invoiceId,
|
|
36
37
|
});
|
|
37
38
|
|
|
38
39
|
const vendorConfigs = await getVendorConfigurations(checkoutSessionId);
|
|
@@ -43,7 +44,7 @@ export async function startVendorFulfillment(checkoutSessionId: string, paymentI
|
|
|
43
44
|
checkoutSessionId,
|
|
44
45
|
});
|
|
45
46
|
|
|
46
|
-
await triggerCommissionProcess(checkoutSessionId,
|
|
47
|
+
await triggerCommissionProcess(checkoutSessionId, invoiceId);
|
|
47
48
|
return;
|
|
48
49
|
}
|
|
49
50
|
|
|
@@ -65,7 +66,7 @@ export async function startVendorFulfillment(checkoutSessionId: string, paymentI
|
|
|
65
66
|
|
|
66
67
|
events.emit('vendor.fulfillment.queued', vendorFulfillmentJobId, {
|
|
67
68
|
checkoutSessionId,
|
|
68
|
-
|
|
69
|
+
invoiceId,
|
|
69
70
|
vendorId: vendorConfig.vendor_id,
|
|
70
71
|
vendorConfig,
|
|
71
72
|
retryOnError: true,
|
|
@@ -79,7 +80,7 @@ export async function startVendorFulfillment(checkoutSessionId: string, paymentI
|
|
|
79
80
|
} catch (error: any) {
|
|
80
81
|
logger.error('Failed to start vendor fulfillment', {
|
|
81
82
|
checkoutSessionId,
|
|
82
|
-
|
|
83
|
+
invoiceId,
|
|
83
84
|
error: error.message,
|
|
84
85
|
});
|
|
85
86
|
throw error;
|
|
@@ -88,7 +89,7 @@ export async function startVendorFulfillment(checkoutSessionId: string, paymentI
|
|
|
88
89
|
|
|
89
90
|
export async function updateVendorFulfillmentStatus(
|
|
90
91
|
checkoutSessionId: string,
|
|
91
|
-
|
|
92
|
+
invoiceId: string,
|
|
92
93
|
vendorId: string,
|
|
93
94
|
result: 'completed' | 'failed' | 'max_retries_exceeded' | 'return_requested' | 'sent',
|
|
94
95
|
details?: {
|
|
@@ -110,11 +111,11 @@ export async function updateVendorFulfillmentStatus(
|
|
|
110
111
|
lastAttemptAt: new Date().toISOString(),
|
|
111
112
|
});
|
|
112
113
|
|
|
113
|
-
await triggerCoordinatorCheck(checkoutSessionId,
|
|
114
|
+
await triggerCoordinatorCheck(checkoutSessionId, invoiceId, `vendor_${vendorId}_${result}`);
|
|
114
115
|
}
|
|
115
116
|
|
|
116
117
|
export async function handleFulfillmentCoordination(job: CoordinatorJob) {
|
|
117
|
-
const { checkoutSessionId,
|
|
118
|
+
const { checkoutSessionId, invoiceId, triggeredBy } = job;
|
|
118
119
|
|
|
119
120
|
logger.info('Processing fulfillment coordination', {
|
|
120
121
|
checkoutSessionId,
|
|
@@ -129,7 +130,7 @@ export async function handleFulfillmentCoordination(job: CoordinatorJob) {
|
|
|
129
130
|
logger.info('No vendors to coordinate, triggering commission directly', {
|
|
130
131
|
checkoutSessionId,
|
|
131
132
|
});
|
|
132
|
-
await triggerCommissionProcess(checkoutSessionId,
|
|
133
|
+
await triggerCommissionProcess(checkoutSessionId, invoiceId);
|
|
133
134
|
return;
|
|
134
135
|
}
|
|
135
136
|
|
|
@@ -146,7 +147,7 @@ export async function handleFulfillmentCoordination(job: CoordinatorJob) {
|
|
|
146
147
|
successfulVendors: analysis.successfulVendors.length,
|
|
147
148
|
});
|
|
148
149
|
|
|
149
|
-
await triggerCommissionProcess(checkoutSessionId,
|
|
150
|
+
await triggerCommissionProcess(checkoutSessionId, invoiceId);
|
|
150
151
|
} else if (analysis.anyMaxRetriesExceeded || analysis.shouldTimeout || analysis.failedVendors.length > 0) {
|
|
151
152
|
logger.warn('Some vendors failed, initiating full refund', {
|
|
152
153
|
checkoutSessionId,
|
|
@@ -155,7 +156,7 @@ export async function handleFulfillmentCoordination(job: CoordinatorJob) {
|
|
|
155
156
|
timeoutVendors: analysis.shouldTimeout ? 'detected' : 'none',
|
|
156
157
|
});
|
|
157
158
|
|
|
158
|
-
await initiateFullRefund(
|
|
159
|
+
await initiateFullRefund(invoiceId, 'vendor_failure_detected');
|
|
159
160
|
} else {
|
|
160
161
|
logger.info('Some vendors still in progress, waiting for completion', {
|
|
161
162
|
checkoutSessionId,
|
|
@@ -170,7 +171,7 @@ export async function handleFulfillmentCoordination(job: CoordinatorJob) {
|
|
|
170
171
|
error: error.message,
|
|
171
172
|
});
|
|
172
173
|
|
|
173
|
-
await initiateFullRefund(
|
|
174
|
+
await initiateFullRefund(invoiceId, 'coordination_failed');
|
|
174
175
|
}
|
|
175
176
|
}
|
|
176
177
|
|
|
@@ -355,74 +356,94 @@ function analyzeVendorStates(vendorInfo: VendorInfo[], vendorConfigs: any[]) {
|
|
|
355
356
|
};
|
|
356
357
|
}
|
|
357
358
|
|
|
358
|
-
export function triggerCoordinatorCheck(checkoutSessionId: string,
|
|
359
|
+
export function triggerCoordinatorCheck(checkoutSessionId: string, invoiceId: string, triggeredBy: string) {
|
|
359
360
|
const jobId = `coordinator-${checkoutSessionId}-${Date.now()}`;
|
|
360
361
|
|
|
361
362
|
return fulfillmentCoordinatorQueue.push({
|
|
362
363
|
id: jobId,
|
|
363
364
|
job: {
|
|
364
365
|
checkoutSessionId,
|
|
365
|
-
|
|
366
|
+
invoiceId,
|
|
366
367
|
triggeredBy,
|
|
367
368
|
},
|
|
368
369
|
});
|
|
369
370
|
}
|
|
370
371
|
|
|
371
|
-
export async function triggerCommissionProcess(checkoutSessionId: string,
|
|
372
|
-
logger.info('Triggering commission process', {
|
|
373
|
-
|
|
374
|
-
|
|
372
|
+
export async function triggerCommissionProcess(checkoutSessionId: string, invoiceId: string): Promise<void> {
|
|
373
|
+
logger.info('Triggering commission process', { checkoutSessionId });
|
|
374
|
+
|
|
375
|
+
const checkoutSession = await CheckoutSession.findByPk(checkoutSessionId);
|
|
376
|
+
if (!checkoutSession) {
|
|
377
|
+
logger.error('Checkout session not found[triggerCommissionProcess]', { checkoutSessionId });
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
const invoice = await Invoice.findByPk(invoiceId);
|
|
381
|
+
if (!invoice) {
|
|
382
|
+
logger.error('Invoice not found[triggerCommissionProcess]', { invoiceId });
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
const paymentIntent = await PaymentIntent.findOne({
|
|
386
|
+
where: {
|
|
387
|
+
[Op.or]: [{ id: invoice.payment_intent_id || checkoutSession.payment_intent_id }, { invoice_id: invoiceId }],
|
|
388
|
+
},
|
|
375
389
|
});
|
|
376
390
|
|
|
377
|
-
|
|
378
|
-
|
|
391
|
+
if (!paymentIntent) {
|
|
392
|
+
logger.error('Payment intent not found[triggerCommissionProcess]', { checkoutSessionId });
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
await VendorFulfillmentService.createVendorPayouts(checkoutSession, paymentIntent);
|
|
397
|
+
await checkoutSession.update({ fulfillment_status: 'completed' });
|
|
379
398
|
|
|
380
|
-
const paymentIntent = await PaymentIntent.findByPk(paymentIntentId);
|
|
381
399
|
if (paymentIntent) {
|
|
382
400
|
const jobId = `deposit-vault-${paymentIntent.currency_id}`;
|
|
383
401
|
const existingJob = await depositVaultQueue.get(jobId);
|
|
384
402
|
|
|
385
403
|
if (!existingJob) {
|
|
386
|
-
|
|
404
|
+
depositVaultQueue.push({
|
|
387
405
|
id: jobId,
|
|
388
406
|
job: { currencyId: paymentIntent.currency_id },
|
|
389
407
|
});
|
|
390
408
|
}
|
|
409
|
+
} else {
|
|
410
|
+
logger.error('Payment intent not found for invoice', { invoiceId });
|
|
391
411
|
}
|
|
392
412
|
|
|
393
413
|
logger.info('Commission process triggered successfully', {
|
|
394
414
|
checkoutSessionId,
|
|
395
|
-
paymentIntentId,
|
|
396
415
|
});
|
|
397
416
|
}
|
|
398
417
|
|
|
399
|
-
export async function initiateFullRefund(
|
|
418
|
+
export async function initiateFullRefund(invoiceId: string, reason: string): Promise<void> {
|
|
400
419
|
logger.warn('Initiating full refund with compensation', {
|
|
401
|
-
|
|
420
|
+
invoiceId,
|
|
402
421
|
reason,
|
|
403
422
|
});
|
|
404
423
|
|
|
405
424
|
try {
|
|
406
|
-
const paymentIntent = await PaymentIntent.
|
|
407
|
-
const checkoutSession = await CheckoutSession.
|
|
425
|
+
const paymentIntent = await PaymentIntent.findOne({ where: { invoice_id: invoiceId } });
|
|
426
|
+
const checkoutSession = await CheckoutSession.findByInvoiceId(invoiceId);
|
|
408
427
|
|
|
409
428
|
if (!checkoutSession || !paymentIntent) {
|
|
410
429
|
logger.error('Missing data for full refund', {
|
|
411
|
-
|
|
430
|
+
invoiceId,
|
|
431
|
+
paymentIntentId: paymentIntent?.id,
|
|
432
|
+
checkoutSessionId: checkoutSession?.id,
|
|
412
433
|
hasCheckoutSession: !!checkoutSession,
|
|
413
434
|
hasPaymentIntent: !!paymentIntent,
|
|
414
435
|
});
|
|
415
436
|
return;
|
|
416
437
|
}
|
|
417
438
|
|
|
418
|
-
await
|
|
419
|
-
await requestReturnsFromCompletedVendors(checkoutSession
|
|
439
|
+
await checkoutSession.update({ fulfillment_status: 'cancelled' });
|
|
440
|
+
await requestReturnsFromCompletedVendors(checkoutSession, reason);
|
|
420
441
|
|
|
421
442
|
// Calculate remaining amount using the same logic as subscription createProration
|
|
422
443
|
const refunds = await Refund.findAll({
|
|
423
444
|
where: {
|
|
424
445
|
status: { [Op.not]: 'canceled' },
|
|
425
|
-
payment_intent_id:
|
|
446
|
+
payment_intent_id: paymentIntent.id,
|
|
426
447
|
type: 'refund',
|
|
427
448
|
},
|
|
428
449
|
});
|
|
@@ -437,7 +458,7 @@ export async function initiateFullRefund(paymentIntentId: string, reason: string
|
|
|
437
458
|
// If no remaining amount or already fully refunded, skip
|
|
438
459
|
if (new BN(remaining).lte(new BN('0'))) {
|
|
439
460
|
logger.info('Payment already fully refunded, skipping', {
|
|
440
|
-
|
|
461
|
+
invoiceId,
|
|
441
462
|
paymentAmount: paymentIntent.amount,
|
|
442
463
|
totalRefundAmount: refundAmount.toString(),
|
|
443
464
|
remaining,
|
|
@@ -478,52 +499,41 @@ export async function initiateFullRefund(paymentIntentId: string, reason: string
|
|
|
478
499
|
logger.info('Full refund created, triggering refund processing', {
|
|
479
500
|
refundId: refund.id,
|
|
480
501
|
checkoutSessionId: checkoutSession.id,
|
|
481
|
-
|
|
502
|
+
invoiceId,
|
|
482
503
|
amount: refund.amount,
|
|
483
504
|
reason,
|
|
484
505
|
});
|
|
485
506
|
} catch (error: any) {
|
|
486
507
|
logger.error('Failed to create full refund', {
|
|
487
|
-
|
|
508
|
+
invoiceId,
|
|
488
509
|
reason,
|
|
489
510
|
error: error.message,
|
|
490
511
|
});
|
|
491
512
|
}
|
|
492
513
|
}
|
|
493
514
|
|
|
494
|
-
async function requestReturnsFromCompletedVendors(
|
|
495
|
-
checkoutSessionId: string,
|
|
496
|
-
paymentIntentId: string,
|
|
497
|
-
reason: string
|
|
498
|
-
): Promise<void> {
|
|
515
|
+
async function requestReturnsFromCompletedVendors(checkoutSession: CheckoutSession, reason: string): Promise<void> {
|
|
499
516
|
logger.info('Starting return request process', {
|
|
500
|
-
checkoutSessionId,
|
|
501
|
-
paymentIntentId,
|
|
517
|
+
checkoutSessionId: checkoutSession.id,
|
|
502
518
|
reason,
|
|
503
519
|
});
|
|
504
520
|
|
|
505
521
|
try {
|
|
506
|
-
const checkoutSession = await CheckoutSession.findByPk(checkoutSessionId);
|
|
507
|
-
if (!checkoutSession) {
|
|
508
|
-
logger.error('CheckoutSession not found for return request', { checkoutSessionId });
|
|
509
|
-
return;
|
|
510
|
-
}
|
|
511
|
-
|
|
512
522
|
const vendorInfos = (checkoutSession.vendor_info as VendorInfo[]) || [];
|
|
513
523
|
const completedVendors = vendorInfos.filter((vendor) => vendor.status === 'completed');
|
|
514
524
|
|
|
515
525
|
if (completedVendors.length === 0) {
|
|
516
|
-
logger.info('No completed vendors to request returns from', { checkoutSessionId });
|
|
526
|
+
logger.info('No completed vendors to request returns from', { checkoutSessionId: checkoutSession.id });
|
|
517
527
|
return;
|
|
518
528
|
}
|
|
519
529
|
|
|
520
530
|
logger.info(`Found ${completedVendors.length} completed vendors requiring return requests`, {
|
|
521
|
-
checkoutSessionId,
|
|
531
|
+
checkoutSessionId: checkoutSession.id,
|
|
522
532
|
completedVendorIds: completedVendors.map((v) => v.vendor_id),
|
|
523
533
|
});
|
|
524
534
|
|
|
525
535
|
const returnRequestPromises = completedVendors.map((vendor) => {
|
|
526
|
-
return requestReturnFromSingleVendor(
|
|
536
|
+
return requestReturnFromSingleVendor(checkoutSession, vendor, reason);
|
|
527
537
|
});
|
|
528
538
|
|
|
529
539
|
const returnResults = await Promise.allSettled(returnRequestPromises);
|
|
@@ -547,27 +557,26 @@ async function requestReturnsFromCompletedVendors(
|
|
|
547
557
|
});
|
|
548
558
|
|
|
549
559
|
logger.info('Return request process completed', {
|
|
550
|
-
checkoutSessionId,
|
|
560
|
+
checkoutSessionId: checkoutSession.id,
|
|
551
561
|
totalVendors: completedVendors.length,
|
|
552
562
|
successful: returnResults.filter((r) => r.status === 'fulfilled').length,
|
|
553
563
|
failed: returnResults.filter((r) => r.status === 'rejected').length,
|
|
554
564
|
});
|
|
555
565
|
} catch (error: any) {
|
|
556
566
|
logger.error('Return request process failed', {
|
|
557
|
-
checkoutSessionId,
|
|
558
|
-
paymentIntentId,
|
|
567
|
+
checkoutSessionId: checkoutSession.id,
|
|
559
568
|
error: error.message,
|
|
560
569
|
});
|
|
561
570
|
}
|
|
562
571
|
}
|
|
563
572
|
|
|
564
573
|
async function requestReturnFromSingleVendor(
|
|
565
|
-
|
|
566
|
-
paymentIntentId: string,
|
|
574
|
+
checkoutSession: CheckoutSession,
|
|
567
575
|
vendor: VendorInfo,
|
|
568
576
|
reason: string
|
|
569
577
|
): Promise<void> {
|
|
570
578
|
logger.info('Requesting return for vendor', {
|
|
579
|
+
checkoutSessionId: checkoutSession.id,
|
|
571
580
|
vendorId: vendor.vendor_id,
|
|
572
581
|
orderId: vendor.order_id,
|
|
573
582
|
reason,
|
|
@@ -582,7 +591,6 @@ async function requestReturnFromSingleVendor(
|
|
|
582
591
|
const returnResult = await vendorAdapter.requestReturn({
|
|
583
592
|
orderId: vendor.order_id,
|
|
584
593
|
reason: `Return request due to: ${reason}`,
|
|
585
|
-
paymentIntentId,
|
|
586
594
|
customParams: {
|
|
587
595
|
returnType: 'order_failure',
|
|
588
596
|
originalAmount: vendor.amount,
|
|
@@ -596,7 +604,7 @@ async function requestReturnFromSingleVendor(
|
|
|
596
604
|
status = 'rejected' as 'rejected';
|
|
597
605
|
}
|
|
598
606
|
|
|
599
|
-
await updateSingleVendorInfo(
|
|
607
|
+
await updateSingleVendorInfo(checkoutSession.id, vendor.vendor_id, {
|
|
600
608
|
status: 'return_requested',
|
|
601
609
|
returnRequest: {
|
|
602
610
|
reason,
|
|
@@ -615,10 +623,10 @@ async function requestReturnFromSingleVendor(
|
|
|
615
623
|
logger.error('Return request failed', {
|
|
616
624
|
vendorId: vendor.vendor_id,
|
|
617
625
|
orderId: vendor.order_id,
|
|
618
|
-
error
|
|
626
|
+
error,
|
|
619
627
|
});
|
|
620
628
|
|
|
621
|
-
await updateSingleVendorInfo(
|
|
629
|
+
await updateSingleVendorInfo(checkoutSession.id, vendor.vendor_id, {
|
|
622
630
|
status: 'return_requested',
|
|
623
631
|
error_message: `Return request failed: ${error.message}`,
|
|
624
632
|
});
|