payment-kit 1.18.30 → 1.18.31
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/metering-subscription-detection.ts +9 -0
- package/api/src/integrations/arcblock/nft.ts +1 -0
- package/api/src/integrations/blocklet/passport.ts +1 -1
- package/api/src/integrations/stripe/handlers/invoice.ts +2 -2
- package/api/src/integrations/stripe/handlers/setup-intent.ts +29 -1
- package/api/src/integrations/stripe/handlers/subscription.ts +19 -15
- package/api/src/integrations/stripe/resource.ts +81 -1
- package/api/src/libs/audit.ts +42 -0
- package/api/src/libs/invoice.ts +54 -7
- package/api/src/libs/notification/index.ts +72 -4
- package/api/src/libs/notification/template/base.ts +2 -0
- package/api/src/libs/notification/template/subscription-renew-failed.ts +1 -5
- package/api/src/libs/notification/template/subscription-renewed.ts +1 -5
- package/api/src/libs/notification/template/subscription-succeeded.ts +8 -18
- package/api/src/libs/notification/template/subscription-trial-start.ts +2 -10
- package/api/src/libs/notification/template/subscription-upgraded.ts +1 -5
- package/api/src/libs/payment.ts +47 -14
- package/api/src/libs/product.ts +1 -4
- package/api/src/libs/session.ts +600 -8
- package/api/src/libs/setting.ts +172 -0
- package/api/src/libs/subscription.ts +7 -69
- package/api/src/libs/ws.ts +5 -0
- package/api/src/queues/checkout-session.ts +42 -36
- package/api/src/queues/notification.ts +3 -2
- package/api/src/queues/payment.ts +33 -6
- package/api/src/queues/usage-record.ts +2 -10
- package/api/src/routes/checkout-sessions.ts +324 -187
- package/api/src/routes/connect/shared.ts +160 -38
- package/api/src/routes/connect/subscribe.ts +123 -64
- package/api/src/routes/payment-currencies.ts +3 -6
- package/api/src/routes/payment-links.ts +11 -1
- package/api/src/routes/payment-stats.ts +2 -2
- package/api/src/routes/payouts.ts +2 -1
- package/api/src/routes/settings.ts +45 -0
- package/api/src/routes/subscriptions.ts +1 -2
- package/api/src/store/migrations/20250408-subscription-grouping.ts +39 -0
- package/api/src/store/migrations/20250419-subscription-grouping.ts +69 -0
- package/api/src/store/models/checkout-session.ts +52 -0
- package/api/src/store/models/index.ts +1 -0
- package/api/src/store/models/payment-link.ts +6 -0
- package/api/src/store/models/subscription.ts +8 -6
- package/api/src/store/models/types.ts +31 -1
- package/api/tests/libs/session.spec.ts +423 -0
- package/api/tests/libs/subscription.spec.ts +0 -110
- package/blocklet.yml +3 -1
- package/package.json +20 -19
- package/scripts/sdk.js +486 -155
- package/src/locales/en.tsx +1 -1
- package/src/locales/zh.tsx +1 -1
- package/src/pages/admin/settings/vault-config/edit-form.tsx +1 -1
- package/src/pages/customer/subscription/change-payment.tsx +8 -3
|
@@ -31,16 +31,15 @@ import {
|
|
|
31
31
|
getStatementDescriptor,
|
|
32
32
|
getSupportedPaymentCurrencies,
|
|
33
33
|
getSupportedPaymentMethods,
|
|
34
|
-
isDonationCheckoutSession,
|
|
35
34
|
isLineItemAligned,
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
isDonationCheckoutSession,
|
|
36
|
+
createGroupSubscriptions,
|
|
38
37
|
formatSubscriptionProduct,
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
} from '../libs/subscription';
|
|
38
|
+
getOneTimeLineItems,
|
|
39
|
+
getCheckoutSessionSubscriptionIds,
|
|
40
|
+
getSubscriptionLineItems,
|
|
41
|
+
} from '../libs/session';
|
|
42
|
+
import { getDaysUntilCancel, getDaysUntilDue, getSubscriptionTrialSetup } from '../libs/subscription';
|
|
44
43
|
import {
|
|
45
44
|
CHECKOUT_SESSION_TTL,
|
|
46
45
|
formatAmountPrecisionLimit,
|
|
@@ -50,7 +49,6 @@ import {
|
|
|
50
49
|
isUserInBlocklist,
|
|
51
50
|
} from '../libs/util';
|
|
52
51
|
import {
|
|
53
|
-
Invoice,
|
|
54
52
|
PaymentBeneficiary,
|
|
55
53
|
SetupIntent,
|
|
56
54
|
Subscription,
|
|
@@ -59,6 +57,8 @@ import {
|
|
|
59
57
|
type SubscriptionData,
|
|
60
58
|
type TPriceExpanded,
|
|
61
59
|
type TProductExpanded,
|
|
60
|
+
type TLineItemExpanded,
|
|
61
|
+
Invoice,
|
|
62
62
|
} from '../store/models';
|
|
63
63
|
import { CheckoutSession } from '../store/models/checkout-session';
|
|
64
64
|
import { Customer } from '../store/models/customer';
|
|
@@ -68,15 +68,24 @@ import { PaymentLink } from '../store/models/payment-link';
|
|
|
68
68
|
import { PaymentMethod } from '../store/models/payment-method';
|
|
69
69
|
import { Price } from '../store/models/price';
|
|
70
70
|
import { Product } from '../store/models/product';
|
|
71
|
-
import {
|
|
71
|
+
import {
|
|
72
|
+
ensureStripePaymentIntent,
|
|
73
|
+
ensureStripeSetupIntentForCheckoutSession,
|
|
74
|
+
ensureStripeSubscription,
|
|
75
|
+
} from '../integrations/stripe/resource';
|
|
72
76
|
import { handleStripePaymentSucceed } from '../integrations/stripe/handlers/payment-intent';
|
|
73
77
|
import { paymentQueue } from '../queues/payment';
|
|
74
78
|
import { invoiceQueue } from '../queues/invoice';
|
|
75
|
-
import { ensureInvoiceForCheckout } from './connect/shared';
|
|
76
|
-
import {
|
|
79
|
+
import { ensureInvoiceForCheckout, ensureInvoicesForSubscriptions } from './connect/shared';
|
|
80
|
+
import {
|
|
81
|
+
isCreditSufficientForPayment,
|
|
82
|
+
isDelegationSufficientForPayment,
|
|
83
|
+
SufficientForPaymentResult,
|
|
84
|
+
} from '../libs/payment';
|
|
77
85
|
import { handleStripeSubscriptionSucceed } from '../integrations/stripe/handlers/subscription';
|
|
78
86
|
import { CHARGE_SUPPORTED_CHAIN_TYPES } from '../libs/constants';
|
|
79
87
|
import { blocklet } from '../libs/auth';
|
|
88
|
+
import { addSubscriptionJob } from '../queues/subscription';
|
|
80
89
|
|
|
81
90
|
const router = Router();
|
|
82
91
|
|
|
@@ -376,6 +385,7 @@ export const formatCheckoutSession = async (payload: any, throwOnEmptyItems = tr
|
|
|
376
385
|
payment_intent_data: {},
|
|
377
386
|
submit_type: 'pay',
|
|
378
387
|
cross_sell_behavior: 'auto',
|
|
388
|
+
enable_subscription_grouping: false,
|
|
379
389
|
},
|
|
380
390
|
pick(payload, [
|
|
381
391
|
'currency_id',
|
|
@@ -398,8 +408,15 @@ export const formatCheckoutSession = async (payload: any, throwOnEmptyItems = tr
|
|
|
398
408
|
'success_url',
|
|
399
409
|
'client_reference_id',
|
|
400
410
|
'after_expiration',
|
|
411
|
+
'enable_subscription_grouping',
|
|
401
412
|
])
|
|
402
413
|
);
|
|
414
|
+
|
|
415
|
+
// TODO: need to support stake subscription
|
|
416
|
+
if (raw.enable_subscription_grouping === true && !raw.subscription_data?.no_stake) {
|
|
417
|
+
throw new Error('Subscription grouping is only supported for stake-free subscriptions');
|
|
418
|
+
}
|
|
419
|
+
|
|
403
420
|
if (payload.include_free_trial && raw.subscription_data) {
|
|
404
421
|
raw.subscription_data.trial_period_days = Number(raw.subscription_data.trial_period_days);
|
|
405
422
|
}
|
|
@@ -441,12 +458,14 @@ export const formatCheckoutSession = async (payload: any, throwOnEmptyItems = tr
|
|
|
441
458
|
if (items.some((x) => !x.price.active)) {
|
|
442
459
|
throw new Error('Invalid line items for checkout session, some price may have been archived');
|
|
443
460
|
}
|
|
461
|
+
const enableSubscriptionGrouping = payload.enable_subscription_grouping;
|
|
444
462
|
for (let i = 0; i < items.length; i++) {
|
|
445
463
|
const result = isLineItemAligned(items, i);
|
|
446
464
|
if (result.currency === false) {
|
|
447
465
|
throw new Error('line_items should have same currency');
|
|
448
466
|
}
|
|
449
|
-
if
|
|
467
|
+
// if subscription grouping is not enabled, we need to check the recurring
|
|
468
|
+
if (result.recurring === false && !enableSubscriptionGrouping) {
|
|
450
469
|
throw new Error('line_items should have same recurring');
|
|
451
470
|
}
|
|
452
471
|
}
|
|
@@ -566,6 +585,118 @@ export async function getCrossSellItem(checkoutSession: CheckoutSession) {
|
|
|
566
585
|
return { error: 'Cross sell not suitable' };
|
|
567
586
|
}
|
|
568
587
|
|
|
588
|
+
/**
|
|
589
|
+
* Process fast checkout for subscriptions with sufficient balance
|
|
590
|
+
*/
|
|
591
|
+
async function processSubscriptionFastCheckout({
|
|
592
|
+
checkoutSession,
|
|
593
|
+
customer,
|
|
594
|
+
subscriptions,
|
|
595
|
+
paymentMethod,
|
|
596
|
+
paymentCurrency,
|
|
597
|
+
paymentSettings,
|
|
598
|
+
lineItems,
|
|
599
|
+
trialEnd,
|
|
600
|
+
now,
|
|
601
|
+
}: {
|
|
602
|
+
checkoutSession: CheckoutSession;
|
|
603
|
+
customer: Customer;
|
|
604
|
+
subscriptions: Subscription[];
|
|
605
|
+
paymentMethod: PaymentMethod;
|
|
606
|
+
paymentCurrency: PaymentCurrency;
|
|
607
|
+
paymentSettings: any;
|
|
608
|
+
lineItems: TLineItemExpanded[];
|
|
609
|
+
trialEnd: number;
|
|
610
|
+
now: number;
|
|
611
|
+
}): Promise<{
|
|
612
|
+
success: boolean;
|
|
613
|
+
invoices?: Invoice[];
|
|
614
|
+
message?: string;
|
|
615
|
+
}> {
|
|
616
|
+
try {
|
|
617
|
+
const primarySubscription = subscriptions.find((x) => x.metadata?.is_primary_subscription) || subscriptions[0];
|
|
618
|
+
const subscriptionAmounts = await Promise.all(
|
|
619
|
+
subscriptions.map(async (sub) => {
|
|
620
|
+
const subItems = await getSubscriptionLineItems(sub, lineItems, primarySubscription);
|
|
621
|
+
return getFastCheckoutAmount(subItems, 'subscription', paymentCurrency.id, trialEnd > now);
|
|
622
|
+
})
|
|
623
|
+
);
|
|
624
|
+
const totalAmount = subscriptionAmounts
|
|
625
|
+
.reduce((sum: BN, amt: string) => sum.add(new BN(amt)), new BN('0'))
|
|
626
|
+
.toString();
|
|
627
|
+
|
|
628
|
+
const delegationCheck = await isDelegationSufficientForPayment({
|
|
629
|
+
paymentMethod,
|
|
630
|
+
paymentCurrency,
|
|
631
|
+
userDid: customer.did,
|
|
632
|
+
amount: totalAmount,
|
|
633
|
+
delegatorAmounts: subscriptionAmounts,
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
if (!delegationCheck.sufficient) {
|
|
637
|
+
logger.warn('Fast checkout for subscription failed due to insufficient delegation or insufficient balance', {
|
|
638
|
+
checkoutSessionId: checkoutSession.id,
|
|
639
|
+
reason: delegationCheck.reason,
|
|
640
|
+
});
|
|
641
|
+
|
|
642
|
+
return {
|
|
643
|
+
success: false,
|
|
644
|
+
message: `Insufficient delegation or insufficient balance: ${delegationCheck.reason}`,
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// All subscriptions have sufficient delegation, proceed with checkout
|
|
649
|
+
logger.info('All subscriptions have sufficient delegation for fast checkout', {
|
|
650
|
+
checkoutSessionId: checkoutSession.id,
|
|
651
|
+
subscriptionIds: subscriptions.map((s) => s.id),
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
// Update payment settings for all subscriptions
|
|
655
|
+
await Promise.all(subscriptions.map((sub) => sub.update({ payment_settings: paymentSettings })));
|
|
656
|
+
|
|
657
|
+
// Create invoices for all subscriptions
|
|
658
|
+
const { invoices } = await ensureInvoicesForSubscriptions({
|
|
659
|
+
checkoutSession,
|
|
660
|
+
customer,
|
|
661
|
+
subscriptions,
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
// Update invoice settings and push to queue
|
|
665
|
+
await Promise.all(
|
|
666
|
+
invoices.map(async (invoice) => {
|
|
667
|
+
if (invoice) {
|
|
668
|
+
await invoice.update({ auto_advance: true, payment_settings: paymentSettings });
|
|
669
|
+
invoiceQueue.push({ id: invoice.id, job: { invoiceId: invoice.id, retryOnError: false } });
|
|
670
|
+
}
|
|
671
|
+
})
|
|
672
|
+
);
|
|
673
|
+
|
|
674
|
+
// Add subscription cycle jobs
|
|
675
|
+
await Promise.all(subscriptions.map((sub) => addSubscriptionJob(sub, 'cycle', false, sub.trial_end)));
|
|
676
|
+
|
|
677
|
+
logger.info('Created and queued invoices for fast checkout with subscriptions', {
|
|
678
|
+
checkoutSessionId: checkoutSession.id,
|
|
679
|
+
subscriptionIds: subscriptions.map((s) => s.id),
|
|
680
|
+
invoiceIds: invoices.map((inv) => inv.id),
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
return {
|
|
684
|
+
success: true,
|
|
685
|
+
invoices,
|
|
686
|
+
};
|
|
687
|
+
} catch (error) {
|
|
688
|
+
logger.error('Error processing subscription fast checkout', {
|
|
689
|
+
error,
|
|
690
|
+
checkoutSessionId: checkoutSession.id,
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
return {
|
|
694
|
+
success: false,
|
|
695
|
+
message: error.message,
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
569
700
|
// create checkout session
|
|
570
701
|
router.post('/', auth, async (req, res) => {
|
|
571
702
|
const raw: Partial<CheckoutSession> = await formatCheckoutSession(req.body);
|
|
@@ -609,6 +740,11 @@ export async function startCheckoutSessionFromPaymentLink(id: string, req: Reque
|
|
|
609
740
|
raw.currency_id = link.currency_id || req.currency.id;
|
|
610
741
|
raw.payment_link_id = link.id;
|
|
611
742
|
|
|
743
|
+
// Inherit multi-subscription settings from payment link
|
|
744
|
+
if (typeof link.enable_subscription_grouping === 'boolean') {
|
|
745
|
+
raw.enable_subscription_grouping = link.enable_subscription_grouping;
|
|
746
|
+
}
|
|
747
|
+
|
|
612
748
|
// Settings priority: PaymentLink.subscription_data > req.query > environments
|
|
613
749
|
const protectedSettings: Partial<SubscriptionData> = {};
|
|
614
750
|
if (link.subscription_data?.min_stake_amount) {
|
|
@@ -776,14 +912,31 @@ router.get('/retrieve/:id', user, async (req, res) => {
|
|
|
776
912
|
return;
|
|
777
913
|
}
|
|
778
914
|
|
|
915
|
+
let subscriptions: Subscription[] = [];
|
|
916
|
+
if (['subscription', 'setup'].includes(doc.mode)) {
|
|
917
|
+
const subscriptionIds = getCheckoutSessionSubscriptionIds(doc);
|
|
918
|
+
if (doc.success_subscription_count === subscriptionIds.length && doc.status === 'incomplete') {
|
|
919
|
+
await doc.update({
|
|
920
|
+
status: 'complete',
|
|
921
|
+
payment_status: 'paid',
|
|
922
|
+
});
|
|
923
|
+
}
|
|
924
|
+
subscriptions = await Subscription.findAll({
|
|
925
|
+
where: { id: subscriptionIds },
|
|
926
|
+
attributes: ['id', 'description', 'status', 'current_period_start', 'current_period_end', 'latest_invoice_id'],
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
|
|
779
930
|
// @ts-ignore
|
|
780
931
|
doc.line_items = await Price.expand(doc.line_items, { upsell: true });
|
|
781
932
|
|
|
782
933
|
// check payment intent
|
|
783
934
|
const paymentIntent = doc.payment_intent_id ? await PaymentIntent.findByPk(doc.payment_intent_id) : null;
|
|
784
|
-
|
|
785
935
|
res.json({
|
|
786
|
-
checkoutSession:
|
|
936
|
+
checkoutSession: {
|
|
937
|
+
...doc.toJSON(),
|
|
938
|
+
subscriptions,
|
|
939
|
+
},
|
|
787
940
|
paymentMethods: await getPaymentMethods(doc),
|
|
788
941
|
paymentLink: doc.payment_link_id ? await PaymentLink.findByPk(doc.payment_link_id) : null,
|
|
789
942
|
paymentIntent,
|
|
@@ -1003,134 +1156,42 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
1003
1156
|
|
|
1004
1157
|
// subscription processing
|
|
1005
1158
|
let subscription: Subscription | null = null;
|
|
1006
|
-
|
|
1007
|
-
if (checkoutSession.subscription_id) {
|
|
1008
|
-
subscription = await Subscription.findByPk(checkoutSession.subscription_id);
|
|
1009
|
-
}
|
|
1010
|
-
if (subscription) {
|
|
1011
|
-
if (subscription.status !== 'incomplete') {
|
|
1012
|
-
return res
|
|
1013
|
-
.status(403)
|
|
1014
|
-
.json({ code: 'SUBSCRIPTION_INVALID', error: 'Checkout session subscription status unexpected' });
|
|
1015
|
-
}
|
|
1016
|
-
const setup = getSubscriptionCreateSetup(lineItems, paymentCurrency.id, trialInDays, trialEnd);
|
|
1017
|
-
subscription = await subscription.update({
|
|
1018
|
-
currency_id: paymentCurrency.id,
|
|
1019
|
-
customer_id: customer.id,
|
|
1020
|
-
default_payment_method_id: paymentMethod.id,
|
|
1021
|
-
current_period_start: setup.period.start,
|
|
1022
|
-
current_period_end: setup.period.end,
|
|
1023
|
-
billing_cycle_anchor: checkoutSession.subscription_data?.billing_cycle_anchor || setup.cycle.anchor,
|
|
1024
|
-
trial_end: setup.trial.end,
|
|
1025
|
-
trial_start: setup.trial.start,
|
|
1026
|
-
pending_invoice_item_interval: setup.recurring,
|
|
1027
|
-
pending_setup_intent: setupIntent?.id,
|
|
1028
|
-
});
|
|
1029
|
-
logger.info('subscription updated on checkout session resubmit', {
|
|
1030
|
-
session: checkoutSession.id,
|
|
1031
|
-
subscription: subscription.id,
|
|
1032
|
-
});
|
|
1159
|
+
let subscriptions: Subscription[] = [];
|
|
1033
1160
|
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
});
|
|
1054
|
-
const invoice = await Invoice.findByPk(subscription?.latest_invoice_id);
|
|
1055
|
-
if (invoice && invoice.customer_id !== customer.id) {
|
|
1056
|
-
invoice.update({
|
|
1057
|
-
customer_id: customer.id,
|
|
1058
|
-
});
|
|
1059
|
-
}
|
|
1060
|
-
} else {
|
|
1061
|
-
const recoveredFromId = checkoutSession.subscription_data?.recovered_from || '';
|
|
1062
|
-
const recoveredFrom = recoveredFromId ? await Subscription.findByPk(recoveredFromId) : null;
|
|
1063
|
-
|
|
1064
|
-
// FIXME: @wangshijun respect all checkoutSession.subscription_data fields
|
|
1065
|
-
const setup = getSubscriptionCreateSetup(lineItems, paymentCurrency.id, trialInDays, trialEnd);
|
|
1066
|
-
subscription = await Subscription.create({
|
|
1067
|
-
livemode: !!checkoutSession.livemode,
|
|
1068
|
-
currency_id: paymentCurrency.id,
|
|
1069
|
-
customer_id: customer.id,
|
|
1070
|
-
status: 'incomplete',
|
|
1071
|
-
current_period_start: setup.period.start,
|
|
1072
|
-
current_period_end: setup.period.end,
|
|
1073
|
-
// FIXME: billing_cycle_anchor implementation is not aligned with stripe
|
|
1074
|
-
billing_cycle_anchor: checkoutSession.subscription_data?.billing_cycle_anchor || setup.cycle.anchor,
|
|
1075
|
-
start_date: dayjs().unix(),
|
|
1076
|
-
trial_end: setup.trial.end,
|
|
1077
|
-
trial_start: setup.trial.start,
|
|
1078
|
-
trial_settings: {
|
|
1079
|
-
end_behavior: {
|
|
1080
|
-
missing_payment_method: 'create_invoice',
|
|
1081
|
-
},
|
|
1082
|
-
},
|
|
1083
|
-
billing_thresholds: {
|
|
1084
|
-
amount_gte: getBillingThreshold(checkoutSession.subscription_data as any),
|
|
1085
|
-
stake_gte: getMinStakeAmount(checkoutSession.subscription_data as any),
|
|
1086
|
-
reset_billing_cycle_anchor: false,
|
|
1087
|
-
},
|
|
1088
|
-
pending_invoice_item_interval: setup.recurring,
|
|
1089
|
-
pending_setup_intent: setupIntent?.id,
|
|
1090
|
-
default_payment_method_id: paymentMethod.id,
|
|
1091
|
-
cancel_at_period_end: false,
|
|
1092
|
-
collection_method: 'charge_automatically',
|
|
1093
|
-
description:
|
|
1094
|
-
checkoutSession.subscription_data?.description ||
|
|
1095
|
-
formatSubscriptionProduct(lineItems.filter((x) => x.price.type === 'recurring')),
|
|
1096
|
-
proration_behavior: checkoutSession.subscription_data?.proration_behavior || 'none',
|
|
1097
|
-
payment_behavior: 'default_incomplete',
|
|
1098
|
-
days_until_due: checkoutSession.subscription_data?.days_until_due ?? checkoutSession.metadata?.days_until_due,
|
|
1099
|
-
days_until_cancel:
|
|
1100
|
-
checkoutSession.subscription_data?.days_until_cancel ?? checkoutSession.metadata?.days_until_cancel,
|
|
1101
|
-
recovered_from: recoveredFrom?.id,
|
|
1102
|
-
metadata: omit(checkoutSession.metadata || {}, ['days_until_due', 'days_until_cancel']),
|
|
1103
|
-
service_actions: checkoutSession.subscription_data?.service_actions || [],
|
|
1104
|
-
});
|
|
1161
|
+
if (checkoutSession.mode === 'subscription' || checkoutSession.mode === 'setup') {
|
|
1162
|
+
const {
|
|
1163
|
+
subscriptions: groupSubscriptions,
|
|
1164
|
+
subscriptionGroups,
|
|
1165
|
+
primarySubscription,
|
|
1166
|
+
} = await createGroupSubscriptions({
|
|
1167
|
+
checkoutSession,
|
|
1168
|
+
lineItems,
|
|
1169
|
+
customer,
|
|
1170
|
+
paymentSettings: {
|
|
1171
|
+
method: paymentMethod,
|
|
1172
|
+
currency: paymentCurrency,
|
|
1173
|
+
},
|
|
1174
|
+
setupIntent,
|
|
1175
|
+
trialConfig: {
|
|
1176
|
+
trialInDays,
|
|
1177
|
+
trialEnd,
|
|
1178
|
+
},
|
|
1179
|
+
});
|
|
1105
1180
|
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
subscription: subscription.id,
|
|
1109
|
-
});
|
|
1181
|
+
subscription = primarySubscription;
|
|
1182
|
+
subscriptions = groupSubscriptions;
|
|
1110
1183
|
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
.map((x) =>
|
|
1116
|
-
SubscriptionItem.create({
|
|
1117
|
-
livemode: !!checkoutSession.livemode,
|
|
1118
|
-
// @ts-ignore
|
|
1119
|
-
subscription_id: subscription.id,
|
|
1120
|
-
price_id: x.upsell_price_id || x.price_id,
|
|
1121
|
-
quantity: x.quantity,
|
|
1122
|
-
metadata: checkoutSession.metadata as any,
|
|
1123
|
-
})
|
|
1124
|
-
)
|
|
1125
|
-
);
|
|
1126
|
-
logger.info('subscription items created on checkout session submit', {
|
|
1127
|
-
session: checkoutSession.id,
|
|
1128
|
-
items: items.map((x) => x.id),
|
|
1129
|
-
});
|
|
1184
|
+
await checkoutSession.update({
|
|
1185
|
+
subscription_id: subscription ? (subscription as any).id : null,
|
|
1186
|
+
subscription_groups: subscriptionGroups,
|
|
1187
|
+
});
|
|
1130
1188
|
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1189
|
+
logger.info('Checkout session updated with subscription information', {
|
|
1190
|
+
session: checkoutSession.id,
|
|
1191
|
+
groups: subscriptionGroups,
|
|
1192
|
+
primarySubscription: subscription ? (subscription as any).id : null,
|
|
1193
|
+
enableSubscriptionGrouping: checkoutSession.enable_subscription_grouping,
|
|
1194
|
+
});
|
|
1134
1195
|
}
|
|
1135
1196
|
|
|
1136
1197
|
const fastCheckoutAmount = getFastCheckoutAmount(
|
|
@@ -1142,7 +1203,7 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
1142
1203
|
const paymentSettings = {
|
|
1143
1204
|
payment_method_types: checkoutSession.payment_method_types,
|
|
1144
1205
|
payment_method_options: {
|
|
1145
|
-
|
|
1206
|
+
[paymentMethod.type]: { payer: customer.did },
|
|
1146
1207
|
},
|
|
1147
1208
|
};
|
|
1148
1209
|
|
|
@@ -1153,16 +1214,18 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
1153
1214
|
customer,
|
|
1154
1215
|
amount: fastCheckoutAmount,
|
|
1155
1216
|
});
|
|
1156
|
-
// if we can complete purchase without any wallet interaction
|
|
1157
|
-
const delegation = await isDelegationSufficientForPayment({
|
|
1158
|
-
paymentMethod,
|
|
1159
|
-
paymentCurrency,
|
|
1160
|
-
userDid: customer.did,
|
|
1161
|
-
amount: fastCheckoutAmount,
|
|
1162
|
-
});
|
|
1163
1217
|
|
|
1164
|
-
const
|
|
1165
|
-
|
|
1218
|
+
const isPayment = checkoutSession.mode === 'payment';
|
|
1219
|
+
let canFastPay = isPayment && canPayWithDelegation(paymentIntent?.beneficiaries || []);
|
|
1220
|
+
let delegation: SufficientForPaymentResult | null = null;
|
|
1221
|
+
if (isPayment && paymentIntent && canFastPay) {
|
|
1222
|
+
// if we can complete purchase without any wallet interaction
|
|
1223
|
+
delegation = await isDelegationSufficientForPayment({
|
|
1224
|
+
paymentMethod,
|
|
1225
|
+
paymentCurrency,
|
|
1226
|
+
userDid: customer.did,
|
|
1227
|
+
amount: fastCheckoutAmount,
|
|
1228
|
+
});
|
|
1166
1229
|
if (balance.sufficient) {
|
|
1167
1230
|
logger.info(`CheckoutSession ${checkoutSession.id} will pay from balance ${paymentIntent?.id}`);
|
|
1168
1231
|
}
|
|
@@ -1182,6 +1245,34 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
1182
1245
|
});
|
|
1183
1246
|
}
|
|
1184
1247
|
}
|
|
1248
|
+
} else if (
|
|
1249
|
+
paymentMethod.type === 'arcblock' &&
|
|
1250
|
+
checkoutSession.mode === 'subscription' &&
|
|
1251
|
+
checkoutSession.subscription_data?.no_stake
|
|
1252
|
+
) {
|
|
1253
|
+
// if we can complete purchase without any wallet interaction
|
|
1254
|
+
const result = await processSubscriptionFastCheckout({
|
|
1255
|
+
checkoutSession,
|
|
1256
|
+
customer,
|
|
1257
|
+
subscriptions,
|
|
1258
|
+
paymentMethod,
|
|
1259
|
+
paymentCurrency,
|
|
1260
|
+
paymentSettings,
|
|
1261
|
+
lineItems,
|
|
1262
|
+
trialEnd,
|
|
1263
|
+
now,
|
|
1264
|
+
});
|
|
1265
|
+
|
|
1266
|
+
if (!result.success) {
|
|
1267
|
+
logger.warn(`Fast checkout processing failed: ${result.message}`, {
|
|
1268
|
+
checkoutSessionId: checkoutSession.id,
|
|
1269
|
+
});
|
|
1270
|
+
} else {
|
|
1271
|
+
delegation = {
|
|
1272
|
+
sufficient: true,
|
|
1273
|
+
};
|
|
1274
|
+
canFastPay = true;
|
|
1275
|
+
}
|
|
1185
1276
|
}
|
|
1186
1277
|
|
|
1187
1278
|
let stripeContext: any = null;
|
|
@@ -1206,46 +1297,91 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
1206
1297
|
intent_type: 'payment_intent',
|
|
1207
1298
|
};
|
|
1208
1299
|
}
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1300
|
+
if (subscriptions.length > 0) {
|
|
1301
|
+
const primarySubscription = (subscriptions.find((x) => x.metadata.is_primary_subscription) ||
|
|
1302
|
+
subscriptions[0]) as Subscription;
|
|
1303
|
+
if (!stripeContext) {
|
|
1304
|
+
stripeContext = {
|
|
1305
|
+
type: 'subscription',
|
|
1306
|
+
stripe_subscriptions: '',
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
stripeContext.stripe_subscriptions = '';
|
|
1310
|
+
await Promise.all(
|
|
1311
|
+
subscriptions.map(async (sub) => {
|
|
1312
|
+
const subscriptionItems = await SubscriptionItem.findAll({ where: { subscription_id: sub.id } });
|
|
1313
|
+
let stripeItems = lineItems.filter((x) =>
|
|
1314
|
+
subscriptionItems.some((y) => y.price_id === x.price_id || y.price_id === x.upsell_price_id)
|
|
1315
|
+
);
|
|
1316
|
+
if (sub.id === primarySubscription.id) {
|
|
1317
|
+
stripeItems = [...stripeItems, ...getOneTimeLineItems(lineItems)];
|
|
1318
|
+
}
|
|
1319
|
+
const stripeSubscription = await ensureStripeSubscription(
|
|
1320
|
+
sub,
|
|
1321
|
+
paymentMethod,
|
|
1322
|
+
paymentCurrency,
|
|
1323
|
+
stripeItems,
|
|
1324
|
+
trialInDays,
|
|
1325
|
+
trialEnd
|
|
1326
|
+
);
|
|
1327
|
+
if (stripeSubscription) {
|
|
1328
|
+
await sub.update({
|
|
1329
|
+
payment_details: {
|
|
1330
|
+
stripe: {
|
|
1331
|
+
customer_id: stripeSubscription.customer,
|
|
1332
|
+
subscription_id: stripeSubscription.id,
|
|
1333
|
+
setup_intent_id: stripeSubscription.pending_setup_intent?.id,
|
|
1334
|
+
},
|
|
1335
|
+
},
|
|
1336
|
+
});
|
|
1337
|
+
}
|
|
1338
|
+
logger.info('ensureStripeSubscription', {
|
|
1339
|
+
subscriptionId: sub.id,
|
|
1340
|
+
stripeSubscriptionId: stripeSubscription?.id,
|
|
1341
|
+
checkoutSessionId: checkoutSession.id,
|
|
1342
|
+
});
|
|
1343
|
+
if (stripeSubscription && sub?.payment_details?.stripe?.subscription_id === stripeSubscription.id) {
|
|
1344
|
+
if (['active', 'trialing'].includes(stripeSubscription.status) && sub.status === 'incomplete') {
|
|
1345
|
+
await handleStripeSubscriptionSucceed(sub, stripeSubscription.status);
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
stripeContext.stripe_subscriptions += `${stripeSubscription.id},`;
|
|
1350
|
+
if (sub.id === primarySubscription.id) {
|
|
1351
|
+
stripeContext = {
|
|
1352
|
+
type: 'subscription',
|
|
1353
|
+
id: stripeSubscription.id,
|
|
1354
|
+
// @ts-ignore
|
|
1355
|
+
client_secret:
|
|
1356
|
+
stripeSubscription.latest_invoice?.payment_intent?.client_secret ||
|
|
1357
|
+
stripeSubscription.pending_setup_intent?.client_secret,
|
|
1358
|
+
intent_type: stripeSubscription.latest_invoice?.payment_intent ? 'payment_intent' : 'setup_intent',
|
|
1359
|
+
status: stripeSubscription.status,
|
|
1360
|
+
};
|
|
1361
|
+
}
|
|
1362
|
+
return {
|
|
1363
|
+
stripeSubscription,
|
|
1364
|
+
subscription: sub,
|
|
1365
|
+
};
|
|
1366
|
+
})
|
|
1218
1367
|
);
|
|
1219
|
-
if (
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
subscription_id: stripeSubscription.id,
|
|
1225
|
-
setup_intent_id: stripeSubscription.pending_setup_intent?.id,
|
|
1226
|
-
},
|
|
1227
|
-
},
|
|
1368
|
+
if (subscriptions.length > 1) {
|
|
1369
|
+
stripeContext.has_multiple_subscriptions = true;
|
|
1370
|
+
const stripeSetupIntent = await ensureStripeSetupIntentForCheckoutSession(checkoutSession, paymentMethod, {
|
|
1371
|
+
checkoutSessionId: checkoutSession.id,
|
|
1372
|
+
subscription_groups: subscriptions.map((x) => x.id).join(','),
|
|
1228
1373
|
});
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1374
|
+
if (stripeSetupIntent) {
|
|
1375
|
+
stripeContext = {
|
|
1376
|
+
type: 'subscription',
|
|
1377
|
+
id: stripeSetupIntent.id,
|
|
1378
|
+
// @ts-ignore
|
|
1379
|
+
client_secret: stripeSetupIntent.client_secret,
|
|
1380
|
+
intent_type: 'setup_intent',
|
|
1381
|
+
status: stripeSetupIntent.status,
|
|
1382
|
+
};
|
|
1237
1383
|
}
|
|
1238
1384
|
}
|
|
1239
|
-
stripeContext = {
|
|
1240
|
-
type: 'subscription',
|
|
1241
|
-
id: stripeSubscription.id,
|
|
1242
|
-
// @ts-ignore
|
|
1243
|
-
client_secret:
|
|
1244
|
-
stripeSubscription.latest_invoice?.payment_intent?.client_secret ||
|
|
1245
|
-
stripeSubscription.pending_setup_intent?.client_secret,
|
|
1246
|
-
intent_type: stripeSubscription.latest_invoice?.payment_intent ? 'payment_intent' : 'setup_intent',
|
|
1247
|
-
status: stripeSubscription.status,
|
|
1248
|
-
};
|
|
1249
1385
|
}
|
|
1250
1386
|
}
|
|
1251
1387
|
|
|
@@ -1262,10 +1398,11 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
1262
1398
|
setupIntent,
|
|
1263
1399
|
stripeContext,
|
|
1264
1400
|
subscription,
|
|
1401
|
+
subscriptions,
|
|
1265
1402
|
checkoutSession,
|
|
1266
1403
|
customer,
|
|
1267
|
-
delegation:
|
|
1268
|
-
balance:
|
|
1404
|
+
delegation: canFastPay ? delegation : null,
|
|
1405
|
+
balance: canFastPay ? balance : null,
|
|
1269
1406
|
});
|
|
1270
1407
|
} catch (err) {
|
|
1271
1408
|
logger.error('Error submitting checkout session', {
|