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
|
@@ -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
|
});
|
|
@@ -7,7 +7,7 @@ import { updateVendorFulfillmentStatus } from './fulfillment-coordinator';
|
|
|
7
7
|
|
|
8
8
|
type VendorFulfillmentJob = {
|
|
9
9
|
checkoutSessionId: string;
|
|
10
|
-
|
|
10
|
+
invoiceId: string;
|
|
11
11
|
vendorId: string;
|
|
12
12
|
vendorConfig: any;
|
|
13
13
|
retryOnError?: boolean;
|
|
@@ -19,7 +19,7 @@ export const handleVendorFulfillment = async (job: VendorFulfillmentJob) => {
|
|
|
19
19
|
jobKeys: Object.keys(job),
|
|
20
20
|
});
|
|
21
21
|
|
|
22
|
-
const { checkoutSessionId,
|
|
22
|
+
const { checkoutSessionId, invoiceId, vendorId, vendorConfig } = job;
|
|
23
23
|
|
|
24
24
|
try {
|
|
25
25
|
const checkoutSession = await CheckoutSession.findByPk(checkoutSessionId);
|
|
@@ -31,7 +31,7 @@ export const handleVendorFulfillment = async (job: VendorFulfillmentJob) => {
|
|
|
31
31
|
checkoutSessionId,
|
|
32
32
|
amount_total: checkoutSession.amount_total,
|
|
33
33
|
customer_id: checkoutSession.customer_id || '',
|
|
34
|
-
|
|
34
|
+
invoiceId,
|
|
35
35
|
currency_id: checkoutSession.currency_id,
|
|
36
36
|
customer_did: checkoutSession.customer_did || '',
|
|
37
37
|
};
|
|
@@ -43,7 +43,7 @@ export const handleVendorFulfillment = async (job: VendorFulfillmentJob) => {
|
|
|
43
43
|
status: fulfillmentResult.status,
|
|
44
44
|
});
|
|
45
45
|
|
|
46
|
-
await updateVendorFulfillmentStatus(checkoutSessionId,
|
|
46
|
+
await updateVendorFulfillmentStatus(checkoutSessionId, invoiceId, vendorId, 'sent', {
|
|
47
47
|
orderId: fulfillmentResult.orderId,
|
|
48
48
|
commissionAmount: fulfillmentResult.commissionAmount,
|
|
49
49
|
serviceUrl: fulfillmentResult.serviceUrl,
|
|
@@ -55,7 +55,7 @@ export const handleVendorFulfillment = async (job: VendorFulfillmentJob) => {
|
|
|
55
55
|
error,
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
-
await updateVendorFulfillmentStatus(checkoutSessionId,
|
|
58
|
+
await updateVendorFulfillmentStatus(checkoutSessionId, invoiceId, vendorId, 'failed', {
|
|
59
59
|
lastError: error.message,
|
|
60
60
|
});
|
|
61
61
|
|
|
@@ -157,7 +157,6 @@ async function callVendorReturn(
|
|
|
157
157
|
const returnResult = await vendorAdapter.requestReturn({
|
|
158
158
|
orderId: vendor.order_id,
|
|
159
159
|
reason: 'Subscription canceled',
|
|
160
|
-
paymentIntentId: checkoutSession.payment_intent_id || '',
|
|
161
160
|
customParams: {
|
|
162
161
|
checkoutSessionId: checkoutSession.id,
|
|
163
162
|
subscriptionId: checkoutSession.subscription_id,
|
|
@@ -83,7 +83,7 @@ export const handleVendorStatusCheck = async (job: VendorStatusCheckJob) => {
|
|
|
83
83
|
id: `fulfillment-coordinator-${checkoutSessionId}-${vendorId}`,
|
|
84
84
|
job: {
|
|
85
85
|
checkoutSessionId,
|
|
86
|
-
|
|
86
|
+
invoiceId: checkoutSession?.invoice_id || '',
|
|
87
87
|
triggeredBy: 'vendor-status-check-timeout',
|
|
88
88
|
},
|
|
89
89
|
});
|
|
@@ -158,7 +158,7 @@ export const handleVendorStatusCheck = async (job: VendorStatusCheckJob) => {
|
|
|
158
158
|
id: `fulfillment-coordinator-${checkoutSessionId}-${vendorId}`,
|
|
159
159
|
job: {
|
|
160
160
|
checkoutSessionId,
|
|
161
|
-
|
|
161
|
+
invoiceId: checkoutSession?.invoice_id || '',
|
|
162
162
|
triggeredBy: 'vendor-status-check',
|
|
163
163
|
},
|
|
164
164
|
});
|
|
@@ -105,8 +105,8 @@ import {
|
|
|
105
105
|
createDiscountRecordsForCheckout,
|
|
106
106
|
updateSubscriptionDiscountReferences,
|
|
107
107
|
} from '../libs/discount/coupon';
|
|
108
|
+
import { rollbackDiscountUsageForCheckoutSession, applyDiscountsToLineItems } from '../libs/discount/discount';
|
|
108
109
|
import { formatToShortUrl } from '../libs/url';
|
|
109
|
-
import { applyDiscountsToLineItems } from '../libs/discount/discount';
|
|
110
110
|
|
|
111
111
|
const router = Router();
|
|
112
112
|
|
|
@@ -2785,6 +2785,7 @@ router.delete('/:id/remove-promotion', user, ensureCheckoutSessionOpen, async (r
|
|
|
2785
2785
|
const trialSetup = getSubscriptionTrialSetup(checkoutSession.subscription_data as any, currency.id);
|
|
2786
2786
|
const isTrialing = trialSetup.trialInDays > 0 || trialSetup.trialEnd > now;
|
|
2787
2787
|
|
|
2788
|
+
await rollbackDiscountUsageForCheckoutSession(checkoutSession.id);
|
|
2788
2789
|
// Calculate original amounts without any discounts
|
|
2789
2790
|
const originalResult = await applyDiscountsToLineItems({
|
|
2790
2791
|
lineItems: originalItems,
|
|
@@ -52,7 +52,9 @@ export default {
|
|
|
52
52
|
amount: fastCheckoutAmount,
|
|
53
53
|
});
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
const requiredStake = !subscription!.billing_thresholds?.no_stake;
|
|
56
|
+
|
|
57
|
+
if (delegation.sufficient === false || !requiredStake) {
|
|
56
58
|
claimsList.push({
|
|
57
59
|
signature: await getDelegationTxClaim({
|
|
58
60
|
mode: 'subscription',
|
|
@@ -69,16 +71,22 @@ export default {
|
|
|
69
71
|
});
|
|
70
72
|
}
|
|
71
73
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
74
|
+
if (requiredStake) {
|
|
75
|
+
claimsList.push({
|
|
76
|
+
prepareTx: await getStakeTxClaim({
|
|
77
|
+
userDid,
|
|
78
|
+
userPk,
|
|
79
|
+
paymentCurrency,
|
|
80
|
+
paymentMethod,
|
|
81
|
+
items,
|
|
82
|
+
subscription: subscription!,
|
|
83
|
+
}),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (claimsList.length === 0) {
|
|
88
|
+
throw new Error('No available claims for your subscription at this time.');
|
|
89
|
+
}
|
|
82
90
|
|
|
83
91
|
return claimsList;
|
|
84
92
|
}
|
|
@@ -123,9 +131,12 @@ export default {
|
|
|
123
131
|
},
|
|
124
132
|
});
|
|
125
133
|
|
|
134
|
+
const requiredStake = !subscription!.billing_thresholds?.no_stake;
|
|
135
|
+
|
|
126
136
|
// 判断是否为最后一步
|
|
127
137
|
const staking = result.find((x: any) => x.claim?.type === 'prepareTx' && x.claim?.meta?.purpose === 'staking');
|
|
128
|
-
const isFinalStep =
|
|
138
|
+
const isFinalStep =
|
|
139
|
+
(paymentMethod.type === 'arcblock' && (staking || !requiredStake)) || paymentMethod.type !== 'arcblock';
|
|
129
140
|
|
|
130
141
|
if (!isFinalStep) {
|
|
131
142
|
await updateSession({
|
|
@@ -774,6 +774,7 @@ export async function getDelegationTxClaim({
|
|
|
774
774
|
paymentCurrency,
|
|
775
775
|
requiredStake,
|
|
776
776
|
});
|
|
777
|
+
|
|
777
778
|
if (mode === 'delegation') {
|
|
778
779
|
tokenRequirements = [];
|
|
779
780
|
}
|
|
@@ -1080,9 +1081,15 @@ export async function getTokenRequirements({
|
|
|
1080
1081
|
billingThreshold = 0,
|
|
1081
1082
|
requiredStake,
|
|
1082
1083
|
}: TokenRequirementArgs) {
|
|
1083
|
-
const tokenRequirements = [];
|
|
1084
|
+
const tokenRequirements: { address: string; value: string }[] = [];
|
|
1084
1085
|
let amount = await getFastCheckoutAmount({ items, mode, currencyId: paymentCurrency.id, trialing: !!trialing });
|
|
1085
1086
|
|
|
1087
|
+
const addStakeRequired = requiredStake && ((paymentMethod.type === 'arcblock' && mode !== 'delegation') || mode === 'setup');
|
|
1088
|
+
|
|
1089
|
+
if (!addStakeRequired && amount === '0') {
|
|
1090
|
+
return tokenRequirements;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1086
1093
|
// If the app has not staked, we need to add the gas fee to the amount
|
|
1087
1094
|
if ((await hasStakedForGas(paymentMethod)) === false) {
|
|
1088
1095
|
const maxGas = await estimateMaxGasForTx(paymentMethod);
|
|
@@ -1106,7 +1113,7 @@ export async function getTokenRequirements({
|
|
|
1106
1113
|
}
|
|
1107
1114
|
|
|
1108
1115
|
// Add stake requirement to token requirement
|
|
1109
|
-
if (
|
|
1116
|
+
if (addStakeRequired) {
|
|
1110
1117
|
const staking = getSubscriptionStakeSetup(
|
|
1111
1118
|
items,
|
|
1112
1119
|
paymentCurrency.id,
|
|
@@ -1115,6 +1122,8 @@ export async function getTokenRequirements({
|
|
|
1115
1122
|
const exist = tokenRequirements.find((x) => x.address === paymentCurrency.contract);
|
|
1116
1123
|
if (exist) {
|
|
1117
1124
|
exist.value = new BN(exist.value).add(staking.licensed).add(staking.metered).toString();
|
|
1125
|
+
} else {
|
|
1126
|
+
tokenRequirements.push({ address: paymentCurrency.contract as string, value: staking.licensed.add(staking.metered).toString() });
|
|
1118
1127
|
}
|
|
1119
1128
|
}
|
|
1120
1129
|
|
|
@@ -320,7 +320,14 @@ router.get('/pending-amount', authMine, async (req, res) => {
|
|
|
320
320
|
where['payload.subscription_id'] = req.query.subscription_id;
|
|
321
321
|
}
|
|
322
322
|
if (req.query.customer_id) {
|
|
323
|
-
|
|
323
|
+
if (typeof req.query.customer_id !== 'string') {
|
|
324
|
+
return res.status(400).json({ error: 'Customer ID must be a string' });
|
|
325
|
+
}
|
|
326
|
+
const customer = await Customer.findByPkOrDid(req.query.customer_id);
|
|
327
|
+
if (!customer) {
|
|
328
|
+
return res.status(404).json({ error: 'Customer not found' });
|
|
329
|
+
}
|
|
330
|
+
where['payload.customer_id'] = customer.id;
|
|
324
331
|
}
|
|
325
332
|
const [summary] = await MeterEvent.getPendingAmounts({
|
|
326
333
|
subscriptionId: req.query.subscription_id as string,
|
|
@@ -44,6 +44,7 @@ const ProductAndPriceSchema = Joi.object({
|
|
|
44
44
|
'string.pattern.base':
|
|
45
45
|
'statement_descriptor should be at least one letter and cannot include Chinese characters and special characters such as <, >、"、’ or \\',
|
|
46
46
|
})
|
|
47
|
+
.allow(null, '')
|
|
47
48
|
.empty('')
|
|
48
49
|
.optional(),
|
|
49
50
|
unit_label: Joi.string().max(12).empty('').optional(),
|
package/api/src/routes/vendor.ts
CHANGED
|
@@ -355,9 +355,12 @@ async function getVendorStatus(sessionId: string, isDetail = false) {
|
|
|
355
355
|
};
|
|
356
356
|
}
|
|
357
357
|
|
|
358
|
+
// FIXME: will remove payment_status @pengfei
|
|
359
|
+
const paymentStatus = doc.status === 'complete' ? 'paid' : 'unpaid';
|
|
360
|
+
|
|
358
361
|
if (doc.status !== 'complete') {
|
|
359
362
|
return {
|
|
360
|
-
payment_status:
|
|
363
|
+
payment_status: paymentStatus,
|
|
361
364
|
session_status: doc.status,
|
|
362
365
|
error: 'CheckoutSession not complete',
|
|
363
366
|
vendors: [],
|
|
@@ -365,7 +368,7 @@ async function getVendorStatus(sessionId: string, isDetail = false) {
|
|
|
365
368
|
}
|
|
366
369
|
if (!doc.vendor_info) {
|
|
367
370
|
return {
|
|
368
|
-
payment_status:
|
|
371
|
+
payment_status: paymentStatus,
|
|
369
372
|
session_status: doc.status,
|
|
370
373
|
error: 'Vendor info not found',
|
|
371
374
|
vendors: [],
|
|
@@ -373,7 +376,13 @@ async function getVendorStatus(sessionId: string, isDetail = false) {
|
|
|
373
376
|
}
|
|
374
377
|
|
|
375
378
|
const vendors = doc.vendor_info.map((item) => {
|
|
376
|
-
return getVendorStatusByVendorId(item.vendor_id, item.order_id, isDetail)
|
|
379
|
+
return getVendorStatusByVendorId(item.vendor_id, item.order_id, isDetail).then((status) => {
|
|
380
|
+
return {
|
|
381
|
+
error_message: item.error_message,
|
|
382
|
+
status: item.status,
|
|
383
|
+
...status,
|
|
384
|
+
};
|
|
385
|
+
});
|
|
377
386
|
});
|
|
378
387
|
|
|
379
388
|
const subscriptionId = doc.subscription_id;
|
|
@@ -390,7 +399,7 @@ async function getVendorStatus(sessionId: string, isDetail = false) {
|
|
|
390
399
|
}
|
|
391
400
|
|
|
392
401
|
return {
|
|
393
|
-
payment_status:
|
|
402
|
+
payment_status: paymentStatus,
|
|
394
403
|
session_status: doc.status,
|
|
395
404
|
subscriptionUrl: shortSubscriptionUrl,
|
|
396
405
|
vendors: await Promise.all(vendors),
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { DataTypes } from 'sequelize';
|
|
2
|
+
import { Migration, safeApplyColumnChanges } from '../migrate';
|
|
3
|
+
|
|
4
|
+
export const up: Migration = async ({ context }) => {
|
|
5
|
+
await safeApplyColumnChanges(context, {
|
|
6
|
+
discounts: [
|
|
7
|
+
{
|
|
8
|
+
name: 'confirmed',
|
|
9
|
+
field: {
|
|
10
|
+
type: DataTypes.BOOLEAN,
|
|
11
|
+
defaultValue: true,
|
|
12
|
+
allowNull: false,
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
],
|
|
16
|
+
});
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const down: Migration = async ({ context }) => {
|
|
20
|
+
await context.removeColumn('discounts', 'confirmed');
|
|
21
|
+
};
|