payment-kit 1.13.210 → 1.13.211

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/libs/api.ts +2 -2
  2. package/api/src/libs/session.ts +90 -4
  3. package/api/src/queues/payment.ts +61 -1
  4. package/api/src/routes/checkout-sessions.ts +61 -10
  5. package/api/src/routes/connect/collect.ts +44 -37
  6. package/api/src/routes/connect/pay.ts +40 -29
  7. package/api/src/routes/connect/setup.ts +39 -33
  8. package/api/src/routes/connect/shared.ts +3 -1
  9. package/api/src/routes/donations.ts +157 -0
  10. package/api/src/routes/index.ts +4 -0
  11. package/api/src/routes/payment-intents.ts +2 -2
  12. package/api/src/routes/payment-links.ts +8 -3
  13. package/api/src/routes/payouts.ts +151 -0
  14. package/api/src/routes/products.ts +24 -6
  15. package/api/src/routes/usage-records.ts +6 -3
  16. package/api/src/store/migrations/20240408-payout.ts +36 -0
  17. package/api/src/store/models/checkout-session.ts +5 -0
  18. package/api/src/store/models/customer.ts +6 -1
  19. package/api/src/store/models/index.ts +12 -0
  20. package/api/src/store/models/payment-intent.ts +38 -26
  21. package/api/src/store/models/payment-link.ts +8 -1
  22. package/api/src/store/models/payout.ts +243 -0
  23. package/api/src/store/models/types.ts +39 -0
  24. package/api/tests/libs/session.spec.ts +101 -0
  25. package/blocklet.yml +1 -1
  26. package/package.json +17 -16
  27. package/src/components/info-card.tsx +5 -5
  28. package/src/components/invoice/list.tsx +2 -0
  29. package/src/components/invoice/table.tsx +1 -1
  30. package/src/components/payment-intent/list.tsx +2 -0
  31. package/src/components/payouts/actions.tsx +43 -0
  32. package/src/components/payouts/list.tsx +255 -0
  33. package/src/components/refund/list.tsx +2 -0
  34. package/src/components/subscription/list.tsx +2 -0
  35. package/src/libs/util.ts +4 -1
  36. package/src/locales/en.tsx +7 -0
  37. package/src/locales/zh.tsx +6 -0
  38. package/src/pages/admin/customers/customers/index.tsx +2 -2
  39. package/src/pages/admin/payments/index.tsx +7 -0
  40. package/src/pages/admin/payments/intents/detail.tsx +7 -0
  41. package/src/pages/admin/payments/payouts/detail.tsx +204 -0
  42. package/src/pages/admin/payments/payouts/index.tsx +5 -0
  43. package/src/pages/admin/products/links/index.tsx +2 -2
  44. package/src/pages/admin/products/prices/detail.tsx +2 -1
  45. package/src/pages/admin/products/pricing-tables/index.tsx +2 -2
  46. package/src/pages/admin/products/products/index.tsx +2 -2
@@ -103,7 +103,7 @@ export const getWhereFromKvQuery = (query?: string) => {
103
103
  return out;
104
104
  };
105
105
 
106
- export function createListParamSchema<T>(schema: any) {
106
+ export function createListParamSchema<T>(schema: any, pageSize: number = 20) {
107
107
  return Joi.object<T & { page: number; pageSize: number; livemode?: boolean; q?: string; o?: string }>({
108
108
  // prettier-ignore
109
109
  page: Joi.number()
@@ -118,7 +118,7 @@ export function createListParamSchema<T>(schema: any) {
118
118
 
119
119
  pageSize: Joi.number()
120
120
  .integer()
121
- .default(20)
121
+ .default(pageSize)
122
122
  .custom((value) => {
123
123
  if (value > 100) {
124
124
  return 100;
@@ -1,4 +1,5 @@
1
1
  import { env } from '@blocklet/sdk/lib/config';
2
+ import type { TransactionInput } from '@ocap/client';
2
3
  import { BN } from '@ocap/util';
3
4
  import cloneDeep from 'lodash/cloneDeep';
4
5
  import isEqual from 'lodash/isEqual';
@@ -6,7 +7,8 @@ import isEqual from 'lodash/isEqual';
6
7
  import type { TLineItemExpanded, TPaymentCurrency, TPaymentMethodExpanded } from '../store/models';
7
8
  import type { Price, TPrice } from '../store/models/price';
8
9
  import type { Product } from '../store/models/product';
9
- import type { PriceCurrency, PriceRecurring } from '../store/models/types';
10
+ import type { PaymentBeneficiary, PriceCurrency, PriceRecurring } from '../store/models/types';
11
+ import { wallet } from './auth';
10
12
 
11
13
  export function getStatementDescriptor(items: any[]) {
12
14
  for (const item of items) {
@@ -39,10 +41,16 @@ export function getPriceUintAmountByCurrency(price: TPrice, currencyId: string)
39
41
  const options = getPriceCurrencyOptions(price);
40
42
  const option = options.find((x) => x.currency_id === currencyId);
41
43
  if (option) {
44
+ if (option.custom_unit_amount) {
45
+ return option.custom_unit_amount.preset || option.custom_unit_amount.presets[0];
46
+ }
42
47
  return option.unit_amount;
43
48
  }
44
49
 
45
50
  if (price.currency_id === currencyId) {
51
+ if (price.custom_unit_amount) {
52
+ return price.custom_unit_amount.preset || price.custom_unit_amount.presets[0];
53
+ }
46
54
  return price.unit_amount;
47
55
  }
48
56
 
@@ -54,18 +62,40 @@ export function getPriceCurrencyOptions(price: TPrice): PriceCurrency[] {
54
62
  return price.currency_options;
55
63
  }
56
64
 
57
- return [{ currency_id: price.currency_id, unit_amount: price.unit_amount, tiers: null, custom_unit_amount: null }];
65
+ return [
66
+ {
67
+ currency_id: price.currency_id,
68
+ unit_amount: price.unit_amount,
69
+ custom_unit_amount: price.custom_unit_amount || null,
70
+ tiers: null,
71
+ },
72
+ ];
58
73
  }
59
74
 
60
75
  // FIXME: apply coupon for discounts
61
76
  export function getCheckoutAmount(items: TLineItemExpanded[], currencyId: string, trialing = false) {
62
77
  let renew = new BN(0);
63
78
 
79
+ if (items.find((x) => (x.upsell_price || x.price).custom_unit_amount) && items.length > 1) {
80
+ throw new Error('Multiple items with custom unit amount are not supported');
81
+ }
82
+
64
83
  const total = items
65
84
  .reduce((acc, x) => {
85
+ if (x.custom_amount) {
86
+ return acc.add(new BN(x.custom_amount));
87
+ }
88
+
66
89
  const price = x.upsell_price || x.price;
90
+ const unitPrice = getPriceUintAmountByCurrency(price, currencyId);
91
+ if (price.custom_unit_amount) {
92
+ if (unitPrice) {
93
+ return acc.add(new BN(unitPrice).mul(new BN(x.quantity)));
94
+ }
95
+ }
96
+
67
97
  if (price?.type === 'recurring') {
68
- renew = renew.add(new BN(getPriceUintAmountByCurrency(price, currencyId)).mul(new BN(x.quantity)));
98
+ renew = renew.add(new BN(unitPrice).mul(new BN(x.quantity)));
69
99
 
70
100
  if (trialing) {
71
101
  return acc;
@@ -74,7 +104,8 @@ export function getCheckoutAmount(items: TLineItemExpanded[], currencyId: string
74
104
  return acc;
75
105
  }
76
106
  }
77
- return acc.add(new BN(getPriceUintAmountByCurrency(price, currencyId)).mul(new BN(x.quantity)));
107
+
108
+ return acc.add(new BN(unitPrice).mul(new BN(x.quantity)));
78
109
  }, new BN(0))
79
110
  .toString();
80
111
 
@@ -267,3 +298,58 @@ export function getBillingThreshold(config: Record<string, any> = {}) {
267
298
 
268
299
  return 0;
269
300
  }
301
+
302
+ export function canPayWithDelegation(beneficiaries: PaymentBeneficiary[]) {
303
+ return beneficiaries.length === 0 || beneficiaries.every((x) => x.address === wallet.address);
304
+ }
305
+
306
+ export function createPaymentBeneficiaries(total: string, beneficiaries: PaymentBeneficiary[]) {
307
+ if (beneficiaries.length) {
308
+ const shares = beneficiaries.reduce((acc, x) => acc + parseInt(x.share, 10), 0);
309
+ const result: PaymentBeneficiary[] = [];
310
+ beneficiaries.forEach((x, i) => {
311
+ let share = new BN(total).div(new BN(shares)).mul(new BN(x.share)).toString();
312
+ if (i === beneficiaries.length - 1) {
313
+ share = result.reduce((acc, d) => acc.sub(new BN(d.share)), new BN(total)).toString();
314
+ }
315
+
316
+ result.push({ ...x, share });
317
+ });
318
+
319
+ return result;
320
+ }
321
+
322
+ return [];
323
+ }
324
+
325
+ export function createPaymentOutput(
326
+ total: string,
327
+ contract: string,
328
+ beneficiaries: PaymentBeneficiary[]
329
+ ): TransactionInput[] {
330
+ if (beneficiaries.length) {
331
+ return beneficiaries.map((x) => ({
332
+ owner: x.address,
333
+ assets: [],
334
+ tokens: [
335
+ {
336
+ address: contract,
337
+ value: x.share,
338
+ },
339
+ ],
340
+ }));
341
+ }
342
+
343
+ return [
344
+ {
345
+ owner: wallet.address,
346
+ assets: [],
347
+ tokens: [
348
+ {
349
+ address: contract,
350
+ value: total,
351
+ },
352
+ ],
353
+ },
354
+ ];
355
+ }
@@ -2,7 +2,7 @@ import isEmpty from 'lodash/isEmpty';
2
2
 
3
3
  import { ensureStakedForGas } from '../integrations/blockchain/stake';
4
4
  import { createEvent } from '../libs/audit';
5
- import { wallet } from '../libs/auth';
5
+ import { blocklet, wallet } from '../libs/auth';
6
6
  import dayjs from '../libs/dayjs';
7
7
  import CustomError from '../libs/error';
8
8
  import { events } from '../libs/event';
@@ -25,6 +25,7 @@ import { Invoice } from '../store/models/invoice';
25
25
  import { PaymentCurrency } from '../store/models/payment-currency';
26
26
  import { PaymentIntent } from '../store/models/payment-intent';
27
27
  import { PaymentMethod } from '../store/models/payment-method';
28
+ import { Payout } from '../store/models/payout';
28
29
  import { Price } from '../store/models/price';
29
30
  import { Subscription } from '../store/models/subscription';
30
31
  import { SubscriptionItem } from '../store/models/subscription-item';
@@ -37,6 +38,65 @@ type PaymentJob = {
37
38
  };
38
39
 
39
40
  export const handlePaymentSucceed = async (paymentIntent: PaymentIntent) => {
41
+ if (paymentIntent.beneficiaries?.length) {
42
+ Promise.all(
43
+ paymentIntent.beneficiaries.map(async (x) => {
44
+ let customer = await Customer.findByPkOrDid(x.address);
45
+ if (!customer) {
46
+ const { user } = await blocklet.getUser(x.address);
47
+ if (user) {
48
+ customer = await Customer.create({
49
+ livemode: paymentIntent.livemode,
50
+ did: user.did,
51
+ name: user.fullName,
52
+ email: user.email,
53
+ phone: '',
54
+ address: {},
55
+ description: user.remark,
56
+ metadata: {},
57
+ balance: '0',
58
+ next_invoice_sequence: 1,
59
+ delinquent: false,
60
+ invoice_prefix: Customer.getInvoicePrefix(),
61
+ });
62
+ logger.info('Customer created on payout record', {
63
+ paymentIntent: paymentIntent.id,
64
+ address: x.address,
65
+ customer: customer.id,
66
+ });
67
+ }
68
+ }
69
+
70
+ const payout = new Payout({
71
+ livemode: paymentIntent.livemode,
72
+ automatic: true,
73
+ description: paymentIntent.description,
74
+ amount: x.share,
75
+ destination: x.address,
76
+ payment_details: paymentIntent.payment_details,
77
+ payment_intent_id: paymentIntent.id,
78
+ customer_id: customer?.id || '',
79
+ currency_id: paymentIntent.currency_id,
80
+ payment_method_id: paymentIntent.payment_method_id,
81
+ status: 'paid',
82
+ attempt_count: 0,
83
+ attempted: false,
84
+ next_attempt: 0,
85
+ last_attempt_error: null,
86
+ metadata: {},
87
+ });
88
+ return payout.save();
89
+ })
90
+ )
91
+ .then(() => logger.info('Payout records created from payment done', { paymentIntent: paymentIntent.id }))
92
+ .catch((err) =>
93
+ logger.error('Payout records creation failed from payment done', {
94
+ paymentIntent: paymentIntent.id,
95
+ error: err,
96
+ })
97
+ );
98
+ }
99
+
40
100
  let invoice;
41
101
  if (paymentIntent.invoice_id) {
42
102
  invoice = await Invoice.findByPk(paymentIntent.invoice_id);
@@ -21,7 +21,9 @@ import logger from '../libs/logger';
21
21
  import { isCreditSufficientForPayment, isDelegationSufficientForPayment } from '../libs/payment';
22
22
  import { authenticate } from '../libs/security';
23
23
  import {
24
+ canPayWithDelegation,
24
25
  canUpsell,
26
+ createPaymentBeneficiaries,
25
27
  expandLineItems,
26
28
  getBillingThreshold,
27
29
  getCheckoutAmount,
@@ -38,10 +40,10 @@ import {
38
40
  getDaysUntilDue,
39
41
  getSubscriptionCreateSetup,
40
42
  } from '../libs/subscription';
41
- import { CHECKOUT_SESSION_TTL, createCodeGenerator, formatMetadata, getDataObjectFromQuery } from '../libs/util';
43
+ import { CHECKOUT_SESSION_TTL, formatMetadata, getDataObjectFromQuery } from '../libs/util';
42
44
  import { invoiceQueue } from '../queues/invoice';
43
45
  import { paymentQueue } from '../queues/payment';
44
- import type { TPriceExpanded, TProductExpanded } from '../store/models';
46
+ import type { LineItem, TPriceExpanded, TProductExpanded } from '../store/models';
45
47
  import { CheckoutSession } from '../store/models/checkout-session';
46
48
  import { Customer } from '../store/models/customer';
47
49
  import { PaymentCurrency } from '../store/models/payment-currency';
@@ -55,8 +57,6 @@ import { Subscription } from '../store/models/subscription';
55
57
  import { SubscriptionItem } from '../store/models/subscription-item';
56
58
  import { ensureInvoiceForCheckout } from './connect/shared';
57
59
 
58
- const getInvoicePrefix = createCodeGenerator('', 8);
59
-
60
60
  const router = Router();
61
61
 
62
62
  const user = userMiddleware();
@@ -306,6 +306,9 @@ export async function startCheckoutSessionFromPaymentLink(id: string, req: Reque
306
306
  }
307
307
 
308
308
  const items = await Price.expand(link.line_items, { upsell: true });
309
+ if (items.find((x) => (x.upsell_price || x.price).custom_unit_amount) && items.length > 1) {
310
+ throw new Error('Multiple items with custom unit amount are not supported in checkout session');
311
+ }
309
312
 
310
313
  const raw: Partial<CheckoutSession> = await formatCheckoutSession(link, false);
311
314
  raw.livemode = link.livemode;
@@ -416,7 +419,6 @@ router.get('/retrieve/:id', user, async (req, res) => {
416
419
  // check payment intent
417
420
  const paymentIntent = doc.payment_intent_id ? await PaymentIntent.findByPk(doc.payment_intent_id) : null;
418
421
 
419
- // FIXME: possible sensitive data leak
420
422
  res.json({
421
423
  checkoutSession: doc.toJSON(),
422
424
  paymentMethods: await getPaymentMethods(doc),
@@ -478,6 +480,9 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
478
480
  amount_tax: amount.tax,
479
481
  },
480
482
  });
483
+ if (checkoutSession.mode === 'payment' && amount.total <= 0) {
484
+ return res.status(400).json({ error: 'Payment amount should be greater than 0' });
485
+ }
481
486
 
482
487
  // ensure customer created or updated
483
488
  let customer = await Customer.findOne({ where: { did: req.user.did } });
@@ -494,7 +499,7 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
494
499
  balance: '0',
495
500
  next_invoice_sequence: 1,
496
501
  delinquent: false,
497
- invoice_prefix: getInvoicePrefix(),
502
+ invoice_prefix: Customer.getInvoicePrefix(),
498
503
  });
499
504
  logger.info('customer created on checkout session submit', { did: req.user.did, id: customer.id });
500
505
  } else {
@@ -508,7 +513,7 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
508
513
  updates.address = req.body.billing_address;
509
514
  }
510
515
  if (!customer.invoice_prefix) {
511
- updates.invoice_prefix = getInvoicePrefix();
516
+ updates.invoice_prefix = Customer.getInvoicePrefix();
512
517
  }
513
518
 
514
519
  await customer.update(updates);
@@ -526,6 +531,10 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
526
531
  // payment intent is only created when checkout session is in payment mode
527
532
  let paymentIntent: PaymentIntent | null = null;
528
533
  if (checkoutSession.mode === 'payment') {
534
+ const paymentLink = checkoutSession.payment_link_id
535
+ ? await PaymentLink.findByPk(checkoutSession.payment_link_id)
536
+ : null;
537
+
529
538
  if (checkoutSession.payment_intent_id) {
530
539
  paymentIntent = await PaymentIntent.findByPk(checkoutSession.payment_intent_id);
531
540
  }
@@ -550,6 +559,10 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
550
559
  payment_method_id: paymentMethod.id,
551
560
  receipt_email: customer.email,
552
561
  last_payment_error: null,
562
+ beneficiaries: createPaymentBeneficiaries(
563
+ checkoutSession.amount_total,
564
+ paymentLink?.donation_settings?.beneficiaries || []
565
+ ),
553
566
  });
554
567
  logger.info('payment intent for checkout session reset', {
555
568
  session: checkoutSession.id,
@@ -574,6 +587,10 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
574
587
  checkoutSession.payment_intent_data?.statement_descriptor || getStatementDescriptor(lineItems),
575
588
  statement_descriptor_suffix: '',
576
589
  setup_future_usage: 'on_session',
590
+ beneficiaries: createPaymentBeneficiaries(
591
+ checkoutSession.amount_total,
592
+ paymentLink?.donation_settings?.beneficiaries || []
593
+ ),
577
594
  metadata: checkoutSession.payment_intent_data?.metadata || checkoutSession.metadata,
578
595
  });
579
596
  logger.info('paymentIntent created on checkout session submit', {
@@ -754,7 +771,8 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
754
771
  amount: fastCheckoutAmount,
755
772
  });
756
773
 
757
- if (checkoutSession.mode === 'payment' && paymentIntent) {
774
+ const canFastPay = canPayWithDelegation(paymentIntent?.beneficiaries || []);
775
+ if (checkoutSession.mode === 'payment' && paymentIntent && canFastPay) {
758
776
  if (balance.sufficient) {
759
777
  logger.info(`CheckoutSession ${checkoutSession.id} will pay from balance ${paymentIntent?.id}`);
760
778
  }
@@ -828,8 +846,8 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
828
846
  subscription,
829
847
  checkoutSession,
830
848
  customer,
831
- delegation: checkoutSession.mode === 'payment' ? delegation : null,
832
- balance: checkoutSession.mode === 'payment' ? balance : null,
849
+ delegation: checkoutSession.mode === 'payment' && canFastPay ? delegation : null,
850
+ balance: checkoutSession.mode === 'payment' && canFastPay ? balance : null,
833
851
  });
834
852
  } catch (err) {
835
853
  console.error(err);
@@ -1022,6 +1040,39 @@ router.delete('/:id/cross-sell', user, ensureCheckoutSessionOpen, async (req, re
1022
1040
  }
1023
1041
  });
1024
1042
 
1043
+ // change payment amount
1044
+ router.put('/:id/amount', user, ensureCheckoutSessionOpen, async (req, res) => {
1045
+ try {
1046
+ const checkoutSession = req.doc as CheckoutSession;
1047
+ const items = await Price.expand(checkoutSession.line_items);
1048
+ const item = items.find((x) => x.price_id === req.body.priceId);
1049
+ if (!item) {
1050
+ return res.status(400).json({ error: 'LineItem not in checkout session' });
1051
+ }
1052
+ if (!item.price.custom_unit_amount) {
1053
+ return res.status(400).json({ error: 'PriceItem not customizable for checkout session' });
1054
+ }
1055
+ // TODO: add more validation for amount
1056
+
1057
+ // update line items
1058
+ const newItems = cloneDeep(checkoutSession.line_items);
1059
+ const newItem = newItems.find((x) => x.price_id === req.body.priceId);
1060
+ if (newItem) {
1061
+ newItem.custom_amount = req.body.amount;
1062
+ }
1063
+ await checkoutSession.update({ line_items: newItems.map((x) => omit(x, ['price'])) as LineItem[] });
1064
+ logger.info('CheckoutSession updated on amount', { id: req.params.id, ...req.body, newItem });
1065
+
1066
+ // recalculate amount
1067
+ await checkoutSession.update(await getCheckoutSessionAmounts(checkoutSession));
1068
+
1069
+ res.json({ ...checkoutSession.toJSON(), line_items: await Price.expand(newItems) });
1070
+ } catch (err) {
1071
+ console.error(err);
1072
+ res.status(500).json({ error: err.message });
1073
+ }
1074
+ });
1075
+
1025
1076
  const schema = Joi.object<{
1026
1077
  page: number;
1027
1078
  pageSize: number;
@@ -3,6 +3,7 @@ import { fromAddress } from '@ocap/wallet';
3
3
 
4
4
  import type { CallbackArgs } from '../../libs/auth';
5
5
  import { wallet } from '../../libs/auth';
6
+ import logger from '../../libs/logger';
6
7
  import { getGasPayerExtra } from '../../libs/payment';
7
8
  import { getTxMetadata } from '../../libs/util';
8
9
  import { invoiceQueue } from '../../queues/invoice';
@@ -75,51 +76,57 @@ export default {
75
76
  const { invoice, paymentIntent, paymentMethod } = await ensureInvoiceForCollect(invoiceId);
76
77
 
77
78
  if (paymentMethod.type === 'arcblock') {
78
- await paymentIntent.update({ status: 'processing' });
79
- const client = paymentMethod.getOcapClient();
80
- const claim = claims.find((x) => x.type === 'prepareTx');
79
+ try {
80
+ await paymentIntent.update({ status: 'processing' });
81
+ const client = paymentMethod.getOcapClient();
82
+ const claim = claims.find((x) => x.type === 'prepareTx');
81
83
 
82
- const tx: Partial<Transaction> = client.decodeTx(claim.finalTx);
83
- if (claim.delegator && claim.from) {
84
- tx.delegator = claim.delegator;
85
- tx.from = claim.from;
86
- }
84
+ const tx: Partial<Transaction> = client.decodeTx(claim.finalTx);
85
+ if (claim.delegator && claim.from) {
86
+ tx.delegator = claim.delegator;
87
+ tx.from = claim.from;
88
+ }
87
89
 
88
- // @ts-ignore
89
- const { buffer } = await client.encodeTransferV3Tx({ tx });
90
- const txHash = await client.sendTransferV3Tx(
91
90
  // @ts-ignore
92
- { tx, wallet: fromAddress(userDid) },
93
- getGasPayerExtra(buffer)
94
- );
91
+ const { buffer } = await client.encodeTransferV3Tx({ tx });
92
+ const txHash = await client.sendTransferV3Tx(
93
+ // @ts-ignore
94
+ { tx, wallet: fromAddress(userDid) },
95
+ getGasPayerExtra(buffer)
96
+ );
95
97
 
96
- await paymentIntent.update({
97
- status: 'succeeded',
98
- amount_received: invoice.amount_due,
99
- capture_method: 'manual',
100
- last_payment_error: null,
101
- payment_details: {
102
- arcblock: {
103
- tx_hash: txHash,
104
- payer: userDid,
105
- type: 'transfer',
98
+ await paymentIntent.update({
99
+ status: 'succeeded',
100
+ amount_received: invoice.amount_due,
101
+ capture_method: 'manual',
102
+ last_payment_error: null,
103
+ payment_details: {
104
+ arcblock: {
105
+ tx_hash: txHash,
106
+ payer: userDid,
107
+ type: 'transfer',
108
+ },
106
109
  },
107
- },
108
- });
110
+ });
109
111
 
110
- await handlePaymentSucceed(paymentIntent);
112
+ await handlePaymentSucceed(paymentIntent);
111
113
 
112
- // cleanup the queue
113
- let exist = await paymentQueue.get(paymentIntent.id);
114
- if (exist) {
115
- await paymentQueue.delete(paymentIntent.id);
116
- }
117
- exist = await invoiceQueue.get(invoice.id);
118
- if (exist) {
119
- await invoiceQueue.delete(invoice.id);
120
- }
114
+ // cleanup the queue
115
+ let exist = await paymentQueue.get(paymentIntent.id);
116
+ if (exist) {
117
+ await paymentQueue.delete(paymentIntent.id);
118
+ }
119
+ exist = await invoiceQueue.get(invoice.id);
120
+ if (exist) {
121
+ await invoiceQueue.delete(invoice.id);
122
+ }
121
123
 
122
- return { hash: txHash };
124
+ return { hash: txHash };
125
+ } catch (err) {
126
+ logger.error('Failed to finalize collect', { paymentIntent: paymentIntent.id, error: err });
127
+ await paymentIntent.update({ status: 'requires_capture' });
128
+ return {};
129
+ }
123
130
  }
124
131
 
125
132
  throw new Error(`Payment method ${paymentMethod.type} not supported`);
@@ -2,8 +2,9 @@ import type { Transaction, TransferV3Tx } from '@ocap/client';
2
2
  import { fromAddress } from '@ocap/wallet';
3
3
 
4
4
  import type { CallbackArgs } from '../../libs/auth';
5
- import { wallet } from '../../libs/auth';
5
+ import logger from '../../libs/logger';
6
6
  import { getGasPayerExtra } from '../../libs/payment';
7
+ import { createPaymentOutput } from '../../libs/session';
7
8
  import { getTxMetadata } from '../../libs/util';
8
9
  import { handlePaymentSucceed } from '../../queues/payment';
9
10
  import { ensureInvoiceForCheckout, ensurePaymentIntent, getAuthPrincipalClaim } from './shared';
@@ -28,7 +29,11 @@ export default {
28
29
  const tokens = [{ address: paymentCurrency.contract as string, value: paymentIntent.amount }];
29
30
  // @ts-ignore
30
31
  const itx: TransferV3Tx = {
31
- outputs: [{ owner: wallet.address, tokens, assets: [] }],
32
+ outputs: createPaymentOutput(
33
+ paymentIntent.amount,
34
+ paymentCurrency.contract as string,
35
+ paymentIntent.beneficiaries || []
36
+ ),
32
37
  data: getTxMetadata({
33
38
  paymentIntentId: paymentIntent.id,
34
39
  checkoutSessionId,
@@ -64,40 +69,46 @@ export default {
64
69
  await ensureInvoiceForCheckout({ checkoutSession, customer, paymentIntent });
65
70
 
66
71
  if (paymentMethod.type === 'arcblock') {
67
- await paymentIntent.update({ status: 'processing' });
68
- const client = paymentMethod.getOcapClient();
69
- const claim = claims.find((x) => x.type === 'prepareTx');
72
+ try {
73
+ await paymentIntent.update({ status: 'processing' });
74
+ const client = paymentMethod.getOcapClient();
75
+ const claim = claims.find((x) => x.type === 'prepareTx');
70
76
 
71
- const tx: Partial<Transaction> = client.decodeTx(claim.finalTx);
72
- if (claim.delegator && claim.from) {
73
- tx.delegator = claim.delegator;
74
- tx.from = claim.from;
75
- }
77
+ const tx: Partial<Transaction> = client.decodeTx(claim.finalTx);
78
+ if (claim.delegator && claim.from) {
79
+ tx.delegator = claim.delegator;
80
+ tx.from = claim.from;
81
+ }
76
82
 
77
- // @ts-ignore
78
- const { buffer } = await client.encodeTransferV3Tx({ tx });
79
- const txHash = await client.sendTransferV3Tx(
80
83
  // @ts-ignore
81
- { tx, wallet: fromAddress(userDid) },
82
- getGasPayerExtra(buffer)
83
- );
84
+ const { buffer } = await client.encodeTransferV3Tx({ tx });
85
+ const txHash = await client.sendTransferV3Tx(
86
+ // @ts-ignore
87
+ { tx, wallet: fromAddress(userDid) },
88
+ getGasPayerExtra(buffer)
89
+ );
84
90
 
85
- await paymentIntent.update({
86
- status: 'succeeded',
87
- amount_received: paymentIntent.amount,
88
- last_payment_error: null,
89
- payment_details: {
90
- arcblock: {
91
- tx_hash: txHash,
92
- payer: userDid,
93
- type: 'transfer',
91
+ await paymentIntent.update({
92
+ status: 'succeeded',
93
+ amount_received: paymentIntent.amount,
94
+ last_payment_error: null,
95
+ payment_details: {
96
+ arcblock: {
97
+ tx_hash: txHash,
98
+ payer: userDid,
99
+ type: 'transfer',
100
+ },
94
101
  },
95
- },
96
- });
102
+ });
97
103
 
98
- await handlePaymentSucceed(paymentIntent);
104
+ await handlePaymentSucceed(paymentIntent);
99
105
 
100
- return { hash: txHash };
106
+ return { hash: txHash };
107
+ } catch (err) {
108
+ logger.error('Failed to finalize paymentIntent', { paymentIntent: paymentIntent.id, error: err });
109
+ await paymentIntent.update({ status: 'requires_capture' });
110
+ return {};
111
+ }
101
112
  }
102
113
 
103
114
  throw new Error(`Payment method ${paymentMethod.type} not supported`);