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.
- package/api/src/libs/api.ts +2 -2
- package/api/src/libs/session.ts +90 -4
- package/api/src/queues/payment.ts +61 -1
- package/api/src/routes/checkout-sessions.ts +61 -10
- package/api/src/routes/connect/collect.ts +44 -37
- package/api/src/routes/connect/pay.ts +40 -29
- package/api/src/routes/connect/setup.ts +39 -33
- package/api/src/routes/connect/shared.ts +3 -1
- package/api/src/routes/donations.ts +157 -0
- package/api/src/routes/index.ts +4 -0
- package/api/src/routes/payment-intents.ts +2 -2
- package/api/src/routes/payment-links.ts +8 -3
- package/api/src/routes/payouts.ts +151 -0
- package/api/src/routes/products.ts +24 -6
- package/api/src/routes/usage-records.ts +6 -3
- package/api/src/store/migrations/20240408-payout.ts +36 -0
- package/api/src/store/models/checkout-session.ts +5 -0
- package/api/src/store/models/customer.ts +6 -1
- package/api/src/store/models/index.ts +12 -0
- package/api/src/store/models/payment-intent.ts +38 -26
- package/api/src/store/models/payment-link.ts +8 -1
- package/api/src/store/models/payout.ts +243 -0
- package/api/src/store/models/types.ts +39 -0
- package/api/tests/libs/session.spec.ts +101 -0
- package/blocklet.yml +1 -1
- package/package.json +17 -16
- package/src/components/info-card.tsx +5 -5
- package/src/components/invoice/list.tsx +2 -0
- package/src/components/invoice/table.tsx +1 -1
- package/src/components/payment-intent/list.tsx +2 -0
- package/src/components/payouts/actions.tsx +43 -0
- package/src/components/payouts/list.tsx +255 -0
- package/src/components/refund/list.tsx +2 -0
- package/src/components/subscription/list.tsx +2 -0
- package/src/libs/util.ts +4 -1
- package/src/locales/en.tsx +7 -0
- package/src/locales/zh.tsx +6 -0
- package/src/pages/admin/customers/customers/index.tsx +2 -2
- package/src/pages/admin/payments/index.tsx +7 -0
- package/src/pages/admin/payments/intents/detail.tsx +7 -0
- package/src/pages/admin/payments/payouts/detail.tsx +204 -0
- package/src/pages/admin/payments/payouts/index.tsx +5 -0
- package/src/pages/admin/products/links/index.tsx +2 -2
- package/src/pages/admin/products/prices/detail.tsx +2 -1
- package/src/pages/admin/products/pricing-tables/index.tsx +2 -2
- package/src/pages/admin/products/products/index.tsx +2 -2
package/api/src/libs/api.ts
CHANGED
|
@@ -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(
|
|
121
|
+
.default(pageSize)
|
|
122
122
|
.custom((value) => {
|
|
123
123
|
if (value > 100) {
|
|
124
124
|
return 100;
|
package/api/src/libs/session.ts
CHANGED
|
@@ -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 [
|
|
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(
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
{
|
|
93
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
112
|
+
await handlePaymentSucceed(paymentIntent);
|
|
111
113
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
-
|
|
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
|
|
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:
|
|
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
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
{
|
|
82
|
-
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
104
|
+
await handlePaymentSucceed(paymentIntent);
|
|
99
105
|
|
|
100
|
-
|
|
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`);
|