payment-kit 1.13.29 → 1.13.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 (46) hide show
  1. package/api/src/integrations/blockchain/nft.ts +0 -1
  2. package/api/src/integrations/blocklet/passport.ts +1 -1
  3. package/api/src/integrations/stripe/handlers/invoice.ts +44 -2
  4. package/api/src/integrations/stripe/handlers/payment-intent.ts +32 -8
  5. package/api/src/integrations/stripe/resource.ts +7 -4
  6. package/api/src/jobs/subscription.ts +1 -1
  7. package/api/src/libs/payment.ts +6 -1
  8. package/api/src/libs/session.ts +78 -27
  9. package/api/src/libs/util.ts +15 -0
  10. package/api/src/routes/checkout-sessions.ts +161 -20
  11. package/api/src/routes/connect/collect.ts +5 -9
  12. package/api/src/routes/connect/pay.ts +5 -9
  13. package/api/src/routes/connect/setup.ts +22 -10
  14. package/api/src/routes/connect/shared.ts +13 -10
  15. package/api/src/routes/connect/subscribe.ts +29 -20
  16. package/api/src/routes/invoices.ts +5 -1
  17. package/api/src/routes/payment-intents.ts +5 -1
  18. package/api/src/routes/payment-links.ts +3 -2
  19. package/api/src/routes/prices.ts +32 -21
  20. package/api/src/routes/products.ts +1 -9
  21. package/api/src/store/migrations/20231023-upsell.ts +11 -0
  22. package/api/src/store/models/index.ts +10 -2
  23. package/api/src/store/models/price.ts +89 -23
  24. package/api/src/store/models/types.ts +1 -0
  25. package/blocklet.yml +1 -1
  26. package/package.json +17 -17
  27. package/src/components/blockchain/tx.tsx +3 -1
  28. package/src/components/checkout/pay.tsx +39 -19
  29. package/src/components/checkout/product-card.tsx +2 -6
  30. package/src/components/checkout/product-item.tsx +84 -21
  31. package/src/components/checkout/summary.tsx +11 -2
  32. package/src/components/info-row.tsx +3 -1
  33. package/src/components/invoice/table.tsx +1 -1
  34. package/src/components/price/upsell-select.tsx +83 -0
  35. package/src/components/price/upsell.tsx +74 -0
  36. package/src/components/status.tsx +1 -1
  37. package/src/components/subscription/actions/cancel.tsx +25 -27
  38. package/src/components/subscription/items/index.tsx +1 -1
  39. package/src/libs/util.ts +51 -31
  40. package/src/locales/en.tsx +23 -2
  41. package/src/locales/zh.tsx +52 -40
  42. package/src/pages/admin/billing/index.tsx +3 -3
  43. package/src/pages/admin/customers/customers/detail.tsx +1 -0
  44. package/src/pages/admin/index.tsx +1 -0
  45. package/src/pages/admin/products/prices/detail.tsx +7 -0
  46. package/src/pages/customer/invoice.tsx +7 -6
@@ -2,6 +2,8 @@
2
2
  import { getUrl } from '@blocklet/sdk/lib/component';
3
3
  import userMiddleware from '@blocklet/sdk/lib/middlewares/user';
4
4
  import { Request, Response, Router } from 'express';
5
+ import cloneDeep from 'lodash/cloneDeep';
6
+ import merge from 'lodash/merge';
5
7
  import omit from 'lodash/omit';
6
8
  import pick from 'lodash/pick';
7
9
  import sortBy from 'lodash/sortBy';
@@ -19,8 +21,10 @@ import logger from '../libs/logger';
19
21
  import { isDelegationSufficientForPayment } from '../libs/payment';
20
22
  import { authenticate } from '../libs/security';
21
23
  import {
24
+ canUpsell,
22
25
  getCheckoutAmount,
23
26
  getCheckoutMode,
27
+ getFastCheckoutAmount,
24
28
  getStatementDescriptor,
25
29
  getSubscriptionCreateSetup,
26
30
  getSupportedPaymentCurrencies,
@@ -28,7 +32,7 @@ import {
28
32
  isLineItemAligned,
29
33
  } from '../libs/session';
30
34
  import { createCodeGenerator, formatMetadata } from '../libs/util';
31
- import type { LineItem, TPaymentCurrency } from '../store/models';
35
+ import type { TPaymentCurrency } from '../store/models';
32
36
  import { CheckoutSession } from '../store/models/checkout-session';
33
37
  import { Customer } from '../store/models/customer';
34
38
  import { PaymentCurrency } from '../store/models/payment-currency';
@@ -137,7 +141,7 @@ export const formatCheckoutSession = async (payload: any, throwOnEmptyItems = tr
137
141
  throw new Error('line items should not be empty for checkout session');
138
142
  }
139
143
 
140
- const items = await Price.expand(raw.line_items as LineItem[]);
144
+ const items = await Price.expand(raw.line_items as any[]);
141
145
  if (items.some((x) => !x.price)) {
142
146
  throw new Error('Invalid line items for checkout session, some price may have been deleted');
143
147
  }
@@ -159,8 +163,6 @@ export const formatCheckoutSession = async (payload: any, throwOnEmptyItems = tr
159
163
  raw.currency_id = items[0]?.price.currency_id;
160
164
  }
161
165
 
162
- const currency = await PaymentCurrency.findByPk(raw.currency_id);
163
- const amount = getCheckoutAmount(items, currency as TPaymentCurrency, !!raw.subscription_data?.trial_period_days);
164
166
  const mode = getCheckoutMode(items);
165
167
 
166
168
  return Object.assign(raw, {
@@ -168,7 +170,19 @@ export const formatCheckoutSession = async (payload: any, throwOnEmptyItems = tr
168
170
  status: 'open',
169
171
  payment_status: 'unpaid',
170
172
  nft_mint_status: raw.nft_mint_settings?.enabled ? 'pending' : 'disabled',
173
+ payment_method_types: await getPaymentTypes(items),
174
+ // always create invoice for subscriptions
175
+ invoice_creation: mode === 'subscription' ? { enabled: true } : raw.invoice_creation,
176
+ ...(await getCheckoutSessionAmounts(raw as CheckoutSession)),
177
+ });
178
+ };
171
179
 
180
+ export async function getCheckoutSessionAmounts(checkoutSession: CheckoutSession) {
181
+ const items = await Price.expand(checkoutSession.line_items);
182
+ const currency = await PaymentCurrency.findByPk(checkoutSession.currency_id);
183
+ const includeTrial = !!checkoutSession.subscription_data?.trial_period_days;
184
+ const amount = getCheckoutAmount(items, currency as TPaymentCurrency, includeTrial);
185
+ return {
172
186
  amount_subtotal: amount.subtotal,
173
187
  amount_total: amount.total,
174
188
  total_details: {
@@ -176,13 +190,8 @@ export const formatCheckoutSession = async (payload: any, throwOnEmptyItems = tr
176
190
  amount_shipping: amount.shipping,
177
191
  amount_tax: amount.tax,
178
192
  },
179
-
180
- payment_method_types: await getPaymentTypes(items),
181
-
182
- // always create invoice for subscriptions
183
- invoice_creation: mode === 'subscription' ? { enabled: true } : raw.invoice_creation,
184
- });
185
- };
193
+ };
194
+ }
186
195
 
187
196
  // create checkout session
188
197
  router.post('/', auth, async (req, res) => {
@@ -206,7 +215,7 @@ export async function startCheckoutSessionFromPaymentLink(id: string, req: Reque
206
215
  return;
207
216
  }
208
217
 
209
- const items = await Price.expand(link.line_items);
218
+ const items = await Price.expand(link.line_items, { upsell: true });
210
219
 
211
220
  const raw: Partial<CheckoutSession> = await formatCheckoutSession(link, false);
212
221
  raw.livemode = link.livemode;
@@ -214,6 +223,20 @@ export async function startCheckoutSessionFromPaymentLink(id: string, req: Reque
214
223
  raw.currency_id = link.currency_id || req.currency.id;
215
224
  raw.payment_link_id = link.id;
216
225
 
226
+ if (link.after_completion?.hosted_confirmation?.custom_message) {
227
+ raw.payment_intent_data = {
228
+ description: link.after_completion?.hosted_confirmation?.custom_message,
229
+ };
230
+ } else {
231
+ raw.payment_intent_data = {
232
+ // TODO: bake default into this
233
+ description: 'Thanks for your purchase',
234
+ };
235
+ }
236
+ if (link.after_completion?.redirect?.url) {
237
+ raw.success_url = link.after_completion?.redirect?.url;
238
+ }
239
+
217
240
  if (req.query.redirect) {
218
241
  raw.success_url = req.query.redirect as string;
219
242
  raw.cancel_url = req.query.redirect as string;
@@ -268,7 +291,7 @@ router.get('/:id', auth, async (req, res) => {
268
291
 
269
292
  if (doc) {
270
293
  // @ts-ignore
271
- doc.line_items = await Price.expand(doc.line_items);
294
+ doc.line_items = await Price.expand(doc.line_items, { upsell: true });
272
295
  doc.url = getUrl(`/checkout/${doc.submit_type}/${doc.id}`);
273
296
  }
274
297
 
@@ -285,7 +308,7 @@ router.get('/retrieve/:id', user, async (req, res) => {
285
308
  }
286
309
 
287
310
  // @ts-ignore
288
- doc.line_items = await Price.expand(doc.line_items);
311
+ doc.line_items = await Price.expand(doc.line_items, { upsell: true });
289
312
 
290
313
  // check payment intent
291
314
  const paymentIntent = doc.payment_intent_id ? await PaymentIntent.findByPk(doc.payment_intent_id) : null;
@@ -334,7 +357,7 @@ router.put('/:id/submit', user, async (req, res) => {
334
357
  await checkoutSession.update({ currency_id: paymentCurrency.id });
335
358
 
336
359
  // always update payment amount in case currency has changed
337
- const lineItems = await Price.expand(checkoutSession.line_items, true);
360
+ const lineItems = await Price.expand(checkoutSession.line_items, { product: true, upsell: true });
338
361
  const trialInDays = checkoutSession.subscription_data?.trial_period_days || 0;
339
362
  const amount = getCheckoutAmount(lineItems, paymentCurrency, !!trialInDays);
340
363
  await checkoutSession.update({
@@ -364,6 +387,7 @@ router.put('/:id/submit', user, async (req, res) => {
364
387
  delinquent: false,
365
388
  invoice_prefix: getInvoicePrefix(),
366
389
  });
390
+ logger.info('customer created on checkout session submit', { did: req.user.did, id: customer.id });
367
391
  } else {
368
392
  const updates: Record<string, string> = {};
369
393
  if (checkoutSession.customer_update?.name) {
@@ -429,6 +453,10 @@ router.put('/:id/submit', user, async (req, res) => {
429
453
  setup_future_usage: 'on_session',
430
454
  metadata: checkoutSession.payment_intent_data?.metadata || checkoutSession.metadata,
431
455
  });
456
+ logger.info('paymentIntent created on checkout session submit', {
457
+ session: checkoutSession.id,
458
+ intent: paymentIntent.id,
459
+ });
432
460
 
433
461
  // lock prices used by this payment
434
462
  await Price.update({ locked: true }, { where: { id: lineItems.map((x) => x.price_id) } });
@@ -473,6 +501,10 @@ router.put('/:id/submit', user, async (req, res) => {
473
501
  usage: 'off_session',
474
502
  metadata: checkoutSession.metadata,
475
503
  });
504
+ logger.info('setupIntent created on checkout session submit', {
505
+ session: checkoutSession.id,
506
+ intent: setupIntent.id,
507
+ });
476
508
 
477
509
  // persist setup intent id
478
510
  await checkoutSession.update({ setup_intent_id: setupIntent.id });
@@ -520,8 +552,13 @@ router.put('/:id/submit', user, async (req, res) => {
520
552
  metadata: checkoutSession.metadata as any,
521
553
  });
522
554
 
555
+ logger.info('subscription created on checkout session submit', {
556
+ session: checkoutSession.id,
557
+ subscription: subscription.id,
558
+ });
559
+
523
560
  // create subscription items
524
- await Promise.all(
561
+ const items = await Promise.all(
525
562
  lineItems
526
563
  .filter((x) => x.price.type === 'recurring')
527
564
  .map((x) =>
@@ -529,15 +566,19 @@ router.put('/:id/submit', user, async (req, res) => {
529
566
  livemode: !!checkoutSession.livemode,
530
567
  // @ts-ignore
531
568
  subscription_id: subscription.id,
532
- price_id: x.price_id,
569
+ price_id: x.upsell_price_id || x.price_id,
533
570
  quantity: x.quantity,
534
571
  metadata: checkoutSession.metadata as any,
535
572
  })
536
573
  )
537
574
  );
575
+ logger.info('subscription items created on checkout session submit', {
576
+ session: checkoutSession.id,
577
+ items: items.map((x) => x.id),
578
+ });
538
579
 
539
580
  // lock prices used by this subscription
540
- await Price.update({ locked: true }, { where: { id: lineItems.map((x) => x.price_id) } });
581
+ await Price.update({ locked: true }, { where: { id: lineItems.map((x) => x.upsell_price_id || x.price_id) } });
541
582
 
542
583
  // persist subscription id
543
584
  await checkoutSession.update({ subscription_id: subscription.id });
@@ -545,11 +586,12 @@ router.put('/:id/submit', user, async (req, res) => {
545
586
  }
546
587
 
547
588
  // if we can complete purchase without any wallet interaction
589
+ const fastCheckoutAmount = getFastCheckoutAmount(lineItems, checkoutSession.mode, paymentCurrency, !!trialInDays);
548
590
  const delegation = await isDelegationSufficientForPayment({
549
591
  paymentMethod,
550
592
  paymentCurrency,
551
593
  userDid: customer.did,
552
- amount: checkoutSession.amount_total,
594
+ amount: fastCheckoutAmount,
553
595
  });
554
596
  if (delegation.sufficient) {
555
597
  const paymentSettings = {
@@ -623,7 +665,7 @@ router.put('/:id/submit', user, async (req, res) => {
623
665
  subscription,
624
666
  paymentMethod,
625
667
  paymentCurrency,
626
- lineItems as any[],
668
+ lineItems,
627
669
  trialInDays
628
670
  );
629
671
  if (stripeSubscription && subscription?.payment_details?.stripe?.subscription_id === stripeSubscription.id) {
@@ -651,6 +693,105 @@ router.put('/:id/submit', user, async (req, res) => {
651
693
  }
652
694
  });
653
695
 
696
+ // upsell
697
+ router.put('/:id/upsell', user, async (req, res) => {
698
+ try {
699
+ // validate session
700
+ const checkoutSession = await CheckoutSession.findByPk(req.params.id);
701
+ if (!checkoutSession) {
702
+ return res.status(404).json({ error: 'Checkout session not found' });
703
+ }
704
+ if (checkoutSession.status === 'complete') {
705
+ return res.status(403).json({ error: 'Checkout session completed' });
706
+ }
707
+ if (checkoutSession.status === 'expired') {
708
+ return res.status(403).json({ error: 'Checkout session expired' });
709
+ }
710
+
711
+ if (checkoutSession.line_items) {
712
+ // validate line items
713
+ if (checkoutSession.line_items.length > 1) {
714
+ return res.status(400).json({ error: 'Upsell not supported for checkoutSession with multiple line items' });
715
+ }
716
+
717
+ // validate from and to
718
+ const [from, to] = await Promise.all([Price.findByPk(req.body.from), Price.findByPk(req.body.to)]);
719
+ if (!from) {
720
+ return res.status(400).json({ error: 'Upsell from price not found' });
721
+ }
722
+ if (!to) {
723
+ return res.status(400).json({ error: 'Upsell to price not found' });
724
+ }
725
+
726
+ if (canUpsell(from, to) === false) {
727
+ return res.status(400).json({ error: `Upsell not possible from ${from} to ${to}` });
728
+ }
729
+
730
+ const index = checkoutSession.line_items.findIndex((x) => x.price_id === from.id);
731
+ if (index === -1) {
732
+ return res.status(400).json({ error: 'Upsell from not exist in checkoutSession line items' });
733
+ }
734
+
735
+ const items = cloneDeep(checkoutSession.line_items);
736
+ items[index] = merge(items[index], { upsell_price_id: to.id });
737
+ await checkoutSession.update({ line_items: items });
738
+ logger.info('CheckoutSession updated on upsell', { id: req.params.id, from: from.id, to: to.id });
739
+
740
+ // recalculate amount
741
+ await checkoutSession.update(await getCheckoutSessionAmounts(checkoutSession));
742
+ }
743
+
744
+ const items = await Price.expand(checkoutSession.line_items, { upsell: true });
745
+ res.json({ ...checkoutSession.toJSON(), line_items: items });
746
+ } catch (err) {
747
+ console.error(err);
748
+ res.status(500).json({ error: err.message });
749
+ }
750
+ });
751
+
752
+ router.put('/:id/downsell', user, async (req, res) => {
753
+ try {
754
+ // validate session
755
+ const checkoutSession = await CheckoutSession.findByPk(req.params.id);
756
+ if (!checkoutSession) {
757
+ return res.status(404).json({ error: 'Checkout session not found' });
758
+ }
759
+ if (checkoutSession.status === 'complete') {
760
+ return res.status(403).json({ error: 'Checkout session completed' });
761
+ }
762
+ if (checkoutSession.status === 'expired') {
763
+ return res.status(403).json({ error: 'Checkout session expired' });
764
+ }
765
+
766
+ // validate from
767
+ const from = await Price.findByPk(req.body.from);
768
+ if (!from) {
769
+ return res.status(400).json({ error: 'Upsell from price not found' });
770
+ }
771
+
772
+ if (checkoutSession.line_items) {
773
+ const index = checkoutSession.line_items.findIndex((x) => x.upsell_price_id === from.id);
774
+ if (index === -1) {
775
+ return res.status(400).json({ error: 'Upsell not configured for checkout session' });
776
+ }
777
+
778
+ const items = cloneDeep(checkoutSession.line_items);
779
+ items[index] = merge(items[index], { upsell_price_id: '' });
780
+ await checkoutSession.update({ line_items: items });
781
+ logger.info('CheckoutSession updated on downsell', { id: req.params.id, from: from.id });
782
+
783
+ // recalculate amount
784
+ await checkoutSession.update(await getCheckoutSessionAmounts(checkoutSession));
785
+ }
786
+
787
+ const items = await Price.expand(checkoutSession.line_items, { upsell: true });
788
+ res.json({ ...checkoutSession.toJSON(), line_items: items });
789
+ } catch (err) {
790
+ console.error(err);
791
+ res.status(500).json({ error: err.message });
792
+ }
793
+ });
794
+
654
795
  // eslint-disable-next-line consistent-return
655
796
  router.put('/:id/expire', auth, async (req, res) => {
656
797
  const doc = await CheckoutSession.findByPk(req.params.id);
@@ -6,6 +6,7 @@ import { paymentQueue } from '../../jobs/payment';
6
6
  import type { CallbackArgs } from '../../libs/auth';
7
7
  import { wallet } from '../../libs/auth';
8
8
  import dayjs from '../../libs/dayjs';
9
+ import { getTxMetadata } from '../../libs/util';
9
10
  import { ensureInvoiceForCollect, getAuthPrincipalClaim } from './shared';
10
11
 
11
12
  // Used to collect an open invoice failed to collect automatically
@@ -26,15 +27,10 @@ export default {
26
27
  // @ts-ignore
27
28
  const itx: TransferV3Tx = {
28
29
  outputs: [{ owner: wallet.address, tokens, assets: [] }],
29
- data: {
30
- type: 'json',
31
- // @ts-ignore
32
- value: {
33
- appId: wallet.address,
34
- paymentIntentId: paymentIntent.id,
35
- invoiceId,
36
- },
37
- },
30
+ data: getTxMetadata({
31
+ paymentIntentId: paymentIntent.id,
32
+ invoiceId,
33
+ }),
38
34
  };
39
35
 
40
36
  if (paymentMethod.type === 'arcblock') {
@@ -4,6 +4,7 @@ import { fromAddress } from '@ocap/wallet';
4
4
  import type { CallbackArgs } from '../../libs/auth';
5
5
  import { wallet } from '../../libs/auth';
6
6
  import dayjs from '../../libs/dayjs';
7
+ import { getTxMetadata } from '../../libs/util';
7
8
  import { ensureInvoiceForCheckout, ensurePaymentIntent, getAuthPrincipalClaim } from './shared';
8
9
 
9
10
  export default {
@@ -27,15 +28,10 @@ export default {
27
28
  // @ts-ignore
28
29
  const itx: TransferV3Tx = {
29
30
  outputs: [{ owner: wallet.address, tokens, assets: [] }],
30
- data: {
31
- type: 'json',
32
- // @ts-ignore
33
- value: {
34
- appId: wallet.address,
35
- paymentIntentId: paymentIntent.id,
36
- checkoutSessionId,
37
- },
38
- },
31
+ data: getTxMetadata({
32
+ paymentIntentId: paymentIntent.id,
33
+ checkoutSessionId,
34
+ }),
39
35
  };
40
36
 
41
37
  return {
@@ -6,6 +6,9 @@ import { fromPublicKey } from '@ocap/wallet';
6
6
  import { subscriptionQueue } from '../../jobs/subscription';
7
7
  import type { CallbackArgs } from '../../libs/auth';
8
8
  import { wallet } from '../../libs/auth';
9
+ import { getFastCheckoutAmount } from '../../libs/session';
10
+ import { getTxMetadata } from '../../libs/util';
11
+ import type { TLineItemExpanded } from '../../store/models';
9
12
  import { ensureSetupIntent, getAuthPrincipalClaim } from './shared';
10
13
 
11
14
  export default {
@@ -19,12 +22,22 @@ export default {
19
22
  },
20
23
  onConnect: async ({ userDid, userPk, extraParams }: CallbackArgs) => {
21
24
  const { checkoutSessionId } = extraParams;
22
- const { paymentMethod, subscription } = await ensureSetupIntent(checkoutSessionId, userDid);
25
+ const { paymentMethod, paymentCurrency, checkoutSession, subscription } = await ensureSetupIntent(
26
+ checkoutSessionId,
27
+ userDid
28
+ );
23
29
  if (!subscription) {
24
30
  throw new Error('Subscription for checkoutSession not found');
25
31
  }
26
32
 
27
33
  if (paymentMethod.type === 'arcblock') {
34
+ const amount = getFastCheckoutAmount(
35
+ checkoutSession.line_items as TLineItemExpanded[],
36
+ checkoutSession.mode,
37
+ paymentCurrency,
38
+ !!checkoutSession.subscription_data?.trial_period_days
39
+ );
40
+
28
41
  return {
29
42
  signature: {
30
43
  type: 'DelegateTx',
@@ -35,17 +48,16 @@ export default {
35
48
  address: toDelegateAddress(userDid, wallet.address),
36
49
  to: wallet.address,
37
50
  ops: [{ typeUrl: 'fg:t:transfer_v2', rules: [] }],
38
- data: {
39
- type: 'json',
40
- // @ts-ignore
41
- value: {
42
- appId: wallet.address,
43
- subscriptionId: subscription.id,
44
- checkoutSessionId,
45
- },
46
- },
51
+ data: getTxMetadata({
52
+ subscriptionId: subscription.id,
53
+ checkoutSessionId,
54
+ }),
47
55
  },
48
56
  },
57
+ nonce: checkoutSessionId,
58
+ requirement: {
59
+ tokens: [{ address: paymentCurrency.contract as string, value: amount }],
60
+ },
49
61
  chainInfo: {
50
62
  host: paymentMethod.settings?.arcblock?.api_host as string,
51
63
  id: paymentMethod.settings?.arcblock?.chain_id as string,
@@ -3,7 +3,8 @@ import { BN } from '@ocap/util';
3
3
  import { blocklet } from '../../libs/auth';
4
4
  import dayjs from '../../libs/dayjs';
5
5
  import logger from '../../libs/logger';
6
- import { TLineItemExpanded, getStatementDescriptor } from '../../libs/session';
6
+ import { getStatementDescriptor } from '../../libs/session';
7
+ import type { TLineItemExpanded } from '../../store/models';
7
8
  import { CheckoutSession } from '../../store/models/checkout-session';
8
9
  import { Customer } from '../../store/models/customer';
9
10
  import { Invoice } from '../../store/models/invoice';
@@ -110,7 +111,7 @@ export async function ensurePaymentIntent(checkoutSessionId: string, userDid?: s
110
111
  throw new Error(`Payment method ${paymentMethod.type} should not be here`);
111
112
  }
112
113
 
113
- checkoutSession.line_items = await Price.expand(checkoutSession.line_items, false);
114
+ checkoutSession.line_items = await Price.expand(checkoutSession.line_items);
114
115
 
115
116
  return {
116
117
  checkoutSession,
@@ -192,7 +193,7 @@ export async function ensureSetupIntent(checkoutSessionId: string, userDid?: str
192
193
  throw new Error(`Payment method ${paymentMethod.type} should not be here`);
193
194
  }
194
195
 
195
- checkoutSession.line_items = await Price.expand(checkoutSession.line_items, false);
196
+ checkoutSession.line_items = await Price.expand(checkoutSession.line_items);
196
197
 
197
198
  return {
198
199
  checkoutSession,
@@ -311,15 +312,16 @@ export async function ensureInvoiceForCheckout({
311
312
  const subscriptionItems = subscription
312
313
  ? await SubscriptionItem.findAll({ where: { subscription_id: subscription?.id } })
313
314
  : [];
314
- const lineItems = await Price.expand(checkoutSession.line_items, true);
315
+ const lineItems = await Price.expand(checkoutSession.line_items, { product: true });
315
316
 
316
317
  const trailing = !!checkoutSession.subscription_data?.trial_period_days;
317
318
  const getLineSetup = (x: TLineItemExpanded) => {
318
- if (x.price.type === 'recurring' && trailing) {
319
+ const price = x.upsell_price || x.price;
320
+ if (price.type === 'recurring' && trailing) {
319
321
  return {
320
322
  amount: '0',
321
323
  // @ts-ignore
322
- description: trailing ? `${x.price.product.name} (trailing)` : x.price.product.name,
324
+ description: trailing ? `${price.product.name} (trailing)` : price.product.name,
323
325
  period: {
324
326
  start: subscription?.current_period_start as number,
325
327
  end: subscription?.current_period_end as number,
@@ -328,9 +330,9 @@ export async function ensureInvoiceForCheckout({
328
330
  }
329
331
 
330
332
  return {
331
- amount: new BN(x.price.unit_amount).mul(new BN(x.quantity)).toString(),
333
+ amount: new BN(price.unit_amount).mul(new BN(x.quantity)).toString(),
332
334
  // @ts-ignore
333
- description: x.price.product.name,
335
+ description: price.product.name,
334
336
  period: undefined,
335
337
  };
336
338
  };
@@ -338,9 +340,10 @@ export async function ensureInvoiceForCheckout({
338
340
  const items = await Promise.all(
339
341
  lineItems.map((x: TLineItemExpanded) => {
340
342
  const setup = getLineSetup(x);
343
+ const price = x.upsell_price || x.price;
341
344
  let { quantity } = x;
342
- if (x.price.type === 'recurring') {
343
- if (x.price.recurring?.usage_type === 'metered') {
345
+ if (price.type === 'recurring') {
346
+ if (price.recurring?.usage_type === 'metered') {
344
347
  quantity = 0;
345
348
  }
346
349
  if (trailing) {
@@ -8,6 +8,9 @@ import { invoiceQueue } from '../../jobs/invoice';
8
8
  import { subscriptionQueue } from '../../jobs/subscription';
9
9
  import type { CallbackArgs } from '../../libs/auth';
10
10
  import { wallet } from '../../libs/auth';
11
+ import { getFastCheckoutAmount } from '../../libs/session';
12
+ import { getTxMetadata } from '../../libs/util';
13
+ import type { TLineItemExpanded } from '../../store/models';
11
14
  import { ensureInvoiceForCheckout, ensurePaymentIntent, getAuthPrincipalClaim } from './shared';
12
15
 
13
16
  export default {
@@ -30,16 +33,12 @@ export default {
30
33
  }
31
34
 
32
35
  if (paymentMethod.type === 'arcblock') {
33
- if (checkoutSession.amount_total > '0') {
34
- const client = paymentMethod.getOcapClient();
35
- const result = await client.getAccountTokens({ address: userDid, token: paymentCurrency.contract });
36
- const balance = result.tokens[0]?.balance || '0';
37
- if (new BN(balance).lt(new BN(checkoutSession.amount_total))) {
38
- throw new Error(
39
- `Your account ${userDid} does not have enough ${paymentCurrency.symbol} to complete this subscription`
40
- );
41
- }
42
- }
36
+ const amount = getFastCheckoutAmount(
37
+ checkoutSession.line_items as TLineItemExpanded[],
38
+ checkoutSession.mode,
39
+ paymentCurrency,
40
+ !!checkoutSession.subscription_data?.trial_period_days
41
+ );
43
42
 
44
43
  return {
45
44
  signature: {
@@ -51,17 +50,16 @@ export default {
51
50
  address: toDelegateAddress(userDid, wallet.address),
52
51
  to: wallet.address,
53
52
  ops: [{ typeUrl: 'fg:t:transfer_v2', rules: [] }],
54
- data: {
55
- type: 'json',
56
- // @ts-ignore
57
- value: {
58
- appId: wallet.address,
59
- subscriptionId: subscription.id,
60
- checkoutSessionId,
61
- },
62
- },
53
+ data: getTxMetadata({
54
+ subscriptionId: subscription.id,
55
+ checkoutSessionId,
56
+ }),
63
57
  },
64
58
  },
59
+ nonce: checkoutSessionId,
60
+ requirement: {
61
+ tokens: [{ address: paymentCurrency.contract as string, value: amount }],
62
+ },
65
63
  chainInfo: {
66
64
  host: paymentMethod.settings?.arcblock?.api_host as string,
67
65
  id: paymentMethod.settings?.arcblock?.chain_id as string,
@@ -74,7 +72,7 @@ export default {
74
72
  },
75
73
  onAuth: async ({ userDid, userPk, claims, request, extraParams }: CallbackArgs) => {
76
74
  const { checkoutSessionId } = extraParams;
77
- const { checkoutSession, customer, paymentMethod, subscription } = await ensurePaymentIntent(
75
+ const { checkoutSession, customer, paymentMethod, paymentCurrency, subscription } = await ensurePaymentIntent(
78
76
  checkoutSessionId,
79
77
  userDid
80
78
  );
@@ -83,6 +81,17 @@ export default {
83
81
  throw new Error('Subscription for checkoutSession not found');
84
82
  }
85
83
 
84
+ if (checkoutSession.amount_total > '0') {
85
+ const client = paymentMethod.getOcapClient();
86
+ const result = await client.getAccountTokens({ address: userDid, token: paymentCurrency.contract });
87
+ const balance = result.tokens[0]?.balance || '0';
88
+ if (new BN(balance).lt(new BN(checkoutSession.amount_total))) {
89
+ throw new Error(
90
+ `Your account ${userDid} does not have enough ${paymentCurrency.symbol} to complete this subscription`
91
+ );
92
+ }
93
+ }
94
+
86
95
  if (paymentMethod.type === 'arcblock') {
87
96
  await subscription.update({
88
97
  payment_settings: {
@@ -3,6 +3,7 @@ import { Router } from 'express';
3
3
  import Joi from 'joi';
4
4
  import type { WhereOptions } from 'sequelize';
5
5
 
6
+ import { syncStripeInvoice } from '../integrations/stripe/handlers/invoice';
6
7
  import { authenticate } from '../libs/security';
7
8
  import { expandLineItems } from '../libs/session';
8
9
  import { Customer } from '../store/models/customer';
@@ -102,7 +103,6 @@ router.get('/', authMine, async (req, res) => {
102
103
  }
103
104
  });
104
105
 
105
- // FIXME: exclude some sensitive fields from PaymentMethod
106
106
  router.get('/:id', authPortal, async (req, res) => {
107
107
  try {
108
108
  const doc = await Invoice.findOne({
@@ -118,6 +118,10 @@ router.get('/:id', authPortal, async (req, res) => {
118
118
  });
119
119
 
120
120
  if (doc) {
121
+ if (doc.status !== 'paid' && doc.metadata?.stripe_id) {
122
+ await syncStripeInvoice(doc);
123
+ }
124
+
121
125
  const json = doc.toJSON();
122
126
  const products = (await Product.findAll()).map((x) => x.toJSON());
123
127
  const prices = (await Price.findAll()).map((x) => x.toJSON());
@@ -3,6 +3,7 @@ import { Router } from 'express';
3
3
  import Joi from 'joi';
4
4
  import type { WhereOptions } from 'sequelize';
5
5
 
6
+ import { syncStripPayment } from '../integrations/stripe/handlers/payment-intent';
6
7
  import { authenticate } from '../libs/security';
7
8
  import { CheckoutSession } from '../store/models/checkout-session';
8
9
  import { Customer } from '../store/models/customer';
@@ -98,7 +99,6 @@ router.get('/', authMine, async (req, res) => {
98
99
  }
99
100
  });
100
101
 
101
- // FIXME: exclude some sensitive fields from PaymentMethod
102
102
  router.get('/:id', authPortal, async (req, res) => {
103
103
  try {
104
104
  const doc = await PaymentIntent.findOne({
@@ -115,6 +115,10 @@ router.get('/:id', authPortal, async (req, res) => {
115
115
  let subscription;
116
116
 
117
117
  if (doc) {
118
+ if (doc.status !== 'succeeded' && doc.metadata?.stripe_id) {
119
+ await syncStripPayment(doc);
120
+ }
121
+
118
122
  checkoutSession = await CheckoutSession.findOne({ where: { payment_intent_id: doc.id } });
119
123
  invoice = await Invoice.findByPk(doc.invoice_id);
120
124
  if (invoice && invoice.subscription_id) {