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.
Files changed (51) hide show
  1. package/api/src/crons/metering-subscription-detection.ts +9 -0
  2. package/api/src/integrations/arcblock/nft.ts +1 -0
  3. package/api/src/integrations/blocklet/passport.ts +1 -1
  4. package/api/src/integrations/stripe/handlers/invoice.ts +2 -2
  5. package/api/src/integrations/stripe/handlers/setup-intent.ts +29 -1
  6. package/api/src/integrations/stripe/handlers/subscription.ts +19 -15
  7. package/api/src/integrations/stripe/resource.ts +81 -1
  8. package/api/src/libs/audit.ts +42 -0
  9. package/api/src/libs/invoice.ts +54 -7
  10. package/api/src/libs/notification/index.ts +72 -4
  11. package/api/src/libs/notification/template/base.ts +2 -0
  12. package/api/src/libs/notification/template/subscription-renew-failed.ts +1 -5
  13. package/api/src/libs/notification/template/subscription-renewed.ts +1 -5
  14. package/api/src/libs/notification/template/subscription-succeeded.ts +8 -18
  15. package/api/src/libs/notification/template/subscription-trial-start.ts +2 -10
  16. package/api/src/libs/notification/template/subscription-upgraded.ts +1 -5
  17. package/api/src/libs/payment.ts +47 -14
  18. package/api/src/libs/product.ts +1 -4
  19. package/api/src/libs/session.ts +600 -8
  20. package/api/src/libs/setting.ts +172 -0
  21. package/api/src/libs/subscription.ts +7 -69
  22. package/api/src/libs/ws.ts +5 -0
  23. package/api/src/queues/checkout-session.ts +42 -36
  24. package/api/src/queues/notification.ts +3 -2
  25. package/api/src/queues/payment.ts +33 -6
  26. package/api/src/queues/usage-record.ts +2 -10
  27. package/api/src/routes/checkout-sessions.ts +324 -187
  28. package/api/src/routes/connect/shared.ts +160 -38
  29. package/api/src/routes/connect/subscribe.ts +123 -64
  30. package/api/src/routes/payment-currencies.ts +3 -6
  31. package/api/src/routes/payment-links.ts +11 -1
  32. package/api/src/routes/payment-stats.ts +2 -2
  33. package/api/src/routes/payouts.ts +2 -1
  34. package/api/src/routes/settings.ts +45 -0
  35. package/api/src/routes/subscriptions.ts +1 -2
  36. package/api/src/store/migrations/20250408-subscription-grouping.ts +39 -0
  37. package/api/src/store/migrations/20250419-subscription-grouping.ts +69 -0
  38. package/api/src/store/models/checkout-session.ts +52 -0
  39. package/api/src/store/models/index.ts +1 -0
  40. package/api/src/store/models/payment-link.ts +6 -0
  41. package/api/src/store/models/subscription.ts +8 -6
  42. package/api/src/store/models/types.ts +31 -1
  43. package/api/tests/libs/session.spec.ts +423 -0
  44. package/api/tests/libs/subscription.spec.ts +0 -110
  45. package/blocklet.yml +3 -1
  46. package/package.json +20 -19
  47. package/scripts/sdk.js +486 -155
  48. package/src/locales/en.tsx +1 -1
  49. package/src/locales/zh.tsx +1 -1
  50. package/src/pages/admin/settings/vault-config/edit-form.tsx +1 -1
  51. 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
- } from '../libs/session';
37
- import {
35
+ isDonationCheckoutSession,
36
+ createGroupSubscriptions,
38
37
  formatSubscriptionProduct,
39
- getDaysUntilCancel,
40
- getDaysUntilDue,
41
- getSubscriptionCreateSetup,
42
- getSubscriptionTrialSetup,
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 { ensureStripePaymentIntent, ensureStripeSubscription } from '../integrations/stripe/resource';
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 { isCreditSufficientForPayment, isDelegationSufficientForPayment } from '../libs/payment';
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 (result.recurring === false) {
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: doc.toJSON(),
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
- if (checkoutSession.mode === 'subscription' || checkoutSession.mode === 'setup') {
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
- // rebuild subscription items
1035
- await SubscriptionItem.destroy({ where: { subscription_id: subscription.id } });
1036
- const items = await Promise.all(
1037
- lineItems
1038
- .filter((x) => x.price.type === 'recurring')
1039
- .map((x) =>
1040
- SubscriptionItem.create({
1041
- livemode: !!checkoutSession.livemode,
1042
- // @ts-ignore
1043
- subscription_id: subscription.id,
1044
- price_id: x.upsell_price_id || x.price_id,
1045
- quantity: x.quantity,
1046
- metadata: checkoutSession.metadata as any,
1047
- })
1048
- )
1049
- );
1050
- logger.info('subscription items rebuilt on checkout session resubmit', {
1051
- session: checkoutSession.id,
1052
- items: items.map((x) => x.id),
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
- logger.info('subscription created on checkout session submit', {
1107
- session: checkoutSession.id,
1108
- subscription: subscription.id,
1109
- });
1181
+ subscription = primarySubscription;
1182
+ subscriptions = groupSubscriptions;
1110
1183
 
1111
- // create subscription items
1112
- const items = await Promise.all(
1113
- lineItems
1114
- .filter((x) => x.price.type === 'recurring')
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
- // persist subscription id
1132
- await checkoutSession.update({ subscription_id: subscription.id });
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
- arcblock: { payer: customer.did },
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 canFastPay = canPayWithDelegation(paymentIntent?.beneficiaries || []);
1165
- if (checkoutSession.mode === 'payment' && paymentIntent && canFastPay) {
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
- if (subscription) {
1211
- const stripeSubscription = await ensureStripeSubscription(
1212
- subscription,
1213
- paymentMethod,
1214
- paymentCurrency,
1215
- lineItems,
1216
- trialInDays,
1217
- trialEnd
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 (stripeSubscription) {
1220
- await subscription.update({
1221
- payment_details: {
1222
- stripe: {
1223
- customer_id: stripeSubscription.customer,
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
- logger.info('ensureStripeSubscription', {
1231
- subscriptionId: subscription.id,
1232
- stripeSubscriptionId: stripeSubscription?.id,
1233
- });
1234
- if (stripeSubscription && subscription?.payment_details?.stripe?.subscription_id === stripeSubscription.id) {
1235
- if (['active', 'trialing'].includes(stripeSubscription.status) && subscription.status === 'incomplete') {
1236
- await handleStripeSubscriptionSucceed(subscription, stripeSubscription.status);
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: checkoutSession.mode === 'payment' && canFastPay ? delegation : null,
1268
- balance: checkoutSession.mode === 'payment' && canFastPay ? balance : null,
1404
+ delegation: canFastPay ? delegation : null,
1405
+ balance: canFastPay ? balance : null,
1269
1406
  });
1270
1407
  } catch (err) {
1271
1408
  logger.error('Error submitting checkout session', {