payment-kit 1.13.49 → 1.13.51
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/integrations/blockchain/nft.ts +16 -9
- package/api/src/integrations/stripe/handlers/payment-intent.ts +5 -5
- package/api/src/jobs/payment.ts +25 -14
- package/api/src/libs/payment.ts +13 -1
- package/api/src/libs/session.ts +3 -1
- package/api/src/routes/checkout-sessions.ts +109 -8
- package/api/src/routes/connect/collect.ts +6 -2
- package/api/src/routes/connect/pay.ts +6 -2
- package/api/src/routes/connect/setup.ts +11 -4
- package/api/src/routes/connect/shared.ts +13 -2
- package/api/src/routes/connect/subscribe.ts +9 -4
- package/api/src/routes/payment-intents.ts +2 -1
- package/api/src/store/models/payment-intent.ts +1 -1
- package/api/src/store/models/setup-intent.ts +1 -1
- package/api/src/store/models/types.ts +1 -1
- package/blocklet.yml +1 -1
- package/package.json +8 -7
- package/src/components/blockchain/tx.tsx +11 -3
- package/src/components/portal/invoice/list.tsx +2 -1
- package/src/pages/customer/invoice.tsx +1 -1
|
@@ -1,10 +1,11 @@
|
|
|
1
|
+
import { isEthereumDid, isValid } from '@arcblock/did';
|
|
1
2
|
// import pick from 'lodash/pick';
|
|
2
3
|
import { formatFactoryState, preMintFromFactory } from '@ocap/asset';
|
|
3
4
|
import merge from 'lodash/merge';
|
|
4
5
|
|
|
5
6
|
import { wallet } from '../../libs/auth';
|
|
6
7
|
import logger from '../../libs/logger';
|
|
7
|
-
import { CheckoutSession,
|
|
8
|
+
import { CheckoutSession, PaymentIntent, PaymentMethod, Subscription } from '../../store/models';
|
|
8
9
|
import { sendNftNotification } from '../blocklet/notification';
|
|
9
10
|
|
|
10
11
|
export async function mintNftForCheckoutSession(id: string) {
|
|
@@ -34,13 +35,23 @@ export async function mintNftForCheckoutSession(id: string) {
|
|
|
34
35
|
return;
|
|
35
36
|
}
|
|
36
37
|
|
|
38
|
+
const { factory, inputs } = checkoutSession.nft_mint_settings as any;
|
|
39
|
+
if (isValid(factory) === false) {
|
|
40
|
+
logger.warn('checkoutSession nft mint settings invalid', { id, factory });
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// TODO: we only support minting from arcblock chain now
|
|
45
|
+
if (isEthereumDid(factory)) {
|
|
46
|
+
logger.warn('nft mint not supported for factory', { id, factory });
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
37
50
|
// TODO: we may need retry here when the chain is temporarily inaccessible
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
if (method?.type === 'arcblock') {
|
|
51
|
+
const method = await PaymentMethod.findOne({ where: { livemode: checkoutSession.livemode, type: 'arcblock' } });
|
|
52
|
+
if (method) {
|
|
41
53
|
const client = method.getOcapClient();
|
|
42
54
|
const nftOwner = checkoutSession.customer_did;
|
|
43
|
-
const { factory, inputs } = checkoutSession.nft_mint_settings as any;
|
|
44
55
|
|
|
45
56
|
const [{ state: factoryState }, { state: appState }] = await Promise.all([
|
|
46
57
|
client.getFactoryState({ address: factory }),
|
|
@@ -118,9 +129,5 @@ export async function mintNftForCheckoutSession(id: string) {
|
|
|
118
129
|
factoryState.name
|
|
119
130
|
);
|
|
120
131
|
logger.info('nft sent for checkoutSession', { id, nftOwner });
|
|
121
|
-
|
|
122
|
-
return;
|
|
123
132
|
}
|
|
124
|
-
|
|
125
|
-
logger.warn('nft mint not supported for payment method', { id, type: method?.type });
|
|
126
133
|
}
|
|
@@ -13,6 +13,7 @@ import { handleStripeInvoiceCreated } from './invoice';
|
|
|
13
13
|
export async function handleStripePaymentSucceed(paymentIntent: PaymentIntent, event?: TEventExpanded) {
|
|
14
14
|
await paymentIntent.update({
|
|
15
15
|
status: 'succeeded',
|
|
16
|
+
last_payment_error: null,
|
|
16
17
|
amount_received: paymentIntent.amount,
|
|
17
18
|
payment_details: merge(
|
|
18
19
|
paymentIntent.metadata,
|
|
@@ -37,8 +38,6 @@ export async function syncStripPayment(paymentIntent: PaymentIntent) {
|
|
|
37
38
|
const client = await method.getStripeClient();
|
|
38
39
|
const stripeIntent = await client.paymentIntents.retrieve(paymentIntent.metadata.stripe_id);
|
|
39
40
|
if (stripeIntent) {
|
|
40
|
-
const justSucceed = stripeIntent.status === 'succeeded' && paymentIntent.status !== 'succeeded';
|
|
41
|
-
|
|
42
41
|
// @ts-ignore
|
|
43
42
|
await paymentIntent.update({
|
|
44
43
|
amount: String(stripeIntent.amount),
|
|
@@ -49,9 +48,7 @@ export async function syncStripPayment(paymentIntent: PaymentIntent) {
|
|
|
49
48
|
});
|
|
50
49
|
logger.info('stripe payment intent synced', { locale: paymentIntent.id, remote: stripeIntent.id });
|
|
51
50
|
|
|
52
|
-
|
|
53
|
-
await handlePaymentSucceed(paymentIntent);
|
|
54
|
-
}
|
|
51
|
+
await handlePaymentSucceed(paymentIntent);
|
|
55
52
|
}
|
|
56
53
|
}
|
|
57
54
|
|
|
@@ -106,6 +103,9 @@ export async function handleStripePaymentCreated(event: TEventExpanded, client:
|
|
|
106
103
|
}
|
|
107
104
|
|
|
108
105
|
logger.info('stripe payment intent mirrored', { locale: paymentIntent.id, remote: stripeIntent.id });
|
|
106
|
+
if (stripeIntent.status === 'succeeded') {
|
|
107
|
+
await handlePaymentSucceed(paymentIntent);
|
|
108
|
+
}
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
|
package/api/src/jobs/payment.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { wallet } from '../libs/auth';
|
|
|
2
2
|
import dayjs from '../libs/dayjs';
|
|
3
3
|
import CustomError from '../libs/error';
|
|
4
4
|
import logger from '../libs/logger';
|
|
5
|
-
import { isDelegationSufficientForPayment } from '../libs/payment';
|
|
5
|
+
import { getGasPayerExtra, isDelegationSufficientForPayment } from '../libs/payment';
|
|
6
6
|
import createQueue from '../libs/queue';
|
|
7
7
|
import { MAX_RETRY_COUNT, getNextRetry } from '../libs/util';
|
|
8
8
|
import { CheckoutSession } from '../store/models/checkout-session';
|
|
@@ -37,17 +37,19 @@ export const handlePaymentSucceed = async (paymentIntent: PaymentIntent) => {
|
|
|
37
37
|
return;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
40
|
+
if (invoice.status !== 'paid') {
|
|
41
|
+
await invoice.update({
|
|
42
|
+
paid: true,
|
|
43
|
+
status: 'paid',
|
|
44
|
+
amount_due: '0',
|
|
45
|
+
amount_paid: paymentIntent.amount,
|
|
46
|
+
amount_remaining: '0',
|
|
47
|
+
attempt_count: invoice.attempt_count + 1,
|
|
48
|
+
attempted: true,
|
|
49
|
+
status_transitions: { ...invoice.status_transitions, paid_at: dayjs().unix() },
|
|
50
|
+
});
|
|
51
|
+
logger.info(`Invoice ${invoice.id} updated on payment done: ${paymentIntent.id}`);
|
|
52
|
+
}
|
|
51
53
|
|
|
52
54
|
if (invoice.subscription_id) {
|
|
53
55
|
const subscription = await Subscription.findByPk(invoice.subscription_id);
|
|
@@ -56,6 +58,7 @@ export const handlePaymentSucceed = async (paymentIntent: PaymentIntent) => {
|
|
|
56
58
|
await subscription.update({ status: subscription.trail_end ? 'trialing' : 'active' });
|
|
57
59
|
logger.info(`Subscription ${subscription.id} updated on payment done ${invoice.id}`);
|
|
58
60
|
} else {
|
|
61
|
+
// FIXME: possible error here
|
|
59
62
|
await subscription.update({ status: 'active' });
|
|
60
63
|
logger.info(`Subscription ${subscription.id} moved to active after payment done ${paymentIntent.id}`);
|
|
61
64
|
}
|
|
@@ -128,12 +131,14 @@ export const handlePayment = async (job: PaymentJob) => {
|
|
|
128
131
|
amount: paymentIntent.amount,
|
|
129
132
|
});
|
|
130
133
|
if (result.sufficient === false) {
|
|
134
|
+
logger.error('PaymentIntent capture aborted on preCheck', { id: paymentIntent.id, result });
|
|
135
|
+
// FIXME: send email to customer, pause subscription
|
|
131
136
|
throw new CustomError(result.reason, 'payer balance or delegation not sufficient for this payment');
|
|
132
137
|
}
|
|
133
138
|
|
|
134
139
|
// do the capture
|
|
135
140
|
await paymentIntent.update({ status: 'processing' });
|
|
136
|
-
const
|
|
141
|
+
const signed = await client.signTransferV2Tx({
|
|
137
142
|
tx: {
|
|
138
143
|
itx: {
|
|
139
144
|
to: wallet.address,
|
|
@@ -150,14 +155,20 @@ export const handlePayment = async (job: PaymentJob) => {
|
|
|
150
155
|
},
|
|
151
156
|
},
|
|
152
157
|
},
|
|
153
|
-
delegator: payer,
|
|
154
158
|
wallet,
|
|
159
|
+
delegator: payer,
|
|
155
160
|
});
|
|
161
|
+
// @ts-ignore
|
|
162
|
+
const { buffer } = await client.encodeTransferV2Tx({ tx: signed });
|
|
163
|
+
// @ts-ignore
|
|
164
|
+
const txHash = await client.sendTransferV2Tx({ tx: signed, wallet, delegator: payer }, getGasPayerExtra(buffer));
|
|
165
|
+
|
|
156
166
|
logger.info(`PaymentIntent capture done: ${paymentIntent.id} with tx ${txHash}`);
|
|
157
167
|
|
|
158
168
|
await paymentIntent.update({
|
|
159
169
|
status: 'succeeded',
|
|
160
170
|
amount_received: paymentIntent.amount,
|
|
171
|
+
last_payment_error: null,
|
|
161
172
|
payment_details: {
|
|
162
173
|
arcblock: {
|
|
163
174
|
tx_hash: txHash,
|
package/api/src/libs/payment.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { toDelegateAddress } from '@arcblock/did-util';
|
|
2
|
+
import { sign } from '@arcblock/jwt';
|
|
2
3
|
import { getWalletDid } from '@blocklet/sdk/lib/did';
|
|
3
4
|
import type { DelegateState } from '@ocap/client';
|
|
5
|
+
import { toTxHash } from '@ocap/mcrypto';
|
|
4
6
|
import { BN } from '@ocap/util';
|
|
5
7
|
|
|
6
8
|
import type { TPaymentCurrency } from '../store/models/payment-currency';
|
|
@@ -42,7 +44,7 @@ export async function isDelegationSufficientForPayment(args: {
|
|
|
42
44
|
if (!token) {
|
|
43
45
|
return { sufficient: false, reason: 'NO_TOKEN' };
|
|
44
46
|
}
|
|
45
|
-
if (new BN(token.balance).
|
|
47
|
+
if (new BN(token.balance).lt(new BN(amount))) {
|
|
46
48
|
return { sufficient: false, reason: 'NO_ENOUGH_TOKEN' };
|
|
47
49
|
}
|
|
48
50
|
|
|
@@ -55,3 +57,13 @@ export async function isDelegationSufficientForPayment(args: {
|
|
|
55
57
|
|
|
56
58
|
throw new Error(`Payment method ${paymentMethod.type} not supported`);
|
|
57
59
|
}
|
|
60
|
+
|
|
61
|
+
export function getGasPayerExtra(txBuffer: Buffer) {
|
|
62
|
+
const txHash = toTxHash(txBuffer);
|
|
63
|
+
return {
|
|
64
|
+
headers: {
|
|
65
|
+
'x-gas-payer-sig': sign(wallet.address, wallet.secretKey, { txHash }),
|
|
66
|
+
'x-gas-payer-pk': wallet.publicKey,
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
package/api/src/libs/session.ts
CHANGED
|
@@ -169,7 +169,9 @@ export function getSubscriptionCycleAmount(items: TLineItemExpanded[], currency:
|
|
|
169
169
|
export function expandLineItems(items: any[], products: Product[], prices: Price[]) {
|
|
170
170
|
items.forEach((item) => {
|
|
171
171
|
item.price = prices.find((x) => x.id === item.price_id);
|
|
172
|
-
|
|
172
|
+
if (item.price) {
|
|
173
|
+
item.price.product = products.find((x) => x.id === item.price.product_id);
|
|
174
|
+
}
|
|
173
175
|
});
|
|
174
176
|
|
|
175
177
|
return items;
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
/* eslint-disable consistent-return */
|
|
2
|
+
import { isValid } from '@arcblock/did';
|
|
2
3
|
import { getUrl } from '@blocklet/sdk/lib/component';
|
|
3
4
|
import userMiddleware from '@blocklet/sdk/lib/middlewares/user';
|
|
4
5
|
import { NextFunction, Request, Response, Router } from 'express';
|
|
6
|
+
import Joi from 'joi';
|
|
5
7
|
import cloneDeep from 'lodash/cloneDeep';
|
|
6
8
|
import merge from 'lodash/merge';
|
|
7
9
|
import omit from 'lodash/omit';
|
|
8
10
|
import pick from 'lodash/pick';
|
|
9
11
|
import sortBy from 'lodash/sortBy';
|
|
10
12
|
import uniq from 'lodash/uniq';
|
|
13
|
+
import type { WhereOptions } from 'sequelize';
|
|
11
14
|
|
|
12
15
|
import { checkPassportForPaymentLink } from '../integrations/blocklet/passport';
|
|
13
16
|
import { handleStripePaymentSucceed } from '../integrations/stripe/handlers/payment-intent';
|
|
@@ -22,6 +25,7 @@ import { isDelegationSufficientForPayment } from '../libs/payment';
|
|
|
22
25
|
import { authenticate } from '../libs/security';
|
|
23
26
|
import {
|
|
24
27
|
canUpsell,
|
|
28
|
+
expandLineItems,
|
|
25
29
|
getCheckoutAmount,
|
|
26
30
|
getCheckoutMode,
|
|
27
31
|
getFastCheckoutAmount,
|
|
@@ -506,11 +510,17 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
506
510
|
return res.status(403).json({ code: 'PAYMENT_PROCESSING', error: 'Checkout session payment processing' });
|
|
507
511
|
}
|
|
508
512
|
paymentIntent = await paymentIntent.update({
|
|
513
|
+
status: 'requires_capture',
|
|
509
514
|
amount: checkoutSession.amount_total,
|
|
510
515
|
customer_id: customer.id,
|
|
511
516
|
currency_id: paymentCurrency.id,
|
|
512
517
|
payment_method_id: paymentMethod.id,
|
|
513
518
|
receipt_email: customer.email,
|
|
519
|
+
last_payment_error: null,
|
|
520
|
+
});
|
|
521
|
+
logger.info('payment intent for checkout session reset', {
|
|
522
|
+
session: checkoutSession.id,
|
|
523
|
+
intent: paymentIntent.id,
|
|
514
524
|
});
|
|
515
525
|
} else {
|
|
516
526
|
paymentIntent = await PaymentIntent.create({
|
|
@@ -547,7 +557,7 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
547
557
|
}
|
|
548
558
|
|
|
549
559
|
let setupIntent: SetupIntent | null = null;
|
|
550
|
-
if (checkoutSession.mode === 'setup'
|
|
560
|
+
if (checkoutSession.mode === 'setup' && paymentMethod.type !== 'stripe') {
|
|
551
561
|
if (checkoutSession.setup_intent_id) {
|
|
552
562
|
setupIntent = await SetupIntent.findByPk(checkoutSession.setup_intent_id);
|
|
553
563
|
}
|
|
@@ -563,16 +573,22 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
563
573
|
return res.status(403).json({ code: 'SETUP_PROCESSING', error: 'Checkout session setup processing' });
|
|
564
574
|
}
|
|
565
575
|
await setupIntent.update({
|
|
576
|
+
status: 'requires_capture',
|
|
566
577
|
customer_id: customer.id,
|
|
567
578
|
currency_id: paymentCurrency.id,
|
|
568
579
|
payment_method_id: paymentMethod.id,
|
|
580
|
+
last_setup_error: null,
|
|
581
|
+
});
|
|
582
|
+
logger.info('setup intent for checkout session reset', {
|
|
583
|
+
session: checkoutSession.id,
|
|
584
|
+
intent: setupIntent.id,
|
|
569
585
|
});
|
|
570
586
|
} else {
|
|
571
587
|
// ensure payment intent
|
|
572
588
|
setupIntent = await SetupIntent.create({
|
|
573
589
|
livemode: !!checkoutSession.livemode,
|
|
574
590
|
customer_id: customer.id,
|
|
575
|
-
description: '',
|
|
591
|
+
description: checkoutSession.payment_intent_data?.description || '',
|
|
576
592
|
currency_id: paymentCurrency.id,
|
|
577
593
|
payment_method_id: paymentMethod.id,
|
|
578
594
|
status: 'requires_payment_method',
|
|
@@ -698,20 +714,15 @@ router.put('/:id/submit', user, ensureCheckoutSessionOpen, async (req, res) => {
|
|
|
698
714
|
});
|
|
699
715
|
}
|
|
700
716
|
if (checkoutSession.mode === 'payment' && paymentIntent) {
|
|
701
|
-
await paymentIntent.update({ status: 'requires_capture' });
|
|
702
717
|
const { invoice } = await ensureInvoiceForCheckout({ checkoutSession, customer, paymentIntent });
|
|
703
718
|
if (invoice) {
|
|
704
719
|
await invoice.update({ auto_advance: true, payment_settings: paymentSettings });
|
|
705
720
|
invoiceQueue.push({ id: invoice.id, job: { invoiceId: invoice.id, retryOnError: false } });
|
|
706
721
|
} else {
|
|
707
|
-
|
|
722
|
+
paymentQueue.push({
|
|
708
723
|
id: paymentIntent.id,
|
|
709
724
|
job: { paymentIntentId: paymentIntent.id, paymentSettings, retryOnError: false },
|
|
710
725
|
});
|
|
711
|
-
job.on('finished', async () => {
|
|
712
|
-
await checkoutSession.update({ status: 'complete', payment_status: 'paid' });
|
|
713
|
-
logger.info(`CheckoutSession ${checkoutSession.id} updated on payment done ${paymentIntent?.id}`);
|
|
714
|
-
});
|
|
715
726
|
}
|
|
716
727
|
}
|
|
717
728
|
if (checkoutSession.mode === 'setup' && setupIntent && subscription) {
|
|
@@ -950,4 +961,94 @@ router.delete('/:id/cross-sell', user, ensureCheckoutSessionOpen, async (req, re
|
|
|
950
961
|
}
|
|
951
962
|
});
|
|
952
963
|
|
|
964
|
+
const schema = Joi.object<{
|
|
965
|
+
page: number;
|
|
966
|
+
pageSize: number;
|
|
967
|
+
status?: string;
|
|
968
|
+
payment_status?: string;
|
|
969
|
+
nft_mint_status?: string;
|
|
970
|
+
customer_id?: string;
|
|
971
|
+
customer_did?: string;
|
|
972
|
+
payment_intent_id?: string;
|
|
973
|
+
subscription_id?: string;
|
|
974
|
+
livemode?: boolean;
|
|
975
|
+
}>({
|
|
976
|
+
page: Joi.number().integer().min(1).default(1),
|
|
977
|
+
pageSize: Joi.number().integer().min(1).max(100).default(20),
|
|
978
|
+
status: Joi.string().empty(''),
|
|
979
|
+
payment_status: Joi.string().empty(''),
|
|
980
|
+
nft_mint_status: Joi.string().empty(''),
|
|
981
|
+
customer_id: Joi.string().empty(''),
|
|
982
|
+
customer_did: Joi.string().empty(''),
|
|
983
|
+
payment_intent_id: Joi.string().empty(''),
|
|
984
|
+
subscription_id: Joi.string().empty(''),
|
|
985
|
+
livemode: Joi.boolean().empty(''),
|
|
986
|
+
});
|
|
987
|
+
router.get('/', auth, async (req, res) => {
|
|
988
|
+
const { page, pageSize, livemode, ...query } = await schema.validateAsync(req.query, {
|
|
989
|
+
stripUnknown: false,
|
|
990
|
+
allowUnknown: true,
|
|
991
|
+
});
|
|
992
|
+
const where: WhereOptions<CheckoutSession> = {};
|
|
993
|
+
|
|
994
|
+
['status', 'payment_status', 'nft_mint_status'].forEach((key) => {
|
|
995
|
+
// @ts-ignore
|
|
996
|
+
if (query[key]) {
|
|
997
|
+
// @ts-ignore
|
|
998
|
+
where[key] = query[key].split(',').map((x: string) => x.trim()).filter(Boolean); // prettier-ignore
|
|
999
|
+
}
|
|
1000
|
+
});
|
|
1001
|
+
if (query.customer_id) {
|
|
1002
|
+
where.customer_id = query.customer_id;
|
|
1003
|
+
}
|
|
1004
|
+
if (query.payment_intent_id) {
|
|
1005
|
+
where.payment_intent_id = query.payment_intent_id;
|
|
1006
|
+
}
|
|
1007
|
+
if (query.subscription_id) {
|
|
1008
|
+
where.subscription_id = query.subscription_id;
|
|
1009
|
+
}
|
|
1010
|
+
if (query.customer_did && isValid(query.customer_did)) {
|
|
1011
|
+
const customer = await Customer.findOne({ where: { did: query.customer_did } });
|
|
1012
|
+
if (customer) {
|
|
1013
|
+
where.customer_id = customer.id;
|
|
1014
|
+
} else {
|
|
1015
|
+
res.json({ count: 0, list: [] });
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
if (typeof livemode === 'boolean') {
|
|
1020
|
+
where.livemode = livemode;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
Object.keys(query)
|
|
1024
|
+
.filter((x) => x.startsWith('metadata.'))
|
|
1025
|
+
.forEach((key: string) => {
|
|
1026
|
+
// @ts-ignore
|
|
1027
|
+
where[key] = query[key];
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
try {
|
|
1031
|
+
const { rows: list, count } = await CheckoutSession.findAndCountAll({
|
|
1032
|
+
where,
|
|
1033
|
+
order: [['created_at', 'DESC']],
|
|
1034
|
+
offset: (page - 1) * pageSize,
|
|
1035
|
+
limit: pageSize,
|
|
1036
|
+
include: [],
|
|
1037
|
+
});
|
|
1038
|
+
|
|
1039
|
+
const condition = { where: { livemode: !!req.livemode } };
|
|
1040
|
+
const products = (await Product.findAll(condition)).map((x) => x.toJSON());
|
|
1041
|
+
const prices = (await Price.findAll(condition)).map((x) => x.toJSON());
|
|
1042
|
+
const docs = list.map((x) => x.toJSON());
|
|
1043
|
+
|
|
1044
|
+
// @ts-ignore
|
|
1045
|
+
docs.forEach((x) => expandLineItems(x.line_items, products, prices));
|
|
1046
|
+
|
|
1047
|
+
res.json({ count, list: docs });
|
|
1048
|
+
} catch (err) {
|
|
1049
|
+
console.error(err);
|
|
1050
|
+
res.json({ count: 0, list: [] });
|
|
1051
|
+
}
|
|
1052
|
+
});
|
|
1053
|
+
|
|
953
1054
|
export default router;
|
|
@@ -5,6 +5,7 @@ import { invoiceQueue } from '../../jobs/invoice';
|
|
|
5
5
|
import { handlePaymentSucceed, paymentQueue } from '../../jobs/payment';
|
|
6
6
|
import type { CallbackArgs } from '../../libs/auth';
|
|
7
7
|
import { wallet } from '../../libs/auth';
|
|
8
|
+
import { getGasPayerExtra } from '../../libs/payment';
|
|
8
9
|
import { getTxMetadata } from '../../libs/util';
|
|
9
10
|
import { ensureInvoiceForCollect, getAuthPrincipalClaim } from './shared';
|
|
10
11
|
|
|
@@ -49,7 +50,7 @@ export default {
|
|
|
49
50
|
|
|
50
51
|
throw new Error(`Payment method ${paymentMethod.type} not supported`);
|
|
51
52
|
},
|
|
52
|
-
onAuth: async ({ userDid, claims,
|
|
53
|
+
onAuth: async ({ userDid, claims, extraParams }: CallbackArgs) => {
|
|
53
54
|
const { invoiceId } = extraParams;
|
|
54
55
|
const { invoice, paymentIntent, paymentMethod } = await ensureInvoiceForCollect(invoiceId);
|
|
55
56
|
|
|
@@ -64,16 +65,19 @@ export default {
|
|
|
64
65
|
tx.from = claim.from;
|
|
65
66
|
}
|
|
66
67
|
|
|
68
|
+
// @ts-ignore
|
|
69
|
+
const { buffer } = await client.encodeTransferV3Tx({ tx });
|
|
67
70
|
const txHash = await client.sendTransferV3Tx(
|
|
68
71
|
// @ts-ignore
|
|
69
72
|
{ tx, wallet: fromAddress(userDid) },
|
|
70
|
-
|
|
73
|
+
getGasPayerExtra(buffer)
|
|
71
74
|
);
|
|
72
75
|
|
|
73
76
|
await paymentIntent.update({
|
|
74
77
|
status: 'succeeded',
|
|
75
78
|
amount_received: invoice.amount_due,
|
|
76
79
|
capture_method: 'manual',
|
|
80
|
+
last_payment_error: null,
|
|
77
81
|
payment_details: {
|
|
78
82
|
arcblock: {
|
|
79
83
|
tx_hash: txHash,
|
|
@@ -4,6 +4,7 @@ import { fromAddress } from '@ocap/wallet';
|
|
|
4
4
|
import { handlePaymentSucceed } from '../../jobs/payment';
|
|
5
5
|
import type { CallbackArgs } from '../../libs/auth';
|
|
6
6
|
import { wallet } from '../../libs/auth';
|
|
7
|
+
import { getGasPayerExtra } from '../../libs/payment';
|
|
7
8
|
import { getTxMetadata } from '../../libs/util';
|
|
8
9
|
import { ensureInvoiceForCheckout, ensurePaymentIntent, getAuthPrincipalClaim } from './shared';
|
|
9
10
|
|
|
@@ -50,7 +51,7 @@ export default {
|
|
|
50
51
|
|
|
51
52
|
throw new Error(`Payment method ${paymentMethod.type} not supported`);
|
|
52
53
|
},
|
|
53
|
-
onAuth: async ({ userDid, claims,
|
|
54
|
+
onAuth: async ({ userDid, claims, extraParams }: CallbackArgs) => {
|
|
54
55
|
const { checkoutSessionId } = extraParams;
|
|
55
56
|
const { checkoutSession, customer, paymentIntent, paymentMethod } = await ensurePaymentIntent(
|
|
56
57
|
checkoutSessionId,
|
|
@@ -73,15 +74,18 @@ export default {
|
|
|
73
74
|
tx.from = claim.from;
|
|
74
75
|
}
|
|
75
76
|
|
|
77
|
+
// @ts-ignore
|
|
78
|
+
const { buffer } = await client.encodeTransferV3Tx({ tx });
|
|
76
79
|
const txHash = await client.sendTransferV3Tx(
|
|
77
80
|
// @ts-ignore
|
|
78
81
|
{ tx, wallet: fromAddress(userDid) },
|
|
79
|
-
|
|
82
|
+
getGasPayerExtra(buffer)
|
|
80
83
|
);
|
|
81
84
|
|
|
82
85
|
await paymentIntent.update({
|
|
83
86
|
status: 'succeeded',
|
|
84
87
|
amount_received: paymentIntent.amount,
|
|
88
|
+
last_payment_error: null,
|
|
85
89
|
payment_details: {
|
|
86
90
|
arcblock: {
|
|
87
91
|
tx_hash: txHash,
|
|
@@ -6,6 +6,7 @@ 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 { getGasPayerExtra } from '../../libs/payment';
|
|
9
10
|
import { getFastCheckoutAmount } from '../../libs/session';
|
|
10
11
|
import { getTxMetadata } from '../../libs/util';
|
|
11
12
|
import type { TLineItemExpanded } from '../../store/models';
|
|
@@ -56,7 +57,7 @@ export default {
|
|
|
56
57
|
},
|
|
57
58
|
nonce: checkoutSessionId,
|
|
58
59
|
requirement: {
|
|
59
|
-
tokens: [{ address: paymentCurrency.contract as string, value: amount }],
|
|
60
|
+
tokens: amount === '0' ? [] : [{ address: paymentCurrency.contract as string, value: amount }],
|
|
60
61
|
},
|
|
61
62
|
chainInfo: {
|
|
62
63
|
host: paymentMethod.settings?.arcblock?.api_host as string,
|
|
@@ -68,7 +69,7 @@ export default {
|
|
|
68
69
|
|
|
69
70
|
throw new Error(`Payment method ${paymentMethod.type} not supported`);
|
|
70
71
|
},
|
|
71
|
-
onAuth: async ({ userDid, userPk, claims,
|
|
72
|
+
onAuth: async ({ userDid, userPk, claims, extraParams }: CallbackArgs) => {
|
|
72
73
|
const { checkoutSessionId } = extraParams;
|
|
73
74
|
const { setupIntent, checkoutSession, paymentMethod, subscription } = await ensureSetupIntent(
|
|
74
75
|
checkoutSessionId,
|
|
@@ -80,6 +81,7 @@ export default {
|
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
if (paymentMethod.type === 'arcblock') {
|
|
84
|
+
await setupIntent.update({ status: 'processing' });
|
|
83
85
|
await subscription.update({
|
|
84
86
|
payment_settings: {
|
|
85
87
|
payment_method_types: ['arcblock'],
|
|
@@ -94,15 +96,20 @@ export default {
|
|
|
94
96
|
|
|
95
97
|
// execute the delegate tx
|
|
96
98
|
const tx: Partial<Transaction> = client.decodeTx(claim.origin);
|
|
99
|
+
tx.signature = claim.sig;
|
|
100
|
+
|
|
101
|
+
// @ts-ignore
|
|
102
|
+
const { buffer } = await client.encodeDelegateTx({ tx });
|
|
97
103
|
const txHash = await client.sendDelegateTx(
|
|
98
104
|
// @ts-ignore
|
|
99
|
-
{ tx, wallet: fromPublicKey(userPk, toTypeInfo(userDid))
|
|
100
|
-
|
|
105
|
+
{ tx, wallet: fromPublicKey(userPk, toTypeInfo(userDid)) },
|
|
106
|
+
getGasPayerExtra(buffer)
|
|
101
107
|
);
|
|
102
108
|
|
|
103
109
|
await setupIntent.update({
|
|
104
110
|
status: 'succeeded',
|
|
105
111
|
payment_method_types: ['arcblock'],
|
|
112
|
+
last_setup_error: null,
|
|
106
113
|
payment_method_options: {
|
|
107
114
|
arcblock: { payer: userDid },
|
|
108
115
|
},
|
|
@@ -226,9 +226,20 @@ export async function ensureInvoiceForCheckout({
|
|
|
226
226
|
|
|
227
227
|
// Do not create invoice if it's already created
|
|
228
228
|
if (checkoutSession.invoice_id) {
|
|
229
|
-
logger.
|
|
229
|
+
logger.info(`Invoice already created for checkout session ${checkoutSession.id}: ${checkoutSession.invoice_id}`);
|
|
230
|
+
const invoice = await Invoice.findByPk(checkoutSession.invoice_id);
|
|
231
|
+
if (invoice) {
|
|
232
|
+
await invoice.update({ status: 'open' });
|
|
233
|
+
logger.info(`Invoice status reset for checkout session ${checkoutSession.id}: ${checkoutSession.invoice_id}`);
|
|
234
|
+
if (invoice.payment_intent_id) {
|
|
235
|
+
await PaymentIntent.update({ status: 'requires_capture' }, { where: { id: invoice.payment_intent_id } });
|
|
236
|
+
logger.info(
|
|
237
|
+
`PaymentIntent status reset for checkout session ${checkoutSession.id}: ${invoice.payment_intent_id}`
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
230
241
|
return {
|
|
231
|
-
invoice
|
|
242
|
+
invoice,
|
|
232
243
|
items: await InvoiceItem.findAll({ where: { invoice_id: checkoutSession.invoice_id } }),
|
|
233
244
|
};
|
|
234
245
|
}
|
|
@@ -8,6 +8,7 @@ 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 { getGasPayerExtra } from '../../libs/payment';
|
|
11
12
|
import { getFastCheckoutAmount } from '../../libs/session';
|
|
12
13
|
import { getTxMetadata } from '../../libs/util';
|
|
13
14
|
import type { TLineItemExpanded } from '../../store/models';
|
|
@@ -58,7 +59,7 @@ export default {
|
|
|
58
59
|
},
|
|
59
60
|
nonce: checkoutSessionId,
|
|
60
61
|
requirement: {
|
|
61
|
-
tokens: [{ address: paymentCurrency.contract as string, value: amount }],
|
|
62
|
+
tokens: amount === '0' ? [] : [{ address: paymentCurrency.contract as string, value: amount }],
|
|
62
63
|
},
|
|
63
64
|
chainInfo: {
|
|
64
65
|
host: paymentMethod.settings?.arcblock?.api_host as string,
|
|
@@ -70,7 +71,7 @@ export default {
|
|
|
70
71
|
|
|
71
72
|
throw new Error(`Payment method ${paymentMethod.type} not supported`);
|
|
72
73
|
},
|
|
73
|
-
onAuth: async ({ userDid, userPk, claims,
|
|
74
|
+
onAuth: async ({ userDid, userPk, claims, extraParams }: CallbackArgs) => {
|
|
74
75
|
const { checkoutSessionId } = extraParams;
|
|
75
76
|
const { checkoutSession, customer, paymentMethod, paymentCurrency, subscription } = await ensurePaymentIntent(
|
|
76
77
|
checkoutSessionId,
|
|
@@ -109,10 +110,14 @@ export default {
|
|
|
109
110
|
|
|
110
111
|
// execute the delegate tx
|
|
111
112
|
const tx: Partial<Transaction> = client.decodeTx(claim.origin);
|
|
113
|
+
tx.signature = claim.sig;
|
|
114
|
+
|
|
115
|
+
// @ts-ignore
|
|
116
|
+
const { buffer } = await client.encodeDelegateTx({ tx });
|
|
112
117
|
const txHash = await client.sendDelegateTx(
|
|
113
118
|
// @ts-ignore
|
|
114
|
-
{ tx, wallet: fromPublicKey(userPk, toTypeInfo(userDid))
|
|
115
|
-
|
|
119
|
+
{ tx, wallet: fromPublicKey(userPk, toTypeInfo(userDid)) },
|
|
120
|
+
getGasPayerExtra(buffer)
|
|
116
121
|
);
|
|
117
122
|
|
|
118
123
|
await subscription.update({
|
|
@@ -118,7 +118,8 @@ router.get('/:id', authPortal, async (req, res) => {
|
|
|
118
118
|
let subscription;
|
|
119
119
|
|
|
120
120
|
if (doc) {
|
|
121
|
-
|
|
121
|
+
const shouldSync = doc.status !== 'succeeded' || req.query.sync === '1';
|
|
122
|
+
if (doc.metadata?.stripe_id && shouldSync) {
|
|
122
123
|
await syncStripPayment(doc);
|
|
123
124
|
}
|
|
124
125
|
|
|
@@ -35,7 +35,7 @@ export class PaymentIntent extends Model<InferAttributes<PaymentIntent>, InferCr
|
|
|
35
35
|
declare last_charge?: string;
|
|
36
36
|
|
|
37
37
|
// The payment error encountered in the previous PaymentIntent confirmation
|
|
38
|
-
declare last_payment_error
|
|
38
|
+
declare last_payment_error: PaymentError | null;
|
|
39
39
|
|
|
40
40
|
declare metadata?: Record<string, any>;
|
|
41
41
|
|
|
@@ -22,7 +22,7 @@ export class SetupIntent extends Model<InferAttributes<SetupIntent>, InferCreati
|
|
|
22
22
|
declare customer_id: string;
|
|
23
23
|
|
|
24
24
|
// The setup error encountered in the previous SetupIntent confirmation
|
|
25
|
-
declare last_setup_error
|
|
25
|
+
declare last_setup_error: PaymentError | null;
|
|
26
26
|
|
|
27
27
|
declare metadata?: Record<string, any>;
|
|
28
28
|
|
|
@@ -272,7 +272,7 @@ export type PaymentDetails = {
|
|
|
272
272
|
export type NftMintSettings = {
|
|
273
273
|
enabled: boolean;
|
|
274
274
|
behavior?: LiteralUnion<'per_customer' | 'per_checkout_session', string>;
|
|
275
|
-
factory?: string;
|
|
275
|
+
factory?: string; // the factory address determines which chain to mint on
|
|
276
276
|
inputs?: Record<string, string>;
|
|
277
277
|
};
|
|
278
278
|
|
package/blocklet.yml
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "payment-kit",
|
|
3
|
-
"version": "1.13.
|
|
3
|
+
"version": "1.13.51",
|
|
4
4
|
"scripts": {
|
|
5
5
|
"dev": "blocklet dev",
|
|
6
6
|
"eject": "vite eject",
|
|
@@ -42,13 +42,14 @@
|
|
|
42
42
|
"dependencies": {
|
|
43
43
|
"@arcblock/did": "^1.18.95",
|
|
44
44
|
"@arcblock/did-auth-storage-nedb": "^1.7.1",
|
|
45
|
-
"@arcblock/did-connect": "^2.8.
|
|
45
|
+
"@arcblock/did-connect": "^2.8.8",
|
|
46
46
|
"@arcblock/did-util": "^1.18.95",
|
|
47
|
-
"@arcblock/
|
|
47
|
+
"@arcblock/jwt": "^1.18.95",
|
|
48
|
+
"@arcblock/ux": "^2.8.8",
|
|
48
49
|
"@blocklet/logger": "1.16.17",
|
|
49
50
|
"@blocklet/sdk": "1.16.17",
|
|
50
|
-
"@blocklet/ui-react": "^2.8.
|
|
51
|
-
"@blocklet/uploader": "^0.0.
|
|
51
|
+
"@blocklet/ui-react": "^2.8.8",
|
|
52
|
+
"@blocklet/uploader": "^0.0.34",
|
|
52
53
|
"@mui/icons-material": "^5.14.16",
|
|
53
54
|
"@mui/lab": "^5.0.0-alpha.151",
|
|
54
55
|
"@mui/material": "^5.14.16",
|
|
@@ -103,7 +104,7 @@
|
|
|
103
104
|
"@abtnode/types": "1.16.17",
|
|
104
105
|
"@arcblock/eslint-config": "^0.2.4",
|
|
105
106
|
"@arcblock/eslint-config-ts": "^0.2.4",
|
|
106
|
-
"@did-pay/types": "1.13.
|
|
107
|
+
"@did-pay/types": "1.13.51",
|
|
107
108
|
"@types/cookie-parser": "^1.4.5",
|
|
108
109
|
"@types/cors": "^2.8.15",
|
|
109
110
|
"@types/dotenv-flow": "^3.3.2",
|
|
@@ -140,5 +141,5 @@
|
|
|
140
141
|
"parser": "typescript"
|
|
141
142
|
}
|
|
142
143
|
},
|
|
143
|
-
"gitHead": "
|
|
144
|
+
"gitHead": "cd8bc5e262b3f6e13f2d9983ade1b689ff43c725"
|
|
144
145
|
}
|
|
@@ -6,7 +6,7 @@ import { joinURL } from 'ufo';
|
|
|
6
6
|
const getTxLink = (method: TPaymentMethod, details: PaymentDetails) => {
|
|
7
7
|
if (method.type === 'arcblock') {
|
|
8
8
|
return {
|
|
9
|
-
link: joinURL(method.settings.arcblock?.explorer_host as string, '/
|
|
9
|
+
link: joinURL(method.settings.arcblock?.explorer_host as string, '/txs', details.arcblock?.tx_hash as string),
|
|
10
10
|
text: details.arcblock?.tx_hash as string,
|
|
11
11
|
};
|
|
12
12
|
}
|
|
@@ -37,8 +37,16 @@ const getTxLink = (method: TPaymentMethod, details: PaymentDetails) => {
|
|
|
37
37
|
return { text: 'N/A', link: '' };
|
|
38
38
|
};
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
TxLink.defaultProps = {
|
|
41
|
+
mode: 'dashboard',
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export default function TxLink(props: {
|
|
45
|
+
details: PaymentDetails;
|
|
46
|
+
method: TPaymentMethod;
|
|
47
|
+
mode?: 'customer' | 'dashboard';
|
|
48
|
+
}) {
|
|
49
|
+
if (!props.details || (props.mode === 'customer' && props.method.type === 'stripe')) {
|
|
42
50
|
return (
|
|
43
51
|
<Typography component="small" color="text.secondary">
|
|
44
52
|
None
|
|
@@ -62,7 +62,7 @@ export default function CustomerInvoiceList({ customer_id }: Props) {
|
|
|
62
62
|
const grouped = groupByDate(data.list);
|
|
63
63
|
|
|
64
64
|
return (
|
|
65
|
-
<Stack direction="column" gap={
|
|
65
|
+
<Stack direction="column" gap={1} sx={{ mt: 1 }}>
|
|
66
66
|
{Object.entries(grouped).map(([date, invoices]) => (
|
|
67
67
|
<Box key={date}>
|
|
68
68
|
<Typography sx={{ fontWeight: 'bold', color: 'text.secondary', mt: 2, mb: 1 }}>{date}</Typography>
|
|
@@ -73,6 +73,7 @@ export default function CustomerInvoiceList({ customer_id }: Props) {
|
|
|
73
73
|
xs: 'column',
|
|
74
74
|
sm: 'row',
|
|
75
75
|
}}
|
|
76
|
+
sx={{ my: 1 }}
|
|
76
77
|
gap={{
|
|
77
78
|
xs: 0.5,
|
|
78
79
|
sm: 1.5,
|
|
@@ -111,7 +111,7 @@ export default function CustomerHome() {
|
|
|
111
111
|
label={t('common.txHash')}
|
|
112
112
|
value={
|
|
113
113
|
data.paymentIntent?.payment_details ? (
|
|
114
|
-
<TxLink details={data.paymentIntent.payment_details} method={data.paymentMethod} />
|
|
114
|
+
<TxLink details={data.paymentIntent.payment_details} method={data.paymentMethod} mode="customer" />
|
|
115
115
|
) : (
|
|
116
116
|
''
|
|
117
117
|
)
|