payment-kit 1.13.72 → 1.13.74

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 (66) hide show
  1. package/api/src/{schedule → crons}/base.ts +1 -1
  2. package/api/src/index.ts +7 -7
  3. package/api/src/integrations/stripe/handlers/customer.ts +24 -0
  4. package/api/src/integrations/stripe/handlers/index.ts +4 -0
  5. package/api/src/integrations/stripe/handlers/payment-intent.ts +1 -1
  6. package/api/src/integrations/stripe/resource.ts +1 -1
  7. package/api/src/libs/audit.ts +34 -28
  8. package/api/src/libs/payment.ts +48 -4
  9. package/api/src/libs/queue/index.ts +18 -1
  10. package/api/src/libs/queue/store.ts +6 -5
  11. package/api/src/libs/session.ts +13 -12
  12. package/api/src/libs/subscription.ts +26 -0
  13. package/api/src/libs/util.ts +5 -1
  14. package/api/src/{jobs → queues}/checkout-session.ts +23 -1
  15. package/api/src/{jobs → queues}/invoice.ts +15 -6
  16. package/api/src/{jobs → queues}/payment.ts +182 -30
  17. package/api/src/{jobs → queues}/subscription.ts +36 -104
  18. package/api/src/{jobs → queues}/webhook.ts +2 -0
  19. package/api/src/routes/checkout-sessions.ts +72 -26
  20. package/api/src/routes/connect/collect.ts +2 -2
  21. package/api/src/routes/connect/pay.ts +1 -1
  22. package/api/src/routes/connect/setup.ts +10 -3
  23. package/api/src/routes/connect/shared.ts +98 -49
  24. package/api/src/routes/connect/subscribe.ts +10 -4
  25. package/api/src/routes/pricing-table.ts +2 -0
  26. package/api/src/routes/subscription-items.ts +1 -1
  27. package/api/src/routes/subscriptions.ts +434 -13
  28. package/api/src/store/migrate.ts +0 -1
  29. package/api/src/store/migrations/20231204-subupdate.ts +50 -0
  30. package/api/src/store/migrations/20231220-setup-intent.ts +22 -0
  31. package/api/src/store/models/checkout-session.ts +8 -0
  32. package/api/src/store/models/customer.ts +52 -15
  33. package/api/src/store/models/invoice-item.ts +6 -1
  34. package/api/src/store/models/invoice.ts +41 -22
  35. package/api/src/store/models/payment-intent.ts +4 -0
  36. package/api/src/store/models/setup-intent.ts +4 -0
  37. package/api/src/store/models/subscription-item.ts +0 -4
  38. package/api/src/store/models/subscription.ts +77 -44
  39. package/api/src/store/models/types.ts +1 -0
  40. package/api/src/store/sequelize.ts +6 -0
  41. package/api/third.d.ts +2 -0
  42. package/blocklet.yml +1 -1
  43. package/jest.config.js +14 -0
  44. package/package.json +24 -19
  45. package/src/components/blockchain/tx.tsx +20 -11
  46. package/src/components/checkout/form/index.tsx +1 -1
  47. package/src/components/invoice/table.tsx +58 -19
  48. package/src/components/layout/admin.tsx +17 -5
  49. package/src/components/portal/invoice/list.tsx +12 -8
  50. package/src/components/portal/subscription/list.tsx +114 -77
  51. package/src/components/subscription/status.tsx +21 -19
  52. package/src/global.css +4 -0
  53. package/src/locales/en.tsx +14 -1
  54. package/src/locales/zh.tsx +14 -0
  55. package/src/pages/admin/customers/customers/detail.tsx +47 -3
  56. package/src/pages/admin/overview.tsx +21 -1
  57. package/src/pages/admin/payments/intents/detail.tsx +12 -3
  58. package/src/pages/customer/invoice.tsx +15 -1
  59. package/src/pages/customer/subscription/index.tsx +9 -2
  60. package/tests/api/libs/subscription.spec.ts +45 -0
  61. /package/api/src/{schedule → crons}/index.ts +0 -0
  62. /package/api/src/{schedule → crons}/interface/diff.ts +0 -0
  63. /package/api/src/{schedule → crons}/subscription-trail-will-end.ts +0 -0
  64. /package/api/src/{schedule → crons}/subscription-will-renew.ts +0 -0
  65. /package/api/src/{jobs → queues}/event.ts +0 -0
  66. /package/api/src/{jobs → queues}/notification.ts +0 -0
@@ -16,12 +16,9 @@ import { checkPassportForPaymentLink } from '../integrations/blocklet/passport';
16
16
  import { handleStripePaymentSucceed } from '../integrations/stripe/handlers/payment-intent';
17
17
  import { handleStripeSubscriptionSucceed } from '../integrations/stripe/handlers/subscription';
18
18
  import { ensureStripePaymentIntent, ensureStripeSubscription } from '../integrations/stripe/resource';
19
- import { invoiceQueue } from '../jobs/invoice';
20
- import { paymentQueue } from '../jobs/payment';
21
- import { subscriptionQueue } from '../jobs/subscription';
22
19
  import dayjs from '../libs/dayjs';
23
20
  import logger from '../libs/logger';
24
- import { isDelegationSufficientForPayment } from '../libs/payment';
21
+ import { isBalanceSufficientForPayment, isDelegationSufficientForPayment } from '../libs/payment';
25
22
  import { authenticate } from '../libs/security';
26
23
  import {
27
24
  canUpsell,
@@ -35,11 +32,15 @@ import {
35
32
  getSupportedPaymentMethods,
36
33
  isLineItemAligned,
37
34
  } from '../libs/session';
35
+ import { getDaysUntilDue } from '../libs/subscription';
38
36
  import { createCodeGenerator, formatMetadata, getMetadataFromQuery } from '../libs/util';
37
+ import { invoiceQueue } from '../queues/invoice';
38
+ import { paymentQueue } from '../queues/payment';
39
+ import { subscriptionQueue } from '../queues/subscription';
39
40
  import type { TPriceExpanded, TProductExpanded } from '../store/models';
40
41
  import { CheckoutSession } from '../store/models/checkout-session';
41
42
  import { Customer } from '../store/models/customer';
42
- import { PaymentCurrency, TPaymentCurrency } from '../store/models/payment-currency';
43
+ import { PaymentCurrency } from '../store/models/payment-currency';
43
44
  import { PaymentIntent } from '../store/models/payment-intent';
44
45
  import { PaymentLink } from '../store/models/payment-link';
45
46
  import { PaymentMethod } from '../store/models/payment-method';
@@ -186,9 +187,8 @@ export const formatCheckoutSession = async (payload: any, throwOnEmptyItems = tr
186
187
 
187
188
  export async function getCheckoutSessionAmounts(checkoutSession: CheckoutSession) {
188
189
  const items = await Price.expand(checkoutSession.line_items);
189
- const currency = await PaymentCurrency.findByPk(checkoutSession.currency_id);
190
190
  const includeTrial = !!checkoutSession.subscription_data?.trial_period_days;
191
- const amount = getCheckoutAmount(items, currency as TPaymentCurrency, includeTrial);
191
+ const amount = getCheckoutAmount(items, checkoutSession.currency_id, includeTrial);
192
192
  return {
193
193
  amount_subtotal: amount.subtotal,
194
194
  amount_total: amount.total,
@@ -333,6 +333,7 @@ export async function startCheckoutSessionFromPaymentLink(id: string, req: Reque
333
333
  raw.metadata = {
334
334
  ...link.metadata,
335
335
  ...getMetadataFromQuery(req.query),
336
+ days_until_due: getDaysUntilDue(req.query),
336
337
  passport: await checkPassportForPaymentLink(link),
337
338
  preview: '1',
338
339
  };
@@ -341,6 +342,7 @@ export async function startCheckoutSessionFromPaymentLink(id: string, req: Reque
341
342
  raw.metadata = {
342
343
  ...link.metadata,
343
344
  ...getMetadataFromQuery(req.query),
345
+ days_until_due: getDaysUntilDue(req.query),
344
346
  passport: await checkPassportForPaymentLink(link),
345
347
  };
346
348
  }
@@ -445,7 +447,7 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
445
447
  // always update payment amount in case currency has changed
446
448
  const lineItems = await Price.expand(checkoutSession.line_items, { product: true, upsell: true });
447
449
  const trialInDays = checkoutSession.subscription_data?.trial_period_days || 0;
448
- const amount = getCheckoutAmount(lineItems, paymentCurrency, !!trialInDays);
450
+ const amount = getCheckoutAmount(lineItems, paymentCurrency.id, !!trialInDays);
449
451
  await checkoutSession.update({
450
452
  amount_subtotal: amount.subtotal,
451
453
  amount_total: amount.total,
@@ -581,7 +583,7 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
581
583
  payment_method_id: paymentMethod.id,
582
584
  last_setup_error: null,
583
585
  });
584
- logger.info('setup intent for checkout session reset', {
586
+ logger.info('setupIntent reset on checkout session submit', {
585
587
  session: checkoutSession.id,
586
588
  intent: setupIntent.id,
587
589
  });
@@ -599,13 +601,13 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
599
601
  usage: 'off_session',
600
602
  metadata: checkoutSession.metadata,
601
603
  });
604
+
605
+ // persist setup intent id
606
+ await checkoutSession.update({ setup_intent_id: setupIntent.id });
602
607
  logger.info('setupIntent created on checkout session submit', {
603
608
  session: checkoutSession.id,
604
609
  intent: setupIntent.id,
605
610
  });
606
-
607
- // persist setup intent id
608
- await checkoutSession.update({ setup_intent_id: setupIntent.id });
609
611
  }
610
612
  }
611
613
 
@@ -627,7 +629,7 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
627
629
  pending_setup_intent: setupIntent?.id,
628
630
  });
629
631
  } else {
630
- const setup = getSubscriptionCreateSetup(lineItems, paymentCurrency, trialInDays);
632
+ const setup = getSubscriptionCreateSetup(lineItems, paymentCurrency.id, trialInDays);
631
633
  subscription = await Subscription.create({
632
634
  livemode: !!checkoutSession.livemode,
633
635
  currency_id: paymentCurrency.id,
@@ -649,6 +651,9 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
649
651
  default_payment_method_id: paymentMethod.id,
650
652
  cancel_at_period_end: false,
651
653
  collection_method: 'charge_automatically',
654
+ proration_behavior: 'none',
655
+ payment_behavior: 'default_incomplete',
656
+ days_until_due: checkoutSession.metadata?.days_until_due,
652
657
  metadata: checkoutSession.metadata as any,
653
658
  });
654
659
 
@@ -678,30 +683,61 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
678
683
  items: items.map((x) => x.id),
679
684
  });
680
685
 
681
- // lock prices used by this subscription
682
- await Price.update({ locked: true }, { where: { id: lineItems.map((x) => x.upsell_price_id || x.price_id) } });
683
-
684
686
  // persist subscription id
685
687
  await checkoutSession.update({ subscription_id: subscription.id });
686
688
  }
687
689
  }
688
690
 
691
+ let isPaymentFromBalance = false;
692
+ const fastCheckoutAmount = getFastCheckoutAmount(
693
+ lineItems,
694
+ checkoutSession.mode,
695
+ paymentCurrency.id,
696
+ !!trialInDays
697
+ );
698
+ const paymentSettings = {
699
+ payment_method_types: checkoutSession.payment_method_types,
700
+ payment_method_options: {
701
+ arcblock: { payer: customer.did },
702
+ },
703
+ };
704
+
705
+ // if we can complete purchase with customer balance
706
+ const balance = isBalanceSufficientForPayment({
707
+ paymentMethod,
708
+ paymentCurrency,
709
+ customer,
710
+ amount: fastCheckoutAmount,
711
+ });
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}`);
716
+
717
+ const { invoice } = await ensureInvoiceForCheckout({ checkoutSession, customer, paymentIntent });
718
+ if (invoice) {
719
+ await invoice.update({ auto_advance: true, payment_settings: paymentSettings });
720
+ invoiceQueue.push({ id: invoice.id, job: { invoiceId: invoice.id, retryOnError: false } });
721
+ } else {
722
+ paymentQueue.push({
723
+ id: paymentIntent.id,
724
+ job: { paymentIntentId: paymentIntent.id, paymentSettings, retryOnError: false },
725
+ });
726
+ }
727
+
728
+ isPaymentFromBalance = true;
729
+ }
730
+ }
731
+
689
732
  // if we can complete purchase without any wallet interaction
690
- const fastCheckoutAmount = getFastCheckoutAmount(lineItems, checkoutSession.mode, paymentCurrency, !!trialInDays);
691
733
  const delegation = await isDelegationSufficientForPayment({
692
734
  paymentMethod,
693
735
  paymentCurrency,
694
736
  userDid: customer.did,
695
737
  amount: fastCheckoutAmount,
696
738
  });
697
- if (delegation.sufficient) {
698
- const paymentSettings = {
699
- payment_method_types: checkoutSession.payment_method_types,
700
- payment_method_options: {
701
- arcblock: { payer: delegation.delegator as string },
702
- },
703
- };
704
739
 
740
+ if (delegation.sufficient) {
705
741
  // all subscription payments are done after delegation
706
742
  if (checkoutSession.mode === 'subscription' && subscription) {
707
743
  await subscription.update({ payment_settings: paymentSettings });
@@ -716,7 +752,8 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
716
752
  runAt: subscription.trail_end || subscription.current_period_end,
717
753
  });
718
754
  }
719
- if (checkoutSession.mode === 'payment' && paymentIntent) {
755
+ if (checkoutSession.mode === 'payment' && paymentIntent && !isPaymentFromBalance) {
756
+ logger.info(`CheckoutSession ${checkoutSession.id} will pay from delegation ${paymentIntent?.id}`);
720
757
  const { invoice } = await ensureInvoiceForCheckout({ checkoutSession, customer, paymentIntent });
721
758
  if (invoice) {
722
759
  await invoice.update({ auto_advance: true, payment_settings: paymentSettings });
@@ -784,7 +821,16 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
784
821
  }
785
822
  }
786
823
 
787
- return res.json({ paymentIntent, setupIntent, stripeContext, subscription, checkoutSession, customer, delegation });
824
+ return res.json({
825
+ paymentIntent,
826
+ setupIntent,
827
+ stripeContext,
828
+ subscription,
829
+ checkoutSession,
830
+ customer,
831
+ delegation,
832
+ balance,
833
+ });
788
834
  } catch (err) {
789
835
  console.error(err);
790
836
  res.status(500).json({ code: err.code, error: err.message });
@@ -1,13 +1,13 @@
1
1
  import type { Transaction, TransferV3Tx } from '@ocap/client';
2
2
  import { fromAddress } from '@ocap/wallet';
3
3
 
4
- import { invoiceQueue } from '../../jobs/invoice';
5
- import { handlePaymentSucceed, paymentQueue } from '../../jobs/payment';
6
4
  import type { CallbackArgs } from '../../libs/auth';
7
5
  import { wallet } from '../../libs/auth';
8
6
  import { events } from '../../libs/event';
9
7
  import { getGasPayerExtra } from '../../libs/payment';
10
8
  import { getTxMetadata } from '../../libs/util';
9
+ import { invoiceQueue } from '../../queues/invoice';
10
+ import { handlePaymentSucceed, paymentQueue } from '../../queues/payment';
11
11
  import { Subscription } from '../../store/models';
12
12
  import { ensureInvoiceForCollect, getAuthPrincipalClaim } from './shared';
13
13
 
@@ -1,11 +1,11 @@
1
1
  import type { Transaction, TransferV3Tx } from '@ocap/client';
2
2
  import { fromAddress } from '@ocap/wallet';
3
3
 
4
- import { handlePaymentSucceed } from '../../jobs/payment';
5
4
  import type { CallbackArgs } from '../../libs/auth';
6
5
  import { wallet } from '../../libs/auth';
7
6
  import { getGasPayerExtra } from '../../libs/payment';
8
7
  import { getTxMetadata } from '../../libs/util';
8
+ import { handlePaymentSucceed } from '../../queues/payment';
9
9
  import { ensureInvoiceForCheckout, ensurePaymentIntent, getAuthPrincipalClaim } from './shared';
10
10
 
11
11
  export default {
@@ -3,12 +3,12 @@ import { toDelegateAddress } from '@arcblock/did-util';
3
3
  import type { Transaction } from '@ocap/client';
4
4
  import { fromPublicKey } from '@ocap/wallet';
5
5
 
6
- import { subscriptionQueue } from '../../jobs/subscription';
7
6
  import type { CallbackArgs } from '../../libs/auth';
8
7
  import { wallet } from '../../libs/auth';
9
8
  import { getGasPayerExtra, getTokenLimitsForDelegation } from '../../libs/payment';
10
9
  import { getFastCheckoutAmount } from '../../libs/session';
11
10
  import { OCAP_PAYMENT_TX_TYPE, getTxMetadata } from '../../libs/util';
11
+ import { subscriptionQueue } from '../../queues/subscription';
12
12
  import type { TLineItemExpanded } from '../../store/models';
13
13
  import { ensureSetupIntent, getAuthPrincipalClaim, getTokenRequirements } from './shared';
14
14
 
@@ -36,9 +36,16 @@ export default {
36
36
  const amount = getFastCheckoutAmount(
37
37
  checkoutSession.line_items as TLineItemExpanded[],
38
38
  checkoutSession.mode,
39
- paymentCurrency
39
+ paymentCurrency.id
40
+ );
41
+
42
+ const tokenLimits = await getTokenLimitsForDelegation(
43
+ checkoutSession,
44
+ paymentMethod,
45
+ paymentCurrency,
46
+ address,
47
+ amount
40
48
  );
41
- const tokenLimits = await getTokenLimitsForDelegation(paymentMethod, paymentCurrency, address, amount);
42
49
  const tokenRequirements = await getTokenRequirements(checkoutSession, paymentMethod, paymentCurrency);
43
50
 
44
51
  return {
@@ -4,11 +4,11 @@ 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, getStatementDescriptor } from '../../libs/session';
7
+ import { getFastCheckoutAmount, getPriceUintAmountByCurrency, getStatementDescriptor } from '../../libs/session';
8
8
  import type { TLineItemExpanded } from '../../store/models';
9
9
  import { CheckoutSession } from '../../store/models/checkout-session';
10
10
  import { Customer } from '../../store/models/customer';
11
- import { Invoice } from '../../store/models/invoice';
11
+ import { Invoice, TInvoice } from '../../store/models/invoice';
12
12
  import { InvoiceItem } from '../../store/models/invoice-item';
13
13
  import { PaymentCurrency } from '../../store/models/payment-currency';
14
14
  import { PaymentIntent } from '../../store/models/payment-intent';
@@ -135,16 +135,16 @@ export async function ensureSetupIntent(checkoutSessionId: string, userDid?: str
135
135
  if (checkoutSession.setup_intent_id) {
136
136
  setupIntent = await SetupIntent.findByPk(checkoutSession.setup_intent_id);
137
137
  if (!setupIntent) {
138
- throw new Error('Payment intent not found');
138
+ throw new Error('Setup intent not found');
139
139
  }
140
140
  if (setupIntent.status === 'succeeded') {
141
- throw new Error('Payment intent completed');
141
+ throw new Error('Setup intent completed');
142
142
  }
143
143
  if (setupIntent.status === 'canceled') {
144
- throw new Error('Payment intent canceled');
144
+ throw new Error('Setup intent canceled');
145
145
  }
146
146
  if (setupIntent.status === 'processing') {
147
- throw new Error('Payment intent processing');
147
+ throw new Error('Setup intent processing');
148
148
  }
149
149
 
150
150
  customerId = setupIntent.customer_id;
@@ -245,48 +245,109 @@ export async function ensureInvoiceForCheckout({
245
245
  };
246
246
  }
247
247
 
248
+ const { invoice, items } = await ensureInvoiceAndItems({
249
+ customer,
250
+ subscription,
251
+ lineItems: await Price.expand(checkoutSession.line_items, { product: true }),
252
+ trailing: !!checkoutSession.subscription_data?.trial_period_days,
253
+ metered: false,
254
+ props: {
255
+ livemode: checkoutSession.livemode,
256
+ description:
257
+ checkoutSession.invoice_creation?.invoice_data?.description ||
258
+ paymentIntent?.description ||
259
+ 'Subscription creation',
260
+ statement_descriptor: paymentIntent?.statement_descriptor || getStatementDescriptor(checkoutSession.line_items),
261
+ period_start: subscription?.current_period_start ?? 0,
262
+ period_end: subscription?.current_period_end ?? 0,
263
+
264
+ auto_advance: !paymentIntent,
265
+ billing_reason: subscription ? 'subscription_create' : 'manual',
266
+
267
+ currency_id: checkoutSession.currency_id,
268
+ payment_intent_id: paymentIntent?.id,
269
+ checkout_session_id: checkoutSession.id,
270
+
271
+ total: checkoutSession.amount_subtotal,
272
+
273
+ default_payment_method_id: (subscription?.default_payment_method_id ||
274
+ paymentIntent?.payment_method_id) as string,
275
+
276
+ custom_fields: checkoutSession.invoice_creation?.invoice_data?.custom_fields || [],
277
+ footer: checkoutSession.invoice_creation?.invoice_data?.footer || '',
278
+ metadata: checkoutSession.invoice_creation?.invoice_data?.metadata || {},
279
+ } as Invoice,
280
+ });
281
+ logger.info(`Invoice created for checkoutSession ${checkoutSession.id}: ${invoice.id}`);
282
+
283
+ // persist invoice id
284
+ await checkoutSession.update({ invoice_id: invoice.id });
285
+ if (paymentIntent) {
286
+ await paymentIntent.update({ invoice_id: invoice.id });
287
+ }
288
+ if (subscription) {
289
+ await subscription.update({ latest_invoice_id: invoice.id });
290
+ }
291
+
292
+ return { invoice, items };
293
+ }
294
+
295
+ export async function ensureInvoiceAndItems({
296
+ customer,
297
+ subscription,
298
+ props,
299
+ lineItems,
300
+ trailing,
301
+ metered,
302
+ }: {
303
+ customer: Customer;
304
+ subscription?: Subscription;
305
+ props: TInvoice;
306
+ lineItems: TLineItemExpanded[];
307
+ trailing: boolean; // do we have trailing
308
+ metered: boolean; // is the quantity metered
309
+ }): Promise<{ invoice: Invoice; items: InvoiceItem[] }> {
248
310
  const invoice = await Invoice.create({
249
- livemode: checkoutSession.livemode,
311
+ livemode: props.livemode,
250
312
  number: await customer.getInvoiceNumber(),
251
- description:
252
- checkoutSession.invoice_creation?.invoice_data?.description ||
253
- paymentIntent?.description ||
254
- 'Subscription creation',
255
- statement_descriptor: paymentIntent?.statement_descriptor || getStatementDescriptor(checkoutSession.line_items),
256
- period_start: subscription?.current_period_start ?? 0,
257
- period_end: subscription?.current_period_end ?? 0,
258
-
259
- auto_advance: !paymentIntent,
313
+ description: props.description,
314
+ statement_descriptor: props.statement_descriptor,
315
+ period_start: props.period_start,
316
+ period_end: props.period_end,
317
+
318
+ auto_advance: props.auto_advance,
260
319
  paid: false,
261
320
  paid_out_of_band: false,
262
321
 
263
- status: 'open',
322
+ status: props.status || 'open',
264
323
  collection_method: 'charge_automatically',
265
- billing_reason: subscription ? 'subscription_create' : 'manual',
324
+ billing_reason: props.billing_reason,
266
325
 
267
- currency_id: checkoutSession.currency_id,
326
+ currency_id: props.currency_id,
268
327
  customer_id: customer.id,
269
- payment_intent_id: paymentIntent?.id,
328
+ payment_intent_id: props.payment_intent_id || '',
270
329
  subscription_id: subscription?.id,
271
- checkout_session_id: checkoutSession.id,
330
+ checkout_session_id: props.checkout_session_id || '',
272
331
 
273
- subtotal: checkoutSession.amount_subtotal,
274
- subtotal_excluding_tax: checkoutSession.amount_subtotal,
332
+ subtotal: props.total || '0',
333
+ subtotal_excluding_tax: props.total || '0',
275
334
  tax: '0',
276
- total: checkoutSession.amount_total,
277
- amount_due: checkoutSession.amount_total,
335
+ total: props.total || '0',
336
+ amount_due: props.total || '0',
278
337
  amount_paid: '0',
279
- amount_remaining: checkoutSession.amount_total,
338
+ amount_remaining: props.total || '0',
280
339
  amount_shipping: '0',
281
340
 
282
341
  starting_balance: '0',
283
342
  ending_balance: '0',
343
+ starting_token_balance: {},
344
+ ending_token_balance: {},
284
345
 
285
346
  attempt_count: 0,
286
347
  attempted: false,
287
348
  // next_payment_attempt: undefined,
288
349
 
289
- custom_fields: checkoutSession.invoice_creation?.invoice_data?.custom_fields || [],
350
+ custom_fields: props.custom_fields || [],
290
351
  customer_address: customer.address,
291
352
  customer_email: customer.email,
292
353
  customer_name: customer.name,
@@ -302,31 +363,19 @@ export async function ensureInvoiceForCheckout({
302
363
  },
303
364
 
304
365
  payment_settings: subscription?.payment_settings,
305
- default_payment_method_id: (subscription?.default_payment_method_id || paymentIntent?.payment_method_id) as string,
366
+ default_payment_method_id: props.default_payment_method_id,
306
367
 
307
368
  account_country: '',
308
369
  account_name: '',
309
- footer: checkoutSession.invoice_creation?.invoice_data?.footer || '',
310
- metadata: checkoutSession.invoice_creation?.invoice_data?.metadata || {},
370
+ footer: props.footer || '',
371
+ metadata: props.metadata || {},
311
372
  });
312
- logger.info(`Invoice created for checkoutSession ${checkoutSession.id}: ${invoice.id}`);
313
-
314
- // persist invoice id
315
- await checkoutSession.update({ invoice_id: invoice.id });
316
- if (paymentIntent) {
317
- await paymentIntent.update({ invoice_id: invoice.id });
318
- }
319
- if (subscription) {
320
- await subscription.update({ latest_invoice_id: invoice.id });
321
- }
322
373
 
323
374
  // create invoice items: for those require payment this time
324
375
  const subscriptionItems = subscription
325
376
  ? await SubscriptionItem.findAll({ where: { subscription_id: subscription?.id } })
326
377
  : [];
327
- const lineItems = await Price.expand(checkoutSession.line_items, { product: true });
328
378
 
329
- const trailing = !!checkoutSession.subscription_data?.trial_period_days;
330
379
  const getLineSetup = (x: TLineItemExpanded) => {
331
380
  const price = x.upsell_price || x.price;
332
381
  if (price.type === 'recurring' && trailing) {
@@ -335,14 +384,14 @@ export async function ensureInvoiceForCheckout({
335
384
  // @ts-ignore
336
385
  description: trailing ? `${price.product.name} (trailing)` : price.product.name,
337
386
  period: {
338
- start: subscription?.current_period_start as number,
339
- end: subscription?.current_period_end as number,
387
+ start: props.period_start,
388
+ end: props.period_end,
340
389
  },
341
390
  };
342
391
  }
343
392
 
344
393
  return {
345
- amount: new BN(price.unit_amount).mul(new BN(x.quantity)).toString(),
394
+ amount: new BN(getPriceUintAmountByCurrency(price, props.currency_id)).mul(new BN(x.quantity)).toString(),
346
395
  // @ts-ignore
347
396
  description: price.product.name,
348
397
  period: undefined,
@@ -355,7 +404,7 @@ export async function ensureInvoiceForCheckout({
355
404
  const price = x.upsell_price || x.price;
356
405
  let { quantity } = x;
357
406
  if (price.type === 'recurring') {
358
- if (price.recurring?.usage_type === 'metered') {
407
+ if (price.recurring?.usage_type === 'metered' && !metered) {
359
408
  quantity = 0;
360
409
  }
361
410
  if (trailing) {
@@ -364,12 +413,12 @@ export async function ensureInvoiceForCheckout({
364
413
  }
365
414
 
366
415
  return InvoiceItem.create({
367
- livemode: checkoutSession.livemode,
416
+ livemode: !!props.livemode,
368
417
  amount: quantity > 0 ? setup.amount : '0',
369
418
  quantity,
370
419
  description: setup.description,
371
420
  period: setup.period,
372
- currency_id: checkoutSession.currency_id,
421
+ currency_id: props.currency_id,
373
422
  customer_id: customer.id,
374
423
  price_id: x.price_id,
375
424
  invoice_id: invoice.id,
@@ -461,7 +510,7 @@ export async function getTokenRequirements(
461
510
  let amount = getFastCheckoutAmount(
462
511
  checkoutSession.line_items as TLineItemExpanded[],
463
512
  checkoutSession.mode,
464
- paymentCurrency,
513
+ paymentCurrency.id,
465
514
  !!checkoutSession.subscription_data?.trial_period_days
466
515
  );
467
516
 
@@ -4,13 +4,13 @@ import type { Transaction } from '@ocap/client';
4
4
  import { BN } from '@ocap/util';
5
5
  import { fromPublicKey } from '@ocap/wallet';
6
6
 
7
- import { invoiceQueue } from '../../jobs/invoice';
8
- import { subscriptionQueue } from '../../jobs/subscription';
9
7
  import type { CallbackArgs } from '../../libs/auth';
10
8
  import { wallet } from '../../libs/auth';
11
9
  import { getGasPayerExtra, getTokenLimitsForDelegation } from '../../libs/payment';
12
10
  import { getFastCheckoutAmount } from '../../libs/session';
13
11
  import { OCAP_PAYMENT_TX_TYPE, getTxMetadata } from '../../libs/util';
12
+ import { invoiceQueue } from '../../queues/invoice';
13
+ import { subscriptionQueue } from '../../queues/subscription';
14
14
  import type { TLineItemExpanded } from '../../store/models';
15
15
  import { ensureInvoiceForCheckout, ensurePaymentIntent, getAuthPrincipalClaim, getTokenRequirements } from './shared';
16
16
 
@@ -38,9 +38,15 @@ export default {
38
38
  const amount = getFastCheckoutAmount(
39
39
  checkoutSession.line_items as TLineItemExpanded[],
40
40
  checkoutSession.mode,
41
- paymentCurrency
41
+ paymentCurrency.id
42
+ );
43
+ const tokenLimits = await getTokenLimitsForDelegation(
44
+ checkoutSession,
45
+ paymentMethod,
46
+ paymentCurrency,
47
+ address,
48
+ amount
42
49
  );
43
- const tokenLimits = await getTokenLimitsForDelegation(paymentMethod, paymentCurrency, address, amount);
44
50
  const tokenRequirements = await getTokenRequirements(checkoutSession, paymentMethod, paymentCurrency);
45
51
 
46
52
  return {
@@ -9,6 +9,7 @@ import { checkPassportForPricingTable } from '../integrations/blocklet/passport'
9
9
  import logger from '../libs/logger';
10
10
  import { authenticate } from '../libs/security';
11
11
  import { isLineItemCurrencyAligned } from '../libs/session';
12
+ import { getDaysUntilDue } from '../libs/subscription';
12
13
  import { formatMetadata, getMetadataFromQuery } from '../libs/util';
13
14
  import { CheckoutSession } from '../store/models/checkout-session';
14
15
  import { PaymentCurrency } from '../store/models/payment-currency';
@@ -340,6 +341,7 @@ router.post('/:id/checkout/:priceId', async (req, res) => {
340
341
  metadata: {
341
342
  ...doc.metadata,
342
343
  ...getMetadataFromQuery(req.query),
344
+ days_until_due: getDaysUntilDue(req.query),
343
345
  passport: await checkPassportForPricingTable(doc),
344
346
  pricing_table_id: doc.id,
345
347
  },
@@ -130,7 +130,7 @@ router.delete('/:id', auth, async (req, res) => {
130
130
  const doc = await SubscriptionItem.findByPk(req.params.id);
131
131
 
132
132
  if (!doc) {
133
- return res.status(404).json({ error: 'webhook endpoint not found' });
133
+ return res.status(404).json({ error: 'subscription item not found' });
134
134
  }
135
135
 
136
136
  if (req.body.clear_usage) {