payment-kit 1.15.33 → 1.15.35

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 (63) hide show
  1. package/api/src/integrations/stripe/handlers/setup-intent.ts +3 -1
  2. package/api/src/integrations/stripe/handlers/subscription.ts +2 -8
  3. package/api/src/integrations/stripe/resource.ts +0 -11
  4. package/api/src/libs/invoice.ts +202 -1
  5. package/api/src/libs/notification/template/subscription-canceled.ts +15 -2
  6. package/api/src/libs/notification/template/subscription-renew-failed.ts +1 -1
  7. package/api/src/libs/notification/template/subscription-renewed.ts +1 -1
  8. package/api/src/libs/notification/template/subscription-trial-will-end.ts +9 -5
  9. package/api/src/libs/notification/template/subscription-will-canceled.ts +9 -5
  10. package/api/src/libs/notification/template/subscription-will-renew.ts +10 -12
  11. package/api/src/libs/payment.ts +3 -2
  12. package/api/src/libs/refund.ts +4 -0
  13. package/api/src/libs/subscription.ts +58 -14
  14. package/api/src/queues/invoice.ts +1 -0
  15. package/api/src/queues/payment.ts +3 -1
  16. package/api/src/queues/refund.ts +9 -8
  17. package/api/src/queues/subscription.ts +111 -40
  18. package/api/src/routes/checkout-sessions.ts +22 -6
  19. package/api/src/routes/connect/change-payment.ts +51 -34
  20. package/api/src/routes/connect/change-plan.ts +25 -3
  21. package/api/src/routes/connect/recharge.ts +28 -3
  22. package/api/src/routes/connect/setup.ts +27 -6
  23. package/api/src/routes/connect/shared.ts +223 -1
  24. package/api/src/routes/connect/subscribe.ts +25 -3
  25. package/api/src/routes/customers.ts +2 -2
  26. package/api/src/routes/invoices.ts +27 -105
  27. package/api/src/routes/payment-links.ts +3 -0
  28. package/api/src/routes/refunds.ts +22 -1
  29. package/api/src/routes/subscriptions.ts +112 -21
  30. package/api/src/routes/webhook-attempts.ts +14 -1
  31. package/api/src/store/models/invoice.ts +3 -1
  32. package/blocklet.yml +1 -1
  33. package/package.json +4 -4
  34. package/src/app.tsx +3 -1
  35. package/src/components/invoice/list.tsx +83 -31
  36. package/src/components/invoice/recharge.tsx +244 -0
  37. package/src/components/payment-intent/actions.tsx +2 -1
  38. package/src/components/payment-link/actions.tsx +6 -6
  39. package/src/components/payment-link/item.tsx +53 -18
  40. package/src/components/pricing-table/actions.tsx +14 -3
  41. package/src/components/pricing-table/payment-settings.tsx +1 -1
  42. package/src/components/refund/actions.tsx +43 -1
  43. package/src/components/refund/list.tsx +1 -1
  44. package/src/components/subscription/actions/cancel.tsx +10 -7
  45. package/src/components/subscription/metrics.tsx +1 -1
  46. package/src/components/subscription/portal/actions.tsx +22 -1
  47. package/src/components/subscription/portal/list.tsx +1 -0
  48. package/src/components/webhook/attempts.tsx +19 -121
  49. package/src/components/webhook/request-info.tsx +139 -0
  50. package/src/locales/en.tsx +4 -0
  51. package/src/locales/zh.tsx +8 -0
  52. package/src/pages/admin/billing/invoices/detail.tsx +15 -0
  53. package/src/pages/admin/billing/invoices/index.tsx +1 -1
  54. package/src/pages/admin/billing/subscriptions/detail.tsx +12 -4
  55. package/src/pages/admin/customers/customers/detail.tsx +1 -0
  56. package/src/pages/admin/payments/refunds/detail.tsx +2 -2
  57. package/src/pages/admin/products/links/create.tsx +4 -1
  58. package/src/pages/customer/index.tsx +1 -1
  59. package/src/pages/customer/invoice/detail.tsx +34 -14
  60. package/src/pages/customer/recharge.tsx +45 -35
  61. package/src/pages/customer/subscription/change-plan.tsx +8 -1
  62. package/src/pages/customer/subscription/detail.tsx +12 -22
  63. package/src/pages/customer/subscription/embed.tsx +3 -1
@@ -220,6 +220,7 @@ export const startInvoiceQueue = async () => {
220
220
  status: 'open',
221
221
  collection_method: 'charge_automatically',
222
222
  amount_remaining: { [Op.gt]: '0' },
223
+ billing_reason: { [Op.ne]: 'stake' },
223
224
  },
224
225
  });
225
226
 
@@ -587,11 +587,13 @@ export const handlePayment = async (job: PaymentJob) => {
587
587
  }
588
588
  const client = paymentMethod.getOcapClient();
589
589
 
590
+ const payer = paymentSettings?.payment_method_options.arcblock?.payer as string;
591
+
590
592
  // check balance before capture with transaction
591
593
  result = await isDelegationSufficientForPayment({
592
594
  paymentMethod,
593
595
  paymentCurrency,
594
- userDid: customer.did,
596
+ userDid: payer || customer.did,
595
597
  amount: paymentIntent.amount,
596
598
  });
597
599
  if (result.sufficient === false) {
@@ -126,10 +126,14 @@ const handleRefundJob = async (
126
126
  // try refund transfer and reschedule on error
127
127
  logger.info('Refund transfer attempt', { id: refund.id, attempt: refund.attempt_count });
128
128
  let result;
129
+ const paymentIntent = await PaymentIntent.findByPk(refund.payment_intent_id);
130
+ if (!paymentIntent) {
131
+ throw new Error('PaymentIntent not found');
132
+ }
133
+
129
134
  try {
130
135
  if (paymentMethod.type === 'arcblock') {
131
136
  const client = paymentMethod.getOcapClient();
132
-
133
137
  // check balance before transfer with transaction
134
138
  result = await isBalanceSufficientForRefund({ paymentMethod, paymentCurrency, amount: refund.amount });
135
139
  if (result.sufficient === false) {
@@ -137,11 +141,12 @@ const handleRefundJob = async (
137
141
  throw new CustomError(result.reason, 'app balance not sufficient for this refund');
138
142
  }
139
143
 
144
+ const payer = paymentIntent?.payment_details?.arcblock?.payer;
140
145
  // do the transfer
141
146
  const signed = await client.signTransferV2Tx({
142
147
  tx: {
143
148
  itx: {
144
- to: customer.did,
149
+ to: payer || customer.did,
145
150
  value: '0',
146
151
  assets: [],
147
152
  tokens: [{ address: paymentCurrency.contract, value: refund.amount }],
@@ -192,7 +197,6 @@ const handleRefundJob = async (
192
197
 
193
198
  // do the capture
194
199
  const client = paymentMethod.getEvmClient();
195
- const paymentIntent = await PaymentIntent.findByPk(refund.payment_intent_id);
196
200
  const payer = paymentIntent!.payment_details?.ethereum?.payer as string;
197
201
  const receipt = await sendErc20ToUser(client, paymentCurrency.contract, payer, refund.amount);
198
202
  logger.info('refund transfer done', { id: refund.id, txHash: receipt.hash });
@@ -216,10 +220,6 @@ const handleRefundJob = async (
216
220
  if (!refund.payment_intent_id) {
217
221
  throw new Error('payment intent id not found');
218
222
  }
219
- const paymentIntent = await PaymentIntent.findByPk(refund.payment_intent_id);
220
- if (!paymentIntent) {
221
- throw new Error('PaymentIntent not found');
222
- }
223
223
  const stripePaymentIntentId = paymentIntent?.payment_details?.stripe?.payment_intent_id;
224
224
  if (!stripePaymentIntentId) {
225
225
  throw new Error('paymentIntent should have stripe payment intent id');
@@ -315,7 +315,8 @@ const handleStakeReturnJob = async (
315
315
  }
316
316
  const client = paymentMethod.getOcapClient();
317
317
  const subscription = await Subscription.findByPk(refund.subscription_id);
318
- const address = await getSubscriptionStakeAddress(subscription!, customer.did);
318
+ const address =
319
+ arcblockDetail?.staking?.address || (await getSubscriptionStakeAddress(subscription!, customer.did));
319
320
  const stakeEnough = await checkRemainingStake(paymentMethod, paymentCurrency, address, refund.amount);
320
321
  if (!stakeEnough.enough) {
321
322
  logger.warn('Stake return aborted because stake is not enough ', {
@@ -23,7 +23,7 @@ import {
23
23
  shouldCancelSubscription,
24
24
  } from '../libs/subscription';
25
25
  import { ensureInvoiceAndItems } from '../routes/connect/shared';
26
- import { PaymentCurrency, PaymentIntent, PaymentMethod, Refund, UsageRecord } from '../store/models';
26
+ import { PaymentCurrency, PaymentIntent, PaymentMethod, Refund, SetupIntent, UsageRecord } from '../store/models';
27
27
  import { Customer } from '../store/models/customer';
28
28
  import { Invoice } from '../store/models/invoice';
29
29
  import { Price } from '../store/models/price';
@@ -325,24 +325,39 @@ const handleSubscriptionAfterRecover = async (subscription: Subscription) => {
325
325
  };
326
326
 
327
327
  const handleStakeSlashAfterCancel = async (subscription: Subscription) => {
328
- const method = await PaymentMethod.findByPk(subscription.default_payment_method_id);
329
- if (!method || method.type !== 'arcblock') {
328
+ const invoice = await Invoice.findByPk(subscription.latest_invoice_id);
329
+ if (!invoice || invoice.status !== 'uncollectible') {
330
+ logger.warn('Stake slashing aborted because invoice status', {
331
+ subscription: subscription.id,
332
+ invoice: invoice?.id,
333
+ status: invoice?.status,
334
+ });
330
335
  return;
331
336
  }
332
- const currency = await PaymentCurrency.findByPk(subscription.currency_id);
337
+ const currency = await PaymentCurrency.findByPk(invoice.currency_id || subscription.currency_id);
333
338
  if (!currency) {
339
+ logger.warn('Stake slashing aborted because currency not found', {
340
+ subscription: subscription.id,
341
+ invoice: invoice.id,
342
+ currency: invoice.currency_id || subscription.currency_id,
343
+ });
334
344
  return;
335
345
  }
336
- const customer = await Customer.findByPk(subscription.customer_id);
337
- if (!customer) {
346
+ const method = await PaymentMethod.findByPk(currency.payment_method_id);
347
+ if (!method || method.type !== 'arcblock') {
348
+ logger.warn('Stake slashing aborted because payment method not arcblock', {
349
+ subscription: subscription.id,
350
+ invoice: invoice.id,
351
+ method: method?.id,
352
+ });
338
353
  return;
339
354
  }
340
- const invoice = await Invoice.findByPk(subscription.latest_invoice_id);
341
- if (!invoice || invoice.status !== 'uncollectible') {
342
- logger.warn('Stake slashing aborted because invoice status', {
355
+ const customer = await Customer.findByPk(subscription.customer_id);
356
+ if (!customer) {
357
+ logger.warn('Stake slashing aborted because customer not found', {
343
358
  subscription: subscription.id,
344
- invoice: invoice?.id,
345
- status: invoice?.status,
359
+ invoice: invoice.id,
360
+ customer: subscription.customer_id,
346
361
  });
347
362
  return;
348
363
  }
@@ -468,8 +483,18 @@ const handleStakeSlashAfterCancel = async (subscription: Subscription) => {
468
483
  });
469
484
  };
470
485
 
471
- const ensureReturnStake = async (subscription: Subscription) => {
472
- const paymentMethod = await PaymentMethod.findByPk(subscription.default_payment_method_id);
486
+ const ensureReturnStake = async (subscription: Subscription, paymentCurrencyId?: string, stakingAddress?: string) => {
487
+ const paymentCurrency = await PaymentCurrency.findByPk(paymentCurrencyId || subscription.currency_id);
488
+ if (!paymentCurrency) {
489
+ logger.warn('Stake return skipped because no payment currency', {
490
+ subscription: subscription.id,
491
+ currency: subscription.currency_id,
492
+ });
493
+ return;
494
+ }
495
+ const paymentMethod = await PaymentMethod.findByPk(
496
+ paymentCurrency.payment_method_id || subscription.default_payment_method_id
497
+ );
473
498
  if (paymentMethod?.type !== 'arcblock') {
474
499
  logger.warn('Stake return skipped because payment method not arcblock', {
475
500
  subscription: subscription.id,
@@ -477,7 +502,7 @@ const ensureReturnStake = async (subscription: Subscription) => {
477
502
  });
478
503
  return;
479
504
  }
480
- const address = subscription?.payment_details?.arcblock?.staking?.address;
505
+ const address = stakingAddress || subscription?.payment_details?.arcblock?.staking?.address;
481
506
  if (!address) {
482
507
  logger.warn('Stake return skipped because no staking address', {
483
508
  subscription: subscription.id,
@@ -486,21 +511,7 @@ const ensureReturnStake = async (subscription: Subscription) => {
486
511
  return;
487
512
  }
488
513
 
489
- const refunds = await Refund.findAll({ where: { subscription_id: subscription.id, type: 'stake_return' } });
490
- if (refunds.length > 0) {
491
- logger.info(`Stake return skipped because subscription ${subscription.id} already has stake return records.`);
492
- return;
493
- }
494
- const paymentCurrency = await PaymentCurrency.findByPk(subscription.currency_id);
495
- if (!paymentCurrency) {
496
- logger.warn('Stake return skipped because no payment currency', {
497
- subscription: subscription.id,
498
- currency: subscription.currency_id,
499
- });
500
- return;
501
- }
502
-
503
- const result = await getSubscriptionStakeReturnSetup(subscription, address, paymentMethod);
514
+ const result = await getSubscriptionStakeReturnSetup(subscription, address, paymentMethod, paymentCurrencyId);
504
515
 
505
516
  const stakeEnough = await checkRemainingStake(paymentMethod, paymentCurrency, address, result.return_amount);
506
517
  if (!stakeEnough.enough) {
@@ -513,17 +524,27 @@ const ensureReturnStake = async (subscription: Subscription) => {
513
524
  return;
514
525
  }
515
526
  if (result.return_amount !== '0') {
527
+ const invoice = await Invoice.findOne({
528
+ where: {
529
+ billing_reason: 'stake',
530
+ subscription_id: subscription.id,
531
+ currency_id: paymentCurrency.id,
532
+ status: 'paid',
533
+ },
534
+ order: [['created_at', 'DESC']],
535
+ });
516
536
  // do the stake return
517
537
  const item = await Refund.create({
518
538
  type: 'stake_return',
519
539
  livemode: subscription.livemode,
520
540
  amount: result.return_amount,
521
- description: 'stake_return_on_subscription_cancel',
541
+ description: 'stake_return_for_subscription',
522
542
  status: 'pending',
523
543
  reason: 'requested_by_admin',
524
- currency_id: subscription.currency_id,
544
+ currency_id: paymentCurrency.id,
545
+ invoice_id: invoice?.id,
525
546
  customer_id: subscription.customer_id,
526
- payment_method_id: subscription.default_payment_method_id,
547
+ payment_method_id: paymentMethod.id,
527
548
  payment_intent_id: result?.lastInvoice?.payment_intent_id as string,
528
549
  subscription_id: subscription.id,
529
550
  attempt_count: 0,
@@ -538,6 +559,10 @@ const ensureReturnStake = async (subscription: Subscription) => {
538
559
  // @ts-ignore
539
560
  arcblock: {
540
561
  receiver: result.sender,
562
+ staking: {
563
+ address,
564
+ tx_hash: '',
565
+ },
541
566
  },
542
567
  },
543
568
  });
@@ -547,7 +572,7 @@ const ensureReturnStake = async (subscription: Subscription) => {
547
572
  item: item.toJSON(),
548
573
  });
549
574
  } else {
550
- logger.info('Skipped stake return for canceled subscription', {
575
+ logger.info('Skipped stake return for subscription', {
551
576
  subscription: subscription.id,
552
577
  return_amount: result.return_amount,
553
578
  });
@@ -668,24 +693,33 @@ const ensureRefundOnCancel = async (subscription: Subscription) => {
668
693
  });
669
694
  return;
670
695
  }
671
- const result = await getSubscriptionRefundSetup(subscription, subscription.cancel_at);
672
- if (result.unused === '0') {
696
+ const lastInvoice = await Invoice.findByPk(subscription.latest_invoice_id);
697
+ if (!lastInvoice) {
698
+ logger.warn('Refund skipped because no latest invoice', {
699
+ subscription: subscription.id,
700
+ });
701
+ return;
702
+ }
703
+
704
+ const result = await getSubscriptionRefundSetup(subscription, subscription.cancel_at, lastInvoice.currency_id);
705
+ if (result.remainingUnused === '0') {
673
706
  logger.warn('Refund skipped because unused amount is 0', {
674
707
  subscription: subscription.id,
675
708
  unused: result.unused,
676
709
  });
677
710
  return;
678
711
  }
712
+ const currency = await PaymentCurrency.findByPk(lastInvoice?.currency_id);
679
713
  const item = await Refund.create({
680
714
  type: 'refund',
681
715
  livemode: subscription.livemode,
682
- amount: refund === 'last' ? result.total : result.unused,
716
+ amount: refund === 'last' ? result.remaining : result.remainingUnused,
683
717
  description: 'refund_transfer_on_subscription_cancel',
684
718
  status: 'pending',
685
719
  reason: 'requested_by_admin',
686
- currency_id: subscription.currency_id,
720
+ currency_id: result.lastInvoice?.currency_id,
687
721
  customer_id: subscription.customer_id,
688
- payment_method_id: subscription.default_payment_method_id,
722
+ payment_method_id: currency?.payment_method_id || subscription.default_payment_method_id,
689
723
  payment_intent_id: result.lastInvoice.payment_intent_id as string,
690
724
  invoice_id: result.lastInvoice.id,
691
725
  subscription_id: subscription.id,
@@ -852,12 +886,12 @@ export const slashStakeQueue = createQueue({
852
886
  export const returnStakeQueue = createQueue({
853
887
  name: 'returnStake',
854
888
  onJob: async (job) => {
855
- const { subscriptionId } = job;
889
+ const { subscriptionId, stakingAddress, paymentCurrencyId } = job;
856
890
  const subscription = await Subscription.findByPk(subscriptionId);
857
891
  if (!subscription) {
858
892
  return;
859
893
  }
860
- await ensureReturnStake(subscription);
894
+ await ensureReturnStake(subscription, paymentCurrencyId, stakingAddress);
861
895
  },
862
896
  options: {
863
897
  concurrency: 1,
@@ -997,3 +1031,40 @@ events.on('customer.stake.revoked', async ({ subscriptionId, tx }: { subscriptio
997
1031
  await new SubscriptionWillCanceledSchedule().reScheduleSubscriptionTasks([subscription]);
998
1032
  await addSubscriptionJob(subscription, 'cancel', true, subscription.current_period_end);
999
1033
  });
1034
+
1035
+ events.on('setup_intent.succeeded', async (setupIntent: SetupIntent) => {
1036
+ logger.info('setup intent succeeded', { setupIntent: setupIntent.id });
1037
+ if (setupIntent.metadata?.from_currency && setupIntent?.metadata?.subscription_id) {
1038
+ const subscription = await Subscription.findByPk(setupIntent.metadata.subscription_id);
1039
+ if (!subscription) {
1040
+ logger.info('skip return stake because no subscription found', { setupIntent: setupIntent.id });
1041
+ return;
1042
+ }
1043
+ const stakingInvoice = await Invoice.findOne({
1044
+ where: {
1045
+ subscription_id: subscription.id,
1046
+ billing_reason: 'stake',
1047
+ currency_id: setupIntent.metadata?.from_currency,
1048
+ status: 'paid',
1049
+ },
1050
+ order: [['created_at', 'DESC']],
1051
+ });
1052
+ logger.info('staking invoice', { stakingInvoice });
1053
+ if (stakingInvoice) {
1054
+ returnStakeQueue.push({
1055
+ id: `return-stake-${subscription.id}-${stakingInvoice.id}`,
1056
+ job: {
1057
+ subscriptionId: subscription.id,
1058
+ stakingAddress: stakingInvoice?.metadata?.payment_details?.arcblock?.address,
1059
+ paymentCurrencyId: setupIntent.metadata?.from_currency,
1060
+ },
1061
+ });
1062
+ logger.info('subscription return stake job scheduled', {
1063
+ jobId: `return-stake-${subscription.id}-${stakingInvoice.id}`,
1064
+ subscription: subscription.id,
1065
+ stakingAddress: stakingInvoice?.metadata?.payment_details?.arcblock?.address,
1066
+ paymentCurrencyId: setupIntent.metadata?.from_currency,
1067
+ });
1068
+ }
1069
+ }
1070
+ });
@@ -1,7 +1,7 @@
1
1
  /* eslint-disable consistent-return */
2
2
  import { isValid } from '@arcblock/did';
3
3
  import { getUrl } from '@blocklet/sdk/lib/component';
4
- import userMiddleware from '@blocklet/sdk/lib/middlewares/user';
4
+ import sessionMiddleware from '@blocklet/sdk/lib/middlewares/session';
5
5
  import { BN, fromUnitToToken } from '@ocap/util';
6
6
  import { NextFunction, Request, Response, Router } from 'express';
7
7
  import Joi from 'joi';
@@ -17,7 +17,11 @@ import { MetadataSchema } from '../libs/api';
17
17
  import { checkPassportForPaymentLink } from '../integrations/blocklet/passport';
18
18
  import { handleStripePaymentSucceed } from '../integrations/stripe/handlers/payment-intent';
19
19
  import { handleStripeSubscriptionSucceed } from '../integrations/stripe/handlers/subscription';
20
- import { ensureStripePaymentIntent, ensureStripeSubscription } from '../integrations/stripe/resource';
20
+ import {
21
+ ensureStripePaymentCustomer,
22
+ ensureStripePaymentIntent,
23
+ ensureStripeSubscription,
24
+ } from '../integrations/stripe/resource';
21
25
  import dayjs from '../libs/dayjs';
22
26
  import logger from '../libs/logger';
23
27
  import { isCreditSufficientForPayment, isDelegationSufficientForPayment } from '../libs/payment';
@@ -69,7 +73,7 @@ import { ensureInvoiceForCheckout } from './connect/shared';
69
73
 
70
74
  const router = Router();
71
75
 
72
- const user = userMiddleware();
76
+ const user = sessionMiddleware();
73
77
  const auth = authenticate<CheckoutSession>({ component: true, roles: ['owner', 'admin'] });
74
78
 
75
79
  const getPaymentMethods = async (doc: CheckoutSession) => {
@@ -141,11 +145,11 @@ const SubscriptionDataSchema = Joi.object({
141
145
  .items(
142
146
  Joi.object({
143
147
  name: Joi.string().optional(),
144
- color: Joi.string().allow('primary', 'secondary', 'success', 'error', 'warning').optional(),
145
- variant: Joi.string().allow('text', 'contained', 'outlined').optional(),
148
+ color: Joi.string().valid('primary', 'secondary', 'success', 'error', 'warning').optional(),
149
+ variant: Joi.string().valid('text', 'contained', 'outlined').optional(),
146
150
  text: Joi.object().required(),
147
151
  link: Joi.string().uri().required(),
148
- type: Joi.string().allow('notification', 'custom').optional(),
152
+ type: Joi.string().valid('notification', 'custom').optional(),
149
153
  triggerEvents: Joi.array().items(Joi.string()).optional(),
150
154
  })
151
155
  )
@@ -1027,6 +1031,18 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
1027
1031
  trialInDays,
1028
1032
  trialEnd
1029
1033
  );
1034
+ const stripeCustomer = await ensureStripePaymentCustomer(subscription, paymentMethod);
1035
+ if (stripeSubscription) {
1036
+ await subscription.update({
1037
+ payment_details: {
1038
+ stripe: {
1039
+ customer_id: stripeCustomer.id,
1040
+ subscription_id: stripeSubscription.id,
1041
+ setup_intent_id: stripeSubscription.pending_setup_intent?.id,
1042
+ },
1043
+ },
1044
+ });
1045
+ }
1030
1046
  logger.info('ensureStripeSubscription', {
1031
1047
  subscriptionId: subscription.id,
1032
1048
  stripeSubscriptionId: stripeSubscription?.id,
@@ -1,15 +1,15 @@
1
1
  import { executeEvmTransaction, waitForEvmTxConfirm } from '../../integrations/ethereum/tx';
2
2
  import type { CallbackArgs } from '../../libs/auth';
3
- import { isDelegationSufficientForPayment } from '../../libs/payment';
4
- import { getFastCheckoutAmount } from '../../libs/session';
5
3
  import { getTxMetadata } from '../../libs/util';
6
- import type { TLineItemExpanded } from '../../store/models';
4
+ import { Lock, type TLineItemExpanded } from '../../store/models';
7
5
  import {
8
6
  ensureChangePaymentContext,
7
+ ensureStakeInvoice,
9
8
  executeOcapTransactions,
10
9
  getAuthPrincipalClaim,
11
10
  getDelegationTxClaim,
12
11
  getStakeTxClaim,
12
+ updateStripeSubscriptionAfterChangePayment,
13
13
  } from './shared';
14
14
 
15
15
  export default {
@@ -23,42 +23,31 @@ export default {
23
23
  },
24
24
  onConnect: async ({ userDid, userPk, extraParams }: CallbackArgs) => {
25
25
  const { subscriptionId } = extraParams;
26
- const { subscription, customer, paymentMethod, paymentCurrency } = await ensureChangePaymentContext(subscriptionId);
26
+ const { subscription, paymentMethod, paymentCurrency } = await ensureChangePaymentContext(subscriptionId);
27
27
 
28
28
  const claims: { [type: string]: [string, object] } = {};
29
29
 
30
30
  // @ts-ignore
31
31
  const items = subscription!.items as TLineItemExpanded[];
32
- const trialing = false;
32
+ const trialing = true;
33
33
  const billingThreshold = Number(subscription.billing_thresholds?.amount_gte || 0);
34
- const fastCheckoutAmount = getFastCheckoutAmount(items, 'setup', paymentCurrency.id, trialing);
35
34
 
36
35
  if (paymentMethod.type === 'arcblock') {
37
- const delegation = await isDelegationSufficientForPayment({
38
- paymentMethod,
39
- paymentCurrency,
40
- userDid: customer!.did,
41
- amount: fastCheckoutAmount,
42
- });
43
-
44
- // if we can complete purchase without any wallet interaction
45
- if (delegation.sufficient === false) {
46
- claims.delegation = [
47
- 'signature',
48
- await getDelegationTxClaim({
49
- mode: 'setup',
50
- userDid,
51
- userPk,
52
- nonce: `change-method-${subscription.id}`,
53
- data: getTxMetadata({ subscriptionId: subscription.id }),
54
- paymentCurrency,
55
- paymentMethod,
56
- trialing,
57
- billingThreshold,
58
- items,
59
- }),
60
- ];
61
- }
36
+ claims.delegation = [
37
+ 'signature',
38
+ await getDelegationTxClaim({
39
+ mode: 'setup',
40
+ userDid,
41
+ userPk,
42
+ nonce: `change-method-${subscription.id}`,
43
+ data: getTxMetadata({ subscriptionId: subscription.id }),
44
+ paymentCurrency,
45
+ paymentMethod,
46
+ trialing,
47
+ billingThreshold,
48
+ items,
49
+ }),
50
+ ];
62
51
 
63
52
  // we always need to stake for the subscription
64
53
  claims.staking = [
@@ -105,7 +94,7 @@ export default {
105
94
 
106
95
  onAuth: async ({ request, userDid, userPk, claims, extraParams }: CallbackArgs) => {
107
96
  const { subscriptionId } = extraParams;
108
- const { subscription, setupIntent, paymentCurrency, paymentMethod } =
97
+ const { subscription, setupIntent, paymentCurrency, paymentMethod, customer } =
109
98
  await ensureChangePaymentContext(subscriptionId);
110
99
 
111
100
  const prepareTxExecution = async () => {
@@ -135,18 +124,46 @@ export default {
135
124
  await subscription?.update({
136
125
  currency_id: paymentCurrency.id,
137
126
  default_payment_method_id: paymentMethod.id,
127
+ payment_details: {
128
+ ...subscription.payment_details,
129
+ [paymentMethod.type]: paymentDetails,
130
+ },
138
131
  });
132
+
133
+ await Lock.acquire(`${subscription.id}-change-plan`, subscription.current_period_end);
134
+ // update stripe subscription
135
+ await updateStripeSubscriptionAfterChangePayment(setupIntent, subscription);
139
136
  };
140
137
 
141
138
  if (paymentMethod.type === 'arcblock') {
142
139
  await prepareTxExecution();
143
- const paymentDetails = await executeOcapTransactions(
140
+ const { stakingAmount, ...paymentDetails } = await executeOcapTransactions(
144
141
  userDid,
145
142
  userPk,
146
143
  claims,
147
144
  paymentMethod,
148
145
  request,
149
- subscription?.id
146
+ subscription?.id,
147
+ paymentCurrency.contract
148
+ );
149
+ await ensureStakeInvoice(
150
+ {
151
+ total: stakingAmount,
152
+ description: 'Stake for subscription payment change',
153
+ currency_id: paymentCurrency.id,
154
+ metadata: {
155
+ payment_details: {
156
+ arcblock: {
157
+ tx_hash: paymentDetails?.staking?.tx_hash,
158
+ payer: paymentDetails?.payer,
159
+ address: paymentDetails?.staking?.address,
160
+ },
161
+ },
162
+ },
163
+ },
164
+ subscription!,
165
+ paymentMethod,
166
+ customer!
150
167
  );
151
168
  await afterTxExecution(paymentDetails);
152
169
  return { hash: paymentDetails.tx_hash };
@@ -8,6 +8,7 @@ import { invoiceQueue } from '../../queues/invoice';
8
8
  import { addSubscriptionJob, subscriptionQueue } from '../../queues/subscription';
9
9
  import type { TLineItemExpanded } from '../../store/models';
10
10
  import {
11
+ ensureStakeInvoice,
11
12
  ensureSubscription,
12
13
  executeOcapTransactions,
13
14
  getAuthPrincipalClaim,
@@ -107,7 +108,8 @@ export default {
107
108
 
108
109
  onAuth: async ({ request, userDid, userPk, claims, extraParams }: CallbackArgs) => {
109
110
  const { subscriptionId } = extraParams;
110
- const { invoice, paymentMethod, subscription } = await ensureSubscription(subscriptionId);
111
+ const { invoice, paymentMethod, subscription, paymentCurrency, customer } =
112
+ await ensureSubscription(subscriptionId);
111
113
 
112
114
  const prepareTxExecution = async () => {
113
115
  await subscription?.update({
@@ -142,13 +144,33 @@ export default {
142
144
  if (paymentMethod.type === 'arcblock') {
143
145
  await prepareTxExecution();
144
146
 
145
- const paymentDetails = await executeOcapTransactions(
147
+ const { stakingAmount, ...paymentDetails } = await executeOcapTransactions(
146
148
  userDid,
147
149
  userPk,
148
150
  claims,
149
151
  paymentMethod,
150
152
  request,
151
- subscription?.id
153
+ subscription?.id,
154
+ paymentCurrency?.contract
155
+ );
156
+ await ensureStakeInvoice(
157
+ {
158
+ total: stakingAmount,
159
+ description: 'Stake for subscription plan change',
160
+ currency_id: paymentCurrency.id,
161
+ metadata: {
162
+ payment_details: {
163
+ arcblock: {
164
+ tx_hash: paymentDetails?.staking?.tx_hash,
165
+ payer: paymentDetails?.payer,
166
+ address: paymentDetails?.staking?.address,
167
+ },
168
+ },
169
+ },
170
+ },
171
+ subscription!,
172
+ paymentMethod,
173
+ customer!
152
174
  );
153
175
  await afterTxExecution(paymentDetails);
154
176
 
@@ -6,7 +6,7 @@ import { executeEvmTransaction, waitForEvmTxConfirm } from 'api/src/integrations
6
6
  import type { CallbackArgs } from '../../libs/auth';
7
7
  import { getGasPayerExtra } from '../../libs/payment';
8
8
  import { getTxMetadata } from '../../libs/util';
9
- import { ensureSubscriptionRecharge, getAuthPrincipalClaim } from './shared';
9
+ import { ensureRechargeInvoice, ensureSubscriptionRecharge, getAuthPrincipalClaim } from './shared';
10
10
  import logger from '../../libs/logger';
11
11
 
12
12
  export default {
@@ -74,10 +74,29 @@ export default {
74
74
  },
75
75
  onAuth: async ({ request, userDid, claims, extraParams }: CallbackArgs) => {
76
76
  const { subscriptionId } = extraParams;
77
- const { paymentMethod, paymentCurrency, receiverAddress } = await ensureSubscriptionRecharge(subscriptionId);
77
+ const { paymentMethod, paymentCurrency, receiverAddress, subscription, customer } =
78
+ await ensureSubscriptionRecharge(subscriptionId);
78
79
  let { amount } = extraParams;
79
80
  amount = fromTokenToUnit(amount, paymentCurrency.decimal).toString();
80
81
 
82
+ const afterTxExecution = async (paymentDetails: any) => {
83
+ await ensureRechargeInvoice(
84
+ {
85
+ total: amount,
86
+ description: 'Subscription recharge',
87
+ currency_id: paymentCurrency.id,
88
+ metadata: {
89
+ payment_details: {
90
+ [paymentMethod.type]: paymentDetails,
91
+ receiverAddress,
92
+ },
93
+ },
94
+ },
95
+ subscription!,
96
+ paymentMethod,
97
+ customer!
98
+ );
99
+ };
81
100
  if (paymentMethod.type === 'arcblock') {
82
101
  try {
83
102
  const client = paymentMethod.getOcapClient();
@@ -104,6 +123,11 @@ export default {
104
123
  paymentMethod: paymentMethod.type,
105
124
  });
106
125
 
126
+ await afterTxExecution({
127
+ tx_hash: txHash,
128
+ payer: userDid,
129
+ type: 'transfer',
130
+ });
107
131
  return { hash: txHash };
108
132
  } catch (err) {
109
133
  console.error(err);
@@ -120,13 +144,14 @@ export default {
120
144
  Number(paymentDetails.block_height),
121
145
  paymentMethod.confirmation.block
122
146
  )
123
- .then(() => {
147
+ .then(async () => {
124
148
  logger.info('Recharge successful', {
125
149
  receiverAddress,
126
150
  amount,
127
151
  subscriptionId,
128
152
  paymentMethod: paymentMethod.type,
129
153
  });
154
+ await afterTxExecution(paymentDetails);
130
155
  })
131
156
  .catch(console.error);
132
157