payment-kit 1.13.92 → 1.13.93

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 (37) hide show
  1. package/api/src/index.ts +2 -0
  2. package/api/src/libs/audit.ts +28 -34
  3. package/api/src/libs/payment.ts +2 -11
  4. package/api/src/libs/session.ts +1 -1
  5. package/api/src/libs/util.ts +8 -5
  6. package/api/src/routes/checkout-sessions.ts +41 -39
  7. package/api/src/routes/connect/collect.ts +12 -12
  8. package/api/src/routes/connect/setup.ts +8 -11
  9. package/api/src/routes/connect/shared.ts +81 -20
  10. package/api/src/routes/connect/subscribe.ts +8 -11
  11. package/api/src/routes/connect/update.ts +134 -0
  12. package/api/src/routes/pricing-table.ts +9 -121
  13. package/api/src/routes/subscriptions.ts +416 -141
  14. package/api/src/store/models/index.ts +3 -0
  15. package/api/src/store/models/pricing-table.ts +125 -1
  16. package/api/src/store/models/subscription.ts +4 -0
  17. package/api/src/store/models/types.ts +8 -0
  18. package/api/tests/libs/util.spec.ts +6 -6
  19. package/blocklet.yml +1 -1
  20. package/package.json +6 -6
  21. package/src/app.tsx +12 -4
  22. package/src/components/checkout/form/address.tsx +41 -34
  23. package/src/components/checkout/form/index.tsx +1 -1
  24. package/src/components/checkout/pricing-table.tsx +205 -0
  25. package/src/components/payment-link/product-select.tsx +13 -3
  26. package/src/components/portal/invoice/list.tsx +1 -1
  27. package/src/components/portal/subscription/actions.tsx +153 -0
  28. package/src/components/portal/subscription/list.tsx +21 -150
  29. package/src/components/subscription/metrics.tsx +46 -0
  30. package/src/contexts/products.tsx +2 -1
  31. package/src/libs/util.ts +43 -0
  32. package/src/locales/en.tsx +15 -1
  33. package/src/locales/zh.tsx +16 -2
  34. package/src/pages/admin/billing/subscriptions/detail.tsx +2 -34
  35. package/src/pages/checkout/pricing-table.tsx +9 -158
  36. package/src/pages/customer/subscription/{index.tsx → detail.tsx} +6 -36
  37. package/src/pages/customer/subscription/update.tsx +281 -0
package/api/src/index.ts CHANGED
@@ -26,6 +26,7 @@ import collectHandlers from './routes/connect/collect';
26
26
  import payHandlers from './routes/connect/pay';
27
27
  import setupHandlers from './routes/connect/setup';
28
28
  import subscribeHandlers from './routes/connect/subscribe';
29
+ import updateHandlers from './routes/connect/update';
29
30
  import { initialize } from './store/models';
30
31
  import { sequelize } from './store/sequelize';
31
32
 
@@ -53,6 +54,7 @@ handlers.attach(Object.assign({ app: router }, collectHandlers));
53
54
  handlers.attach(Object.assign({ app: router }, payHandlers));
54
55
  handlers.attach(Object.assign({ app: router }, setupHandlers));
55
56
  handlers.attach(Object.assign({ app: router }, subscribeHandlers));
57
+ handlers.attach(Object.assign({ app: router }, updateHandlers));
56
58
 
57
59
  router.use('/api', routes);
58
60
 
@@ -16,24 +16,21 @@ export async function createEvent(scope: string, type: LiteralUnion<EventType, s
16
16
  data.previous_attributes = pick(model._previousDataValues, options.fields);
17
17
  }
18
18
 
19
- const event = await Event.create(
20
- {
21
- type,
22
- api_version: API_VERSION,
23
- livemode: !!model.livemode,
24
- object_id: model.id,
25
- object_type: scope,
26
- data,
27
- request: {
28
- // FIXME:
29
- id: '',
30
- idempotency_key: '',
31
- },
32
- metadata: {},
33
- pending_webhooks: 99, // force all events goto the event queue
19
+ const event = await Event.create({
20
+ type,
21
+ api_version: API_VERSION,
22
+ livemode: !!model.livemode,
23
+ object_id: model.id,
24
+ object_type: scope,
25
+ data,
26
+ request: {
27
+ // FIXME:
28
+ id: '',
29
+ idempotency_key: '',
34
30
  },
35
- { transaction: null }
36
- );
31
+ metadata: {},
32
+ pending_webhooks: 99, // force all events goto the event queue
33
+ });
37
34
 
38
35
  events.emit('event.created', { id: event.id });
39
36
  events.emit(event.type, data.object);
@@ -61,24 +58,21 @@ export async function createStatusEvent(
61
58
  }
62
59
 
63
60
  const suffix = config[data.object.status];
64
- const event = await Event.create(
65
- {
66
- type: [prefix, suffix].join('.'),
67
- api_version: API_VERSION,
68
- livemode: !!model.livemode,
69
- object_id: model.id,
70
- object_type: scope,
71
- data,
72
- request: {
73
- // FIXME:
74
- id: '',
75
- idempotency_key: '',
76
- },
77
- metadata: {},
78
- pending_webhooks: 99, // force all events goto the event queue
61
+ const event = await Event.create({
62
+ type: [prefix, suffix].join('.'),
63
+ api_version: API_VERSION,
64
+ livemode: !!model.livemode,
65
+ object_id: model.id,
66
+ object_type: scope,
67
+ data,
68
+ request: {
69
+ // FIXME:
70
+ id: '',
71
+ idempotency_key: '',
79
72
  },
80
- { transaction: null }
81
- );
73
+ metadata: {},
74
+ pending_webhooks: 99, // force all events goto the event queue
75
+ });
82
76
 
83
77
  events.emit('event.created', { id: event.id });
84
78
  events.emit(event.type, data.object);
@@ -8,15 +8,7 @@ import { BN, fromUnitToToken } from '@ocap/util';
8
8
  import cloneDeep from 'lodash/cloneDeep';
9
9
  import type { LiteralUnion } from 'type-fest';
10
10
 
11
- import {
12
- CheckoutSession,
13
- Invoice,
14
- PaymentCurrency,
15
- PaymentIntent,
16
- PaymentMethod,
17
- TCustomer,
18
- TLineItemExpanded,
19
- } from '../store/models';
11
+ import { Invoice, PaymentCurrency, PaymentIntent, PaymentMethod, TCustomer, TLineItemExpanded } from '../store/models';
20
12
  import type { TPaymentCurrency } from '../store/models/payment-currency';
21
13
  import { blocklet, wallet } from './auth';
22
14
  import { OCAP_PAYMENT_TX_TYPE } from './util';
@@ -209,7 +201,7 @@ export async function getPaymentDetail(userDid: string, invoice: Invoice): Promi
209
201
  }
210
202
 
211
203
  export async function getTokenLimitsForDelegation(
212
- checkoutSession: CheckoutSession,
204
+ items: TLineItemExpanded[],
213
205
  paymentMethod: PaymentMethod,
214
206
  paymentCurrency: PaymentCurrency,
215
207
  address: string,
@@ -218,7 +210,6 @@ export async function getTokenLimitsForDelegation(
218
210
  const client = paymentMethod.getOcapClient();
219
211
  const { state } = await client.getDelegateState({ address });
220
212
 
221
- const items = checkoutSession.line_items as TLineItemExpanded[];
222
213
  const hasMetered = items.some((x) => x.price.recurring?.usage_type === 'metered');
223
214
  const allowance = hasMetered ? '0' : amount;
224
215
 
@@ -109,7 +109,7 @@ export function getSubscriptionCreateSetup(items: TLineItemExpanded[], currencyI
109
109
  setup = setup.add(new BN(unit).mul(new BN(x.quantity)));
110
110
  if (price.type === 'recurring') {
111
111
  if (trialInDays === 0) {
112
- subscription = setup.add(new BN(unit).mul(new BN(x.quantity)));
112
+ subscription = subscription.add(new BN(unit).mul(new BN(x.quantity)));
113
113
  }
114
114
  }
115
115
  });
@@ -152,15 +152,18 @@ export function getTxMetadata(extra: Record<string, any> = {}): any {
152
152
  };
153
153
  }
154
154
 
155
- export function getMetadataFromQuery(query: Record<string, any> = {}): Record<string, any> {
156
- const metadata: Record<string, any> = {};
155
+ export function getDataObjectFromQuery(
156
+ query: Record<string, any> = {},
157
+ prefix: string = 'metadata'
158
+ ): Record<string, any> {
159
+ const result: Record<string, any> = {};
157
160
  Object.keys(query).forEach((key) => {
158
- if (key.startsWith('metadata.') && query[key]) {
159
- metadata[key.replace('metadata.', '')] = query[key];
161
+ if (key.startsWith(`${prefix}.`) && query[key]) {
162
+ result[key.replace(`${prefix}.`, '')] = query[key];
160
163
  }
161
164
  });
162
165
 
163
- return metadata;
166
+ return result;
164
167
  }
165
168
 
166
169
  // @FIXME: 这个应该封装在某个通用类库里面 @jianchao @wangshijun
@@ -33,7 +33,7 @@ import {
33
33
  isLineItemAligned,
34
34
  } from '../libs/session';
35
35
  import { getDaysUntilDue } from '../libs/subscription';
36
- import { createCodeGenerator, formatMetadata, getMetadataFromQuery } from '../libs/util';
36
+ import { createCodeGenerator, formatMetadata, getDataObjectFromQuery } from '../libs/util';
37
37
  import { invoiceQueue } from '../queues/invoice';
38
38
  import { paymentQueue } from '../queues/payment';
39
39
  import { subscriptionQueue } from '../queues/subscription';
@@ -304,6 +304,13 @@ export async function startCheckoutSessionFromPaymentLink(id: string, req: Reque
304
304
  raw.created_via = 'portal';
305
305
  raw.currency_id = link.currency_id || req.currency.id;
306
306
  raw.payment_link_id = link.id;
307
+ raw.subscription_data = merge(
308
+ {
309
+ description: '',
310
+ trial_period_days: 0,
311
+ },
312
+ getDataObjectFromQuery(req.query, 'subscription_data')
313
+ );
307
314
 
308
315
  if (link.after_completion?.hosted_confirmation?.custom_message) {
309
316
  raw.payment_intent_data = {
@@ -332,7 +339,7 @@ export async function startCheckoutSessionFromPaymentLink(id: string, req: Reque
332
339
  } else {
333
340
  raw.metadata = {
334
341
  ...link.metadata,
335
- ...getMetadataFromQuery(req.query),
342
+ ...getDataObjectFromQuery(req.query),
336
343
  days_until_due: getDaysUntilDue(req.query),
337
344
  passport: await checkPassportForPaymentLink(link),
338
345
  preview: '1',
@@ -341,7 +348,7 @@ export async function startCheckoutSessionFromPaymentLink(id: string, req: Reque
341
348
  } else {
342
349
  raw.metadata = {
343
350
  ...link.metadata,
344
- ...getMetadataFromQuery(req.query),
351
+ ...getDataObjectFromQuery(req.query),
345
352
  days_until_due: getDaysUntilDue(req.query),
346
353
  passport: await checkPassportForPaymentLink(link),
347
354
  };
@@ -629,6 +636,7 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
629
636
  pending_setup_intent: setupIntent?.id,
630
637
  });
631
638
  } else {
639
+ // FIXME: @wangshijun respect all checkoutSession.subscription_data fields
632
640
  const setup = getSubscriptionCreateSetup(lineItems, paymentCurrency.id, trialInDays);
633
641
  subscription = await Subscription.create({
634
642
  livemode: !!checkoutSession.livemode,
@@ -637,7 +645,7 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
637
645
  status: 'incomplete',
638
646
  current_period_start: setup.period.start,
639
647
  current_period_end: setup.period.end,
640
- billing_cycle_anchor: setup.cycle.anchor,
648
+ billing_cycle_anchor: checkoutSession.subscription_data?.billing_cycle_anchor || setup.cycle.anchor,
641
649
  start_date: dayjs().unix(),
642
650
  trail_end: setup.trail.end,
643
651
  trail_start: setup.trail.start,
@@ -651,7 +659,8 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
651
659
  default_payment_method_id: paymentMethod.id,
652
660
  cancel_at_period_end: false,
653
661
  collection_method: 'charge_automatically',
654
- proration_behavior: 'none',
662
+ description: checkoutSession.subscription_data?.description || '',
663
+ proration_behavior: checkoutSession.subscription_data?.proration_behavior || 'none',
655
664
  payment_behavior: 'default_incomplete',
656
665
  days_until_due: checkoutSession.metadata?.days_until_due,
657
666
  metadata: checkoutSession.metadata as any,
@@ -688,7 +697,6 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
688
697
  }
689
698
  }
690
699
 
691
- let isPaymentFromBalance = false;
692
700
  const fastCheckoutAmount = getFastCheckoutAmount(
693
701
  lineItems,
694
702
  checkoutSession.mode,
@@ -709,11 +717,23 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
709
717
  customer,
710
718
  amount: fastCheckoutAmount,
711
719
  });
712
- if (balance.sufficient) {
713
- if (checkoutSession.mode === 'payment' && paymentIntent) {
714
- await paymentIntent.update({ status: 'requires_capture' });
715
- logger.info(`CheckoutSession ${checkoutSession.id} will pay from balance ${paymentIntent?.id}`);
720
+ // if we can complete purchase without any wallet interaction
721
+ const delegation = await isDelegationSufficientForPayment({
722
+ paymentMethod,
723
+ paymentCurrency,
724
+ userDid: customer.did,
725
+ amount: fastCheckoutAmount,
726
+ });
716
727
 
728
+ if (checkoutSession.mode === 'payment' && paymentIntent) {
729
+ if (balance.sufficient) {
730
+ logger.info(`CheckoutSession ${checkoutSession.id} will pay from balance ${paymentIntent?.id}`);
731
+ }
732
+ if (delegation.sufficient) {
733
+ logger.info(`CheckoutSession ${checkoutSession.id} will pay from delegation ${paymentIntent?.id}`);
734
+ }
735
+ if (balance.sufficient || delegation.sufficient) {
736
+ await paymentIntent.update({ status: 'requires_capture' });
717
737
  const { invoice } = await ensureInvoiceForCheckout({ checkoutSession, customer, paymentIntent });
718
738
  if (invoice) {
719
739
  await invoice.update({ auto_advance: true, payment_settings: paymentSettings });
@@ -724,22 +744,15 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
724
744
  job: { paymentIntentId: paymentIntent.id, paymentSettings, retryOnError: false },
725
745
  });
726
746
  }
727
-
728
- isPaymentFromBalance = true;
729
747
  }
730
748
  }
731
749
 
732
- // if we can complete purchase without any wallet interaction
733
- const delegation = await isDelegationSufficientForPayment({
734
- paymentMethod,
735
- paymentCurrency,
736
- userDid: customer.did,
737
- amount: fastCheckoutAmount,
738
- });
739
-
740
- if (delegation.sufficient) {
741
- // all subscription payments are done after delegation
742
- if (checkoutSession.mode === 'subscription' && subscription) {
750
+ // all subscription payments are done after delegation
751
+ if (checkoutSession.mode === 'subscription' && subscription) {
752
+ if (
753
+ delegation.sufficient || // we can pay from delegation
754
+ (balance.sufficient && ['NO_TOKEN', 'NO_ENOUGH_TOKEN'].includes(delegation.reason as string)) // we can pay from balance
755
+ ) {
743
756
  await subscription.update({ payment_settings: paymentSettings });
744
757
 
745
758
  const { invoice } = await ensureInvoiceForCheckout({ checkoutSession, customer, subscription });
@@ -752,28 +765,17 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
752
765
  runAt: subscription.trail_end || subscription.current_period_end,
753
766
  });
754
767
  }
755
- if (checkoutSession.mode === 'payment' && paymentIntent && !isPaymentFromBalance) {
756
- logger.info(`CheckoutSession ${checkoutSession.id} will pay from delegation ${paymentIntent?.id}`);
757
- const { invoice } = await ensureInvoiceForCheckout({ checkoutSession, customer, paymentIntent });
758
- if (invoice) {
759
- await invoice.update({ auto_advance: true, payment_settings: paymentSettings });
760
- invoiceQueue.push({ id: invoice.id, job: { invoiceId: invoice.id, retryOnError: false } });
761
- } else {
762
- await paymentIntent.update({ status: 'requires_capture' });
763
- paymentQueue.push({
764
- id: paymentIntent.id,
765
- job: { paymentIntentId: paymentIntent.id, paymentSettings, retryOnError: false },
766
- });
767
- }
768
- }
769
- if (checkoutSession.mode === 'setup' && setupIntent && subscription) {
768
+ }
769
+
770
+ if (checkoutSession.mode === 'setup' && setupIntent && subscription) {
771
+ if (delegation.sufficient) {
770
772
  await setupIntent.update({ status: 'succeeded', ...paymentSettings });
771
773
  await subscription.update({
772
774
  status: subscription.trail_end ? 'trialing' : 'active',
773
775
  payment_settings: paymentSettings,
774
776
  });
775
777
  await checkoutSession.update({ status: 'complete', payment_status: 'no_payment_required' });
776
- logger.info(`CheckoutSession ${checkoutSession.id} updated on payment done ${paymentIntent?.id}`);
778
+ logger.info(`CheckoutSession ${checkoutSession.id} updated on setup done ${setupIntent.id}`);
777
779
  }
778
780
  }
779
781
 
@@ -25,17 +25,17 @@ export default {
25
25
  const { invoiceId } = extraParams;
26
26
  const { invoice, paymentIntent, paymentCurrency, paymentMethod } = await ensureInvoiceForCollect(invoiceId);
27
27
 
28
- const tokens = [{ address: paymentCurrency.contract as string, value: invoice.amount_due }];
29
- // @ts-ignore
30
- const itx: TransferV3Tx = {
31
- outputs: [{ owner: wallet.address, tokens, assets: [] }],
32
- data: getTxMetadata({
33
- paymentIntentId: paymentIntent.id,
34
- invoiceId,
35
- }),
36
- };
37
-
38
28
  if (paymentMethod.type === 'arcblock') {
29
+ const tokens = [{ address: paymentCurrency.contract as string, value: invoice.amount_due }];
30
+ // @ts-ignore
31
+ const itx: TransferV3Tx = {
32
+ outputs: [{ owner: wallet.address, tokens, assets: [] }],
33
+ data: getTxMetadata({
34
+ paymentIntentId: paymentIntent.id,
35
+ invoiceId,
36
+ }),
37
+ };
38
+
39
39
  return {
40
40
  prepareTx: {
41
41
  type: 'TransferV3Tx',
@@ -93,11 +93,11 @@ export default {
93
93
  // cleanup the queue
94
94
  let exist = await paymentQueue.get(paymentIntent.id);
95
95
  if (exist) {
96
- await paymentQueue.cancel(paymentIntent.id);
96
+ await paymentQueue.delete(paymentIntent.id);
97
97
  }
98
98
  exist = await invoiceQueue.get(invoice.id);
99
99
  if (exist) {
100
- await invoiceQueue.cancel(invoice.id);
100
+ await invoiceQueue.delete(invoice.id);
101
101
  }
102
102
 
103
103
  if (invoice.subscription_id) {
@@ -32,21 +32,18 @@ export default {
32
32
  }
33
33
 
34
34
  if (paymentMethod.type === 'arcblock') {
35
+ const items = checkoutSession.line_items as TLineItemExpanded[];
35
36
  const address = toDelegateAddress(userDid, wallet.address);
36
- const amount = getFastCheckoutAmount(
37
- checkoutSession.line_items as TLineItemExpanded[],
38
- checkoutSession.mode,
39
- paymentCurrency.id
40
- );
37
+ const amount = getFastCheckoutAmount(items, checkoutSession.mode, paymentCurrency.id);
41
38
 
42
- const tokenLimits = await getTokenLimitsForDelegation(
43
- checkoutSession,
39
+ const tokenLimits = await getTokenLimitsForDelegation(items, paymentMethod, paymentCurrency, address, amount);
40
+ const tokenRequirements = await getTokenRequirements({
41
+ items,
42
+ mode: checkoutSession.mode,
43
+ includeFreeTrial: !!checkoutSession.subscription_data?.trial_period_days,
44
44
  paymentMethod,
45
45
  paymentCurrency,
46
- address,
47
- amount
48
- );
49
- const tokenRequirements = await getTokenRequirements(checkoutSession, paymentMethod, paymentCurrency);
46
+ });
50
47
 
51
48
  return {
52
49
  signature: {
@@ -4,7 +4,12 @@ import { estimateMaxGasForTx, hasStakedForGas } from '../../integrations/blockch
4
4
  import { blocklet } from '../../libs/auth';
5
5
  import dayjs from '../../libs/dayjs';
6
6
  import logger from '../../libs/logger';
7
- import { getFastCheckoutAmount, getPriceUintAmountByCurrency, getStatementDescriptor } from '../../libs/session';
7
+ import {
8
+ expandLineItems,
9
+ getFastCheckoutAmount,
10
+ getPriceUintAmountByCurrency,
11
+ getStatementDescriptor,
12
+ } from '../../libs/session';
8
13
  import type { TLineItemExpanded } from '../../store/models';
9
14
  import { CheckoutSession } from '../../store/models/checkout-session';
10
15
  import { Customer } from '../../store/models/customer';
@@ -14,6 +19,7 @@ import { PaymentCurrency } from '../../store/models/payment-currency';
14
19
  import { PaymentIntent } from '../../store/models/payment-intent';
15
20
  import { PaymentMethod } from '../../store/models/payment-method';
16
21
  import { Price } from '../../store/models/price';
22
+ import { Product } from '../../store/models/product';
17
23
  import { SetupIntent } from '../../store/models/setup-intent';
18
24
  import { Subscription } from '../../store/models/subscription';
19
25
  import { SubscriptionItem } from '../../store/models/subscription-item';
@@ -25,6 +31,7 @@ type Result = {
25
31
  subscription?: Subscription;
26
32
  paymentCurrency: PaymentCurrency;
27
33
  paymentMethod: PaymentMethod;
34
+ invoice?: Invoice;
28
35
  };
29
36
 
30
37
  export async function ensureCheckoutSession(checkoutSessionId: string) {
@@ -440,30 +447,30 @@ export async function ensureInvoiceAndItems({
440
447
  export async function ensureInvoiceForCollect(invoiceId: string) {
441
448
  const invoice = await Invoice.findByPk(invoiceId);
442
449
  if (!invoice) {
443
- throw new Error('Invoice not found');
450
+ throw new Error(`Invoice ${invoiceId} not found`);
444
451
  }
445
452
  if (invoice.status === 'paid') {
446
- throw new Error('Invoice already paid');
453
+ throw new Error(`Invoice ${invoiceId} already paid`);
447
454
  }
448
455
  if (invoice.status === 'void') {
449
- throw new Error('Invoice already void');
456
+ throw new Error(`Invoice ${invoiceId} already void`);
450
457
  }
451
458
  if (invoice.status === 'draft') {
452
- throw new Error('Invoice is draft');
459
+ throw new Error(`Invoice ${invoiceId} is draft`);
453
460
  }
454
461
 
455
462
  const paymentIntent = await PaymentIntent.findByPk(invoice.payment_intent_id);
456
463
  if (!paymentIntent) {
457
- throw new Error('Payment intent not found for invoice');
464
+ throw new Error(`Payment intent not found for invoice ${invoiceId}`);
458
465
  }
459
466
  if (paymentIntent.status === 'canceled') {
460
- throw new Error('Payment intent already canceled');
467
+ throw new Error(`Payment intent already canceled for invoice ${invoiceId}`);
461
468
  }
462
469
  if (paymentIntent.status === 'succeeded') {
463
- throw new Error('Payment intent already succeeded');
470
+ throw new Error(`Payment intent already succeeded for invoice ${invoiceId}`);
464
471
  }
465
472
  if (paymentIntent.status === 'processing') {
466
- throw new Error('Payment intent processing');
473
+ throw new Error(`Payment intent processing for invoice ${invoiceId}`);
467
474
  }
468
475
 
469
476
  const paymentCurrency = await PaymentCurrency.findByPk(paymentIntent.currency_id);
@@ -501,18 +508,23 @@ export function getAuthPrincipalClaim(method: PaymentMethod, action: string) {
501
508
  };
502
509
  }
503
510
 
504
- export async function getTokenRequirements(
505
- checkoutSession: CheckoutSession,
506
- paymentMethod: PaymentMethod,
507
- paymentCurrency: PaymentCurrency
508
- ) {
511
+ export type TokenRequirementArgs = {
512
+ items: TLineItemExpanded[];
513
+ mode: string;
514
+ includeFreeTrial: boolean;
515
+ paymentMethod: PaymentMethod;
516
+ paymentCurrency: PaymentCurrency;
517
+ };
518
+
519
+ export async function getTokenRequirements({
520
+ items,
521
+ mode,
522
+ includeFreeTrial,
523
+ paymentMethod,
524
+ paymentCurrency,
525
+ }: TokenRequirementArgs) {
509
526
  const tokenRequirements = [];
510
- let amount = getFastCheckoutAmount(
511
- checkoutSession.line_items as TLineItemExpanded[],
512
- checkoutSession.mode,
513
- paymentCurrency.id,
514
- !!checkoutSession.subscription_data?.trial_period_days
515
- );
527
+ let amount = getFastCheckoutAmount(items, mode, paymentCurrency.id, !!includeFreeTrial);
516
528
 
517
529
  // If the app has not staked, we need to add the gas fee to the amount
518
530
  if ((await hasStakedForGas(paymentMethod)) === false) {
@@ -538,3 +550,52 @@ export async function getTokenRequirements(
538
550
 
539
551
  return tokenRequirements;
540
552
  }
553
+
554
+ export async function ensureSubscription(subscriptionId: string): Promise<Result> {
555
+ const subscription = await Subscription.findByPk(subscriptionId);
556
+ if (!subscription) {
557
+ throw new Error(`Subscription not found: ${subscriptionId}`);
558
+ }
559
+ if (subscription.status !== 'past_due') {
560
+ throw new Error(`Subscription ${subscriptionId} is not in past_due status`);
561
+ }
562
+
563
+ const paymentCurrencyId = subscription.currency_id;
564
+ const paymentMethodId = subscription.default_payment_method_id;
565
+
566
+ const [paymentMethod, paymentCurrency, invoice] = await Promise.all([
567
+ PaymentMethod.findByPk(paymentMethodId),
568
+ PaymentCurrency.findByPk(paymentCurrencyId),
569
+ Invoice.findByPk(subscription.latest_invoice_id),
570
+ ]);
571
+ if (!paymentMethod) {
572
+ throw new Error(`Payment method not found for subscription ${subscriptionId}`);
573
+ }
574
+ if (!paymentCurrency) {
575
+ throw new Error(`Payment currency not found for subscription ${subscriptionId}`);
576
+ }
577
+
578
+ if (['arcblock', 'ethereum'].includes(paymentMethod.type) === false) {
579
+ throw new Error(`Payment method ${paymentMethod.type} should not be here`);
580
+ }
581
+
582
+ const items = (await SubscriptionItem.findAll({ where: { subscription_id: subscription.id } })).map((x) =>
583
+ x.toJSON()
584
+ );
585
+ const products = (await Product.findAll()).map((x) => x.toJSON());
586
+ const prices = (await Price.findAll()).map((x) => x.toJSON());
587
+ // @ts-ignore
588
+ expandLineItems(items, products, prices);
589
+ // @ts-ignore
590
+ subscription.items = await Price.expand(items);
591
+
592
+ return {
593
+ // @ts-ignore
594
+ checkoutSession: null,
595
+ subscription,
596
+ paymentMethod,
597
+ paymentCurrency,
598
+ // @ts-ignore
599
+ invoice,
600
+ };
601
+ }
@@ -34,20 +34,17 @@ export default {
34
34
  }
35
35
 
36
36
  if (paymentMethod.type === 'arcblock') {
37
+ const items = checkoutSession.line_items as TLineItemExpanded[];
37
38
  const address = toDelegateAddress(userDid, wallet.address);
38
- const amount = getFastCheckoutAmount(
39
- checkoutSession.line_items as TLineItemExpanded[],
40
- checkoutSession.mode,
41
- paymentCurrency.id
42
- );
43
- const tokenLimits = await getTokenLimitsForDelegation(
44
- checkoutSession,
39
+ const amount = getFastCheckoutAmount(items, checkoutSession.mode, paymentCurrency.id);
40
+ const tokenLimits = await getTokenLimitsForDelegation(items, paymentMethod, paymentCurrency, address, amount);
41
+ const tokenRequirements = await getTokenRequirements({
42
+ items,
43
+ mode: checkoutSession.mode,
44
+ includeFreeTrial: !!checkoutSession.subscription_data?.trial_period_days,
45
45
  paymentMethod,
46
46
  paymentCurrency,
47
- address,
48
- amount
49
- );
50
- const tokenRequirements = await getTokenRequirements(checkoutSession, paymentMethod, paymentCurrency);
47
+ });
51
48
 
52
49
  return {
53
50
  signature: {